mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 23:08:11 +00:00
feat(companion): repeater mode, radio presets, and manual RF tuning in Settings
Settings > Radio gains: - Repeater toggle — turns on flood forwarding (client_repeat) on the current operating frequency, with loop-detection and an 8-hop advert cap ported from simple_repeater's allowPacketForward. - Packet pool bumped 16->32: forwarding's queued retransmits were starving Dispatcher::checkRecv()'s allocNew(), silently dropping incoming DMs and channel messages while repeating. - Preset picker (16 community-suggested region presets) plus manual Freq/SF/BW/CR fields for fine-tuning. Freq bounds come from the radio driver's own RadioLib-validated range (SX1262: 150-960MHz) instead of a guessed sanity bound. - Save current radio settings as a named user preset (4 persisted slots), alongside the built-in list. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -331,6 +331,7 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
rd(&_prefs.bot_quiet_start, sizeof(_prefs.bot_quiet_start));
|
||||
rd(&_prefs.bot_quiet_end, sizeof(_prefs.bot_quiet_end));
|
||||
rd(_prefs.bot_trigger_ch, sizeof(_prefs.bot_trigger_ch));
|
||||
rd(_prefs.user_radio_presets, sizeof(_prefs.user_radio_presets));
|
||||
// → 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;
|
||||
@@ -385,6 +386,9 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no
|
||||
// so an existing channel bot keeps reacting to the same word after upgrade.
|
||||
if (_prefs.bot_trigger_ch[0] == '\0')
|
||||
strncpy(_prefs.bot_trigger_ch, _prefs.bot_trigger, sizeof(_prefs.bot_trigger_ch) - 1);
|
||||
// → 0xC0DE000D: append user_radio_presets. No clamping needed — rd() already
|
||||
// zero-inits it on a pre-0x0D file, and name[0]=='\0' is exactly the "empty
|
||||
// slot" sentinel the UI already expects.
|
||||
}
|
||||
|
||||
file.close();
|
||||
@@ -484,6 +488,7 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_
|
||||
file.write((uint8_t *)&_prefs.bot_quiet_start, sizeof(_prefs.bot_quiet_start));
|
||||
file.write((uint8_t *)&_prefs.bot_quiet_end, sizeof(_prefs.bot_quiet_end));
|
||||
file.write((uint8_t *)_prefs.bot_trigger_ch, sizeof(_prefs.bot_trigger_ch));
|
||||
file.write((uint8_t *)_prefs.user_radio_presets, sizeof(_prefs.user_radio_presets));
|
||||
|
||||
// Tail sentinel — must be last. See NodePrefs::SCHEMA_SENTINEL.
|
||||
uint32_t sentinel = NodePrefs::SCHEMA_SENTINEL;
|
||||
|
||||
@@ -521,8 +521,36 @@ bool MyMesh::filterRecvFloodPacket(mesh::Packet* packet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Loop guard for flood packets, ported from simple_repeater's LOOP_DETECT_MODERATE
|
||||
// thresholds (max times this node's hash may already appear in the path, by hash size).
|
||||
// A companion moves around, so it re-enters its own flood's path more easily than a
|
||||
// fixed repeater — hardcoded rather than configurable since there's no CLI here.
|
||||
static const uint8_t REPEAT_LOOP_MAX[] = { 0, /*1-byte*/ 2, /*2-byte*/ 1, /*3-byte*/ 1 };
|
||||
// Caps how many hops an ADVERT flood gets repeated, matching simple_repeater's default
|
||||
// flood_max_advert — adverts are the most frequent flood traffic, so this is the one
|
||||
// depth limit worth keeping even without the rest of simple_repeater's flood_max knobs.
|
||||
static const uint8_t REPEAT_MAX_ADVERT_HOPS = 8;
|
||||
|
||||
bool MyMesh::isRepeatLooped(const mesh::Packet* packet) const {
|
||||
uint8_t hash_size = packet->getPathHashSize();
|
||||
uint8_t hash_count = packet->getPathHashCount();
|
||||
uint8_t n = 0;
|
||||
const uint8_t* path = packet->path;
|
||||
while (hash_count > 0) {
|
||||
if (self_id.isHashMatch(path, hash_size)) n++;
|
||||
hash_count--;
|
||||
path += hash_size;
|
||||
}
|
||||
return n >= REPEAT_LOOP_MAX[hash_size];
|
||||
}
|
||||
|
||||
bool MyMesh::allowPacketForward(const mesh::Packet* packet) {
|
||||
return _prefs.client_repeat != 0;
|
||||
if (_prefs.client_repeat == 0) return false;
|
||||
if (packet->isRouteFlood()) {
|
||||
if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->getPathHashCount() >= REPEAT_MAX_ADVERT_HOPS) return false;
|
||||
if (isRepeatLooped(packet)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void MyMesh::sendFloodScoped(const TransportKey& scope, mesh::Packet* pkt, uint32_t delay_millis) {
|
||||
@@ -1214,7 +1242,13 @@ void MyMesh::onAckRecv(mesh::Packet* packet, uint32_t ack_crc) {
|
||||
}
|
||||
|
||||
MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMeshTables &tables, DataStore& store, AbstractUITask* ui)
|
||||
: BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(16), tables),
|
||||
// Sized to match simple_repeater's pool (32), not the old client-only 16: with the
|
||||
// on-device Repeater toggle, queued retransmits (adverts/channel flood from
|
||||
// neighbours) can now hold packet-pool slots for their retransmit delay window. A
|
||||
// pool too small for that starves Dispatcher::checkRecv()'s allocNew(), which then
|
||||
// silently drops every incoming packet — DMs and channels included — until a slot
|
||||
// frees up.
|
||||
: BaseChatMesh(radio, *new ArduinoMillis(), rng, rtc, *new StaticPoolPacketManager(32), tables),
|
||||
_serial(NULL), telemetry(MAX_PACKET_PAYLOAD - 4), _store(&store), _ui(ui) {
|
||||
_iter_started = false;
|
||||
_cli_rescue = false;
|
||||
|
||||
@@ -152,6 +152,7 @@ protected:
|
||||
uint8_t getExtraAckTransmitCount() const override;
|
||||
bool filterRecvFloodPacket(mesh::Packet* packet) override;
|
||||
bool allowPacketForward(const mesh::Packet* packet) override;
|
||||
bool isRepeatLooped(const mesh::Packet* packet) const;
|
||||
|
||||
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;
|
||||
|
||||
@@ -140,11 +140,23 @@ struct NodePrefs { // persisted to file
|
||||
// shows ✗. 0 = no auto-resend (single attempt). Default 2.
|
||||
uint8_t dm_resend_count;
|
||||
|
||||
// User-saved radio presets (Settings > Radio > Preset > "Save current...").
|
||||
// name[0] == '\0' marks an empty slot.
|
||||
struct UserRadioPreset {
|
||||
char name[16];
|
||||
float freq;
|
||||
float bw;
|
||||
uint8_t sf;
|
||||
uint8_t cr;
|
||||
};
|
||||
static const uint8_t USER_RADIO_PRESET_MAX = 4;
|
||||
UserRadioPreset user_radio_presets[USER_RADIO_PRESET_MAX];
|
||||
|
||||
// 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 = 0xC0DE000C;
|
||||
static const uint32_t SCHEMA_SENTINEL = 0xC0DE000D;
|
||||
|
||||
// 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
|
||||
|
||||
35
examples/companion_radio/RadioPresets.h
Normal file
35
examples/companion_radio/RadioPresets.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
// Community-suggested per-region RF presets (freq/bw/sf/cr), sourced from the
|
||||
// MeshCore app's "suggested_radio_settings" config plus a few community additions.
|
||||
// Kept here (rather than guessed) since wrong values are a regulatory/connectivity
|
||||
// concern, not just cosmetic.
|
||||
struct RadioPreset {
|
||||
const char* name;
|
||||
float freq; // MHz
|
||||
float bw; // kHz
|
||||
uint8_t sf;
|
||||
uint8_t cr;
|
||||
};
|
||||
|
||||
// Deprecated entries and anything outside the 868-9xx MHz bands (433MHz) have been
|
||||
// dropped — only current, in-band presets are kept.
|
||||
static const RadioPreset RADIO_PRESETS[] = {
|
||||
{ "Australia", 915.800f, 250.0f, 10, 5 },
|
||||
{ "Australia (Narrow)", 916.575f, 62.5f, 7, 8 },
|
||||
{ "Australia (Mid)", 915.075f, 125.0f, 9, 5 },
|
||||
{ "Australia: SA, WA", 923.125f, 62.5f, 8, 8 },
|
||||
{ "Australia: QLD", 923.125f, 62.5f, 8, 5 },
|
||||
{ "Brazil", 923.125f, 62.5f, 8, 8 },
|
||||
{ "EU/UK (Narrow)", 869.618f, 62.5f, 8, 8 },
|
||||
{ "Czech Republic (Narrow)", 869.432f, 62.5f, 7, 5 },
|
||||
{ "Netherlands", 869.618f, 62.5f, 7, 5 },
|
||||
{ "New Zealand", 917.375f, 250.0f, 11, 5 },
|
||||
{ "New Zealand (Narrow)", 917.375f, 62.5f, 7, 5 },
|
||||
{ "Portugal 868", 869.618f, 62.5f, 7, 6 },
|
||||
{ "Switzerland", 869.618f, 62.5f, 8, 8 },
|
||||
{ "USA/Canada (Recommended)",910.525f, 62.5f, 7, 5 },
|
||||
{ "USA/SOCAL", 927.875f, 62.5f, 7, 5 },
|
||||
{ "Vietnam (Narrow)", 920.250f, 62.5f, 8, 5 },
|
||||
};
|
||||
static const int RADIO_PRESET_COUNT = sizeof(RADIO_PRESETS) / sizeof(RADIO_PRESETS[0]);
|
||||
@@ -6,7 +6,7 @@
|
||||
// Caller owns item string lifetimes — addItem() stores const char* pointers.
|
||||
// Navigation wraps around: up from first item goes to last, and vice versa.
|
||||
struct PopupMenu {
|
||||
static const int PM_MAX_ITEMS = 16;
|
||||
static const int PM_MAX_ITEMS = 24;
|
||||
|
||||
const char* _items[PM_MAX_ITEMS];
|
||||
int _count;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// Included by UITask.cpp after SensorPlaceholders.h is defined.
|
||||
|
||||
#include "../Features.h"
|
||||
#include "../RadioPresets.h"
|
||||
|
||||
class SettingsScreen : public UIScreen {
|
||||
UITask* _task;
|
||||
@@ -54,6 +55,9 @@ class SettingsScreen : public UIScreen {
|
||||
// Radio section
|
||||
SECTION_RADIO,
|
||||
TX_POWER,
|
||||
RADIO_PRESET,
|
||||
CUSTOM_FREQ, CUSTOM_SF, CUSTOM_BW, CUSTOM_CR,
|
||||
REPEATER,
|
||||
POWER_SAVE,
|
||||
TX_APC,
|
||||
// System section
|
||||
@@ -98,6 +102,10 @@ class SettingsScreen : public UIScreen {
|
||||
static const int SOUND_COUNT = 4;
|
||||
static const char* AD_SCOPE_LABELS[2];
|
||||
static const int AD_SCOPE_COUNT = 2;
|
||||
// Standard SX126x/SX127x LoRa bandwidths (kHz) — manual BW field cycles through
|
||||
// these rather than a free-form value, so it can't land on an unsupported setting.
|
||||
static const float CUSTOM_BW_OPTS[10];
|
||||
static const int CUSTOM_BW_COUNT = 10;
|
||||
#if FEAT_FULL_REFRESH_SETTING
|
||||
static const char* EINK_FULL_REFRESH_LABELS[5];
|
||||
static const int EINK_FULL_REFRESH_COUNT = 5;
|
||||
@@ -111,6 +119,79 @@ class SettingsScreen : public UIScreen {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool matchesPreset(NodePrefs* p, float freq, float bw, uint8_t sf, uint8_t cr) {
|
||||
return fabsf(p->freq - freq) < 0.001f && fabsf(p->bw - bw) < 0.001f && p->sf == sf && p->cr == cr;
|
||||
}
|
||||
|
||||
// Display name for the Preset row: a built-in preset, a saved user preset, or
|
||||
// "Custom" if freq/sf/bw/cr don't exactly match any of them.
|
||||
const char* currentPresetName() {
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
if (!p) return "Custom";
|
||||
for (int i = 0; i < RADIO_PRESET_COUNT; i++) {
|
||||
const RadioPreset& r = RADIO_PRESETS[i];
|
||||
if (matchesPreset(p, r.freq, r.bw, r.sf, r.cr)) return r.name;
|
||||
}
|
||||
for (int i = 0; i < NodePrefs::USER_RADIO_PRESET_MAX; i++) {
|
||||
const auto& u = p->user_radio_presets[i];
|
||||
if (u.name[0] && matchesPreset(p, u.freq, u.bw, u.sf, u.cr)) return u.name;
|
||||
}
|
||||
return "Custom";
|
||||
}
|
||||
|
||||
// Position of the current settings within the popup list built by
|
||||
// openPresetMenu() below (built-ins first, then non-empty user slots in
|
||||
// slot order) — or -1 ("Custom") if nothing matches.
|
||||
int currentPresetListIndex() {
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
if (!p) return -1;
|
||||
for (int i = 0; i < RADIO_PRESET_COUNT; i++) {
|
||||
const RadioPreset& r = RADIO_PRESETS[i];
|
||||
if (matchesPreset(p, r.freq, r.bw, r.sf, r.cr)) return i;
|
||||
}
|
||||
int pos = RADIO_PRESET_COUNT;
|
||||
for (int i = 0; i < NodePrefs::USER_RADIO_PRESET_MAX; i++) {
|
||||
const auto& u = p->user_radio_presets[i];
|
||||
if (!u.name[0]) continue;
|
||||
if (matchesPreset(p, u.freq, u.bw, u.sf, u.cr)) return pos;
|
||||
pos++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Save the current freq/sf/bw/cr as a named user preset: overwrite a slot with
|
||||
// the same name if one exists, else the first empty slot, else slot 0 (oldest).
|
||||
void saveCurrentAsPreset(const char* name) {
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
if (!p || !name || !name[0]) return;
|
||||
int slot = -1;
|
||||
for (int i = 0; i < NodePrefs::USER_RADIO_PRESET_MAX; i++)
|
||||
if (strcmp(p->user_radio_presets[i].name, name) == 0) { slot = i; break; }
|
||||
if (slot < 0)
|
||||
for (int i = 0; i < NodePrefs::USER_RADIO_PRESET_MAX; i++)
|
||||
if (!p->user_radio_presets[i].name[0]) { slot = i; break; }
|
||||
if (slot < 0) slot = 0;
|
||||
NodePrefs::UserRadioPreset& u = p->user_radio_presets[slot];
|
||||
strncpy(u.name, name, sizeof(u.name) - 1);
|
||||
u.name[sizeof(u.name) - 1] = '\0';
|
||||
u.freq = p->freq; u.bw = p->bw; u.sf = p->sf; u.cr = p->cr;
|
||||
_dirty = true;
|
||||
_task->showAlert("Preset saved", 800);
|
||||
}
|
||||
|
||||
// Nearest entry in CUSTOM_BW_OPTS to the current bw (also used to step left/right).
|
||||
int customBwIndex() {
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
float bw = p ? p->bw : 62.5f;
|
||||
int best = 0;
|
||||
float best_diff = 1e9f;
|
||||
for (int i = 0; i < CUSTOM_BW_COUNT; i++) {
|
||||
float diff = fabsf(bw - CUSTOM_BW_OPTS[i]);
|
||||
if (diff < best_diff) { best_diff = diff; best = i; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
// Value column start, pulled left by the scrollbar gutter so right-side
|
||||
// values never render under the indicator when the list scrolls.
|
||||
int valCol(DisplayDriver& display) const { return display.valCol() - _reserve; }
|
||||
@@ -489,6 +570,39 @@ class SettingsScreen : public UIScreen {
|
||||
snprintf(buf, sizeof(buf),"%ddBm", p ? p->tx_power_dbm : 0);
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(buf);
|
||||
} else if (item == RADIO_PRESET) {
|
||||
display.print("Preset");
|
||||
const char* name = currentPresetName();
|
||||
int xc = valCol(display);
|
||||
display.drawTextEllipsized(xc, y, display.width() - xc - _reserve, name);
|
||||
} else if (item == CUSTOM_FREQ) {
|
||||
display.print("Freq");
|
||||
char buf[10];
|
||||
snprintf(buf, sizeof(buf), "%.3f", p ? p->freq : 0.0f);
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(buf);
|
||||
} else if (item == CUSTOM_SF) {
|
||||
display.print("SF");
|
||||
char buf[6];
|
||||
snprintf(buf, sizeof(buf), "%d", p ? (int)p->sf : 0);
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(buf);
|
||||
} else if (item == CUSTOM_BW) {
|
||||
display.print("BW");
|
||||
char buf[10];
|
||||
snprintf(buf, sizeof(buf), "%.1f", p ? p->bw : 0.0f);
|
||||
display.setCursor(valCol(display), y);
|
||||
display.print(buf);
|
||||
} else if (item == CUSTOM_CR) {
|
||||
display.print("CR");
|
||||
char buf[6];
|
||||
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 == POWER_SAVE) {
|
||||
display.print("Pwr save");
|
||||
display.setCursor(valCol(display), y);
|
||||
@@ -595,6 +709,32 @@ class SettingsScreen : public UIScreen {
|
||||
int _edit_slot; // -1 = not editing, 0..9 = slot being edited
|
||||
KeyboardWidget* _kb;
|
||||
|
||||
// Radio preset picker — names are too long for the value column, so Enter on
|
||||
// RADIO_PRESET opens this as a full-width scrollable list instead of cycling.
|
||||
PopupMenu _preset_menu;
|
||||
// Maps a user-preset row's position in the open popup back to its slot in
|
||||
// NodePrefs::user_radio_presets (empty slots are skipped when building the list).
|
||||
uint8_t _preset_user_slot[NodePrefs::USER_RADIO_PRESET_MAX];
|
||||
int _preset_user_count = 0;
|
||||
bool _preset_saving = false; // _kb is open to name a new preset, not a msg slot
|
||||
|
||||
void openPresetMenu() {
|
||||
NodePrefs* p = _task->getNodePrefs();
|
||||
_preset_menu.begin("Radio Preset", 6);
|
||||
for (int i = 0; i < RADIO_PRESET_COUNT; i++) _preset_menu.addItem(RADIO_PRESETS[i].name);
|
||||
_preset_user_count = 0;
|
||||
if (p) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
_preset_menu.addItem("+ Save current...");
|
||||
int idx = currentPresetListIndex();
|
||||
_preset_menu.setSelected(idx >= 0 ? idx : 0);
|
||||
}
|
||||
|
||||
public:
|
||||
SettingsScreen(UITask* task, KeyboardWidget* kb)
|
||||
: _task(task), _kb(kb), _selected(SECTION_DISPLAY), _scroll(0), _visible(4), _dirty(false), _edit_slot(-1) {
|
||||
@@ -613,7 +753,7 @@ public:
|
||||
int render(DisplayDriver& display) override {
|
||||
display.setTextSize(1);
|
||||
|
||||
if (_edit_slot >= 0) {
|
||||
if (_edit_slot >= 0 || _preset_saving) {
|
||||
return _kb->render(display);
|
||||
}
|
||||
|
||||
@@ -630,6 +770,8 @@ public:
|
||||
|
||||
drawScrollIndicator(display, start_y, _visible * item_h, _vis_count, _visible, _scroll);
|
||||
|
||||
if (_preset_menu.active) _preset_menu.render(display);
|
||||
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@@ -652,6 +794,43 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
// Keyboard editing mode for naming a new saved preset
|
||||
if (_preset_saving) {
|
||||
auto res = _kb->handleInput(c);
|
||||
if (res == KeyboardWidget::DONE) {
|
||||
saveCurrentAsPreset(_kb->buf);
|
||||
_preset_saving = false;
|
||||
} else if (res == KeyboardWidget::CANCELLED) {
|
||||
_preset_saving = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Radio preset popup
|
||||
if (_preset_menu.active) {
|
||||
auto res = _preset_menu.handleInput(c);
|
||||
if (res == PopupMenu::SELECTED && p) {
|
||||
int idx = _preset_menu.selectedIndex();
|
||||
int user_base = RADIO_PRESET_COUNT;
|
||||
int save_idx = RADIO_PRESET_COUNT + _preset_user_count;
|
||||
if (idx >= 0 && idx < user_base) {
|
||||
const RadioPreset& r = RADIO_PRESETS[idx];
|
||||
p->freq = r.freq; p->bw = r.bw; p->sf = r.sf; p->cr = r.cr;
|
||||
_task->applyRadioParams();
|
||||
_dirty = true;
|
||||
} else if (idx >= user_base && idx < save_idx) {
|
||||
const NodePrefs::UserRadioPreset& u = p->user_radio_presets[_preset_user_slot[idx - user_base]];
|
||||
p->freq = u.freq; p->bw = u.bw; p->sf = u.sf; p->cr = u.cr;
|
||||
_task->applyRadioParams();
|
||||
_dirty = true;
|
||||
} else if (idx == save_idx) {
|
||||
_preset_saving = true;
|
||||
_kb->begin("", (int)sizeof(p->user_radio_presets[0].name) - 1);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (c == KEY_UP && _vis_count > 0) {
|
||||
int vi = visIndexOf(_selected);
|
||||
vi = (vi > 0) ? vi - 1 : _vis_count - 1;
|
||||
@@ -743,6 +922,37 @@ public:
|
||||
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 (_selected == RADIO_PRESET && p && enter) {
|
||||
openPresetMenu();
|
||||
return true;
|
||||
}
|
||||
// Manual fine-tune of the active radio params. Freq bounds come from the
|
||||
// radio driver itself (RadioLib's own validated range for this chip) so the
|
||||
// field can never be nudged to a value setFrequency() would silently reject.
|
||||
if (_selected == CUSTOM_FREQ && p) {
|
||||
float min_mhz, max_mhz;
|
||||
radio_driver.getFreqBounds(min_mhz, max_mhz);
|
||||
if (right && p->freq < max_mhz) { p->freq += 0.025f; _task->applyRadioParams(); _dirty = true; return true; }
|
||||
if (left && p->freq > min_mhz) { p->freq -= 0.025f; _task->applyRadioParams(); _dirty = true; return true; }
|
||||
}
|
||||
if (_selected == CUSTOM_SF && p) {
|
||||
if (right && p->sf < 12) { p->sf++; _task->applyRadioParams(); _dirty = true; return true; }
|
||||
if (left && p->sf > 5) { p->sf--; _task->applyRadioParams(); _dirty = true; return true; }
|
||||
}
|
||||
if (_selected == CUSTOM_BW && p) {
|
||||
int idx = customBwIndex();
|
||||
if (right && idx < CUSTOM_BW_COUNT - 1) { p->bw = CUSTOM_BW_OPTS[idx + 1]; _task->applyRadioParams(); _dirty = true; return true; }
|
||||
if (left && idx > 0) { p->bw = CUSTOM_BW_OPTS[idx - 1]; _task->applyRadioParams(); _dirty = true; return true; }
|
||||
}
|
||||
if (_selected == CUSTOM_CR && p) {
|
||||
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;
|
||||
_dirty = true;
|
||||
return true;
|
||||
}
|
||||
if (_selected == POWER_SAVE && p && (left || right || enter)) {
|
||||
p->rx_powersave ^= 1;
|
||||
_task->applyPowerSave();
|
||||
@@ -882,3 +1092,4 @@ const char* SettingsScreen::AD_SCOPE_LABELS[2] = { "All", "Zero-hop" };
|
||||
#if FEAT_FULL_REFRESH_SETTING
|
||||
const char* SettingsScreen::EINK_FULL_REFRESH_LABELS[5] = { "off", "5", "10", "20", "30" };
|
||||
#endif
|
||||
const float SettingsScreen::CUSTOM_BW_OPTS[10] = { 7.8f, 10.4f, 15.6f, 20.8f, 31.25f, 41.7f, 62.5f, 125.0f, 250.0f, 500.0f };
|
||||
|
||||
@@ -2093,6 +2093,11 @@ void UITask::applyApc() {
|
||||
the_mesh.applyApc(); // (re)initialise Adaptive Power Control from prefs
|
||||
}
|
||||
|
||||
void UITask::applyRadioParams() {
|
||||
if (_node_prefs == NULL) return;
|
||||
radio_driver.setParams(_node_prefs->freq, _node_prefs->bw, _node_prefs->sf, _node_prefs->cr);
|
||||
}
|
||||
|
||||
void UITask::applyBrightness() {
|
||||
if (_display != NULL && _node_prefs != NULL) {
|
||||
_display->setBrightness(_node_prefs->display_brightness);
|
||||
|
||||
@@ -258,6 +258,7 @@ public:
|
||||
void applyTxPower();
|
||||
void applyPowerSave(); // hardware duty-cycle RX on/off from prefs
|
||||
void applyApc(); // Adaptive Power Control on/off from prefs
|
||||
void applyRadioParams(); // freq/bw/sf/cr from prefs (radio preset change)
|
||||
void applyFont();
|
||||
void applyRotation();
|
||||
void applyFullRefreshInterval();
|
||||
|
||||
Reference in New Issue
Block a user