From 056a786fc85ee5f45212407fc05c65a572c63f8f Mon Sep 17 00:00:00 2001 From: Jakub <106778416+MarekZegare4@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:23:30 +0200 Subject: [PATCH] feat(companion): repeater mode, radio presets, and manual RF tuning in Settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- examples/companion_radio/DataStore.cpp | 5 + examples/companion_radio/MyMesh.cpp | 38 +++- examples/companion_radio/MyMesh.h | 1 + examples/companion_radio/NodePrefs.h | 14 +- examples/companion_radio/RadioPresets.h | 35 +++ examples/companion_radio/ui-new/PopupMenu.h | 2 +- .../companion_radio/ui-new/SettingsScreen.h | 213 +++++++++++++++++- examples/companion_radio/ui-new/UITask.cpp | 5 + examples/companion_radio/ui-new/UITask.h | 1 + src/helpers/radiolib/CustomSX1262Wrapper.h | 3 + src/helpers/radiolib/RadioLibWrappers.h | 7 + 11 files changed, 319 insertions(+), 5 deletions(-) create mode 100644 examples/companion_radio/RadioPresets.h diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index b5c46f96..6bc3a69d 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -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; diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 3c588a33..bbd2ae19 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -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; diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 6ee4a520..ee98d86a 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -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; diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index a036a9e2..562151f7 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -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 diff --git a/examples/companion_radio/RadioPresets.h b/examples/companion_radio/RadioPresets.h new file mode 100644 index 00000000..2be4fa9d --- /dev/null +++ b/examples/companion_radio/RadioPresets.h @@ -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]); diff --git a/examples/companion_radio/ui-new/PopupMenu.h b/examples/companion_radio/ui-new/PopupMenu.h index 48d248a4..764c1cc7 100644 --- a/examples/companion_radio/ui-new/PopupMenu.h +++ b/examples/companion_radio/ui-new/PopupMenu.h @@ -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; diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 1c7217e2..7d0cf491 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -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 }; diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 5395b34c..327c434a 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -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); diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 3f92190c..58a892fd 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -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(); diff --git a/src/helpers/radiolib/CustomSX1262Wrapper.h b/src/helpers/radiolib/CustomSX1262Wrapper.h index bd1fe279..03f93852 100644 --- a/src/helpers/radiolib/CustomSX1262Wrapper.h +++ b/src/helpers/radiolib/CustomSX1262Wrapper.h @@ -20,6 +20,9 @@ public: updatePreamble(sf); } + // From RadioLib's SX1262::setFrequency(): RADIOLIB_CHECK_RANGE(freq, 150.0f, 960.0f, ...). + void getFreqBounds(float& min_mhz, float& max_mhz) const override { min_mhz = 150.0f; max_mhz = 960.0f; } + bool isReceivingPacket() override { return ((CustomSX1262 *)_radio)->isReceiving(); } diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index d5b8b434..ef762255 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -56,6 +56,13 @@ public: } virtual void setParams(float freq, float bw, uint8_t sf, uint8_t cr) = 0; + // RadioLib's own setFrequency() silently rejects values outside the chip's + // validated range and leaves the radio retuned to its previous frequency — + // setParams() above doesn't check that return code, so the UI clamps to this + // instead of letting NodePrefs drift out of sync with the actual radio. + // Default is the generic sanity bound the app's CMD_SET_RADIO_PARAMS already + // uses; chips with a narrower RadioLib-validated range override it. + virtual void getFreqBounds(float& min_mhz, float& max_mhz) const { min_mhz = 150.0f; max_mhz = 2500.0f; } uint32_t getRngSeed(); void setTxPower(int8_t dbm); int8_t getTxPower() const { return _tx_dbm; } // actual current power (reflects APC)