Add support for Heltec V3

This commit is contained in:
Matthew Willcock committed 2025-10-21 13:29:27 +01:00
1 parent fd0de3f390
commit 60b2e0f145
1 file changed
+311 -189
+311 -189
View File
@@ -28,6 +28,8 @@
// LoRa radio object
#ifdef RAK4631_ETH
SX1262 radio = new Module(LORA_CS, LORA_DIO1, LORA_RST, LORA_BUSY);
#elif defined(HELTEC_V3)
SX1262 radio = new Module(LORA_CS, LORA_DIO0, LORA_RST, LORA_BUSY);
#else
SX1276 radio = new Module(LORA_CS, LORA_DIO0, LORA_RST, LORA_DIO1);
#endif
@@ -35,18 +37,23 @@ SX1276 radio = new Module(LORA_CS, LORA_DIO0, LORA_RST, LORA_DIO1);
// UI helpers for consistent boxed output
static const int BOX_CONTENT_WIDTH = 54; // width between bars, excluding leading/trailing single spaces
static void printBoxLine(const String& content) {
static void printBoxLine(const String &content)
{
int pad = BOX_CONTENT_WIDTH - (int)content.length();
if (pad < 0) pad = 0; // truncate visually if too long
if (pad < 0)
pad = 0; // truncate visually if too long
Serial.print("│ ");
Serial.print(content);
for (int i = 0; i < pad; ++i) Serial.print(' ');
for (int i = 0; i < pad; ++i)
Serial.print(' ');
Serial.println(" │");
}
static void printBoxKeyValue(const char* key, const String& value, int keyWidth = 14) {
static void printBoxKeyValue(const char *key, const String &value, int keyWidth = 14)
{
String line = String(key);
while ((int)line.length() < keyWidth) line += ' ';
while ((int)line.length() < keyWidth)
line += ' ';
line += value;
printBoxLine(line);
}
@@ -54,8 +61,8 @@ static void printBoxKeyValue(const char* key, const String& value, int keyWidth
// Global objects
GatewayConfig config;
SettingsManager settingsManager;
MQTTHandler* mqttHandler = nullptr;
ConfigMenu* serialConfig = nullptr;
MQTTHandler *mqttHandler = nullptr;
ConfigMenu *serialConfig = nullptr;
// Statistics
uint32_t packetsReceived = 0;
@@ -80,25 +87,32 @@ static unsigned long lastAdvertSent = 0;
// Simple recent-packet deduplication to prevent rapid re-repeat loops
static const size_t RECENT_PACKET_SLOTS = 8;
struct RecentPacketEntry {
struct RecentPacketEntry
{
uint32_t hash;
unsigned long timestampMs;
};
static RecentPacketEntry recentPackets[RECENT_PACKET_SLOTS] = {};
static uint32_t fnv1aHash32(const uint8_t* data, size_t length) {
static uint32_t fnv1aHash32(const uint8_t *data, size_t length)
{
uint32_t hash = 2166136261u; // FNV offset basis
for (size_t i = 0; i < length; ++i) {
for (size_t i = 0; i < length; ++i)
{
hash ^= data[i];
hash *= 16777619u; // FNV prime
}
return hash;
}
static bool wasPacketSeenRecently(uint32_t hash, unsigned long nowMs, unsigned long windowMs) {
for (size_t i = 0; i < RECENT_PACKET_SLOTS; ++i) {
if (recentPackets[i].hash == hash) {
if (nowMs - recentPackets[i].timestampMs <= windowMs) {
static bool wasPacketSeenRecently(uint32_t hash, unsigned long nowMs, unsigned long windowMs)
{
for (size_t i = 0; i < RECENT_PACKET_SLOTS; ++i)
{
if (recentPackets[i].hash == hash)
{
if (nowMs - recentPackets[i].timestampMs <= windowMs)
{
return true;
}
}
@@ -106,12 +120,15 @@ static bool wasPacketSeenRecently(uint32_t hash, unsigned long nowMs, unsigned l
return false;
}
static void rememberPacket(uint32_t hash, unsigned long nowMs) {
static void rememberPacket(uint32_t hash, unsigned long nowMs)
{
// Insert/overwrite the oldest slot
size_t oldest = 0;
unsigned long oldestTs = recentPackets[0].timestampMs;
for (size_t i = 1; i < RECENT_PACKET_SLOTS; ++i) {
if (recentPackets[i].timestampMs < oldestTs) {
for (size_t i = 1; i < RECENT_PACKET_SLOTS; ++i)
{
if (recentPackets[i].timestampMs < oldestTs)
{
oldestTs = recentPackets[i].timestampMs;
oldest = i;
}
@@ -123,8 +140,8 @@ static void rememberPacket(uint32_t hash, unsigned long nowMs) {
// Function declarations
void setupLoRa();
void handleLoRaReceive();
void handleLoRaPacket(uint8_t* data, size_t length, int rssi, float snr);
bool sendLoRaPacket(const uint8_t* data, size_t length);
void handleLoRaPacket(uint8_t *data, size_t length, int rssi, float snr);
bool sendLoRaPacket(const uint8_t *data, size_t length);
void checkSerialInput();
void publishStats();
void publishNeighbours();
@@ -137,32 +154,38 @@ void printNeighboursToSerial();
// Radio interrupt flag
volatile uint32_t interruptCount = 0;
void IRAM_ATTR setRadioFlag() {
void IRAM_ATTR setRadioFlag()
{
packetReceived = true;
interruptCount++;
}
void setup() {
void setup()
{
Serial.begin(115200);
delay(1000);
// Initialize settings manager
if (!settingsManager.begin()) {
if (!settingsManager.begin())
{
Serial.println(F("✗ Failed to initialize settings manager"));
}
// Load configuration or use defaults
if (!settingsManager.loadConfig(config)) {
if (!settingsManager.loadConfig(config))
{
Serial.println(F("⚠ No saved configuration found, using defaults"));
config = getDefaultConfig();
settingsManager.saveConfig(config);
} else {
}
else
{
Serial.println(F("✓ Configuration loaded"));
}
// Generate node ID if not set
if (config.repeater.nodeId == 0) {
if (config.repeater.nodeId == 0)
{
uint64_t chipid = ESP.getEfuseMac();
config.repeater.nodeId = (uint32_t)(chipid & 0xFFFFFFFF);
settingsManager.saveConfig(config);
@@ -177,7 +200,8 @@ void setup() {
strncpy(prevId, config.mqtt.clientId, sizeof(prevId));
prevId[sizeof(prevId) - 1] = '\0';
deriveClientIdFromNodeName(config.repeater.nodeName, config.mqtt.clientId, sizeof(config.mqtt.clientId));
if (strcmp(prevId, config.mqtt.clientId) != 0) {
if (strcmp(prevId, config.mqtt.clientId) != 0)
{
settingsManager.saveConfig(config);
Serial.printf("✓ MQTT Client ID set to: %s\n", config.mqtt.clientId);
}
@@ -192,7 +216,7 @@ void setup() {
}
{
char buf[32];
snprintf(buf, sizeof(buf), "%.2f MHz", config.lora.frequency);
snprintf(buf, sizeof(buf), "%.3f MHz", config.lora.frequency);
printBoxLine(String("LoRa Freq: ") + buf);
}
printBoxLine(String("WiFi: ") + (config.wifi.enabled ? "Enabled" : "Disabled"));
@@ -206,23 +230,29 @@ void setup() {
Serial.println(F("✓ LoRa initialized"));
// Setup MQTT if enabled
if (config.wifi.enabled && config.mqtt.enabled) {
if (config.wifi.enabled && config.mqtt.enabled)
{
Serial.println(F("\nInitializing MQTT..."));
mqttHandler = new MQTTHandler(config);
// Set callback for MQTT -> LoRa messages
mqttHandler->setMessageCallback([](const uint8_t* payload, size_t length) {
mqttHandler->setMessageCallback([](const uint8_t *payload, size_t length)
{
Serial.printf("Forwarding MQTT message to LoRa (%d bytes)\n", length);
sendLoRaPacket(payload, length);
});
sendLoRaPacket(payload, length); });
if (mqttHandler->begin()) {
if (mqttHandler->begin())
{
Serial.println(F("✓ MQTT initialized"));
mqttHandler->publishGatewayStatus(true);
} else {
}
else
{
Serial.println(F("✗ MQTT initialization failed"));
}
} else {
}
else
{
Serial.println(F("⚠ MQTT disabled (WiFi or MQTT not enabled in config)"));
}
@@ -247,40 +277,49 @@ void setup() {
Serial.println(F("(Hint) Press 'c' at any time to open the configuration menu"));
}
void loop() {
void loop()
{
// Handle LoRa messages
if (!configMode) {
if (!configMode)
{
handleLoRaReceive();
// Handle MQTT
if (mqttHandler) {
if (mqttHandler)
{
mqttHandler->loop();
}
}
// Check for serial commands
if (!configMode) {
if (!configMode)
{
checkSerialInput();
} else {
}
else
{
serialConfig->handleMenu();
}
// Publish statistics periodically
unsigned long now = millis();
if (!configMode && mqttHandler && mqttHandler->isConnected() && now - lastStatsPublish > 60000) {
if (!configMode && mqttHandler && mqttHandler->isConnected() && now - lastStatsPublish > 60000)
{
publishStats();
publishNeighbours(); // Also publish neighbor list with stats
lastStatsPublish = now;
}
// Periodic advert broadcast
if (!configMode && config.discovery.advertEnabled && now - lastAdvertSent > (unsigned long)config.discovery.advertIntervalSec * 1000UL) {
if (!configMode && config.discovery.advertEnabled && now - lastAdvertSent > (unsigned long)config.discovery.advertIntervalSec * 1000UL)
{
sendAdvert();
lastAdvertSent = now;
}
// Blink status LED
if (now - lastStatusBlink > 1000) {
if (now - lastStatusBlink > 1000)
{
blinkLED();
lastStatusBlink = now;
}
@@ -288,7 +327,8 @@ void loop() {
yield();
}
void setupLoRa() {
void setupLoRa()
{
// Initialize SPI
SPI.begin(LORA_SCK, LORA_MISO, LORA_MOSI, LORA_CS);
@@ -305,10 +345,18 @@ void setupLoRa() {
config.lora.codingRate,
config.lora.syncWord,
config.lora.txPower,
8
);
8);
// Enable DIO2 RF switch control and DIO3 TCXO if needed (defaults okay for WisBlock)
radio.setDio2AsRfSwitch(true);
#elif defined(HELTEC_V3)
state = radio.begin(
config.lora.frequency,
config.lora.bandwidth,
config.lora.spreadingFactor,
config.lora.codingRate,
config.lora.syncWord,
config.lora.txPower,
8, 1.8F, false);
#else
state = radio.begin(
config.lora.frequency,
@@ -317,31 +365,36 @@ void setupLoRa() {
config.lora.codingRate,
config.lora.syncWord,
config.lora.txPower,
8, // preamble length
0 // gain (0 = auto)
8, // preamble length
0 // gain (0 = auto)
);
#endif
if (state == RADIOLIB_ERR_NONE) {
if (state == RADIOLIB_ERR_NONE)
{
Serial.println(F("success!"));
// Enable CRC if configured
if (config.lora.enableCRC) {
if (config.lora.enableCRC)
{
radio.setCRC(true);
}
// CRITICAL: For LilyGo boards, explicitly set output power and PA config
// This ensures the PA (Power Amplifier) is actually enabled
#if defined(LILYGO_LORA32_V21)
// CRITICAL: For LilyGo boards, explicitly set output power and PA config
// This ensures the PA (Power Amplifier) is actually enabled
#if defined(LILYGO_LORA32_V21)
Serial.print(F("Configuring PA... "));
// Use PA_BOOST pin (required for LilyGo V2.1)
state = radio.setOutputPower(config.lora.txPower, true); // true = use PA_BOOST
if (state == RADIOLIB_ERR_NONE) {
state = radio.setOutputPower(config.lora.txPower, true); // true = use PA_BOOST
if (state == RADIOLIB_ERR_NONE)
{
Serial.println(F("OK"));
} else {
}
else
{
Serial.printf("FAILED (%d)\n", state);
}
#endif
#endif
// CRITICAL: Set packet received action (RadioLib's proper method)
Serial.print(F("Setting packet received action... "));
@@ -350,14 +403,19 @@ void setupLoRa() {
// Start continuous listening
state = radio.startReceive();
if (state == RADIOLIB_ERR_NONE) {
if (state == RADIOLIB_ERR_NONE)
{
Serial.println(F("✓ Radio listening for packets"));
radioInitialized = true;
} else {
}
else
{
Serial.print(F("✗ Failed to start receive, code: "));
Serial.println(state);
}
} else {
}
else
{
Serial.print(F("failed, code: "));
Serial.println(state);
Serial.println(F("✗ Check wiring and antenna!"));
@@ -367,7 +425,7 @@ void setupLoRa() {
Serial.println(F("\n┌── LoRa Configuration ──────────────────────────────────┐"));
{
char buf[32];
snprintf(buf, sizeof(buf), "%.2f MHz", config.lora.frequency);
snprintf(buf, sizeof(buf), "%.3f MHz", config.lora.frequency);
printBoxKeyValue("Frequency:", buf, 16);
}
{
@@ -399,11 +457,14 @@ void setupLoRa() {
Serial.println(F("└────────────────────────────────────────────────────────┘\n"));
}
void handleLoRaReceive() {
if (!radioInitialized) return;
void handleLoRaReceive()
{
if (!radioInitialized)
return;
// Check if packet was received
if (packetReceived) {
if (packetReceived)
{
packetReceived = false;
Serial.println(F("🔔 Interrupt fired! Reading packet..."));
@@ -413,7 +474,8 @@ void handleLoRaReceive() {
// Read received data
int state = radio.readData(buffer, sizeof(buffer));
if (state == RADIOLIB_ERR_NONE) {
if (state == RADIOLIB_ERR_NONE)
{
// Get packet info
size_t length = radio.getPacketLength();
int rssi = radio.getRSSI();
@@ -424,16 +486,20 @@ void handleLoRaReceive() {
// Handle the packet
handleLoRaPacket(buffer, length, rssi, snr);
} else if (state == RADIOLIB_ERR_CRC_MISMATCH) {
}
else if (state == RADIOLIB_ERR_CRC_MISMATCH)
{
Serial.println(F("⚠ CRC error!"));
} else {
}
else
{
Serial.printf("⚠ Read error, code: %d\n", state);
}
// ✅ CRITICAL FIX: Put radio back into receive mode
state = radio.startReceive();
if (state != RADIOLIB_ERR_NONE) {
if (state != RADIOLIB_ERR_NONE)
{
Serial.print(F("✗ Failed to restart receive, code: "));
Serial.println(state);
radioInitialized = false;
@@ -441,16 +507,19 @@ void handleLoRaReceive() {
}
}
void handleLoRaPacket(uint8_t* data, size_t length, int rssi, float snr) {
void handleLoRaPacket(uint8_t *data, size_t length, int rssi, float snr)
{
// Log to serial
Serial.printf("\n📡 LoRa RX: %d bytes | RSSI: %d dBm | SNR: %.1f dB\n", length, rssi, snr);
// Print hex dump (first 32 bytes)
Serial.print(" Data: ");
for (size_t i = 0; i < min(length, (size_t)32); i++) {
for (size_t i = 0; i < min(length, (size_t)32); i++)
{
Serial.printf("%02X ", data[i]);
}
if (length > 32) Serial.print("...");
if (length > 32)
Serial.print("...");
Serial.println();
// Try to interpret as text if printable
@@ -461,55 +530,66 @@ void handleLoRaPacket(uint8_t* data, size_t length, int rssi, float snr) {
char advertName[32] = {0};
float advertLat = 0.0f;
float advertLon = 0.0f;
for (size_t i = 0; i < length; i++) {
if (data[i] < 32 || data[i] > 126) {
for (size_t i = 0; i < length; i++)
{
if (data[i] < 32 || data[i] > 126)
{
isPrintable = false;
break;
}
}
if (isPrintable && length > 0) {
if (isPrintable && length > 0)
{
Serial.print(" Text: \"");
for (size_t i = 0; i < length; i++) {
for (size_t i = 0; i < length; i++)
{
Serial.write(data[i]);
}
Serial.println("\"");
// Simple neighbour discovery on ADVERT messages: "ADVERT <nodeIdHex> <nodeName> <lat> <lon>"
if (length >= 6 && strncmp((const char*)data, "ADVERT", 6) == 0) {
if (length >= 6 && strncmp((const char *)data, "ADVERT", 6) == 0)
{
char buf[256];
size_t copyLen = min(length, sizeof(buf) - 1);
memcpy(buf, data, copyLen);
buf[copyLen] = '\0';
char* saveptr;
char* tok = strtok_r(buf, " ", &saveptr); // ADVERT
char *saveptr;
char *tok = strtok_r(buf, " ", &saveptr); // ADVERT
tok = strtok_r(nullptr, " ", &saveptr); // nodeIdHex
uint32_t nid = 0;
if (tok) {
if (tok)
{
nid = (uint32_t)strtoul(tok, nullptr, 16);
}
tok = strtok_r(nullptr, " ", &saveptr); // nodeName
tok = strtok_r(nullptr, " ", &saveptr); // nodeName
char nname[32] = {0};
if (tok) {
if (tok)
{
strncpy(nname, tok, sizeof(nname) - 1);
}
tok = strtok_r(nullptr, " ", &saveptr); // lat
tok = strtok_r(nullptr, " ", &saveptr); // lat
float lat = tok ? atof(tok) : 0.0f;
tok = strtok_r(nullptr, " ", &saveptr); // lon
tok = strtok_r(nullptr, " ", &saveptr); // lon
float lon = tok ? atof(tok) : 0.0f;
// Apply access control denylist: drop adverts from denied node IDs
bool denied = false;
if (config.access.denyEnabled) {
for (uint8_t i = 0; i < config.access.denyCount && i < (sizeof(config.access.denylist)/sizeof(config.access.denylist[0])); ++i) {
if (config.access.denylist[i] == nid && nid != 0) {
if (config.access.denyEnabled)
{
for (uint8_t i = 0; i < config.access.denyCount && i < (sizeof(config.access.denylist) / sizeof(config.access.denylist[0])); ++i)
{
if (config.access.denylist[i] == nid && nid != 0)
{
denied = true;
break;
}
}
}
if (denied) {
if (denied)
{
Serial.println(F(" ✗ Advert dropped (denied node)"));
// Skip neighbor update and further processing for denied node
return;
@@ -523,13 +603,20 @@ void handleLoRaPacket(uint8_t* data, size_t length, int rssi, float snr) {
advertLon = lon;
size_t idx = neighborCount;
for (size_t i = 0; i < neighborCount; ++i) {
if (neighbors[i].nodeId == nid) { idx = i; break; }
for (size_t i = 0; i < neighborCount; ++i)
{
if (neighbors[i].nodeId == nid)
{
idx = i;
break;
}
}
if (idx == neighborCount && neighborCount < (sizeof(neighbors)/sizeof(neighbors[0]))) {
if (idx == neighborCount && neighborCount < (sizeof(neighbors) / sizeof(neighbors[0])))
{
neighborCount++;
}
if (idx < (sizeof(neighbors)/sizeof(neighbors[0]))) {
if (idx < (sizeof(neighbors) / sizeof(neighbors[0])))
{
neighbors[idx].nodeId = nid;
strncpy(neighbors[idx].nodeName, nname, sizeof(neighbors[idx].nodeName) - 1);
neighbors[idx].nodeName[sizeof(neighbors[idx].nodeName) - 1] = '\0';
@@ -544,18 +631,22 @@ void handleLoRaPacket(uint8_t* data, size_t length, int rssi, float snr) {
}
// Forward to MQTT if connected
if (mqttHandler && mqttHandler->isConnected()) {
if (mqttHandler && mqttHandler->isConnected())
{
// If this was an ADVERT received over RF, publish a structured advert event
if (parsedAdvert) {
if (parsedAdvert)
{
mqttHandler->publishAdvert(advertNodeId, advertName, advertLat, advertLon);
}
// Publish raw packet
if (config.mqtt.publishRaw) {
if (config.mqtt.publishRaw)
{
mqttHandler->publishRawPacket(data, length, rssi, snr);
}
// Publish decoded message if it looks like text
if (config.mqtt.publishDecoded && isPrintable) {
if (config.mqtt.publishDecoded && isPrintable)
{
char message[256] = {0};
size_t msgLen = min(length, sizeof(message) - 1);
memcpy(message, data, msgLen);
@@ -564,12 +655,12 @@ void handleLoRaPacket(uint8_t* data, size_t length, int rssi, float snr) {
uint32_t fromId = parsedAdvert && advertNodeId != 0 ? advertNodeId : config.repeater.nodeId;
mqttHandler->publishDecodedMessage(
fromId,
0xFFFFFFFF, // to (broadcast)
0xFFFFFFFF, // to (broadcast)
message,
0, // message type
0, // message type
rssi,
snr,
0 // hop count
0 // hop count
);
}
@@ -579,25 +670,32 @@ void handleLoRaPacket(uint8_t* data, size_t length, int rssi, float snr) {
// Optional: Repeat packet if configured as repeater
// This is a simple repeater - just retransmit what we receive
// In a real mesh implementation, you'd check hop count, routing, etc.
if (config.repeater.maxHops > 0 && length > 0) {
if (config.repeater.maxHops > 0 && length > 0)
{
unsigned long nowMs = millis();
uint32_t h = fnv1aHash32(data, length);
if (!wasPacketSeenRecently(h, nowMs, 2000UL)) {
if (!wasPacketSeenRecently(h, nowMs, 2000UL))
{
// Simple delay to avoid collisions
delay(random(100, 300));
// Retransmit
if (sendLoRaPacket(data, length)) {
if (sendLoRaPacket(data, length))
{
Serial.println(" ↻ Packet repeated");
rememberPacket(h, millis());
}
} else {
}
else
{
Serial.println(" ↻ Skipped repeat (duplicate seen recently)");
}
}
}
bool sendLoRaPacket(const uint8_t* data, size_t length) {
if (!radioInitialized || length == 0 || length > 255) {
bool sendLoRaPacket(const uint8_t *data, size_t length)
{
if (!radioInitialized || length == 0 || length > 255)
{
packetsFailed++;
return false;
}
@@ -605,16 +703,19 @@ bool sendLoRaPacket(const uint8_t* data, size_t length) {
Serial.printf("\n📤 LoRa TX: %d bytes\n", length);
// Transmit the packet
int state = radio.transmit((uint8_t*)data, length);
int state = radio.transmit((uint8_t *)data, length);
if (state == RADIOLIB_ERR_NONE) {
if (state == RADIOLIB_ERR_NONE)
{
packetsSent++;
Serial.println(" ✓ Sent successfully");
// Put radio back into receive mode
radio.startReceive();
return true;
} else {
}
else
{
packetsFailed++;
Serial.print(" ✗ Failed, code: ");
Serial.println(state);
@@ -625,95 +726,110 @@ bool sendLoRaPacket(const uint8_t* data, size_t length) {
}
}
void checkSerialInput() {
if (Serial.available()) {
void checkSerialInput()
{
if (Serial.available())
{
char c = Serial.read();
switch (c) {
case 'c':
case 'C':
configMode = true;
serialConfig->showMainMenu();
break;
switch (c)
{
case 'c':
case 'C':
configMode = true;
serialConfig->showMainMenu();
break;
case 's':
case 'S':
Serial.println(F("\n┌── Gateway Statistics ──────────────────────────────────┐"));
printTelemetryToSerial();
Serial.println(F("└────────────────────────────────────────────────────────┘\n"));
break;
case 's':
case 'S':
Serial.println(F("\n┌── Gateway Statistics ──────────────────────────────────┐"));
printTelemetryToSerial();
Serial.println(F("└────────────────────────────────────────────────────────┘\n"));
break;
case 'n':
case 'N':
Serial.println(F("\n┌── Neighbours ──────────────────────────────────────────┐"));
printNeighboursToSerial();
Serial.println(F("└────────────────────────────────────────────────────────┘\n"));
break;
case 'n':
case 'N':
Serial.println(F("\n┌── Neighbours ──────────────────────────────────────────┐"));
printNeighboursToSerial();
Serial.println(F("└────────────────────────────────────────────────────────┘\n"));
break;
case 'r':
case 'R':
Serial.println(F("\n⚠ Restarting device..."));
delay(1000);
ESP.restart();
break;
case 'r':
case 'R':
Serial.println(F("\n⚠ Restarting device..."));
delay(1000);
ESP.restart();
break;
case 'd':
case 'D':
Serial.println(F("\n┌── DEBUG INFO ──────────────────────────────────────────┐"));
Serial.printf("│ Radio Interrupts: %u\n", interruptCount);
Serial.printf("│ Packets Received: %u\n", packetsReceived);
Serial.printf("│ Packets Sent: %u\n", packetsSent);
Serial.printf("│ Packets Forwarded: %u\n", packetsForwarded);
Serial.printf("│ Packets Failed: %u\n", packetsFailed);
Serial.printf("│ Radio Initialized: %s\n", radioInitialized ? "YES" : "NO");
Serial.printf("│ Packet Flag: %s\n", packetReceived ? "SET" : "CLEAR");
// Check radio status
if (radioInitialized) {
Serial.println(F("│"));
Serial.print(F("│ Radio status check... "));
int state = radio.startReceive();
if (state == RADIOLIB_ERR_NONE) {
Serial.println(F("RX ACTIVE"));
// Try to read RSSI to verify radio is listening
int rssi = radio.getRSSI();
Serial.printf("│ Current RSSI: %d dBm\n", rssi);
} else {
Serial.printf("RX FAILED (code: %d)\n", state);
}
}
Serial.println(F("└────────────────────────────────────────────────────────┘\n"));
break;
case 't':
case 'T':
Serial.println(F("\n📡 Sending test packet..."));
case 'd':
case 'D':
Serial.println(F("\n┌── DEBUG INFO ──────────────────────────────────────────┐"));
Serial.printf("│ Radio Interrupts: %u\n", interruptCount);
Serial.printf("│ Packets Received: %u\n", packetsReceived);
Serial.printf("│ Packets Sent: %u\n", packetsSent);
Serial.printf("│ Packets Forwarded: %u\n", packetsForwarded);
Serial.printf("│ Packets Failed: %u\n", packetsFailed);
Serial.printf("│ Radio Initialized: %s\n", radioInitialized ? "YES" : "NO");
Serial.printf("│ Packet Flag: %s\n", packetReceived ? "SET" : "CLEAR");
// Check radio status
if (radioInitialized)
{
Serial.println(F("│"));
Serial.print(F("│ Radio status check... "));
int state = radio.startReceive();
if (state == RADIOLIB_ERR_NONE)
{
uint8_t testPacket[] = "TEST_GATEWAY_TX";
if (sendLoRaPacket(testPacket, sizeof(testPacket))) {
Serial.println(F("✓ Test packet transmitted successfully!"));
Serial.println(F(" (Your other radio should receive this if in range)"));
} else {
Serial.println(F("✗ Test packet transmission failed!"));
}
Serial.println(F("RX ACTIVE"));
// Try to read RSSI to verify radio is listening
int rssi = radio.getRSSI();
Serial.printf("│ Current RSSI: %d dBm\n", rssi);
}
break;
else
{
Serial.printf("RX FAILED (code: %d)\n", state);
}
}
Serial.println(F("└────────────────────────────────────────────────────────┘\n"));
break;
case 't':
case 'T':
Serial.println(F("\n📡 Sending test packet..."));
{
uint8_t testPacket[] = "TEST_GATEWAY_TX";
if (sendLoRaPacket(testPacket, sizeof(testPacket)))
{
Serial.println(F("✓ Test packet transmitted successfully!"));
Serial.println(F(" (Your other radio should receive this if in range)"));
}
else
{
Serial.println(F("✗ Test packet transmission failed!"));
}
}
break;
}
}
}
void publishStats() {
if (mqttHandler) {
void publishStats()
{
if (mqttHandler)
{
mqttHandler->publishStats(packetsReceived, packetsSent, packetsForwarded, packetsFailed);
}
}
void publishNeighbours() {
if (mqttHandler) {
void publishNeighbours()
{
if (mqttHandler)
{
mqttHandler->publishNeighbors(neighbors, neighborCount);
}
}
void blinkLED() {
void blinkLED()
{
// TODO: Add LED blinking based on status
// - Slow blink: Normal operation
// - Fast blink: WiFi connecting
@@ -721,7 +837,8 @@ void blinkLED() {
// You can use the built-in LED or an external LED
}
void sendAdvert() {
void sendAdvert()
{
// Compose a simple advert string: ADVERT <nodeIdHex> <nodeName> <lat> <lon>
char payload[160];
snprintf(payload, sizeof(payload), "ADVERT %08X %s %.6f %.6f",
@@ -729,37 +846,42 @@ void sendAdvert() {
config.repeater.nodeName,
(double)config.location.latitude,
(double)config.location.longitude);
sendLoRaPacket((const uint8_t*)payload, strlen(payload));
sendLoRaPacket((const uint8_t *)payload, strlen(payload));
// Also publish an advert event on MQTT for visibility if connected
if (mqttHandler && mqttHandler->isConnected()) {
if (mqttHandler && mqttHandler->isConnected())
{
mqttHandler->publishAdvert(
config.repeater.nodeId,
config.repeater.nodeName,
config.location.latitude,
config.location.longitude
);
config.location.longitude);
}
}
void printTelemetryToSerial() {
void printTelemetryToSerial()
{
Serial.printf("Uptime: %-36lu \n", millis() / 1000);
Serial.printf("Packets Received: %-36lu \n", packetsReceived);
Serial.printf("Packets Sent: %-36lu \n", packetsSent);
Serial.printf("Packets Forwarded:%-36lu \n", packetsForwarded);
Serial.printf("Packets Failed: %-36lu \n", packetsFailed);
if (WiFi.status() == WL_CONNECTED) {
if (WiFi.status() == WL_CONNECTED)
{
Serial.printf("WiFi RSSI: %-36d \n", WiFi.RSSI());
Serial.printf("IP Address: %-36s \n", WiFi.localIP().toString().c_str());
}
}
void printNeighboursToSerial() {
if (neighborCount == 0) {
void printNeighboursToSerial()
{
if (neighborCount == 0)
{
Serial.println(F("(none)"));
return;
}
unsigned long now = millis();
for (size_t i = 0; i < neighborCount; ++i) {
for (size_t i = 0; i < neighborCount; ++i)
{
unsigned long age = (now - neighbors[i].lastSeenMs) / 1000UL;
Serial.printf("ID: 0x%08X Name: %-16s RSSI: %4d SNR: %5.1f Age: %lus Lat: %.5f Lon: %.5f\n",
neighbors[i].nodeId,
@@ -773,7 +895,8 @@ void printNeighboursToSerial() {
}
// Exit configuration mode helper
void exitConfigMode() {
void exitConfigMode()
{
configMode = false;
Serial.println(F("\n✓ Exited configuration mode"));
@@ -781,4 +904,3 @@ void exitConfigMode() {
Serial.println(F("⚠ Some changes may require a restart"));
Serial.println(F("(Hint) Live view resumed. Press 'c' to return to the menu"));
}