From 3c10296b90e5fb7e38127ada56a295f1b06ba132 Mon Sep 17 00:00:00 2001 From: Roman Terekhov Date: Thu, 8 Oct 2026 19:31:28 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B9?= =?UTF-8?q?=D0=BA=D0=B0=20=D1=80=D0=B0=D0=B4=D0=B8=D0=BE=20=D1=81=20=D1=8D?= =?UTF-8?q?=D0=BA=D1=80=D0=B0=D0=BD=D0=B0=20RADIO:=20freq/bw/sf/cr/tx,=20?= =?UTF-8?q?=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D0=B8=20=D0=B0=D0=B2=D1=82=D0=BE=D0=B2=D1=8B=D1=85=D0=BE=D0=B4?= =?UTF-8?q?=20=D0=B8=D0=B7=20=D0=BC=D0=B5=D0=BD=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- examples/companion_radio/MyMesh.cpp | 26 +++ examples/companion_radio/MyMesh.h | 6 + examples/companion_radio/ui-new/UITask.cpp | 255 ++++++++++++++++++++- examples/companion_radio/ui-new/UITask.h | 5 + src/helpers/radiolib/RadioLibWrappers.h | 5 + src/helpers/ui/UIScreen.h | 5 + 6 files changed, 298 insertions(+), 4 deletions(-) diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 2c334066..fc91b2ad 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -992,6 +992,32 @@ uint32_t MyMesh::getBLEPin() { return _active_ble_pin; } +bool MyMesh::setRadioParams(float freq, float bw, uint8_t sf, uint8_t cr) { + if (freq < 150.0f || freq > 2500.0f) return false; + if (bw < 7.0f || bw > 500.0f) return false; + if (sf < 5 || sf > 12) return false; + if (cr < 5 || cr > 8) return false; + if (_prefs.isRepeatEn() && !isValidClientRepeatFreq((uint32_t)(freq * 1000.0f + 0.5f))) return false; + + _prefs.freq = freq; + _prefs.bw = bw; + _prefs.sf = sf; + _prefs.cr = cr; + + radio_driver.setParams(_prefs.freq, _prefs.bw, _prefs.sf, _prefs.cr); + // Сеттеры RadioLib уводят чип в standby, а враппер при этом может считать, что + // он всё ещё в RX (state не меняется) — перезапускаем приём с новыми параметрами. + radio_driver.restartRecv(); + return true; +} + +void MyMesh::setTxPowerDbm(int8_t dbm) { + if (dbm < -9) dbm = -9; + if (dbm > MAX_LORA_TX_POWER) dbm = MAX_LORA_TX_POWER; + _prefs.tx_power_dbm = dbm; + radio_driver.setTxPower(_prefs.tx_power_dbm); +} + struct FreqRange { uint32_t lower_freq, upper_freq; }; diff --git a/examples/companion_radio/MyMesh.h b/examples/companion_radio/MyMesh.h index 238adada..54e2ff2d 100644 --- a/examples/companion_radio/MyMesh.h +++ b/examples/companion_radio/MyMesh.h @@ -171,6 +171,12 @@ public: _store->savePrefs(_prefs); } + // Ручная настройка радио (экран RADIO в ui-new). Проверяет допустимые + // диапазоны, применяет параметры к радио и обновляет prefs в памяти; + // запись в файл — только через savePrefs(). + bool setRadioParams(float freq, float bw, uint8_t sf, uint8_t cr); + void setTxPowerDbm(int8_t dbm); + #if ENV_INCLUDE_GPS == 1 void applyGpsPrefs() { sensors.setSettingValue("gps", _prefs.gps_enabled ? "1" : "0"); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index 540b6ab3..4bca679d 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -30,6 +30,25 @@ #endif #include "icons.h" +#include + +// --- Ручная настройка радио (страница RADIO) --- +// Границы частоты по умолчанию — для SX126x (150..960 МГц). Для других модулей +// можно переопределить в env варианта. +#ifndef UI_RADIO_FREQ_MIN + #define UI_RADIO_FREQ_MIN 150.0f +#endif +#ifndef UI_RADIO_FREQ_MAX + #define UI_RADIO_FREQ_MAX 960.0f +#endif +#define UI_RADIO_FREQ_STEP_KHZ 25 // шаг перестройки частоты +#define UI_RADIO_MENU_TIMEOUT_MILLIS 300000UL // 5 минут простоя — выйти из меню без сохранения + +// BW перебирается по списку допустимых значений, а не шагом +static const float UI_RADIO_BW_STEPS[] = { + 7.8f, 10.4f, 15.6f, 20.8f, 31.25f, 41.7f, 62.5f, 125.0f, 250.0f, 500.0f +}; +#define UI_RADIO_BW_STEPS_N (sizeof(UI_RADIO_BW_STEPS) / sizeof(UI_RADIO_BW_STEPS[0])) class SplashScreen : public UIScreen { UITask* _task; @@ -114,6 +133,35 @@ class HomeScreen : public UIScreen { bool _shutdown_init; AdvertPath recent[UI_RECENT_LIST_SIZE]; + // режим ручной настройки радио (страница RADIO) + enum RadioRow { ROW_FQ, ROW_BW, ROW_SF, ROW_CR, ROW_TX, ROW_SAVE, ROW_COUNT }; + bool _radio_edit; + bool _radio_val_edit; + uint8_t _radio_sel; + unsigned long _radio_last_activity; // для автовыхода из меню по простою + + void radioEditEnter() { + _task->clearAlert(); // висящий попап (например, от входящего сообщения) не должен перекрывать меню + _radio_edit = true; + _radio_val_edit = false; + _radio_sel = ROW_FQ; + _radio_last_activity = millis(); + } + void radioEditLeave(bool save) { + _radio_edit = false; + _radio_val_edit = false; + if (save) { + the_mesh.savePrefs(); + _task->showAlert("Saved", 1000); + } + _task->showPendingMessages(); // сообщения, пришедшие во время настройки + } + void radioRowStep(int8_t dir) { _radio_sel = (_radio_sel + ROW_COUNT + dir) % ROW_COUNT; } + void radioValueStep(int8_t dir); + void radioRowValue(uint8_t row, char* buf, size_t len); + const char* radioRowName(uint8_t row); + void renderRadioEditor(DisplayDriver& display); + void renderBatteryIndicator(DisplayDriver& display, uint16_t batteryMilliVolts) { // Convert millivolts to percentage @@ -185,15 +233,33 @@ class HomeScreen : public UIScreen { public: HomeScreen(UITask* task, mesh::RTCClock* rtc, SensorManager* sensors, NodePrefs* node_prefs) : _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0), - _shutdown_init(false), sensors_lpp(200) { } + _shutdown_init(false), _radio_edit(false), _radio_val_edit(false), _radio_sel(0), + _radio_last_activity(0), sensors_lpp(200) { } + + // в меню настройки радио входящие сообщения не всплывают поверх экрана + bool suppressAlerts() override { return _radio_edit; } void poll() override { + if (_radio_edit) { + if (millis() - _radio_last_activity >= UI_RADIO_MENU_TIMEOUT_MILLIS) { + radioEditLeave(false); // 5 минут без действий: выходим без сохранения + _task->blankDisplay(); // и гасим экран + } else { + _task->keepDisplayOn(); // пока работаем — экран не гасим по таймауту + } + } if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released _task->shutdown(); } } int render(DisplayDriver& display) override { + if (_radio_edit) { + // режим настройки радио: без статус-бара (имя ноды / заряд) и точек страниц — + // всё место отдано параметрам + renderRadioEditor(display); + return 5000; + } display.setColor(UIColor::title_bkg); display.fillRect(0, 0, display.width(), 12); char tmp[80]; @@ -279,17 +345,19 @@ public: sprintf(tmp, "FQ: %06.3f SF: %d", _node_prefs->freq, _node_prefs->sf); display.print(tmp); - display.setCursor(0, 31); + display.setCursor(0, 29); sprintf(tmp, "BW: %03.2f CR: %d", _node_prefs->bw, _node_prefs->cr); display.print(tmp); // tx power, noise floor - display.setCursor(0, 42); + display.setCursor(0, 38); sprintf(tmp, "TX: %ddBm", _node_prefs->tx_power_dbm); display.print(tmp); - display.setCursor(0, 53); + display.setCursor(0, 47); sprintf(tmp, "Noise floor: %d", radio_driver.getNoiseFloor()); display.print(tmp); + display.setColor(UIColor::secondary_txt); + display.drawTextCentered(display.width() / 2, 56, "setup: " PRESS_LABEL); } else if (_page == HomePage::BLUETOOTH) { display.setColor(UIColor::corp_blue); display.drawXbm((display.width() - 32) / 2, 18, @@ -441,6 +509,28 @@ public: } bool handleInput(char c) override { + if (_radio_edit) { // режим настройки радио: страницы не листаются + _radio_last_activity = millis(); // любое нажатие продлевает жизнь меню + if (c == KEY_NEXT || c == KEY_RIGHT) { + if (_radio_val_edit) radioValueStep(+1); else radioRowStep(+1); + return true; + } + if (c == KEY_PREV || c == KEY_LEFT) { + if (_radio_val_edit) radioValueStep(-1); else radioRowStep(-1); + return true; + } + if (c == KEY_ENTER) { + if (_radio_val_edit) { + _radio_val_edit = false; // назад к списку + } else if (_radio_sel == ROW_SAVE) { + radioEditLeave(true); // сохранить в память и выйти + } else { + _radio_val_edit = true; // править значение + } + return true; + } + return true; // остальные события в этом режиме игнорируем + } if (c == KEY_LEFT || c == KEY_PREV) { _page = (_page + HomePage::Count - 1) % HomePage::Count; return true; @@ -452,6 +542,12 @@ public: } return true; } + if (c == KEY_ENTER && _page == HomePage::RADIO) { + radioEditEnter(); + // предупреждение: остальные узлы сети должны иметь те же параметры + _task->showAlert("Nodes must match", 1500); + return true; + } if (c == KEY_ENTER && _page == HomePage::BLUETOOTH) { if (_task->isBluetoothEnabled()) { // toggle Bluetooth on/off _task->disableBluetooth(); @@ -490,6 +586,125 @@ public: } }; +void HomeScreen::radioValueStep(int8_t dir) { + switch (_radio_sel) { + case ROW_FQ: { + int32_t khz = (int32_t)(_node_prefs->freq * 1000.0f + 0.5f) + dir * UI_RADIO_FREQ_STEP_KHZ; + int32_t lo = (int32_t)(UI_RADIO_FREQ_MIN * 1000.0f), hi = (int32_t)(UI_RADIO_FREQ_MAX * 1000.0f); + if (khz < lo) khz = lo; + if (khz > hi) khz = hi; + if (!the_mesh.setRadioParams((float)khz / 1000.0f, _node_prefs->bw, _node_prefs->sf, _node_prefs->cr)) { + _task->showAlert("Freq not allowed", 1200); + } + break; + } + case ROW_BW: { + // ищем ближайшее допустимое значение; если текущее не из списка — + // первый шаг приводит к ближайшему, дальше идём по списку + int idx = 0; + for (int i = 1; i < (int)UI_RADIO_BW_STEPS_N; i++) { + if (fabsf(UI_RADIO_BW_STEPS[i] - _node_prefs->bw) < fabsf(UI_RADIO_BW_STEPS[idx] - _node_prefs->bw)) idx = i; + } + int next = idx; + if (fabsf(UI_RADIO_BW_STEPS[idx] - _node_prefs->bw) < 0.01f) { + next = idx + dir; + if (next < 0) next = 0; + if (next >= (int)UI_RADIO_BW_STEPS_N) next = (int)UI_RADIO_BW_STEPS_N - 1; + } + the_mesh.setRadioParams(_node_prefs->freq, UI_RADIO_BW_STEPS[next], _node_prefs->sf, _node_prefs->cr); + break; + } + case ROW_SF: { + int v = (int)_node_prefs->sf + dir; + if (v < 5) v = 5; + if (v > 12) v = 12; + the_mesh.setRadioParams(_node_prefs->freq, _node_prefs->bw, (uint8_t)v, _node_prefs->cr); + break; + } + case ROW_CR: { + int v = (int)_node_prefs->cr + dir; + if (v < 5) v = 5; + if (v > 8) v = 8; + the_mesh.setRadioParams(_node_prefs->freq, _node_prefs->bw, _node_prefs->sf, (uint8_t)v); + break; + } + case ROW_TX: { + the_mesh.setTxPowerDbm((int8_t)((int)_node_prefs->tx_power_dbm + dir)); + break; + } + } +} + +void HomeScreen::radioRowValue(uint8_t row, char* buf, size_t len) { + switch (row) { + case ROW_FQ: snprintf(buf, len, "%.3f MHz", _node_prefs->freq); break; + case ROW_BW: snprintf(buf, len, "%.2f kHz", _node_prefs->bw); break; + case ROW_SF: snprintf(buf, len, "%u", (unsigned)_node_prefs->sf); break; + case ROW_CR: snprintf(buf, len, "%u", (unsigned)_node_prefs->cr); break; + case ROW_TX: snprintf(buf, len, "%d dBm", (int)_node_prefs->tx_power_dbm); break; + default: buf[0] = 0; break; + } +} + +const char* HomeScreen::radioRowName(uint8_t row) { + switch (row) { + case ROW_FQ: return "FQ"; + case ROW_BW: return "BW"; + case ROW_SF: return "SF"; + case ROW_CR: return "CR"; + case ROW_TX: return "TX"; + default: return "Save & exit"; + } +} + +void HomeScreen::renderRadioEditor(DisplayDriver& display) { + char buf[24]; + char head[32]; + + if (_radio_val_edit) { // фаза правки значения + switch (_radio_sel) { + case ROW_FQ: snprintf(head, sizeof(head), "FQ %.0f-%.0f MHz", UI_RADIO_FREQ_MIN, UI_RADIO_FREQ_MAX); break; + case ROW_BW: strcpy(head, "BW 7.8-500 kHz"); break; + case ROW_SF: strcpy(head, "SF 5-12"); break; + case ROW_CR: strcpy(head, "CR 5-8"); break; + default: snprintf(head, sizeof(head), "TX -9-%d dBm", MAX_LORA_TX_POWER); break; + } + display.setColor(UIColor::secondary_txt); + display.setTextSize(1); + display.drawTextLeftAlign(0, 2, head); + + radioRowValue(_radio_sel, buf, sizeof(buf)); + display.setColor(UIColor::warning_txt); + display.setTextSize(2); + display.drawTextCentered(display.width() / 2, 20, buf); + + display.setTextSize(1); + display.drawTextCentered(display.width() / 2, 64 - 10, "click+ dbl- hold=back"); + return; + } + + // фаза выбора: выбранная строка инвертирована + display.setTextSize(1); + int y = 2; + for (uint8_t i = 0; i < ROW_COUNT; i++, y += 10) { + radioRowValue(i, buf, sizeof(buf)); + if (i == _radio_sel) { + display.setColor(UIColor::primary_txt); + display.fillRect(0, y - 1, display.width(), 10); + display.setColor(UIColor::window_bkg); + display.drawTextLeftAlign(1, y, radioRowName(i)); + if (buf[0]) display.drawTextRightAlign(display.width() - 2, y, buf); + } else { + display.setColor(UIColor::primary_txt); + display.drawTextLeftAlign(1, y, radioRowName(i)); + if (buf[0]) { + display.setColor(UIColor::secondary_txt); + display.drawTextRightAlign(display.width() - 2, y, buf); + } + } + } +} + class MsgPreviewScreen : public UIScreen { UITask* _task; mesh::RTCClock* _rtc; @@ -611,6 +826,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no ui_started_at = millis(); _alert_expiry = 0; + _msg_pending = false; splash = new SplashScreen(this); home = new HomeScreen(this, &rtc_clock, sensors, node_prefs); @@ -623,6 +839,29 @@ void UITask::showAlert(const char* text, int duration_millis) { _alert_expiry = millis() + duration_millis; } +void UITask::keepDisplayOn() { + _auto_off = millis() + AUTO_OFF_MILLIS; // экран не гаснет, пока идёт работа с меню +} + +void UITask::blankDisplay() { + if (_display != NULL && _display->isOn()) { + _display->turnOff(); + } +} + +void UITask::clearAlert() { + _alert_expiry = 0; +} + +void UITask::showPendingMessages() { + if (_msg_pending) { + _msg_pending = false; + if (_msgcount > 0) { + setCurrScreen(msg_preview); + } + } +} + void UITask::notify(UIEventType t) { #if defined(PIN_BUZZER) switch(t){ @@ -664,6 +903,14 @@ void UITask::newMsg(uint8_t path_len, const char* from_name, const char* text, i _msgcount = msgcount; ((MsgPreviewScreen *) msg_preview)->addPreview(path_len, from_name, text); + + if (curr != NULL && curr->suppressAlerts()) { + // Экран попросил не перекрывать его (меню настройки радио): сообщение уже + // в очереди непрочитанных, покажем его после выхода из меню. + _msg_pending = true; + _next_refresh = 100; + return; + } setCurrScreen(msg_preview); if (_display != NULL) { diff --git a/examples/companion_radio/ui-new/UITask.h b/examples/companion_radio/ui-new/UITask.h index 52d3ffa1..90904681 100644 --- a/examples/companion_radio/ui-new/UITask.h +++ b/examples/companion_radio/ui-new/UITask.h @@ -35,6 +35,7 @@ class UITask : public AbstractUITask { NodePrefs* _node_prefs; char _alert[80]; unsigned long _alert_expiry; + bool _msg_pending; // сообщение пришло, пока экран просил не перекрывать его int _msgcount; unsigned long ui_started_at, next_batt_chck; int next_backlight_btn_check = 0; @@ -74,6 +75,10 @@ public: void gotoHomeScreen() { setCurrScreen(home); } void showAlert(const char* text, int duration_millis); + void keepDisplayOn(); // продлить таймаут автогашения (например, пока открыто меню) + void blankDisplay(); // погасить экран немедленно + void clearAlert(); // снять текущий попап + void showPendingMessages(); // открыть предпросмотр, если сообщение пришло «в фоне» int getMsgCount() const { return _msgcount; } bool hasDisplay() const { return _display != NULL; } bool isButtonPressed() const; diff --git a/src/helpers/radiolib/RadioLibWrappers.h b/src/helpers/radiolib/RadioLibWrappers.h index 77dd9311..a819af09 100644 --- a/src/helpers/radiolib/RadioLibWrappers.h +++ b/src/helpers/radiolib/RadioLibWrappers.h @@ -51,6 +51,11 @@ public: uint32_t getRngSeed(); void setTxPower(int8_t dbm); + // Перезапуск приёма после смены радио-параметров: сеттеры RadioLib уводят чип + // в standby, а state обёртки остаётся STATE_RX, поэтому recvRaw() сам приём не + // поднимет. Нужен после setParams()/setTxPower() при настройке на ходу. + void restartRecv() { idle(); startRecv(); } + virtual float getCurrentRSSI() =0; virtual uint8_t getSpreadingFactor() const { return LORA_SF; } static uint16_t preambleLengthForSF(uint8_t sf) { return sf <= 8 ? 32 : 16; } diff --git a/src/helpers/ui/UIScreen.h b/src/helpers/ui/UIScreen.h index 6aa1d69c..d6d8a53c 100644 --- a/src/helpers/ui/UIScreen.h +++ b/src/helpers/ui/UIScreen.h @@ -21,5 +21,10 @@ public: virtual int render(DisplayDriver& display) =0; // return value is number of millis until next render virtual bool handleInput(char c) { return false; } virtual void poll() { } + + // Экран может попросить не перекрывать его всплывающими уведомлениями + // (например, меню настройки радио): входящие сообщения копятся в очереди, + // но экран не подменяется. + virtual bool suppressAlerts() { return false; } };