Farwa Zafar
Published

ESP32-S3 LoRa Environment & GPS Tracker with ThingsBoard MQT

A dual-node ESP32-S3 system that transmits environmental sensor data and live GPS telemetry over LoRa, displaying local metrics on an OLED a

BeginnerFull instructions provided2 hours25
ESP32-S3 LoRa Environment & GPS Tracker with ThingsBoard MQT

Things used in this project

Hardware components

2x ESP32-S3 Dev Boards
×1
2x Reyax RYLR998 LoRa Modules
×1
1x Adafruit SSD1306 OLED Display (128x64 I2C)
×1
1x DHT11 / DHT22 Sensor
×1
1x GPS Module
×1
1x Push Button
×1
1x LED + 220Ω Resistor
×1
Breadboards and Jumper Wires
×1

Story

Read more

Schematics

Schemetic

Code

Node 2: Display & Cloud Gateway (main.cpp)

C/C++
I am using thingsboard thats why i have used some other things
#include <Arduino.h>
#include <Wire.h>
#include <WiFi.h>
#include <PubSubClient.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// --- Network & ThingsBoard Credentials ---
const char* WIFI_SSID       = "YOUR_WIFI_SSID";         
const char* WIFI_PASS       = "YOUR_WIFI_PASSWORD";     
const char* TB_SERVER       = "your thingsboard server";
const int   TB_PORT         = 1883;
const char* TB_ACCESS_TOKEN = "YOUR_THINGSBOARD_TOKEN";

// --- Hardware Pins ---
#define SCREEN_WIDTH  128
#define SCREEN_HEIGHT 64
#define OLED_RESET    -1
#define SCREEN_ADDRESS 0x3C

#define OLED_SDA_PIN  8
#define OLED_SCL_PIN  9
#define BUTTON_PIN    6

#define LORA_RX_PIN   18 
#define LORA_TX_PIN   17 

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
WiFiClient espClient;
PubSubClient mqttClient(espClient);

bool lastBtnState = HIGH;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50;

// Telemetry Variables
String tempVal = "--.-";
String humVal  = "--.-";
String latVal  = "0.000000";
String lngVal  = "0.000000";
String satsVal = "0";
String fixVal  = "SEARCHING";

void sendLoraLedToggle() {
    String msg = "LED_TOGGLE";
    String cmd = "AT+SEND=1," + String(msg.length()) + "," + msg;
    Serial2.print(cmd + "\r\n");
    Serial.println(">>> Transmitted LED_TOGGLE to Node 1 via LoRa!");
}

// MQTT RPC Callback - Handles Commands from ThingsBoard
void mqttCallback(char* topic, byte* payload, unsigned int length) {
    String message;
    for (unsigned int i = 0; i < length; i++) {
        message += (char)payload[i];
    }
    Serial.print("RPC Received on [");
    Serial.print(topic);
    Serial.print("]: ");
    Serial.println(message);

    // Trigger LED Toggle
    if (message.indexOf("toggleLed") != -1 || message.indexOf("setValue") != -1) {
        sendLoraLedToggle();
    }

    // Send RPC Response Back to Cloud
    String topicStr = String(topic);
    int requestIdIndex = topicStr.lastIndexOf('/');
    if (requestIdIndex != -1) {
        String requestId = topicStr.substring(requestIdIndex + 1);
        String responseTopic = "v1/devices/me/rpc/response/" + requestId;
        String responsePayload = "{\"status\":\"success\"}";
        mqttClient.publish(responseTopic.c_str(), responsePayload.c_str());
        Serial.println(">>> Replied to RPC Request ID: " + requestId);
    }
}

void connectWiFi() {
    Serial.print("Connecting to Wi-Fi");
    WiFi.begin(WIFI_SSID, WIFI_PASS);
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println("\nWi-Fi Connected! IP: " + WiFi.localIP().toString());
}

void connectMQTT() {
    while (!mqttClient.connected()) {
        Serial.print("Connecting to ThingsBoard MQTT...");
        if (mqttClient.connect("ESP32S3_Gateway", TB_ACCESS_TOKEN, NULL)) {
            Serial.println(" Connected!");
            mqttClient.subscribe("v1/devices/me/rpc/request/+");
        } else {
            Serial.print(" Failed, rc=");
            Serial.print(mqttClient.state());
            Serial.println(" Retrying in 3 seconds...");
            delay(3000);
        }
    }
}

