├── .gitignore ├── gplug_AP_V8 ├── MBUS_data_structure.h └── gplug_AP_V8.ino ├── gplug_MQTT_gateway_V8 ├── MBUS_data_structure.h └── gplug_MQTT_gateway_V8.ino ├── 433MHz_Test_Repeater └── 433MHz_Test_Repeater.ino ├── 433MHz_Test_Transmitter └── 433MHz_Test_Transmitter.ino ├── README.md └── gplug_AP_LORAWAN_V3 └── gplug_AP_LORAWAN_V3.ino /.gitignore: -------------------------------------------------------------------------------- 1 | # Claude AI files 2 | claude 3 | claude.md 4 | -------------------------------------------------------------------------------- /gplug_AP_V8/MBUS_data_structure.h: -------------------------------------------------------------------------------- 1 | // --------- Dynamic Structures (for parsing JSON) --------- 2 | struct SensorData { 3 | float Pi; 4 | float Po; 5 | float Pi1; 6 | float Pi2; 7 | float Pi3; 8 | float Po1; 9 | float Po2; 10 | float Po3; 11 | float U1; 12 | float U2; 13 | float U3; 14 | float I1; 15 | float I2; 16 | float I3; 17 | float Ei; 18 | float Eo; 19 | float Ei1; 20 | float Ei2; 21 | float Eo1; 22 | float Eo2; 23 | float Q5; 24 | float Q6; 25 | float Q7; 26 | float Q8; 27 | float Q51; 28 | float Q52; 29 | float Q61; 30 | float Q62; 31 | float Q71; 32 | float Q72; 33 | float Q81; 34 | float Q82; 35 | }; 36 | 37 | struct SensorMessage { 38 | SensorData data; // Contains only sensor data (no timestamp) 39 | }; 40 | 41 | // --------- Packed Structures (for raw binary transmission) --------- 42 | // These structures are defined as packed so that no padding is inserted. 43 | struct __attribute__((packed)) SensorDataPacked { 44 | float Pi; 45 | float Po; 46 | float Pi1; 47 | float Pi2; 48 | float Pi3; 49 | float Po1; 50 | float Po2; 51 | float Po3; 52 | float U1; 53 | float U2; 54 | float U3; 55 | float I1; 56 | float I2; 57 | float I3; 58 | float Ei; 59 | float Eo; 60 | float Ei1; 61 | float Ei2; 62 | float Eo1; 63 | float Eo2; 64 | float Q5; 65 | float Q6; 66 | float Q7; 67 | float Q8; 68 | float Q51; 69 | float Q52; 70 | float Q61; 71 | float Q62; 72 | float Q71; 73 | float Q72; 74 | float Q81; 75 | float Q82; 76 | }; 77 | 78 | struct __attribute__((packed)) SensorMessagePacked { 79 | SensorDataPacked data; 80 | }; -------------------------------------------------------------------------------- /gplug_MQTT_gateway_V8/MBUS_data_structure.h: -------------------------------------------------------------------------------- 1 | #ifndef MBUS_DATA_STRUCTURE_H 2 | #define MBUS_DATA_STRUCTURE_H 3 | 4 | #define LORA_FREQUENCY 430.8E6 // 434 MHz 5 | #define LORA_SF 9 6 | 7 | // --------- Dynamic Structures (for parsing JSON) --------- 8 | struct SensorData { 9 | float Pi; 10 | float Po; 11 | float Pi1; 12 | float Pi2; 13 | float Pi3; 14 | float Po1; 15 | float Po2; 16 | float Po3; 17 | float U1; 18 | float U2; 19 | float U3; 20 | float I1; 21 | float I2; 22 | float I3; 23 | float Ei; 24 | float Eo; 25 | float Ei1; 26 | float Ei2; 27 | float Eo1; 28 | float Eo2; 29 | float Q5; 30 | float Q6; 31 | float Q7; 32 | float Q8; 33 | float Q51; 34 | float Q52; 35 | float Q61; 36 | float Q62; 37 | float Q71; 38 | float Q72; 39 | float Q81; 40 | float Q82; 41 | }; 42 | 43 | struct SensorMessage { 44 | SensorData data; // Contains only sensor data (no timestamp) 45 | }; 46 | 47 | // --------- Packed Structures (for raw binary transmission) --------- 48 | // These structures are defined as packed so that no padding is inserted. 49 | struct __attribute__((packed)) SensorDataPacked { 50 | float Pi; 51 | float Po; 52 | float Pi1; 53 | float Pi2; 54 | float Pi3; 55 | float Po1; 56 | float Po2; 57 | float Po3; 58 | float I1; 59 | float I2; 60 | float I3; 61 | float Ei; 62 | float Eo; 63 | float Ei1; 64 | float Ei2; 65 | float Eo1; 66 | float Eo2; 67 | float Q5; 68 | float Q6; 69 | float Q7; 70 | float Q8; 71 | uint16_t parity; 72 | }; 73 | 74 | struct __attribute__((packed)) SensorMessagePacked { 75 | SensorDataPacked data; 76 | }; 77 | 78 | //---------------------------------------- 79 | 80 | struct __attribute__((packed)) SensorDataLoRaWAN { 81 | float Pi; 82 | float Po; 83 | float Pi1; 84 | float Pi2; 85 | float Pi3; 86 | float Po1; 87 | float Po2; 88 | float Po3; 89 | float I1; 90 | float I2; 91 | float I3; 92 | float Ei; 93 | float Eo; 94 | float Ei1; 95 | float Ei2; 96 | float Eo1; 97 | float Eo2; 98 | float Q5; 99 | float Q6; 100 | float Q7; 101 | float Q8; 102 | }; 103 | 104 | struct __attribute__((packed)) SensorMessageLoRaWAN { 105 | SensorDataLoRaWAN data; 106 | }; 107 | 108 | #endif // MBUS_DATA_STRUCTURE_H -------------------------------------------------------------------------------- /433MHz_Test_Repeater/433MHz_Test_Repeater.ino: -------------------------------------------------------------------------------- 1 | /* 2 | * TTGO LoRa32 Repeater Sketch with Debug Messages 3 | * 4 | * This sketch configures the TTGO LoRa32 board as a LoRa repeater. 5 | * It listens for incoming LoRa packets, displays the RSSI and SNR values on an OLED, 6 | * and sends an acknowledgment packet containing these signal metrics. 7 | * Debug messages are printed to the Serial monitor to help trace the execution. 8 | */ 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | // Pin definitions for the LoRa module 16 | #define LORA_SCK 5 17 | #define LORA_MISO 19 18 | #define LORA_MOSI 27 19 | #define LORA_CS 18 20 | #define LORA_RST 23 21 | #define LORA_DIO0 26 22 | 23 | // Create a SPI interface using VSPI for the LoRa module 24 | SPIClass spiLora(VSPI); 25 | 26 | // Initialize the LoRa module instance using RadioLib. 27 | // Constructor parameters: Chip Select, DIO0, Reset, unused pin (-1), and SPI interface. 28 | SX1276 lora = new Module(LORA_CS, LORA_DIO0, LORA_RST, -1, spiLora); 29 | 30 | // Initialize the OLED display (128x64 pixels) using hardware I2C 31 | U8G2_SSD1306_128X64_NONAME_F_HW_I2C display(U8G2_R0, /* reset=*/U8X8_PIN_NONE); 32 | 33 | void setup() { 34 | // Start Serial communication for debugging at 115200 baud 35 | Serial.begin(115200); 36 | while (!Serial) 37 | ; // Wait for Serial port to be ready 38 | 39 | Serial.println("Initializing SPI interface for LoRa..."); 40 | // Initialize SPI for LoRa communication with defined SCK, MISO, and MOSI pins 41 | spiLora.begin(LORA_SCK, LORA_MISO, LORA_MOSI); 42 | 43 | Serial.println("Setting up LoRa parameters..."); 44 | 45 | Serial.println("Initializing OLED display..."); 46 | // Initialize the OLED display and show an initialization message 47 | display.begin(); 48 | display.clearBuffer(); 49 | display.setFont(u8g2_font_ncenB08_tr); 50 | display.drawStr(0, 10, "Repeater Init"); 51 | display.sendBuffer(); 52 | 53 | Serial.println("Initializing LoRa module..."); 54 | // spreading factor (9), coding rate (7), and sync word (0x12). 55 | int state = lora.begin(434.0, 125.0, 7, 7, 0x12); 56 | if (state != RADIOLIB_ERR_NONE) { 57 | Serial.print("LoRa init failed with error code: "); 58 | Serial.println(state); 59 | while (true) 60 | ; // Halt execution if LoRa fails to initialize 61 | } 62 | 63 | // Set the LoRa module's transmission power to 10 dBm 64 | lora.setOutputPower(10); 65 | Serial.println("LoRa module initialized successfully. Entering main loop..."); 66 | } 67 | 68 | void loop() { 69 | uint8_t buf[10]; // Buffer to store received data (up to 10 bytes) 70 | size_t len = sizeof(buf); 71 | 72 | // Attempt to receive a packet over LoRa 73 | int state = lora.receive(buf, len); 74 | 75 | // Check if a packet was received successfully 76 | if (state == RADIOLIB_ERR_NONE) { 77 | Serial.println("Packet received!"); 78 | 79 | // Retrieve the received signal strength (RSSI) and signal-to-noise ratio (SNR) 80 | float rssi = lora.getRSSI(); 81 | float snr = lora.getSNR(); 82 | Serial.print("RSSI: "); 83 | Serial.println(rssi); 84 | Serial.print("SNR: "); 85 | Serial.println(snr); 86 | 87 | // Update the OLED display with RSSI and SNR values 88 | display.clearBuffer(); 89 | display.setCursor(0, 15); 90 | display.print("Remote RSSI: "); 91 | display.println(rssi); 92 | display.setCursor(0, 35); 93 | display.print("Remote SNR: "); 94 | display.println(snr); 95 | display.sendBuffer(); 96 | 97 | // Prepare an acknowledgment packet containing the RSSI and SNR values. 98 | // Casting float values to int8_t for a compact 2-byte payload. 99 | uint8_t ack[2]; 100 | ack[0] = (int8_t)rssi; 101 | ack[1] = (int8_t)snr; 102 | 103 | // Transmit the acknowledgment packet back to the sender 104 | Serial.println("Transmitting acknowledgment..."); 105 | lora.transmit(ack, 2); 106 | Serial.println("Acknowledgment transmitted."); 107 | } else { 108 | // To avoid flooding the Serial monitor, only log when no packet is received at intervals. 109 | static unsigned long lastDebugTime = 0; 110 | if (millis() - lastDebugTime > 5000) { 111 | Serial.print("Listening for packets... (state code: "); 112 | Serial.print(state); 113 | Serial.println(")"); 114 | lastDebugTime = millis(); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /433MHz_Test_Transmitter/433MHz_Test_Transmitter.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | // ----------------------------- 7 | // Pin definitions for LoRa module 8 | // ----------------------------- 9 | #define LORA_SCK 5 10 | #define LORA_MISO 19 11 | #define LORA_MOSI 27 12 | #define LORA_CS 18 13 | #define LORA_RST 23 14 | #define LORA_DIO0 26 15 | 16 | // ----------------------------- 17 | // Create an SPI instance for LoRa 18 | // ----------------------------- 19 | SPIClass spiLora(VSPI); 20 | 21 | // ----------------------------- 22 | // Initialize the LoRa module instance using RadioLib. 23 | // The constructor takes the Chip Select, DIO0, Reset pins, 24 | // a dummy parameter (-1), and the SPI instance. 25 | // ----------------------------- 26 | SX1276 lora = new Module(LORA_CS, LORA_DIO0, LORA_RST, -1, spiLora); 27 | 28 | // ----------------------------- 29 | // Initialize the OLED display using U8g2 library. 30 | // ----------------------------- 31 | U8G2_SSD1306_128X64_NONAME_F_HW_I2C display(U8G2_R0, U8X8_PIN_NONE); 32 | 33 | void setup() { 34 | // Initialize serial communication for debugging 35 | Serial.begin(115200); 36 | Serial.println("System initializing..."); 37 | 38 | // Initialize SPI for LoRa module with defined pins 39 | spiLora.begin(LORA_SCK, LORA_MISO, LORA_MOSI); 40 | Serial.println("SPI interface initialized."); 41 | 42 | // Initialize the OLED display 43 | display.begin(); 44 | display.clearBuffer(); 45 | display.setFont(u8g2_font_ncenB08_tr); 46 | display.drawStr(0, 10, "Transmitter Init"); 47 | display.sendBuffer(); 48 | Serial.println("OLED display initialized."); 49 | 50 | // Initialize the LoRa module with parameters: 51 | // Frequency: 434.0 MHz, Bandwidth: 125.0 kHz, Spreading Factor: 9, 52 | // Coding Rate: 7, Sync Word: 0x12. 53 | int state = lora.begin(434.0, 125.0, 7, 7, 0x12); 54 | if (state != RADIOLIB_ERR_NONE) { 55 | Serial.print("LoRa init failed with error code: "); 56 | Serial.println(state); 57 | 58 | // Display error message on OLED 59 | display.clearBuffer(); 60 | display.setCursor(0, 10); 61 | display.print("LoRa init failed: "); 62 | display.println(state); 63 | display.sendBuffer(); 64 | 65 | while (true); // Halt execution if initialization fails 66 | } 67 | Serial.println("LoRa module initialized successfully."); 68 | 69 | // Set LoRa module's output power to 10 dBm 70 | lora.setOutputPower(10); 71 | Serial.println("LoRa output power set to 10 dBm."); 72 | } 73 | 74 | void loop() { 75 | // Prepare a 1-byte payload to be sent 76 | uint8_t payload = 42; 77 | Serial.println("Sending message..."); 78 | 79 | // Transmit the payload over LoRa 80 | int state = lora.transmit((uint8_t *)&payload, 1); 81 | if (state == RADIOLIB_ERR_NONE) { 82 | Serial.println("Message sent successfully."); 83 | Serial.println("Waiting for reply..."); 84 | 85 | // Buffer to store the received response (expecting at least 2 bytes) 86 | uint8_t buf[10]; 87 | size_t len = sizeof(buf); 88 | int rcvState = lora.receive(buf, len); 89 | 90 | if (rcvState == RADIOLIB_ERR_NONE) { 91 | // Retrieve local LoRa signal metrics 92 | float localRSSI = lora.getRSSI(); 93 | float localSNR = lora.getSNR(); 94 | 95 | // Assume that the first two bytes of the received buffer contain 96 | // the remote device's RSSI and SNR, respectively 97 | int8_t remoteRSSI = buf[0]; 98 | int8_t remoteSNR = buf[1]; 99 | 100 | // Display local and remote signal metrics on the OLED display 101 | display.clearBuffer(); 102 | display.setCursor(0, 10); 103 | display.print("Local RSSI: "); 104 | display.println(localRSSI); 105 | display.setCursor(0, 25); 106 | display.print("Local SNR: "); 107 | display.println(localSNR); 108 | display.setCursor(0, 40); 109 | display.print("Remote RSSI: "); 110 | display.println(remoteRSSI); 111 | display.setCursor(0, 55); 112 | display.print("Remote SNR: "); 113 | display.println(remoteSNR); 114 | display.sendBuffer(); 115 | 116 | // Print the signal metrics to the Serial Monitor for debugging 117 | Serial.print("Local RSSI: "); 118 | Serial.println(localRSSI); 119 | Serial.print("Local SNR: "); 120 | Serial.println(localSNR); 121 | Serial.print("Remote RSSI: "); 122 | Serial.println(remoteRSSI); 123 | Serial.print("Remote SNR: "); 124 | Serial.println(remoteSNR); 125 | } else { 126 | Serial.print("Receive failed with error code: "); 127 | Serial.println(rcvState); 128 | } 129 | } else { 130 | Serial.print("Transmit failed with error code: "); 131 | Serial.println(state); 132 | } 133 | 134 | // Wait 3 seconds before the next transmission 135 | delay(3000); 136 | } 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 📡 MQTT-Extender 2 | 3 | > Extend MQTT sensor data over long distances using LoRa and LoRaWAN technologies 4 | 5 | [![YouTube Video](https://img.shields.io/badge/YouTube-Watch%20Video-red?style=for-the-badge&logo=youtube)](https://youtu.be/EMUxSV9rrCg) 6 | [![License](https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge)](LICENSE) 7 | [![Platform](https://img.shields.io/badge/Platform-ESP32-green.svg?style=for-the-badge&logo=espressif)](https://www.espressif.com/) 8 | [![Arduino](https://img.shields.io/badge/Arduino-Compatible-00979D?style=for-the-badge&logo=arduino)](https://www.arduino.cc/) 9 | 10 | --- 11 | 12 | ## 📋 Overview 13 | 14 | MQTT-Extender bridges local MQTT sensor networks with remote data collection systems using LoRa/LoRaWAN wireless communication. Perfect for IoT deployments where WiFi/Ethernet connectivity is limited or unavailable. 15 | 16 | ### ✨ Key Features 17 | 18 | - 🔌 **MQTT Broker**: Built-in lightweight MQTT broker on ESP32 19 | - 📶 **LoRa/LoRaWAN**: Dual support for point-to-point LoRa and LoRaWAN networks 20 | - 📊 **Data Aggregation**: Averages multiple sensor readings before transmission 21 | - 🔐 **CRC16 Validation**: Ensures data integrity with checksum verification 22 | - ✅ **ACK/NACK Protocol**: Reliable delivery with acknowledgment mechanism 23 | - 📺 **OLED Display**: Real-time status monitoring 24 | - 🔄 **OTA Updates**: Over-the-air firmware updates support 25 | - ⚡ **Watchdog Protection**: Automatic recovery from failures 26 | 27 | --- 28 | 29 | ## 🏗️ Architecture 30 | 31 | ### System 1: Point-to-Point LoRa (V8) 32 | ``` 33 | Sensor → MQTT → [ESP32 AP] ─── LoRa 433MHz ───→ [ESP32 Gateway] → MQTT Broker 34 | ↓ ↓ 35 | WiFi AP WiFi Station 36 | ``` 37 | 38 | ### System 2: LoRaWAN (V3) 39 | ``` 40 | Sensor → MQTT → [ESP32 AP] ─── LoRaWAN EU868 ───→ LoRaWAN Network Server 41 | ↓ 42 | WiFi AP 43 | ``` 44 | 45 | --- 46 | 47 | ## 📁 Repository Structure 48 | 49 | ``` 50 | MQTT-Extender/ 51 | │ 52 | ├── 🧪 Test Projects (433MHz) 53 | │ ├── 433MHz_Test_Repeater/ # LoRa repeater with RSSI/SNR display 54 | │ └── 433MHz_Test_Transmitter/ # LoRa test transmitter 55 | │ 56 | ├── 🔵 Point-to-Point LoRa System (V8) 57 | │ ├── gplug_AP_V8/ # Access Point + LoRa Transmitter 58 | │ │ ├── gplug_AP_V8.ino 59 | │ │ └── MBUS_data_structure.h 60 | │ └── gplug_MQTT_gateway_V8/ # LoRa Receiver + MQTT Publisher 61 | │ ├── gplug_MQTT_gateway_V8.ino 62 | │ └── MBUS_data_structure.h 63 | │ 64 | └── 🟢 LoRaWAN System (V3) 65 | └── gplug_AP_LORAWAN_V3/ # Access Point + LoRaWAN Gateway 66 | └── gplug_AP_LORAWAN_V3.ino 67 | ``` 68 | 69 | --- 70 | 71 | ## 🚀 Getting Started 72 | 73 | ### Hardware Requirements 74 | 75 | - 2x **TTGO LoRa32** (ESP32 with LoRa module) 76 | - **SSD1306 OLED Display** (128x64) 77 | - **433MHz** or **868MHz** antenna (depending on region) 78 | 79 | ### Software Requirements 80 | 81 | - **Arduino IDE** or **PlatformIO** 82 | - **Libraries**: 83 | - `RadioLib` (for LoRaWAN) 84 | - `LoRa` (for point-to-point) 85 | - `ArduinoJson` 86 | - `Adafruit GFX` + `Adafruit SSD1306` 87 | - `PubSubClient` (for MQTT) 88 | 89 | ### Installation 90 | 91 | 1. Clone the repository: 92 | ```bash 93 | git clone https://github.com/SensorsIot/MQTT-Extender.git 94 | ``` 95 | 96 | 2. Open the desired sketch in Arduino IDE 97 | 98 | 3. Install required libraries via Library Manager 99 | 100 | 4. Configure your settings: 101 | - WiFi credentials 102 | - MQTT broker settings 103 | - LoRa frequency and spreading factor 104 | - LoRaWAN keys (for V3) 105 | 106 | 5. Upload to your ESP32 boards 107 | 108 | --- 109 | 110 | ## ⚙️ Configuration 111 | 112 | ### Point-to-Point LoRa (V8) 113 | 114 | **Access Point (gplug_AP_V8):** 115 | - Creates WiFi AP: `ESP32_AP` / `esp32password` 116 | - MQTT broker on port `1883` 117 | - LoRa frequency: Configure in `secrets.h` 118 | - Debug level: `DEBUG_LEVEL 1` 119 | 120 | **Gateway (gplug_MQTT_gateway_V8):** 121 | - Connects to your WiFi network 122 | - Publishes to MQTT topics: 123 | - `MBUS/values` - Sensor data 124 | - `MBUS/signal` - RSSI/SNR 125 | - `MBUS/log` - Error logs 126 | 127 | ### LoRaWAN (V3) 128 | 129 | **Access Point (gplug_AP_LORAWAN_V3):** 130 | - Creates WiFi AP: `ESP32_AP` / `esp32password` 131 | - MQTT broker on port `1883` 132 | - LoRaWAN region: `EU868` 133 | - Averages 6 messages before transmission 134 | - Debug level: `DEBUG_LEVEL 2` 135 | - Configure device keys in code (lines 462-486) 136 | 137 | --- 138 | 139 | ## 📊 Data Structure 140 | 141 | ### Full Structure (V8 - 32 fields, 128 bytes) 142 | - Power: `Pi, Po, Pi1, Pi2, Pi3, Po1, Po2, Po3` 143 | - Voltage: `U1, U2, U3` 144 | - Current: `I1, I2, I3` 145 | - Energy: `Ei, Eo, Ei1, Ei2, Eo1, Eo2` 146 | - Reactive: `Q5, Q6, Q7, Q8, Q51, Q52, Q61, Q62, Q71, Q72, Q81, Q82` 147 | 148 | ### Compact Structure (V3 - 15 fields, 60 bytes) 149 | - Power: `Pi, Po` 150 | - Current: `I1, I2, I3` 151 | - Energy: `Ei, Eo, Ei1, Ei2, Eo1, Eo2` 152 | - Reactive: `Q5, Q6, Q7, Q8` 153 | 154 | --- 155 | 156 | ## 🎯 Use Cases 157 | 158 | - 🏭 **Industrial Monitoring**: Remote equipment monitoring 159 | - 🌾 **Agriculture**: Soil sensors in fields without connectivity 160 | - 🏘️ **Smart Cities**: Distributed sensor networks 161 | - ⚡ **Energy Monitoring**: Smart meter data collection 162 | - 🌡️ **Environmental**: Weather stations and air quality monitoring 163 | 164 | --- 165 | 166 | ## 🐛 Troubleshooting 167 | 168 | ### Common Issues 169 | 170 | **LoRa not transmitting:** 171 | - Check antenna connection 172 | - Verify frequency settings match between TX/RX 173 | - Ensure spreading factor is compatible 174 | 175 | **MQTT broker not responding:** 176 | - Verify WiFi connection 177 | - Check MQTT port (default 1883) 178 | - Review debug output for errors 179 | 180 | **Watchdog reboots:** 181 | - Check MQTT message timeout settings 182 | - Verify sensor is publishing data 183 | - Review ACK timeout values (V8 only) 184 | 185 | ### Debug Levels 186 | 187 | Set `DEBUG_LEVEL` in code: 188 | - `1` - Error messages only 189 | - `2` - Info messages (recommended) 190 | - `3` - Full debugging output 191 | 192 | --- 193 | 194 | ## 📺 Video Tutorial 195 | 196 | For a complete walkthrough and demonstration, watch the video: 197 | 198 | [![MQTT Extender Video](https://img.youtube.com/vi/EMUxSV9rrCg/maxresdefault.jpg)](https://youtu.be/EMUxSV9rrCg) 199 | 200 | --- 201 | 202 | ## 📄 License 203 | 204 | This project is open source and available under the MIT License. 205 | 206 | --- 207 | 208 | ## 👤 Author 209 | 210 | **Andreas Spiess** 211 | 212 | - YouTube: [@AndreasSpiess](https://www.youtube.com/AndreasSpiess) 213 | - GitHub: [@SensorsIot](https://github.com/SensorsIot) 214 | 215 | --- 216 | 217 | ## 🤝 Contributing 218 | 219 | Contributions, issues, and feature requests are welcome! 220 | 221 | --- 222 | 223 | ## ⭐ Show Your Support 224 | 225 | If this project helped you, please give it a ⭐️! 226 | -------------------------------------------------------------------------------- /gplug_MQTT_gateway_V8/gplug_MQTT_gateway_V8.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include // Contains the updated SensorDataPacked structure 8 | #include 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | // ----- Three-Level Debugging Macros ----- 15 | // Set the debug level at compile time (1 = simple, 2 = intermediate, 3 = extended) 16 | #ifndef DEBUG_LEVEL 17 | #define DEBUG_LEVEL 3 18 | #endif 19 | 20 | #if DEBUG_LEVEL >= 1 21 | #define DEBUG_SIMPLE(x) Serial.println(x) 22 | #define DEBUG_SIMPLEF(fmt, ...) Serial.printf(fmt, __VA_ARGS__) 23 | #else 24 | #define DEBUG_SIMPLE(x) 25 | #define DEBUG_SIMPLEF(fmt, ...) 26 | #endif 27 | 28 | #if DEBUG_LEVEL >= 2 29 | #define DEBUG_INTERMEDIATE(x) Serial.println(x) 30 | #define DEBUG_INTERMEDIATEF(fmt, ...) Serial.printf(fmt, __VA_ARGS__) 31 | #else 32 | #define DEBUG_INTERMEDIATE(x) 33 | #define DEBUG_INTERMEDIATEF(fmt, ...) 34 | #endif 35 | 36 | #if DEBUG_LEVEL >= 3 37 | #define DEBUG_EXTENDED(x) Serial.println(x) 38 | #define DEBUG_EXTENDEDF(fmt, ...) Serial.printf(fmt, __VA_ARGS__) 39 | #else 40 | #define DEBUG_EXTENDED(x) 41 | #define DEBUG_EXTENDEDF(fmt, ...) 42 | #endif 43 | 44 | // --------- WiFi and MQTT Settings --------- 45 | #define MQTT_PORT 1883 46 | #define MQTT_TOPIC_VALUES "MBUS/values" 47 | #define MQTT_TOPIC_SIGNAL "MBUS/signal" 48 | #define MQTT_TOPIC_LOG "MBUS/log" 49 | 50 | // Create WiFi and MQTT clients. 51 | WiFiClient wifiClient; 52 | PubSubClient mqttClient(wifiClient); 53 | 54 | // --------- LoRa Settings (TTGO LoRa32 V1.6.1, 433MHz) --------- 55 | #define LORA_SCK 5 56 | #define LORA_MISO 19 57 | #define LORA_MOSI 27 58 | #define LORA_CS 18 59 | #define LORA_RST 23 60 | #define LORA_DIO0 26 61 | 62 | // Macro to get only the file name from __FILE__ 63 | #define __SHORT_FILE__ ((strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : (strrchr(__FILE__, '\\') ? strrchr(__FILE__, '\\') + 1 : __FILE__))) 64 | 65 | // --------- ACK and NACK Settings --------- 66 | enum { 67 | ACK_BYTE = 0x55, 68 | NACK_BYTE = 0xAA 69 | }; 70 | 71 | // Expected size for SensorDataPacked (includes 16-bit CRC field) 72 | #define EXPECTED_MSG_SIZE (sizeof(SensorDataPacked)) 73 | 74 | // --------- OLED Display Settings --------- 75 | #define SCREEN_WIDTH 128 76 | #define SCREEN_HEIGHT 64 77 | #define OLED_RESET -1 78 | Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); 79 | 80 | // --------- Buffer for Received LoRa Packet --------- 81 | #define RX_BUFFER_SIZE 128 82 | volatile bool packetReceived = false; 83 | volatile uint8_t rxBuffer[RX_BUFFER_SIZE]; 84 | volatile int rxLength = 0; 85 | 86 | // --------- Timing Settings --------- 87 | #define PACKET_TIMEOUT 120000UL // 2 minutes 88 | unsigned long lastPacketTime = 0; 89 | unsigned long lastMQTTClientConnect = 0; // Timestamp of last MQTT activity 90 | #define MQTT_CONNECT_TIMEOUT 60000UL // 60 seconds 91 | #define MAX_RETRIES 5 92 | 93 | // --------- OLED Display Functions --------- 94 | void displayMBUSGateway() { 95 | display.setCursor(25, 55); 96 | display.println("MBUS-Gateway"); 97 | display.display(); 98 | } 99 | 100 | void displaySensorData(const SensorDataPacked& data) { 101 | display.clearDisplay(); 102 | display.setCursor(0, 0); 103 | display.println("Pi:" + String(data.Pi, 2) + " Po:" + String(data.Po, 2)); 104 | display.setCursor(0, 10); 105 | display.println("i1:" + String(data.Pi1, 1) + " i2:" + String(data.Pi2, 1) + " i3:" + String(data.Pi3, 1)); 106 | display.setCursor(0, 20); 107 | display.println("o1:" + String(data.Po1, 1) + " o2:" + String(data.Po2, 1) + " o3:" + String(data.Po3, 1)); 108 | displayMBUSGateway(); 109 | } 110 | 111 | void displaySignalQuality(int rssi, float snr) { 112 | display.fillRect(0, 30, SCREEN_WIDTH, 10, SSD1306_BLACK); 113 | display.setCursor(0, 30); 114 | display.print("RSSI: "); 115 | display.print(rssi); 116 | display.print(" SNR: "); 117 | display.print(snr); 118 | displayMBUSGateway(); 119 | } 120 | 121 | void displayAckStatus(const String& statusMessage) { 122 | display.fillRect(0, 40, SCREEN_WIDTH, 10, SSD1306_BLACK); 123 | display.setCursor(0, 40); 124 | display.println(statusMessage); 125 | displayMBUSGateway(); 126 | } 127 | 128 | void displayError(const String& message) { 129 | display.fillRect(0, 40, SCREEN_WIDTH, 10, SSD1306_BLACK); 130 | display.setCursor(0, 40); 131 | display.println(message); 132 | displayMBUSGateway(); 133 | } 134 | 135 | // --------- CRC16 Calculation (CCITT, polynomial 0x1021) --------- 136 | uint16_t crc16(const uint8_t* data, size_t length) { 137 | uint16_t crc = 0xFFFF; // Initial value 138 | for (size_t i = 0; i < length; i++) { 139 | crc ^= ((uint16_t)data[i] << 8); 140 | for (int j = 0; j < 8; j++) { 141 | if (crc & 0x8000) 142 | crc = (crc << 1) ^ 0x1021; 143 | else 144 | crc = (crc << 1); 145 | } 146 | } 147 | return crc; 148 | } 149 | 150 | // --------- System and MQTT Functions --------- 151 | void rebootSystem(const String& reason) { 152 | DEBUG_SIMPLE("Critical error: " + reason); 153 | mqttLog("Critical error: " + reason); 154 | displayError(reason); 155 | delay(3000); 156 | ESP.restart(); 157 | } 158 | 159 | void reconnectMQTT() { 160 | int retryCount = 0; 161 | while (!mqttClient.connected() && retryCount < MAX_RETRIES) { 162 | DEBUG_INTERMEDIATEF("Attempting MQTT connection... (Attempt %d of %d)\n", retryCount + 1, MAX_RETRIES); 163 | 164 | String clientId = "ESP32Receiver-"; 165 | clientId += String(random(0xffff), HEX); 166 | 167 | if (mqttClient.connect(clientId.c_str())) { 168 | DEBUG_SIMPLE("MQTT connected"); 169 | lastMQTTClientConnect = millis(); 170 | return; 171 | } else { 172 | DEBUG_INTERMEDIATEF("MQTT connection failed, rc=%d\n", mqttClient.state()); 173 | mqttLog("MQTT connection failed with state: " + String(mqttClient.state())); 174 | retryCount++; 175 | if (retryCount < MAX_RETRIES) { 176 | delay(5000); // Reduced delay for faster retry 177 | } else { 178 | DEBUG_SIMPLE("Max retry attempts reached, rebooting..."); 179 | mqttLog("MQTT connection failed after max retries, rebooting system."); 180 | rebootSystem("MQTT connection failed after max retries."); 181 | } 182 | } 183 | } 184 | } 185 | 186 | void mqttLog(const String& message) { 187 | DEBUG_EXTENDED("MQTT Log (Error): " + message); 188 | if (!mqttClient.publish(MQTT_TOPIC_LOG, message.c_str())) { 189 | DEBUG_INTERMEDIATE("MQTT log publish failed for " + String(MQTT_TOPIC_LOG)); 190 | } 191 | } 192 | 193 | void checkPacketTimeout() { 194 | if (millis() - lastPacketTime > PACKET_TIMEOUT) { 195 | rebootSystem("Packet timeout"); 196 | } 197 | } 198 | 199 | // --------- LoRa Interrupt Handler --------- 200 | void IRAM_ATTR onReceive(int packetSize) { 201 | if (packetSize == 0) return; 202 | rxLength = 0; 203 | while (LoRa.available() && rxLength < RX_BUFFER_SIZE) { 204 | rxBuffer[rxLength++] = LoRa.read(); 205 | } 206 | packetReceived = true; 207 | lastPacketTime = millis(); 208 | DEBUG_EXTENDED("LoRa packet received, size: " + String(packetSize)); 209 | } 210 | 211 | // --------- LoRa ACK/NACK Transmission --------- 212 | bool sendLoRaAck() { 213 | LoRa.beginPacket(); 214 | LoRa.write(ACK_BYTE); 215 | int result = LoRa.endPacket(); 216 | if (result == 0) { 217 | DEBUG_INTERMEDIATE("Failed to send ACK (LoRa.endPacket returned 0)"); 218 | mqttLog("Failed to send ACK (LoRa.endPacket returned 0)"); 219 | return false; 220 | } 221 | LoRa.receive(); 222 | DEBUG_EXTENDED("ACK sent to transmitter"); 223 | return true; 224 | } 225 | 226 | bool sendLoRaNack() { 227 | LoRa.beginPacket(); 228 | LoRa.write(NACK_BYTE); 229 | int result = LoRa.endPacket(); 230 | if (result == 0) { 231 | DEBUG_INTERMEDIATE("Failed to send NACK (LoRa.endPacket returned 0)"); 232 | mqttLog("Failed to send NACK (LoRa.endPacket returned 0)"); 233 | return false; 234 | } 235 | LoRa.receive(); 236 | DEBUG_EXTENDED("NACK sent to transmitter"); 237 | return true; 238 | } 239 | 240 | // --------- Hardware Initialization --------- 241 | bool initializeHardware() { 242 | // Initialize OLED display. 243 | if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { 244 | DEBUG_SIMPLE("SSD1306 allocation failed"); 245 | return false; 246 | } 247 | display.clearDisplay(); 248 | display.display(); 249 | display.setTextSize(1); 250 | display.setTextColor(SSD1306_WHITE); 251 | display.setCursor(0, 0); 252 | display.println("OLED init OK!"); 253 | display.display(); 254 | DEBUG_SIMPLE("OLED initialized"); 255 | 256 | // Set WiFi to Station mode. 257 | WiFi.mode(WIFI_STA); 258 | WiFi.begin(SSID, PASSWORD); 259 | while (WiFi.status() != WL_CONNECTED) { 260 | delay(500); 261 | DEBUG_SIMPLE("."); 262 | } 263 | DEBUG_SIMPLE("\nWiFi connected, IP: " + WiFi.localIP().toString()); 264 | 265 | // Set up MQTT client. 266 | mqttClient.setServer(MQTT_SERVER, MQTT_PORT); 267 | DEBUG_SIMPLE("MQTT client configured"); 268 | 269 | // Initialize LoRa module. 270 | SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); 271 | LoRa.setPins(LORA_CS, LORA_RST, LORA_DIO0); 272 | if (!LoRa.begin(LORA_FREQUENCY)) { 273 | DEBUG_SIMPLE("Starting LoRa failed!"); 274 | display.clearDisplay(); 275 | display.setCursor(0, 0); 276 | display.println("LoRa init fail!"); 277 | display.display(); 278 | return false; 279 | } 280 | LoRa.setSpreadingFactor(LORA_SF); 281 | LoRa.setTxPower(20); 282 | DEBUG_SIMPLE("LoRa initialized"); 283 | display.clearDisplay(); 284 | display.setCursor(0, 0); 285 | display.println("LoRa init OK!"); 286 | display.display(); 287 | 288 | return true; 289 | } 290 | 291 | // --------- Setup and Loop --------- 292 | void setup() { 293 | Serial.begin(115200); 294 | while (!Serial) 295 | ; // Wait for Serial to be ready 296 | DEBUG_SIMPLE("\n-------------------------------------------"); 297 | DEBUG_SIMPLEF("File: %s", __SHORT_FILE__); // Fixed line 298 | DEBUG_SIMPLE("\n-------------------------------------------"); 299 | DEBUG_SIMPLE("\nMBUS LoRa to MQTT Receiver starting..."); 300 | lastMQTTClientConnect = millis(); 301 | if (!initializeHardware()) { 302 | while (true) { 303 | delay(1000); 304 | } 305 | } 306 | 307 | // Setup OTA updates. 308 | ArduinoOTA.onStart([]() { 309 | String type = (ArduinoOTA.getCommand() == U_FLASH) ? "sketch" : "filesystem"; 310 | DEBUG_SIMPLE("Start updating " + type); 311 | }); 312 | ArduinoOTA.onEnd([]() { 313 | DEBUG_SIMPLE("\nOTA Update End"); 314 | }); 315 | ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) { 316 | DEBUG_EXTENDEDF("OTA Progress: %u%%\n", (progress * 100) / total); 317 | }); 318 | ArduinoOTA.onError([](ota_error_t error) { 319 | DEBUG_SIMPLEF("OTA Error[%u]: ", error); 320 | if (error == OTA_AUTH_ERROR) DEBUG_SIMPLE("Auth Failed"); 321 | else if (error == OTA_BEGIN_ERROR) DEBUG_SIMPLE("Begin Failed"); 322 | else if (error == OTA_CONNECT_ERROR) DEBUG_SIMPLE("Connect Failed"); 323 | else if (error == OTA_RECEIVE_ERROR) DEBUG_SIMPLE("Receive Failed"); 324 | else if (error == OTA_END_ERROR) DEBUG_SIMPLE("End Failed"); 325 | }); 326 | ArduinoOTA.setHostname("MBUS_Gateway"); 327 | ArduinoOTA.begin(); 328 | DEBUG_SIMPLE("OTA initialized"); 329 | 330 | reconnectMQTT(); // Connect to the MQTT broker. 331 | LoRa.onReceive(onReceive); 332 | LoRa.receive(); 333 | lastPacketTime = millis(); 334 | DEBUG_SIMPLE("LoRa receiver started"); 335 | } 336 | 337 | void loop() { 338 | ArduinoOTA.handle(); 339 | 340 | if (WiFi.status() != WL_CONNECTED) { 341 | rebootSystem("WiFi lost"); 342 | } 343 | 344 | if (!mqttClient.connected()) { 345 | reconnectMQTT(); 346 | } 347 | 348 | mqttClient.loop(); 349 | checkPacketTimeout(); 350 | 351 | // Reboot if no MQTT activity occurs within the timeout period. 352 | if (millis() - lastMQTTClientConnect > MQTT_CONNECT_TIMEOUT) { 353 | rebootSystem("No MQTT client connected within 60 seconds"); 354 | } 355 | 356 | // Process received LoRa packet. 357 | if (packetReceived) { 358 | noInterrupts(); 359 | int packetSize = rxLength; 360 | uint8_t packetBuffer[RX_BUFFER_SIZE]; 361 | memcpy(packetBuffer, (const void*)rxBuffer, rxLength); 362 | packetReceived = false; 363 | interrupts(); 364 | 365 | if (packetSize == EXPECTED_MSG_SIZE) { 366 | SensorDataPacked sensorData; 367 | memcpy(&sensorData, packetBuffer, EXPECTED_MSG_SIZE); 368 | 369 | // Compute CRC16 over the received data (excluding the CRC field). 370 | uint16_t computedCrc = crc16((uint8_t*)&sensorData, sizeof(sensorData) - sizeof(sensorData.parity)); 371 | if (computedCrc != sensorData.parity) { 372 | DEBUG_INTERMEDIATE("CRC error detected. Sending NACK and discarding packet."); 373 | mqttLog("CRC error detected. Sending NACK and discarding packet."); 374 | sendLoRaNack(); 375 | return; 376 | } else { 377 | if (!sendLoRaAck()) { 378 | DEBUG_INTERMEDIATE("Failed to send ACK."); 379 | mqttLog("Failed to send ACK."); 380 | } 381 | } 382 | 383 | // Build JSON payload from sensor data. 384 | StaticJsonDocument<512> doc; 385 | doc["Pi"] = sensorData.Pi; 386 | doc["Po"] = sensorData.Po; 387 | doc["Pi1"] = sensorData.Pi1; 388 | doc["Pi2"] = sensorData.Pi2; 389 | doc["Pi3"] = sensorData.Pi3; 390 | doc["Po1"] = sensorData.Po1; 391 | doc["Po2"] = sensorData.Po2; 392 | doc["Po3"] = sensorData.Po3; 393 | doc["I1"] = sensorData.I1; 394 | doc["I2"] = sensorData.I2; 395 | doc["I3"] = sensorData.I3; 396 | doc["Ei"] = sensorData.Ei; 397 | doc["Eo"] = sensorData.Eo; 398 | doc["Ei1"] = sensorData.Ei1; 399 | doc["Ei2"] = sensorData.Ei2; 400 | doc["Eo1"] = sensorData.Eo1; 401 | doc["Eo2"] = sensorData.Eo2; 402 | doc["Q5"] = sensorData.Q5; 403 | doc["Q6"] = sensorData.Q6; 404 | doc["Q7"] = sensorData.Q7; 405 | doc["Q8"] = sensorData.Q8; 406 | 407 | String jsonPayload; 408 | serializeJson(doc, jsonPayload); 409 | DEBUG_EXTENDED("Decoded LoRa binary message:"); 410 | DEBUG_EXTENDED(jsonPayload); 411 | 412 | if (!mqttClient.publish(MQTT_TOPIC_VALUES, jsonPayload.c_str())) { 413 | DEBUG_INTERMEDIATE("MQTT publish failed for MBUS/values."); 414 | mqttLog("MQTT publish failed for MBUS/values."); 415 | } else { 416 | lastMQTTClientConnect = millis(); // Reset MQTT activity timestamp. 417 | } 418 | 419 | // Publish signal quality. 420 | int rssi = LoRa.packetRssi(); 421 | float snr = LoRa.packetSnr(); 422 | DEBUG_EXTENDEDF("Packet RSSI: %d dB, SNR: %.2f dB\n", rssi, snr); 423 | String signalPayload = "{ \"rssi\": " + String(rssi) + ", \"snr\": " + String(snr) + " }"; 424 | if (!mqttClient.publish(MQTT_TOPIC_SIGNAL, signalPayload.c_str())) { 425 | DEBUG_INTERMEDIATE("MQTT publish failed for MBUS/signal."); 426 | mqttLog("MQTT publish failed for MBUS/signal."); 427 | } else { 428 | lastMQTTClientConnect = millis(); 429 | } 430 | 431 | // Display sensor data and signal quality. 432 | displaySensorData(sensorData); 433 | displaySignalQuality(rssi, snr); 434 | displayAckStatus("ACK OK"); 435 | 436 | } else { 437 | DEBUG_INTERMEDIATEF("Received packet size (%d) does not match expected size (%d).\n", packetSize, EXPECTED_MSG_SIZE); 438 | mqttLog("Received packet size (" + String(packetSize) + ") does not match expected (" + String(EXPECTED_MSG_SIZE) + ")"); 439 | } 440 | } 441 | } 442 | -------------------------------------------------------------------------------- /gplug_AP_V8/gplug_AP_V8.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include "MBUS_data_structure.h" // Contains SensorMessage and SensorMessagePacked 9 | 10 | // ----- Timeout Definitions ----- 11 | // MQTT Message Timeout: If no MQTT message is received for 60000 ms (1 minute), reboot. 12 | #define MQTT_MSG_TIMEOUT 60000 13 | // ACK Watchdog Timeout: If no ACK is received for 60000 ms (1 minute), reboot. 14 | #define ACK_WD_TIMEOUT 60000 15 | // ACK Wait Timeout for each sendWithAck trial (used in sendWithAck below). 16 | #define ACK_TIMEOUT 2000 // 2000 ms as before 17 | // Maximum retransmission attempts for LoRa message. 18 | #define MAX_RETRIES 3 19 | 20 | // ----- Three-Level Debugging Macros ----- 21 | // Set the debug level at compile time (1 = simple, 2 = intermediate, 3 = extended) 22 | #ifndef DEBUG_LEVEL 23 | #define DEBUG_LEVEL 1 24 | #endif 25 | 26 | #if DEBUG_LEVEL >= 1 27 | #define DEBUG_SIMPLE(x) Serial.println(x) 28 | #define DEBUG_SIMPLEF(fmt, ...) Serial.printf(fmt, __VA_ARGS__) 29 | #else 30 | #define DEBUG_SIMPLE(x) 31 | #define DEBUG_SIMPLEF(fmt, ...) 32 | #endif 33 | 34 | #if DEBUG_LEVEL >= 2 35 | #define DEBUG_INTERMEDIATE(x) Serial.println(x) 36 | #define DEBUG_INTERMEDIATEF(fmt, ...) Serial.printf(fmt, __VA_ARGS__) 37 | #else 38 | #define DEBUG_INTERMEDIATE(x) 39 | #define DEBUG_INTERMEDIATEF(fmt, ...) 40 | #endif 41 | 42 | #if DEBUG_LEVEL >= 3 43 | #define DEBUG_EXTENDED(x) Serial.println(x) 44 | #define DEBUG_EXTENDEDF(fmt, ...) Serial.printf(fmt, __VA_ARGS__) 45 | #else 46 | #define DEBUG_EXTENDED(x) 47 | #define DEBUG_EXTENDEDF(fmt, ...) 48 | #endif 49 | 50 | // ----- LoRa Settings ----- 51 | #define LORA_SCK 5 52 | #define LORA_MISO 19 53 | #define LORA_MOSI 27 54 | #define LORA_CS 18 55 | #define LORA_RST 23 56 | #define LORA_DIO0 26 57 | #define LORA_DIO1 33 58 | 59 | // Macro to get only the file name from __FILE__ 60 | #define __SHORT_FILE__ ((strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : (strrchr(__FILE__, '\\') ? strrchr(__FILE__, '\\') + 1 : __FILE__))) 61 | 62 | // ----- OLED Display Settings ----- 63 | #define SCREEN_WIDTH 128 // OLED display width, in pixels 64 | #define SCREEN_HEIGHT 64 // OLED display height, in pixels 65 | #define OLED_RESET -1 // Reset pin (or -1 if sharing Arduino reset pin) 66 | Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); 67 | 68 | // ----- WiFi & MQTT Server Settings ----- 69 | const char *ssid = "ESP32_AP"; 70 | const char *password = "esp32password"; 71 | WiFiServer mqttServer(1883); // Minimal MQTT broker on port 1883 72 | const size_t MAX_PACKET_SIZE = 1024; 73 | #define MQTT_CONNECT_TIMEOUT 30000 // 30-second timeout if no client connects 74 | 75 | // ----- ACK Settings for LoRa Transmission ----- 76 | // (ACK_TIMEOUT and MAX_RETRIES already defined above) 77 | // Expected ACK byte from the gateway: 78 | #define ACK_BYTE 0x55 79 | 80 | // ----- Global Variables for Watchdog ----- 81 | // Record the last time a valid MQTT message was received. 82 | unsigned long lastMQTTMessageTime = 0; 83 | // Record the last time an ACK was received. 84 | unsigned long lastAckTime = 0; 85 | 86 | // ----- Global Variable for MQTT Client Connection ----- 87 | // (Used in Task1_MQTT to reboot if no client connects) 88 | unsigned long lastMQTTClientConnect = 0; 89 | 90 | // ----- RTOS Task Handles ----- 91 | TaskHandle_t Task1_MQTT_Handle = NULL; // MQTT broker & OLED task 92 | TaskHandle_t Task2_LoRa_Handle = NULL; // LoRa transmission task 93 | TaskHandle_t Watchdog_Handle = NULL; // Watchdog task 94 | 95 | // ----- Inter-Task Communication ----- 96 | // Queue for transferring SensorMessage from Task 1 to Task 2. 97 | QueueHandle_t sensorMsgQueue; 98 | 99 | // ----- OLED Display Helper Functions ----- 100 | // displaySensorData() prints sensor values from the SensorMessage. 101 | // Line 1: "Pi:" and "Po:" values 102 | // Line 2: "i1:", "i2:", "i3:" values 103 | // Line 3: "o1:", "o2:", "o3:" values 104 | // Line 4: "RSSI:" and "SNR:" values (defaults to 0 if not provided) 105 | // Line 5: "MBUS-AP" 106 | 107 | void displaySensorData(const SensorMessage &msg, int rssi = 0, float snr = 0.0) { 108 | display.clearDisplay(); 109 | 110 | // Line 1: Sensor summary (Pi and Po) 111 | display.setCursor(0, 0); 112 | display.println("Pi:" + String(msg.data.Pi, 2) + " Po:" + String(msg.data.Po, 2)); 113 | 114 | // Line 2: Sensor details (i1, i2, i3) 115 | display.setCursor(0, 10); 116 | display.println("i1:" + String(msg.data.Pi1, 1) + " i2:" + String(msg.data.Pi2, 1) + " i3:" + String(msg.data.Pi3, 1)); 117 | 118 | // Line 3: Sensor details (o1, o2, o3) 119 | display.setCursor(0, 20); 120 | display.println("o1:" + String(msg.data.Po1, 1) + " o2:" + String(msg.data.Po2, 1) + " o3:" + String(msg.data.Po3, 1)); 121 | 122 | // Line 4: RSSI and SNR values 123 | display.setCursor(0, 30); 124 | display.print("RSSI: "); 125 | display.print(rssi); 126 | display.print(" SNR: "); 127 | display.println(snr); 128 | 129 | // Line 5: Display static text "MBUS-AP" (centered horizontally) 130 | display.setCursor(35, 50); 131 | display.println("MBUS-AP"); 132 | 133 | display.display(); 134 | DEBUG_EXTENDED("OLED updated with sensor data, RSSI, SNR, and MBUS-AP."); 135 | } 136 | 137 | // Displays error messages on OLED (line 40). 138 | void displayError(const String &message) { 139 | display.fillRect(0, 40, SCREEN_WIDTH, 10, SSD1306_BLACK); 140 | display.setCursor(0, 40); 141 | display.println(message); 142 | display.display(); 143 | DEBUG_SIMPLE("Error displayed on OLED: " + message); 144 | } 145 | 146 | // ----- MQTT Packet Processing Functions ----- 147 | // Decodes the MQTT remaining length field. 148 | bool decodeRemainingLength(uint8_t *buf, int available, int &remainingLength, int &rlBytes) { 149 | remainingLength = 0; 150 | rlBytes = 0; 151 | int multiplier = 1; 152 | uint8_t digit; 153 | do { 154 | if (rlBytes >= available || rlBytes > 4) { 155 | DEBUG_EXTENDED("decodeRemainingLength: Not enough bytes or too many iterations."); 156 | return false; 157 | } 158 | digit = buf[rlBytes++]; 159 | remainingLength += (digit & 127) * multiplier; 160 | multiplier *= 128; 161 | } while (digit & 0x80); 162 | DEBUG_EXTENDEDF("Decoded remaining length: %d, bytes used: %d\n", remainingLength, rlBytes); 163 | return true; 164 | } 165 | 166 | // Parses a JSON payload into a SensorMessage structure. 167 | SensorMessage parseSensorMessage(const String &payload) { 168 | SensorMessage msg; 169 | StaticJsonDocument<1024> doc; 170 | DeserializationError error = deserializeJson(doc, payload); 171 | if (error) { 172 | DEBUG_SIMPLE("JSON deserialization failed: " + String(error.c_str())); 173 | return msg; 174 | } 175 | JsonObject sensorObj = doc["z"]; 176 | msg.data.Pi = sensorObj["Pi"] | 0.0; 177 | msg.data.Po = sensorObj["Po"] | 0.0; 178 | msg.data.Pi1 = sensorObj["Pi1"] | 0.0; 179 | msg.data.Pi2 = sensorObj["Pi2"] | 0.0; 180 | msg.data.Pi3 = sensorObj["Pi3"] | 0.0; 181 | msg.data.Po1 = sensorObj["Po1"] | 0.0; 182 | msg.data.Po2 = sensorObj["Po2"] | 0.0; 183 | msg.data.Po3 = sensorObj["Po3"] | 0.0; 184 | msg.data.U1 = sensorObj["U1"] | 0.0; 185 | msg.data.U2 = sensorObj["U2"] | 0.0; 186 | msg.data.U3 = sensorObj["U3"] | 0.0; 187 | msg.data.I1 = sensorObj["I1"] | 0.0; 188 | msg.data.I2 = sensorObj["I2"] | 0.0; 189 | msg.data.I3 = sensorObj["I3"] | 0.0; 190 | msg.data.Ei = sensorObj["Ei"] | 0.0; 191 | msg.data.Eo = sensorObj["Eo"] | 0.0; 192 | msg.data.Ei1 = sensorObj["Ei1"] | 0.0; 193 | msg.data.Ei2 = sensorObj["Ei2"] | 0.0; 194 | msg.data.Eo1 = sensorObj["Eo1"] | 0.0; 195 | msg.data.Eo2 = sensorObj["Eo2"] | 0.0; 196 | msg.data.Q5 = sensorObj["Q5"] | 0.0; 197 | msg.data.Q6 = sensorObj["Q6"] | 0.0; 198 | msg.data.Q7 = sensorObj["Q7"] | 0.0; 199 | msg.data.Q8 = sensorObj["Q8"] | 0.0; 200 | DEBUG_INTERMEDIATE("JSON parsed into SensorMessage."); 201 | // Update the MQTT message watchdog timer. 202 | lastMQTTMessageTime = millis(); 203 | return msg; 204 | } 205 | 206 | // Processes the received MQTT buffer (RTOS-adapted). 207 | void processBuffer_Rtos(WiFiClient &client, uint8_t *buffer, int len) { 208 | int index = 0; 209 | DEBUG_EXTENDEDF("Processing buffer of length: %d\n", len); 210 | while (index < len) { 211 | if (len - index < 2) { 212 | DEBUG_EXTENDED("Error: Not enough bytes for fixed header and remaining length"); 213 | break; 214 | } 215 | uint8_t header = buffer[index]; 216 | uint8_t packetType = header >> 4; 217 | int remainingLength = 0; 218 | int rlBytes = 0; 219 | if (!decodeRemainingLength(buffer + index + 1, len - (index + 1), remainingLength, rlBytes)) { 220 | DEBUG_EXTENDED("Error: Malformed remaining length field"); 221 | break; 222 | } 223 | int fixedHeaderSize = 1 + rlBytes; 224 | int packetTotalLength = fixedHeaderSize + remainingLength; 225 | if (index + packetTotalLength > len) { 226 | DEBUG_EXTENDED("Error: Incomplete packet received"); 227 | break; 228 | } 229 | // Process PUBLISH packets (packet type 3) 230 | if (packetType == 3 && remainingLength >= 2) { 231 | int topicLengthIndex = index + fixedHeaderSize; 232 | uint16_t topicLength = (buffer[topicLengthIndex] << 8) | buffer[topicLengthIndex + 1]; 233 | if (remainingLength >= 2 + topicLength) { 234 | String topic = String((char *)(buffer + topicLengthIndex + 2), topicLength); 235 | DEBUG_SIMPLE("Publish topic: " + topic); 236 | DEBUG_INTERMEDIATEF("Packet length: %d\n", packetTotalLength); 237 | uint8_t qos = (header & 0x06) >> 1; 238 | int extraBytes = (qos > 0) ? 2 : 0; 239 | int payloadStart = index + fixedHeaderSize + 2 + topicLength + extraBytes; 240 | int payloadLength = packetTotalLength - (fixedHeaderSize + 2 + topicLength + extraBytes); 241 | if (payloadLength >= 0) { 242 | String payload = String((char *)(buffer + payloadStart), payloadLength); 243 | DEBUG_SIMPLE("Payload: " + payload); 244 | // Parse the JSON payload into a SensorMessage. 245 | SensorMessage sensorMsg = parseSensorMessage(payload); 246 | // Send the SensorMessage to Task2 via the queue. 247 | if (xQueueSend(sensorMsgQueue, &sensorMsg, 0) != pdPASS) { 248 | DEBUG_EXTENDED("Failed to send SensorMessage to queue."); 249 | displayError("Queue full!"); 250 | } else { 251 | DEBUG_INTERMEDIATE("SensorMessage sent to queue."); 252 | // Immediately update OLED with MQTT data (using default RSSI and SNR values). 253 | displaySensorData(sensorMsg); 254 | } 255 | } 256 | } 257 | } 258 | // Handle CONNECT packets (packet type 1) 259 | else if (packetType == 1) { 260 | uint8_t connack[] = { 0x20, 0x02, 0x00, 0x00 }; 261 | client.write(connack, sizeof(connack)); 262 | DEBUG_SIMPLE("Processed CONNECT packet, sent CONNACK."); 263 | } 264 | // Handle PINGREQ packets (packet type 12) 265 | else if (packetType == 12) { 266 | uint8_t pingresp[] = { 0xD0, 0x00 }; 267 | client.write(pingresp, sizeof(pingresp)); 268 | DEBUG_SIMPLE("Processed PINGREQ, sent PINGRESP."); 269 | } 270 | index += packetTotalLength; 271 | DEBUG_EXTENDEDF("Moving to next packet at index: %d\n", index); 272 | } 273 | } 274 | 275 | // Processes an MQTT client connection using the RTOS-adapted functions. 276 | void processMQTTClient_Rtos(WiFiClient &client) { 277 | DEBUG_SIMPLE("New MQTT client connected"); 278 | lastMQTTClientConnect = millis(); // Update connection time 279 | while (client.connected()) { 280 | if (client.available()) { 281 | uint8_t buffer[MAX_PACKET_SIZE] = { 0 }; 282 | int len = client.readBytes(buffer, sizeof(buffer)); 283 | if (len > 0) { 284 | DEBUG_INTERMEDIATEF("Received MQTT packet of length: %d\n", len); 285 | processBuffer_Rtos(client, buffer, len); 286 | } else { 287 | DEBUG_EXTENDED("Received zero-length packet, skipping."); 288 | } 289 | } 290 | delay(10); // Short delay to yield 291 | } 292 | client.stop(); 293 | DEBUG_SIMPLE("MQTT client disconnected"); 294 | } 295 | 296 | // ----- CRC16 Calculation Function ----- 297 | // Implements the CRC-CCITT algorithm (polynomial 0x1021, initial value 0xFFFF) 298 | uint16_t crc16(const uint8_t *data, size_t length) { 299 | uint16_t crc = 0xFFFF; 300 | for (size_t i = 0; i < length; i++) { 301 | crc ^= ((uint16_t)data[i]) << 8; 302 | for (uint8_t j = 0; j < 8; j++) { 303 | if (crc & 0x8000) 304 | crc = (crc << 1) ^ 0x1021; 305 | else 306 | crc <<= 1; 307 | } 308 | } 309 | return crc; 310 | } 311 | 312 | // Sends sensor message over LoRa with CRC16 checksum. 313 | void sendSensorMessageLoRaBinary(const SensorMessage &msg) { 314 | SensorMessagePacked packed; 315 | // Copy sensor data into the packed structure. 316 | packed.data.Pi = msg.data.Pi; 317 | packed.data.Po = msg.data.Po; 318 | packed.data.Pi1 = msg.data.Pi1; 319 | packed.data.Pi2 = msg.data.Pi2; 320 | packed.data.Pi3 = msg.data.Pi3; 321 | packed.data.Po1 = msg.data.Po1; 322 | packed.data.Po2 = msg.data.Po2; 323 | packed.data.Po3 = msg.data.Po3; 324 | packed.data.I1 = msg.data.I1; 325 | packed.data.I2 = msg.data.I2; 326 | packed.data.I3 = msg.data.I3; 327 | packed.data.Ei = msg.data.Ei; 328 | packed.data.Eo = msg.data.Eo; 329 | packed.data.Ei1 = msg.data.Ei1; 330 | packed.data.Ei2 = msg.data.Ei2; 331 | packed.data.Eo1 = msg.data.Eo1; 332 | packed.data.Eo2 = msg.data.Eo2; 333 | packed.data.Q5 = msg.data.Q5; 334 | packed.data.Q6 = msg.data.Q6; 335 | packed.data.Q7 = msg.data.Q7; 336 | packed.data.Q8 = msg.data.Q8; 337 | 338 | // Calculate CRC16 over the sensor data (excluding the crc16 field). 339 | uint8_t *dataBytes = (uint8_t *)&packed.data; 340 | size_t dataSize = sizeof(packed.data) - sizeof(packed.data.parity); 341 | uint16_t crc = crc16(dataBytes, dataSize); 342 | packed.data.parity = crc; 343 | 344 | // Transmit the packed structure over LoRa. 345 | LoRa.beginPacket(); 346 | LoRa.write((uint8_t *)&packed, sizeof(packed)); 347 | LoRa.endPacket(); 348 | DEBUG_SIMPLEF("LoRa binary message with CRC16 sent, size: %d\n", sizeof(packed)); 349 | } 350 | 351 | // ----- sendWithAck() Function ----- 352 | // Sends a sensor message via LoRa and waits for an ACK. 353 | // Retransmits up to MAX_RETRIES if no ACK is received. 354 | bool sendWithAck(const SensorMessage &msg) { 355 | int retries = MAX_RETRIES; 356 | bool ackReceived = false; 357 | while (retries > 0 && !ackReceived) { 358 | sendSensorMessageLoRaBinary(msg); 359 | DEBUG_SIMPLE("LoRa message sent. Waiting for ACK..."); 360 | // Put LoRa into receive mode. 361 | LoRa.receive(); 362 | unsigned long startTime = millis(); 363 | bool ackThisTrial = false; 364 | while (millis() - startTime < ACK_TIMEOUT) { 365 | int packetSize = LoRa.parsePacket(); 366 | if (packetSize > 0) { 367 | uint8_t ackBuffer[packetSize]; 368 | int bytesRead = LoRa.readBytes(ackBuffer, packetSize); 369 | if (bytesRead > 0 && ackBuffer[0] == ACK_BYTE) { 370 | // Read RSSI and SNR values from the LoRa module. 371 | int rssi = LoRa.packetRssi(); 372 | float snr = LoRa.packetSnr(); 373 | DEBUG_SIMPLE("ACK received."); 374 | // Update the ACK watchdog timer. 375 | lastAckTime = millis(); 376 | // Display sensor data along with the actual RSSI and SNR values. 377 | displaySensorData(msg, rssi, snr); 378 | ackReceived = true; 379 | ackThisTrial = true; 380 | break; 381 | } 382 | } 383 | vTaskDelay(10 / portTICK_PERIOD_MS); 384 | } 385 | if (!ackThisTrial) { 386 | DEBUG_SIMPLE("No ACK received, retransmitting..."); 387 | retries--; 388 | } 389 | } 390 | if (!ackReceived) { 391 | DEBUG_SIMPLE("Message not acknowledged after maximum retries."); 392 | } 393 | return ackReceived; 394 | } 395 | 396 | // ----- Watchdog Task ----- 397 | // Monitors the time since the last MQTT message and the last ACK. 398 | // Reboots the ESP32 if either exceeds the defined timeouts. 399 | void WatchdogTask(void *parameter) { 400 | for (;;) { 401 | unsigned long now = millis(); 402 | if (now - lastMQTTMessageTime > MQTT_MSG_TIMEOUT) { 403 | rebootSystem("No MQTT message received in 1 minute"); 404 | } 405 | if (now - lastAckTime > ACK_WD_TIMEOUT) { 406 | rebootSystem("No ACK received in 1 minute"); 407 | } 408 | // Check every 5 seconds. 409 | vTaskDelay(5000 / portTICK_PERIOD_MS); 410 | } 411 | } 412 | 413 | // ----- Hardware Initialization ----- 414 | bool initializeHardware() { 415 | // Initialize OLED display. 416 | if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { 417 | DEBUG_SIMPLE("SSD1306 allocation failed"); 418 | return false; 419 | } 420 | display.clearDisplay(); 421 | display.display(); 422 | display.setTextSize(1); 423 | display.setTextColor(SSD1306_WHITE); 424 | display.setCursor(0, 0); 425 | display.println("OLED init OK!"); 426 | display.display(); 427 | DEBUG_INTERMEDIATE("OLED initialized successfully."); 428 | 429 | // Configure the ESP32 as an Access Point. 430 | IPAddress local_ip(192, 168, 4, 1); 431 | IPAddress gateway(192, 168, 4, 1); 432 | IPAddress subnet(255, 255, 255, 0); 433 | WiFi.softAPConfig(local_ip, gateway, subnet); 434 | WiFi.softAP(ssid, password); 435 | DEBUG_SIMPLEF("Access Point started, IP: %s", WiFi.softAPIP().toString().c_str()); 436 | 437 | // Start the MQTT server. 438 | mqttServer.begin(); 439 | DEBUG_SIMPLE("MQTT Broker started on port 1883"); 440 | 441 | // Initialize LoRa module. 442 | SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); 443 | LoRa.setPins(LORA_CS, LORA_RST, LORA_DIO0); 444 | if (!LoRa.begin(LORA_FREQUENCY)) { 445 | DEBUG_SIMPLE("Starting LoRa failed!"); 446 | display.clearDisplay(); 447 | display.setCursor(0, 0); 448 | display.println("LoRa init fail!"); 449 | display.display(); 450 | return false; 451 | } 452 | LoRa.setSpreadingFactor(LORA_SF); 453 | LoRa.setTxPower(20); 454 | DEBUG_SIMPLE("LoRa init OK!"); 455 | display.clearDisplay(); 456 | display.setCursor(0, 0); 457 | display.println("LoRa init OK!"); 458 | display.display(); 459 | return true; 460 | } 461 | 462 | void rebootSystem(String reason) { 463 | DEBUG_SIMPLE("Critical error: " + reason); 464 | displayError(reason); 465 | delay(3000); 466 | ESP.restart(); 467 | } 468 | 469 | // ----- RTOS Tasks ----- 470 | // Task 1: MQTT Broker and OLED update task. 471 | // Listens for MQTT clients, processes packets, and sends parsed SensorMessage structures via a queue. 472 | void Task1_MQTT(void *parameter) { 473 | for (;;) { 474 | WiFiClient client = mqttServer.available(); 475 | if (client) { 476 | DEBUG_SIMPLE("MQTT client available, processing connection..."); 477 | processMQTTClient_Rtos(client); 478 | lastMQTTClientConnect = millis(); 479 | DEBUG_EXTENDED("Updated lastMQTTClientConnect after client processing."); 480 | } 481 | // Reboot if no client has connected within the defined timeout. 482 | if (millis() - lastMQTTClientConnect > MQTT_CONNECT_TIMEOUT) { 483 | rebootSystem("No MQTT client connected within 30 seconds"); 484 | } 485 | vTaskDelay(10 / portTICK_PERIOD_MS); // Yield to other tasks. 486 | } 487 | } 488 | 489 | // Task 2: LoRa Transmission Task. 490 | // Waits for a SensorMessage from Task 1 via the queue and sends it over LoRa using sendWithAck. 491 | void Task2_LoRa(void *parameter) { 492 | SensorMessage sensorMsg; 493 | for (;;) { 494 | if (xQueueReceive(sensorMsgQueue, &sensorMsg, portMAX_DELAY) == pdPASS) { 495 | DEBUG_SIMPLE("Received SensorMessage from queue, transmitting via LoRa."); 496 | bool ackOk = sendWithAck(sensorMsg); 497 | if (ackOk) { 498 | DEBUG_INTERMEDIATE("LoRa transmission completed successfully."); 499 | } else { 500 | DEBUG_SIMPLE("LoRa transmission failed after retries."); 501 | } 502 | } 503 | vTaskDelay(10 / portTICK_PERIOD_MS); // Yield to other tasks. 504 | } 505 | } 506 | 507 | void setup() { 508 | Serial.begin(115200); 509 | while (!Serial) { 510 | ; // Wait for Serial to initialize 511 | } 512 | Serial.println("Starting ESP32 RTOS sketch with MQTT, LoRa, and Watchdog tasks with extended debug..."); 513 | Serial.println(); 514 | Serial.println("---------------------------------------------------------------"); 515 | 516 | Serial.print("File: "); 517 | Serial.println(__SHORT_FILE__); 518 | Serial.println("---------------------------------------------------------------"); 519 | Serial.println(); 520 | lastMQTTClientConnect = millis(); 521 | lastMQTTMessageTime = millis(); // Initialize watchdog timers. 522 | lastAckTime = millis(); 523 | 524 | if (!initializeHardware()) { 525 | Serial.println("Hardware initialization failed. Halting."); 526 | while (true) { 527 | vTaskDelay(1000 / portTICK_PERIOD_MS); 528 | } 529 | } 530 | 531 | // Create a queue for SensorMessage transfers between tasks. 532 | sensorMsgQueue = xQueueCreate(10, sizeof(SensorMessage)); 533 | if (sensorMsgQueue == NULL) { 534 | Serial.println("Failed to create sensor message queue."); 535 | while (true) { 536 | vTaskDelay(1000 / portTICK_PERIOD_MS); 537 | } 538 | } 539 | 540 | // Create RTOS Task 1: MQTT Broker and OLED update. 541 | xTaskCreate(Task1_MQTT, "Task1_MQTT", 8192, NULL, 1, &Task1_MQTT_Handle); 542 | 543 | // Create RTOS Task 2: LoRa Transmission. 544 | xTaskCreate(Task2_LoRa, "Task2_LoRa", 8192, NULL, 1, &Task2_LoRa_Handle); 545 | 546 | // Create the Watchdog Task. 547 | xTaskCreate(WatchdogTask, "WatchdogTask", 4096, NULL, 1, &Watchdog_Handle); 548 | } 549 | 550 | void loop() { 551 | vTaskDelay(1000 / portTICK_PERIOD_MS); // Idle loop 552 | } 553 | -------------------------------------------------------------------------------- /gplug_AP_LORAWAN_V3/gplug_AP_LORAWAN_V3.ino: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | // ----- Sensor Data Structures ----- 10 | // Pi, Po, Ei, Eo, Q5, Q6, Q7, Q8, I1, I2, I3, Ei1, Ei2, Eo1, Eo2 11 | typedef struct { 12 | struct { 13 | float Pi; 14 | float Po; 15 | float Ei; 16 | float Eo; 17 | float Q5; 18 | float Q6; 19 | float Q7; 20 | float Q8; 21 | float I1; 22 | float I2; 23 | float I3; 24 | float Ei1; 25 | float Ei2; 26 | float Eo1; 27 | float Eo2; 28 | } data; 29 | } SensorMessage; 30 | 31 | typedef struct __attribute__((packed)) { 32 | struct { 33 | float Pi; 34 | float Po; 35 | float Ei; 36 | float Eo; 37 | float Q5; 38 | float Q6; 39 | float Q7; 40 | float Q8; 41 | float I1; 42 | float I2; 43 | float I3; 44 | float Ei1; 45 | float Ei2; 46 | float Eo1; 47 | float Eo2; 48 | } data; 49 | } SensorMessageLoRaWAN; 50 | 51 | // ----- Global variable for message counting. 52 | volatile int messageCount = 0; 53 | 54 | // ----- Timeout Definitions ----- 55 | #define MQTT_MSG_TIMEOUT 120000 // 2 minutes timeout for MQTT messages 56 | // Messages before transmission 57 | #define NUMBER_AVERAGED_MESSAGES 6 58 | 59 | #define DEBUG_LEVEL 2 60 | #define DEVICE 1 // 1 is productive, 2 is test 61 | 62 | // Uncomment the following line to enable fake data injection for testing. 63 | // #define SIMULATE_FAKE_MQTT 64 | 65 | 66 | // ----- Three-Level Debugging Macros ----- 67 | #if DEBUG_LEVEL >= 1 68 | #define DEBUG_ERROR(x) Serial.println(x) 69 | #define DEBUG_ERRORF(fmt, ...) Serial.printf(fmt, ##__VA_ARGS__) 70 | #else 71 | #define DEBUG_ERROR(x) 72 | #define DEBUG_ERRORF(fmt, ...) 73 | #endif 74 | 75 | #if DEBUG_LEVEL >= 2 76 | #define DEBUG_INFO(x) Serial.println(x) 77 | #define DEBUG_INFOF(fmt, ...) Serial.printf(fmt, ##__VA_ARGS__) 78 | #else 79 | #define DEBUG_INFO(x) 80 | #define DEBUG_INFOF(fmt, ...) 81 | #endif 82 | 83 | #if DEBUG_LEVEL >= 3 84 | #define DEBUG_DEBUG(x) Serial.println(x) 85 | #define DEBUG_DEBUGF(fmt, ...) Serial.printf(fmt, ##__VA_ARGS__) 86 | #else 87 | #define DEBUG_DEBUG(x) 88 | #define DEBUG_DEBUGF(fmt, ...) 89 | #endif 90 | 91 | // ----- LoRa (now LoRaWAN) Settings ----- 92 | #define LORA_SCK 5 93 | #define LORA_MISO 19 94 | #define LORA_MOSI 27 95 | #define LORA_CS 18 96 | #define LORA_RST 23 97 | #define LORA_DIO0 26 98 | #define LORA_DIO1 33 99 | 100 | // ----- OLED Display Settings ----- 101 | #define SCREEN_WIDTH 128 // OLED display width, in pixels 102 | #define SCREEN_HEIGHT 64 // OLED display height, in pixels 103 | #define OLED_RESET -1 // Reset pin (or -1 if sharing Arduino reset pin) 104 | Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET); 105 | 106 | // ----- WiFi & MQTT Server Settings ----- 107 | const char *ssid = "ESP32_AP"; 108 | const char *password = "esp32password"; 109 | WiFiServer mqttServer(1883); // Minimal MQTT broker on port 1883 110 | const size_t MAX_PACKET_SIZE = 1024; 111 | 112 | // ----- Global Variables for Watchdog ----- 113 | unsigned long lastMQTTMessageTime = 0; 114 | unsigned long lastMQTTClientConnect = 0; 115 | 116 | // ----- RTOS Task Handles ----- 117 | TaskHandle_t Task1_MQTT_Handle = NULL; // MQTT broker & OLED task 118 | TaskHandle_t Task2_LoRa_Handle = NULL; // LoRaWAN transmission task 119 | TaskHandle_t Watchdog_Handle = NULL; // Watchdog task 120 | 121 | // ----- Inter-Task Communication ----- 122 | QueueHandle_t sensorMsgQueue; 123 | 124 | // This function returns a SensorMessage with stable fake data between 1 and 100000. 125 | SensorMessage generateFakeSensorMessage() { 126 | SensorMessage msg = {}; 127 | msg.data.Pi = 12345; // Example stable value 128 | msg.data.Po = 23456; 129 | msg.data.Ei = 34567; 130 | msg.data.Eo = 45678; 131 | msg.data.Q5 = 56789; 132 | msg.data.Q6 = 67890; 133 | msg.data.Q7 = 78901; 134 | msg.data.Q8 = 89012; 135 | msg.data.I1 = 90123; 136 | msg.data.I2 = 81234; 137 | msg.data.I3 = 72345; 138 | msg.data.Ei1 = 63456; 139 | msg.data.Ei2 = 54567; 140 | msg.data.Eo1 = 45678; 141 | msg.data.Eo2 = 36789; 142 | return msg; 143 | } 144 | 145 | // ----- OLED Display Helper Functions ----- 146 | 147 | // Clears the region of the specified line (assumes 10-pixel high lines) and displays the given message. 148 | void displayStatus(const String &message, int line = 1) { 149 | // Clear the area for the given line 150 | display.fillRect(0, line * 10, SCREEN_WIDTH, 10, SSD1306_BLACK); 151 | display.setCursor(0, line * 10); 152 | display.println(message); 153 | display.display(); 154 | DEBUG_DEBUG("OLED Status (line " + String(line) + "): " + message); 155 | } 156 | 157 | // Clears only the area reserved for the message count (line 50) and displays the updated count. 158 | void updateMessageCountDisplay(int count) { 159 | // Clear only the area reserved for the message count. 160 | display.fillRect(0, 50, SCREEN_WIDTH, 10, SSD1306_BLACK); 161 | display.setCursor(0, 50); 162 | display.println("Msg#:" + String(count)); 163 | display.display(); 164 | } 165 | 166 | // Clears only the sensor data region (assumed from y=0 to y=40) and then displays the sensor values. 167 | void displaySensorData(const SensorMessage &msg) { 168 | // Clear only the sensor data region. 169 | display.fillRect(0, 0, SCREEN_WIDTH, 40, SSD1306_BLACK); 170 | 171 | display.setTextSize(1); // Use text size 1 for more lines 172 | display.setCursor(0, 0); 173 | 174 | // Display the selected sensor fields. 175 | display.println("Pi:" + String(msg.data.Pi, 0) + " Po:" + String(msg.data.Po, 0)); 176 | display.println("Ei:" + String(msg.data.Ei, 0) + " Eo:" + String(msg.data.Eo, 0)); 177 | display.println("Ei1:" + String(msg.data.Ei1, 0) + " Ei2:" + String(msg.data.Ei2, 0)); 178 | display.println("Eo1:" + String(msg.data.Eo1, 0) + " Eo2:" + String(msg.data.Eo2, 0)); 179 | 180 | display.display(); // Update the physical display 181 | DEBUG_DEBUG("Task1: OLED updated with sensor data."); 182 | } 183 | 184 | // Clears only the error display region (assumed to be y=40 to y=50) and displays the error message. 185 | void displayError(const String &message) { 186 | display.fillRect(0, 40, SCREEN_WIDTH, 10, SSD1306_BLACK); 187 | display.setCursor(0, 40); 188 | display.println(message); 189 | display.display(); 190 | DEBUG_ERRORF("Task1: Error displayed on OLED: %s\n", message.c_str()); 191 | } 192 | 193 | // ----- MQTT Packet Processing Functions ----- 194 | bool decodeRemainingLength(uint8_t *buf, int available, int &remainingLength, int &rlBytes) { 195 | remainingLength = 0; 196 | rlBytes = 0; 197 | int multiplier = 1; 198 | uint8_t digit; 199 | do { 200 | if (rlBytes >= available || rlBytes > 4) { 201 | DEBUG_DEBUGF("Task1: decodeRemainingLength: Not enough bytes or too many iterations. Available: %d, rlBytes: %d\n", available, rlBytes); 202 | return false; 203 | } 204 | digit = buf[rlBytes++]; 205 | remainingLength += (digit & 127) * multiplier; 206 | multiplier *= 128; 207 | } while (digit & 0x80); 208 | DEBUG_DEBUGF("Task1: Decoded remaining length: %d, bytes used: %d\n", remainingLength, rlBytes); 209 | return true; 210 | } 211 | 212 | SensorMessage parseSensorMessage(const String &payload) { 213 | SensorMessage msg = {}; // Zero-initialize all fields. 214 | 215 | const int MIN_VALID_PAYLOAD_LENGTH = 30; 216 | if (payload.length() < MIN_VALID_PAYLOAD_LENGTH) { 217 | DEBUG_DEBUGF("Task1: Payload too short (%d characters), skipping: %s\n", payload.length(), payload.c_str()); 218 | return msg; 219 | } 220 | 221 | StaticJsonDocument<1024> doc; 222 | DeserializationError error = deserializeJson(doc, payload); 223 | if (error) { 224 | DEBUG_ERRORF("Task1: JSON deserialization failed: %s | Payload: %s\n", error.c_str(), payload.c_str()); 225 | return msg; 226 | } 227 | JsonObject sensorObj = doc["z"]; 228 | msg.data.Pi = (sensorObj["Pi"] | 0.0) * 1000; 229 | msg.data.Po = (sensorObj["Po"] | 0.0) * 1000; 230 | msg.data.Ei = sensorObj["Ei"] | 0.0; 231 | msg.data.Eo = sensorObj["Eo"] | 0.0; 232 | msg.data.Q5 = sensorObj["Q5"] | 0.0; 233 | msg.data.Q6 = sensorObj["Q6"] | 0.0; 234 | msg.data.Q7 = sensorObj["Q7"] | 0.0; 235 | msg.data.Q8 = sensorObj["Q8"] | 0.0; 236 | msg.data.I1 = sensorObj["I1"] | 0.0; 237 | msg.data.I2 = sensorObj["I2"] | 0.0; 238 | msg.data.I3 = sensorObj["I3"] | 0.0; 239 | msg.data.Ei1 = sensorObj["Ei1"] | 0.0; 240 | msg.data.Ei2 = sensorObj["Ei2"] | 0.0; 241 | msg.data.Eo1 = sensorObj["Eo1"] | 0.0; 242 | msg.data.Eo2 = sensorObj["Eo2"] | 0.0; 243 | 244 | // Update the last MQTT message time. 245 | lastMQTTMessageTime = millis(); 246 | 247 | DEBUG_DEBUGF("Task1: MQTT message parsed: Pi=%.2f, Po=%.2f, Ei=%.2f, Eo=%.2f, Q5=%.2f, Q6=%.2f, Q7=%.2f, Q8=%.2f, I1=%.2f, I2=%.2f, I3=%.2f, Ei1=%.2f, Ei2=%.2f, Eo1=%.2f, Eo2=%.2f\n", 248 | msg.data.Pi, msg.data.Po, msg.data.Ei, msg.data.Eo, 249 | msg.data.Q5, msg.data.Q6, msg.data.Q7, msg.data.Q8, 250 | msg.data.I1, msg.data.I2, msg.data.I3, 251 | msg.data.Ei1, msg.data.Ei2, msg.data.Eo1, msg.data.Eo2); 252 | return msg; 253 | } 254 | 255 | // ----- Accumulate & Average Sensor Messages ----- 256 | void accumulateSensorMessage(const SensorMessage &msg) { 257 | static SensorMessage accumMsg = {}; // Zero-initialized accumulator. 258 | 259 | // Accumulate all sensor fields. 260 | accumMsg.data.Pi += msg.data.Pi; 261 | accumMsg.data.Po += msg.data.Po; 262 | accumMsg.data.Ei += msg.data.Ei; 263 | accumMsg.data.Eo += msg.data.Eo; 264 | accumMsg.data.Q5 += msg.data.Q5; 265 | accumMsg.data.Q6 += msg.data.Q6; 266 | accumMsg.data.Q7 += msg.data.Q7; 267 | accumMsg.data.Q8 += msg.data.Q8; 268 | accumMsg.data.I1 += msg.data.I1; 269 | accumMsg.data.I2 += msg.data.I2; 270 | accumMsg.data.I3 += msg.data.I3; 271 | accumMsg.data.Ei1 += msg.data.Ei1; 272 | accumMsg.data.Ei2 += msg.data.Ei2; 273 | accumMsg.data.Eo1 += msg.data.Eo1; 274 | accumMsg.data.Eo2 += msg.data.Eo2; 275 | 276 | messageCount++; 277 | 278 | // Debug: Log the full details of the current message. 279 | DEBUG_INFOF("Task1: Received SensorMessage #%d: Pi=%.2f, Po=%.2f, Ei=%.2f, Eo=%.2f, Q5=%.2f, Q6=%.2f, Q7=%.2f, Q8=%.2f, I1=%.2f, I2=%.2f, I3=%.2f, Ei1=%.2f, Ei2=%.2f, Eo1=%.2f, Eo2=%.2f\n", 280 | messageCount, 281 | msg.data.Pi, msg.data.Po, msg.data.Ei, msg.data.Eo, 282 | msg.data.Q5, msg.data.Q6, msg.data.Q7, msg.data.Q8, 283 | msg.data.I1, msg.data.I2, msg.data.I3, 284 | msg.data.Ei1, msg.data.Ei2, msg.data.Eo1, msg.data.Eo2); 285 | 286 | // Update the OLED message count every time a new MQTT message arrives. 287 | updateMessageCountDisplay(messageCount); 288 | 289 | // Debug: Log the current accumulated values. 290 | DEBUG_DEBUGF("Task1: Accumulated count: %d\n", messageCount); 291 | DEBUG_DEBUGF("Task1: Accumulated values: Pi=%.2f, Po=%.2f, Ei=%.2f, Eo=%.2f, Q5=%.2f, Q6=%.2f, Q7=%.2f, Q8=%.2f, I1=%.2f, I2=%.2f, I3=%.2f, Ei1=%.2f, Ei2=%.2f, Eo1=%.2f, Eo2=%.2f\n", 292 | accumMsg.data.Pi, accumMsg.data.Po, accumMsg.data.Ei, accumMsg.data.Eo, 293 | accumMsg.data.Q5, accumMsg.data.Q6, accumMsg.data.Q7, accumMsg.data.Q8, 294 | accumMsg.data.I1, accumMsg.data.I2, accumMsg.data.I3, 295 | accumMsg.data.Ei1, accumMsg.data.Ei2, accumMsg.data.Eo1, accumMsg.data.Eo2); 296 | 297 | if (messageCount == NUMBER_AVERAGED_MESSAGES) { 298 | SensorMessage avgMsg = {}; // Zero-initialize the averaged message. 299 | avgMsg.data.Pi = accumMsg.data.Pi / NUMBER_AVERAGED_MESSAGES; 300 | avgMsg.data.Po = accumMsg.data.Po / NUMBER_AVERAGED_MESSAGES; 301 | avgMsg.data.Ei = accumMsg.data.Ei / NUMBER_AVERAGED_MESSAGES; 302 | avgMsg.data.Eo = accumMsg.data.Eo / NUMBER_AVERAGED_MESSAGES; 303 | avgMsg.data.Q5 = accumMsg.data.Q5 / NUMBER_AVERAGED_MESSAGES; 304 | avgMsg.data.Q6 = accumMsg.data.Q6 / NUMBER_AVERAGED_MESSAGES; 305 | avgMsg.data.Q7 = accumMsg.data.Q7 / NUMBER_AVERAGED_MESSAGES; 306 | avgMsg.data.Q8 = accumMsg.data.Q8 / NUMBER_AVERAGED_MESSAGES; 307 | avgMsg.data.I1 = accumMsg.data.I1 / NUMBER_AVERAGED_MESSAGES; 308 | avgMsg.data.I2 = accumMsg.data.I2 / NUMBER_AVERAGED_MESSAGES; 309 | avgMsg.data.I3 = accumMsg.data.I3 / NUMBER_AVERAGED_MESSAGES; 310 | avgMsg.data.Ei1 = accumMsg.data.Ei1 / NUMBER_AVERAGED_MESSAGES; 311 | avgMsg.data.Ei2 = accumMsg.data.Ei2 / NUMBER_AVERAGED_MESSAGES; 312 | avgMsg.data.Eo1 = accumMsg.data.Eo1 / NUMBER_AVERAGED_MESSAGES; 313 | avgMsg.data.Eo2 = accumMsg.data.Eo2 / NUMBER_AVERAGED_MESSAGES; 314 | 315 | DEBUG_DEBUGF("Task1: Averaged values to be sent: Pi=%.2f, Po=%.2f, Ei=%.2f, Eo=%.2f, Q5=%.2f, Q6=%.2f, Q7=%.2f, Q8=%.2f, I1=%.2f, I2=%.2f, I3=%.2f, Ei1=%.2f, Ei2=%.2f, Eo1=%.2f, Eo2=%.2f\n", 316 | avgMsg.data.Pi, avgMsg.data.Po, avgMsg.data.Ei, avgMsg.data.Eo, 317 | avgMsg.data.Q5, avgMsg.data.Q6, avgMsg.data.Q7, avgMsg.data.Q8, 318 | avgMsg.data.I1, avgMsg.data.I2, avgMsg.data.I3, 319 | avgMsg.data.Ei1, avgMsg.data.Ei2, avgMsg.data.Eo1, avgMsg.data.Eo2); 320 | 321 | // Send the averaged message to the queue. 322 | if (xQueueSend(sensorMsgQueue, &avgMsg, 0) != pdPASS) { 323 | DEBUG_ERROR("Task1: Error - Queue full! Averaged SensorMessage not sent."); 324 | displayError("Queue full!"); 325 | } else { 326 | DEBUG_DEBUG("Task1: Averaged SensorMessage sent to queue."); 327 | // Call displaySensorData() only with the data sent via LoRaWAN (averaged message). 328 | displaySensorData(avgMsg); 329 | } 330 | // Reset the accumulator. 331 | accumMsg = {}; 332 | messageCount = 0; 333 | } 334 | } 335 | 336 | void processBuffer_Rtos(WiFiClient &client, uint8_t *buffer, int len) { 337 | // Build a raw string from the entire buffer and print it (DEBUG level). 338 | String rawMsg = ""; 339 | for (int i = 0; i < len; i++) { 340 | rawMsg += (char)buffer[i]; 341 | } 342 | DEBUG_DEBUG("Task1: Raw MQTT message: " + rawMsg); 343 | 344 | int index = 0; 345 | DEBUG_DEBUGF("Task1: Processing buffer of length: %d\n", len); 346 | 347 | while (index < len) { 348 | // Ensure at least 2 bytes remain for the fixed header and remaining length. 349 | if (len - index < 2) { 350 | DEBUG_DEBUG("Task1: Error: Not enough bytes for fixed header and remaining length."); 351 | break; 352 | } 353 | 354 | uint8_t header = buffer[index]; 355 | uint8_t packetType = header >> 4; 356 | int remainingLength = 0, rlBytes = 0; 357 | 358 | // Decode the remaining length field. 359 | if (!decodeRemainingLength(buffer + index + 1, len - (index + 1), remainingLength, rlBytes)) { 360 | DEBUG_DEBUG("Task1: Error: Malformed remaining length field."); 361 | break; 362 | } 363 | 364 | int fixedHeaderSize = 1 + rlBytes; 365 | int packetTotalLength = fixedHeaderSize + remainingLength; 366 | 367 | // Ensure the full packet is available. 368 | if (index + packetTotalLength > len) { 369 | DEBUG_DEBUGF("Task1: Incomplete packet received. Discarding remainder of buffer.\n"); 370 | break; 371 | } 372 | 373 | if (packetType == 3 && remainingLength >= 2) { // PUBLISH packet 374 | int topicLengthIndex = index + fixedHeaderSize; 375 | uint16_t topicLength = (buffer[topicLengthIndex] << 8) | buffer[topicLengthIndex + 1]; 376 | 377 | if (remainingLength >= 2 + topicLength) { 378 | String topic = String((char *)(buffer + topicLengthIndex + 2), topicLength); 379 | DEBUG_DEBUG("Task1: Publish topic: " + topic); 380 | 381 | // Extract the portion after the last '/' 382 | String topicSuffix = topic.substring(topic.lastIndexOf("/") + 1); 383 | 384 | if (topicSuffix == "SENSOR") { 385 | uint8_t qos = (header & 0x06) >> 1; 386 | int extraBytes = (qos > 0) ? 2 : 0; 387 | int payloadStart = index + fixedHeaderSize + 2 + topicLength + extraBytes; 388 | int payloadLength = packetTotalLength - (fixedHeaderSize + 2 + topicLength + extraBytes); 389 | 390 | if (payloadLength >= 0) { 391 | String payload = String((char *)(buffer + payloadStart), payloadLength); 392 | // Report the raw payload and message count (expected count = messageCount+1) 393 | DEBUG_INFOF("Task1: Received SensorMessage #%d on topic %s: %s\n", messageCount + 1, topic.c_str(), payload.c_str()); 394 | SensorMessage sensorMsg = parseSensorMessage(payload); 395 | accumulateSensorMessage(sensorMsg); 396 | } else { 397 | DEBUG_DEBUG("Task1: Negative payload length computed."); 398 | } 399 | } else if (topic == "gPlugM/STATE") { 400 | // Report STATE topic messages at debug level. 401 | uint8_t qos = (header & 0x06) >> 1; 402 | int extraBytes = (qos > 0) ? 2 : 0; 403 | int payloadStart = index + fixedHeaderSize + 2 + topicLength + extraBytes; 404 | int payloadLength = packetTotalLength - (fixedHeaderSize + 2 + topicLength + extraBytes); 405 | if (payloadLength >= 0) { 406 | String payload = String((char *)(buffer + payloadStart), payloadLength); 407 | DEBUG_DEBUG("Task1: Received STATE message: " + payload); 408 | } 409 | } else { 410 | DEBUG_DEBUG("Task1: Ignoring topic: " + topic); 411 | } 412 | } else { 413 | DEBUG_DEBUG("Task1: Remaining length less than expected topic length."); 414 | } 415 | } 416 | 417 | else if (packetType == 1) { // CONNECT packet 418 | uint8_t connack[] = { 0x20, 0x02, 0x00, 0x00 }; 419 | client.write(connack, sizeof(connack)); 420 | DEBUG_INFO("Task1: Processed CONNECT packet, sent CONNACK."); 421 | } else if (packetType == 12) { // PINGREQ packet 422 | uint8_t pingresp[] = { 0xD0, 0x00 }; 423 | client.write(pingresp, sizeof(pingresp)); 424 | DEBUG_DEBUG("Task1: Processed PINGREQ, sent PINGRESP."); 425 | } 426 | 427 | index += packetTotalLength; 428 | DEBUG_DEBUGF("Task1: Moving to next packet at index: %d\n", index); 429 | } 430 | } 431 | 432 | void processMQTTClient_Rtos(WiFiClient &client) { 433 | DEBUG_INFO("Task1: New MQTT client connected"); 434 | lastMQTTClientConnect = millis(); 435 | while (client.connected()) { 436 | if (client.available()) { 437 | uint8_t buffer[MAX_PACKET_SIZE] = { 0 }; 438 | int len = client.readBytes(buffer, sizeof(buffer)); 439 | if (len > 0) { 440 | // Convert the raw buffer to a String for display 441 | String rawMsg = ""; 442 | for (int i = 0; i < len; i++) { 443 | rawMsg += (char)buffer[i]; 444 | } 445 | DEBUG_DEBUG("Task1: MQTT raw message: " + rawMsg); 446 | DEBUG_DEBUGF("Task1: Received MQTT packet of length: %d\n", len); 447 | processBuffer_Rtos(client, buffer, len); 448 | } else { 449 | DEBUG_DEBUG("Task1: Received zero-length packet, skipping."); 450 | } 451 | } 452 | vTaskDelay(10 / portTICK_PERIOD_MS); 453 | } 454 | client.stop(); 455 | DEBUG_INFO("Task1: MQTT client disconnected"); 456 | } 457 | 458 | // ----- LoRaWAN (RadioLib) Global Variables and Configuration ----- 459 | SX1276 radio = new Module(LORA_CS, LORA_DIO0, LORA_RST, LORA_DIO1); 460 | const uint32_t uplinkIntervalSeconds = 5UL * 60UL; 461 | 462 | #if DEVICE == 1 463 | #define RADIOLIB_LORAWAN_JOIN_EUI 0x0000000000000000 464 | #ifndef RADIOLIB_LORAWAN_DEV_EUI 465 | #define RADIOLIB_LORAWAN_DEV_EUI 0x70B3D57ED006F35A 466 | #endif 467 | #ifndef RADIOLIB_LORAWAN_APP_KEY 468 | #define RADIOLIB_LORAWAN_APP_KEY 0x30, 0xFC, 0xD1, 0x35, 0x15, 0x98, 0x62, 0x9A, 0xE9, 0x8E, 0xD9, 0xC4, 0x92, 0x28, 0x47, 0x15 469 | #endif 470 | #ifndef RADIOLIB_LORAWAN_NWK_KEY 471 | #define RADIOLIB_LORAWAN_NWK_KEY 0xBF, 0xE4, 0xCE, 0x6A, 0xE1, 0xA7, 0xC1, 0xFE, 0x92, 0x66, 0x0A, 0xA3, 0xD3, 0x79, 0x65, 0xB0 472 | #endif 473 | #endif 474 | 475 | #if DEVICE == 2 476 | #define RADIOLIB_LORAWAN_JOIN_EUI 0x0000000000000000 477 | #ifndef RADIOLIB_LORAWAN_DEV_EUI 478 | #define RADIOLIB_LORAWAN_DEV_EUI 0x70B3D57ED006F8C6 479 | #endif 480 | #ifndef RADIOLIB_LORAWAN_APP_KEY 481 | #define RADIOLIB_LORAWAN_APP_KEY 0x64, 0x3C, 0x5A, 0x25, 0xF8, 0x5A, 0x84, 0xE7, 0xFC, 0xA0, 0xE6, 0xEE, 0x8E, 0xF1, 0xEB, 0x5A 482 | #endif 483 | #ifndef RADIOLIB_LORAWAN_NWK_KEY 484 | #define RADIOLIB_LORAWAN_NWK_KEY 0xBE, 0xE7, 0xA0, 0x57, 0x98, 0x66, 0xDD, 0x45, 0x18, 0x40, 0x59, 0x4D, 0x67, 0x0C, 0xAB, 0xCC 485 | #endif 486 | #endif 487 | 488 | const LoRaWANBand_t Region = EU868; 489 | const uint8_t subBand = 0; 490 | uint64_t joinEUI = RADIOLIB_LORAWAN_JOIN_EUI; 491 | uint64_t devEUI = RADIOLIB_LORAWAN_DEV_EUI; 492 | uint8_t appKey[] = { RADIOLIB_LORAWAN_APP_KEY }; 493 | uint8_t nwkKey[] = { RADIOLIB_LORAWAN_NWK_KEY }; 494 | 495 | LoRaWANNode node(&radio, &Region, subBand); 496 | 497 | String stateDecode(const int16_t result) { 498 | switch (result) { 499 | case RADIOLIB_ERR_NONE: return "ERR_NONE"; 500 | case RADIOLIB_ERR_CHIP_NOT_FOUND: return "ERR_CHIP_NOT_FOUND"; 501 | case RADIOLIB_ERR_PACKET_TOO_LONG: return "ERR_PACKET_TOO_LONG"; 502 | case RADIOLIB_ERR_RX_TIMEOUT: return "ERR_RX_TIMEOUT"; 503 | case RADIOLIB_ERR_CRC_MISMATCH: return "ERR_CRC_MISMATCH"; 504 | case RADIOLIB_ERR_INVALID_BANDWIDTH: return "ERR_INVALID_BANDWIDTH"; 505 | case RADIOLIB_ERR_INVALID_SPREADING_FACTOR: return "ERR_INVALID_SPREADING_FACTOR"; 506 | case RADIOLIB_ERR_INVALID_CODING_RATE: return "ERR_INVALID_CODING_RATE"; 507 | case RADIOLIB_ERR_INVALID_FREQUENCY: return "ERR_INVALID_FREQUENCY"; 508 | case RADIOLIB_ERR_INVALID_OUTPUT_POWER: return "ERR_INVALID_OUTPUT_POWER"; 509 | case RADIOLIB_ERR_NETWORK_NOT_JOINED: return "RADIOLIB_ERR_NETWORK_NOT_JOINED"; 510 | case RADIOLIB_ERR_DOWNLINK_MALFORMED: return "RADIOLIB_ERR_DOWNLINK_MALFORMED"; 511 | case RADIOLIB_ERR_INVALID_REVISION: return "RADIOLIB_ERR_INVALID_REVISION"; 512 | case RADIOLIB_ERR_INVALID_PORT: return "RADIOLIB_ERR_INVALID_PORT"; 513 | case RADIOLIB_ERR_NO_RX_WINDOW: return "RADIOLIB_ERR_NO_RX_WINDOW"; 514 | case RADIOLIB_ERR_INVALID_CID: return "RADIOLIB_ERR_INVALID_CID"; 515 | case RADIOLIB_ERR_UPLINK_UNAVAILABLE: return "RADIOLIB_ERR_UPLINK_UNAVAILABLE"; 516 | case RADIOLIB_ERR_COMMAND_QUEUE_FULL: return "RADIOLIB_ERR_COMMAND_QUEUE_FULL"; 517 | case RADIOLIB_ERR_COMMAND_QUEUE_ITEM_NOT_FOUND: return "RADIOLIB_ERR_COMMAND_QUEUE_ITEM_NOT_FOUND"; 518 | case RADIOLIB_ERR_JOIN_NONCE_INVALID: return "RADIOLIB_ERR_JOIN_NONCE_INVALID"; 519 | case RADIOLIB_ERR_N_FCNT_DOWN_INVALID: return "RADIOLIB_ERR_N_FCNT_DOWN_INVALID"; 520 | case RADIOLIB_ERR_A_FCNT_DOWN_INVALID: return "RADIOLIB_ERR_A_FCNT_DOWN_INVALID"; 521 | case RADIOLIB_ERR_DWELL_TIME_EXCEEDED: return "RADIOLIB_ERR_DWELL_TIME_EXCEEDED"; 522 | case RADIOLIB_ERR_CHECKSUM_MISMATCH: return "RADIOLIB_ERR_CHECKSUM_MISMATCH"; 523 | case RADIOLIB_ERR_NO_JOIN_ACCEPT: return "RADIOLIB_ERR_NO_JOIN_ACCEPT"; 524 | case RADIOLIB_LORAWAN_SESSION_RESTORED: return "RADIOLIB_LORAWAN_SESSION_RESTORED"; 525 | case RADIOLIB_LORAWAN_NEW_SESSION: return "RADIOLIB_LORAWAN_NEW_SESSION"; 526 | case RADIOLIB_ERR_NONCES_DISCARDED: return "RADIOLIB_ERR_NONCES_DISCARDED"; 527 | case RADIOLIB_ERR_SESSION_DISCARDED: return "RADIOLIB_ERR_SESSION_DISCARDED"; 528 | default: return "Unknown error"; 529 | } 530 | } 531 | 532 | void debug(bool failed, const __FlashStringHelper *message, int state, bool halt) { 533 | if (failed) { 534 | Serial.print("Task2: "); 535 | Serial.print(message); 536 | Serial.print(" - "); 537 | Serial.print(stateDecode(state)); 538 | Serial.print(" ("); 539 | Serial.print(state); 540 | Serial.println(")"); 541 | while (halt) { delay(1); } 542 | } 543 | } 544 | 545 | void sendSensorMessageLoRaWAN(const SensorMessage &msg) { 546 | DEBUG_DEBUGF("Task2: SensorMessage before packing: Pi=%.2f, Po=%.2f, Ei=%.2f, Eo=%.2f, Q5=%.2f, Q6=%.2f, Q7=%.2f, Q8=%.2f, I1=%.2f, I2=%.2f, I3=%.2f, Ei1=%.2f, Ei2=%.2f, Eo1=%.2f, Eo2=%.2f\n", 547 | msg.data.Pi, msg.data.Po, msg.data.Ei, msg.data.Eo, 548 | msg.data.Q5, msg.data.Q6, msg.data.Q7, msg.data.Q8, 549 | msg.data.I1, msg.data.I2, msg.data.I3, 550 | msg.data.Ei1, msg.data.Ei2, msg.data.Eo1, msg.data.Eo2); 551 | 552 | // Pack the values into a LoRaWAN packet. 553 | SensorMessageLoRaWAN packed; 554 | packed.data.Pi = msg.data.Pi; 555 | packed.data.Po = msg.data.Po; 556 | packed.data.Ei = msg.data.Ei; 557 | packed.data.Eo = msg.data.Eo; 558 | packed.data.Q5 = msg.data.Q5; 559 | packed.data.Q6 = msg.data.Q6; 560 | packed.data.Q7 = msg.data.Q7; 561 | packed.data.Q8 = msg.data.Q8; 562 | packed.data.I1 = msg.data.I1; 563 | packed.data.I2 = msg.data.I2; 564 | packed.data.I3 = msg.data.I3; 565 | packed.data.Ei1 = msg.data.Ei1; 566 | packed.data.Ei2 = msg.data.Ei2; 567 | packed.data.Eo1 = msg.data.Eo1; 568 | packed.data.Eo2 = msg.data.Eo2; 569 | 570 | DEBUG_INFOF("Task2: Sending LoRaWAN packet with values: Pi=%.2f, Po=%.2f, Ei=%.2f, Eo=%.2f, Q5=%.2f, Q6=%.2f, Q7=%.2f, Q8=%.2f, I1=%.2f, I2=%.2f, I3=%.2f, Ei1=%.2f, Ei2=%.2f, Eo1=%.2f, Eo2=%.2f\n", 571 | packed.data.Pi, packed.data.Po, packed.data.Ei, packed.data.Eo, 572 | packed.data.Q5, packed.data.Q6, packed.data.Q7, packed.data.Q8, 573 | packed.data.I1, packed.data.I2, packed.data.I3, 574 | packed.data.Ei1, packed.data.Ei2, packed.data.Eo1, packed.data.Eo2); 575 | 576 | int16_t state = node.sendReceive((uint8_t *)&packed, sizeof(packed)); 577 | int16_t rssi = radio.getRSSI(); 578 | String rssiMessage = "RSSI: " + String(rssi); 579 | displayStatus(rssiMessage, 4); 580 | debug(state < RADIOLIB_ERR_NONE, F("Error in sendReceive"), state, false); 581 | if (state > 0) { 582 | Serial.println(F("Task2: Downlink received")); 583 | } else { 584 | DEBUG_DEBUG("Task2: No downlink received"); 585 | } 586 | DEBUG_DEBUG("Task2: LoRa message sent"); 587 | } 588 | 589 | void WatchdogTask(void *parameter) { 590 | for (;;) { 591 | unsigned long now = millis(); 592 | DEBUG_DEBUGF("Watchdog: Checking MQTT message timeout... %lu ms\n", now - lastMQTTMessageTime); 593 | if (now - lastMQTTMessageTime > MQTT_MSG_TIMEOUT) { 594 | // Critical error: no MQTT message received in timeout period. 595 | DEBUG_ERROR("Task1: Critical error - No MQTT message received in 2 minutes"); 596 | displayError("No MQTT msg!"); 597 | vTaskDelay(3000); 598 | ESP.restart(); 599 | } 600 | vTaskDelay(5000 / portTICK_PERIOD_MS); 601 | } 602 | } 603 | 604 | bool initializeHardware() { 605 | if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { 606 | DEBUG_ERROR("Task1: SSD1306 allocation failed"); 607 | return false; 608 | } 609 | display.clearDisplay(); 610 | display.display(); 611 | display.setTextSize(1); 612 | display.setTextColor(SSD1306_WHITE); 613 | display.setCursor(0, 0); 614 | display.println("OLED init OK!"); 615 | display.display(); 616 | DEBUG_INFO("Task1: OLED initialized successfully."); 617 | 618 | IPAddress local_ip(192, 168, 4, 1); 619 | IPAddress gateway(192, 168, 4, 1); 620 | IPAddress subnet(255, 255, 255, 0); 621 | WiFi.softAPConfig(local_ip, gateway, subnet); 622 | WiFi.softAP(ssid, password); 623 | DEBUG_INFOF("Task1: Access Point started, IP: %s\n", WiFi.softAPIP().toString().c_str()); 624 | displayStatus("WiFi started!", 1); 625 | 626 | mqttServer.begin(); 627 | DEBUG_INFO("Task1: MQTT Broker started on port 1883"); 628 | 629 | return true; 630 | } 631 | 632 | #ifdef SIMULATE_FAKE_MQTT 633 | void Task1_MQTT(void *parameter) { 634 | for (;;) { 635 | // Generate a fake sensor message with stable data. 636 | SensorMessage fakeMsg = generateFakeSensorMessage(); 637 | accumulateSensorMessage(fakeMsg); 638 | // You can adjust the delay to simulate message frequency. 639 | vTaskDelay(1000 / portTICK_PERIOD_MS); // Inject every second 640 | } 641 | } 642 | #else 643 | void Task1_MQTT(void *parameter) { 644 | for (;;) { 645 | WiFiClient client = mqttServer.available(); 646 | if (client) { 647 | DEBUG_INFO("Task1: MQTT client available, processing connection..."); 648 | displayStatus("MQTT broker connected", 3); 649 | processMQTTClient_Rtos(client); 650 | lastMQTTClientConnect = millis(); 651 | DEBUG_INFO("Task1: Updated lastMQTTClientConnect after client processing."); 652 | } 653 | vTaskDelay(10 / portTICK_PERIOD_MS); 654 | } 655 | } 656 | #endif 657 | 658 | 659 | void Task2_LoRa(void *parameter) { 660 | Serial.println(F("Task2: Initializing LoRa radio")); 661 | SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS); 662 | int16_t state = radio.begin(); 663 | debug(state != RADIOLIB_ERR_NONE, F("Radio initialization failed"), state, false); 664 | 665 | Serial.println(F("Task2: Setting up OTAA session")); 666 | state = node.beginOTAA(joinEUI, devEUI, nwkKey, appKey); 667 | debug(state != RADIOLIB_ERR_NONE, F("Node initialization failed"), state, false); 668 | 669 | Serial.println(F("Task2: Joining the LoRaWAN Network")); 670 | while (true) { 671 | state = node.activateOTAA(); 672 | if (state == RADIOLIB_LORAWAN_NEW_SESSION) { 673 | int16_t rssi = radio.getRSSI(); 674 | Serial.print(F("Task2: LoRaWAN Joined Successfully! RSSI: ")); 675 | Serial.println(rssi); 676 | displayStatus("Joined! RSSI: " + String(rssi), 2); 677 | break; 678 | } else { 679 | Serial.print(F("Task2: LoRaWAN join failed with state ")); 680 | Serial.println(state); 681 | displayStatus("LoRa join error!", 2); 682 | vTaskDelay(5000 / portTICK_PERIOD_MS); 683 | } 684 | } 685 | 686 | SensorMessage sensorMsgForLoRa; 687 | for (;;) { 688 | if (xQueueReceive(sensorMsgQueue, &sensorMsgForLoRa, portMAX_DELAY) == pdPASS) { 689 | DEBUG_DEBUG("Task2: Received SensorMessage from queue, transmitting via LoRaWAN."); 690 | sendSensorMessageLoRaWAN(sensorMsgForLoRa); 691 | } 692 | vTaskDelay(10 / portTICK_PERIOD_MS); 693 | } 694 | } 695 | 696 | void setup() { 697 | Serial.begin(115200); 698 | while (!Serial) { ; } 699 | Serial.println("Starting ESP32 RTOS sketch with MQTT, LoRaWAN, and Watchdog tasks..."); 700 | Serial.println("---------------------------------------------------------------"); 701 | Serial.print("File: "); 702 | Serial.println(__FILE__); 703 | Serial.println("---------------------------------------------------------------"); 704 | 705 | lastMQTTClientConnect = millis(); 706 | lastMQTTMessageTime = millis(); 707 | 708 | if (!initializeHardware()) { 709 | Serial.println("Task1: Hardware initialization failed. Halting."); 710 | while (true) { vTaskDelay(1000 / portTICK_PERIOD_MS); } 711 | } 712 | 713 | sensorMsgQueue = xQueueCreate(10, sizeof(SensorMessage)); 714 | if (sensorMsgQueue == NULL) { 715 | DEBUG_ERROR("Task1: Failed to create sensor message queue."); 716 | while (true) { vTaskDelay(1000 / portTICK_PERIOD_MS); } 717 | } 718 | 719 | xTaskCreate(Task1_MQTT, "Task1_MQTT", 8192, NULL, 1, &Task1_MQTT_Handle); 720 | xTaskCreate(Task2_LoRa, "Task2_LoRa", 16384, NULL, 1, &Task2_LoRa_Handle); 721 | xTaskCreate(WatchdogTask, "WatchdogTask", 4096, NULL, 1, &Watchdog_Handle); 722 | } 723 | 724 | void loop() { 725 | vTaskDelay(1000 / portTICK_PERIOD_MS); 726 | } 727 | --------------------------------------------------------------------------------