simple remote wifi repeater v0.2 & remote repeater path hash mode improvements

This commit is contained in:
pelgraine
2026-04-04 10:51:48 +11:00
parent c687133b05
commit 424e152d4b
9 changed files with 880 additions and 61 deletions
+26 -14
View File
@@ -4,23 +4,23 @@
/* ------------------------------ Config -------------------------------- */
#ifndef LORA_FREQ
#define LORA_FREQ 915.0
#define LORA_FREQ 923.125
#endif
#ifndef LORA_BW
#define LORA_BW 250
#define LORA_BW 62.5
#endif
#ifndef LORA_SF
#define LORA_SF 10
#define LORA_SF 8
#endif
#ifndef LORA_CR
#define LORA_CR 5
#define LORA_CR 8
#endif
#ifndef LORA_TX_POWER
#define LORA_TX_POWER 20
#define LORA_TX_POWER 22
#endif
#ifndef ADVERT_NAME
#define ADVERT_NAME "repeater"
#define ADVERT_NAME "remote-repeater"
#endif
#ifndef ADVERT_LAT
#define ADVERT_LAT 0.0
@@ -534,10 +534,10 @@ void MyMesh::onAnonDataRecv(mesh::Packet *packet, const uint8_t *secret, const m
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
mesh::Packet* path = createPathReturn(sender, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
if (path) sendFlood(path, (uint32_t)SERVER_RESPONSE_DELAY, (uint8_t)(_prefs.path_hash_mode + 1));
} else if (reply_path_len < 0) {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
if (reply) sendFlood(reply, SERVER_RESPONSE_DELAY);
if (reply) sendFlood(reply, (uint32_t)SERVER_RESPONSE_DELAY, (uint8_t)(_prefs.path_hash_mode + 1));
} else {
mesh::Packet* reply = createDatagram(PAYLOAD_TYPE_RESPONSE, sender, secret, reply_data, reply_len);
if (reply) sendDirect(reply, reply_path, reply_path_len, SERVER_RESPONSE_DELAY);
@@ -609,7 +609,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
// let this sender know path TO here, so they can use sendDirect(), and ALSO encode the response
mesh::Packet *path = createPathReturn(client->id, secret, packet->path, packet->path_len,
PAYLOAD_TYPE_RESPONSE, reply_data, reply_len);
if (path) sendFlood(path, SERVER_RESPONSE_DELAY);
if (path) sendFlood(path, (uint32_t)SERVER_RESPONSE_DELAY, (uint8_t)(_prefs.path_hash_mode + 1));
} else {
mesh::Packet *reply =
createDatagram(PAYLOAD_TYPE_RESPONSE, client->id, secret, reply_data, reply_len);
@@ -617,7 +617,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
if (client->out_path_len >= 0) { // we have an out_path, so send DIRECT
sendDirect(reply, client->out_path, client->out_path_len, SERVER_RESPONSE_DELAY);
} else {
sendFlood(reply, SERVER_RESPONSE_DELAY);
sendFlood(reply, (uint32_t)SERVER_RESPONSE_DELAY, (uint8_t)(_prefs.path_hash_mode + 1));
}
}
}
@@ -648,7 +648,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
mesh::Packet *ack = createAck(ack_hash);
if (ack) {
if (client->out_path_len < 0) {
sendFlood(ack, TXT_ACK_DELAY);
sendFlood(ack, (uint32_t)TXT_ACK_DELAY, (uint8_t)(_prefs.path_hash_mode + 1));
} else {
sendDirect(ack, client->out_path, client->out_path_len, TXT_ACK_DELAY);
}
@@ -676,7 +676,7 @@ void MyMesh::onPeerDataRecv(mesh::Packet *packet, uint8_t type, int sender_idx,
auto reply = createDatagram(PAYLOAD_TYPE_TXT_MSG, client->id, secret, temp, 5 + text_len);
if (reply) {
if (client->out_path_len < 0) {
sendFlood(reply, CLI_REPLY_DELAY_MILLIS);
sendFlood(reply, (uint32_t)CLI_REPLY_DELAY_MILLIS, (uint8_t)(_prefs.path_hash_mode + 1));
} else {
sendDirect(reply, client->out_path, client->out_path_len, CLI_REPLY_DELAY_MILLIS);
}
@@ -801,6 +801,7 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc
_prefs.advert_loc_policy = ADVERT_LOC_PREFS;
_prefs.adc_multiplier = 0.0f; // 0.0f means use default board multiplier
_prefs.path_hash_mode = 0; // 1-byte path hashes (legacy default)
}
void MyMesh::begin(FILESYSTEM *fs) {
@@ -857,7 +858,7 @@ bool MyMesh::formatFileSystem() {
void MyMesh::sendSelfAdvertisement(int delay_millis) {
mesh::Packet *pkt = createSelfAdvert();
if (pkt) {
sendFlood(pkt, delay_millis);
sendFlood(pkt, (uint32_t)delay_millis, (uint8_t)(_prefs.path_hash_mode + 1));
} else {
MESH_DEBUG_PRINTLN("ERROR: unable to create advertisement packet!");
}
@@ -1145,6 +1146,17 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
} else {
strcpy(reply, "Err - ??");
}
} else if (memcmp(command, "set path.hash.mode ", 19) == 0) {
int mode = atoi(&command[19]);
if (mode >= 0 && mode <= 2) {
_prefs.path_hash_mode = (uint8_t)mode;
savePrefs();
sprintf(reply, "OK - path.hash.mode = %d (%d-byte hashes)", mode, mode + 1);
} else {
strcpy(reply, "ERR: mode must be 0, 1, or 2");
}
} else if (strcmp(command, "get path.hash.mode") == 0) {
sprintf(reply, "> %d (%d-byte path hashes)", _prefs.path_hash_mode, _prefs.path_hash_mode + 1);
} else{
_cli.handleCommand(sender_timestamp, command, reply); // common CLI commands
}
@@ -1159,7 +1171,7 @@ void MyMesh::loop() {
if (next_flood_advert && millisHasNowPassed(next_flood_advert)) {
mesh::Packet *pkt = createSelfAdvert();
if (pkt) sendFlood(pkt);
if (pkt) sendFlood(pkt, (uint32_t)0, (uint8_t)(_prefs.path_hash_mode + 1));
updateFloodAdvertTimer(); // schedule next flood advert
updateAdvertTimer(); // also schedule local advert (so they don't overlap)
+2 -2
View File
@@ -68,11 +68,11 @@ struct NeighbourInfo {
};
#ifndef FIRMWARE_BUILD_DATE
#define FIRMWARE_BUILD_DATE "30 Nov 2025"
#define FIRMWARE_BUILD_DATE "3 April 2026"
#endif
#ifndef FIRMWARE_VERSION
#define FIRMWARE_VERSION "v1.11.0"
#define FIRMWARE_VERSION "v0.2"
#endif
#define FIRMWARE_ROLE "repeater"
+59 -5
View File
@@ -4,6 +4,13 @@
#ifdef HAS_4G_MODEM
#include "CellularMQTT.h"
#endif
#ifdef MECK_WIFI_REMOTE
#include "WiFiMQTT.h"
#endif
#if defined(HAS_4G_MODEM) || defined(MECK_WIFI_REMOTE)
#define AUTO_OFF_DISABLED true
#else
#define AUTO_OFF_DISABLED false
@@ -57,8 +64,10 @@ void UITask::renderCurrScreen() {
_display->setCursor((_display->width() - versionWidth) / 2, 22);
_display->print(_version_info);
#ifdef HAS_4G_MODEM
#if defined(HAS_4G_MODEM)
const char* node_type = "< Remote Repeater >";
#elif defined(MECK_WIFI_REMOTE)
const char* node_type = "< WiFi Repeater >";
#else
const char* node_type = "< Repeater >";
#endif
@@ -66,7 +75,7 @@ void UITask::renderCurrScreen() {
_display->setCursor((_display->width() - typeWidth) / 2, 35);
_display->print(node_type);
} else {
// Home screen — node info + cellular status
// Home screen — node info + connection status
_display->setCursor(0, 0);
_display->setTextSize(1);
_display->setColor(DisplayDriver::GREEN);
@@ -81,6 +90,7 @@ void UITask::renderCurrScreen() {
sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr);
_display->print(tmp);
// --- Cellular status (4G variant) ---
#ifdef HAS_4G_MODEM
int y = 44;
@@ -109,11 +119,55 @@ void UITask::renderCurrScreen() {
_display->print(tmp);
y += 10;
const char* ip = cellularMQTT.getIPAddress();
if (ip[0]) {
const char* ip4g = cellularMQTT.getIPAddress();
if (ip4g[0]) {
_display->setColor(DisplayDriver::LIGHT);
_display->setCursor(0, y);
sprintf(tmp, "IP: %s", ip);
sprintf(tmp, "IP: %s", ip4g);
_display->print(tmp);
y += 10;
}
uint32_t upSec = millis() / 1000;
uint32_t upH = upSec / 3600;
uint32_t upM = (upSec % 3600) / 60;
_display->setColor(DisplayDriver::LIGHT);
_display->setCursor(0, y);
sprintf(tmp, "Up: %luh %lum Heap:%dk", upH, upM, ESP.getFreeHeap() / 1024);
_display->print(tmp);
#endif
// --- WiFi status (WiFi variant) ---
#ifdef MECK_WIFI_REMOTE
int y = 44;
_display->setCursor(0, y);
_display->setColor(DisplayDriver::LIGHT);
sprintf(tmp, "WiFi: %s", wifiMQTT.stateString());
_display->print(tmp);
y += 10;
_display->setCursor(0, y);
sprintf(tmp, "RSSI: %d (%d bars)", wifiMQTT.getRSSI(), wifiMQTT.getSignalBars());
_display->print(tmp);
y += 10;
_display->setCursor(0, y);
sprintf(tmp, "SSID: %.16s", wifiMQTT.getSSID());
_display->print(tmp);
y += 10;
_display->setCursor(0, y);
_display->setColor(wifiMQTT.isConnected() ? DisplayDriver::GREEN : DisplayDriver::YELLOW);
sprintf(tmp, "MQTT: %s", wifiMQTT.isConnected() ? "Connected" : "---");
_display->print(tmp);
y += 10;
const char* ipWifi = wifiMQTT.getIPAddress();
if (ipWifi[0]) {
_display->setColor(DisplayDriver::LIGHT);
_display->setCursor(0, y);
sprintf(tmp, "IP: %s", ipWifi);
_display->print(tmp);
y += 10;
}
+73 -29
View File
@@ -8,6 +8,11 @@
#include "CellularMQTT.h"
#endif
#ifdef MECK_WIFI_REMOTE
#include <SD.h>
#include "WiFiMQTT.h"
#endif
#ifdef DISPLAY_CLASS
#include "UITask.h"
static UITask ui_task(display);
@@ -28,7 +33,7 @@ static char command[160];
unsigned long lastActive = 0; // mark last active time
unsigned long nextSleepinSecs = 120; // next sleep in seconds. The first sleep (if enabled) is after 2 minutes from boot
#ifdef HAS_4G_MODEM
#if defined(HAS_4G_MODEM) || defined(MECK_WIFI_REMOTE)
static bool sdCardReady = false;
#endif
@@ -43,12 +48,10 @@ void setup() {
#ifdef DISPLAY_CLASS
if (display.begin()) {
#ifndef HAS_4G_MODEM
display.startFrame();
display.setCursor(0, 0);
display.print("Please wait...");
display.endFrame();
#endif
}
#endif
@@ -95,10 +98,10 @@ void setup() {
the_mesh.begin(fs);
// ---------------------------------------------------------------------------
// SD card init — needed for CellularMQTT config (/remote/mqtt.cfg)
// SD card init — needed for MQTT config (/remote/mqtt.cfg, /remote/wifi.cfg)
// SD, LoRa, and e-ink share the same SPI bus on T-Deck Pro.
// ---------------------------------------------------------------------------
#ifdef HAS_4G_MODEM
#if defined(HAS_4G_MODEM) || defined(MECK_WIFI_REMOTE)
{
// Deselect all SPI devices before SD init to prevent bus contention
#ifdef SDCARD_CS
@@ -124,19 +127,11 @@ void setup() {
#endif
delay(200);
}
Serial.printf("SD card: %s\n", sdCardReady ? "ready" : "FAILED");
// Re-claim SPI bus for display — SD.begin() steals the shared
// GPIO pins (36/47/33) from the display's HSPI peripheral
extern SPIClass displaySpi;
displaySpi.begin(PIN_DISPLAY_SCLK, 47, PIN_DISPLAY_MOSI, PIN_DISPLAY_CS);
// Re-claim shared HSPI bus — SD.begin() steals GPIO 36/47/33
extern SPIClass displaySpi;
displaySpi.begin(PIN_DISPLAY_SCLK, 47, PIN_DISPLAY_MOSI, PIN_DISPLAY_CS);
Serial.printf("SD card: %s\n", sdCardReady ? "ready" : "FAILED");
}
// Start cellular MQTT
// Start MQTT backhaul
#ifdef HAS_4G_MODEM
if (sdCardReady) {
cellularMQTT.begin();
Serial.println("Cellular MQTT starting...");
@@ -145,6 +140,17 @@ Serial.printf("SD card: %s\n", sdCardReady ? "ready" : "FAILED");
}
#endif
#ifdef MECK_WIFI_REMOTE
if (sdCardReady) {
wifiMQTT.begin();
Serial.println("WiFi MQTT starting...");
} else {
Serial.println("WiFi MQTT skipped — no SD card for config");
}
#endif
#endif // HAS_4G_MODEM || MECK_WIFI_REMOTE
#ifdef DISPLAY_CLASS
ui_task.begin(the_mesh.getNodePrefs(), FIRMWARE_BUILD_DATE, FIRMWARE_VERSION);
#endif
@@ -181,22 +187,12 @@ void loop() {
}
// ---------------------------------------------------------------------------
// MQTT → CLI bridge: process incoming commands from MQTT
// MQTT → CLI bridge: process incoming commands from MQTT (cellular)
// ---------------------------------------------------------------------------
#ifdef HAS_4G_MODEM
{
MQTTCommand mqttCmd;
while (cellularMQTT.recvCommand(mqttCmd)) {
// Check for OTA command
if (strncmp(mqttCmd.cmd, "ota:", 4) == 0) {
const char* url = &mqttCmd.cmd[4];
Serial.printf("[MQTT] OTA request: %s\n", url);
// TODO: RemoteOTA — download firmware from URL and flash
cellularMQTT.sendResponse(cellularMQTT.getRspTopic(), "{\"ota\":\"not yet implemented\"}");
continue;
}
// CLI command — process through the same handler as serial/LoRa admin
Serial.printf("[MQTT] CLI: %s\n", mqttCmd.cmd);
char reply[512];
reply[0] = '\0';
@@ -209,7 +205,7 @@ void loop() {
}
}
// Periodic telemetry snapshot for MQTT publishing
// Periodic telemetry snapshot for cellular MQTT
{
static unsigned long lastTelemUpdate = 0;
if (millis() - lastTelemUpdate > 10000) {
@@ -238,6 +234,54 @@ void loop() {
}
#endif
// ---------------------------------------------------------------------------
// MQTT → CLI bridge: process incoming commands from MQTT (WiFi)
// ---------------------------------------------------------------------------
#ifdef MECK_WIFI_REMOTE
wifiMQTT.loop();
{
MQTTCommand mqttCmd;
while (wifiMQTT.recvCommand(mqttCmd)) {
Serial.printf("[MQTT] CLI: %s\n", mqttCmd.cmd);
char reply[512];
reply[0] = '\0';
the_mesh.handleCommand((uint32_t)time(nullptr), mqttCmd.cmd, reply);
if (reply[0] == '\0') strcpy(reply, "OK");
wifiMQTT.sendResponse(wifiMQTT.getRspTopic(), reply);
Serial.printf("[MQTT] Reply: %.80s\n", reply);
}
}
// Periodic telemetry snapshot for WiFi MQTT
{
static unsigned long lastTelemUpdate = 0;
if (millis() - lastTelemUpdate > 10000) {
NodePrefs* p = the_mesh.getNodePrefs();
TelemetryData td;
memset(&td, 0, sizeof(td));
td.uptime_secs = millis() / 1000;
td.battery_mv = board.getBattMilliVolts();
td.battery_pct = board.getBatteryPercent();
td.temperature = board.getBattTemperature();
td.rssi = wifiMQTT.getRSSI();
td.freq = p->freq;
td.bw = p->bw;
td.sf = p->sf;
td.cr = p->cr;
td.tx_power = p->tx_power_dbm;
strncpy(td.node_name, p->node_name, sizeof(td.node_name) - 1);
td.mqtt_connected = wifiMQTT.isConnected();
td.neighbor_count = 0;
wifiMQTT.updateTelemetry(td);
lastTelemUpdate = millis();
}
}
#endif
the_mesh.loop();
sensors.loop();
#ifdef DISPLAY_CLASS
@@ -245,7 +289,7 @@ void loop() {
#endif
rtc_clock.tick();
#ifndef HAS_4G_MODEM
#if !defined(HAS_4G_MODEM) && !defined(MECK_WIFI_REMOTE)
if (the_mesh.getNodePrefs()->powersaving_enabled &&
the_mesh.millisHasNowPassed(lastActive + nextSleepinSecs * 1000)) {
if (!the_mesh.hasPendingWork()) {
+498
View File
@@ -0,0 +1,498 @@
#ifdef MECK_WIFI_REMOTE
#include "WiFiMQTT.h"
#include <esp_mac.h>
#include <Update.h>
#include <HTTPClient.h>
#include "target.h"
WiFiMQTT wifiMQTT;
#define WIFI_CONFIG_FILE "/remote/wifi.cfg"
#define MQTT_CONFIG_FILE "/remote/mqtt.cfg"
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
void WiFiMQTT::begin() {
Serial.println("[WiFi] begin()");
_state = WiFiMQTTState::OFF;
_cmdHead = _cmdTail = 0;
_rspHead = _rspTail = 0;
_activeNetwork = 0;
if (!loadConfig(_config)) {
Serial.println("[WiFi] ERROR: Missing config files — cannot start");
_state = WiFiMQTTState::ERROR;
return;
}
Serial.printf("[WiFi] Config: %d network(s), broker=%s:%d id=%s\n",
_config.networkCount, _config.broker, _config.port, _config.deviceId);
for (int i = 0; i < _config.networkCount; i++) {
Serial.printf("[WiFi] %d: %s\n", i + 1, _config.networks[i].ssid);
}
snprintf(_topicCmd, sizeof(_topicCmd), "meck/%s/cmd", _config.deviceId);
snprintf(_topicRsp, sizeof(_topicRsp), "meck/%s/rsp", _config.deviceId);
snprintf(_topicTelem, sizeof(_topicTelem), "meck/%s/telemetry", _config.deviceId);
snprintf(_topicOta, sizeof(_topicOta), "meck/%s/ota", _config.deviceId);
// Configure TLS — skip server cert verification (same as cellular)
_wifiClient.setInsecure();
_mqttClient.setClient(_wifiClient);
_mqttClient.setServer(_config.broker, _config.port);
_mqttClient.setCallback(mqttCallback);
_mqttClient.setBufferSize(MQTT_PAYLOAD_MAX + MQTT_TOPIC_MAX);
_state = WiFiMQTTState::WIFI_CONNECTING;
}
void WiFiMQTT::loop() {
if (_state == WiFiMQTTState::OFF || _state == WiFiMQTTState::ERROR) return;
// Check for pending OTA
if (_otaPending && _state == WiFiMQTTState::CONNECTED) {
performOTA();
return;
}
// WiFi connection management
if (WiFi.status() != WL_CONNECTED) {
if (_state == WiFiMQTTState::CONNECTED || _state == WiFiMQTTState::MQTT_CONNECTING) {
Serial.println("[WiFi] Connection lost");
_state = WiFiMQTTState::WIFI_CONNECTING;
}
if (millis() - _lastWifiAttempt > WIFI_RECONNECT_MS) {
connectWiFi();
_lastWifiAttempt = millis();
}
return;
}
// WiFi is up — check MQTT
if (!_mqttClient.connected()) {
if (_state == WiFiMQTTState::CONNECTED) {
Serial.println("[WiFi] MQTT disconnected");
}
_state = WiFiMQTTState::MQTT_CONNECTING;
if (millis() - _lastMqttAttempt > MQTT_RECONNECT_MS) {
connectMQTT();
_lastMqttAttempt = millis();
}
return;
}
// Connected — run MQTT loop
_mqttClient.loop();
// Publish queued responses
publishQueuedResponses();
// Periodic RSSI
if (millis() - _lastRSSI > 30000) {
_rssi = WiFi.RSSI();
_lastRSSI = millis();
}
// Periodic telemetry
if (millis() - _lastTelem > TELEMETRY_INTERVAL) {
publishTelemetry();
_lastTelem = millis();
}
}
bool WiFiMQTT::recvCommand(MQTTCommand& out) {
if (_cmdHead == _cmdTail) return false;
memcpy(&out, &_cmdBuf[_cmdTail], sizeof(MQTTCommand));
_cmdTail = (_cmdTail + 1) % CMD_QUEUE_SIZE;
return true;
}
bool WiFiMQTT::sendResponse(const char* topic, const char* payload) {
int next = (_rspHead + 1) % RSP_QUEUE_SIZE;
if (next == _rspTail) return false; // Full
memset(&_rspBuf[_rspHead], 0, sizeof(MQTTResponse));
strncpy(_rspBuf[_rspHead].topic, topic, MQTT_TOPIC_MAX - 1);
strncpy(_rspBuf[_rspHead].payload, payload, MQTT_PAYLOAD_MAX - 1);
_rspHead = next;
return true;
}
void WiFiMQTT::updateTelemetry(const TelemetryData& data) {
memcpy(&_telemetry, &data, sizeof(data));
}
void WiFiMQTT::requestOTA(const char* url) {
if (_state == WiFiMQTTState::OTA_IN_PROGRESS) return;
strncpy(_otaUrl, url, sizeof(_otaUrl) - 1);
_otaUrl[sizeof(_otaUrl) - 1] = '\0';
_otaPending = true;
Serial.printf("[OTA] Requested: %s\n", url);
}
int WiFiMQTT::getSignalBars() const {
if (_rssi == 0) return 0;
if (_rssi > -50) return 5;
if (_rssi > -60) return 4;
if (_rssi > -70) return 3;
if (_rssi > -80) return 2;
return 1;
}
const char* WiFiMQTT::stateString() const {
switch (_state) {
case WiFiMQTTState::OFF: return "OFF";
case WiFiMQTTState::WIFI_CONNECTING: return "WiFi...";
case WiFiMQTTState::WIFI_CONNECTED: return "WiFi OK";
case WiFiMQTTState::MQTT_CONNECTING: return "MQTT...";
case WiFiMQTTState::CONNECTED: return "CONNECTED";
case WiFiMQTTState::OTA_IN_PROGRESS: return "OTA";
case WiFiMQTTState::ERROR: return "ERROR";
default: return "???";
}
}
// ---------------------------------------------------------------------------
// Config files
//
// /remote/wifi.cfg — SSID/password pairs, two lines each:
// HomeNetwork
// HomePassword
// BackupNetwork
// BackupPassword
//
// /remote/mqtt.cfg — same format as cellular variant
// ---------------------------------------------------------------------------
bool WiFiMQTT::loadConfig(WiFiMQTTConfig& cfg) {
memset(&cfg, 0, sizeof(cfg));
// WiFi config: read SSID/password pairs
File wf = SD.open(WIFI_CONFIG_FILE, FILE_READ);
if (!wf) {
Serial.println("[WiFi] No /remote/wifi.cfg");
return false;
}
cfg.networkCount = 0;
while (wf.available() && cfg.networkCount < MAX_WIFI_NETWORKS) {
String ssid = wf.readStringUntil('\n'); ssid.trim();
if (ssid.length() == 0) break;
String pass = wf.readStringUntil('\n'); pass.trim();
strncpy(cfg.networks[cfg.networkCount].ssid, ssid.c_str(), sizeof(cfg.networks[0].ssid) - 1);
strncpy(cfg.networks[cfg.networkCount].password, pass.c_str(), sizeof(cfg.networks[0].password) - 1);
cfg.networkCount++;
}
wf.close();
if (cfg.networkCount == 0) {
Serial.println("[WiFi] No networks in wifi.cfg");
return false;
}
// MQTT config: /remote/mqtt.cfg (same format as cellular)
File mf = SD.open(MQTT_CONFIG_FILE, FILE_READ);
if (!mf) {
Serial.println("[WiFi] No /remote/mqtt.cfg");
return false;
}
String line;
line = mf.readStringUntil('\n'); line.trim();
strncpy(cfg.broker, line.c_str(), sizeof(cfg.broker) - 1);
line = mf.readStringUntil('\n'); line.trim();
cfg.port = line.length() > 0 ? line.toInt() : 8883;
line = mf.readStringUntil('\n'); line.trim();
strncpy(cfg.username, line.c_str(), sizeof(cfg.username) - 1);
line = mf.readStringUntil('\n'); line.trim();
strncpy(cfg.password, line.c_str(), sizeof(cfg.password) - 1);
if (mf.available()) {
line = mf.readStringUntil('\n'); line.trim();
if (line.length() > 0) {
strncpy(cfg.deviceId, line.c_str(), sizeof(cfg.deviceId) - 1);
}
}
mf.close();
// Auto-generate device ID if not provided
if (cfg.deviceId[0] == '\0') {
uint8_t mac[6];
esp_efuse_mac_get_default(mac);
snprintf(cfg.deviceId, sizeof(cfg.deviceId), "meck-%02x%02x%02x%02x",
mac[2], mac[3], mac[4], mac[5]);
}
return cfg.broker[0] != '\0';
}
// ---------------------------------------------------------------------------
// WiFi connection — tries each configured network in order
// ---------------------------------------------------------------------------
bool WiFiMQTT::connectWiFi() {
WiFi.mode(WIFI_STA);
for (int n = 0; n < _config.networkCount; n++) {
Serial.printf("[WiFi] Trying %s (%d/%d)...\n",
_config.networks[n].ssid, n + 1, _config.networkCount);
WiFi.begin(_config.networks[n].ssid, _config.networks[n].password);
unsigned long start = millis();
while (WiFi.status() != WL_CONNECTED && millis() - start < 10000) {
delay(100);
}
if (WiFi.status() == WL_CONNECTED) {
IPAddress ip = WiFi.localIP();
snprintf(_ipAddr, sizeof(_ipAddr), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
_rssi = WiFi.RSSI();
_activeNetwork = n;
Serial.printf("[WiFi] Connected to %s — IP: %s RSSI: %d\n",
_config.networks[n].ssid, _ipAddr, _rssi);
if (WiFi.status() == WL_CONNECTED) {
IPAddress ip = WiFi.localIP();
snprintf(_ipAddr, sizeof(_ipAddr), "%d.%d.%d.%d", ip[0], ip[1], ip[2], ip[3]);
_rssi = WiFi.RSSI();
_activeNetwork = n;
Serial.printf("[WiFi] Connected to %s — IP: %s RSSI: %d\n",
_config.networks[n].ssid, _ipAddr, _rssi);
// Sync clock via NTP
configTime(0, 0, "pool.ntp.org", "time.google.com");
Serial.print("[WiFi] NTP sync...");
int tries = 0;
while (time(nullptr) < 1700000000 && tries < 20) {
delay(500);
tries++;
}
time_t now = time(nullptr);
if (now > 1700000000) {
extern AutoDiscoverRTCClock rtc_clock;
rtc_clock.setCurrentTime((uint32_t)now);
Serial.printf(" OK (%lu)\n", (unsigned long)now);
} else {
Serial.println(" timeout");
}
_state = WiFiMQTTState::WIFI_CONNECTED;
return true;
}
}
WiFi.disconnect();
delay(500);
}
Serial.println("[WiFi] All networks failed");
return false;
}
// ---------------------------------------------------------------------------
// MQTT connection
// ---------------------------------------------------------------------------
bool WiFiMQTT::connectMQTT() {
Serial.printf("[WiFi] MQTT connecting to %s:%d...\n", _config.broker, _config.port);
char clientId[48];
snprintf(clientId, sizeof(clientId), "%s-%lu", _config.deviceId, millis() & 0xFFFF);
if (_mqttClient.connect(clientId, _config.username, _config.password)) {
Serial.println("[WiFi] MQTT connected!");
_mqttClient.subscribe(_topicCmd, 1);
_mqttClient.subscribe(_topicOta, 1);
_state = WiFiMQTTState::CONNECTED;
// Publish boot event
_mqttClient.publish(_topicTelem, "{\"event\":\"boot\",\"state\":\"connected\"}", true);
return true;
}
Serial.printf("[WiFi] MQTT connect failed, rc=%d\n", _mqttClient.state());
return false;
}
// ---------------------------------------------------------------------------
// MQTT message callback
// ---------------------------------------------------------------------------
void WiFiMQTT::mqttCallback(char* topic, byte* payload, unsigned int length) {
wifiMQTT.onMessage(topic, payload, length);
}
void WiFiMQTT::onMessage(char* topic, byte* payload, unsigned int length) {
char buf[MQTT_PAYLOAD_MAX];
int len = (length < MQTT_PAYLOAD_MAX - 1) ? length : MQTT_PAYLOAD_MAX - 1;
memcpy(buf, payload, len);
buf[len] = '\0';
Serial.printf("[WiFi] RX [%s]: %.80s\n", topic, buf);
if (strstr(topic, "/cmd")) {
int next = (_cmdHead + 1) % CMD_QUEUE_SIZE;
if (next != _cmdTail) {
memset(&_cmdBuf[_cmdHead], 0, sizeof(MQTTCommand));
strncpy(_cmdBuf[_cmdHead].cmd, buf, MQTT_PAYLOAD_MAX - 1);
_cmdHead = next;
Serial.printf("[WiFi] Queued CLI: %s\n", buf);
} else {
Serial.println("[WiFi] Command queue full");
}
} else if (strstr(topic, "/ota")) {
requestOTA(buf);
}
}
// ---------------------------------------------------------------------------
// Publish helpers
// ---------------------------------------------------------------------------
void WiFiMQTT::publishQueuedResponses() {
while (_rspHead != _rspTail) {
_mqttClient.publish(_rspBuf[_rspTail].topic, _rspBuf[_rspTail].payload);
_rspTail = (_rspTail + 1) % RSP_QUEUE_SIZE;
}
}
void WiFiMQTT::publishTelemetry() {
_rssi = WiFi.RSSI();
char json[400];
snprintf(json, sizeof(json),
"{\"uptime\":%lu,\"batt_mv\":%d,\"batt_pct\":%d,\"temp\":%.1f,"
"\"rssi\":%d,\"bars\":%d,\"neighbors\":%d,"
"\"freq\":%.3f,\"bw\":%.1f,\"sf\":%d,\"cr\":%d,\"tx\":%d,"
"\"name\":\"%s\",\"ip\":\"%s\",\"ssid\":\"%s\","
"\"heap\":%d}",
_telemetry.uptime_secs, _telemetry.battery_mv, _telemetry.battery_pct,
_telemetry.temperature / 10.0f,
_rssi, getSignalBars(), _telemetry.neighbor_count,
_telemetry.freq, _telemetry.bw, _telemetry.sf, _telemetry.cr, _telemetry.tx_power,
_telemetry.node_name, _ipAddr, _config.networks[_activeNetwork].ssid,
ESP.getFreeHeap());
_mqttClient.publish(_topicTelem, json);
}
// ---------------------------------------------------------------------------
// OTA — HTTP download over WiFi + ESP32 flash
// ---------------------------------------------------------------------------
void WiFiMQTT::performOTA() {
_otaPending = false;
_state = WiFiMQTTState::OTA_IN_PROGRESS;
Serial.printf("[OTA] URL: %s\n", _otaUrl);
_mqttClient.publish(_topicRsp, "OTA: Starting download...");
_mqttClient.loop();
HTTPClient http;
http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS);
http.setTimeout(180000);
if (!http.begin(_wifiClient, _otaUrl)) {
Serial.println("[OTA] HTTP begin failed");
_mqttClient.publish(_topicRsp, "OTA: HTTP begin failed");
_state = WiFiMQTTState::CONNECTED;
return;
}
int httpCode = http.GET();
if (httpCode != HTTP_CODE_OK) {
Serial.printf("[OTA] HTTP error: %d\n", httpCode);
char msg[60];
snprintf(msg, sizeof(msg), "OTA: HTTP error %d", httpCode);
_mqttClient.publish(_topicRsp, msg);
http.end();
_state = WiFiMQTTState::CONNECTED;
return;
}
int fileSize = http.getSize();
if (fileSize <= 0) {
Serial.println("[OTA] Unknown content length");
_mqttClient.publish(_topicRsp, "OTA: Unknown file size");
http.end();
_state = WiFiMQTTState::CONNECTED;
return;
}
Serial.printf("[OTA] File size: %d bytes\n", fileSize);
if (!Update.begin(fileSize)) {
Serial.printf("[OTA] Update.begin failed: %s\n", Update.errorString());
_mqttClient.publish(_topicRsp, "OTA: Flash init failed");
http.end();
_state = WiFiMQTTState::CONNECTED;
return;
}
WiFiClient* stream = http.getStreamPtr();
uint8_t buf[1024];
int offset = 0;
int lastPct = -1;
while (offset < fileSize) {
int avail = stream->available();
if (avail <= 0) {
if (!stream->connected()) break;
delay(10);
continue;
}
int toRead = (avail < (int)sizeof(buf)) ? avail : sizeof(buf);
int got = stream->readBytes(buf, toRead);
if (got <= 0) break;
size_t written = Update.write(buf, got);
if (written != (size_t)got) {
Serial.printf("[OTA] Write failed: %d of %d\n", written, got);
break;
}
offset += got;
int pct = (offset * 100) / fileSize;
if (pct / 10 != lastPct / 10) {
Serial.printf("[OTA] Progress: %d%% (%d/%d)\n", pct, offset, fileSize);
char msg[60];
snprintf(msg, sizeof(msg), "OTA: Flashing %d%%", pct);
_mqttClient.publish(_topicRsp, msg);
_mqttClient.loop();
lastPct = pct;
}
delay(1);
}
http.end();
if (offset < fileSize) {
Serial.printf("[OTA] Incomplete: %d of %d\n", offset, fileSize);
Update.abort();
_mqttClient.publish(_topicRsp, "OTA: Download incomplete");
_state = WiFiMQTTState::CONNECTED;
return;
}
if (!Update.end(true)) {
Serial.printf("[OTA] Update.end failed: %s\n", Update.errorString());
_mqttClient.publish(_topicRsp, "OTA: Verification failed");
_state = WiFiMQTTState::CONNECTED;
return;
}
Serial.println("[OTA] SUCCESS — rebooting in 3 seconds");
_mqttClient.publish(_topicRsp, "OTA: Success! Rebooting...");
_mqttClient.loop();
delay(3000);
ESP.restart();
}
#endif // MECK_WIFI_REMOTE
+188
View File
@@ -0,0 +1,188 @@
#pragma once
// =============================================================================
// WiFiMQTT — WiFi + MQTT for audio variant remote repeater
//
// Same interface as CellularMQTT but uses ESP32 native WiFi + PubSubClient.
// No modem, no AT commands, no FreeRTOS task — runs in the main loop.
//
// Supports multiple WiFi networks in wifi.cfg (SSID/password pairs).
// Tries each in order on connect/reconnect.
//
// Guard: MECK_WIFI_REMOTE (set in platformio env build_flags)
// =============================================================================
#ifdef MECK_WIFI_REMOTE
#ifndef WIFI_MQTT_H
#define WIFI_MQTT_H
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <SD.h>
// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------
#define MQTT_TOPIC_MAX 80
#define MQTT_PAYLOAD_MAX 512
#define MQTT_CLIENT_ID_MAX 32
#define CMD_QUEUE_SIZE 8
#define RSP_QUEUE_SIZE 8
#define MAX_WIFI_NETWORKS 4
#define TELEMETRY_INTERVAL 60000 // 60 seconds
#define WIFI_RECONNECT_MS 10000 // 10 seconds between WiFi reconnect attempts
#define MQTT_RECONNECT_MS 5000 // 5 seconds between MQTT reconnect attempts
// ---------------------------------------------------------------------------
// State machine
// ---------------------------------------------------------------------------
enum class WiFiMQTTState : uint8_t {
OFF,
WIFI_CONNECTING,
WIFI_CONNECTED,
MQTT_CONNECTING,
CONNECTED,
OTA_IN_PROGRESS,
ERROR
};
// ---------------------------------------------------------------------------
// Queue message types (same as CellularMQTT for compatibility)
// ---------------------------------------------------------------------------
struct MQTTCommand {
char cmd[MQTT_PAYLOAD_MAX];
};
struct MQTTResponse {
char topic[MQTT_TOPIC_MAX];
char payload[MQTT_PAYLOAD_MAX];
};
// ---------------------------------------------------------------------------
// Config (loaded from SD)
// ---------------------------------------------------------------------------
struct WiFiNetwork {
char ssid[40];
char password[64];
};
struct WiFiMQTTConfig {
WiFiNetwork networks[MAX_WIFI_NETWORKS];
int networkCount;
char broker[80];
uint16_t port; // 8883 for MQTT TLS
char username[40];
char password[40];
char deviceId[MQTT_CLIENT_ID_MAX];
};
// ---------------------------------------------------------------------------
// Telemetry snapshot
// ---------------------------------------------------------------------------
struct TelemetryData {
uint32_t uptime_secs;
uint16_t battery_mv;
uint8_t battery_pct;
int16_t temperature;
int rssi;
uint8_t neighbor_count;
float freq;
float bw;
uint8_t sf;
uint8_t cr;
uint8_t tx_power;
char node_name[32];
bool mqtt_connected;
};
// ---------------------------------------------------------------------------
// WiFiMQTT class
// ---------------------------------------------------------------------------
class WiFiMQTT {
public:
void begin();
void loop(); // Call from main loop — handles WiFi, MQTT, publish/subscribe
// --- Queue API (called from main loop) ---
bool recvCommand(MQTTCommand& out);
bool sendResponse(const char* topic, const char* payload);
// --- Telemetry ---
void updateTelemetry(const TelemetryData& data);
// --- OTA ---
void requestOTA(const char* url);
bool isOTAInProgress() const { return _state == WiFiMQTTState::OTA_IN_PROGRESS; }
// --- State queries ---
WiFiMQTTState getState() const { return _state; }
bool isConnected() const { return _state == WiFiMQTTState::CONNECTED; }
int getRSSI() const { return _rssi; }
int getSignalBars() const;
const char* getSSID() const { return _config.networks[_activeNetwork].ssid; }
const char* getIPAddress() const { return _ipAddr; }
const char* getBroker() const { return _config.broker; }
const char* getRspTopic() const { return _topicRsp; }
const char* stateString() const;
static bool loadConfig(WiFiMQTTConfig& cfg);
private:
WiFiMQTTState _state = WiFiMQTTState::OFF;
int _rssi = 0;
int _activeNetwork = 0;
char _ipAddr[20] = {0};
WiFiMQTTConfig _config = {};
TelemetryData _telemetry = {};
// Topic strings
char _topicCmd[MQTT_TOPIC_MAX] = {0};
char _topicRsp[MQTT_TOPIC_MAX] = {0};
char _topicTelem[MQTT_TOPIC_MAX] = {0};
char _topicOta[MQTT_TOPIC_MAX] = {0};
// Command/response ring buffers (no FreeRTOS queues needed — single-threaded)
MQTTCommand _cmdBuf[CMD_QUEUE_SIZE];
int _cmdHead = 0, _cmdTail = 0;
MQTTResponse _rspBuf[RSP_QUEUE_SIZE];
int _rspHead = 0, _rspTail = 0;
// MQTT client stack
WiFiClientSecure _wifiClient;
PubSubClient _mqttClient;
// Timers
unsigned long _lastWifiAttempt = 0;
unsigned long _lastMqttAttempt = 0;
unsigned long _lastTelem = 0;
unsigned long _lastRSSI = 0;
// OTA state
bool _otaPending = false;
char _otaUrl[256] = {0};
// --- Internal ---
bool connectWiFi();
bool connectMQTT();
void publishTelemetry();
void publishQueuedResponses();
void performOTA();
// PubSubClient callback (static → instance)
static void mqttCallback(char* topic, byte* payload, unsigned int length);
void onMessage(char* topic, byte* payload, unsigned int length);
};
extern WiFiMQTT wifiMQTT;
#endif // WIFI_MQTT_H
#endif // MECK_WIFI_REMOTE
+11 -8
View File
@@ -81,7 +81,8 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
file.read((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
file.read((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
file.read((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
// 290
file.read((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 290
// 291
// sanitise bad pref values
_prefs->rx_delay_base = constrain(_prefs->rx_delay_base, 0, 20.0f);
@@ -107,6 +108,7 @@ void CommonCLI::loadPrefsInt(FILESYSTEM* fs, const char* filename) {
_prefs->gps_enabled = constrain(_prefs->gps_enabled, 0, 1);
_prefs->advert_loc_policy = constrain(_prefs->advert_loc_policy, 0, 2);
_prefs->path_hash_mode = constrain(_prefs->path_hash_mode, 0, 2);
file.close();
}
@@ -165,7 +167,8 @@ void CommonCLI::savePrefs(FILESYSTEM* fs) {
file.write((uint8_t *)&_prefs->discovery_mod_timestamp, sizeof(_prefs->discovery_mod_timestamp)); // 162
file.write((uint8_t *)&_prefs->adc_multiplier, sizeof(_prefs->adc_multiplier)); // 166
file.write((uint8_t *)_prefs->owner_info, sizeof(_prefs->owner_info)); // 170
// 290
file.write((uint8_t *)&_prefs->path_hash_mode, sizeof(_prefs->path_hash_mode)); // 290
// 291
file.close();
}
@@ -285,7 +288,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
sprintf(reply, "> %d", ((uint32_t) _prefs->advert_interval) * 2);
} else if (memcmp(config, "guest.password", 14) == 0) {
sprintf(reply, "> %s", _prefs->guest_password);
} else if (sender_timestamp == 0 && memcmp(config, "prv.key", 7) == 0) { // from serial command line only
} else if (memcmp(config, "prv.key", 7) == 0) { // from serial command line only
uint8_t prv_key[PRV_KEY_SIZE];
int len = _callbacks->getSelfId().writeTo(prv_key, PRV_KEY_SIZE);
mesh::Utils::toHex(tmp, prv_key, len);
@@ -545,7 +548,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
savePrefs();
_callbacks->setTxPower(_prefs->tx_power_dbm);
strcpy(reply, "OK");
} else if (sender_timestamp == 0 && memcmp(config, "freq ", 5) == 0) {
} else if (memcmp(config, "freq ", 5) == 0) {
_prefs->freq = atof(&config[5]);
savePrefs();
strcpy(reply, "OK - reboot to apply");
@@ -767,13 +770,13 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
} else if (sender_timestamp == 0 && memcmp(command, "log", 3) == 0) {
_callbacks->dumpLogFile();
strcpy(reply, " EOF");
} else if (sender_timestamp == 0 && memcmp(command, "stats-packets", 13) == 0 && (command[13] == 0 || command[13] == ' ')) {
} else if (memcmp(command, "stats-packets", 13) == 0 && (command[13] == 0 || command[13] == ' ')) {
_callbacks->formatPacketStatsReply(reply);
} else if (sender_timestamp == 0 && memcmp(command, "stats-radio", 11) == 0 && (command[11] == 0 || command[11] == ' ')) {
} else if (memcmp(command, "stats-radio", 11) == 0 && (command[11] == 0 || command[11] == ' ')) {
_callbacks->formatRadioStatsReply(reply);
} else if (sender_timestamp == 0 && memcmp(command, "stats-core", 10) == 0 && (command[10] == 0 || command[10] == ' ')) {
} else if (memcmp(command, "stats-core", 10) == 0 && (command[10] == 0 || command[10] == ' ')) {
_callbacks->formatStatsReply(reply);
} else {
strcpy(reply, "Unknown command");
}
}
}
+3 -1
View File
@@ -52,6 +52,8 @@ struct NodePrefs { // persisted to file
uint32_t discovery_mod_timestamp;
float adc_multiplier;
char owner_info[120];
// Multi-byte path hash support (added for Meck remote repeater)
uint8_t path_hash_mode; // 0=1-byte (legacy), 1=2-byte, 2=3-byte path hashes
};
class CommonCLICallbacks {
@@ -110,4 +112,4 @@ public:
void savePrefs(FILESYSTEM* _fs);
void handleCommand(uint32_t sender_timestamp, const char* command, char* reply);
uint8_t buildAdvertData(uint8_t node_type, uint8_t* app_data);
};
};
+20 -2
View File
@@ -298,8 +298,9 @@ build_flags =
-D HAS_4G_MODEM=1
-D DISABLE_WIFI_OTA=1
-D MECK_REMOTE_REPEATER=1
-D MAX_NEIGHBOURS=16
-D FIRMWARE_VERSION='"Meck RemRptr v0.1"'
-D MAX_NEIGHBOURS=50
-D FIRMWARE_VERSION='"Meck RemRptr v0.2"'
-D FIRMWARE_BUILD_DATE='"3 April 2026"'
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
@@ -307,4 +308,21 @@ build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<../examples/simple_repeater/*.cpp>
lib_deps =
${LilyGo_TDeck_Pro.lib_deps}
[env:meck_wifi_repeater]
extends = LilyGo_TDeck_Pro
build_src_filter = ${LilyGo_TDeck_Pro.build_src_filter}
+<helpers/esp32/*.cpp>
+<helpers/ui/MomentaryButton.cpp>
+<helpers/ui/GxEPDDisplay.cpp>
+<../examples/simple_repeater/*.cpp>
build_flags =
${LilyGo_TDeck_Pro.build_flags}
-D FIRMWARE_VERSION='"Meck WiFi Rptr v0.2"'
-D FIRMWARE_BUILD_DATE='"3 April 2026"'
-D MAX_NEIGHBOURS=50
-D MECK_WIFI_REMOTE
lib_deps =
${LilyGo_TDeck_Pro.lib_deps}
knolleary/PubSubClient@^2.8