diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 28963f7a..57a292eb 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -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) diff --git a/examples/simple_repeater/MyMesh.h b/examples/simple_repeater/MyMesh.h index 282fc8c2..e0e590c3 100644 --- a/examples/simple_repeater/MyMesh.h +++ b/examples/simple_repeater/MyMesh.h @@ -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" diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index 57b2d4ab..38566760 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -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; } diff --git a/examples/simple_repeater/main.cpp b/examples/simple_repeater/main.cpp index 7bcefb70..51fb0b8f 100644 --- a/examples/simple_repeater/main.cpp +++ b/examples/simple_repeater/main.cpp @@ -8,6 +8,11 @@ #include "CellularMQTT.h" #endif +#ifdef MECK_WIFI_REMOTE +#include +#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()) { diff --git a/examples/simple_repeater/wifimqtt.cpp b/examples/simple_repeater/wifimqtt.cpp new file mode 100644 index 00000000..a8212235 --- /dev/null +++ b/examples/simple_repeater/wifimqtt.cpp @@ -0,0 +1,498 @@ +#ifdef MECK_WIFI_REMOTE + +#include "WiFiMQTT.h" +#include +#include +#include +#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 \ No newline at end of file diff --git a/examples/simple_repeater/wifimqtt.h b/examples/simple_repeater/wifimqtt.h new file mode 100644 index 00000000..4dc3b5a1 --- /dev/null +++ b/examples/simple_repeater/wifimqtt.h @@ -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 +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// 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 \ No newline at end of file diff --git a/src/helpers/CommonCLI.cpp b/src/helpers/CommonCLI.cpp index a8dd9d09..9373589a 100644 --- a/src/helpers/CommonCLI.cpp +++ b/src/helpers/CommonCLI.cpp @@ -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"); } -} +} \ No newline at end of file diff --git a/src/helpers/CommonCLI.h b/src/helpers/CommonCLI.h index b0530102..5acf51db 100644 --- a/src/helpers/CommonCLI.h +++ b/src/helpers/CommonCLI.h @@ -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); -}; +}; \ No newline at end of file diff --git a/variants/lilygo_tdeck_pro/platformio.ini b/variants/lilygo_tdeck_pro/platformio.ini index a805afd9..e74dbbfc 100644 --- a/variants/lilygo_tdeck_pro/platformio.ini +++ b/variants/lilygo_tdeck_pro/platformio.ini @@ -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} + + @@ -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} + + + + + + + +<../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 \ No newline at end of file