The rapid growth of the Internet of Things (IoT) has led to the development of innovative solutions to enhance device performance, making them more user-friendly and cost-effective. IoT devices like sensors, actuators, and gateways form ecosystems that address various problems, with Low-Power Wide-Area Network (LPWAN) technologies playing a crucial role.
Traditional wireless technologies are often inefficient for specific embedded tasks. The need for extensive coverage, low energy consumption, and affordable equipment has driven the adoption of LPWAN technologies such as LoRaWAN, SigFox, and NB-IoT. Among these, LoRaWAN has become particularly popular due to its low energy consumption, high communication capacity, and robustness in urban environments.
Geolocation of IoT devices poses a significant challenge, especially with the high energy demands of GPS modules. This project aims to develop a geolocation system using the Heltec WiFi LoRa32(V2)-F board, combining WiFi scanning and LoRaWAN communication to accurately locate IoT devices in urban settings. This approach offers a practical and energy-efficient solution for IoT geolocation.
The objective of this project is to develop an integrated geolocation system using the Heltec WiFi LoRa32(V2)-F board that combines WiFi scanning and LoRaWAN communication to accurately determine the position of IoT devices in urban environments. This system aims to:
Through this project, I aim to demonstrate the feasibility and practical application of combining WiFi and LoRaWAN technologies for geolocation purposes in IoT deployments within urban settings.
Heltec WiFi LoRa32(V2)-F
#include "LoRaWan_APP.h" #include <WiFi.h> #include <set>
The First library provides the functions and definitions needed to work with LoRaWAN, a protocol for low-power, wide-area networks. The second one is used for managing WiFi connections on the device and the last one provides the std::set container, which is used to store unique elements in an ordered manner.
/* OTAA para */ uint8_t devEui[] = { hidden }; uint8_t appEui[] = { hidden }; uint8_t appKey[] = { hidden }; /* LoraWan region, select in Arduino IDE tools */ LoRaMacRegion_t loraWanRegion = ACTIVE_REGION; //868 /* LoraWan Class, Class A and Class C are supported */ DeviceClass_t loraWanClass = CLASS_A;
These are the parameters for Over-The-Air Activation (OTAA) in LoRaWAN:
/* Prepares the payload of the frame */ static void prepareTxFrame(uint8_t port) { appDataSize = 0; // Reset the payload size int n = WiFi.scanNetworks(); Serial.println("scan done"); if (n == 0) { Serial.println("no networks found"); appData[appDataSize++] = 0x00; // Indicate no networks found } else { Serial.print(n); Serial.println(" networks found"); std::set<String> ssidSet; // To store unique SSIDs for (int i = 0; i < n && ssidSet.size() < 5; ++i) { String ssid = WiFi.SSID(i); // Add unique SSID to the set if (ssidSet.find(ssid) == ssidSet.end()) { ssidSet.insert(ssid); } } // Add SSIDs to the payload for (const auto& ssid : ssidSet) { uint8_t ssidLength = ssid.length(); // Ensure there is enough space in the buffer for the length byte and the SSID characters if (appDataSize + ssidLength + 1 > LORAWAN_APP_DATA_MAX_SIZE) { break; } appData[appDataSize++] = ssidLength; // Store SSID length for (int j = 0; j < ssidLength; ++j) { appData[appDataSize++] = ssid[j]; } // Log the SSID being added Serial.print("SSID: "); Serial.print(ssid); Serial.print(" Length: "); Serial.println(ssidLength); } } }
This function prepares the data to be sent over LoRaWAN:
void setup() { Serial.begin(115200); Mcu.begin(HELTEC_BOARD, SLOW_CLK_TPYE); WiFi.mode(WIFI_STA); WiFi.disconnect(); if (firstrun) { LoRaWAN.displayMcuInit(); firstrun = false; } }
The setup function initializes the device:
void loop() { switch (deviceState) { case DEVICE_STATE_INIT: { #if (LORAWAN_DEVEUI_AUTO) LoRaWAN.generateDeveuiByChipID(); #endif LoRaWAN.init(loraWanClass, loraWanRegion); // both set join DR and DR when ADR off LoRaWAN.setDefaultDR(3); break; } case DEVICE_STATE_JOIN: { LoRaWAN.displayJoining(); LoRaWAN.join(); break; } case DEVICE_STATE_SEND: { LoRaWAN.displaySending(); prepareTxFrame(appPort); LoRaWAN.send(); deviceState = DEVICE_STATE_CYCLE; break; } case DEVICE_STATE_CYCLE: { // Schedule next packet transmission txDutyCycleTime = appTxDutyCycle + randr(-APP_TX_DUTYCYCLE_RND, APP_TX_DUTYCYCLE_RND); LoRaWAN.cycle(txDutyCycleTime); deviceState = DEVICE_STATE_SLEEP; break; } case DEVICE_STATE_SLEEP: { LoRaWAN.displayAck(); LoRaWAN.sleep(loraWanClass); break; } default: { deviceState = DEVICE_STATE_INIT; break; } } }
The loop function handles the main state machine for the device:
A TTN (The Things Network) application was created, and the device was registered using Over-The-Air Activation (OTAA). The device is configured to operate on the European frequency plan (863-870 MHz) and adheres to LoRaWAN Specification 1.0.2 with Regional Parameters version RP001 1.0.2 revision B. The live data section shows successful uplink and downlink communication events, indicating that the device is correctly sending and receiving data through the TTN gateway.
// Decode an uplink message from a buffer // port: the port number // bytes: the payload as a byte array function decodeUplink(input) { var bytes = input.bytes; var port = input.fPort; var ssids = []; var i = 0; while (i < bytes.length) { var ssidLength = bytes[i]; i += 1; if (ssidLength === 0 || i + ssidLength > bytes.length) { break; } var ssid = ""; for (var j = 0; j < ssidLength; j++) { ssid += String.fromCharCode(bytes[i + j]); } ssids.push(ssid); i += ssidLength; } // Return the decoded values in an object return { data: { ssids: ssids } }; }
The purpose of this TTN (The Things Network) decoder function is to interpret the payload received from the device. This payload contains WiFi SSIDs detected by the device. Here’s a breakdown of what the function does:
Inputs: The function takes an input object containing two properties:
It initializes an empty array ssids to store the SSIDs and a variable i to keep track of the current position in the byte array.
The function enters a while loop that iterates through the bytes array:
The webhook configuration is designed to bridge The Things Network (TTN) application with Google Sheets. The webhook, identified as “iot-sric-project-webhook,” is set to format data in JSON and directs uplink messages to a specified Google Script URL. This setup ensures that uplink messages received by the TTN application are forwarded to Google Sheets for logging and further analysis. This integration facilitates seamless data transfer from IoT devices to a user-friendly and accessible data management platform.
The data received in Google Sheets includes the following fields: Device ID, Application ID, Device EUI, Device Address, Received At, Port, Undecoded Payload, SSIDs, Gateway ID, EUI, RSSI, SNR, Latitude, Longitude, and Altitude of the gateway.
var SHEET_NAME = "Sheet1"; var SCRIPT_PROP = PropertiesService.getScriptProperties(); // new property service function doGet(e) { return handleResponse(e); } function doPost(e) { return handleResponse(e); } function handleResponse(e) { var sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); var data = JSON.parse(e.postData.contents); if (sheet.getLastRow() === 0) { sheet.appendRow([ 'Device ID', 'Application ID', 'Device EUI', 'Device Address', 'Received At', 'Port', 'Payload', 'SSIDs', 'Gateway ID', 'EUI', 'RSSI', 'SNR', 'Latitude', 'Longitude', 'Altitude' ]); } var rowData = []; rowData.push(data.end_device_ids.device_id); rowData.push(data.end_device_ids.application_ids.application_id); rowData.push(data.end_device_ids.dev_eui); rowData.push(data.end_device_ids.dev_addr); rowData.push(data.received_at); rowData.push(data.uplink_message.f_port); rowData.push(data.uplink_message.frm_payload); // Push SSIDs into a single cell var decodedPayload = data.uplink_message.decoded_payload; var ssidList = decodedPayload.ssids.join(", "); // Check if SSID list already exists for the same device and timestamp var lastRow = sheet.getLastRow(); var existingSSID = false; for (var i = 1; i <= lastRow; i++) { var deviceId = sheet.getRange(i, 1).getValue(); var receivedAt = sheet.getRange(i, 5).getValue(); var ssids = sheet.getRange(i, 8).getValue(); if (deviceId === data.end_device_ids.device_id && receivedAt === data.received_at && ssids === ssidList) { existingSSID = true; break; } } if (!existingSSID) { rowData.push(ssidList); // Append the SSID data once sheet.appendRow(rowData); } // Append the gateway data for each gateway for (var i = 0; i < data.uplink_message.rx_metadata.length; i++) { var gateway = data.uplink_message.rx_metadata[i]; var gatewayRowData = rowData.slice(0, 7); // Make a copy of the base rowData without the SSID list gatewayRowData.push(''); // Empty cell for SSIDs gatewayRowData.push(gateway.gateway_ids.gateway_id); gatewayRowData.push(gateway.gateway_ids.eui); gatewayRowData.push(gateway.rssi); gatewayRowData.push(gateway.snr); gatewayRowData.push(gateway.location.latitude); gatewayRowData.push(gateway.location.longitude); gatewayRowData.push(gateway.location.altitude); sheet.appendRow(gatewayRowData); // Write the new row of data } return ContentService.createTextOutput(JSON.stringify({ "status": "success" })).setMimeType(ContentService.MimeType.JSON); } function setup() { var doc = SpreadsheetApp.getActiveSpreadsheet(); SCRIPT_PROP.setProperty("key", doc.getId()); }
This Google Sheets script is designed to handle HTTP GET and POST requests to log and process data from LoRaWAN devices into a Google Sheets spreadsheet.
Script Properties and Initialization:
HTTP Request Handling:
Response Handling:
Sheet Initialization:
Data Logging:
Gateway Metadata:
Response Generation:
The primary purpose of this script is to collect and organize data from LoRaWAN devices into a structured format within a Google Sheets spreadsheet. It handles both the data from the devices themselves (including SSIDs detected by the devices) and metadata from the gateways that received the uplink messages, ensuring all relevant information is logged efficiently.
%% Calculate geoid coordinates of the esp32 board %% fixed constants % attenuation of signal - can be modified, depending on the local area n = 3.6667; % RSSI of the unit distance (1 meter) - need to be measured in advanced A = -29; %% Reference ellipsoid for World Geodetic System 1984 wgs84 = wgs84Ellipsoid('meter'); %% First end-device coordinates p(1).lat = p(1).long = p(1).altitude = rssi1 = % convert the coordinates from World Geodetic System to Earth-centered, Earth-fixed coordinate system [p(1).x,p(1).y,p(1).z] = geodetic2ecef(wgs84,p(1).lat,p(1).long,p(1).altitude); %calculate the distance between the first end-node and the gateway based on RSSI dist1 = 10.^((A-rssi1)/(10*n)); %% Second end-device coordinates p(2).lat = p(2).long = p(2).altitude = rssi2 = % convert the coordinates from World Geodetic System to Earth-centered, Earth-fixed coordinate system [p(2).x,p(2).y,p(2).z] = geodetic2ecef(wgs84,p(2).lat,p(2).long,p(2).altitude); % calculate the distance between second end-device and the gateway based on the RSSI dist2 = 10.^((A-rssi2)/(10*n)); %% Third end-device coordinates p(3).lat = p(3).long = p(3).altitude = rssi3 = % convert the coordinates from World Geodetic System to Earth-centered, Earth-fixed coordinate system [p(3).x,p(3).y,p(3).z] = geodetic2ecef(wgs84,p(3).lat,p(3).long,p(3).altitude); % calculate the distance between the second end-device and the gateway based on the RSSI dist3 = 10.^((A-rssi3)/(10*n)); %% Fourth end-device coordinates p(4).lat = p(4).long = p(4).altitude = rssi4 = - % convert the coordinates from World Geodetic System to Earth-centered, Earth-fixed coordinate system [p(4).x,p(4).y,p(4).z] = geodetic2ecef(wgs84,p(4).lat,p(4).long,p(4).altitude); % calculate the distance between the fourth end-device and the gateway based on the RSSI dist4 = 10.^((A-rssi4)/(10*n)); %% Finding gateway coordinates % calculate the initial Guess for GPS equations x0 = (p(1).x + p(2).x + p(3).x + p(4).x) / 4; y0 = (p(1).y + p(2).y + p(3).y + p(4).y) / 4; z0 = (p(1).z + p(2).z + p(3).z + p(4).z) / 4; Guess = [x0,y0,z0]; % matrix form - the coordinates of the four end-devices and distances btw them and gw A = [p(1).x , p(2).x , p(3).x, p(4).x]; B = [p(1).y , p(2).y , p(3).y, p(4).y]; C = [p(1).z , p(2).z , p(3).z, p(4).z]; D = [dist1 , dist2 , dist3, dist4]; % vector A length LA = length(A); % solving GPS equations % Result vector contains the ECEF coordinates of the gateway [Result,~,~] = MVNewtonsInputs(Guess,A,B,C,D,LA); % Convert the gateway coordinates from Earth-centered, Earth-fixed % coordinate system to World Geodetic System 1984 [esp_lat,esp_lon,esp_alt] = ecef2geodetic(wgs84,Result(1),Result(2),Result(3))
The main MATLAB script calculates the geoid coordinates of an ESP32 board using RSSI values from four end-devices. It first defines constants for signal attenuation (`n`) and the RSSI at a unit distance (`A`). It uses the World Geodetic System 1984 (`wgs84`) as the reference ellipsoid.
For each end-device, the script assigns latitude, longitude, altitude, and RSSI values, then converts these geodetic coordinates to Earth-centered, Earth-fixed (ECEF) coordinates. Using the RSSI values, it calculates the distance between each end-device and the gateway.
An initial guess for the gateway’s coordinates is determined by averaging the ECEF coordinates of the four end-devices. The coordinates and distances are then arranged in matrix form, and the `MVNewtonsInputs` function is used to solve for the gateway’s ECEF coordinates. Finally, these ECEF coordinates are converted back to geodetic coordinates (latitude, longitude, altitude) using the `ecef2geodetic` function, giving the geoid coordinates of the ESP32 board.
The table and map show the data collected from three gateways: mylps8, multitech-00009a75, and ttnd35. Only these three gateways were able to be received at the same time and from the same location. Although four gateways are typically needed for more accurate triangulation, it's very unlikely to receive signals from that many due to the urban density in Bucharest, so an estimate for a fourth was made.
The results are satisfactory, especially considering the significant signal disruption in an urban environment. The estimated error margin is around 200-300 meters. The gateway coordinates were manually entered by the user, which often results in unrealistic altitude values. Therefore, the altitude in the result does not accurately reflect reality.
Additionally, the LoRa packets sent by the ESP32 board also contain surrounding SSIDs identified by the WiFi module. These SSIDs are stored in Google Sheets, but due to privacy concerns, they have not been included in the displayed figure.
Hardware and Software Integration:
Data Collection and Decoding:
WiFi Scanning:
Geolocation Using RSSI:
Accuracy and Challenges:
Practical Implications:
Future Improvements:
Overall, the integration of hardware, software, and geolocation techniques proves to be a viable approach for IoT applications, with room for enhancements in gateway density, data accuracy, and the use of WiFi scans to provide additional context and data points for the geolocation process.