mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-27 15:28:11 +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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user