diff --git a/examples/companion_radio/DataStore.cpp b/examples/companion_radio/DataStore.cpp index 6bc3a69d..85a5c6f7 100644 --- a/examples/companion_radio/DataStore.cpp +++ b/examples/companion_radio/DataStore.cpp @@ -210,6 +210,10 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no #ifdef DISPLAY_ROTATION _prefs.display_rotation = DISPLAY_ROTATION; #endif + // 0 is a valid SNR threshold, so the "off" state needs its own sentinel set + // before reading — an older file lacking this field must read as disabled, + // not as "filter everything below 0 dB". + _prefs.repeat_min_snr = NodePrefs::REPEAT_SNR_DISABLED; File file = openRead(_fs, filename); if (!file) return; @@ -332,6 +336,19 @@ void DataStore::loadPrefsInt(const char *filename, NodePrefs& _prefs, double& no 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)); + // → 0xC0DE000E: repeater politeness knobs. On a pre-E file the bytes here are + // that file's own sentinel tail, so clamp every out-of-range value back to its + // "off" default (same stray-byte handling as the fields below). + rd(&_prefs.repeat_skip_adverts, sizeof(_prefs.repeat_skip_adverts)); + rd(&_prefs.repeat_max_hops, sizeof(_prefs.repeat_max_hops)); + rd(&_prefs.repeat_delay_boost, sizeof(_prefs.repeat_delay_boost)); + rd(&_prefs.repeat_min_snr, sizeof(_prefs.repeat_min_snr)); + if (_prefs.repeat_skip_adverts > 1) _prefs.repeat_skip_adverts = 0; + 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; // → 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; @@ -489,6 +506,10 @@ void DataStore::savePrefs(const NodePrefs& _prefs, double node_lat, double node_ 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)); + file.write((uint8_t *)&_prefs.repeat_skip_adverts, sizeof(_prefs.repeat_skip_adverts)); + file.write((uint8_t *)&_prefs.repeat_max_hops, sizeof(_prefs.repeat_max_hops)); + 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)); // 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 8e861c85..7a1f7f37 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -282,7 +282,12 @@ int MyMesh::calcRxDelay(float score, uint32_t air_time) const { uint32_t MyMesh::getRetransmitDelay(const mesh::Packet *packet) { uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.5f); - return getRNG()->nextInt(0, 5*t + 1); + uint32_t d = getRNG()->nextInt(0, 5*t + 1); + // Politeness yield (Settings > Radio): scale the flood retransmit delay so a + // mobile companion waits longer and lets better-sited fixed repeaters win the + // flood first. Only forwarded floods reach here — own sends pass their own + // delay to sendFlood() — so this never slows the companion's own traffic. + return d * (1 + _prefs.repeat_delay_boost); } uint32_t MyMesh::getDirectRetransmitDelay(const mesh::Packet *packet) { uint32_t t = (_radio->getEstAirtimeFor(packet->getPathByteLen() + packet->payload_len + 2) * 0.2f); @@ -551,7 +556,15 @@ bool MyMesh::isRepeatLooped(const mesh::Packet* packet) const { bool MyMesh::allowPacketForward(const mesh::Packet* packet) { if (_prefs.client_repeat == 0) return false; + // "Politeness" filters (Settings > Radio) — all default off, so a plain repeater + // is unaffected. Flood-only by design: on a direct route this node is the named + // next hop, so dropping there would kill delivery with no alternate path, while + // dropping a flood copy just trims redundancy other nodes still carry. if (packet->isRouteFlood()) { + if (_prefs.repeat_min_snr != NodePrefs::REPEAT_SNR_DISABLED + && packet->getSNR() < (float)_prefs.repeat_min_snr) return false; + if (_prefs.repeat_skip_adverts && packet->getPayloadType() == PAYLOAD_TYPE_ADVERT) return false; + if (_prefs.repeat_max_hops > 0 && packet->getPathHashCount() >= _prefs.repeat_max_hops) return false; if (packet->getPayloadType() == PAYLOAD_TYPE_ADVERT && packet->getPathHashCount() >= REPEAT_MAX_ADVERT_HOPS) return false; if (isRepeatLooped(packet)) return false; } diff --git a/examples/companion_radio/NodePrefs.h b/examples/companion_radio/NodePrefs.h index 562151f7..afc39671 100644 --- a/examples/companion_radio/NodePrefs.h +++ b/examples/companion_radio/NodePrefs.h @@ -152,11 +152,29 @@ struct NodePrefs { // persisted to file static const uint8_t USER_RADIO_PRESET_MAX = 4; UserRadioPreset user_radio_presets[USER_RADIO_PRESET_MAX]; + // Repeater "politeness" — only consulted when client_repeat is on, via + // MyMesh::allowPacketForward(). Both default to off (0) so behaviour is + // unchanged until the user opts in (Settings > Radio). + // repeat_skip_adverts: 1 = don't re-flood ADVERT packets (the highest-volume + // flood traffic); messages/acks still relay. + // repeat_max_hops: drop a flood packet once it has already travelled this + // many hops. 0 = no hop limit. + // repeat_delay_boost: extra retransmit-delay multiplier for FORWARDED floods + // only (own sends are unaffected) — a mobile companion yields to better-sited + // fixed repeaters. Effective delay = base * (1 + repeat_delay_boost). 0 = off. + // repeat_min_snr: drop a flood packet received below this SNR (dB), so marginal + // fringe traffic isn't re-flooded. REPEAT_SNR_DISABLED (-128) = off. + uint8_t repeat_skip_adverts; + uint8_t repeat_max_hops; + uint8_t repeat_delay_boost; + int8_t repeat_min_snr; + static const int8_t REPEAT_SNR_DISABLED = -128; + // 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 = 0xC0DE000D; + static const uint32_t SCHEMA_SENTINEL = 0xC0DE000E; // 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/ui-new/SettingsScreen.h b/examples/companion_radio/ui-new/SettingsScreen.h index e7e34602..f963c988 100644 --- a/examples/companion_radio/ui-new/SettingsScreen.h +++ b/examples/companion_radio/ui-new/SettingsScreen.h @@ -59,6 +59,10 @@ class SettingsScreen : public UIScreen { 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, POWER_SAVE, TX_APC, // System section @@ -274,11 +278,21 @@ 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; + } + bool isHomePage(int item) const { return item == HOME_CLOCK || item == HOME_RECENT || item == HOME_RADIO || item == HOME_BT || item == HOME_ADVERT || item == HOME_TOOLS || @@ -609,6 +623,31 @@ class SettingsScreen : public UIScreen { 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 == POWER_SAVE) { display.print("Pwr save"); display.setCursor(valCol(display), y); @@ -1004,9 +1043,36 @@ public: } 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 == POWER_SAVE && p && (left || right || enter)) { p->rx_powersave ^= 1; _task->applyPowerSave();