настройка радио с экрана RADIO: freq/bw/sf/cr/tx, сохранение и автовыход из меню
Run Unit Tests / test (push) Canceled after 0s
Build and deploy Docs site to GitHub Pages / github-pages (push) Canceled after 0s
PR Build Check / build (Heltec_v3_companion_radio_ble) (push) Canceled after 0s
PR Build Check / build (Heltec_v3_repeater) (push) Canceled after 0s
PR Build Check / build (Heltec_v3_room_server) (push) Canceled after 0s
PR Build Check / build (LilyGo_Tlora_C6_repeater_) (push) Canceled after 0s
PR Build Check / build (PicoW_repeater) (push) Canceled after 0s
PR Build Check / build (RAK_4631_companion_radio_ble) (push) Canceled after 0s
PR Build Check / build (RAK_4631_companion_radio_ethernet) (push) Canceled after 0s
PR Build Check / build (RAK_4631_repeater) (push) Canceled after 0s
PR Build Check / build (RAK_4631_repeater_ethernet) (push) Canceled after 0s
PR Build Check / build (RAK_4631_room_server) (push) Canceled after 0s
PR Build Check / build (RAK_4631_room_server_ethernet) (push) Canceled after 0s
PR Build Check / build (Tbeam_SX1276_repeater) (push) Canceled after 0s
PR Build Check / build (wio-e5-mini_repeater) (push) Canceled after 0s
PR Build Check / build (wio_wm1110_repeater) (push) Canceled after 0s

This commit is contained in:
2026-10-08 19:31:28 +03:00
parent 64771a4f51
commit 3c10296b90
6 changed files with 298 additions and 4 deletions
+26
View File
@@ -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;
};
+6
View File
@@ -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");
+251 -4
View File
@@ -30,6 +30,25 @@
#endif
#include "icons.h"
#include <math.h>
// --- Ручная настройка радио (страница 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) {
+5
View File
@@ -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;
+5
View File
@@ -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; }
+5
View File
@@ -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; }
};