#pragma once // Tools › Repeater — consolidates the repeater toggle, its flood forwarding // filters, and an optional dedicated radio profile 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. Live forwarding stats // live separately on Tools › Diagnostics. // // Network modes: // - Current — repeat on the companion's current frequency. Opt-in only: // relaying on whatever network the operator is chatting on // isn't the MeshCore community norm. // - Custom — enabling the repeater switches the radio to a dedicated profile // (preset or manual), and disabling restores the companion params. // Default for a never-configured device, seeded with a frequency // in the same band as the companion's own network (so the default // can't land outside what's legal for the operator's region). // Included by UITask.cpp. #include #include #include "icons.h" #include "RadioParamsEditor.h" #include "RadioPresetPicker.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; RadioPresetPicker _picker; RadioParamsEditor _editor; // The dedicated repeater profile's fields, as the shared preset picker's target // (Settings › Radio points the same picker at the companion's own params). RadioPresetPicker::Target rptTarget(NodePrefs* p) const { return { &p->repeater_freq, &p->repeater_bw, &p->repeater_sf, &p->repeater_cr }; } 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 ""; } 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, _picker.currentName(p, rptTarget(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; } public: RepeaterScreen(UITask* task) : _task(task), _dirty(false), _sel(0), _scroll(0), _item_count(1) {} void onShow() override { _dirty = false; _sel = 0; _scroll = 0; _picker.menu.active = false; _editor.freq.active = false; _picker.saving = false; _picker.deleting = false; } int render(DisplayDriver& display) override { if (_picker.saving) return _task->keyboard().render(display); NodePrefs* p = _task->getNodePrefs(); buildItems(p); display.setTextSize(1); display.setColor(DisplayDriver::LIGHT); display.drawCenteredHeader("REPEATER"); // Config only — live forwarding stats live on Tools › Diagnostics. drawList(display, _item_count, _sel, _scroll, [&](int row, int y, bool sel, int reserve) { int item = _items[row]; drawRowSelection(display, y, sel, reserve); display.setCursor(2, y); display.print(itemLabel(item)); if (item == IT_RFREQ && sel && _editor.active()) { _editor.render(display, display.valCol(), y); } else { char val[16]; itemValue(item, p, val, sizeof(val)); display.drawTextRightAlign(display.width() - reserve - 2, y, val); } display.setColor(DisplayDriver::LIGHT); }); display.setColor(DisplayDriver::LIGHT); if (_picker.menu.active) _picker.menu.render(display); return (_picker.menu.active || _editor.active()) ? 50 : 500; } bool handleInput(char c) override { NodePrefs* p = _task->getNodePrefs(); // Keyboard editing mode for naming a new saved preset if (_picker.saving) { auto res = _task->keyboard().handleInput(c); if (res == KeyboardWidget::DONE) { if (_picker.save(p, _task->keyboard().buf, rptTarget(p))) { _dirty = true; _task->showAlert("Preset saved", 800); } _picker.saving = false; } else if (res == KeyboardWidget::CANCELLED) { _picker.saving = false; } return true; } // Modal overlays first. if (_picker.menu.active) { auto res = _picker.menu.handleInput(c); if (res == PopupMenu::SELECTED && p) { switch (_picker.onSelected(_picker.menu.selectedIndex(), p, rptTarget(p))) { case RadioPresetPicker::START_SAVE: _task->keyboard().begin("", (int)sizeof(p->user_radio_presets[0].name) - 1); break; case RadioPresetPicker::APPLIED: the_mesh.applyRepeaterRadio(); // live if currently relaying on the profile _dirty = true; break; case RadioPresetPicker::DELETED: _dirty = true; _task->showAlert("Preset deleted", 800); break; case RadioPresetPicker::NONE: break; } } else if (res == PopupMenu::CANCELLED) { _picker.deleting = false; } return true; } if (_editor.active()) { if (_editor.handleFreqInput(c) && p) { the_mesh.applyRepeaterRadio(); _dirty = true; } return true; } if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) { _task->savePrefsIfDirty(_dirty); _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 _task->applyPowerSave(); // duty-cycle RX is forced off while repeating _task->applyApc(); // pin TX power to the ceiling while repeating (APC suppressed) 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) { _picker.open(p, rptTarget(p), "Repeater Preset"); return true; } if (item == IT_RFREQ && enter) { float lo, hi; radio_driver.getFreqBounds(lo, hi); _editor.beginFreq(p->repeater_freq, lo, hi); return true; } int dir = right ? 1 : (left ? -1 : 0); if (item == IT_RSF && dir && RadioParamsEditor::stepSF(p->repeater_sf, dir)) { the_mesh.applyRepeaterRadio(); _dirty = true; return true; } if (item == IT_RBW && dir && RadioParamsEditor::stepBW(p->repeater_bw, dir)) { the_mesh.applyRepeaterRadio(); _dirty = true; return true; } if (item == IT_RCR && dir && RadioParamsEditor::stepCR(p->repeater_cr, dir)) { 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; } };