From f7f5a3e10bf63026ad9f6dd8bca3cd34dd0bcaf8 Mon Sep 17 00:00:00 2001 From: pelgraine <140762863+pelgraine@users.noreply.github.com> Date: Wed, 14 Oct 2026 07:03:29 +1100 Subject: [PATCH] Companion repeat mode (upstream v9) and path hash mode in device info - Device info reply now ends with byte 80 (repeat on/off) and byte 81 (path hash mode), as upstream v9/v10, so the app can show Meck's path hash size. - Companion repeat mode, switched on only from the app: forwards packets while on. The app's radio command refuses repeat except on 433.000, 869.495 or 918.000 MHz; command 60 returns those frequencies. Stored at prefs byte 62 (spare, so existing devices start with it off). - While repeat is on, Settings, the serial CLI and import accept only those three frequencies. Settings shows a pop-up (English and French). - main.cpp: meckShowAlert now defined in every display build (was WiFi builds only), for the Settings pop-up. EOF Updated firmware versions for the Max and firmware build date. --- examples/companion_radio/DataStore.cpp | 4 +- examples/companion_radio/MeckImport.h | 8 +- examples/companion_radio/MyMesh.cpp | 75 ++++++++++++++++++- examples/companion_radio/MyMesh.h | 12 ++- examples/companion_radio/NodePrefs.h | 6 ++ examples/companion_radio/main.cpp | 8 +- .../companion_radio/ui-new/SettingsScreen.h | 23 +++++- variants/lilygo_tdeck_max/platformio.ini | 14 ++-- 8 files changed, 125 insertions(+), 25 deletions(-) diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 1fd52407..f36bed74 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -195,7 +195,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no file.read((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56 file.read((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60 file.read((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61 - file.read(pad, 1); // 62 + file.read((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat)); // 62 file.read((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63 file.read((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64 file.read((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68 @@ -353,7 +353,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ file.write((uint8_t *)&_prefs.freq, sizeof(_prefs.freq)); // 56 file.write((uint8_t *)&_prefs.sf, sizeof(_prefs.sf)); // 60 file.write((uint8_t *)&_prefs.cr, sizeof(_prefs.cr)); // 61 - file.write(pad, 1); // 62 + file.write((uint8_t *)&_prefs.client_repeat, sizeof(_prefs.client_repeat)); // 62 file.write((uint8_t *)&_prefs.manual_add_contacts, sizeof(_prefs.manual_add_contacts)); // 63 file.write((uint8_t *)&_prefs.bw, sizeof(_prefs.bw)); // 64 file.write((uint8_t *)&_prefs.tx_power_dbm, sizeof(_prefs.tx_power_dbm)); // 68 diff --git a/examples/companion_radio/MeckImport.h b/examples/companion_radio/MeckImport.h index ce96f578..ac481285 100644 --- a/examples/companion_radio/MeckImport.h +++ b/examples/companion_radio/MeckImport.h @@ -316,8 +316,12 @@ static int meckImportConfig(MyMesh& mesh, double freq = atof(val); // Auto-detect units: >3000 = kHz, <=3000 = MHz if (freq > 3000.0) freq /= 1000.0; - prefs->freq = (float)freq; - prefsChanged = true; + if (mesh.isFreqChangeAllowed((float)freq)) { + prefs->freq = (float)freq; + prefsChanged = true; + } else { + Serial.printf("Config Import: frequency %.3f skipped (repeat mode is on)\n", freq); + } } else if (strcmp(key, "bandwidth") == 0) { double bw = atof(val); diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index bbb6c049..013f7280 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -69,6 +69,7 @@ #define CMD_SEND_ANON_REQ 57 #define CMD_SET_AUTOADD_CONFIG 58 #define CMD_GET_AUTOADD_CONFIG 59 +#define CMD_GET_ALLOWED_REPEAT_FREQ 60 #define CMD_SET_PATH_HASH_MODE 61 #define CMD_SEND_CHANNEL_DATA 62 #define CMD_SET_DEFAULT_FLOOD_SCOPE 63 // v1.15+ (device-wide default scope name+key) @@ -696,6 +697,10 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) { return false; // never filter — let normal processing continue } +bool MyMesh::allowPacketForward(const mesh::Packet* packet) { + return _prefs.client_repeat != 0; +} + uint8_t MyMesh::getHeardBy(const uint8_t* payload, uint16_t payload_len, uint8_t& out_bph, uint8_t& out_count, uint8_t* out_hash, int8_t* out_snr, uint8_t max_src) const { @@ -1872,6 +1877,37 @@ uint32_t MyMesh::getBLEPin() { return _active_ble_pin; } +struct FreqRange { + uint32_t lower_freq, upper_freq; +}; + +// Companion repeat mode: the only frequencies it may be on (kHz), as +// upstream MeshCore. The Settings alert and serial CLI messages list them. +static FreqRange repeat_freq_ranges[] = { + { 433000, 433000 }, + { 869495, 869495 }, + { 918000, 918000 } +}; + +bool MyMesh::isValidClientRepeatFreq(uint32_t f) const { + for (int i = 0; i < sizeof(repeat_freq_ranges)/sizeof(repeat_freq_ranges[0]); i++) { + auto r = &repeat_freq_ranges[i]; + if (f >= r->lower_freq && f <= r->upper_freq) return true; + } + return false; +} + +bool MyMesh::isFreqChangeAllowed(float freq_mhz) const { + if (_prefs.client_repeat == 0) return true; + return isValidClientRepeatFreq((uint32_t)(freq_mhz * 1000.0f + 0.5f)); // MHz to kHz, rounded +} + +// Serial CLI reply when a frequency change is refused (repeat mode is on) +static void printRepeatFreqRefusal() { + Serial.println(" Error: repeat mode is on, so freq must be 433.000, 869.495 or 918.000"); + Serial.println(" (turn repeat mode off in the companion app to use other frequencies)"); +} + void MyMesh::startInterface(BaseSerialInterface &serial) { _serial = &serial; #if defined(ESP32) && defined(BLE_PIN_CODE) && !defined(WIFI_SSID) && !defined(MECK_WIFI_COMPANION) @@ -1904,6 +1940,8 @@ void MyMesh::handleCmdFrame(size_t len) { i += 40; StrHelper::strzcpy((char *)&out_frame[i], FIRMWARE_VERSION, 20); i += 20; + out_frame[i++] = _prefs.client_repeat ? 1 : 0; // v9+ + out_frame[i++] = _prefs.path_hash_mode; // v10+ _serial->writeFrame(out_frame, i); } else if (cmd_frame[0] == CMD_APP_START && len >= 8) { // sent when app establishes connection, respond with node ID @@ -2457,13 +2495,20 @@ void MyMesh::handleCmdFrame(size_t len) { i += 4; uint8_t sf = cmd_frame[i++]; uint8_t cr = cmd_frame[i++]; + uint8_t repeat = 0; // default - false + if (len > i) { + repeat = cmd_frame[i++]; // FIRMWARE_VER_CODE 9+ + } - if (freq >= 300000 && freq <= 2500000 && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7000 && + if (repeat && !isValidClientRepeatFreq(freq)) { + writeErrFrame(ERR_CODE_ILLEGAL_ARG); + } else if (freq >= 300000 && freq <= 2500000 && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8 && bw >= 7000 && bw <= 500000) { _prefs.sf = sf; _prefs.cr = cr; _prefs.freq = (float)freq / 1000.0; _prefs.bw = (float)bw / 1000.0; + _prefs.client_repeat = repeat ? 1 : 0; savePrefs(); radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); @@ -3056,6 +3101,15 @@ void MyMesh::handleCmdFrame(size_t len) { out_frame[i++] = RESP_CODE_AUTOADD_CONFIG; out_frame[i++] = _prefs.autoadd_config; _serial->writeFrame(out_frame, i); + } else if (cmd_frame[0] == CMD_GET_ALLOWED_REPEAT_FREQ) { + int i = 0; + out_frame[i++] = RESP_ALLOWED_REPEAT_FREQ; + for (int k = 0; k < sizeof(repeat_freq_ranges)/sizeof(repeat_freq_ranges[0]) && i + 8 < sizeof(out_frame); k++) { + auto r = &repeat_freq_ranges[k]; + memcpy(&out_frame[i], &r->lower_freq, 4); i += 4; + memcpy(&out_frame[i], &r->upper_freq, 4); i += 4; + } + _serial->writeFrame(out_frame, i); #if defined(BLE_PIN_CODE) && defined(MECK_WIFI_COMPANION) } else if (cmd_frame[0] == CMD_RUN_CLI_COMMAND && len >= 3) { // App command line (upstream MeshCore, v14+). This build answers @@ -3369,7 +3423,9 @@ void MyMesh::checkCLIRescueCmd() { } else if (memcmp(config, "freq ", 5) == 0) { float f = atof(&config[5]); - if (f >= 400.0f && f <= 928.0f) { + if (f >= 400.0f && f <= 928.0f && !isFreqChangeAllowed(f)) { + printRepeatFreqRefusal(); + } else if (f >= 400.0f && f <= 928.0f) { _prefs.freq = f; savePrefs(); radio_set_params(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); @@ -3472,7 +3528,9 @@ void MyMesh::checkCLIRescueCmd() { float bw = strtof(parts[1], nullptr); int sf = atoi(parts[2]); int cr = atoi(parts[3]); - if (freq >= 400.0f && freq <= 928.0f && bw >= 7.8f && bw <= 500.0f + if (freq >= 400.0f && freq <= 928.0f && !isFreqChangeAllowed(freq)) { + printRepeatFreqRefusal(); + } else if (freq >= 400.0f && freq <= 928.0f && bw >= 7.8f && bw <= 500.0f && sf >= 5 && sf <= 12 && cr >= 5 && cr <= 8) { _prefs.freq = freq; _prefs.bw = bw; @@ -3496,6 +3554,11 @@ void MyMesh::checkCLIRescueCmd() { bool found = false; for (int i = 0; i < (int)NUM_RADIO_PRESETS; i++) { if (strcasecmp(RADIO_PRESETS[i].name, name) == 0) { + if (!isFreqChangeAllowed(RADIO_PRESETS[i].freq)) { + printRepeatFreqRefusal(); + found = true; // matched, but not applied + break; + } _prefs.freq = RADIO_PRESETS[i].freq; _prefs.bw = RADIO_PRESETS[i].bw; _prefs.sf = RADIO_PRESETS[i].sf; @@ -3515,7 +3578,11 @@ void MyMesh::checkCLIRescueCmd() { if (!found) { char* endp; long idx = strtol(name, &endp, 10); - if (endp != name && *endp == '\0' && idx >= 0 && idx < (int)NUM_RADIO_PRESETS) { + if (endp != name && *endp == '\0' && idx >= 0 && idx < (int)NUM_RADIO_PRESETS + && !isFreqChangeAllowed(RADIO_PRESETS[idx].freq)) { + printRepeatFreqRefusal(); + found = true; // matched, but not applied + } else if (endp != name && *endp == '\0' && idx >= 0 && idx < (int)NUM_RADIO_PRESETS) { _prefs.freq = RADIO_PRESETS[idx].freq; _prefs.bw = RADIO_PRESETS[idx].bw; _prefs.sf = RADIO_PRESETS[idx].sf; diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 86e3fa30..736921e3 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -8,11 +8,11 @@ #define FIRMWARE_VER_CODE 11 #ifndef FIRMWARE_BUILD_DATE -#define FIRMWARE_BUILD_DATE "10 Oct 2026" +#define FIRMWARE_BUILD_DATE "14 Oct 2026" #endif #ifndef FIRMWARE_VERSION -#define FIRMWARE_VERSION "Meck v1.15" +#define FIRMWARE_VERSION "Meck v1.15.1" #endif #if defined(STM32_PLATFORM) @@ -238,6 +238,12 @@ public: // otherwise the candidate region name. Used by the channel path-detail overlay. const char* getScopeName(uint8_t idx) const; + // Companion repeat mode (switched on from the app): while it is on, the + // frequency may only change to an allowed repeat frequency. True when a + // change to freq_mhz is allowed (repeat off, or an allowed frequency). + // Public: SettingsScreen and MeckImport call it before changing freq. + bool isFreqChangeAllowed(float freq_mhz) const; + protected: float getAirtimeBudgetFactor() const override; @@ -254,6 +260,7 @@ protected: uint8_t getExtraAckTransmitCount() const override; uint8_t getAutoAddMaxHops() const override; bool filterRecvFloodPacket(mesh::Packet* packet) override; + bool allowPacketForward(const mesh::Packet* packet) override; uint8_t getPathHashSize() const override { return _prefs.path_hash_mode + 1; } void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0) override; @@ -364,6 +371,7 @@ private: void checkCLIRescueCmd(); void checkSerialInterface(); + bool isValidClientRepeatFreq(uint32_t f) const; DataStore* _store; NodePrefs _prefs; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 4bd54a5c..6f171419 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -118,6 +118,12 @@ struct NodePrefs { // persisted to file // final field and keep the default 0 (off). uint8_t agc_reset_interval; // default 0 + // Companion repeat mode (upstream MeshCore, protocol v9): 1 = this + // companion also forwards packets, as a repeater does. Switched on only + // from the app, on an allowed repeat frequency. Stored at prefs byte 62, + // which was spare, so older prefs files load it as 0 (off). + uint8_t client_repeat; // default 0 + // --- Font helpers (inline, no overhead) --- // Returns the DisplayDriver text-size index for "small/body" text. // T-Deck Pro: 0 = built-in 6×8 (or 7pt with custom fonts), 1 = 9pt. diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index b95a3caa..d49d705a 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -884,11 +884,11 @@ static uint32_t _atoi(const char* sp) { // the lock state without pulling in the full UITask header. bool meck_alarm_is_locked() { return ui_task.isLocked(); } #endif - #ifdef MECK_WIFI_COMPANION - // Lets the Settings WiFi setup (which only forward-declares UITask) show - // its Connecting / IP / Could not connect popups. + // Lets the Settings screen (which only forward-declares UITask) show its + // popups: WiFi Connecting / IP / Could not connect, and the repeat mode + // frequency refusal, which any display build can show. void meckShowAlert(const char* text, int duration_millis) { ui_task.showAlert(text, duration_millis); } - + #ifdef MECK_WIFI_COMPANION // Why the last WiFi connection attempt failed. The WiFi driver reports a // reason code each time a connection attempt drops; it is recorded here so // the "Could not connect" popups and the serial log can say why. diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index feba4d1b..bf48cb50 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -734,6 +734,14 @@ private: _prefs->freq, _prefs->bw, _prefs->sf, _prefs->cr, _prefs->tx_power_dbm); } + // Companion repeat mode is on (switched on from the app): a frequency + // change was refused because it is not an allowed repeat frequency + void alertRepeatFreq() { + extern void meckShowAlert(const char* text, int duration_millis); + meckShowAlert(MECK_TR("Repeat mode on: only\n433, 869.495 or 918 MHz", + "R\xC3\xA9p\xC3\xA9teur actif : seuls\n433, 869.495, 918 MHz"), 2500); + } + public: SettingsScreen(UITask* task, mesh::RTCClock* rtc, NodePrefs* prefs) : _task(task), _rtc(rtc), _prefs(prefs), @@ -3351,9 +3359,13 @@ public: if (_editPos > 0) { float f = strtof(_editBuf, nullptr); f = constrain(f, 400.0f, 2500.0f); - _prefs->freq = f; - _radioChanged = true; - Serial.printf("Settings: Freq typed to %.3f\n", f); + if (!the_mesh.isFreqChangeAllowed(f)) { + alertRepeatFreq(); + } else { + _prefs->freq = f; + _radioChanged = true; + Serial.printf("Settings: Freq typed to %.3f\n", f); + } } _editMode = EDIT_NONE; } else if (type == ROW_ADD_CHANNEL) { @@ -3552,7 +3564,10 @@ public: _prefs->ui_font_style); } else { // Apply radio preset - if (_editPickerIdx >= 0 && _editPickerIdx < (int)NUM_RADIO_PRESETS) { + if (_editPickerIdx >= 0 && _editPickerIdx < (int)NUM_RADIO_PRESETS + && !the_mesh.isFreqChangeAllowed(RADIO_PRESETS[_editPickerIdx].freq)) { + alertRepeatFreq(); + } else if (_editPickerIdx >= 0 && _editPickerIdx < (int)NUM_RADIO_PRESETS) { const RadioPreset& p = RADIO_PRESETS[_editPickerIdx]; _prefs->freq = p.freq; _prefs->bw = p.bw; diff --git a/variants/lilygo_tdeck_max/platformio.ini b/variants/lilygo_tdeck_max/platformio.ini index cbccee40..63d32284 100644 --- a/variants/lilygo_tdeck_max/platformio.ini +++ b/variants/lilygo_tdeck_max/platformio.ini @@ -179,7 +179,7 @@ build_flags = ; -D MECK_BLE_SMALL_MTU_SPLIT=1 ; -D MECK_WATCH_EXT=1 ; -D MECK_BLE_SLOW_INTERVAL=1 - -D FIRMWARE_VERSION='"Meck v1.15.MAX"' + -D FIRMWARE_VERSION='"Meck v1.15.1.MAX"' build_src_filter = ${LilyGo_TDeck_Pro_Max.build_src_filter} + + @@ -213,7 +213,7 @@ build_flags = -D MECK_BLE_SMALL_MTU_SPLIT=1 -D MECK_WATCH_EXT=1 -D MECK_BLE_SLOW_INTERVAL=1 - -D FIRMWARE_VERSION='"Meck v1.15.MAX"' + -D FIRMWARE_VERSION='"Meck v1.15.1.MAX"' build_src_filter = ${LilyGo_TDeck_Pro_Max.build_src_filter} + + @@ -250,7 +250,7 @@ build_flags = -D MECK_OTA_UPDATE=1 -D EINK_FASTLUT_EXPERIMENT=1 -D FAST_LUT_FRAMES=9 - -D FIRMWARE_VERSION='"Meck v1.15.MAX.FASTLUT"' + -D FIRMWARE_VERSION='"Meck v1.15.1.MAX.FASTLUT"' build_src_filter = ${LilyGo_TDeck_Pro_Max.build_src_filter} + + @@ -282,7 +282,7 @@ build_flags = -D MECK_WEB_READER=1 -D MECK_OTA_UPDATE=1 -D HAS_4G_MODEM=1 - -D FIRMWARE_VERSION='"Meck v1.15.MAX.WiFi"' + -D FIRMWARE_VERSION='"Meck v1.15.1.MAX.WiFi"' build_src_filter = ${LilyGo_TDeck_Pro_Max.build_src_filter} + + @@ -316,7 +316,7 @@ build_flags = -D MECK_WEB_READER=1 -D MECK_OTA_UPDATE=1 -D HAS_4G_MODEM=1 - -D FIRMWARE_VERSION='"Meck v1.15.MAX.BLE-WiFi"' + -D FIRMWARE_VERSION='"Meck v1.15.1.MAX.BLE-WiFi"' build_src_filter = ${LilyGo_TDeck_Pro_Max.build_src_filter} + + @@ -342,7 +342,7 @@ build_flags = -D MECK_OTA_UPDATE=1 -D HAS_4G_MODEM=1 -D MECK_WEB_READER=1 - -D FIRMWARE_VERSION='"Meck v1.15.MAX.SA"' + -D FIRMWARE_VERSION='"Meck v1.15.1.MAX.SA"' build_src_filter = ${LilyGo_TDeck_Pro_Max.build_src_filter} + + @@ -398,7 +398,7 @@ build_flags = -D CPU_FREQ_BOOST=40 -D CPU_FREQ_LOW_POWER=40 ;-D MESH_DEBUG=1 - -D FIRMWARE_VERSION='"Meck v1.15.MAX.SA40"' + -D FIRMWARE_VERSION='"Meck v1.15.1.MAX.SA40"' build_src_filter = ${LilyGo_TDeck_Pro_Max.build_src_filter} + +