Initial commit of PlatformIO project structure and code

This commit is contained in:
Sam
2025-04-24 21:23:42 +10:00
commit 1c25cbd561
7 changed files with 514 additions and 0 deletions

5
Lora Transmitter type 01/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch

View File

@@ -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"
]
}

View File

@@ -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

View File

@@ -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 <Foo.h>
#include <Bar.h>
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

View File

@@ -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

View File

@@ -0,0 +1,387 @@
#include <WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <LoRa.h>
#include <SPI.h>
#include <vector>
#include <algorithm>
// --- 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<Message> 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<String>();
// 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<String>();
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.");
}
}

View File

@@ -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