void sendToThingsBoard() {
    if (!mqttClient.connected()) {
        connectMQTT();
    }

    String jsonPayload = "{";
    jsonPayload += "\"temperature\":" + tempVal + ",";
    jsonPayload += "\"humidity\":" + humVal + ",";
    jsonPayload += "\"latitude\":" + latVal + ",";
    jsonPayload += "\"longitude\":" + lngVal + ",";
    jsonPayload += "\"satellites\":" + satsVal + ",";
    jsonPayload += "\"gps_fix\":\"" + fixVal + "\"";
    jsonPayload += "}";

    mqttClient.publish("v1/devices/me/telemetry", jsonPayload.c_str());
    Serial.print(">>> Published to ThingsBoard: ");
    Serial.println(jsonPayload);
}

void updateDisplay() {
    display.clearDisplay();
    display.setTextColor(SSD1306_WHITE);
    
    display.setTextSize(1);
    display.setCursor(12, 0);
    display.print("LORA TRACKER NODE");
    display.drawFastHLine(0, 10, 128, SSD1306_WHITE);

    display.setCursor(0, 15);
    display.print("T: "); display.print(tempVal);
    display.print("C  H: "); display.print(humVal); display.print("%");

    display.drawFastHLine(0, 26, 128, SSD1306_WHITE);

    display.setCursor(0, 31);
    display.print("Lat: "); display.print(latVal);

    display.setCursor(0, 42);
    display.print("Lng: "); display.print(lngVal);

    display.setCursor(0, 54);
    display.print("Sats: "); display.print(satsVal);
    display.print(" | "); display.print(fixVal);

    display.display();
}

void setup() {
    Serial.begin(115200);
    delay(1500);

    Wire.begin(OLED_SDA_PIN, OLED_SCL_PIN);
    if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
        Serial.println("OLED allocation failed!");
    }

    pinMode(BUTTON_PIN, INPUT_PULLUP);
    pinMode(LORA_RX_PIN, INPUT_PULLUP);
    Serial2.begin(115200, SERIAL_8N1, LORA_RX_PIN, LORA_TX_PIN);

    Serial2.print("AT+RESET\r\n"); delay(500);
    Serial2.print("AT+ADDRESS=2\r\n"); delay(100);
    Serial2.print("AT+NETWORKID=6\r\n"); delay(100);
    Serial2.print("AT+PARAMETER=9,7,1,4\r\n"); delay(100);

    connectWiFi();
    mqttClient.setServer(TB_SERVER, TB_PORT);
    mqttClient.setCallback(mqttCallback);
    connectMQTT();

    updateDisplay();
    Serial.println("--- Display Gateway Ready ---\n");
}

void loop() {
    if (WiFi.status() != WL_CONNECTED) {
        connectWiFi();
    }
    if (!mqttClient.connected()) {
        connectMQTT();
    }
    mqttClient.loop();

    // 1. Process Incoming LoRa Telemetry
    if (Serial2.available()) {
        String rxData = Serial2.readStringUntil('\n');
        rxData.trim();
        
        if (rxData.startsWith("+RCV=")) {
            int c1 = rxData.indexOf(',');
            int c2 = rxData.indexOf(',', c1 + 1);
            int cLast = rxData.lastIndexOf(',');
            int cPrev = rxData.lastIndexOf(',', cLast - 1);

            if (c2 != -1 && cPrev != -1 && c2 < cPrev) {
                String payload = rxData.substring(c2 + 1, cPrev);
                payload.trim();

                int pIdx[5];
                pIdx[0] = payload.indexOf(',');
                pIdx[1] = payload.indexOf(',', pIdx[0] + 1);
                pIdx[2] = payload.indexOf(',', pIdx[1] + 1);
                pIdx[3] = payload.indexOf(',', pIdx[2] + 1);
                pIdx[4] = payload.indexOf(',', pIdx[3] + 1);

                if (pIdx[4] != -1) {
                    tempVal = payload.substring(0, pIdx[0]);
                    humVal  = payload.substring(pIdx[0] + 1, pIdx[1]);
                    latVal  = payload.substring(pIdx[1] + 1, pIdx[2]);
                    lngVal  = payload.substring(pIdx[2] + 1, pIdx[3]);
                    satsVal = payload.substring(pIdx[3] + 1, pIdx[4]);
                    fixVal  = payload.substring(pIdx[4] + 1);

                    updateDisplay();
                    sendToThingsBoard();
                }
            }
        }
    }

    // 2. Hardware Button Handler
    bool reading = digitalRead(BUTTON_PIN);
    if (reading != lastBtnState) {
        lastDebounceTime = millis();
    }

    if ((millis() - lastDebounceTime) > debounceDelay) {
        static bool btnState = HIGH;
        if (reading != btnState) {
            btnState = reading;
            if (btnState == LOW) {
                Serial.println(">>> Physical Button Pressed!");
                sendLoraLedToggle();
            }
        }
    }
    lastBtnState = reading;
}

