From 439dc7da3cc0187e27c71dc4f5ba9c0ade20fc9c Mon Sep 17 00:00:00 2001 From: mikecarper Date: Tue, 15 Sep 2026 21:08:10 -0700 Subject: [PATCH] Complete upstream transport integration and regression coverage --- .github/workflows/run-unit-tests.yml | 1 + examples/companion_radio/MyMesh.cpp | 14 +++++ examples/companion_radio/main.cpp | 2 +- platformio.ini | 3 + src/helpers/rp2040/SerialBLEInterface.cpp | 11 ++++ src/helpers/rp2040/SerialBLEInterface.h | 4 +- test/test_bluetooth_mac_contract.py | 17 ++++++ test/test_indicator_render_profile.py | 4 +- test/test_mqtt_presets/test_mqtt_presets.cpp | 11 ++++ test/test_pico_ble_queues.py | 60 ++++++++++++++++++++ 10 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 test/test_pico_ble_queues.py diff --git a/.github/workflows/run-unit-tests.yml b/.github/workflows/run-unit-tests.yml index fbb5cc6f..c9e6741b 100644 --- a/.github/workflows/run-unit-tests.yml +++ b/.github/workflows/run-unit-tests.yml @@ -82,6 +82,7 @@ jobs: sudo apt-get install -y libssl-dev python3 -B test/test_companion_tx_routing.py -v python3 -B test/test_companion_discovery.py -v + python3 -B test/test_pico_ble_queues.py -v python3 -B test/test_companion_preferences_transaction.py -v python3 -B test/test_companion_radio_settings_transaction.py -v python3 -B test/test_companion_prefs_transactions.py -v diff --git a/examples/companion_radio/MyMesh.cpp b/examples/companion_radio/MyMesh.cpp index 69e9b0b3..4d8e1b58 100644 --- a/examples/companion_radio/MyMesh.cpp +++ b/examples/companion_radio/MyMesh.cpp @@ -6841,6 +6841,13 @@ bool MyMesh::applyAndSaveBluetoothMac(const char* value, char* reply, mode = mesh::companion::BLUETOOTH_MAC_CUSTOM; } +#if defined(RP2040_PLATFORM) + if (mode != mesh::companion::BLUETOOTH_MAC_DEFAULT) { + snprintf(reply, reply_size, "Error: Bluetooth address overrides are not supported on Pico W"); + return false; + } +#endif + if (!saveBluetoothMac(mode, address)) { snprintf(reply, reply_size, "Error: Bluetooth MAC save failed"); return false; @@ -6913,6 +6920,13 @@ bool MyMesh::applyAndSaveBluetoothStealth(const char* value, char* reply, return false; } +#if defined(RP2040_PLATFORM) + if (strcmp(value, "on") == 0) { + snprintf(reply, reply_size, "Error: Bluetooth stealth is not supported on Pico W"); + return false; + } +#endif + const uint8_t previous_mode = _prefs.bluetooth_stealth_mode; const uint8_t previous_type = _prefs.bluetooth_stealth_peer_type; uint8_t previous_peer[mesh::companion::BLUETOOTH_MAC_BYTES]; diff --git a/examples/companion_radio/main.cpp b/examples/companion_radio/main.cpp index a8a99eea..c31ee5ad 100644 --- a/examples/companion_radio/main.cpp +++ b/examples/companion_radio/main.cpp @@ -2799,7 +2799,7 @@ void setup() { #endif // Load WiFi credentials without starting the radio. -#ifdef WIFI_SSID +#if defined(ESP32) && defined(WIFI_SSID) loadCompanionWiFiCredentials(); #endif diff --git a/platformio.ini b/platformio.ini index 64cad5a0..7afe500c 100644 --- a/platformio.ini +++ b/platformio.ini @@ -112,6 +112,9 @@ lib_deps = ${arduino_base.lib_deps} ricmoo/QRCode @ 0.0.1 build_src_filter = ${arduino_base.build_src_filter} + + ; Shared WiFi transport moved out of esp32/ upstream. Keep it available to + ; this fork's WiFi/MQTT and synthesized Full profiles as well. + + + + + diff --git a/src/helpers/rp2040/SerialBLEInterface.cpp b/src/helpers/rp2040/SerialBLEInterface.cpp index ef6f609a..ade4bbc1 100644 --- a/src/helpers/rp2040/SerialBLEInterface.cpp +++ b/src/helpers/rp2040/SerialBLEInterface.cpp @@ -172,9 +172,20 @@ bool SerialBLEInterface::isConnected() const { } bool SerialBLEInterface::isWriteBusy() const { + BluetoothLock lock; return send_queue_len >= (FRAME_QUEUE_SIZE * 2 / 3); } +bool SerialBLEInterface::isReadBusy() const { + BluetoothLock lock; + return recv_queue_len > 0; +} + +bool SerialBLEInterface::hasPendingIO() const { + BluetoothLock lock; + return recv_queue_len > 0 || send_queue_len > 0 || _tx_pending; +} + size_t SerialBLEInterface::writeFrame(const uint8_t src[], size_t len) { if (len == 0 || len > MAX_FRAME_SIZE) { BLE_DEBUG_PRINTLN("writeFrame(), frame too big, len=%u", (unsigned)len); diff --git a/src/helpers/rp2040/SerialBLEInterface.h b/src/helpers/rp2040/SerialBLEInterface.h index 80bdfbe1..c85f8cd9 100644 --- a/src/helpers/rp2040/SerialBLEInterface.h +++ b/src/helpers/rp2040/SerialBLEInterface.h @@ -51,7 +51,7 @@ public: /** * init the BLE interface. * @param prefix a prefix for the device name - * @param name IN/OUT - a name for the device (combined with prefix). If "@@MAC", is modified and returned + * @param name a name for the device (combined with prefix), or "@@MAC" * @param pin_code the BLE security pin */ bool begin(const char* prefix, const char* name, uint32_t pin_code, @@ -70,7 +70,9 @@ public: void disable() override; bool isEnabled() const override { return _isEnabled; } bool isConnected() const override; + bool isReadBusy() const override; bool isWriteBusy() const override; + bool hasPendingIO() const override; size_t writeFrame(const uint8_t src[], size_t len) override; size_t checkRecvFrame(uint8_t dest[]) override; }; diff --git a/test/test_bluetooth_mac_contract.py b/test/test_bluetooth_mac_contract.py index 93891d5e..a6be736f 100644 --- a/test/test_bluetooth_mac_contract.py +++ b/test/test_bluetooth_mac_contract.py @@ -41,6 +41,23 @@ class BluetoothMacContractTests(unittest.TestCase): self.nrf_source, ) + def test_pico_rejects_unsupported_identity_policies_before_saving(self): + pico = source("src/helpers/rp2040/SerialBLEInterface.cpp") + self.assertIn( + "if (custom_address || clear_bonds || stealth_pair_once || bonded_only_peer) return false;", + pico, + ) + mac = self.mesh[self.mesh.index("bool MyMesh::applyAndSaveBluetoothMac("): + self.mesh.index("void MyMesh::formatBluetoothMacStatus(")] + self.assertLess(mac.index("#if defined(RP2040_PLATFORM)"), + mac.index("saveBluetoothMac(mode, address)")) + self.assertIn("mode != mesh::companion::BLUETOOTH_MAC_DEFAULT", mac) + stealth = self.mesh[self.mesh.index("bool MyMesh::applyAndSaveBluetoothStealth("): + self.mesh.index("void MyMesh::formatBluetoothStealthStatus(")] + self.assertLess(stealth.index("#if defined(RP2040_PLATFORM)"), + stealth.index("setCompanionBluetoothStealth(_prefs,")) + self.assertIn('if (strcmp(value, "on") == 0)', stealth) + def test_esp32_supports_nimble_and_bluedroid(self): self.assertIn("BLEDevice::setOwnAddr(native_address)", self.esp_source) self.assertIn("BLEDevice::setOwnAddrType(BLE_OWN_ADDR_RANDOM)", self.esp_source) diff --git a/test/test_indicator_render_profile.py b/test/test_indicator_render_profile.py index 6ac8cd17..edcf95a5 100644 --- a/test/test_indicator_render_profile.py +++ b/test/test_indicator_render_profile.py @@ -191,7 +191,9 @@ int main() { enum_start = ui.index(" enum HomePage {") enum = ui[enum_start : ui.index("\n };", enum_start)] self.assertIn( - "#if UI_WIFI_SETUP_HOME_PAGE == 1\n WIFI_SETUP,\n#endif\n Count", + "#if UI_WIFI_SETUP_HOME_PAGE == 1\n WIFI_SETUP,\n#endif\n" + "#if !(defined(UI_NO_DISCOVER_SCREEN) && (UI_NO_DISCOVER_SCREEN + 0 != 0))\n" + " DISCOVERY,\n#endif\n Count", enum, ) self.assertIn("_page == HomePage::WIFI_SETUP", ui) diff --git a/test/test_mqtt_presets/test_mqtt_presets.cpp b/test/test_mqtt_presets/test_mqtt_presets.cpp index 1750696b..799d088b 100644 --- a/test/test_mqtt_presets/test_mqtt_presets.cpp +++ b/test/test_mqtt_presets/test_mqtt_presets.cpp @@ -156,6 +156,17 @@ TEST(MQTTPresets, WcmeshIsJwtWithIsrgRootX1) { EXPECT_FALSE(mqttPresetUsesDevicePubkeyUsername(p)); } +TEST(MQTTPresets, BsmeshIsJwtWithIsrgRootX1) { + const MQTTPresetDef* p = findMQTTPreset("bsmesh"); + ASSERT_NE(nullptr, p); + EXPECT_EQ(MQTT_AUTH_JWT, p->auth_type); + EXPECT_EQ(MQTT_TOPIC_MESHCORE, p->topic_style); + EXPECT_STREQ("wss://mqtt.bsmesh.de:8885", p->server_url); + EXPECT_STREQ("mqtt.bsmesh.de", p->jwt_audience); + EXPECT_EQ(ISRG_ROOT_X1, p->ca_cert); + EXPECT_FALSE(mqttPresetNeedsSlotCredentials(p)); +} + // ---- slot count constants ------------------------------------------------- TEST(MQTTPresets, SlotCountsAreSane) { diff --git a/test/test_pico_ble_queues.py b/test/test_pico_ble_queues.py new file mode 100644 index 00000000..a80fd47c --- /dev/null +++ b/test/test_pico_ble_queues.py @@ -0,0 +1,60 @@ +"""Execute Pico W's production queue predicates used by companion sleep control.""" +from pathlib import Path +import subprocess +import tempfile +import unittest +from test_radio_receive_contract import method + +ROOT = Path(__file__).resolve().parents[1] + + +class PicoBleQueueTests(unittest.TestCase): + def test_pending_frames_keep_the_transport_awake_below_high_water(self): + source = (ROOT / 'src/helpers/rp2040/SerialBLEInterface.cpp').read_text() + methods = '\n'.join(method(source, 'bool SerialBLEInterface::' + name + '() const') + for name in ('isReadBusy', 'isWriteBusy', 'hasPendingIO')) + code = r''' +#include +#define FRAME_QUEUE_SIZE 8 +static int locks = 0; +struct BluetoothLock { + BluetoothLock() { ++locks; } + ~BluetoothLock() { --locks; } +}; +struct SerialBLEInterface { + unsigned recv_queue_len = 0, send_queue_len = 0; + bool _tx_pending = false; + bool isReadBusy() const; + bool isWriteBusy() const; + bool hasPendingIO() const; +}; +@METHODS@ +int main() { + SerialBLEInterface transport; + for (unsigned rx = 0; rx <= FRAME_QUEUE_SIZE; ++rx) { + for (unsigned tx = 0; tx <= FRAME_QUEUE_SIZE; ++tx) { + for (unsigned pending = 0; pending <= 1; ++pending) { + transport.recv_queue_len = rx; + transport.send_queue_len = tx; + transport._tx_pending = pending; + assert(transport.isReadBusy() == (rx != 0)); + assert(transport.isWriteBusy() == (tx >= 5)); + assert(transport.hasPendingIO() == (rx || tx || pending)); + assert(locks == 0); + } + } + } +} +'''.replace('@METHODS@', methods) + for name in ('isReadBusy', 'isWriteBusy', 'hasPendingIO'): + self.assertIn('BluetoothLock lock;', method(source, 'bool SerialBLEInterface::' + name)) + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) + (path / 'test.cpp').write_text(code) + subprocess.run(['g++', '-std=c++17', '-fsanitize=address,undefined', + '-fno-pie', '-no-pie', str(path / 'test.cpp'), '-o', str(path / 'test')], check=True) + subprocess.run([str(path / 'test')], check=True) + + +if __name__ == '__main__': + unittest.main()