mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
feat(repeater): status-bar indicator, power-save lock, diag polish
- Status bar: blinking » indicator while relaying (new ICON_REPEATER), added to the 1 s blink cadence alongside the advert/trail markers. - Power-save lock: a repeater must hear all traffic, so duty-cycle RX is forced off whenever client_repeat is on. Effective state = rx_powersave && !repeat, applied at boot, in applyPowerSave(), on the on-device toggle, and on the app's CMD_SET_RADIO_PARAMS. Settings shows "--" and blocks the toggle while repeating; the user's pref is preserved and restored when the repeater is switched off. - Diagnostics: reset now uses the standard Hold-Enter one-item action menu (Back dismisses) instead of an Enter/Cancel popup; new "Errors" row decodes the Dispatcher ERR_EVENT_* flags (F/C/R, or OK). - RepeaterScreen: drop the live stats block (they live on Diagnostics). - Min-SNR load clamp tightened to the UI's -20..10 range. Docs updated (settings_screen, tools_screen, FEATURES). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -453,10 +453,10 @@ Tracker L1 OLED/e-ink, GAT562 30S). All on `feature/companion-repeater-presets`.
|
||||
flood-depth cap are always applied. Packet pool bumped 16→32 to match the
|
||||
repeater workload (a too-small pool starved channel/DM reception once relaying
|
||||
queued retransmits).
|
||||
- **Consolidated on Tools › Repeater** — the toggle, the five filters, and live
|
||||
forwarding stats (Forwarded / Pool free / Queue) share one full-width screen,
|
||||
rather than the original Settings › Radio sub-items (whose indented labels
|
||||
collided with the value column on a 128px OLED).
|
||||
- **Consolidated on Tools › Repeater** — the toggle and the five filters share
|
||||
one full-width screen, rather than the original Settings › Radio sub-items
|
||||
(whose indented labels collided with the value column on a 128px OLED). Live
|
||||
forwarding stats (Forwarded / Pool free / Queue) stay on Tools › Diagnostics.
|
||||
- **Optional dedicated radio profile** (`Network: Current/Custom`,
|
||||
`repeater_use_profile` + `repeater_freq/bw/sf/cr`). Custom switches the radio to
|
||||
a preset/manual profile when the repeater is enabled and restores the
|
||||
|
||||
@@ -71,7 +71,7 @@ Lists all available home screen pages. For each entry:
|
||||
| SF | 5–12 | LEFT/RIGHT. Spreading factor. |
|
||||
| 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). |
|
||||
| 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). |
|
||||
|
||||
The **repeater** mode and its flood filters live on their own screen — see **Tools › Repeater**.
|
||||
|
||||
@@ -309,16 +309,17 @@ A single read-only screen of live device and mesh stats, refreshed once a second
|
||||
| RSSI/SNR | Signal strength / signal-to-noise of the last received packet |
|
||||
| Pool free | Free entries in the packet pool |
|
||||
| Queue | Packets waiting in the outbound queue |
|
||||
| Errors | Radio error flags since boot/reset — `OK`, or tokens `F` (queue full), `C` (CAD timeout), `R` (RX-start timeout) |
|
||||
|
||||
The packet counters and **Forwarded** are cumulative since boot. Press **Enter** to open a confirm popup that resets them (defaults to *Cancel*); the live readings (noise, RSSI/SNR, pool, queue, uptime) are not affected. **Cancel/Back** returns to the Tools list.
|
||||
The packet counters, **Forwarded** and **Errors** are cumulative since boot. **Hold Enter** opens a one-item *Reset counters* menu (Back dismisses it); the live readings (noise, RSSI/SNR, pool, queue, uptime) are not affected. **Cancel/Back** returns to the Tools list.
|
||||
|
||||
The counters make the repeater behaviour observable: **Forwarded** confirms the node is actually relaying (not just configured to), and **Pool free** / **Queue** show whether forwarding is exhausting the packet pool. See **Tools › Repeater** for the relaying options (which also shows these same forwarding stats in context).
|
||||
The counters make the repeater behaviour observable: **Forwarded** confirms the node is actually relaying (not just configured to), and **Pool free** / **Queue** show whether forwarding is exhausting the packet pool. See **Tools › Repeater** for the relaying options.
|
||||
|
||||
---
|
||||
|
||||
## Repeater
|
||||
|
||||
Turns the companion into a packet **repeater** while it keeps working as a normal companion — no separate firmware. By default it relays on its **current** frequency, or you can give it a dedicated radio profile to switch to while relaying (see **Network** below). Loop-detection and an advert flood-depth cap are always applied. This screen keeps the toggle, the network/profile, its flood-filter options, and live forwarding stats together.
|
||||
Turns the companion into a packet **repeater** while it keeps working as a normal companion — no separate firmware. By default it relays on its **current** frequency, or you can give it a dedicated radio profile to switch to while relaying (see **Network** below). Loop-detection and an advert flood-depth cap are always applied. This screen keeps the toggle, the network/profile, and its flood-filter options together; live forwarding stats are on **Tools › Diagnostics**.
|
||||
|
||||
Navigate with **UP/DOWN**; change a value with **LEFT/RIGHT** (or **Enter** for toggles). **Cancel/Back** saves and returns to Tools.
|
||||
|
||||
@@ -339,4 +340,8 @@ 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.
|
||||
|
||||
Live forwarding stats — **Forwarded**, **Pool free**, **Queue** — are shown on **Tools › Diagnostics**.
|
||||
|
||||
While the repeater is ON, the bottom of the screen shows live stats: **Forwarded** (packets actually re-transmitted — reflects Suppress dup), **Pool free**, and **Queue**. The same numbers appear, alongside the full device picture, in **Tools › Diagnostics**.
|
||||
|
||||
@@ -348,8 +348,8 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
if (_prefs.repeat_max_hops > 64) _prefs.repeat_max_hops = 0;
|
||||
if (_prefs.repeat_delay_boost > 8) _prefs.repeat_delay_boost = 0;
|
||||
if (_prefs.repeat_min_snr != NodePrefs::REPEAT_SNR_DISABLED &&
|
||||
(_prefs.repeat_min_snr < -30 || _prefs.repeat_min_snr > 20))
|
||||
_prefs.repeat_min_snr = NodePrefs::REPEAT_SNR_DISABLED;
|
||||
(_prefs.repeat_min_snr < -20 || _prefs.repeat_min_snr > 10))
|
||||
_prefs.repeat_min_snr = NodePrefs::REPEAT_SNR_DISABLED; // match the UI's -20..10 range
|
||||
if (_prefs.repeat_suppress_dup > 1) _prefs.repeat_suppress_dup = 0;
|
||||
rd(&_prefs.repeater_use_profile, sizeof(_prefs.repeater_use_profile));
|
||||
rd(&_prefs.repeater_freq, sizeof(_prefs.repeater_freq));
|
||||
|
||||
@@ -1421,7 +1421,7 @@ void MyMesh::begin(bool has_display) {
|
||||
applyRepeaterRadio(); // companion params, or the repeater profile if relaying with one set
|
||||
applyApc(); // sets TX power to the ceiling and arms APC if enabled
|
||||
radio_driver.setRxBoostedGainMode(_prefs.rx_boosted_gain);
|
||||
radio_driver.setPowerSaving(_prefs.rx_powersave); // hardware duty-cycle RX (battery saver)
|
||||
radio_driver.setPowerSaving(_prefs.rx_powersave && !_prefs.client_repeat); // duty-cycle RX off while repeating (must hear all traffic)
|
||||
MESH_DEBUG_PRINTLN("RX Boosted Gain Mode: %s",
|
||||
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
|
||||
}
|
||||
@@ -1861,6 +1861,9 @@ 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).
|
||||
radio_driver.setPowerSaving(_prefs.rx_powersave && !_prefs.client_repeat);
|
||||
MESH_DEBUG_PRINTLN("OK: CMD_SET_RADIO_PARAMS: f=%d, bw=%d, sf=%d, cr=%d", freq, bw, (uint32_t)sf,
|
||||
(uint32_t)cr);
|
||||
|
||||
|
||||
@@ -19,10 +19,10 @@ extern MyMesh the_mesh;
|
||||
class DiagnosticsScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
int _scroll = 0;
|
||||
PopupMenu _reset_menu; // Enter → confirm before zeroing the cumulative counters
|
||||
PopupMenu _reset_menu; // Hold Enter → 1-item "Reset counters" action menu (Back dismisses)
|
||||
|
||||
struct Row { const char* label; char value[20]; };
|
||||
static const int MAX_ROWS = 13;
|
||||
static const int MAX_ROWS = 14;
|
||||
Row _rows[MAX_ROWS];
|
||||
int _row_count = 0;
|
||||
|
||||
@@ -100,6 +100,18 @@ class DiagnosticsScreen : public UIScreen {
|
||||
addRow("Pool free", buf);
|
||||
snprintf(buf, sizeof(buf), "%d", the_mesh.getOutboundQueueLen());
|
||||
addRow("Queue", buf);
|
||||
|
||||
// Radio error flags since boot/reset, decoded to short tokens (F=queue full,
|
||||
// C=CAD timeout, R=RX-start timeout). "OK" when none have fired.
|
||||
uint16_t err = the_mesh.getErrFlags();
|
||||
if (err == 0) strcpy(buf, "OK");
|
||||
else {
|
||||
buf[0] = '\0';
|
||||
if (err & ERR_EVENT_FULL) strcat(buf, "F ");
|
||||
if (err & ERR_EVENT_CAD_TIMEOUT) strcat(buf, "C ");
|
||||
if (err & ERR_EVENT_STARTRX_TIMEOUT) strcat(buf, "R ");
|
||||
}
|
||||
addRow("Errors", buf);
|
||||
}
|
||||
|
||||
public:
|
||||
@@ -136,25 +148,21 @@ public:
|
||||
|
||||
bool handleInput(char c) override {
|
||||
if (_reset_menu.active) {
|
||||
auto res = _reset_menu.handleInput(c);
|
||||
// Index 0 = "Reset", 1 = "Cancel" (destructive option first, but never the
|
||||
// default landing row — see setSelected below).
|
||||
if (res == PopupMenu::SELECTED && _reset_menu.selectedIndex() == 0) {
|
||||
the_mesh.resetStats(); // zeroes Dispatcher per-type counters + Mesh forward count
|
||||
auto res = _reset_menu.handleInput(c); // Back/Cancel dismisses; the only item is "Reset counters"
|
||||
if (res == PopupMenu::SELECTED) {
|
||||
the_mesh.resetStats(); // zeroes Dispatcher per-type counters + Mesh forward count + err flags
|
||||
_task->showAlert("Counters reset", 800);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_UP) { if (_scroll > 0) _scroll--; return true; }
|
||||
if (c == KEY_DOWN) { _scroll++; return true; } // clamped to max_scroll in render()
|
||||
if (c == KEY_ENTER) {
|
||||
_reset_menu.begin("Reset counters?", 2);
|
||||
_reset_menu.addItem("Reset");
|
||||
_reset_menu.addItem("Cancel");
|
||||
_reset_menu.setSelected(1); // default to Cancel so a stray Enter doesn't wipe stats
|
||||
if (c == KEY_CONTEXT_MENU) { // Hold Enter — same action-menu gesture as the rest of the UI
|
||||
_reset_menu.begin("Diagnostics", 1);
|
||||
_reset_menu.addItem("Reset counters");
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) { _task->gotoToolsScreen(); return true; }
|
||||
if (c == KEY_CANCEL) { _task->gotoToolsScreen(); return true; }
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -172,12 +172,10 @@ public:
|
||||
int visible = display.listVisible(item_h);
|
||||
if (visible < 1) visible = 1;
|
||||
|
||||
bool show_stats = p && p->client_repeat;
|
||||
int total = _item_count + (show_stats ? 3 : 0);
|
||||
|
||||
// Config only — live forwarding stats live on Tools › Diagnostics.
|
||||
int total = _item_count;
|
||||
if (_sel < _scroll) _scroll = _sel;
|
||||
if (_sel >= _scroll + visible) _scroll = _sel - visible + 1;
|
||||
if (_sel == _item_count - 1) _scroll = total - visible; // reveal stats tail on last item
|
||||
int max_scroll = total - visible;
|
||||
if (max_scroll < 0) max_scroll = 0;
|
||||
if (_scroll > max_scroll) _scroll = max_scroll;
|
||||
@@ -188,35 +186,23 @@ public:
|
||||
int row = _scroll + i;
|
||||
int y = start_y + i * item_h;
|
||||
char val[16];
|
||||
if (row < _item_count) {
|
||||
bool sel = (row == _sel);
|
||||
int item = _items[row];
|
||||
display.drawSelectionRow(0, y - 1, display.width() - reserve, item_h - 1, sel);
|
||||
display.setCursor(2, y);
|
||||
display.print(itemLabel(item));
|
||||
if (item == IT_RFREQ && sel && _freq_editor.active) {
|
||||
_freq_editor.render(display, valCol(display), y);
|
||||
} else {
|
||||
itemValue(item, p, val, sizeof(val));
|
||||
display.drawTextRightAlign(display.width() - reserve - 2, y, val);
|
||||
}
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
bool sel = (row == _sel);
|
||||
int item = _items[row];
|
||||
display.drawSelectionRow(0, y - 1, display.width() - reserve, item_h - 1, sel);
|
||||
display.setCursor(2, y);
|
||||
display.print(itemLabel(item));
|
||||
if (item == IT_RFREQ && sel && _freq_editor.active) {
|
||||
_freq_editor.render(display, valCol(display), y);
|
||||
} else {
|
||||
int s = row - _item_count;
|
||||
const char* label = (s == 0) ? "Forwarded" : (s == 1) ? "Pool free" : "Queue";
|
||||
if (s == 0) snprintf(val, sizeof(val), "%lu", (unsigned long)the_mesh.getNumForwarded());
|
||||
else if (s == 1) snprintf(val, sizeof(val), "%d", the_mesh.getPoolFreeCount());
|
||||
else snprintf(val, sizeof(val), "%d", the_mesh.getOutboundQueueLen());
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.setCursor(2, y);
|
||||
display.print(label);
|
||||
itemValue(item, p, val, sizeof(val));
|
||||
display.drawTextRightAlign(display.width() - reserve - 2, y, val);
|
||||
}
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
drawScrollIndicator(display, start_y, visible * item_h, total, visible, _scroll);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
if (_preset_menu.active) _preset_menu.render(display);
|
||||
return (_preset_menu.active || _freq_editor.active) ? 50 : (show_stats ? 1000 : 500);
|
||||
return (_preset_menu.active || _freq_editor.active) ? 50 : 500;
|
||||
}
|
||||
|
||||
// x where the right-side value column starts (matches Settings' valCol math).
|
||||
@@ -273,6 +259,7 @@ public:
|
||||
if (item == IT_REPEATER && (left || right || enter)) {
|
||||
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
|
||||
buildItems(p);
|
||||
_dirty = true;
|
||||
return true;
|
||||
|
||||
@@ -607,7 +607,9 @@ class SettingsScreen : public UIScreen {
|
||||
} else if (item == POWER_SAVE) {
|
||||
display.print("Pwr save");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print((p && p->rx_powersave) ? "ON" : "OFF");
|
||||
// Forced off (and locked) while the repeater is on — it must hear all traffic.
|
||||
if (p && p->client_repeat) display.print("--");
|
||||
else display.print((p && p->rx_powersave) ? "ON" : "OFF");
|
||||
} else if (item == TX_APC) {
|
||||
display.print("Auto pwr");
|
||||
display.setCursor(valCol(display), y);
|
||||
@@ -998,6 +1000,7 @@ public:
|
||||
if (left && p->cr > 5) { p->cr--; _task->applyRadioParams(); _dirty = true; return true; }
|
||||
}
|
||||
if (_selected == POWER_SAVE && p && (left || right || enter)) {
|
||||
if (p->client_repeat) { _task->showAlert("Off while repeating", 900); return true; }
|
||||
p->rx_powersave ^= 1;
|
||||
_task->applyPowerSave();
|
||||
_dirty = true;
|
||||
|
||||
@@ -478,6 +478,15 @@ class HomeScreen : public UIScreen {
|
||||
if (blinkOn()) drawBoxedIcon(display, gX, ind, ind_h, ICON_TRAIL);
|
||||
leftmostX = gX - 1;
|
||||
}
|
||||
|
||||
// Repeater relaying. Same blink convention — a "leave it on and forget"
|
||||
// mode, so an at-a-glance cue matters (especially on a Custom profile,
|
||||
// where the device is off your own network while this is shown).
|
||||
if (_node_prefs && _node_prefs->client_repeat) {
|
||||
int rX = leftmostX - ind;
|
||||
if (blinkOn()) drawBoxedIcon(display, rX, ind, ind_h, ICON_REPEATER);
|
||||
leftmostX = rX - 1;
|
||||
}
|
||||
}
|
||||
return leftmostX;
|
||||
}
|
||||
@@ -995,7 +1004,8 @@ public:
|
||||
// Any blinking status-bar indicator needs a 1 s refresh to animate evenly —
|
||||
// but the status bar (and its icons) is hidden on the CLOCK page, so don't
|
||||
// pay the 1 s cadence there for icons that aren't drawn.
|
||||
bool need_blink = (_page != HomePage::CLOCK) && (auto_adv || _task->trail().isActive());
|
||||
bool repeating = _node_prefs && _node_prefs->client_repeat;
|
||||
bool need_blink = (_page != HomePage::CLOCK) && (auto_adv || _task->trail().isActive() || repeating);
|
||||
if (Features::IS_EINK) {
|
||||
// slow display: poll every 30 s; inbound msgs force immediate refresh via notify()
|
||||
return Features::HOME_REFRESH_MS;
|
||||
@@ -2099,7 +2109,10 @@ void UITask::applyTxPower() {
|
||||
|
||||
void UITask::applyPowerSave() {
|
||||
if (_node_prefs == NULL) return;
|
||||
radio_driver.setPowerSaving(_node_prefs->rx_powersave);
|
||||
// A repeater must hear every packet to relay it, so duty-cycle RX (which sleeps
|
||||
// between preamble checks) is forced off while repeating — the user's pref is
|
||||
// kept and restored when the repeater is switched off.
|
||||
radio_driver.setPowerSaving(_node_prefs->rx_powersave && !_node_prefs->client_repeat);
|
||||
}
|
||||
|
||||
void UITask::applyApc() {
|
||||
|
||||
@@ -158,6 +158,13 @@ MINI_ICON(ICON_TRAIL, 6, // map pin / location marker (GPS trail logging)
|
||||
packRow(".####."),
|
||||
packRow("..##.."));
|
||||
|
||||
MINI_ICON(ICON_REPEATER, 6, // » double chevron — relaying/forwarding (repeater active)
|
||||
packRow("#..#.."),
|
||||
packRow(".#..#."),
|
||||
packRow("..#..#"),
|
||||
packRow(".#..#."),
|
||||
packRow("#..#.."));
|
||||
|
||||
// Trail-map markers — centred on a point (see miniIconDrawCentered) rather
|
||||
// than anchored to a text line.
|
||||
MINI_ICON(ICON_MAP_DOT, 3, // ● filled trail point
|
||||
|
||||
@@ -197,6 +197,7 @@ public:
|
||||
uint32_t getNumRecvByType(uint8_t payload_type) const { return n_recv_by_type[payload_type & 0x0F]; }
|
||||
int getPoolFreeCount() const { return _mgr->getFreeCount(); }
|
||||
int getOutboundQueueLen() const { return _mgr->getOutboundTotal(); }
|
||||
uint16_t getErrFlags() const { return _err_flags; } // ERR_EVENT_* bitmask since last resetStats()
|
||||
virtual void resetStats() {
|
||||
n_sent_flood = n_sent_direct = n_recv_flood = n_recv_direct = 0;
|
||||
memset(n_sent_by_type, 0, sizeof(n_sent_by_type));
|
||||
|
||||
Reference in New Issue
Block a user