From ce1b71d6fd69fff6ee812219a20011a4fc7ef25a Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 11 Oct 2026 18:06:55 +0300 Subject: [PATCH] feat: separate radio settings in the on-device setup wizard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - радиопресет заменён на четыре отдельных пункта меню: Frequency, Bandwidth, CR, SF - частота: целые МГц шагом +-1, кГц правятся по разрядам (принцип как у координат) - полоса: поразрядный ввод значения с точкой (формат 062.50) - CR и SF: шаг +-1 с ограничением 5-8 и 5-12 - применение любого из четырёх пунктов отправляет set radio <частота>,<полоса>,, целиком, остальные значения сохраняются; внизу меню остаётся Save & exit - документация device-setup.md обновлена, README: раздел и журнал изменений - проверено сборкой: heltec_v4_repeater_mqtt, Meshadventurer_sx1262_repeater_mqtt, Generic_E22_sx1262_repeater_mqtt (без дисплея) --- README.md | 7 +- examples/simple_repeater/UITask.cpp | 322 ++++++++++++++++++++++++---- examples/simple_repeater/UITask.h | 34 ++- posadmesh-docs/device-setup.md | 20 +- 4 files changed, 341 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index b2cba8d6..e4c076ab 100644 --- a/README.md +++ b/README.md @@ -304,9 +304,9 @@ get bridge.enabled ### Настройка с дисплея и кнопки -На платах с дисплеем и кнопкой мастер настройки доступен прямо на устройстве — без компьютера и телефона: двойное нажатие кнопки из экрана состояния открывает меню. Меняются координаты, радиопресет сети MeshCoreTel, мощность передатчика, пересылка пакетов, размер хэша пути, интервалы advert и flood advert, лимит flood. Значения применяются через тот же обработчик, что и последовательный CLI, поэтому проверка и сохранение одинаковы для обоих путей, а ответ устройства показывается на экране. +На платах с дисплеем и кнопкой мастер настройки доступен прямо на устройстве — без компьютера и телефона: двойное нажатие кнопки из экрана состояния открывает меню. Настраиваются координаты, частота, полоса, CR и SF по отдельности, мощность передатчика, пересылка пакетов, размер хэша пути, интервалы advert и flood advert, лимит flood. Внизу меню — `Save & exit`. Значения применяются через тот же обработчик, что и последовательный CLI, поэтому проверка и сохранение одинаковы для обоих путей, а ответ устройства показывается на экране. -Координаты вводятся в формате `xx.xxxx yy.yyyy`: длинное нажатие входит в правку цифры, короткое — `+1`, двойное — `−1`, длинное — применить, короткое после применения — перейти к следующей цифре, в конце экран подтверждения. +Координаты вводятся в формате `xx.xxxx yy.yyyy`, частота — как `868.731` (целые МГц шагом 1, кГц по разрядам), полоса — поразрядно: длинное нажатие входит в правку цифры или позиции, короткое — `+1`, двойное — `−1`, длинное — применить, короткое после применения — перейти к следующей цифре, в конце экран подтверждения. Подробное описание кнопок, меню и ограничений — в [Настройке с дисплея и кнопки](./posadmesh-docs/device-setup.md). @@ -375,7 +375,8 @@ MQTT-репитер использует: - добавлен мастер настройки репитера ([`examples/simple_repeater/UITask.*`](./examples/simple_repeater/UITask.cpp)): работает на любой цели, у которой есть дисплей и кнопка, и полностью исключается препроцессором там, где их нет; - вход в мастер — двойное нажатие кнопки из экрана состояния (на узле без имени мастер открывается сам после заставки); выход — пункт `Save & exit`; - ввод координат в формате `xx.xxxx yy.yyyy`: длинное нажатие — правка текущей цифры, короткое — `+1`, двойное — `−1`, длинное — применить, короткое после применения — следующая цифра; после всех 12 цифр — экран сохранения; -- остальные пункты: радиопресет MeshCoreTel (868,731 МГц, 62,5 кГц, SF7, CR7), мощность TX, пересылка пакетов, размер хэша пути, интервалы advert и flood advert, лимит flood; +- радиопараметры настраиваются по отдельности: `Frequency` (целые МГц шагом ±1, кГц по разрядам — как координаты), `Bandwidth` (поразрядно, формат `062.50`), `CR` и `SF` (шагом); любой из четырёх пунктов отправляет `set radio <частота>,<полоса>,,`, поэтому остальные значения сохраняются; +- остальные пункты: мощность TX, пересылка пакетов, размер хэша пути, интервалы advert и flood advert, лимит flood; внизу меню — `Save & exit`; - значения применяются через тот же `CommonCLI`, что и последовательный порт, а ответ устройства (`OK` или текст ошибки) выводится на экран; - документация — [Настройка с дисплея и кнопки](./posadmesh-docs/device-setup.md); - сборка проверена на `heltec_v4_repeater_mqtt`, `Meshadventurer_sx1262_repeater_mqtt`, `Generic_E22_sx1262_repeater_mqtt` (без дисплея) и `RAK_3112_repeater_mqtt`. diff --git a/examples/simple_repeater/UITask.cpp b/examples/simple_repeater/UITask.cpp index 683e92f0..c6e422af 100644 --- a/examples/simple_repeater/UITask.cpp +++ b/examples/simple_repeater/UITask.cpp @@ -16,11 +16,15 @@ #define BOOT_SCREEN_MILLIS 4000 // 4 seconds #define POWEROFF_DELAY 3000 #define TOAST_MILLIS 3000 +#define CHAR_W 6 // ширина символа при setTextSize(1) // Пункты мастера. Подписи латиницей: шрифты драйверов не содержат кириллицы. enum { MENU_COORDS = 0, - MENU_RADIO, + MENU_FREQ, + MENU_BW, + MENU_CR, + MENU_SF, MENU_TX, MENU_REPEAT, MENU_PATHHASH, @@ -33,7 +37,10 @@ enum { static const char* const MENU_NAMES[MENU_COUNT] = { "Coordinates", - "Radio preset", + "Frequency", + "Bandwidth", + "CR", + "SF", "TX power", "Repeat", "Path hash", @@ -43,9 +50,6 @@ static const char* const MENU_NAMES[MENU_COUNT] = { "Save & exit" }; -// 0 = не менять; MeshCoreTel MOW: 868.731 МГц, 62.5 кГц, SF7, CR7 -static const char* const RADIO_PRESET_CMDS[] = { nullptr, "set radio 868.731,62.5,7,7" }; -static const char* const RADIO_PRESET_NAMES[] = { "keep current", "MeshCoreTel MOW" }; static const char* const PATH_HASH_NAMES[] = { "0 bytes", "1 byte", "2 bytes" }; // позиции цифр в строке вида "xx.xxxx" @@ -143,90 +147,160 @@ void UITask::openMenu() { _auto_off = millis() + WIZARD_AUTO_OFF_MILLIS; } +// --- радиопараметры --------------------------------------------------------- + +void UITask::loadRadio() { + if (_radio_loaded || _node_prefs == nullptr) return; + _radio_freq = _node_prefs->freq; + _radio_bw = _node_prefs->bw; + _radio_sf = _node_prefs->sf; + _radio_cr = _node_prefs->cr; + _radio_loaded = true; +} + +void UITask::applyRadio() { + char cmd[96]; + snprintf(cmd, sizeof(cmd), "set radio %.3f,%.2f,%d,%d", _radio_freq, _radio_bw, _radio_sf, _radio_cr); + runCommand(cmd); +} + +void UITask::beginFreqEdit() { + loadRadio(); + for (int i = 0; i < 4; i++) _freq_applied[i] = false; + _freq_index = 0; + _freq_editing = false; + + double v = _radio_freq < 0 ? -_radio_freq : _radio_freq; + int mhz = (int)v; + int khz = (int)((v - mhz) * 1000.0 + 0.5); + if (khz > 999) { khz = 0; mhz++; } + _freq_mhz = mhz; + _freq_khz[0] = (char)('0' + (khz / 100) % 10); + _freq_khz[1] = (char)('0' + (khz / 10) % 10); + _freq_khz[2] = (char)('0' + khz % 10); + _screen = SCREEN_FREQ; +} + +double UITask::freqValue() const { + int khz = (_freq_khz[0] - '0') * 100 + (_freq_khz[1] - '0') * 10 + (_freq_khz[2] - '0'); + return _freq_mhz + khz / 1000.0; +} + +void UITask::beginBwEdit() { + loadRadio(); + _digit_int = 3; + _digit_count = 5; // xxx.xx, например 062.50 + for (int i = 0; i < _digit_count; i++) _digit_applied[i] = false; + _digit_index = 0; + _digit_editing = false; + + double v = _radio_bw < 0 ? -_radio_bw : _radio_bw; + int ip = (int)v; + int fp = (int)((v - ip) * 100.0 + 0.5); + if (fp > 99) { fp = 0; ip++; } + char buf[8]; + snprintf(buf, sizeof(buf), "%03d%02d", ip % 1000, fp); + memcpy(_digit_digits, buf, _digit_count); + _screen = SCREEN_DIGITS; +} + +double UITask::digitValue() const { + int ip = 0, fp = 0; + for (int i = 0; i < _digit_int; i++) ip = ip * 10 + (_digit_digits[i] - '0'); + for (int i = _digit_int; i < _digit_count; i++) fp = fp * 10 + (_digit_digits[i] - '0'); + double scale = 1; + for (int i = _digit_int; i < _digit_count; i++) scale *= 10; + return ip + fp / scale; +} + +// --- меню ------------------------------------------------------------------- + void UITask::openField(uint8_t index) { _menu_index = index; + _auto_off = millis() + WIZARD_AUTO_OFF_MILLIS; + switch (index) { case MENU_COORDS: coordFromPrefs(); _screen = SCREEN_COORDS; + return; + case MENU_FREQ: + beginFreqEdit(); + return; + case MENU_BW: + beginBwEdit(); + return; + case MENU_CR: + loadRadio(); + _value = _radio_cr; break; - case MENU_RADIO: - _value = 0; // keep current - _screen = SCREEN_VALUE; + case MENU_SF: + loadRadio(); + _value = _radio_sf; break; case MENU_TX: _value = _node_prefs ? _node_prefs->tx_power_dbm : 0; - _screen = SCREEN_VALUE; break; case MENU_REPEAT: _value = (_node_prefs && _node_prefs->disable_fwd) ? 0 : 1; - _screen = SCREEN_VALUE; break; case MENU_PATHHASH: _value = _node_prefs ? _node_prefs->path_hash_mode : 0; - _screen = SCREEN_VALUE; break; case MENU_ADVERT: _value = _node_prefs ? _node_prefs->advert_interval * 2 : 0; - _screen = SCREEN_VALUE; break; case MENU_FLOODADV: _value = _node_prefs ? _node_prefs->flood_advert_interval : 0; - _screen = SCREEN_VALUE; break; case MENU_FLOODMAX: _value = _node_prefs ? _node_prefs->flood_max : 0; - _screen = SCREEN_VALUE; break; default: _screen = SCREEN_STATUS; - break; + return; } - _auto_off = millis() + WIZARD_AUTO_OFF_MILLIS; + _screen = SCREEN_VALUE; } void UITask::applyField() { char cmd[96]; - bool ok = false; switch (_menu_index) { - case MENU_RADIO: - if (_value <= 0 || RADIO_PRESET_CMDS[_value] == nullptr) { - setToast("keep current"); - return; - } - ok = runCommand(RADIO_PRESET_CMDS[_value]); - break; + case MENU_CR: + _radio_cr = _value; + applyRadio(); + return; + case MENU_SF: + _radio_sf = _value; + applyRadio(); + return; case MENU_TX: snprintf(cmd, sizeof(cmd), "set tx %d", _value); - ok = runCommand(cmd); break; case MENU_REPEAT: snprintf(cmd, sizeof(cmd), "set repeat %s", _value ? "on" : "off"); - ok = runCommand(cmd); break; case MENU_PATHHASH: snprintf(cmd, sizeof(cmd), "set path.hash.mode %d", _value); - ok = runCommand(cmd); break; case MENU_ADVERT: snprintf(cmd, sizeof(cmd), "set advert.interval %d", _value); - ok = runCommand(cmd); break; case MENU_FLOODADV: snprintf(cmd, sizeof(cmd), "set flood.advert.interval %d", _value); - ok = runCommand(cmd); break; case MENU_FLOODMAX: snprintf(cmd, sizeof(cmd), "set flood.max %d", _value); - ok = runCommand(cmd); break; default: return; } - (void)ok; + runCommand(cmd); } +// --- координаты ------------------------------------------------------------- + void UITask::coordFromPrefs() { for (int i = 0; i < 12; i++) _coord_applied[i] = false; _coord_digit = 0; @@ -274,6 +348,8 @@ void UITask::coordSave() { } } +// --- кнопка ----------------------------------------------------------------- + void UITask::handleButton(int ev) { switch (_screen) { case SCREEN_STATUS: @@ -295,6 +371,7 @@ void UITask::handleButton(int ev) { _menu_index = (uint8_t)((_menu_index + MENU_COUNT - 1) % MENU_COUNT); } else if (ev == BUTTON_EVENT_LONG_PRESS) { if (_menu_index == MENU_EXIT) { + applyRadio(); // на случай правок частоты/полосы/SF/CR _screen = SCREEN_STATUS; } else { openField(_menu_index); @@ -306,8 +383,11 @@ void UITask::handleButton(int ev) { if (ev == BUTTON_EVENT_CLICK || ev == BUTTON_EVENT_DOUBLE_CLICK) { int dir = (ev == BUTTON_EVENT_CLICK) ? 1 : -1; switch (_menu_index) { - case MENU_RADIO: - _value = (_value + 1) % 2; + case MENU_CR: + stepClamped(_value, dir, 5, 8, 1); + break; + case MENU_SF: + stepClamped(_value, dir, 5, 12, 1); break; case MENU_REPEAT: _value = _value ? 0 : 1; @@ -336,6 +416,82 @@ void UITask::handleButton(int ev) { } break; + case SCREEN_DIGITS: { // полоса: поразрядно, как координаты + if (_digit_editing) { + if (ev == BUTTON_EVENT_CLICK) { + int d = _digit_digits[_digit_index] - '0'; + _digit_digits[_digit_index] = (char)('0' + (d + 1) % 10); + } else if (ev == BUTTON_EVENT_DOUBLE_CLICK) { + int d = _digit_digits[_digit_index] - '0'; + _digit_digits[_digit_index] = (char)('0' + (d + 9) % 10); + } else if (ev == BUTTON_EVENT_LONG_PRESS) { + _digit_editing = false; + _digit_applied[_digit_index] = true; + } + } else if (ev == BUTTON_EVENT_LONG_PRESS) { + _digit_editing = true; + } else if (ev == BUTTON_EVENT_DOUBLE_CLICK) { + _digit_index = (uint8_t)((_digit_index + _digit_count - 1) % _digit_count); + } else if (ev == BUTTON_EVENT_CLICK) { + bool all = true; + for (int i = 0; i < _digit_count; i++) { + if (!_digit_applied[i]) { all = false; break; } + } + if (_digit_index == (uint8_t)(_digit_count - 1) && all) { + _radio_bw = digitValue(); + applyRadio(); + _screen = SCREEN_MENU; + } else { + _digit_index = (uint8_t)((_digit_index + 1) % _digit_count); + } + } + break; + } + + case SCREEN_FREQ: { // частота: МГц шагом 1, кГц по разрядам + if (_freq_editing) { + if (_freq_index == 0) { + if (ev == BUTTON_EVENT_CLICK) { + if (_freq_mhz < 2500) _freq_mhz++; + } else if (ev == BUTTON_EVENT_DOUBLE_CLICK) { + if (_freq_mhz > 150) _freq_mhz--; + } else if (ev == BUTTON_EVENT_LONG_PRESS) { + _freq_editing = false; + _freq_applied[0] = true; + } + } else { + uint8_t k = (uint8_t)(_freq_index - 1); + if (ev == BUTTON_EVENT_CLICK) { + int d = _freq_khz[k] - '0'; + _freq_khz[k] = (char)('0' + (d + 1) % 10); + } else if (ev == BUTTON_EVENT_DOUBLE_CLICK) { + int d = _freq_khz[k] - '0'; + _freq_khz[k] = (char)('0' + (d + 9) % 10); + } else if (ev == BUTTON_EVENT_LONG_PRESS) { + _freq_editing = false; + _freq_applied[_freq_index] = true; + } + } + } else if (ev == BUTTON_EVENT_LONG_PRESS) { + _freq_editing = true; + } else if (ev == BUTTON_EVENT_DOUBLE_CLICK) { + _freq_index = (uint8_t)((_freq_index + 3) % 4); + } else if (ev == BUTTON_EVENT_CLICK) { + bool all = true; + for (int i = 0; i < 4; i++) { + if (!_freq_applied[i]) { all = false; break; } + } + if (_freq_index == 3 && all) { + _radio_freq = freqValue(); + applyRadio(); + _screen = SCREEN_MENU; + } else { + _freq_index = (uint8_t)((_freq_index + 1) % 4); + } + } + break; + } + case SCREEN_COORDS: if (_coord_editing) { // правка текущей цифры: короткое +1, двойное -1, длинное — применить @@ -412,6 +568,8 @@ void UITask::loop() { } } +// --- отрисовка -------------------------------------------------------------- + void UITask::renderTitleBar(const char* title) { _display->setColor(UIColor::primary_txt); _display->fillRect(0, 0, _display->width(), 11); @@ -478,8 +636,11 @@ void UITask::renderValue() { const char* title = MENU_NAMES[_menu_index]; switch (_menu_index) { - case MENU_RADIO: - snprintf(value, sizeof(value), "%s", RADIO_PRESET_NAMES[_value % 2]); + case MENU_CR: + snprintf(value, sizeof(value), "%d", _value); + break; + case MENU_SF: + snprintf(value, sizeof(value), "%d", _value); break; case MENU_TX: snprintf(value, sizeof(value), "%d dBm", _value); @@ -514,6 +675,91 @@ void UITask::renderValue() { renderToast(); } +// поразрядный ввод значения с точкой (полоса): "062.50" +void UITask::renderDigits() { + renderTitleBar("Bandwidth"); + _display->setTextSize(1); + int y = 22; + int cx = 2; + for (int i = 0; i < _digit_count; i++) { + if (i == _digit_int) { + _display->setColor(UIColor::primary_txt); + _display->setCursor(cx, y); + _display->print("."); + cx += CHAR_W; + } + char c[2] = { _digit_digits[i], 0 }; + if (i == _digit_index) { + _display->setColor(UIColor::primary_txt); + _display->fillRect(cx, y - 1, CHAR_W, 9); + _display->setColor(UIColor::window_bkg); + } else { + _display->setColor(UIColor::primary_txt); + } + _display->setCursor(cx, y); + _display->print(c); + _display->setColor(UIColor::primary_txt); + cx += CHAR_W; + } + _display->setCursor(2, 36); + _display->print("MHz"); + renderHint(_digit_editing ? "clk+ dbl- hold=set" : "hold=edit clk=next"); + renderToast(); +} + +// частота: целые МГц выделяются целиком (шаг 1), кГц — по разрядам +void UITask::renderFreq() { + renderTitleBar("Frequency"); + _display->setTextSize(1); + + int y = 22; + int cx = 2; + char mhz[8]; + snprintf(mhz, sizeof(mhz), "%d", _freq_mhz); + int mhzLen = (int)strlen(mhz); + + if (_freq_index == 0) { + _display->setColor(UIColor::primary_txt); + _display->fillRect(cx, y - 1, mhzLen * CHAR_W, 9); + _display->setColor(UIColor::window_bkg); + } else { + _display->setColor(UIColor::primary_txt); + } + _display->setCursor(cx, y); + _display->print(mhz); + _display->setColor(UIColor::primary_txt); + cx += mhzLen * CHAR_W; + + _display->setCursor(cx, y); + _display->print("."); + cx += CHAR_W; + + for (int i = 0; i < 3; i++) { + char c[2] = { _freq_khz[i], 0 }; + if (_freq_index == (uint8_t)(i + 1)) { + _display->setColor(UIColor::primary_txt); + _display->fillRect(cx, y - 1, CHAR_W, 9); + _display->setColor(UIColor::window_bkg); + } else { + _display->setColor(UIColor::primary_txt); + } + _display->setCursor(cx, y); + _display->print(c); + _display->setColor(UIColor::primary_txt); + cx += CHAR_W; + } + + _display->setColor(UIColor::primary_txt); + _display->setCursor(2, 36); + _display->print("MHz"); + if (_freq_editing) { + renderHint(_freq_index == 0 ? "clk +1MHz dbl -1" : "clk+ dbl- hold=set"); + } else { + renderHint("hold=edit clk=next"); + } + renderToast(); +} + void UITask::renderCoords() { char text[10]; renderTitleBar("Coordinates"); @@ -528,12 +774,12 @@ void UITask::renderCoords() { for (int k = 0; k < 6; k++) { if (COORD_POS[k] == i) { digitIndex = k; break; } } - int x = i * 6; + int x = i * CHAR_W; bool active = (digitIndex >= 0) && ((axis * 6 + digitIndex) == _coord_digit); char c[2] = { text[i], 0 }; if (active) { _display->setColor(UIColor::primary_txt); - _display->fillRect(x, y - 1, 6, 9); + _display->fillRect(x, y - 1, CHAR_W, 9); _display->setColor(UIColor::window_bkg); _display->setCursor(x, y); _display->print(c); @@ -642,6 +888,8 @@ void UITask::renderCurrScreen() { switch (_screen) { case SCREEN_MENU: renderMenu(); break; case SCREEN_VALUE: renderValue(); break; + case SCREEN_DIGITS: renderDigits(); break; + case SCREEN_FREQ: renderFreq(); break; case SCREEN_COORDS: renderCoords(); break; case SCREEN_COORDS_SAVE: renderCoordsSave(); break; default: renderStatus(); break; diff --git a/examples/simple_repeater/UITask.h b/examples/simple_repeater/UITask.h index 3dffc5a0..bdf3c6a1 100644 --- a/examples/simple_repeater/UITask.h +++ b/examples/simple_repeater/UITask.h @@ -15,7 +15,9 @@ public: enum Screen : uint8_t { SCREEN_STATUS = 0, SCREEN_MENU, - SCREEN_VALUE, + SCREEN_VALUE, // числа и переключатели: SF, CR, TX, repeat, path hash, advert, flood + SCREEN_DIGITS, // поразрядный ввод значения с точкой (полоса) + SCREEN_FREQ, // частота: МГц шагом 1, кГц по разрядам SCREEN_COORDS, SCREEN_COORDS_SAVE }; @@ -43,10 +45,38 @@ private: char _toast[40]; unsigned long _toast_until = 0; + // радиопараметры: рабочая копия, пока мастер открыт + bool _radio_loaded = false; + double _radio_freq = 0; + double _radio_bw = 0; + int _radio_sf = 0; + int _radio_cr = 0; + + // поразрядный редактор значения с десятичной точкой (полоса) + uint8_t _digit_count = 0; + uint8_t _digit_int = 0; // цифр до точки + uint8_t _digit_index = 0; + bool _digit_editing = false; + bool _digit_applied[8]; + char _digit_digits[8]; + + // редактор частоты: целые МГц меняются шагом 1, кГц — по разрядам + int _freq_mhz = 0; + char _freq_khz[3] = { '0', '0', '0' }; + uint8_t _freq_index = 0; // 0 = МГц, 1..3 = разряды кГц + bool _freq_editing = false; + bool _freq_applied[4]; + bool wizardActive() const { return _screen != SCREEN_STATUS; } void openMenu(); void openField(uint8_t index); void applyField(); + void loadRadio(); + void applyRadio(); + void beginBwEdit(); + void beginFreqEdit(); + double digitValue() const; + double freqValue() const; void coordFromPrefs(); void coordSave(); double coordAxisValue(uint8_t axis) const; // axis 0 = lat, 1 = lon @@ -59,6 +89,8 @@ private: void renderStatus(); void renderMenu(); void renderValue(); + void renderDigits(); + void renderFreq(); void renderCoords(); void renderCoordsSave(); void renderTitleBar(const char* title); diff --git a/posadmesh-docs/device-setup.md b/posadmesh-docs/device-setup.md index 40c3be12..d83ddf2f 100644 --- a/posadmesh-docs/device-setup.md +++ b/posadmesh-docs/device-setup.md @@ -9,7 +9,10 @@ | Пункт меню | Что делает | Команда CLI | | --- | --- | --- | | `Coordinates` | широта и долгота узла | `set lat`, `set lon` | -| `Radio preset` | `keep current` или готовый пресет сети MeshCoreTel (868,731 МГц, 62,5 кГц, SF7, CR7) | `set radio` | +| `Frequency` | частота: целые МГц шагом 1, кГц — по разрядам | `set radio` | +| `Bandwidth` | полоса, поразрядно | `set radio` | +| `CR` | coding rate, 5–8 | `set radio` | +| `SF` | spreading factor, 5–12 | `set radio` | | `TX power` | мощность передатчика, 1–30 дБм | `set tx` | | `Repeat` | пересылка пакетов включена или выключена | `set repeat` | | `Path hash` | размер хэша пути: 0, 1 или 2 байта | `set path.hash.mode` | @@ -58,6 +61,21 @@ Ограничение: мастер вводит положительные координаты (формат без знака). Для южного полушария и западной долготы используйте CLI (`set lat -33.1234`) или веб-панель. +## Ввод частоты и полосы + +Частота показывается как `868.731`. Курсор проходит четыре позиции: целые мегагерцы и три разряда килогерц. + +- **целые МГц** (выделены целиком): короткое нажатие — `+1 МГц`, двойное — `−1 МГц`; +- **разряды кГц** (по одному): короткое нажатие — `+1`, двойное — `−1`, принцип тот же, что у координат; +- длинное нажатие — войти в правку позиции, а в режиме правки — применить её; +- короткое нажатие после применения — перейти к следующей позиции; на последней позиции короткое нажатие применяет частоту и возвращает в меню. + +Полоса вводится так же поразрядно, формат `062.50` — то есть 62,5 кГц. + +`CR` и `SF` меняются шагом: короткое нажатие — `+1`, двойное — `−1`, длинное — применить. + +Любой из четырёх пунктов отправляет команду целиком — `set radio <частота>,<полоса>,,`, поэтому остальные три параметра сохраняют свои значения. Устройство отвечает `OK - reboot to apply`: изменения радио вступают в силу после перезагрузки. + ## Ограничения - На экранах без кириллицы подписи меню выводятся латиницей.