There are many health benefits of owning a pet. They can increase opportunities to exercise, get outside, and socialize. Pets can help manage loneliness and depression by giving us companionship. Giving these considerations, losing a pet is, for the owner, the worst thing that could ever happen to him.
Pet tracking is an essential aspect of pet ownership, and it can be challenging to keep an eye on pets at all times. This project aims to develop a pet tracking system that utilizes an ESP32 Heltec LoRa v2 and GPS module to track pets' movements. The system allows pet owners to monitor their pets' location, view their pets' movement history, and receive alerts when their pets leave a designated area.
Geofencing is a powerful technique used in location-based applications to trigger certain actions when a device enters or exits a defined geographic area. In this tutorial, we will explore how to build a low-power geofencing application using RIOT OS, an open-source operating system for the Internet of Things (IoT), and the IOT-LAB platform for IoT device testing. By combining these technologies, we can develop an efficient and reliable geofencing solution for various IoT applications.
Basic knowledge of IoT concepts and programming
Familiarity with RIOT OS and the IOT-LAB platform
Access to IOT-LAB for device testing and experimentation
Hardware and Software Requirements:
A based IoT device (e.g., B-L072Z-LRWAN1). LoRa code is for B-072Z-LRWAN1, while the gps main code is for the HELTEC ESP32 LoRa32 WiFi v2.
IOT-LAB account (signup at https://www.iot-lab.info/)
RIOT OS development environment (installation instructions: https://github.com/RIOT-OS/RIOT)
AWS account (https://console.aws.amazon.com/console/home?nc2=h_ct&src=header-signin)
FeaturesThe Pet Tracking System has the following features:
- Location Tracking: The system utilizes GPS technology to track pets' location.
- Geofencing: Pet owners can set up a designated area for their pets, and the system will send an alert when the pet leaves the designated area.
- Movement History: If the pet leaves the designated area, the system keeps track of pets' movements and displays the data in a graphical format for pet owners to review.
- Notifications: Pet owners receive en email when their pets leave the designated area that incities the owner to access the site.
The circuit is the following:
And here we have the prototype:
As we can see, the connections are identical to the fritzing figure.
Network DiagramMore in detail, we have:
The main IOT elements are, LoRaWAN as communication protocol, The Things Network (TTn), IOT-Lab, AWS.
LoRaWAN
The LoRaWAN protocol is a Low Power Wide Area Networking (LPWAN) communication protocol that functions on LoRa. The LoRaWAN specification is open so anyone can set up and operate a LoRa network. LoRa is a wireless audio frequency technology that operates in a license-free radio frequency spectrum.
The Things Network
The Things network is a global collaborative IOT ecosystem that creates networks devices and solutions using LoraWan. It provides a set of open tools and a global open network to build IOT applications at low-cost featuring maximum security and ready to scale. What we used is The Things stack that is the critical component for any lorawan solution.
Fit IOT-Lab
Fit IOT-Lab is an open testbed in which you can:
- Build your application without any dependency requirement. Develop from scratch, or based on open-source libraries, or even with an OS.
- Deploy and run your experiments with our webportal, automate with the CLI tools or make direct calls to our open API.
UART PIN CONFIGURATION ON ESP32 HELTEC LORA32 WIFI (V2)
In order to let the E108-GN02D GPS module work with ESP32 Heltec Lora V2 in Riot OS, we should edit the default pins of UART configuration:
Open the file `RIOT/boards/esp32-heltec-lora32-v2/include/periph_conf.h`
Edit the lines:
#ifndef UART1_TXD
#define UART1_TXD GPIO10 /**< direct I/O pin for UART_DEV(1) TxD */
#endif
#ifndef UART1_RXD
#define UART1_RXD GPIO9 /**< direct I/O pin for UART_DEV(1) RxD */
To make them like this:
#ifndef UART1_TXD
#define UART1_TXD GPIO17 /**< direct I/O pin for UART_DEV(1) TxD */
#endif
#ifndef UART1_RXD
#define UART1_RXD GPIO16 /**< direct I/O pin for UART_DEV(1) RxD */
Then connect the TXD of the GPS to PIN 16 and the RXD of the GPS to PIN 17
MINMEA
To read GPS values we need a library, we used minmea that is a minimalistic GPS parser library mantained by "Kosma Moczek" and you can find if from that link github.
GPS Configuration
#define GPS_UART_DEV UART_DEV(1)
#define GPS_BAUDRATE 9600
#define GPS_CE_PIN GPIO2
static void gps_rx_cb(void *arg, uint8_t data)
{
(void)arg;
if (data != '\n')
{
nmea_buffer[i++] = data;
}
else
{
nmea_buffer[i] = '\0';
switch (minmea_sentence_id(nmea_buffer, false))
{
case MINMEA_SENTENCE_GGA:
{
if (minmea_parse_gga(&frame, nmea_buffer))
{
latitudeStruct = frame.latitude;
longitudeStruct = frame.longitude;
satellitesNum = frame.satellites_tracked;
}
}
break;
default:
break;
}
i = 0;
}
}
uart_init(GPS_UART_DEV, GPS_BAUDRATE, gps_rx_cb, NULL);
gpio_init(GPS_CE_PIN, GPIO_OUT);
These lines of code are initializing the UART (Universal Asynchronous Receiver-Transmitter) and GPIO (General Purpose Input/Output) modules for a GPS device. Let's break down each line:
The "uart_init" function initializes the UART module for communication with the GPS device. The function uart_init
is called with the following parameters:
GPS_UART_DEV
: This is the identifier for the specific UART device being used for GPS communication. It could be a constant or a variable representing the UART device number or name.GPS_BAUDRATE
: This specifies the baud rate at which the GPS device and the microcontroller (where this code is running) will communicate. Baud rate determines the rate of data transmission.gps_rx_cb
: This is a callback function that will be called when data is received on the UART. It's likely that this function is responsible for processing and handling the received GPS data.NULL
: This parameter represents any additional configuration options or settings that are not required in this particular case. PassingNULL
indicates that no extra configuration is provided.
The "gpio_init" initializes a GPIO pin that is used to control the GPS device. The function gpio_init
is called with the following parameters:
GPS_CE_PIN
: This is the identifier or number of the GPIO pin being initialized. It could be a constant or a variable representing the specific pin used to control the GPS device.GPIO_OUT
: This specifies the mode or direction of the GPIO pin, which is set to output mode. This means that the microcontroller will be able to send signals or control the state (high or low) of this pin.
An important requirement is the confidentiality and so the encryption of the latitude and longitude value sent in the messages. Of course every owner of a purebred animal knows that there are thieves of these specimens to speculate, so make illegible the value of the GPS become crucial.
In order to prevent this, we used the AES algorithm and the implementation is the following:
void bytes_to_hex_string(const uint8_t *bytes, size_t size, char *hex_string)
{
size_t index = 0;
for (size_t i = 0; i < size; i++)
{
// Format the byte as a two-digit hexadecimal string
sprintf(&hex_string[index], "%02x", bytes[i] & 0xff);
// Move the index two positions forward
index += 2;
}
hex_string[index] = '\0';
}
// Encrypt msg using AES-128
uint8_t *aes_128_encrypt(const char *msg)
{
printf("Unencrypted: %s\n", msg);
size_t msg_len = strlen(msg);
size_t padding_len = 16 - (msg_len % 16);
size_t padded_len = msg_len + padding_len;
uint8_t padded_msg[padded_len];
memcpy(padded_msg, msg, msg_len);
memset(padded_msg + msg_len, 0, padding_len);
aes_init(&cyctx, key, AES_KEY_SIZE_128);
print_bytes(padded_msg, padded_len);
uint8_t *encrypted_msg = malloc(padded_len);
if (encrypted_msg == NULL)
{
printf("Failed to allocate memory\n");
return NULL;
}
for (int i = 0; i < (int)padded_len; i += 16)
{
aes_encrypt(&cyctx, padded_msg + i, encrypted_msg + i);
}
printf("Encrypted:\n");
print_bytes(encrypted_msg, padded_len);
// Convert encrypted_msg to hex string
char *hex_string = malloc((padded_len * 2) + 1);
if (hex_string == NULL)
{
printf("Failed to allocate memory\n");
return NULL;
}
bytes_to_hex_string(encrypted_msg, padded_len, hex_string);
return (uint8_t *)hex_string;
}
The AlgorithmThis is the main function:
int main(void)
{
// Seed the random number generator
printf("PaaT: a LoRaWAN Class A low-power application\n");
printf("=====================================\n");
/*
* Enable deep sleep power mode (e.g. STOP mode on STM32) which
* in general provides RAM retention after wake-up.
*/
#if IS_USED(MODULE_PM_LAYERED)
for (unsigned i = 1; i < PM_NUM_MODES - 1; ++i) {
pm_unblock(i);
}
#endif
#ifdef USE_OTAA /* OTAA activation mode */
/* Convert identifiers and keys strings to byte arrays */
fmt_hex_bytes(deveui, CONFIG_LORAMAC_DEV_EUI_DEFAULT);
fmt_hex_bytes(appeui, CONFIG_LORAMAC_APP_EUI_DEFAULT);
fmt_hex_bytes(appkey, CONFIG_LORAMAC_APP_KEY_DEFAULT);
semtech_loramac_set_deveui(&loramac, deveui);
semtech_loramac_set_appeui(&loramac, appeui);
semtech_loramac_set_appkey(&loramac, appkey);
/* Use a fast datarate, e.g. BW125/SF7 in EU868 */
semtech_loramac_set_dr(&loramac, LORAMAC_DR_5);
/* Join the network if not already joined */
if (!semtech_loramac_is_mac_joined(&loramac)) {
/* Start the Over-The-Air Activation (OTAA) procedure to retrieve the
* generated device address and to get the network and application session
* keys.
*/
if (!joinLoRaNetwork()) {
printf("Failed to join the network\n");
return 1;
}
#ifdef MODULE_PERIPH_EEPROM
/* Save current MAC state to EEPROM */
semtech_loramac_save_config(&loramac);
#endif
}
#endif
/* start the sender thread */
recv_pid = thread_create(_recv_stack, sizeof(_recv_stack),
THREAD_PRIORITY_MAIN - 1, 0, _recv, NULL, "recv thread");
xtimer_sleep(5);
char* geo_request = (char*)malloc(strlen("geofence")+1);
if (geo_request == NULL)
{
printf("Failed to allocate memory for message\n");
return 1;
}
// Construct the JSON message
snprintf(geo_request, strlen("geofence")+1, "geofence");
printf("Sending: %s\n", geo_request);
uint8_t req = semtech_loramac_send(&loramac, (uint8_t*)geo_request, strlen(geo_request));
if (req != SEMTECH_LORAMAC_TX_DONE)
{
printf("Cannot send message '%s', ret code: %d\n", geo_request, req);
}
// Initialize the LED pin as an output
gpio_init(LED1_PIN_NUM, GPIO_OUT);
// Turn on LED1 by setting the GPIO pin to high
gpio_set(LED1_PIN_NUM);
free(geo_request);
xtimer_sleep(2);
thread_wakeup(recv_pid);
int count = 0;
// Parse the string and extract the geofence
char* token = strtok(msg_received, ",");
while (token != NULL && count < 4)
{
geofence[count] = (char*)malloc(strlen(token) + 1); // Allocate memory for the geofence value
strcpy(geofence[count], token); // Copy the geofence value
printf("geofence[%d] = %s\n", count, geofence[count]);
token = strtok(NULL, ",");
count++;
}
free(msg_received);
thread_kill_zombie(recv_pid);
gpio_clear(LED1_PIN_NUM);
char* latitude;
char* longitude;
count = 1;
xtimer_sleep(20);
while (1) {
if (count >=3 && count < 6){
latitude = "51.896800742599";
longitude = "12.489154256234";
}
else{
latitude = "41.896800742599";
longitude = "12.489154256234";
}
// Wait for reliable position or timeout
satellitesNum=4;
printf("Latitude : %s, Longitude : %s\n", latitude, longitude);
//if (satellitesNum >= 4)
{
geofenceViolated = isInGeofence(latitude, longitude);
}
if (geofenceViolated)
{
geofence_tmp = true;
lightOn = true;
soundOn = true;
gpio_set(LED1_PIN_NUM);
// encrypt msg with AES
uint8_t* lat_encrypted = (uint8_t *)aes_128_encrypt(latitude);
printf("Encrypted lat: %s\n", lat_encrypted);
uint8_t* lon_encrypted = (uint8_t *)aes_128_encrypt(longitude);
printf("Encrypted lon: %s\n", lon_encrypted);
char json[500];
sprintf(json, "{\"lat\": \"%s\", \"lng\": \"%s\", \"s\": \"%d\"}",
lat_encrypted, lon_encrypted, satellitesNum);
msg_to_be_sent = (uint8_t *)json;
printf("Sending: %s\n", msg_to_be_sent);
uint8_t ret = semtech_loramac_send(&loramac, (uint8_t*)msg_to_be_sent, strlen(json));
if (ret != SEMTECH_LORAMAC_TX_DONE)
{
printf("Cannot send message '%s', ret code: %d\n", msg_to_be_sent, ret);
free(msg_to_be_sent);
}
free(lat_encrypted);
free(lon_encrypted);
xtimer_sleep(300);
}
else
{
if (lightOn) gpio_clear(LED1_PIN_NUM);
soundOn = false;
lightOn = false;
if (geofence_tmp){
// encrypt msg with AES
uint8_t* lat_encrypted = (uint8_t *)aes_128_encrypt(latitude);
printf("Encrypted lat: %s\n", lat_encrypted);
uint8_t* lon_encrypted = (uint8_t *)aes_128_encrypt(longitude);
printf("Encrypted lon: %s\n", lon_encrypted);
char json[500];
sprintf(json, "{\"lat\": \"%s\", \"lng\": \"%s\", \"s\": \"%d\"}",
lat_encrypted, lon_encrypted, satellitesNum);
msg_to_be_sent = (uint8_t *)json;
printf("Sending: %s\n", msg_to_be_sent);
uint8_t ret = semtech_loramac_send(&loramac, (uint8_t*)msg_to_be_sent, strlen(json));
if (ret != SEMTECH_LORAMAC_TX_DONE)
{
printf("Cannot send message '%s', ret code: %d\n", msg_to_be_sent, ret);
free(msg_to_be_sent);
}
free(lat_encrypted);
free(lon_encrypted);
geofence_tmp = false;
}
printf("Geofence not violated\n");
xtimer_sleep(300);
}
count++;
}
}
Let's go through the code and understand its main components:
1.Joining the LoRaWAN Network:
The application is using Over-The-Air Activation (OTAA), so it converts identifiers and keys to byte arrays.
The code sets the LoRaWAN data rate and checks if the device has already joined the network. If not, it attempts to join the network using the joinLoRaNetwork() function, a function defined since sometimes the join is not successful, which the purpose is to retry if the join fails.
If the network join is successful, the configuration is optionally saved to EEPROM.
2.Sending Geofence Request:
The code allocates memory for a geofence request message and constructs the JSON message using the "geofence" keyword.
The message is then sent using the semtech_loramac_send() function.
3.Receiving Geofence Data:
After a delay of 2 seconds, the code wakes up a receiver thread (_recv) to receive geofence data.
The received message is parsed and the geofence values are extracted and stored in the geofence array.
4.Geofence Violation Checking:
The code enters an infinite loop where it waits for a reliable position or a timeout.
In this example, latitude and longitude values are hardcoded for demonstration purposes.
The code checks if the number of satellites is greater than or equal to 4, and if so, determines if the current position is within the geofence using the isInGeofence() function. The code is simulating a scenario in which when the board is initialized, the pet is inside the geofence, while after 10 minutes it exit.
4.1 If the geofence is violated, is turned on an LED, is encrypted and sent the latitude and longitude coordinates using AES encryption, and is performed a sleep for 300 seconds.
4.2 If the geofence is not violated it prints a message, turns off the LED, sets soundOn and lightOn flags to false, and sleeps for 300 seconds. Of course this because there is no need to know the actual position of the pet since it is inside the geofence. If the geofence is not violated for the first time after that it was outside the geofence, it will send the actual position.
EvaluationLoRaWAN
To evaluate the product, and more specifically the consumption of the radio, we exploite Fit IOT-Lab explained above. Thanks to it we could obtain a graph of the consumption of the entire device when it sends and receives the messages, and the graphs are the follows:
As we can see, the device present a major consumption when it tries to send a Lora message and when it receive the response.
Clearly, we organaized the code in order that the device stay awake only in the moment that it need to send messages. Let going to see more in detail:
From that graph we can calculate the consumption in a sample way, in detail below there are the consumption of all the elements and the entire consumption of the all device.
In order to permit to the system to work properly we suggest to use a battery like this, with 4.8V and 2, 400 mAh.
We can estimate that the device, when the pet is always inside the geofence, guarantees an autonomy of 2 days.
On the other hand, when the pet is always outside the geofence, it guarantees an autonomy of 1 day.
In Real World, a digital multimeter has been used in order to check the real values of current of each sensor and actuator, discovering that the values are nearly close to the declared ones (check in the table above).
The GPS sensor whenever has found the coordinates switches to tracking mode, which consumes only 20mA instead of 35mA, but it is not in our interest because once the position is discovered the board puts every sensor in sleep mode until the following wake up. Moreover, in the table all the totals are based on the maximum time required to perform position discovery (100 seconds), but in real world scenario has been proved that in open field the expected time is on average less than 30 seconds.
These values are TOO high. The problem is that RIOT doesn't provide no library or function to put the ESP32 in deep sleep. This is a problem, but, since the Heltec Automation provides an Arduino code (take a look at this code) in which some tests (look here) found out that the deep sleep current should close to 900uA, by adding a deep sleep algorithm between the active sessions of the board, we will improve the life time of the board.
In fact, we studied also the analysis done with this variation:
With our assumption, we can increase the autonomy of the prototype by making it during 35.5 days inside the geofence or 1.4 days outside the geofence.
User InterfaceIn order to interface with the location collected by the device, a web app has been made that allows map viewing, and our animal is identified by the footprint image.
Through the web app, the user is able to perform several tasks:
- monitor the position of the pet on the map and check when last position was sent,
- open the recent positions history, with the time the last 10 positions,
- edit the geofence borders.
Comments