mirror of
https://github.com/pelgraine/Meck.git
synced 2026-05-09 23:04:53 +02:00
simple remote wifi repeater v0.2 & remote repeater path hash mode improvements
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
};
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user