diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index e78636ca..97d11075 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -364,15 +364,8 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no // norm; that stays opt-in. Band-matched rather than a flat frequency so the // default can't land outside what's legal where the companion is set up. 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 = 1; - _prefs.repeater_freq = defaultRepeaterFreqForBand(_prefs.freq); - _prefs.repeater_bw = LORA_BW; - _prefs.repeater_sf = LORA_SF; - _prefs.repeater_cr = LORA_CR; + if (!isValidRepeaterProfile(_prefs.repeater_freq, _prefs.repeater_bw, _prefs.repeater_sf, _prefs.repeater_cr)) { + seedDefaultRepeaterProfile(_prefs); } // → 0xC0DE000B: append bot_commands_enabled + quiet-hours. Older files leave // stray bytes here; clamp so upgraders fall back to off / no quiet hours. diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 3aaa07aa..a59a1095 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -1309,12 +1309,8 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.tx_power_dbm = LORA_TX_POWER; // Repeater profile default for a true first boot (no prefs file yet, so // loadPrefs() below is a no-op) — same band-matched seed as the upgrade - // path in DataStore.cpp, kept in sync rather than left at memset's 0/Current. - _prefs.repeater_use_profile = 1; - _prefs.repeater_freq = defaultRepeaterFreqForBand(_prefs.freq); - _prefs.repeater_bw = LORA_BW; - _prefs.repeater_sf = LORA_SF; - _prefs.repeater_cr = LORA_CR; + // path in DataStore.cpp. + seedDefaultRepeaterProfile(_prefs); _prefs.gps_enabled = 0; // GPS disabled by default _prefs.gps_interval = 0; // No automatic GPS updates by default _prefs.display_brightness = 2; // medium brightness by default @@ -1868,7 +1864,7 @@ void MyMesh::handleCmdFrame(size_t len) { _prefs.client_repeat = repeat; savePrefs(); - radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + applyRepeaterRadio(); // companion params, or the repeater profile if relaying with one set // Keep the "repeating ⇒ continuous RX, full TX power" invariants when repeat // is toggled via the app, mirroring the on-device path (a repeater must hear // all traffic and relay at consistent power). diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 13edb0f5..de6849eb 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -224,10 +224,7 @@ public: // 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; + return isValidRepeaterProfile(_prefs.repeater_freq, _prefs.repeater_bw, _prefs.repeater_sf, _prefs.repeater_cr); } // Load the radio with the correct params for the current mode: the repeater // profile when relaying with a valid dedicated profile, otherwise the diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index a44e1ef5..3007d8af 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -291,3 +291,23 @@ struct NodePrefs { // persisted to file if (pos < size) buf[pos] = '\0'; } }; + +// Bounds for a usable repeater radio profile — must match the radio chip's own +// validated range (see CustomSX1262Wrapper::getFreqBounds(), 150-960 MHz), so +// a profile that passes here is guaranteed to actually take effect on the radio. +static inline bool isValidRepeaterProfile(float freq, float bw, uint8_t sf, uint8_t cr) { + return freq >= 150.0f && freq <= 960.0f + && sf >= 5 && sf <= 12 + && cr >= 5 && cr <= 8 + && bw >= 7.0f && bw <= 510.0f; +} + +// Seed a never-configured repeater profile in the same band as the companion's +// own network (see defaultRepeaterFreqForBand() above for why). +static inline void seedDefaultRepeaterProfile(NodePrefs& prefs) { + prefs.repeater_use_profile = 1; + prefs.repeater_freq = defaultRepeaterFreqForBand(prefs.freq); + prefs.repeater_bw = LORA_BW; + prefs.repeater_sf = LORA_SF; + prefs.repeater_cr = LORA_CR; +} diff --git a/examples/companion_radio/RadioPresets.h b/examples/companion_radio/RadioPresets.h index 2be4fa9d..3446b7b8 100644 --- a/examples/companion_radio/RadioPresets.h +++ b/examples/companion_radio/RadioPresets.h @@ -1,4 +1,5 @@ #pragma once +#include // 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. @@ -33,3 +34,17 @@ static const RadioPreset RADIO_PRESETS[] = { { "Vietnam (Narrow)", 920.250f, 62.5f, 8, 5 }, }; static const int RADIO_PRESET_COUNT = sizeof(RADIO_PRESETS) / sizeof(RADIO_PRESETS[0]); + +// Standard SX126x/SX127x LoRa bandwidths (kHz) — manual BW fields (companion and +// repeater profile alike) cycle through these rather than a free-form value, so +// they can't land on an unsupported setting. +static const float LORA_BW_OPTS[] = { 7.8f, 10.4f, 15.6f, 20.8f, 31.25f, 41.7f, 62.5f, 125.0f, 250.0f, 500.0f }; +static const int LORA_BW_OPT_COUNT = sizeof(LORA_BW_OPTS) / sizeof(LORA_BW_OPTS[0]); + +// True if (freq, bw, sf, cr) matches the radio's currently-applied (cur_*) params. +// Floats are compared with a small epsilon since they may have round-tripped +// through unit conversions (e.g. kHz integers from the companion app). +static inline bool radioParamsMatchPreset(float cur_freq, float cur_bw, uint8_t cur_sf, uint8_t cur_cr, + float freq, float bw, uint8_t sf, uint8_t cr) { + return fabsf(cur_freq - freq) < 0.001f && fabsf(cur_bw - bw) < 0.001f && cur_sf == sf && cur_cr == cr; +} diff --git a/examples/companion_radio/ui-new/RepeaterScreen.h b/examples/companion_radio/ui-new/RepeaterScreen.h index 518c16a1..98356e7f 100644 --- a/examples/companion_radio/ui-new/RepeaterScreen.h +++ b/examples/companion_radio/ui-new/RepeaterScreen.h @@ -45,15 +45,15 @@ class RepeaterScreen : public UIScreen { 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; - } + // Nearest entry in LORA_BW_OPTS to the repeater profile's bw. 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 + int best = 0; + float best_diff = 1e9f; + for (int i = 0; i < LORA_BW_OPT_COUNT; i++) { + float diff = fabsf(p->repeater_bw - LORA_BW_OPTS[i]); + if (diff < best_diff) { best_diff = diff; best = i; } + } + return best; } void buildItems(NodePrefs* p) { @@ -100,13 +100,13 @@ class RepeaterScreen : public UIScreen { 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) + if (radioParamsMatchPreset(p->repeater_freq, p->repeater_bw, p->repeater_sf, p->repeater_cr, r.freq, r.bw, r.sf, r.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) + if (radioParamsMatchPreset(p->repeater_freq, p->repeater_bw, p->repeater_sf, p->repeater_cr, u.freq, u.bw, u.sf, u.cr)) return u.name; } return "Custom"; @@ -196,7 +196,7 @@ public: display.setCursor(2, y); display.print(itemLabel(item)); if (item == IT_RFREQ && sel && _freq_editor.active) { - _freq_editor.render(display, valCol(display), y); + _freq_editor.render(display, display.valCol(), y); } else { itemValue(item, p, val, sizeof(val)); display.drawTextRightAlign(display.width() - reserve - 2, y, val); @@ -209,9 +209,6 @@ public: return (_preset_menu.active || _freq_editor.active) ? 50 : 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(); @@ -289,9 +286,9 @@ public: 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; } + int idx = rptBwIndex(p); + if (right && idx < LORA_BW_OPT_COUNT - 1) { p->repeater_bw = LORA_BW_OPTS[idx + 1]; the_mesh.applyRepeaterRadio(); _dirty = true; return true; } + if (left && idx > 0) { p->repeater_bw = LORA_BW_OPTS[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; } diff --git a/examples/companion_radio/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index 6e3ef985..c84265f4 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -102,10 +102,6 @@ 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; @@ -120,7 +116,7 @@ class SettingsScreen : public UIScreen { } 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; + return radioParamsMatchPreset(p->freq, p->bw, p->sf, p->cr, freq, bw, sf, cr); } // Display name for the Preset row: a built-in preset, a saved user preset, or @@ -179,14 +175,14 @@ class SettingsScreen : public UIScreen { _task->showAlert("Preset saved", 800); } - // Nearest entry in CUSTOM_BW_OPTS to the current bw (also used to step left/right). + // Nearest entry in LORA_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]); + for (int i = 0; i < LORA_BW_OPT_COUNT; i++) { + float diff = fabsf(bw - LORA_BW_OPTS[i]); if (diff < best_diff) { best_diff = diff; best = i; } } return best; @@ -994,8 +990,8 @@ public: } 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 (right && idx < LORA_BW_OPT_COUNT - 1) { p->bw = LORA_BW_OPTS[idx + 1]; _task->applyRadioParams(); _dirty = true; return true; } + if (left && idx > 0) { p->bw = LORA_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; } @@ -1142,4 +1138,3 @@ 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 6804efe9..9f4821f6 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -2125,7 +2125,7 @@ void UITask::applyApc() { void UITask::applyRadioParams() { if (_node_prefs == NULL) return; - radio_driver.setParams(_node_prefs->freq, _node_prefs->bw, _node_prefs->sf, _node_prefs->cr); + the_mesh.applyRepeaterRadio(); // companion params, or the repeater profile if relaying with one set } void UITask::applyBrightness() { diff --git a/src/Dispatcher.cpp b/src/Dispatcher.cpp index 59b8ca55..c52c9259 100644 --- a/src/Dispatcher.cpp +++ b/src/Dispatcher.cpp @@ -272,7 +272,7 @@ void Dispatcher::suppressQueuedDuplicate(Packet* pkt) { int n = _mgr->getOutboundTotal(); for (int i = 0; i < n; i++) { Packet* q = _mgr->getOutboundByIdx(i); - if (q == NULL) continue; + if (q == NULL || !q->isRouteFlood()) continue; // only ever suppress a queued flood retransmit uint8_t qh[MAX_HASH_SIZE]; q->calculatePacketHash(qh); if (memcmp(h, qh, MAX_HASH_SIZE) == 0) {