commit 1c25cbd561b0de4d0f06a9339ed90bd3c9ab6dfa Author: Sam Date: Thu Apr 24 21:23:42 2025 +1000 Initial commit of PlatformIO project structure and code diff --git a/Lora Transmitter type 01/.gitignore b/Lora Transmitter type 01/.gitignore new file mode 100644 index 0000000..89cc49c --- /dev/null +++ b/Lora Transmitter type 01/.gitignore @@ -0,0 +1,5 @@ +.pio +.vscode/.browse.c_cpp.db* +.vscode/c_cpp_properties.json +.vscode/launch.json +.vscode/ipch diff --git a/Lora Transmitter type 01/.vscode/extensions.json b/Lora Transmitter type 01/.vscode/extensions.json new file mode 100644 index 0000000..080e70d --- /dev/null +++ b/Lora Transmitter type 01/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "platformio.platformio-ide" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/Lora Transmitter type 01/include/README b/Lora Transmitter type 01/include/README new file mode 100644 index 0000000..49819c0 --- /dev/null +++ b/Lora Transmitter type 01/include/README @@ -0,0 +1,37 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the convention is to give header files names that end with `.h'. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/Lora Transmitter type 01/lib/README b/Lora Transmitter type 01/lib/README new file mode 100644 index 0000000..9379397 --- /dev/null +++ b/Lora Transmitter type 01/lib/README @@ -0,0 +1,46 @@ + +This directory is intended for project specific (private) libraries. +PlatformIO will compile them to static libraries and link into the executable file. + +The source code of each library should be placed in a separate directory +("lib/your_library_name/[Code]"). + +For example, see the structure of the following example libraries `Foo` and `Bar`: + +|--lib +| | +| |--Bar +| | |--docs +| | |--examples +| | |--src +| | |- Bar.c +| | |- Bar.h +| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html +| | +| |--Foo +| | |- Foo.c +| | |- Foo.h +| | +| |- README --> THIS FILE +| +|- platformio.ini +|--src + |- main.c + +Example contents of `src/main.c` using Foo and Bar: +``` +#include +#include + +int main (void) +{ + ... +} + +``` + +The PlatformIO Library Dependency Finder will find automatically dependent +libraries by scanning project source files. + +More information about PlatformIO Library Dependency Finder +- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/Lora Transmitter type 01/platformio.ini b/Lora Transmitter type 01/platformio.ini new file mode 100644 index 0000000..9d8d47d --- /dev/null +++ b/Lora Transmitter type 01/platformio.ini @@ -0,0 +1,18 @@ +; PlatformIO Project Configuration File +; +; Build options: build flags, source filter +; Upload options: custom upload port, speed and extra flags +; Library options: dependencies, extra library storages +; Advanced options: extra scripting +; +; Please visit documentation for the other options and examples +; https://docs.platformio.org/page/projectconf.html + +[env:esp32dev] +platform = espressif32 +board = esp32dev +framework = arduino +lib_deps = + knolleary/PubSubClient@^2.8 + bblanchon/ArduinoJson@^7.4.1 + sandeepmistry/LoRa@^0.8.0 diff --git a/Lora Transmitter type 01/src/main.cpp b/Lora Transmitter type 01/src/main.cpp new file mode 100644 index 0000000..00e44c6 --- /dev/null +++ b/Lora Transmitter type 01/src/main.cpp @@ -0,0 +1,387 @@ +#include +#include +#include +#include +#include +#include +#include + +// --- Pin Definitions --- +// LoRa Module pin definition +#define LORA_CS 5 // NSS +#define LORA_RST 14 // Reset +#define LORA_IRQ 26 // DIO0 (Used by library, not explicitly for interrupts here) + +// --- WiFi & MQTT Configuration --- +const char* ssid = "Aussie Broadband 8729"; // Replace with your SSID +const char* password = "Ffdfmunfca"; // Replace with your WiFi password +const char* mqtt_server = "192.168.20.30"; // Replace with your MQTT broker IP/hostname +const char* mqtt_user = "mqtt-user"; // Replace with your MQTT username (if any) +const char* mqtt_password = "sam4jo"; // Replace with your MQTT password (if any) +// MQTT Topics +const char* mqtt_topic_subscribe = "homeassistant/ESP32_RA/control"; // Topic to receive commands FOR LoRa devices +const char* mqtt_topic_publish = "homeassistant/ESP32_RA/state"; // Topic to publish status FROM LoRa devices + +WiFiClient espClient; +PubSubClient client(espClient); + +// --- LoRa Configuration --- +#define LORA_FREQUENCY 434E6 // 434 MHz + +// --- Resend Queue Logic --- +struct Message { + String message; // Stores the FULL JSON string sent via LoRa + String message_id; // Stores the unique ID generated for this specific transmission attempt + String targetDevice; // Stores the target device for queue management + unsigned long lastSentTime; + int retryCount; +}; + +std::vector resendQueue; +const int resend_times = 80; // Max retries +const unsigned long timerInterval = 1000; // Check queue every 1 second +const int effectiveIntervalSeconds = 5; // Resend attempts every 5 seconds +unsigned long lastTimerCheck = 0; +int sendCounter = 0; + +// --- Function Declarations --- +void connectToWiFi(); +void reconnect(); +void callback(char* topic, byte* message, unsigned int length); // MQTT Callback +void sendMessage(String jsonMessageToSendViaLoRa, String message_id, String targetDevice); // Sends LoRa & adds to queue +void checkResendQueue(); // Handles retries +void handleReceivedLoRaMessage(String receivedMessage); // Processes incoming LoRa (ACKs/Status) +// --- End Function Declarations --- + +void setup() { + Serial.begin(115200); + while (!Serial); + Serial.println("\n\n--- ESP32 RA Transmitter/Gateway ---"); + + // Initialize LoRa + Serial.println("Initializing LoRa..."); + SPI.begin(); // Initialize SPI explicitly + LoRa.setPins(LORA_CS, LORA_RST, LORA_IRQ); + if (!LoRa.begin(LORA_FREQUENCY)) { + Serial.println("Starting LoRa failed!"); + while (1); + } + // Set LoRa parameters (ensure these match ALL receivers) + LoRa.setSpreadingFactor(9); + LoRa.setSignalBandwidth(125E3); + LoRa.setCodingRate4(5); + LoRa.setPreambleLength(8); + LoRa.setSyncWord(0x34); // Use a non-default sync word + Serial.println("LoRa initialized successfully!"); + + connectToWiFi(); + client.setServer(mqtt_server, 1883); + client.setCallback(callback); + reconnect(); // Connects to MQTT and subscribes + + Serial.println("ESP32 RA Transmitter is ready."); +} + +void loop() { + // 1. Maintain MQTT connection + if (!client.connected()) { + reconnect(); + } + client.loop(); // Process MQTT messages & keepalive + + // 2. Check for incoming LoRa messages (ACKs or Status Updates) + int packetSize = LoRa.parsePacket(); + if (packetSize) { + String receivedMessage = ""; + while (LoRa.available()) { + receivedMessage += (char)LoRa.read(); + } + receivedMessage.trim(); + if (receivedMessage.length() > 0) { + handleReceivedLoRaMessage(receivedMessage); // Process incoming LoRa + } + } + + // 3. Check resend queue periodically (non-blocking timer) + unsigned long currentTime = millis(); + if (currentTime - lastTimerCheck >= timerInterval) { + lastTimerCheck = currentTime; + sendCounter++; + if (sendCounter >= effectiveIntervalSeconds) { + checkResendQueue(); // Check if any messages need resending + sendCounter = 0; + } + } + + // 4. Handle Serial Monitor Input (for debugging/testing) + if (Serial.available() > 0) { + String command = Serial.readStringUntil('\n'); + command.trim(); + Serial.print("Received from Serial Monitor: "); Serial.println(command); + + // Allow sending test JSON via Serial Monitor + // Example: {"targetDevice":"driveway_light", "action":"turn_on"} + JsonDocument testDoc; // Parse the input from Serial + DeserializationError testErr = deserializeJson(testDoc, command); + if (!testErr) { + // Check if targetDevice exists using isNull() + if (testDoc["targetDevice"].isNull()) { + Serial.println("Error: JSON from Serial Monitor must contain 'targetDevice'."); + } else { + // Proceed only if targetDevice exists + String message_id = String(random(10000, 99999)); + JsonDocument loraDoc; // Create the document specifically for LoRa + + // Extract targetDevice as String + String targetDeviceString = testDoc["targetDevice"].as(); + + // Build the LoRa JSON document + loraDoc["message_id"] = message_id; + loraDoc["targetDevice"] = targetDeviceString; // Use the extracted string + + // Copy action if present using isNull() + if (!testDoc["action"].isNull()) { + loraDoc["action"] = testDoc["action"]; + } + // Add other fields if needed for test, copying from testDoc to loraDoc + // Example: if(!testDoc["someValue"].isNull()) loraDoc["someValue"] = testDoc["someValue"]; + + String jsonMessage; + serializeJson(loraDoc, jsonMessage); + + // Call sendMessage with all required arguments + sendMessage(jsonMessage, message_id, targetDeviceString); + } + } else { + Serial.println("Invalid JSON via Serial Monitor."); + } + } // End Serial Monitor handling + + delay(5); // Small delay to prevent tight loop +} // End loop() + +// --- WiFi and MQTT Functions --- + +void connectToWiFi() { + Serial.print("Initializing WiFi..."); + WiFi.mode(WIFI_STA); + WiFi.disconnect(true); + delay(100); + Serial.print("Connecting to WiFi..."); + WiFi.begin(ssid, password); + int retryCount = 0; + while (WiFi.status() != WL_CONNECTED) { + delay(1000); + Serial.print("."); + retryCount++; + if (retryCount > 30) { + Serial.println("\nFailed to connect to WiFi. Restarting..."); + ESP.restart(); + } + } + Serial.println("\nConnected to WiFi!"); + Serial.print("IP Address: "); Serial.println(WiFi.localIP()); +} + +void reconnect() { + while (!client.connected()) { + Serial.print("Connecting to MQTT..."); + // Attempt to connect with client ID, user, and password + if (client.connect("ESP32_RA_Transmitter", mqtt_user, mqtt_password)) { + Serial.println("connected"); + // Subscribe to the control topic upon connection + client.subscribe(mqtt_topic_subscribe); + Serial.print("Subscribed to: "); Serial.println(mqtt_topic_subscribe); + } else { + Serial.print("failed, rc="); Serial.print(client.state()); + Serial.println(" trying again in 5 seconds"); + // Wait 5 seconds before retrying + delay(5000); + } + } +} + + +// Handles messages received FROM MQTT (structured JSON commands for LoRa devices) +void callback(char* topic, byte* message, unsigned int length) { + String messageReceived; // This is the JSON string from Home Assistant + messageReceived.reserve(length + 1); // Pre-allocate string memory + for (unsigned int i = 0; i < length; i++) { + messageReceived += (char)message[i]; + } + Serial.println("--- MQTT Callback ---"); + Serial.println("Topic: " + String(topic)); + Serial.println("Raw Payload (" + String(length) + " bytes): " + messageReceived); // Print raw payload + + if (String(topic) == mqtt_topic_subscribe) { + // Use dynamic JsonDocument (recommended for ArduinoJson v7+) + JsonDocument doc; + Serial.println("Attempting to parse MQTT JSON..."); + DeserializationError mqttError = deserializeJson(doc, messageReceived); + + // Check for parsing errors, including memory allocation failure + if (mqttError == DeserializationError::NoMemory) { + Serial.println("!!! Failed to allocate memory for MQTT JSON. Increase heap?"); + return; // Exit if memory allocation failed + } else if (mqttError) { + Serial.print("!!! Failed to parse JSON from MQTT: "); + Serial.println(mqttError.c_str()); + Serial.println("Payload was: " + messageReceived); + return; // Ignore invalid MQTT messages + } + Serial.println("MQTT JSON Parsed Successfully."); + + // Extract targetDevice using isNull() check + if (doc["targetDevice"].isNull()) { + Serial.println("!!! MQTT message missing 'targetDevice'. Ignoring."); + return; // Cannot process without a target + } + String targetDevice = doc["targetDevice"].as(); + Serial.println("Target Device: " + targetDevice); + + // Generate a unique message ID for LoRa tracking + String message_id = String(random(10000, 99999)); + + // Add/overwrite the message_id in the document + doc["message_id"] = message_id; + Serial.println("Added message_id: " + message_id); + + // Serialize the *modified* document for LoRa transmission + String jsonMessageToSendViaLoRa; + Serial.println("Serializing document for LoRa..."); + size_t jsonSize = serializeJson(doc, jsonMessageToSendViaLoRa); + Serial.println("Serialized LoRa JSON (" + String(jsonSize) + " bytes): " + jsonMessageToSendViaLoRa); + + // Send the message via LoRa (will also add/replace in queue) + // Pass targetDevice to sendMessage + sendMessage(jsonMessageToSendViaLoRa, message_id, targetDevice); + } else { + Serial.println("Message received on incorrect topic."); + } + Serial.println("--- End MQTT Callback ---"); +} + + +// --- LoRa Communication Functions --- + +// Sends a LoRa message AND adds/replaces it in the resend queue +void sendMessage(String jsonMessageToSendViaLoRa, String message_id, String targetDevice) { + // Basic validation of the JSON before sending (optional but good) + JsonDocument doc; + DeserializationError error = deserializeJson(doc, jsonMessageToSendViaLoRa); + if (error) { + Serial.print("sendMessage: Invalid JSON format, cannot send: "); + Serial.println(error.c_str()); + return; + } + + // Remove existing message for this targetDevice from queue + // Capture targetDevice by value in the lambda + auto it = std::find_if(resendQueue.begin(), resendQueue.end(), + [targetDevice](const Message& msg_in_queue) { + return msg_in_queue.targetDevice == targetDevice; + }); + + if (it != resendQueue.end()) { + Serial.println("Found existing message for target '" + targetDevice + "' (ID: " + it->message_id + ") in queue. Removing it."); + resendQueue.erase(it); + } + + // Send the LoRa packet + Serial.print("Sending LoRa packet for target '" + targetDevice + "': "); Serial.println(jsonMessageToSendViaLoRa); + LoRa.beginPacket(); + LoRa.print(jsonMessageToSendViaLoRa); + int success = LoRa.endPacket(); // This blocks until transmission is complete + if(success){ + Serial.println("LoRa packet sent initially."); + } else { + Serial.println("LoRa packet initial send FAILED."); + // Consider not adding to queue if initial send fails? Or let retry handle it. + } + + // Add the new message to the queue for potential resends + Message newMsg = {jsonMessageToSendViaLoRa, message_id, targetDevice, millis(), 0}; + resendQueue.push_back(newMsg); + Serial.println("Added message ID " + message_id + " for target '" + targetDevice + "' to queue."); +} + +// Checks the queue and resends messages if no ACK received +void checkResendQueue() { + unsigned long currentMillis = millis(); + unsigned long resendIntervalMillis = effectiveIntervalSeconds * 1000; + + if (resendQueue.empty()) { + return; // Nothing to do + } + // Serial.println("--- checkResendQueue ---"); // Enable for debugging queue + + for (auto it = resendQueue.begin(); it != resendQueue.end(); /* increment inside loop */) { + if (it->retryCount >= resend_times) { + Serial.println("Message ID " + it->message_id + " for target '" + it->targetDevice + "' failed after max retries. Removing."); + it = resendQueue.erase(it); // Erase and get next iterator + continue; + } + + // Check if enough time has passed since the last send attempt for this message + if (currentMillis - it->lastSentTime >= resendIntervalMillis) { + Serial.print("Resending message ID " + it->message_id + " for target '" + it->targetDevice + "': " + it->message); + LoRa.beginPacket(); + LoRa.print(it->message); // Resend the full JSON string + LoRa.endPacket(); + Serial.println(" ...Resent."); + + it->lastSentTime = currentMillis; + it->retryCount++; + ++it; // Move to next item after processing + } else { + ++it; // Move to next item if not time to resend + } + } +} + +// Handles messages received VIA LORA (ACKs, Status Updates, etc.) +void handleReceivedLoRaMessage(String receivedMessage) { + Serial.print(">>> Transmitter Received LoRa: "); Serial.println(receivedMessage); + + JsonDocument doc; + DeserializationError error = deserializeJson(doc, receivedMessage); + if (error) { + Serial.print(">>> Transmitter: Failed to parse received LoRa JSON: "); Serial.println(error.c_str()); + return; + } + + // Check for Acknowledgment + String msgContent = doc["message"] | "no_message"; + String ackMsgId = doc["message_id"] | "no_id"; + + if (msgContent == "acknowledged" && ackMsgId != "no_id") { + Serial.print(">>> Transmitter: Received ACK for message_id: "); Serial.println(ackMsgId); + // Find and remove the message from the resendQueue based on the ACK's message_id + // Capture ackMsgId by value in the lambda + auto it = std::find_if(resendQueue.begin(), resendQueue.end(), + [ackMsgId](const Message& msg_in_queue) { + // Compare the ID from the ACK with the ID stored when the message was added to the queue + return msg_in_queue.message_id == ackMsgId; + }); + if (it != resendQueue.end()) { + Serial.println(">>> Transmitter: Found matching message (Target: " + it->targetDevice + ") in queue. Erasing."); + resendQueue.erase(it); + } else { + Serial.println(">>> Transmitter: ACK received, but no matching message found in queue (might have timed out or already ACKed)."); + } + return; // Handled ACK, nothing more to do with this message + } + + // If not an ACK, assume it's a Status Update from a receiver device + Serial.println(">>> Transmitter: Received Status Update from LoRa device."); + // Check if essential status info is present (e.g., device name) using isNull() + if (!doc["device"].isNull()) { + // Publish the *entire received JSON* to the state MQTT topic + Serial.print(">>> Transmitter: Publishing to MQTT topic ["); + Serial.print(mqtt_topic_publish); Serial.print("]: "); + Serial.println(receivedMessage); // Publish the raw JSON received + client.publish(mqtt_topic_publish, receivedMessage.c_str()); + } else { + Serial.println(">>> Transmitter: Received LoRa message is not ACK and lacks 'device' field for status update."); + } +} diff --git a/Lora Transmitter type 01/test/README b/Lora Transmitter type 01/test/README new file mode 100644 index 0000000..9b1e87b --- /dev/null +++ b/Lora Transmitter type 01/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html