mirror of
https://github.com/jmead/Meshcore-Repeater-MQTT-Gateway.git
synced 2026-10-30 22:11:57 +01:00
Add support for Heltec V3
This commit is contained in:
1 parent
fd0de3f390
commit
60b2e0f145
1 file changed
+311
-189
+311
-189
@@ -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"));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user