From 885662beae76353a63f636db49576b8e08d017d3 Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:45:34 +0200 Subject: [PATCH] feat(repeater): suppress Adaptive Power Control while relaying MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A repeater wants full, consistent TX power for relay reach, and APC's feedback (own ACKs / own flood echoes) never fires on forwarded traffic — so leaving APC on would relay at whatever reduced power it last trimmed to, or sit inert on a dedicated repeater network. Force it off whenever client_repeat is on, mirroring the power-save lock: - apcActive() = tx_apc && !client_repeat gates every APC control site (sample/failure/track). - applyApc() pins power to the tx_power_dbm ceiling; called at boot, on the on-device repeater toggle, and on the app's CMD_SET_RADIO_PARAMS. - Settings "Auto pwr" shows "--" and blocks the toggle while repeating; the user's pref is preserved and restored when the repeater is switched off. Also refreshes the FEATURES repeater entry (Hold-Enter reset, Errors row, schema 0x10, the radio-setting locks and status indicator). Co-Authored-By: Claude Sonnet 4.6 --- FEATURES.md | 21 +++++++++++++------ .../settings_screen/settings_screen.md | 2 +- .../tools_screen/tools_screen.md | 2 +- examples/companion_radio/MyMesh.cpp | 16 +++++++------- examples/companion_radio/MyMesh.h | 5 +++++ .../companion_radio/ui-new/RepeaterScreen.h | 1 + .../companion_radio/ui-new/SettingsScreen.h | 5 ++++- 7 files changed, 36 insertions(+), 16 deletions(-) diff --git a/FEATURES.md b/FEATURES.md index f967f388..d7138681 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -485,12 +485,21 @@ Tracker L1 OLED/e-ink, GAT562 30S). All on `feature/companion-repeater-presets`. RX/TX counters (generic `Dispatcher::n_recv_by_type`/`n_sent_by_type`), uptime, heap + stack (new `DeviceDiag` helper, nRF52 linker-symbol/`sbrk` heap + FreeRTOS stack high-water), noise floor, RSSI/SNR, pool free, outbound queue, - and **Forwarded** (`Mesh::n_forwarded` — actual retransmits; backed out on - overhear cancel so it reflects what hits the air). Enter opens a confirm popup - to reset the cumulative counters (`resetStats` made virtual; `Mesh` override - also clears `n_forwarded`). -- Prefs persisted behind schema sentinels `0xC0DE000E` (four knobs) then - `0xC0DE000F` (suppress-dup), with stray-byte clamps for upgraders. + **Forwarded** (`Mesh::n_forwarded` — actual retransmits; backed out on overhear + cancel so it reflects what hits the air), and **Errors** (Dispatcher + `ERR_EVENT_*` flags decoded to F/C/R). Hold Enter opens a one-item "Reset + counters" menu (Back dismisses) — `resetStats` made virtual; `Mesh` override + also clears `n_forwarded`. +- **Radio settings locked while relaying** — a repeater must hear all traffic and + relay at consistent power, so duty-cycle RX ("Pwr save") and APC ("Auto pwr") + are forced off whenever `client_repeat` is on (effective `pref && !client_repeat` + via `applyPowerSave()` / `apcActive()`), applied at boot, on the on-device + toggle, and on the app's `CMD_SET_RADIO_PARAMS`; Settings shows `--` and blocks + the toggle, preserving the user's pref for when the repeater goes off. A + blinking `»` status-bar indicator (`ICON_REPEATER`) shows relaying at a glance. +- Prefs persisted behind schema sentinels `0xC0DE000E` (four knobs), + `0xC0DE000F` (suppress-dup), `0xC0DE0010` (radio profile), with stray-byte + clamps for upgraders. - **Open question:** the app-side dedicated-band gate in `CMD_SET_RADIO_PARAMS` was commented out (not deleted) to match the on-device toggle's any-frequency behaviour — undecided whether that gate was UX-only or regulatory. diff --git a/docs/solo_features/settings_screen/settings_screen.md b/docs/solo_features/settings_screen/settings_screen.md index db7e6595..e23298af 100644 --- a/docs/solo_features/settings_screen/settings_screen.md +++ b/docs/solo_features/settings_screen/settings_screen.md @@ -72,7 +72,7 @@ Lists all available home screen pages. For each entry: | BW | 7.8–500 kHz | LEFT/RIGHT cycles the standard LoRa bandwidths. | | CR | 5–8 | LEFT/RIGHT. Coding rate (4/5–4/8). | | Pwr save | ON / OFF | **Battery saver.** Hardware duty-cycle receive: the SX126x cycles RX↔sleep on its own and wakes on a preamble, cutting average RX current. Trades a little receive latency; leave OFF for lowest-latency reception. Requires an SX126x radio (otherwise stays on continuous RX). **Forced off (shown as `--`) while the repeater is on** — a repeater must listen continuously; your setting is restored when the repeater is switched off. | -| Auto pwr | ON / OFF | **Adaptive Power Control.** Lowers actual TX power on strong links to save energy, ramping back up — to the **TX Pwr** ceiling — on weak or lost links. Link quality comes from direct-message ACK SNR and, for channel messages (no ACK), from hearing a repeater rebroadcast your packet. The radio page / name bar shows the live power. Default OFF (fixed TX power). | +| Auto pwr | ON / OFF | **Adaptive Power Control.** Lowers actual TX power on strong links to save energy, ramping back up — to the **TX Pwr** ceiling — on weak or lost links. Link quality comes from direct-message ACK SNR and, for channel messages (no ACK), from hearing a repeater rebroadcast your packet. The radio page / name bar shows the live power. Default OFF (fixed TX power). **Suppressed (shown as `--`) while the repeater is on** — a repeater holds full TX power for consistent relay reach; your setting is restored when the repeater is switched off. | The **repeater** mode and its flood filters live on their own screen — see **Tools › Repeater**. diff --git a/docs/solo_features/tools_screen/tools_screen.md b/docs/solo_features/tools_screen/tools_screen.md index 8287ebc3..2cbf39bb 100644 --- a/docs/solo_features/tools_screen/tools_screen.md +++ b/docs/solo_features/tools_screen/tools_screen.md @@ -340,7 +340,7 @@ The five flood filters are **opt-in** (default OFF, so a plain repeater is unaff **Same network vs. separate network.** With **Network = Current** (or a Custom profile set equal to your companion settings) the repeater stays on your own network — you keep messaging while relaying. With a *different* Custom profile the device moves entirely onto that network while relaying (a single radio can't be on two at once) and returns to your companion network when the repeater is switched off. The profile also re-applies after a reboot if the repeater was left on. -While the repeater is on, a **»** indicator appears in the status bar (same blink convention as the auto-advert and trail markers) so you can tell it's relaying at a glance, and **Settings › Radio › Pwr save** is forced off (a repeater must listen continuously) — your power-save setting returns when you switch the repeater off. +While the repeater is on, a **»** indicator appears in the status bar (same blink convention as the auto-advert and trail markers) so you can tell it's relaying at a glance. Two radio settings are also overridden while relaying and restored afterwards: **Settings › Radio › Pwr save** is forced off (a repeater must listen continuously) and **Auto pwr** is forced off (a repeater holds full TX power for consistent relay reach). Both show `--` in Settings while the repeater is on. Live forwarding stats — **Forwarded**, **Pool free**, **Queue** — are shown on **Tools › Diagnostics**. diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 4cfc864d..018d70a0 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -496,7 +496,7 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) { // positive feedback on the repeater link — sample its SNR. This is the only // confirmation a channel send gets (no ACK), and is what lets APC recover power // after it has trimmed too far for the repeaters to hear. - if (_prefs.tx_apc && _apc_flood_pending && packet->payload_len == _apc_flood_len) { + if (apcActive() && _apc_flood_pending && packet->payload_len == _apc_flood_len) { uint8_t h[MAX_HASH_SIZE]; packet->calculatePacketHash(h); if (memcmp(h, _apc_flood_hash, MAX_HASH_SIZE) == 0) { @@ -596,7 +596,7 @@ void MyMesh::sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, ui } void MyMesh::sendFloodScoped(const mesh::GroupChannel& channel, mesh::Packet* pkt, uint32_t delay_millis) { // TODO: have per-channel send_scope - if (_prefs.tx_apc) apcTrackFloodSend(pkt); // listen for a repeater echo to drive APC (channels have no ACK) + if (apcActive()) apcTrackFloodSend(pkt); // listen for a repeater echo to drive APC (channels have no ACK) trackRelaySend(pkt); // and for the UI "relayed" marker if (send_unscoped) { sendFlood(pkt, delay_millis, _prefs.path_hash_mode + 1); // app has explicitly requested un-scoped @@ -1241,7 +1241,7 @@ void MyMesh::trackRelaySend(const mesh::Packet* pkt) { } void MyMesh::onSendTimeout() { - if (_prefs.tx_apc) apcOnFailure(); + if (apcActive()) apcOnFailure(); } void MyMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) { @@ -1250,7 +1250,7 @@ void MyMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) { // Capture the match before the base handler's processAck() clears the entry. bool mine = isAckPending(ack_crc); BaseChatMesh::onAckRecv(packet, ack_crc); - if (mine && _prefs.tx_apc) apcSampleSnr(radio_driver.getLastSNR()); + if (mine && apcActive()) apcSampleSnr(radio_driver.getLastSNR()); // Drive the DM delivery-status marker. Note isAckPending() only covers // app/serial-initiated sends (expected_ack_table); a DM composed on the // device UI registers in BaseChatMesh's own ack table instead, so gating on @@ -1861,9 +1861,11 @@ void MyMesh::handleCmdFrame(size_t len) { savePrefs(); radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); - // Keep the "repeating ⇒ continuous RX" invariant when repeat is toggled via - // the app, mirroring the on-device path (a repeater must hear all traffic). + // Keep the "repeating ⇒ continuous RX, full TX power" invariants when repeat + // is toggled via the app, mirroring the on-device path (a repeater must hear + // all traffic and relay at consistent power). radio_driver.setPowerSaving(_prefs.rx_powersave && !_prefs.client_repeat); + applyApc(); // pins power to the ceiling; apcActive() keeps it there while repeating MESH_DEBUG_PRINTLN("OK: CMD_SET_RADIO_PARAMS: f=%d, bw=%d, sf=%d, cr=%d", freq, bw, (uint32_t)sf, (uint32_t)cr); @@ -2774,7 +2776,7 @@ void MyMesh::loop() { // treated as a lost confirmation → ramp power up (lets channel sends recover). if (_apc_flood_pending && millisHasNowPassed(_apc_flood_deadline)) { _apc_flood_pending = false; - if (_prefs.tx_apc) apcOnFailure(); + if (apcActive()) apcOnFailure(); } // UI relay windows expired with no echo — just drop them (no echo is not a // failure for channels; the marker simply stays "sent"). diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 816390fa..28b81536 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -156,6 +156,11 @@ protected: // Overhear suppression only makes sense while repeating; gated behind its own // opt-in pref (Settings > Radio > Suppress dup). bool wantsOverhearSuppress() const override { return _prefs.client_repeat && _prefs.repeat_suppress_dup; } + // Adaptive Power Control is suppressed while repeating: a repeater wants full, + // consistent TX power for relay reach, and its feedback sources (own ACKs / + // own flood echoes) don't fire on forwarded traffic anyway. applyApc() then + // pins power to the ceiling. + bool apcActive() const { return _prefs.tx_apc && !_prefs.client_repeat; } void sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis); void sendFloodScoped(const ContactInfo& recipient, mesh::Packet* pkt, uint32_t delay_millis=0) override; diff --git a/examples/companion_radio/ui-new/RepeaterScreen.h b/examples/companion_radio/ui-new/RepeaterScreen.h index 88a7585e..3e1737f2 100644 --- a/examples/companion_radio/ui-new/RepeaterScreen.h +++ b/examples/companion_radio/ui-new/RepeaterScreen.h @@ -260,6 +260,7 @@ public: p->client_repeat ^= 1; the_mesh.applyRepeaterRadio(); // switch to profile on enable / restore companion on disable _task->applyPowerSave(); // duty-cycle RX is forced off while repeating + _task->applyApc(); // pin TX power to the ceiling while repeating (APC suppressed) buildItems(p); _dirty = true; return true; diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 0c05ca8c..6e3ef985 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -613,7 +613,9 @@ class SettingsScreen : public UIScreen { } else if (item == TX_APC) { display.print("Auto pwr"); display.setCursor(valCol(display), y); - display.print((p && p->tx_apc) ? "ON" : "OFF"); + // Suppressed (and locked) while repeating — a repeater holds full TX power. + if (p && p->client_repeat) display.print("--"); + else display.print((p && p->tx_apc) ? "ON" : "OFF"); #if AUTO_OFF_MILLIS > 0 } else if (item == AUTO_OFF) { display.print("AutoOff"); @@ -1007,6 +1009,7 @@ public: return true; } if (_selected == TX_APC && p && (left || right || enter)) { + if (p->client_repeat) { _task->showAlert("Off while repeating", 900); return true; } p->tx_apc ^= 1; _task->applyApc(); _dirty = true;