mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 23:08:11 +00:00
feat(repeater): dedicated Tools screen + optional radio profile
Move the repeater controls out of Settings > Radio (where the indented sub-item labels collided with the value column on a 128px OLED) onto their own full-width Tools > Repeater screen, holding the toggle, the five flood-politeness filters, and live forwarding stats (Forwarded / Pool free / Queue) together. Toggle text standardised to ON/OFF throughout. Adds an optional dedicated radio profile (Network: Current/Custom): enabling the repeater can switch the radio to a preset/manual freq/bw/sf/cr and disabling restores the companion's params (revert-on-disable). A profile equal to the companion = same network; a different one = drop onto a separate repeater network. MyMesh::applyRepeaterRadio() is the single decision point (boot + every toggle/edit), gated by repeaterProfileValid(); the profile re-applies after a reboot left with the repeater on. Persisted behind schema 0xC0DE0010, cleared to off when an upgraded file yields an invalid config. Docs updated (settings_screen, tools_screen, README, FEATURES). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -351,6 +351,22 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
(_prefs.repeat_min_snr < -30 || _prefs.repeat_min_snr > 20))
|
||||
_prefs.repeat_min_snr = NodePrefs::REPEAT_SNR_DISABLED;
|
||||
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));
|
||||
rd(&_prefs.repeater_bw, sizeof(_prefs.repeater_bw));
|
||||
rd(&_prefs.repeater_sf, sizeof(_prefs.repeater_sf));
|
||||
rd(&_prefs.repeater_cr, sizeof(_prefs.repeater_cr));
|
||||
// Pre-0x10 files leave stray sentinel bytes here. If the profile fields aren't a
|
||||
// valid radio config, force the profile off so the repeater can't switch to junk.
|
||||
if (_prefs.repeater_use_profile > 1) _prefs.repeater_use_profile = 0;
|
||||
if (!(_prefs.repeater_freq >= 100.0f && _prefs.repeater_freq <= 960.0f
|
||||
&& _prefs.repeater_sf >= 5 && _prefs.repeater_sf <= 12
|
||||
&& _prefs.repeater_cr >= 5 && _prefs.repeater_cr <= 8
|
||||
&& _prefs.repeater_bw >= 7.0f && _prefs.repeater_bw <= 510.0f)) {
|
||||
_prefs.repeater_use_profile = 0;
|
||||
_prefs.repeater_freq = _prefs.repeater_bw = 0.0f;
|
||||
_prefs.repeater_sf = _prefs.repeater_cr = 0;
|
||||
}
|
||||
// → 0xC0DE000B: append bot_commands_enabled + quiet-hours. Older files leave
|
||||
// stray bytes here; clamp so upgraders fall back to off / no quiet hours.
|
||||
if (_prefs.bot_commands_enabled > 1) _prefs.bot_commands_enabled = 0;
|
||||
@@ -513,6 +529,11 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
|
||||
file.write((uint8_t *)&_prefs.repeat_delay_boost, sizeof(_prefs.repeat_delay_boost));
|
||||
file.write((uint8_t *)&_prefs.repeat_min_snr, sizeof(_prefs.repeat_min_snr));
|
||||
file.write((uint8_t *)&_prefs.repeat_suppress_dup, sizeof(_prefs.repeat_suppress_dup));
|
||||
file.write((uint8_t *)&_prefs.repeater_use_profile, sizeof(_prefs.repeater_use_profile));
|
||||
file.write((uint8_t *)&_prefs.repeater_freq, sizeof(_prefs.repeater_freq));
|
||||
file.write((uint8_t *)&_prefs.repeater_bw, sizeof(_prefs.repeater_bw));
|
||||
file.write((uint8_t *)&_prefs.repeater_sf, sizeof(_prefs.repeater_sf));
|
||||
file.write((uint8_t *)&_prefs.repeater_cr, sizeof(_prefs.repeater_cr));
|
||||
|
||||
// Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL.
|
||||
uint32_t sentinel = NodePrefs::SCHEMA_SENTINEL;
|
||||
|
||||
@@ -1418,7 +1418,7 @@ void MyMesh::begin(bool has_display) {
|
||||
addChannel("Public", PUBLIC_GROUP_PSK); // pre-configure Andy's public channel
|
||||
_store->loadChannels(this);
|
||||
|
||||
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
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)
|
||||
@@ -1426,6 +1426,13 @@ void MyMesh::begin(bool has_display) {
|
||||
radio_driver.getRxBoostedGainMode() ? "Enabled" : "Disabled");
|
||||
}
|
||||
|
||||
void MyMesh::applyRepeaterRadio() {
|
||||
if (_prefs.client_repeat && _prefs.repeater_use_profile && repeaterProfileValid())
|
||||
radio_driver.setParams(_prefs.repeater_freq, _prefs.repeater_bw, _prefs.repeater_sf, _prefs.repeater_cr);
|
||||
else
|
||||
radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr);
|
||||
}
|
||||
|
||||
const char *MyMesh::getNodeName() {
|
||||
return _prefs.node_name;
|
||||
}
|
||||
|
||||
@@ -228,6 +228,19 @@ public:
|
||||
DataStore* getDataStore() const { return _store; }
|
||||
void applyApc(); // (re)initialise Adaptive Power Control from prefs
|
||||
|
||||
// True when the optional repeater radio profile is a valid LoRa config.
|
||||
bool repeaterProfileValid() const {
|
||||
return _prefs.repeater_freq >= 100.0f && _prefs.repeater_freq <= 960.0f
|
||||
&& _prefs.repeater_sf >= 5 && _prefs.repeater_sf <= 12
|
||||
&& _prefs.repeater_cr >= 5 && _prefs.repeater_cr <= 8
|
||||
&& _prefs.repeater_bw >= 7.0f && _prefs.repeater_bw <= 510.0f;
|
||||
}
|
||||
// Load the radio with the correct params for the current mode: the repeater
|
||||
// profile when relaying with a valid dedicated profile, otherwise the
|
||||
// companion's own params. Single source of truth, used at boot and whenever
|
||||
// the repeater toggle / network / profile changes.
|
||||
void applyRepeaterRadio();
|
||||
|
||||
bool isAckPending(uint32_t expected_ack) const {
|
||||
for (int i = 0; i < EXPECTED_ACK_TABLE_SIZE; i++)
|
||||
if (expected_ack_table[i].ack == expected_ack) return true;
|
||||
|
||||
@@ -173,11 +173,21 @@ struct NodePrefs { // persisted to file
|
||||
static const int8_t REPEAT_SNR_DISABLED = -128;
|
||||
uint8_t repeat_suppress_dup;
|
||||
|
||||
// Optional dedicated radio profile for repeater mode. When repeater_use_profile
|
||||
// is 1, enabling the repeater switches the radio to repeater_freq/bw/sf/cr and
|
||||
// disabling restores the companion's freq/bw/sf/cr (the fields above). 0 = the
|
||||
// repeater stays on the current companion frequency (default — no switch).
|
||||
uint8_t repeater_use_profile;
|
||||
float repeater_freq;
|
||||
float repeater_bw;
|
||||
uint8_t repeater_sf;
|
||||
uint8_t repeater_cr;
|
||||
|
||||
// Tail sentinel written at the end of /new_prefs. Bump the low byte when
|
||||
// adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so
|
||||
// older saves are detected on load and skipped (zero-init defaults kept).
|
||||
// High 24 bits identify the file format; low byte is the schema revision.
|
||||
static const uint32_t SCHEMA_SENTINEL = 0xC0DE000F;
|
||||
static const uint32_t SCHEMA_SENTINEL = 0xC0DE0010;
|
||||
|
||||
// Bit-index for each home page. Used by page_order (entries store bit+1) and
|
||||
// by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage
|
||||
|
||||
336
examples/companion_radio/ui-new/RepeaterScreen.h
Normal file
336
examples/companion_radio/ui-new/RepeaterScreen.h
Normal file
@@ -0,0 +1,336 @@
|
||||
#pragma once
|
||||
// Tools › Repeater — consolidates the repeater toggle, its flood "politeness"
|
||||
// filters, an optional dedicated radio profile, and live forwarding stats on one
|
||||
// screen. A dedicated screen (vs. the old Settings › Radio sub-items) gives
|
||||
// full-width rows, so the longer labels no longer collide with the value column,
|
||||
// and keeps the relaying controls next to the numbers that show them working.
|
||||
//
|
||||
// Network modes:
|
||||
// - Current — repeat on the companion's current frequency (default).
|
||||
// - Custom — enabling the repeater switches the radio to a dedicated profile
|
||||
// (preset or manual), and disabling restores the companion params.
|
||||
// Set the profile equal to the companion's for "same network".
|
||||
// Included by UITask.cpp.
|
||||
|
||||
#include <helpers/ui/DisplayDriver.h>
|
||||
#include <helpers/ui/UIScreen.h>
|
||||
#include "icons.h"
|
||||
#include "PopupMenu.h"
|
||||
#include "DigitEditor.h"
|
||||
#include "../RadioPresets.h"
|
||||
#include "../MyMesh.h"
|
||||
|
||||
extern MyMesh the_mesh;
|
||||
|
||||
class RepeaterScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
bool _dirty;
|
||||
int _sel; // index into the interactive items currently shown
|
||||
int _scroll; // first visible row (render keeps _sel in view)
|
||||
|
||||
enum Item {
|
||||
IT_REPEATER, IT_NETWORK,
|
||||
IT_RPRESET, IT_RFREQ, IT_RSF, IT_RBW, IT_RCR, // dedicated profile (Custom network)
|
||||
IT_SKIP, IT_HOPS, IT_YIELD, IT_SNR, IT_SUPPRESS
|
||||
};
|
||||
uint8_t _items[12];
|
||||
int _item_count;
|
||||
|
||||
PopupMenu _preset_menu;
|
||||
DigitEditor _freq_editor;
|
||||
uint8_t _preset_user_slot[NodePrefs::USER_RADIO_PRESET_MAX];
|
||||
int _preset_user_count = 0;
|
||||
|
||||
static const float* bwOpts(int& count) {
|
||||
static const float OPTS[] = { 7.8f, 10.4f, 15.6f, 20.8f, 31.25f, 41.7f, 62.5f, 125.0f, 250.0f, 500.0f };
|
||||
count = 10;
|
||||
return OPTS;
|
||||
}
|
||||
int rptBwIndex(NodePrefs* p) const {
|
||||
int n; const float* o = bwOpts(n);
|
||||
for (int i = 0; i < n; i++) if (o[i] == p->repeater_bw) return i;
|
||||
return 6; // default to 62.5
|
||||
}
|
||||
|
||||
void buildItems(NodePrefs* p) {
|
||||
_item_count = 0;
|
||||
_items[_item_count++] = IT_REPEATER;
|
||||
if (p && p->client_repeat) {
|
||||
_items[_item_count++] = IT_NETWORK;
|
||||
if (p->repeater_use_profile) {
|
||||
_items[_item_count++] = IT_RPRESET;
|
||||
_items[_item_count++] = IT_RFREQ;
|
||||
_items[_item_count++] = IT_RSF;
|
||||
_items[_item_count++] = IT_RBW;
|
||||
_items[_item_count++] = IT_RCR;
|
||||
}
|
||||
_items[_item_count++] = IT_SKIP;
|
||||
_items[_item_count++] = IT_HOPS;
|
||||
_items[_item_count++] = IT_YIELD;
|
||||
_items[_item_count++] = IT_SNR;
|
||||
_items[_item_count++] = IT_SUPPRESS;
|
||||
}
|
||||
if (_sel >= _item_count) _sel = _item_count - 1;
|
||||
if (_sel < 0) _sel = 0;
|
||||
}
|
||||
|
||||
static const char* itemLabel(int item) {
|
||||
switch (item) {
|
||||
case IT_REPEATER: return "Repeater";
|
||||
case IT_NETWORK: return "Network";
|
||||
case IT_RPRESET: return "Rpt preset";
|
||||
case IT_RFREQ: return "Rpt freq";
|
||||
case IT_RSF: return "Rpt SF";
|
||||
case IT_RBW: return "Rpt BW";
|
||||
case IT_RCR: return "Rpt CR";
|
||||
case IT_SKIP: return "Skip advert";
|
||||
case IT_HOPS: return "Max hops";
|
||||
case IT_YIELD: return "Yield";
|
||||
case IT_SNR: return "Min SNR";
|
||||
case IT_SUPPRESS: return "Suppress dup";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Name of the preset matching the repeater profile, or "Custom".
|
||||
const char* rptPresetName(NodePrefs* p) const {
|
||||
for (int i = 0; i < RADIO_PRESET_COUNT; i++) {
|
||||
const RadioPreset& r = RADIO_PRESETS[i];
|
||||
if (r.freq == p->repeater_freq && r.bw == p->repeater_bw && r.sf == p->repeater_sf && r.cr == p->repeater_cr)
|
||||
return r.name;
|
||||
}
|
||||
for (int i = 0; i < NodePrefs::USER_RADIO_PRESET_MAX; i++) {
|
||||
const NodePrefs::UserRadioPreset& u = p->user_radio_presets[i];
|
||||
if (!u.name[0]) continue;
|
||||
if (u.freq == p->repeater_freq && u.bw == p->repeater_bw && u.sf == p->repeater_sf && u.cr == p->repeater_cr)
|
||||
return u.name;
|
||||
}
|
||||
return "Custom";
|
||||
}
|
||||
|
||||
void itemValue(int item, NodePrefs* p, char* buf, size_t n) const {
|
||||
if (!p) { strncpy(buf, "OFF", n); buf[n-1]=0; return; }
|
||||
switch (item) {
|
||||
case IT_REPEATER: strncpy(buf, p->client_repeat ? "ON" : "OFF", n); break;
|
||||
case IT_NETWORK: strncpy(buf, p->repeater_use_profile ? "Custom" : "Current", n); break;
|
||||
case IT_RPRESET: strncpy(buf, rptPresetName(p), n); break;
|
||||
case IT_RFREQ: snprintf(buf, n, "%.3f", p->repeater_freq); break;
|
||||
case IT_RSF: snprintf(buf, n, "%d", (int)p->repeater_sf); break;
|
||||
case IT_RBW: snprintf(buf, n, "%.1f", p->repeater_bw); break;
|
||||
case IT_RCR: snprintf(buf, n, "%d", (int)p->repeater_cr); break;
|
||||
case IT_SKIP: strncpy(buf, p->repeat_skip_adverts ? "ON" : "OFF", n); break;
|
||||
case IT_HOPS:
|
||||
if (p->repeat_max_hops > 0) snprintf(buf, n, "%d", (int)p->repeat_max_hops);
|
||||
else strncpy(buf, "OFF", n);
|
||||
break;
|
||||
case IT_YIELD:
|
||||
if (p->repeat_delay_boost > 0) snprintf(buf, n, "x%d", (int)p->repeat_delay_boost + 1);
|
||||
else strncpy(buf, "OFF", n);
|
||||
break;
|
||||
case IT_SNR:
|
||||
if (p->repeat_min_snr != NodePrefs::REPEAT_SNR_DISABLED) snprintf(buf, n, "%ddB", (int)p->repeat_min_snr);
|
||||
else strncpy(buf, "OFF", n);
|
||||
break;
|
||||
case IT_SUPPRESS: strncpy(buf, p->repeat_suppress_dup ? "ON" : "OFF", n); break;
|
||||
default: strncpy(buf, "", n); break;
|
||||
}
|
||||
buf[n - 1] = '\0';
|
||||
}
|
||||
|
||||
// Seed the profile from the companion's current params (used when first
|
||||
// switching to Custom, so the starting point is a valid, familiar config).
|
||||
void seedProfileFromCompanion(NodePrefs* p) {
|
||||
p->repeater_freq = p->freq; p->repeater_bw = p->bw;
|
||||
p->repeater_sf = p->sf; p->repeater_cr = p->cr;
|
||||
}
|
||||
|
||||
void openPresetMenu(NodePrefs* p) {
|
||||
_preset_menu.begin("Repeater Preset", 6);
|
||||
for (int i = 0; i < RADIO_PRESET_COUNT; i++) _preset_menu.addItem(RADIO_PRESETS[i].name);
|
||||
_preset_user_count = 0;
|
||||
for (int i = 0; i < NodePrefs::USER_RADIO_PRESET_MAX; i++) {
|
||||
if (!p->user_radio_presets[i].name[0]) continue;
|
||||
_preset_menu.addItem(p->user_radio_presets[i].name);
|
||||
_preset_user_slot[_preset_user_count++] = (uint8_t)i;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
RepeaterScreen(UITask* task) : _task(task), _dirty(false), _sel(0), _scroll(0), _item_count(1) {}
|
||||
|
||||
void enter() { _dirty = false; _sel = 0; _scroll = 0; _preset_menu.active = false; _freq_editor.active = false; }
|
||||
|
||||
int render(DisplayDriver& display) override {
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
buildItems(p);
|
||||
display.setTextSize(1);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
display.drawCenteredHeader("REPEATER");
|
||||
|
||||
const int item_h = display.lineStep();
|
||||
const int start_y = display.listStart();
|
||||
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);
|
||||
|
||||
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;
|
||||
if (_scroll < 0) _scroll = 0;
|
||||
|
||||
const int reserve = scrollIndicatorReserve(display, total, visible);
|
||||
for (int i = 0; i < visible && (_scroll + i) < total; i++) {
|
||||
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);
|
||||
} 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);
|
||||
display.drawTextRightAlign(display.width() - reserve - 2, y, val);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
// x where the right-side value column starts (matches Settings' valCol math).
|
||||
static int valCol(DisplayDriver& d) { return d.width() - d.getCharWidth() * 8; }
|
||||
|
||||
bool handleInput(char c) override {
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
|
||||
// Modal overlays first.
|
||||
if (_preset_menu.active) {
|
||||
auto res = _preset_menu.handleInput(c);
|
||||
if (res == PopupMenu::SELECTED && p) {
|
||||
int idx = _preset_menu.selectedIndex();
|
||||
if (idx >= 0 && idx < RADIO_PRESET_COUNT) {
|
||||
const RadioPreset& r = RADIO_PRESETS[idx];
|
||||
p->repeater_freq = r.freq; p->repeater_bw = r.bw; p->repeater_sf = r.sf; p->repeater_cr = r.cr;
|
||||
} else {
|
||||
int u = idx - RADIO_PRESET_COUNT;
|
||||
if (u >= 0 && u < _preset_user_count) {
|
||||
const NodePrefs::UserRadioPreset& up = p->user_radio_presets[_preset_user_slot[u]];
|
||||
p->repeater_freq = up.freq; p->repeater_bw = up.bw; p->repeater_sf = up.sf; p->repeater_cr = up.cr;
|
||||
}
|
||||
}
|
||||
the_mesh.applyRepeaterRadio(); // live if currently relaying on the profile
|
||||
_dirty = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (_freq_editor.active) {
|
||||
float before = _freq_editor.value;
|
||||
_freq_editor.handleInput(c);
|
||||
if (p && _freq_editor.value != before) {
|
||||
p->repeater_freq = _freq_editor.value;
|
||||
the_mesh.applyRepeaterRadio();
|
||||
_dirty = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) {
|
||||
if (_dirty) the_mesh.savePrefs();
|
||||
_task->gotoToolsScreen();
|
||||
return true;
|
||||
}
|
||||
if (c == KEY_UP) { _sel = (_sel > 0) ? _sel - 1 : _item_count - 1; return true; }
|
||||
if (c == KEY_DOWN) { _sel = (_sel < _item_count - 1) ? _sel + 1 : 0; return true; }
|
||||
if (!p) return false;
|
||||
|
||||
bool right = keyIsNext(c);
|
||||
bool left = keyIsPrev(c);
|
||||
bool enter = (c == KEY_ENTER);
|
||||
int item = _items[_sel];
|
||||
|
||||
if (item == IT_REPEATER && (left || right || enter)) {
|
||||
p->client_repeat ^= 1;
|
||||
the_mesh.applyRepeaterRadio(); // switch to profile on enable / restore companion on disable
|
||||
buildItems(p);
|
||||
_dirty = true;
|
||||
return true;
|
||||
}
|
||||
if (item == IT_NETWORK && (left || right || enter)) {
|
||||
p->repeater_use_profile ^= 1;
|
||||
if (p->repeater_use_profile && !the_mesh.repeaterProfileValid())
|
||||
seedProfileFromCompanion(p); // start Custom from a valid, familiar config
|
||||
the_mesh.applyRepeaterRadio();
|
||||
buildItems(p);
|
||||
_dirty = true;
|
||||
return true;
|
||||
}
|
||||
if (item == IT_RPRESET && enter) { openPresetMenu(p); return true; }
|
||||
if (item == IT_RFREQ && enter) {
|
||||
float lo, hi; radio_driver.getFreqBounds(lo, hi);
|
||||
_freq_editor.begin(p->repeater_freq, lo, hi, 3, 3);
|
||||
return true;
|
||||
}
|
||||
if (item == IT_RSF) {
|
||||
if (right && p->repeater_sf < 12) { p->repeater_sf++; the_mesh.applyRepeaterRadio(); _dirty = true; return true; }
|
||||
if (left && p->repeater_sf > 5) { p->repeater_sf--; the_mesh.applyRepeaterRadio(); _dirty = true; return true; }
|
||||
}
|
||||
if (item == IT_RBW) {
|
||||
int n; const float* o = bwOpts(n); int idx = rptBwIndex(p);
|
||||
if (right && idx < n - 1) { p->repeater_bw = o[idx + 1]; the_mesh.applyRepeaterRadio(); _dirty = true; return true; }
|
||||
if (left && idx > 0) { p->repeater_bw = o[idx - 1]; the_mesh.applyRepeaterRadio(); _dirty = true; return true; }
|
||||
}
|
||||
if (item == IT_RCR) {
|
||||
if (right && p->repeater_cr < 8) { p->repeater_cr++; the_mesh.applyRepeaterRadio(); _dirty = true; return true; }
|
||||
if (left && p->repeater_cr > 5) { p->repeater_cr--; the_mesh.applyRepeaterRadio(); _dirty = true; return true; }
|
||||
}
|
||||
if (item == IT_SKIP && (left || right || enter)) {
|
||||
p->repeat_skip_adverts ^= 1; _dirty = true; return true;
|
||||
}
|
||||
if (item == IT_HOPS) {
|
||||
if (right && p->repeat_max_hops < 8) { p->repeat_max_hops++; _dirty = true; return true; }
|
||||
if (left && p->repeat_max_hops > 0) { p->repeat_max_hops--; _dirty = true; return true; }
|
||||
}
|
||||
if (item == IT_YIELD) {
|
||||
if (right && p->repeat_delay_boost < 8) { p->repeat_delay_boost++; _dirty = true; return true; }
|
||||
if (left && p->repeat_delay_boost > 0) { p->repeat_delay_boost--; _dirty = true; return true; }
|
||||
}
|
||||
if (item == IT_SNR) {
|
||||
int8_t v = p->repeat_min_snr;
|
||||
if (right) {
|
||||
v = (v == NodePrefs::REPEAT_SNR_DISABLED) ? -20 : (v < 10 ? v + 1 : 10);
|
||||
p->repeat_min_snr = v; _dirty = true; return true;
|
||||
}
|
||||
if (left) {
|
||||
if (v != NodePrefs::REPEAT_SNR_DISABLED)
|
||||
p->repeat_min_snr = (v <= -20) ? NodePrefs::REPEAT_SNR_DISABLED : (int8_t)(v - 1);
|
||||
_dirty = true; return true;
|
||||
}
|
||||
}
|
||||
if (item == IT_SUPPRESS && (left || right || enter)) {
|
||||
p->repeat_suppress_dup ^= 1; _dirty = true; return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -58,12 +58,6 @@ class SettingsScreen : public UIScreen {
|
||||
TX_POWER,
|
||||
RADIO_PRESET,
|
||||
CUSTOM_FREQ, CUSTOM_SF, CUSTOM_BW, CUSTOM_CR,
|
||||
REPEATER,
|
||||
RPT_SKIP_ADV, // repeater politeness sub-items, only shown when REPEATER is on
|
||||
RPT_MAX_HOPS,
|
||||
RPT_DELAY,
|
||||
RPT_MIN_SNR,
|
||||
RPT_SUPPRESS,
|
||||
POWER_SAVE,
|
||||
TX_APC,
|
||||
// System section
|
||||
@@ -279,22 +273,11 @@ class SettingsScreen : public UIScreen {
|
||||
cur_collapsed = (_collapsed >> cur_sec) & 1;
|
||||
if (_vis_count < MAX_VIS) _vis[_vis_count++] = (uint8_t)i;
|
||||
} else if (!cur_collapsed && _vis_count < MAX_VIS) {
|
||||
// Repeater politeness knobs are meaningless when the repeater is off, so
|
||||
// hide them until it's enabled — keeps the Radio section short by default.
|
||||
if (isRepeaterSubItem(i)) {
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
if (!p || !p->client_repeat) continue;
|
||||
}
|
||||
_vis[_vis_count++] = (uint8_t)i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool isRepeaterSubItem(int item) const {
|
||||
return item == RPT_SKIP_ADV || item == RPT_MAX_HOPS || item == RPT_DELAY
|
||||
|| item == RPT_MIN_SNR || item == RPT_SUPPRESS;
|
||||
}
|
||||
|
||||
bool isHomePage(int item) const {
|
||||
return item == HOME_CLOCK || item == HOME_RECENT || item == HOME_RADIO ||
|
||||
item == HOME_BT || item == HOME_ADVERT || item == HOME_TOOLS ||
|
||||
@@ -621,39 +604,6 @@ class SettingsScreen : public UIScreen {
|
||||
snprintf(buf, sizeof(buf), "%d", p ? (int)p->cr : 0);
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(buf);
|
||||
} else if (item == REPEATER) {
|
||||
display.print("Repeater");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print((p && p->client_repeat) ? "ON" : "OFF");
|
||||
} else if (item == RPT_SKIP_ADV) {
|
||||
display.print(" Skip advert");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print((p && p->repeat_skip_adverts) ? "ON" : "OFF");
|
||||
} else if (item == RPT_MAX_HOPS) {
|
||||
display.print(" Max hops");
|
||||
char buf[6];
|
||||
if (p && p->repeat_max_hops > 0) snprintf(buf, sizeof(buf), "%d", (int)p->repeat_max_hops);
|
||||
else strcpy(buf, "Off");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(buf);
|
||||
} else if (item == RPT_DELAY) {
|
||||
display.print(" Yield");
|
||||
char buf[6];
|
||||
if (p && p->repeat_delay_boost > 0) snprintf(buf, sizeof(buf), "x%d", (int)p->repeat_delay_boost + 1);
|
||||
else strcpy(buf, "Off");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(buf);
|
||||
} else if (item == RPT_MIN_SNR) {
|
||||
display.print(" Min SNR");
|
||||
char buf[8];
|
||||
if (p && p->repeat_min_snr != NodePrefs::REPEAT_SNR_DISABLED) snprintf(buf, sizeof(buf), "%ddB", (int)p->repeat_min_snr);
|
||||
else strcpy(buf, "Off");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(buf);
|
||||
} else if (item == RPT_SUPPRESS) {
|
||||
display.print(" Suppress dup");
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print((p && p->repeat_suppress_dup) ? "ON" : "OFF");
|
||||
} else if (item == POWER_SAVE) {
|
||||
display.print("Pwr save");
|
||||
display.setCursor(valCol(display), y);
|
||||
@@ -1047,43 +997,6 @@ public:
|
||||
if (right && p->cr < 8) { p->cr++; _task->applyRadioParams(); _dirty = true; return true; }
|
||||
if (left && p->cr > 5) { p->cr--; _task->applyRadioParams(); _dirty = true; return true; }
|
||||
}
|
||||
if (_selected == REPEATER && p && (left || right || enter)) {
|
||||
p->client_repeat = p->client_repeat ? 0 : 1;
|
||||
buildVis(); // show/hide the politeness sub-items immediately
|
||||
_dirty = true;
|
||||
return true;
|
||||
}
|
||||
if (_selected == RPT_SKIP_ADV && p && (left || right || enter)) {
|
||||
p->repeat_skip_adverts ^= 1;
|
||||
_dirty = true;
|
||||
return true;
|
||||
}
|
||||
if (_selected == RPT_MAX_HOPS && p) {
|
||||
if (right && p->repeat_max_hops < 8) { p->repeat_max_hops++; _dirty = true; return true; }
|
||||
if (left && p->repeat_max_hops > 0) { p->repeat_max_hops--; _dirty = true; return true; }
|
||||
}
|
||||
if (_selected == RPT_DELAY && p) {
|
||||
if (right && p->repeat_delay_boost < 8) { p->repeat_delay_boost++; _dirty = true; return true; }
|
||||
if (left && p->repeat_delay_boost > 0) { p->repeat_delay_boost--; _dirty = true; return true; }
|
||||
}
|
||||
if (_selected == RPT_MIN_SNR && p) {
|
||||
// Steps -20..10 dB; stepping left past the floor turns the filter off.
|
||||
int8_t v = p->repeat_min_snr;
|
||||
if (right) {
|
||||
v = (v == NodePrefs::REPEAT_SNR_DISABLED) ? -20 : (v < 10 ? v + 1 : 10);
|
||||
p->repeat_min_snr = v; _dirty = true; return true;
|
||||
}
|
||||
if (left) {
|
||||
if (v != NodePrefs::REPEAT_SNR_DISABLED)
|
||||
p->repeat_min_snr = (v <= -20) ? NodePrefs::REPEAT_SNR_DISABLED : (int8_t)(v - 1);
|
||||
_dirty = true; return true;
|
||||
}
|
||||
}
|
||||
if (_selected == RPT_SUPPRESS && p && (left || right || enter)) {
|
||||
p->repeat_suppress_dup ^= 1;
|
||||
_dirty = true;
|
||||
return true;
|
||||
}
|
||||
if (_selected == POWER_SAVE && p && (left || right || enter)) {
|
||||
p->rx_powersave ^= 1;
|
||||
_task->applyPowerSave();
|
||||
|
||||
@@ -7,7 +7,7 @@ class ToolsScreen : public UIScreen {
|
||||
int _sel;
|
||||
int _scroll = 0;
|
||||
|
||||
static const int ITEM_COUNT = 7;
|
||||
static const int ITEM_COUNT = 8;
|
||||
static const char* ITEMS[ITEM_COUNT];
|
||||
|
||||
public:
|
||||
@@ -38,8 +38,9 @@ public:
|
||||
if (_sel == 4) { _task->gotoTrailScreen(); return true; }
|
||||
if (_sel == 5) { _task->gotoCompassScreen(); return true; }
|
||||
if (_sel == 6) { _task->gotoDiagnosticsScreen(); return true; }
|
||||
if (_sel == 7) { _task->gotoRepeaterScreen(); return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const char* ToolsScreen::ITEMS[7] = { "Ringtone Editor", "Auto-Reply Bot", "Nearby Nodes", "Auto-Advert", "Trail", "Compass", "Diagnostics" };
|
||||
const char* ToolsScreen::ITEMS[8] = { "Ringtone Editor", "Auto-Reply Bot", "Nearby Nodes", "Auto-Advert", "Trail", "Compass", "Diagnostics", "Repeater" };
|
||||
|
||||
@@ -126,6 +126,7 @@ static const int QUICK_MSGS_MAX = 10;
|
||||
#include "TrailScreen.h"
|
||||
#include "CompassScreen.h"
|
||||
#include "DiagnosticsScreen.h"
|
||||
#include "RepeaterScreen.h"
|
||||
#include "ToolsScreen.h"
|
||||
|
||||
#ifndef BATT_MIN_MILLIVOLTS
|
||||
@@ -1200,6 +1201,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no
|
||||
trail_screen = new TrailScreen(this, &_trail);
|
||||
compass_screen = new CompassScreen(this);
|
||||
diag_screen = new DiagnosticsScreen(this);
|
||||
repeater_screen = new RepeaterScreen(this);
|
||||
applyBrightness();
|
||||
applyFont();
|
||||
applyRotation();
|
||||
@@ -1250,6 +1252,11 @@ void UITask::gotoDiagnosticsScreen() {
|
||||
setCurrScreen(diag_screen);
|
||||
}
|
||||
|
||||
void UITask::gotoRepeaterScreen() {
|
||||
((RepeaterScreen*)repeater_screen)->enter();
|
||||
setCurrScreen(repeater_screen);
|
||||
}
|
||||
|
||||
void UITask::gotoAutoAdvertScreen() {
|
||||
((AutoAdvertScreen*)auto_advert_screen)->enter();
|
||||
setCurrScreen(auto_advert_screen);
|
||||
|
||||
@@ -80,6 +80,7 @@ class UITask : public AbstractUITask {
|
||||
UIScreen* trail_screen;
|
||||
UIScreen* compass_screen;
|
||||
UIScreen* diag_screen;
|
||||
UIScreen* repeater_screen;
|
||||
UIScreen* curr;
|
||||
CayenneLPP _dash_lpp;
|
||||
TrailStore _trail;
|
||||
@@ -154,6 +155,7 @@ public:
|
||||
void gotoTrailScreen();
|
||||
void gotoCompassScreen();
|
||||
void gotoDiagnosticsScreen();
|
||||
void gotoRepeaterScreen();
|
||||
TrailStore& trail() { return _trail; }
|
||||
WaypointStore& waypoints() { return _waypoints; }
|
||||
// Shared on-screen keyboard — only one screen drives it at a time.
|
||||
|
||||
Reference in New Issue
Block a user