From 28d8291e333c995de6880c2164ec22d730cf3526 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:00:49 +0200 Subject: [PATCH] fix(companion): CLI NaN, APC on TX changes, tz-aware alarm, late ACKs, ping timeout and widths, TX limits - set af / dutycycle / rxdelay reject nan. - A TX power change from the app or CLI re-baselines APC. - CMD_SET_RADIO_PARAMS stores repeat as 0/1. - The alarm reschedules itself when the time zone changes. - A late ACK for an earlier DM attempt marks it delivered. - A new auto-advert interval counts from the last advert. - Both UIs take the TX power range from radioctl, up to MAX_LORA_TX_POWER (30 dBm on the GAT562 30S). - The ping timeout comes from the airtime; 3-byte path hash mode pings with 4-byte trace hashes, and trace flags are right for width 4. Co-Authored-By: Claude Opus 5.5 --- examples/companion_radio/MyMesh.cpp | 11 +++++++++- examples/companion_radio/MyMesh.h | 4 +++- examples/companion_radio/MyMeshCli.h | 7 +++--- examples/companion_radio/MyMeshPing.h | 11 +++++++--- .../companion_radio/ui-core/ClockEngine.h | 4 ++++ .../companion_radio/ui-core/MessageHistory.h | 22 +++++++++++++++++++ examples/companion_radio/ui-core/PingEngine.h | 15 ++++++++----- .../companion_radio/ui-core/RadioControl.h | 5 +++++ examples/companion_radio/ui-lvgl/NodesView.h | 5 ++--- .../companion_radio/ui-lvgl/RadioScreen.h | 8 ++----- .../companion_radio/ui-new/NearbyScreen.h | 3 +-- .../companion_radio/ui-new/SettingsScreen.h | 4 ++-- examples/companion_radio/ui-new/UITask.cpp | 5 ++--- examples/companion_radio/ui-new/UITask.h | 1 + 14 files changed, 76 insertions(+), 29 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index b0165e2b..6791e700 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1940,7 +1940,7 @@ void MyMesh::handleCmdFrame(size_t len) { _prefs.cr = cr; _prefs.freq = (float)freq / 1000.0; _prefs.bw = (float)bw / 1000.0; - _prefs.client_repeat = repeat; + _prefs.client_repeat = repeat ? 1 : 0; // a bool for every reader, whatever byte the app sent savePrefs(); applyRepeaterRadio(); // companion params, or the repeater profile if relaying with one set @@ -1971,6 +1971,7 @@ void MyMesh::handleCmdFrame(size_t len) { // Store what the radio actually applied, not the raw request -- on a // board with an external-PA gain curve those can differ (see setTxPower()). _prefs.tx_power_dbm = radio_driver.getTxPower(); + if (_prefs.tx_apc) applyApc(); // the new ceiling: re-baseline APC, as the device's own setting does savePrefs(); writeOKFrame(); } @@ -2882,10 +2883,18 @@ void MyMesh::loop() { _last_rtc_save_ms = millis(); } + if (_prefs.advert_auto_interval_sec != _auto_advert_sec) { + // Interval changed (Settings, CLI, app): count the new one from the last + // advert instead of waiting out what was left of the old one. + bool first = _auto_advert_sec == UINT32_MAX; // boot: keep the advert due straight away + _auto_advert_sec = _prefs.advert_auto_interval_sec; + if (!first) _next_auto_advert_ms = _last_auto_advert_ms + _auto_advert_sec * 1000UL; + } if (_prefs.advert_auto_interval_sec > 0 && millisHasNowPassed(_next_auto_advert_ms)) { // The position only where Privacy lets adverts carry it, at its precision. mesh::Packet* pkt = createOwnAdvert(); if (pkt) sendZeroHop(pkt); + _last_auto_advert_ms = _ms->getMillis(); _next_auto_advert_ms = futureMillis(_prefs.advert_auto_interval_sec * 1000UL); } } diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 7d1f6d52..0764190f 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -254,7 +254,7 @@ public: unsigned long sent_ms; }; - uint32_t sendPing(const uint8_t* dest_pubkey, uint8_t hash_width = 1); + uint32_t sendPing(const uint8_t* dest_pubkey, uint8_t hash_width = 1, uint32_t* est_timeout = nullptr); void setPingCallback(PingCallback cb, void* arg); void clearPingResult(uint32_t tag); PingResult* getPingResult(uint32_t tag); @@ -684,6 +684,8 @@ private: unsigned long _bot_last_ch_reply_ms; unsigned long _bot_last_room_reply_ms; unsigned long _next_auto_advert_ms; + unsigned long _last_auto_advert_ms = 0; + uint32_t _auto_advert_sec = UINT32_MAX; // the interval _next_auto_advert_ms was set with (MAX: not yet) // Per-contact DM reply throttle: a small ring of the most recent recipients so // one chatty contact can't be spammed while a different sender is still served. diff --git a/examples/companion_radio/MyMeshCli.h b/examples/companion_radio/MyMeshCli.h index 5f36f00a..f7453ce8 100644 --- a/examples/companion_radio/MyMeshCli.h +++ b/examples/companion_radio/MyMeshCli.h @@ -120,7 +120,7 @@ bool MyMesh::cliRadioCommand(const char* command, char* reply, int n) { if (memcmp(command, "set af ", 7) == 0) { char* end; float af = strtof(&command[7], &end); - if (end == &command[7] || af < 0 || af > 9) snprintf(reply, n, "ERROR: af must be 0-9"); + if (end == &command[7] || !(af >= 0 && af <= 9)) snprintf(reply, n, "ERROR: af must be 0-9"); else { _prefs.airtime_factor = af; savePrefs(); snprintf(reply, n, "OK"); } return true; } @@ -131,7 +131,7 @@ bool MyMesh::cliRadioCommand(const char* command, char* reply, int n) { } if (memcmp(command, "set dutycycle ", 14) == 0) { float dc = atof(&command[14]); - if (dc < 1 || dc > 100) { snprintf(reply, n, "ERROR: dutycycle must be 1-100"); return true; } + if (!(dc >= 1 && dc <= 100)) // written so "nan" fails too { snprintf(reply, n, "ERROR: dutycycle must be 1-100"); return true; } _prefs.airtime_factor = 100.0f / dc - 1.0f; savePrefs(); float a = 100.0f / (_prefs.airtime_factor + 1.0f); @@ -172,6 +172,7 @@ bool MyMesh::cliRadioCommand(const char* command, char* reply, int n) { if (p < -9 || p > MAX_LORA_TX_POWER) { snprintf(reply, n, "Error, must be -9 to %d", MAX_LORA_TX_POWER); return true; } radio_driver.setTxPower(p); _prefs.tx_power_dbm = radio_driver.getTxPower(); // what the radio applied (see CMD_SET_RADIO_TX_POWER) + if (_prefs.tx_apc) applyApc(); // the new APC ceiling savePrefs(); snprintf(reply, n, "OK"); return true; @@ -179,7 +180,7 @@ bool MyMesh::cliRadioCommand(const char* command, char* reply, int n) { if (strcmp(command, "get rxdelay") == 0) { snprintf(reply, n, "> %s", StrHelper::ftoa(_prefs.rx_delay_base)); return true; } if (memcmp(command, "set rxdelay ", 12) == 0) { float d = atof(&command[12]); - if (d < 0 || d > 20.0f) snprintf(reply, n, "Error, must be 0-20"); + if (!(d >= 0 && d <= 20.0f)) snprintf(reply, n, "Error, must be 0-20"); else { _prefs.rx_delay_base = d; savePrefs(); snprintf(reply, n, "OK"); } return true; } diff --git a/examples/companion_radio/MyMeshPing.h b/examples/companion_radio/MyMeshPing.h index 6c54e357..ea19e024 100644 --- a/examples/companion_radio/MyMeshPing.h +++ b/examples/companion_radio/MyMeshPing.h @@ -25,8 +25,9 @@ int MyMesh::getDiscoverResults(DiscoverResult dest[], int max_count) { // ── Ping/Trace functionality ───────────────────────────────────────────────── -uint32_t MyMesh::sendPing(const uint8_t* dest_pubkey, uint8_t hash_width) { - if (hash_width == 0 || hash_width > 2) return 0; +uint32_t MyMesh::sendPing(const uint8_t* dest_pubkey, uint8_t hash_width, uint32_t* est_timeout) { + // A trace's hashes are 1 << flags bytes wide: 1, 2, 4 (no 3). + if (hash_width != 1 && hash_width != 2 && hash_width != 4) return 0; // Generate random tag and auth code uint32_t tag, auth; @@ -61,9 +62,13 @@ uint32_t MyMesh::sendPing(const uint8_t* dest_pubkey, uint8_t hash_width) { memcpy(path, dest_pubkey, path_len); // Create and send trace packet - auto pkt = createTrace(tag, auth, hash_width - 1); // flags = hash_width - 1 + auto pkt = createTrace(tag, auth, hash_width == 4 ? 2 : hash_width - 1); // flags = log2(hash_width) if (pkt) { sendDirect(pkt, path, path_len); + if (est_timeout) { // as CMD_SEND_TRACE_PATH answers the app: from the airtime, one hop + uint32_t t = _radio->getEstAirtimeFor(pkt->payload_len + pkt->path_len + 2); + *est_timeout = calcDirectTimeoutMillisFor(t, 1); + } return tag; } diff --git a/examples/companion_radio/ui-core/ClockEngine.h b/examples/companion_radio/ui-core/ClockEngine.h index 38be535d..84eb93b2 100644 --- a/examples/companion_radio/ui-core/ClockEngine.h +++ b/examples/companion_radio/ui-core/ClockEngine.h @@ -101,6 +101,9 @@ private: _alarm_check_ms = now_ms; uint32_t now_wall = rtc_clock.getCurrentTime(); if (now_wall < 1000000000UL) return; // need a real time sync first + // A time zone change (Settings, CLI tz.offset, the app) moves the local + // alarm time: reschedule here rather than trusting every caller to say so. + if (_alarm_tz != _prefs->tz_offset_hours) { _alarm_tz = _prefs->tz_offset_hours; _alarm_next_fire = 0; } if (_alarm_next_fire == 0) _alarm_next_fire = computeAlarmNextFire(now_wall); if (now_wall < _alarm_next_fire) return; if (now_wall - _alarm_next_fire < ALARM_CATCHUP_SECS) { @@ -124,6 +127,7 @@ private: NodePrefs* _prefs = nullptr; UiEventQueue* _events = nullptr; uint32_t _alarm_next_fire = 0; // unix; 0 = (re)compute lazily once time is valid + int8_t _alarm_tz = 0; // the tz_offset_hours _alarm_next_fire was computed with uint32_t _alarm_check_ms = 0; // throttle the wall-clock read to ~2 Hz bool _timer_running = false; uint32_t _timer_deadline_ms = 0; diff --git a/examples/companion_radio/ui-core/MessageHistory.h b/examples/companion_radio/ui-core/MessageHistory.h index 93ca47c5..a6b37dbd 100644 --- a/examples/companion_radio/ui-core/MessageHistory.h +++ b/examples/companion_radio/ui-core/MessageHistory.h @@ -459,6 +459,28 @@ public: e.ack_status = ACK_OK; #ifdef HIST_ARCHIVE if (_arc) _arc->dmUpdate(e); +#endif + return; + } + } + // A late ACK for an earlier attempt still proves delivery. Only the latest + // attempt's tag is kept, so rebuild the earlier ones (BaseChatMesh:: + // composeMsgPacket(): timestamp, attempt & 3, text, our public key). + for (int i = 0; i < _dm_hist_count; i++) { + DmHistEntry& e = _dm_hist[(_dm_hist_head + i) % DM_HIST_MAX]; + if (!e.outgoing || e.ack_status != ACK_PENDING || e.attempt == 0) continue; + uint8_t buf[5 + MSG_TEXT_BUF]; + int tl = strnlen(e.text, MSG_TEXT_BUF - 1); + memcpy(buf, &e.msg_ts, 4); + memcpy(&buf[5], e.text, tl); + for (uint8_t a = 0; a < e.attempt && a < 4; a++) { // the hash only sees attempt & 3 + buf[4] = (uint8_t)(a & 3); + uint32_t crc; + mesh::Utils::sha256((uint8_t*)&crc, 4, buf, 5 + tl, the_mesh.self_id.pub_key, PUB_KEY_SIZE); + if (crc != ack_crc) continue; + e.ack_status = ACK_OK; +#ifdef HIST_ARCHIVE + if (_arc) _arc->dmUpdate(e); #endif return; } diff --git a/examples/companion_radio/ui-core/PingEngine.h b/examples/companion_radio/ui-core/PingEngine.h index c6d4f80f..b47bf99c 100644 --- a/examples/companion_radio/ui-core/PingEngine.h +++ b/examples/companion_radio/ui-core/PingEngine.h @@ -4,18 +4,17 @@ // view can keep showing it. The reply arrives through MyMesh's ping callback, // which is a plain function pointer -- routed here via a single static instance. // -// The ping timeout is still decided by the view (NearbyScreen's -// PING_TIMEOUT_MS), which calls clear() when it gives up. +// The view gives up after timeoutMs() (from the airtime, so it scales with +// the spreading factor) and calls clear(). class PingEngine { public: - enum StartResult : uint8_t { STARTED = 0, BUSY, UNSUPPORTED, SEND_FAILED }; + enum StartResult : uint8_t { STARTED = 0, BUSY, SEND_FAILED }; void begin(NodePrefs* prefs) { _prefs = prefs; s_instance = this; } StartResult start(const uint8_t* pub_key) { if (_active || !pub_key) return BUSY; - if (_prefs && _prefs->path_hash_mode > 1) return UNSUPPORTED; // no 3-byte path hash support _active = true; _tag = 0; @@ -25,7 +24,11 @@ public: // Always install the callback before sending so the response cannot race it. the_mesh.setPingCallback(onPingResult, NULL); - _tag = the_mesh.sendPing(pub_key, _prefs ? _prefs->path_hash_mode + 1 : 1); + // The trace's hash width follows the path hash mode; a trace has no 3-byte + // width, so mode 2 pings with 4 bytes of the key. + uint8_t mode = _prefs ? _prefs->path_hash_mode : 0; + _timeout_ms = 3000; + _tag = the_mesh.sendPing(pub_key, mode >= 2 ? 4 : mode + 1, &_timeout_ms); if (_tag == 0) { clear(); return SEND_FAILED; @@ -41,6 +44,7 @@ public: } bool isActive() const { return _active; } + uint32_t timeoutMs() const { return _timeout_ms; } void getResult(int16_t& snr_out_x4, int16_t& snr_back_x4, uint32_t& rtt_ms) const { snr_out_x4 = _snr_out_x4; snr_back_x4 = _snr_back_x4; @@ -70,6 +74,7 @@ private: int16_t _snr_out_x4 = 0; int16_t _snr_back_x4 = 0; uint32_t _rtt_ms = 0; + uint32_t _timeout_ms = 3000; }; // Header-only Core, single TU (see UiCore.h) -- the one definition lives here. diff --git a/examples/companion_radio/ui-core/RadioControl.h b/examples/companion_radio/ui-core/RadioControl.h index 3c206fcc..5c487821 100644 --- a/examples/companion_radio/ui-core/RadioControl.h +++ b/examples/companion_radio/ui-core/RadioControl.h @@ -11,6 +11,11 @@ namespace radioctl { +// The TX power range the device's own settings offer (the app and CLI take +// -9 up). The top is the board's limit: 30 dBm on the GAT562 30S, not 22. +static const int TX_MIN_DBM = 2; +static const int TX_MAX_DBM = MAX_LORA_TX_POWER; + // freq / bw / sf / cr -> the radio (or the repeater profile, while relaying with one). static void applyParams() { the_mesh.applyRepeaterRadio(); } diff --git a/examples/companion_radio/ui-lvgl/NodesView.h b/examples/companion_radio/ui-lvgl/NodesView.h index 1160e43b..e90d498d 100644 --- a/examples/companion_radio/ui-lvgl/NodesView.h +++ b/examples/companion_radio/ui-lvgl/NodesView.h @@ -449,7 +449,7 @@ void UITask::refreshNode() { setTextFmt(_node_ping, "Ping %lu ms - SNR out %.1f back %.1f", (unsigned long)rtt, out / 4.0f, back / 4.0f); _pinging = false; - } else if (millis() - _ping_started_ms > 3000) { + } else if (millis() - _ping_started_ms > _core->ping.timeoutMs()) { _core->ping.clear(); setText(_node_ping, "Ping: no reply"); _pinging = false; @@ -472,8 +472,7 @@ void UITask::nodeAction(uint8_t action) { _next_nearby_ms = millis() + 250; lv_label_set_text(_node_ping, "Pinging..."); } else { - lv_label_set_text(_node_ping, r == PingEngine::UNSUPPORTED ? "Ping needs 1-2 byte path hashes" - : "Ping failed"); + lv_label_set_text(_node_ping, "Ping failed"); } break; } diff --git a/examples/companion_radio/ui-lvgl/RadioScreen.h b/examples/companion_radio/ui-lvgl/RadioScreen.h index 80aa2267..805ecfe2 100644 --- a/examples/companion_radio/ui-lvgl/RadioScreen.h +++ b/examples/companion_radio/ui-lvgl/RadioScreen.h @@ -10,12 +10,8 @@ namespace radioview { enum : uint8_t { R_PRESET, R_SF, R_BW, R_CR, R_TX, R_APC }; -static const int TX_MIN = 2; -#ifdef LORA_TX_POWER -static const int TX_MAX = LORA_TX_POWER; -#else -static const int TX_MAX = 22; -#endif +static const int TX_MIN = radioctl::TX_MIN_DBM; +static const int TX_MAX = radioctl::TX_MAX_DBM; static lv_obj_t* s_overlay = nullptr; // frequency entry static lv_obj_t* s_ta = nullptr; diff --git a/examples/companion_radio/ui-new/NearbyScreen.h b/examples/companion_radio/ui-new/NearbyScreen.h index 8e08d3f6..0c22df0d 100644 --- a/examples/companion_radio/ui-new/NearbyScreen.h +++ b/examples/companion_radio/ui-new/NearbyScreen.h @@ -66,7 +66,6 @@ class NearbyScreen : public UIScreen, protected NearbyModel { char _ping_snr_back_str[24]; bool _pinging; unsigned long _ping_started_ms; - static const unsigned long PING_TIMEOUT_MS = 3000UL; // ── helpers ────────────────────────────────────────────────────────────────── static void pubKeyToBase64(const uint8_t* key, char* out, int out_len) { @@ -266,7 +265,7 @@ class NearbyScreen : public UIScreen, protected NearbyModel { if (snr_back != 0) snprintf(_ping_snr_back_str, sizeof(_ping_snr_back_str), "SNR back: %.1f", snr_back / 4.0f); _pinging = false; - } else if (_pinging && millis() - _ping_started_ms >= PING_TIMEOUT_MS) { + } else if (_pinging && millis() - _ping_started_ms >= _task->pingTimeoutMs()) { snprintf(_ping_time_str, sizeof(_ping_time_str), "RTT: timeout"); _ping_snr_out_str[0] = '\0'; _ping_snr_back_str[0] = '\0'; diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index bcb7dccf..089cfccc 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -1036,8 +1036,8 @@ public: return enter; } if (_selected == TX_POWER && p) { - if (right && p->tx_power_dbm < 22) { p->tx_power_dbm++; _task->applyTxPower(); _dirty = true; return true; } - if (left && p->tx_power_dbm > 2) { p->tx_power_dbm--; _task->applyTxPower(); _dirty = true; return true; } + if (right && p->tx_power_dbm < radioctl::TX_MAX_DBM) { p->tx_power_dbm++; _task->applyTxPower(); _dirty = true; return true; } + if (left && p->tx_power_dbm > radioctl::TX_MIN_DBM) { p->tx_power_dbm--; _task->applyTxPower(); _dirty = true; return true; } } if (_selected == RADIO_PRESET && p && enter) { _picker.open(p, radioTarget(p), "Radio Preset"); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 26b35667..b8d5e4d3 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -356,10 +356,9 @@ void UITask::gotoAutoAdvertScreen() { setCurrScreen(auto_advert_screen); } void UITask::gotoBatteryCurve() { setCurrScreen(batt_curve_screen); } bool UITask::startPing(const uint8_t* pub_key) { - PingEngine::StartResult r = _core->ping.start(pub_key); - if (r == PingEngine::UNSUPPORTED) showAlert("Ping not supported with 3-byte path hashes", 3000); - return r == PingEngine::STARTED; + return _core->ping.start(pub_key) == PingEngine::STARTED; } +uint32_t UITask::pingTimeoutMs() const { return _core->ping.timeoutMs(); } bool UITask::isPingActive() const { return _core->ping.isActive(); } void UITask::getPingResult(int16_t& snr_out_x4, int16_t& snr_back_x4, uint32_t& rtt_ms) const { _core->ping.getResult(snr_out_x4, snr_back_x4, rtt_ms); diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 86fe04e5..8e96ffbc 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -473,6 +473,7 @@ public: // Ping helpers (engine in ui-core/PingEngine.h) bool startPing(const uint8_t* pub_key); bool isPingActive() const; + uint32_t pingTimeoutMs() const; void getPingResult(int16_t& snr_out_x4, int16_t& snr_back_x4, uint32_t& rtt_ms) const; void clearPing();