From 2ff1166c154d8ef5157d06972fcb75f8e68ef122 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 10 Aug 2026 19:42:44 -0700 Subject: [PATCH] Fix BLE pairing display and RX gain defaults --- docs/cli_commands.md | 9 ++- docs/companion_protocol.md | 2 +- docs/companion_radio_full.md | 10 +++- docs/faq.md | 5 +- examples/companion_radio/AbstractUITask.h | 1 + examples/companion_radio/MyMesh.cpp | 13 ++--- examples/companion_radio/ui-new/UITask.cpp | 6 +- examples/companion_radio/ui-orig/UITask.cpp | 56 ++++++++++++++++-- examples/companion_radio/ui-orig/UITask.h | 5 ++ examples/companion_radio/ui-tiny/UITask.cpp | 58 ++++++++++++++++++- examples/companion_radio/ui-tiny/UITask.h | 5 ++ examples/simple_repeater/MyMesh.cpp | 11 ++-- examples/simple_room_server/MyMesh.cpp | 11 ++-- src/helpers/MultiSerialInterface.h | 15 ++++- src/helpers/nrf52/SerialBLEInterface.cpp | 8 ++- src/helpers/nrf52/SerialBLEInterface.h | 5 ++ src/helpers/radiolib/LR11x0Reset.h | 2 + src/helpers/radiolib/RxBoostedGainDefaults.h | 33 +++++++++++ src/helpers/radiolib/SX126xReset.h | 2 + test/README.md | 3 +- .../test_radio_liveness.cpp | 18 ++++++ .../test_serial_mode_switch.cpp | 58 +++++++++++++++++++ 22 files changed, 297 insertions(+), 39 deletions(-) create mode 100644 src/helpers/radiolib/RxBoostedGainDefaults.h diff --git a/docs/cli_commands.md b/docs/cli_commands.md index d598f823..aed3765b 100644 --- a/docs/cli_commands.md +++ b/docs/cli_commands.md @@ -554,9 +554,14 @@ disabled. **Parameters:** - `state`: `on`|`off` -**Default:** `on` +**Default:** Target-specific. Most SX1262 and LR1110 targets default to `on`; +Station G2/G3 targets default to `off`. -**Temporary Note:** If you upgraded from an older version to 1.14.1 without erasing flash, this setting is `off` because of [#2118](https://github.com/meshcore-dev/MeshCore/issues/2118) +**Notes:** +- The saved setting is applied immediately and persists across reboots. +- Periodic AGC resets restore the saved runtime setting; they do not replace it + with the target's compile-time default. +- Existing installations retain their previously saved value after an upgrade. --- diff --git a/docs/companion_protocol.md b/docs/companion_protocol.md index 63c8ec64..a9426bc9 100644 --- a/docs/companion_protocol.md +++ b/docs/companion_protocol.md @@ -676,7 +676,7 @@ Bytes 2+: Variable length based on firmware version For firmware version >= 3: Byte 2: Max Contacts Raw (uint8, actual = value * 2) Byte 3: Max Channels (uint8) -Bytes 4-7: BLE PIN (32-bit little-endian) +Bytes 4-7: Active BLE PIN (32-bit little-endian; includes a generated session PIN) Bytes 8-19: Firmware Build (12 bytes, UTF-8, null-padded) Bytes 20-59: Model (40 bytes, UTF-8, null-padded) Bytes 60-79: Version (20 bytes, UTF-8, null-padded) diff --git a/docs/companion_radio_full.md b/docs/companion_radio_full.md index 392132e3..2a42ca83 100644 --- a/docs/companion_radio_full.md +++ b/docs/companion_radio_full.md @@ -80,7 +80,7 @@ are requested. | Platform | Interface | Purpose | | --- | --- | --- | | Both | USB, 115200 baud | Binary Companion by default; terminal switch available | -| Both | BLE | Binary Companion; default pairing PIN `123456` | +| Both | BLE | Binary Companion; display builds show a random session PIN, while headless builds default to `123456` | | ESP32 | TCP 5000 | Binary Companion over WiFi | | ESP32 | HTTP 80 | Companion WebConfig and first-boot WiFi setup | | ESP32 | TCP 5001 | Host `.mota` folder from `motatool serve --tcp` | @@ -91,6 +91,14 @@ Binary Companion replies are broadcast through the multi-interface manager, so use one active Companion application at a time. On nRF52, BLE remains available while USB is in terminal or mOTA mode. +When a BLE client requests pairing, a display-equipped build wakes the screen, +switches to the first home page, and keeps the active six-digit PIN visible +until Bluetooth connects or the two-minute pairing window expires. USB, WiFi, +Ethernet, and hardware-serial connections do not suppress this screen. With no +saved BLE PIN, display builds generate a new PIN at boot; builds without a +physical display use `123456`. A PIN saved through the Companion protocol takes +effect after reboot. + ESP32 ports 5000, 5001, 5002, and WebConfig have no independent login layer. Expose them only on a trusted LAN or temporary setup network. See [WiFi setup](./WiFi.md) for credential setup and reconnect behavior. diff --git a/docs/faq.md b/docs/faq.md index 7211d627..96e90fd6 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -686,7 +686,10 @@ You can get the epoch time on and use it to set **A:** Make sure that you flashed the Bluetooth companion firmware and not the USB-only companion firmware. ### 6.5. Q: I can't connect via Bluetooth, what is the Bluetooth pairing code? -**A:** The default Bluetooth pairing code is `123456` +**A:** The default Bluetooth pairing code is `123456` on builds without a +physical display. Display-equipped Companion builds generate a six-digit PIN +at boot and show it when a client requests pairing. The display stays awake on +the PIN page until Bluetooth connects or the two-minute pairing window expires. ### 6.6. Q: My Heltec V3 keeps disconnecting from my smartphone. It can't hold a solid Bluetooth connection. **A:** Heltec V3 has a very small coil antenna on its PCB for Wi-Fi and Bluetooth connectivity. It has a very short range, only a few feet. It is possible to remove the coil antenna and replace it with a 31mm wire. The BT range is much improved with the modification. diff --git a/examples/companion_radio/AbstractUITask.h b/examples/companion_radio/AbstractUITask.h index 3d2a9d5f..fe158628 100644 --- a/examples/companion_radio/AbstractUITask.h +++ b/examples/companion_radio/AbstractUITask.h @@ -53,6 +53,7 @@ protected: public: void setHasConnection(bool connected) { _connected = connected; } bool hasConnection() const { return _connected; } + bool hasBluetoothConnection() const { return _interfaceManager->isBluetoothConnected(); } uint16_t getBattMilliVolts() const { return _board->getBattMilliVolts(); } bool isBluetoothEnabled() const { return _interfaceManager->isBluetoothEnabled(); } void enableBluetooth() { _interfaceManager->enableBluetooth(); } diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 923986cd..0d10461f 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -3,6 +3,7 @@ #include // needed for PlatformIO #include #include "helpers/radiolib/RXPowerSaving.h" +#include "helpers/radiolib/RxBoostedGainDefaults.h" #ifdef ENABLE_USB_INTERFACE #include @@ -1336,12 +1337,10 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe _prefs.path_hash_mode = DEFAULT_PATH_HASH_MODE; //_prefs.rx_delay_base = 10.0f; enable once new algo fixed _prefs.setRepeatEn(false); -#if defined(USE_SX1262) || defined(USE_SX1268) -#ifdef SX126X_RX_BOOSTED_GAIN - _prefs.rx_boosted_gain = SX126X_RX_BOOSTED_GAIN; -#else - _prefs.rx_boosted_gain = 1; // enabled by default -#endif +#if defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_LR1110) \ + || defined(USE_LR2021) || defined(SX126X_RX_BOOSTED_GAIN) \ + || defined(RX_BOOSTED_GAIN) + _prefs.rx_boosted_gain = mesh::radio::configuredRxBoostedGainDefault(); #endif _prefs.radio_fem_rxgain = DEFAULT_FEM_RX_GAIN; @@ -2257,7 +2256,7 @@ void MyMesh::handleCmdFrame(size_t len) { out_frame[i++] = FIRMWARE_VER_CODE; out_frame[i++] = MAX_CONTACTS / 2; // v3+ out_frame[i++] = MAX_GROUP_CHANNELS; // v3+ - memcpy(&out_frame[i], &_prefs.ble_pin, 4); + memcpy(&out_frame[i], &_active_ble_pin, 4); i += 4; memset(&out_frame[i], 0, 12); strcpy((char *)&out_frame[i], FIRMWARE_BUILD_DATE); diff --git a/examples/companion_radio/ui-new/UITask.cpp b/examples/companion_radio/ui-new/UITask.cpp index a7546adc..aa7512fe 100644 --- a/examples/companion_radio/ui-new/UITask.cpp +++ b/examples/companion_radio/ui-new/UITask.cpp @@ -239,7 +239,7 @@ public: display.setTextSize(1); display.drawTextCentered(display.width() / 2, 54, tmp); #endif - if (_task->hasConnection()) { + if (_task->hasBluetoothConnection()) { display.setColor(UIColor::warning_txt); display.setTextSize(1); display.drawTextCentered(display.width() / 2, 43, "< Connected >"); @@ -725,7 +725,7 @@ void UITask::setCurrScreen(UIScreen* c) { bool UITask::isPairingScreenActive() const { return _pairing_screen_until != 0 - && !hasConnection() + && !hasBluetoothConnection() && static_cast(millis() - _pairing_screen_until) < 0; } @@ -805,7 +805,7 @@ void UITask::loop() { if (_pairing_screen_until != 0) { const bool timed_out = static_cast(millis() - _pairing_screen_until) >= 0; - if (hasConnection() || timed_out) { + if (hasBluetoothConnection() || timed_out) { finishPairingScreen(timed_out); } } diff --git a/examples/companion_radio/ui-orig/UITask.cpp b/examples/companion_radio/ui-orig/UITask.cpp index 5f9c0f34..9ed0ab72 100644 --- a/examples/companion_radio/ui-orig/UITask.cpp +++ b/examples/companion_radio/ui-orig/UITask.cpp @@ -5,6 +5,9 @@ #define AUTO_OFF_MILLIS 15000 // 15 seconds #define BOOT_SCREEN_MILLIS 3000 // 3 seconds +#ifndef BLE_PAIRING_DISPLAY_MILLIS +#define BLE_PAIRING_DISPLAY_MILLIS 120000UL +#endif #if defined(PIN_STATUS_LED) || defined(PIN_STATUS_LED_R) #define LED_ON_MILLIS 20 @@ -104,7 +107,7 @@ void UITask::begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* no bool UITask::shouldPlayMessageTone() const { #ifdef BLE_PIN_CODE - return !hasConnection(); + return !hasBluetoothConnection(); #else // A USB or Ethernet companion connection is not a Bluetooth connection and // should not silence the standalone new-message alert. @@ -214,7 +217,8 @@ void UITask::renderCurrScreen() { if (_display == NULL) return; // assert() ?? char tmp[80]; - if (_alert[0]) { + const bool pairing_screen_active = isPairingScreenActive(); + if (!pairing_screen_active && _alert[0]) { _display->setTextSize(1.4); uint16_t textWidth = _display->getTextWidth(_alert); _display->setCursor((_display->width() - textWidth) / 2, 22); @@ -223,7 +227,7 @@ void UITask::renderCurrScreen() { _alert[0] = 0; _need_refresh = true; return; - } else if (_origin[0] && _msg[0]) { // message preview + } else if (!pairing_screen_active && _origin[0] && _msg[0]) { // message preview // render message preview _display->setCursor(0, 0); _display->setTextSize(1); @@ -242,7 +246,7 @@ void UITask::renderCurrScreen() { sprintf(tmp, "%d", _msgcount); _display->print(tmp); _display->setColor(UIColor::secondary_txt); // last color will be kept on T114 - } else if ((millis() - ui_started_at) < BOOT_SCREEN_MILLIS) { // boot screen + } else if (!pairing_screen_active && (millis() - ui_started_at) < BOOT_SCREEN_MILLIS) { // boot screen // meshcore logo _display->setColor(UIColor::corp_blue); int logoWidth = 128; @@ -276,7 +280,7 @@ void UITask::renderCurrScreen() { _display->print(tmp); // BT pin - if (!_connected && the_mesh.getBLEPin() != 0) { + if (!hasBluetoothConnection() && the_mesh.getBLEPin() != 0) { _display->setColor(UIColor::warning_txt); _display->setTextSize(2); _display->setCursor(0, 43); @@ -447,7 +451,47 @@ void UITask::shutdown(bool restart){ } } +bool UITask::isPairingScreenActive() const { + return _pairing_screen_until != 0 + && !hasBluetoothConnection() + && static_cast(millis() - _pairing_screen_until) < 0; +} + +void UITask::showPairingPin() { + const unsigned long now = millis(); + _pairing_screen_until = now + BLE_PAIRING_DISPLAY_MILLIS; + _need_refresh = true; + _next_refresh = 0; + if (_display != NULL && !_display->isOn()) { + _display->turnOn(); + } +} + +void UITask::finishPairingScreen(bool timed_out) { + _pairing_screen_until = 0; + _need_refresh = true; + _next_refresh = 0; + if (_display == NULL) return; + + if (timed_out) { + _display->turnOff(); + } else { + _auto_off = millis() + AUTO_OFF_MILLIS; + } +} + void UITask::loop() { + if (_interfaceManager->takePairingRequest()) { + showPairingPin(); + } + + if (_pairing_screen_until != 0) { + const bool timed_out = static_cast(millis() - _pairing_screen_until) >= 0; + if (hasBluetoothConnection() || timed_out) { + finishPairingScreen(timed_out); + } + } + #ifdef PIN_USER_BTN if (_userButton) { _userButton->update(); @@ -486,7 +530,7 @@ void UITask::loop() { _auto_off = millis() + AUTO_OFF_MILLIS; } #endif - if (isDisplayAutoOffDue(_auto_off, AUTO_OFF_MILLIS)) { + if (!isPairingScreenActive() && isDisplayAutoOffDue(_auto_off, AUTO_OFF_MILLIS)) { _display->turnOff(); } } diff --git a/examples/companion_radio/ui-orig/UITask.h b/examples/companion_radio/ui-orig/UITask.h index 045f08a4..db96df2a 100644 --- a/examples/companion_radio/ui-orig/UITask.h +++ b/examples/companion_radio/ui-orig/UITask.h @@ -36,6 +36,7 @@ class UITask : public AbstractUITask { int _msgcount; bool _need_refresh = true; bool _displayWasOn = false; // Track display state before button press + unsigned long _pairing_screen_until; unsigned long ui_started_at; // Button handlers @@ -58,12 +59,16 @@ class UITask : public AbstractUITask { void handleButtonQuadruplePress(); void handleButtonLongPress(); bool shouldPlayMessageTone() const; + bool isPairingScreenActive() const; + void showPairingPin(); + void finishPairingScreen(bool timed_out); public: UITask(mesh::MainBoard* board, MultiSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { _next_refresh = 0; + _pairing_screen_until = 0; ui_started_at = 0; } void begin(DisplayDriver* display, SensorManager* sensors, NodePrefs* node_prefs); diff --git a/examples/companion_radio/ui-tiny/UITask.cpp b/examples/companion_radio/ui-tiny/UITask.cpp index 558f97a0..8a23d8c2 100644 --- a/examples/companion_radio/ui-tiny/UITask.cpp +++ b/examples/companion_radio/ui-tiny/UITask.cpp @@ -11,6 +11,9 @@ #ifndef AUTO_OFF_MILLIS #define AUTO_OFF_MILLIS 15000 // 15 seconds #endif +#ifndef BLE_PAIRING_DISPLAY_MILLIS + #define BLE_PAIRING_DISPLAY_MILLIS 120000UL +#endif #define BOOT_SCREEN_MILLIS 4000 // 4 seconds #ifdef PIN_STATUS_LED @@ -144,6 +147,8 @@ public: : _task(task), _rtc(rtc), _sensors(sensors), _node_prefs(node_prefs), _page(0), _shutdown_init(false), sensors_lpp(200) { } + void showFirstPage() { _page = HomePage::FIRST; } + void poll() override { if (_shutdown_init && !_task->isButtonPressed()) { // must wait for USR button to be released _task->shutdown(); @@ -179,7 +184,7 @@ public: display.setTextSize(1); display.drawTextCentered(display.width() / 2, 54, tmp); #endif - if (_task->hasConnection()) { + if (_task->hasBluetoothConnection()) { display.setColor(UIColor::warning_txt); display.setTextSize(1); display.drawTextCentered(display.width() / 2, display.height()-8, "< Connected >"); @@ -550,6 +555,36 @@ void UITask::setCurrScreen(UIScreen* c) { _next_refresh = 100; } +bool UITask::isPairingScreenActive() const { + return _pairing_screen_until != 0 + && !hasBluetoothConnection() + && static_cast(millis() - _pairing_screen_until) < 0; +} + +void UITask::showPairingPin() { + const unsigned long now = millis(); + _pairing_screen_until = now + BLE_PAIRING_DISPLAY_MILLIS; + static_cast(home)->showFirstPage(); + setCurrScreen(home); + + if (_display != NULL) { + if (!_display->isOn()) _display->turnOn(); + _next_refresh = 0; + } +} + +void UITask::finishPairingScreen(bool timed_out) { + _pairing_screen_until = 0; + _next_refresh = 0; + if (_display == NULL) return; + + if (timed_out) { + _display->turnOff(); + } else { + _auto_off = millis() + AUTO_OFF_MILLIS; + } +} + /* hardware-agnostic pre-shutdown activity should be done here */ @@ -585,6 +620,17 @@ bool UITask::isButtonPressed() const { } void UITask::loop() { + if (_interfaceManager->takePairingRequest()) { + showPairingPin(); + } + + if (_pairing_screen_until != 0) { + const bool timed_out = static_cast(millis() - _pairing_screen_until) >= 0; + if (hasBluetoothConnection() || timed_out) { + finishPairingScreen(timed_out); + } + } + char c = 0; #if UI_HAS_JOYSTICK int ev = user_btn.check(); @@ -657,6 +703,12 @@ void UITask::loop() { } #endif + if (isPairingScreenActive()) { + static_cast(home)->showFirstPage(); + if (curr != home) setCurrScreen(home); + c = 0; + } + if (c != 0 && curr) { curr->handleInput(c); _auto_off = millis() + AUTO_OFF_MILLIS; // extend auto-off timer @@ -693,7 +745,7 @@ void UITask::loop() { } } - if (millis() < _alert_expiry) { // render alert popup + if (!isPairingScreenActive() && millis() < _alert_expiry) { // render alert popup _display->setTextSize(1); int y = _display->height() / 3; int p = _display->height() / 32; @@ -717,7 +769,7 @@ void UITask::loop() { _auto_off = millis() + AUTO_OFF_MILLIS; } #endif - if (isDisplayAutoOffDue(_auto_off, AUTO_OFF_MILLIS)) { + if (!isPairingScreenActive() && isDisplayAutoOffDue(_auto_off, AUTO_OFF_MILLIS)) { _display->turnOff(); } #endif diff --git a/examples/companion_radio/ui-tiny/UITask.h b/examples/companion_radio/ui-tiny/UITask.h index dc689478..7b937d74 100644 --- a/examples/companion_radio/ui-tiny/UITask.h +++ b/examples/companion_radio/ui-tiny/UITask.h @@ -39,6 +39,7 @@ class UITask : public AbstractUITask { NodePrefs* _node_prefs; char _alert[80]; unsigned long _alert_expiry; + unsigned long _pairing_screen_until; int _msgcount; unsigned long ui_started_at, next_batt_chck; int next_backlight_btn_check = 0; @@ -68,12 +69,16 @@ class UITask : public AbstractUITask { char handleTripleClick(char c); void setCurrScreen(UIScreen* c); + bool isPairingScreenActive() const; + void showPairingPin(); + void finishPairingScreen(bool timed_out); public: UITask(mesh::MainBoard* board, MultiSerialInterface* serial) : AbstractUITask(board, serial), _display(NULL), _sensors(NULL) { next_batt_chck = _next_refresh = 0; _cached_batt_mv = 0; + _pairing_screen_until = 0; ui_started_at = 0; curr = NULL; } diff --git a/examples/simple_repeater/MyMesh.cpp b/examples/simple_repeater/MyMesh.cpp index 7cb0a5e7..8ed1fcb5 100644 --- a/examples/simple_repeater/MyMesh.cpp +++ b/examples/simple_repeater/MyMesh.cpp @@ -1,4 +1,5 @@ #include "MyMesh.h" +#include #include #include // for qsort() #include @@ -3023,12 +3024,10 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.adc_multiplier = 0.0f; // 0.0f means use default board multiplier -#if defined(USE_SX1262) || defined(USE_SX1268) -#ifdef SX126X_RX_BOOSTED_GAIN - _prefs.rx_boosted_gain = SX126X_RX_BOOSTED_GAIN; -#else - _prefs.rx_boosted_gain = 1; // enabled by default; -#endif +#if defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_LR1110) \ + || defined(USE_LR2021) || defined(SX126X_RX_BOOSTED_GAIN) \ + || defined(RX_BOOSTED_GAIN) + _prefs.rx_boosted_gain = mesh::radio::configuredRxBoostedGainDefault(); #endif _prefs.radio_fem_rxgain = 1; // LoRa FEM RX gain on by default (FEM boards) diff --git a/examples/simple_room_server/MyMesh.cpp b/examples/simple_room_server/MyMesh.cpp index fed75b37..08406ba8 100644 --- a/examples/simple_room_server/MyMesh.cpp +++ b/examples/simple_room_server/MyMesh.cpp @@ -1,4 +1,5 @@ #include "MyMesh.h" +#include #include #if defined(MESHCORE_ESP32_FULL_PROFILE) #include @@ -1157,12 +1158,10 @@ MyMesh::MyMesh(mesh::MainBoard &board, mesh::Radio &radio, mesh::MillisecondCloc _prefs.gps_interval = 0; _prefs.advert_loc_policy = ADVERT_LOC_PREFS; -#if defined(USE_SX1262) || defined(USE_SX1268) -#ifdef SX126X_RX_BOOSTED_GAIN - _prefs.rx_boosted_gain = SX126X_RX_BOOSTED_GAIN; -#else - _prefs.rx_boosted_gain = 1; // enabled by default; -#endif +#if defined(USE_SX1262) || defined(USE_SX1268) || defined(USE_LR1110) \ + || defined(USE_LR2021) || defined(SX126X_RX_BOOSTED_GAIN) \ + || defined(RX_BOOSTED_GAIN) + _prefs.rx_boosted_gain = mesh::radio::configuredRxBoostedGainDefault(); #endif _prefs.radio_fem_rxgain = 1; diff --git a/src/helpers/MultiSerialInterface.h b/src/helpers/MultiSerialInterface.h index 3a10b9d3..ee371b25 100644 --- a/src/helpers/MultiSerialInterface.h +++ b/src/helpers/MultiSerialInterface.h @@ -90,6 +90,18 @@ public: return false; } + bool isBluetoothConnected() const { + if (!_enabled) return false; + + for (auto iface : _interfaces) { + if (iface.instance && iface.type == InterfaceType::Bluetooth + && iface.instance->isEnabled() && iface.instance->isConnected()) { + return true; + } + } + return false; + } + // enable all interfaces void enable() override { _enabled = true; @@ -181,7 +193,8 @@ public: bool takePairingRequest() override { for (auto iface : _interfaces) { - if (iface.instance && iface.instance->isEnabled() + if (iface.instance && iface.type == InterfaceType::Bluetooth + && iface.instance->isEnabled() && iface.instance->takePairingRequest()) { return true; } diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index 5617f605..310662f3 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -35,6 +35,7 @@ static bool isBondAuthenticationFailure(uint8_t reason) { void SerialBLEInterface::onConnect(uint16_t connection_handle) { BLE_DEBUG_PRINTLN("SerialBLEInterface: connected handle=0x%04X", connection_handle); if (instance) { + instance->_pairingRequestPending.store(false, std::memory_order_release); instance->_conn_handle = connection_handle; instance->_isDeviceConnected = false; instance->_security_timer.start(millis()); @@ -46,6 +47,7 @@ void SerialBLEInterface::onDisconnect(uint16_t connection_handle, uint8_t reason BLE_DEBUG_PRINTLN("SerialBLEInterface: disconnected handle=0x%04X reason=%u", connection_handle, reason); if (instance) { if (instance->_conn_handle == connection_handle) { + instance->_pairingRequestPending.store(false, std::memory_order_release); instance->_conn_handle = BLE_CONN_HANDLE_INVALID; instance->_isDeviceConnected = false; instance->_security_timer.cancel(); @@ -99,9 +101,11 @@ void SerialBLEInterface::onSecured(uint16_t connection_handle) { } bool SerialBLEInterface::onPairingPasskey(uint16_t connection_handle, uint8_t const passkey[6], bool match_request) { - (void)connection_handle; (void)passkey; BLE_DEBUG_PRINTLN("SerialBLEInterface: pairing passkey request match=%d", match_request); + if (instance && instance->isValidConnection(connection_handle)) { + instance->_pairingRequestPending.store(true, std::memory_order_release); + } return true; } @@ -397,6 +401,7 @@ bool SerialBLEInterface::isAdvertising() const { void SerialBLEInterface::enable() { if (_isEnabled) return; + _pairingRequestPending.store(false, std::memory_order_release); _isEnabled = true; clearBuffers(); _last_health_check = millis(); @@ -413,6 +418,7 @@ void SerialBLEInterface::disconnect() { void SerialBLEInterface::disable() { _isEnabled = false; + _pairingRequestPending.store(false, std::memory_order_release); BLE_DEBUG_PRINTLN("SerialBLEInterface: disable"); Bluefruit.Advertising.restartOnDisconnect(false); diff --git a/src/helpers/nrf52/SerialBLEInterface.h b/src/helpers/nrf52/SerialBLEInterface.h index 081b1d5d..cc1ce810 100644 --- a/src/helpers/nrf52/SerialBLEInterface.h +++ b/src/helpers/nrf52/SerialBLEInterface.h @@ -4,6 +4,7 @@ #include "../BleTxStallWatchdog.h" #include "SecuritySessionTimer.h" #include +#include #ifndef BLE_TX_POWER #define BLE_TX_POWER 4 @@ -20,6 +21,7 @@ class SerialBLEInterface : public BaseSerialInterface { ble_gap_addr_t _peer_address = {}; bool _peer_address_valid; bool _bond_removed_for_connection; + std::atomic _pairingRequestPending{false}; SecuritySessionTimer _security_timer; mesh::BleTxStallWatchdog _tx_stall_watchdog; mesh::BleDisconnectRecovery _tx_disconnect_recovery; @@ -83,6 +85,9 @@ public: bool isReadBusy() const override; bool isWriteBusy() const override; bool hasPendingIO() const override; + bool takePairingRequest() override { + return _pairingRequestPending.exchange(false, std::memory_order_acq_rel); + } size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; }; diff --git a/src/helpers/radiolib/LR11x0Reset.h b/src/helpers/radiolib/LR11x0Reset.h index d06ffc53..5c4c62d6 100644 --- a/src/helpers/radiolib/LR11x0Reset.h +++ b/src/helpers/radiolib/LR11x0Reset.h @@ -16,6 +16,8 @@ inline void lr11x0ResetAGC(LR11x0* radio, float freqMHz) { radio->calibrateImageRejection(freqMHz - 4.0f, freqMHz + 4.0f); #ifdef RX_BOOSTED_GAIN + // Fallback for callers without a runtime override. RadioLibWrapper::resetAGC() + // reapplies its cached radio.rxgain selection before receive mode resumes. radio->setRxBoostedGainMode(RX_BOOSTED_GAIN); #endif } diff --git a/src/helpers/radiolib/RxBoostedGainDefaults.h b/src/helpers/radiolib/RxBoostedGainDefaults.h new file mode 100644 index 00000000..5a896c97 --- /dev/null +++ b/src/helpers/radiolib/RxBoostedGainDefaults.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +namespace mesh { +namespace radio { + +// SX126x targets use SX126X_RX_BOOSTED_GAIN while LR11xx and several other +// radios use RX_BOOSTED_GAIN. Keep persisted first-install defaults aligned +// with whichever target-level setting initialized the physical radio. +constexpr uint8_t selectRxBoostedGainDefault(bool sx_configured, + int sx_value, + bool generic_configured, + int generic_value, + uint8_t fallback = 1) { + return sx_configured ? (sx_value ? 1 : 0) + : (generic_configured ? (generic_value ? 1 : 0) + : (fallback ? 1 : 0)); +} + +inline uint8_t configuredRxBoostedGainDefault() { +#ifdef SX126X_RX_BOOSTED_GAIN + return selectRxBoostedGainDefault(true, SX126X_RX_BOOSTED_GAIN, + false, 0); +#elif defined(RX_BOOSTED_GAIN) + return selectRxBoostedGainDefault(false, 0, true, RX_BOOSTED_GAIN); +#else + return selectRxBoostedGainDefault(false, 0, false, 0); +#endif +} + +} // namespace radio +} // namespace mesh diff --git a/src/helpers/radiolib/SX126xReset.h b/src/helpers/radiolib/SX126xReset.h index 39ddb73e..a0ee46e3 100644 --- a/src/helpers/radiolib/SX126xReset.h +++ b/src/helpers/radiolib/SX126xReset.h @@ -26,6 +26,8 @@ inline void sx126xResetAGC(SX126x* radio) { radio->setDio2AsRfSwitch(SX126X_DIO2_AS_RF_SWITCH); #endif #ifdef SX126X_RX_BOOSTED_GAIN + // Fallback for callers without a runtime override. RadioLibWrapper::resetAGC() + // reapplies its cached radio.rxgain selection before receive mode resumes. radio->setRxBoostedGainMode(SX126X_RX_BOOSTED_GAIN); #endif #ifdef SX126X_REGISTER_PATCH diff --git a/test/README.md b/test/README.md index 36aafab0..ffc22d81 100644 --- a/test/README.md +++ b/test/README.md @@ -31,6 +31,7 @@ does not reflect the GoogleTest count -- run the built binary directly | `test_mqtt_topic_router` | `src/helpers/MQTTTopicRouter.h` | complete preset/custom topic-routing contract; MeshRank packets-only behavior; required identifiers; invalid inputs/slots; exact buffer boundaries | | `test_mqtt_connection_policy` | `src/helpers/MQTTConnectionPolicy.h` | reconnect guard/backoff/stagger and breaker transitions; stable reset; JWT lifetime/renewal policy; exact timing boundaries and 32-bit `millis()` rollover | | `test_wifi_reconnect_policy` | `src/helpers/WiFiReconnectPolicy.h` | five-minute forced reconnect cadence, connection resets, duplicate disconnect observations, and 32-bit `millis()` rollover | +| `test_radio_liveness` | `src/helpers/RadioLivenessTracker.h`, `src/helpers/radiolib/RxBoostedGainDefaults.h` | staged radio recovery, activity reset and rollover handling; target-specific RX boosted-gain defaults and SX126x precedence | | `test_lr1110_rx_recovery` | `src/helpers/radiolib/LR1110RxRecovery.h` | LR1110 four-byte RX-buffer shift signature; captured and accumulated shifts; ordinary/scoped packet exclusions | | `test_mqtt_packet_queue_policy` | `src/helpers/MQTTPacketQueuePolicy.h` | queue-full eviction; stale-disconnect flush; adaptive drain limits; bounded QoS0 retries; exact timing boundaries and 32-bit `millis()` rollover | | `test_mqtt_runtime_buffer_lifecycle` | `src/helpers/MQTTRuntimeBufferLifecycle.h` | idempotent allocation/release; partial-allocation degradation; retry of only missing buffers | @@ -43,7 +44,7 @@ does not reflect the GoogleTest count -- run the built binary directly | `test_logical_message_cache` | `src/helpers/LogicalMessageCache.h` | bounded logical-message mapping; stable retry timestamps; exact older retries after newer messages; stale and same-timestamp mismatch rejection | | `test_remote_cli_reply_cache` | `src/helpers/RemoteCliReplyCache.h`, `src/helpers/RemoteCliRequest.h` | authenticated logical-request matching; bounded recent-reply history; backward-compatible retry identity; empty-response completion; on-air truncation and clearing | | `test_companion_frame_queue` | `src/helpers/CompanionFrameQueue.h` | response/required/best-effort classification; reserved capacity; stable priority; safe eviction; message-waiting coalescing | -| `test_serial_mode_switch` | `src/helpers/ArduinoSerialInterface.cpp` | independent terminal/seeder control-sequence recognition across reads and binary-frame boundaries; passthrough ownership of USB input and suppression of binary output | +| `test_serial_mode_switch` | `src/helpers/ArduinoSerialInterface.cpp`, `src/helpers/MultiSerialInterface.h` | independent terminal/seeder control-sequence recognition across reads and binary-frame boundaries; passthrough ownership of USB input and suppression of binary output; Bluetooth-only connection and pairing-request routing | | `test_ble_tx_stall_watchdog` | `src/helpers/BleTxStallWatchdog.h` | exact BLE fragment progress; blocked-reply timeout; rollover-safe elapsed time; disconnect recovery retry and completion | | `test_utils` | `src/Utils.cpp` | `Utils::toHex` (upstream) | diff --git a/test/test_radio_liveness/test_radio_liveness.cpp b/test/test_radio_liveness/test_radio_liveness.cpp index 269cf18c..43ab9b99 100644 --- a/test/test_radio_liveness/test_radio_liveness.cpp +++ b/test/test_radio_liveness/test_radio_liveness.cpp @@ -1,10 +1,28 @@ #include #include +#include using mesh::RadioLivenessTracker; using mesh::RadioRecoveryAction; +TEST(RxBoostedGainDefaults, UsesTargetCompileTimeSetting) { + using mesh::radio::selectRxBoostedGainDefault; + + EXPECT_EQ(selectRxBoostedGainDefault(true, 0, false, 0), 0); + EXPECT_EQ(selectRxBoostedGainDefault(true, 1, false, 0), 1); + EXPECT_EQ(selectRxBoostedGainDefault(false, 0, true, 0), 0); + EXPECT_EQ(selectRxBoostedGainDefault(false, 0, true, 1), 1); + EXPECT_EQ(selectRxBoostedGainDefault(false, 0, false, 0), 1); +} + +TEST(RxBoostedGainDefaults, Sx126xSettingTakesPrecedence) { + using mesh::radio::selectRxBoostedGainDefault; + + EXPECT_EQ(selectRxBoostedGainDefault(true, 0, true, 1), 0); + EXPECT_EQ(selectRxBoostedGainDefault(true, 1, true, 0), 1); +} + TEST(RadioLivenessTracker, StagesSoftThenHardRecovery) { RadioLivenessTracker tracker; tracker.begin(1000); diff --git a/test/test_serial_mode_switch/test_serial_mode_switch.cpp b/test/test_serial_mode_switch/test_serial_mode_switch.cpp index 35933ed7..dd8db604 100644 --- a/test/test_serial_mode_switch/test_serial_mode_switch.cpp +++ b/test/test_serial_mode_switch/test_serial_mode_switch.cpp @@ -4,6 +4,7 @@ #include #include "helpers/ArduinoSerialInterface.h" +#include "helpers/MultiSerialInterface.h" class BufferStream : public Stream { public: @@ -41,6 +42,63 @@ public: static const char START_TOKEN[] = "+++MESHCORE-TERM-START"; static const char SEEDER_TOKEN[] = "ota folder on"; +class FakeSerialInterface : public BaseSerialInterface { +public: + bool enabled = false; + bool connected = false; + bool pairing_request = false; + + void enable() override { enabled = true; } + void disable() override { enabled = false; } + bool isEnabled() const override { return enabled; } + bool isConnected() const override { return connected; } + bool isReadBusy() const override { return false; } + bool isWriteBusy() const override { return false; } + bool takePairingRequest() override { + bool pending = pairing_request; + pairing_request = false; + return pending; + } + size_t writeFrame(const uint8_t[], size_t len) override { return len; } + size_t checkRecvFrame(uint8_t[]) override { return 0; } +}; + +TEST(MultiSerialInterface, TracksBluetoothConnectionSeparately) { + MultiSerialInterface manager; + FakeSerialInterface usb; + FakeSerialInterface bluetooth; + usb.connected = true; + + ASSERT_TRUE(manager.addInterface(InterfaceType::USB, &usb)); + ASSERT_TRUE(manager.addInterface(InterfaceType::Bluetooth, &bluetooth)); + manager.enable(); + + EXPECT_TRUE(manager.isConnected()); + EXPECT_FALSE(manager.isBluetoothConnected()); + + bluetooth.connected = true; + EXPECT_TRUE(manager.isBluetoothConnected()); + + manager.disableBluetooth(); + EXPECT_FALSE(manager.isBluetoothConnected()); +} + +TEST(MultiSerialInterface, PairingRequestsComeOnlyFromBluetooth) { + MultiSerialInterface manager; + FakeSerialInterface usb; + FakeSerialInterface bluetooth; + ASSERT_TRUE(manager.addInterface(InterfaceType::USB, &usb)); + ASSERT_TRUE(manager.addInterface(InterfaceType::Bluetooth, &bluetooth)); + manager.enable(); + + usb.pairing_request = true; + EXPECT_FALSE(manager.takePairingRequest()); + + bluetooth.pairing_request = true; + EXPECT_TRUE(manager.takePairingRequest()); + EXPECT_FALSE(manager.takePairingRequest()); +} + TEST(SerialModeSwitch, RecognizesControlSequenceAcrossReads) { BufferStream stream; ArduinoSerialInterface interface;