Node 1: Sensor & GPS Transmitter (main.cpp)

C/C++
#include <Arduino.h>
#include <DHT.h>
#include <TinyGPS++.h>

#define DHTPIN        4
#define DHTTYPE       DHT11
#define LED_PIN       5

#define LORA_RX_PIN   18
#define LORA_TX_PIN   17

#define GPS_RX_PIN    16
#define GPS_TX_PIN    15

DHT dht(DHTPIN, DHTTYPE);
TinyGPSPlus gps;
HardwareSerial gpsSerial(1);

unsigned long lastSendTime = 0;
const unsigned long sendInterval = 5000;
bool ledState = LOW;

void setup() {
    Serial.begin(115200);
    delay(1000);

    pinMode(LED_PIN, OUTPUT);
    digitalWrite(LED_PIN, ledState);

    dht.begin();

    // LoRa Serial Setup
    Serial2.begin(115200, SERIAL_8N1, LORA_RX_PIN, LORA_TX_PIN);
    delay(500);
    Serial2.print("AT+RESET\r\n"); delay(500);
    Serial2.print("AT+ADDRESS=1\r\n"); delay(100);
    Serial2.print("AT+NETWORKID=6\r\n"); delay(100);
    Serial2.print("AT+PARAMETER=9,7,1,4\r\n"); delay(100);

    // GPS Serial Setup
    gpsSerial.begin(115200, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN);

    Serial.println("--- Node 1 (Transmitter) Ready ---");
}

void processIncomingLora() {
    if (Serial2.available()) {
        String rxData = Serial2.readStringUntil('\n');
        rxData.trim();

        if (rxData.startsWith("+RCV=") && rxData.indexOf("LED_TOGGLE") != -1) {
            ledState = !ledState;
            digitalWrite(LED_PIN, ledState);
            Serial.print(">>> LED Toggled remotely! State: ");
            Serial.println(ledState ? "ON" : "OFF");
        }
    }
}

void sendTelemetryOverLora() {
    float t = dht.readTemperature();
    float h = dht.readHumidity();

    String tempStr = isnan(t) ? "0.0" : String(t, 1);
    String humStr  = isnan(h) ? "0.0" : String(h, 1);

    String latStr  = gps.location.isValid() ? String(gps.location.lat(), 6) : "0.000000";
    String lngStr  = gps.location.isValid() ? String(gps.location.lng(), 6) : "0.000000";
    String satsStr = String(gps.satellites.value());
    String fixStr  = gps.location.isValid() ? "3D_FIX" : "NO_FIX";

    String payload = tempStr + "," + humStr + "," + latStr + "," + lngStr + "," + satsStr + "," + fixStr;
    String atCmd = "AT+SEND=2," + String(payload.length()) + "," + payload;
    
    Serial2.print(atCmd + "\r\n");
    Serial.println(">>> Sent via LoRa: " + payload);
}

void loop() {
    while (gpsSerial.available()) {
        gps.encode(gpsSerial.read());
    }

    processIncomingLora();

    if (millis() - lastSendTime >= sendInterval) {
        lastSendTime = millis();
        sendTelemetryOverLora();
    }
}

Node 1: Sensor & GPS Transmitter (platform.ini)

C/C++
[env:esp32s3_node1]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
monitor_speed = 115200

lib_deps = 
    adafruit/DHT sensor library @ ^1.4.6
    adafruit/Adafruit Unified Sensor @ ^1.1.14
    mikalhart/TinyGPSPlus @ ^1.0.3
    adafruit/Adafruit SSD1306 @ ^2.5.9
    adafruit/Adafruit GFX Library @ ^1.11.9
    knolleary/PubSubClient @ ^2.8

Receiver / Gateway Node (Node 2) Configuration

C/C++
[env:esp32s3_node2]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
monitor_speed = 115200

lib_deps = 
    adafruit/Adafruit SSD1306 @ ^2.5.9
    adafruit/Adafruit GFX Library @ ^1.11.9
    knolleary/PubSubClient @ ^2.8

Credits

Farwa Zafar
1 project • 0 followers
Hey there! I am Farwa Zafar a 14 years old school student and i am in 9 class.

Comments