Add opt-in NimBLE Full Companion trials for V4 and XIAO S3

This commit is contained in:
mikecarper
2026-09-08 09:30:31 -07:00
parent 7ff11a2d64
commit 7f1bd8b1d2
11 changed files with 789 additions and 27 deletions
+111
View File
@@ -0,0 +1,111 @@
# ESP32-S3 NimBLE Full Companion trial
These optional builds use NimBLE-Arduino 2.5.1 with the existing ESP32 Arduino
2.0.17 toolchain. They are hardware qualification builds, outside the normal
release matrix and firmware picker.
| Hardware | Trial environment | Contacts | Channels | Offline queue |
| --- | --- | ---: | ---: | ---: |
| Heltec V4.2/V4.3 OLED, FEM on | `heltec_v4_2_v4_3_companion_radio_full_femon_nimble` | 350 | 40 | 512 |
| XIAO ESP32-S3 with WIO SX1262 | `Xiao_S3_WIO_companion_radio_full_nimble` | 350 | 40 | 256 |
The XIAO profile uses WIO radio pins CS 41, DIO1 39, BUSY 40 and RESET 42.
It is not the generic XIAO profile for separately wired radio modules. Both
boards retain their parent Full Companion features, USB mOTA sender, WiFi
update slots, and PSRAM-backed offline queue. The V4 also retains direct MQTT.
## Build
Enable the optional configuration in the ignored `platformio.local.ini`:
```ini
[platformio]
extra_configs =
variants/*/platformio.ini
platformio.nimble.ini
```
If that file already contains local settings, merge this list with them. Run
one PlatformIO/build.sh command at a time. Both commands below use USA Cascade:
```sh
OUTPUT_DIR=.releases/nimble-v4 bash build.sh build-firmware \
heltec_v4_2_v4_3_companion_radio_full_femon_nimble \
--firmware-version v1.17.1.5-halo-keymind-cascade-nimble-test \
--radio-preset usa-cascadia --profile cascade --standard --require-ota
OUTPUT_DIR=.releases/nimble-xiao bash build.sh build-firmware \
Xiao_S3_WIO_companion_radio_full_nimble \
--firmware-version v1.17.1.5-halo-keymind-cascade-nimble-test \
--radio-preset usa-cascadia --profile cascade --standard --require-ota
```
Use the merged image at address 0 for a USB installation. The V4 uses 16 MB
flash; the XIAO uses 8 MB. Both use DIO flash mode. Use the application-only
`.bin` for an existing, matching WiFi OTA layout. Remove the optional config
entry when finished to return to the ordinary release matrix.
## Bluetooth identity and pairing
All existing MAC settings remain available from the ASCII USB terminal and
the app's authenticated local CLI. A setting takes effect after reboot.
| Command | Behavior |
| --- | --- |
| `get bluetooth.mac` | Show the saved MAC policy. |
| `set bluetooth.mac C2:17:15:04:00:01` | Use this custom random-static address. |
| `set bluetooth.mac random` | Generate one address and retain it across boots. |
| `set bluetooth.mac random-every-boot` | Generate a new address on each boot. |
| `set bluetooth.mac random-after-connect` | Rotate on the next boot after an authenticated connection; otherwise retain the address. |
| `set bluetooth.mac default` | Restore the factory Bluetooth address. |
| `set bluetooth.stealth on` | Pair once, then accept the saved bonded peer. |
| `set bluetooth.stealth off` | Restore ordinary discovery without changing the MAC policy. |
| `get bluetooth.stealth` | Show pairing or bonded-peer-only state. |
The `ble.mac` and `ble.stealth` aliases also work. A literal custom address must
be a valid BLE random-static address (first byte C0-FF). Forget the old entry
in the phone's Bluetooth settings and pair again after an address change or
switching Bluetooth libraries. Existing Bluedroid bonds are not migrated.
Stealth does not disable the selected rotation policy. If rotation changes
the address on reboot, first pairing opens again for the new identity.
Pairing requires encryption, PIN authentication and a stored bond. NimBLE's
controller byte order is converted explicitly, so a custom address appears
over the air in the order entered. Failed identity setup stops advertising
and retries initialization instead of advertising the wrong address.
## V4 WiFi setup QR
The active WiFi setup page displays a compact QR code with a one-pixel white
border. The normal open setup network fits a 44 x 44 pixel code, with 2 x 2
pixel modules. The SSID, portal address and `HOLD STOP` hint remain beside it.
Short-click to leave the page; hold the button on this page to start or stop
the setup AP. From USB, `start webconfig ap` starts the same setup session.
## Regression checks
```sh
python3 -B test/test_nimble_companion.py
python3 -B test/test_bluetooth_mac_contract.py
python3 -B test/test_bluetooth_pairing_ui.py
python3 -B test/test_heltec_v4_wifi_setup_page.py
python3 -B test/test_firmware_ram.py
python3 -B test/test_esp32_dram.py
python3 -B test/test_shared_mota_queue.py
pio test -e native -f test_companion_node_prefs \
-f test_ble_tx_stall_watchdog -f test_companion_frame_queue \
-f test_display_driver
```
The adapter test compiles the actual transport under address/undefined-behavior
sanitizers with API doubles. It injects allocation, identity and bond failures;
checks PIN authentication and stealth; and exercises 176-byte frames, MTU and
notification subscription gating, retry and duplicate prevention. Embedded
builds compile against the pinned real library. RAM reports retain the existing
wireless budget; linked headroom is not a measurement of live free heap.
Hardware qualification must additionally verify pairing and reconnection,
advertised addresses for every MAC policy, contact synchronization, WiFi/BLE
coexistence, and uptime under load. BlueZ results do not establish iOS or
Android camera/pairing compatibility.
+30
View File
@@ -0,0 +1,30 @@
; Opt-in hardware qualification builds. This file is not part of the normal
; release matrix. See docs/nimble_companion_trial.md for activation and tests.
[companion_nimble_trial]
build_flags =
-D MESH_USE_NIMBLE_ARDUINO=1
-D CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1
-D CONFIG_BT_NIMBLE_ROLE_CENTRAL_DISABLED=1
-D CONFIG_BT_NIMBLE_ROLE_OBSERVER_DISABLED=1
lib_deps = h2zero/NimBLE-Arduino @ 2.5.1
lib_ignore = ESP32 BLE Arduino
[env:heltec_v4_2_v4_3_companion_radio_full_femon_nimble]
extends = env:heltec_v4_2_v4_3_companion_radio_full_femon
build_flags =
${env:heltec_v4_2_v4_3_companion_radio_full_femon.build_flags}
${companion_nimble_trial.build_flags}
lib_deps =
${env:heltec_v4_companion_radio_wifi_mqtt_femon.lib_deps}
${companion_nimble_trial.lib_deps}
lib_ignore = ${companion_nimble_trial.lib_ignore}
[env:Xiao_S3_WIO_companion_radio_full_nimble]
extends = env:Xiao_S3_WIO_companion_radio_full
build_flags =
${env:Xiao_S3_WIO_companion_radio_full.build_flags}
${companion_nimble_trial.build_flags}
lib_deps =
${env:Xiao_S3_WIO_companion_radio_wifi.lib_deps}
${companion_nimble_trial.lib_deps}
lib_ignore = ${companion_nimble_trial.lib_ignore}
+173 -22
View File
@@ -3,10 +3,10 @@
#include "../CompanionFrameQueue.h"
#include "esp_mac.h"
#include <stdlib.h>
#if defined(CONFIG_BLUEDROID_ENABLED)
#if defined(CONFIG_BLUEDROID_ENABLED) && !defined(MESH_USE_NIMBLE_ARDUINO)
#include "esp_gap_ble_api.h"
#endif
#if defined(CONFIG_NIMBLE_ENABLED)
#if MESH_BLE_USES_NIMBLE && !defined(MESH_USE_NIMBLE_ARDUINO)
#include <host/ble_store.h>
#endif
@@ -29,8 +29,17 @@ extern "C" bool bleInUse(void) {
#define ADVERT_RESTART_DELAY 1000 // millis
#define BLE_BOND_PERSIST_TIMEOUT_MS 15000
static void cleanupFailedBluetoothStart() {
#if defined(MESH_USE_NIMBLE_ARDUINO)
BLEDevice::deinit(true);
#else
BLEDevice::deinit(false);
#endif
}
#if !defined(MESH_USE_NIMBLE_ARDUINO)
static void clearStoredBluetoothBonds() {
#if defined(CONFIG_NIMBLE_ENABLED)
#if MESH_BLE_USES_NIMBLE
const int result = ble_store_clear();
if (result != 0) {
BLE_DEBUG_PRINTLN("Could not clear NimBLE bonds: %d", result);
@@ -59,6 +68,8 @@ static void clearStoredBluetoothBonds() {
#endif
}
#endif
static bool bluetoothPeersEqual(
const mesh::companion::BluetoothPeerIdentity& lhs,
const mesh::companion::BluetoothPeerIdentity& rhs) {
@@ -81,7 +92,7 @@ void SerialBLEInterface::noteSuccessfulConnection(
}
}
#if defined(CONFIG_NIMBLE_ENABLED)
#if MESH_BLE_USES_NIMBLE
static void bluetoothPeerFromNimbleAddress(
const ble_addr_t& address,
mesh::companion::BluetoothPeerIdentity& peer) {
@@ -110,6 +121,9 @@ static ble_addr_t nimbleAddressFromBluetoothPeer(
static bool nimbleBondExistsForPeer(
const mesh::companion::BluetoothPeerIdentity& peer) {
#if defined(MESH_USE_NIMBLE_ARDUINO)
return BLEDevice::isBonded(BLEAddress(nimbleAddressFromBluetoothPeer(peer)));
#else
ble_addr_t bonded_peers[8];
int count = 0;
if (ble_store_util_bonded_peers(
@@ -123,6 +137,7 @@ static bool nimbleBondExistsForPeer(
if (bluetoothPeersEqual(candidate, peer)) return true;
}
return false;
#endif
}
#else
static void bluetoothPeerFromBluedroidBond(
@@ -181,7 +196,7 @@ static bool findBluedroidBondedPeer(
bool SerialBLEInterface::resolveSuccessfulPeer(
mesh::companion::BluetoothPeerIdentity& peer) const {
#if defined(CONFIG_NIMBLE_ENABLED)
#if MESH_BLE_USES_NIMBLE
if (!mesh::companion::isValidBluetoothPeerIdentity(_successfulPeer)
|| !nimbleBondExistsForPeer(_successfulPeer)) {
return false;
@@ -204,7 +219,7 @@ bool SerialBLEInterface::configureBondedOnlyAdvertising(
return false;
}
#if defined(CONFIG_NIMBLE_ENABLED)
#if MESH_BLE_USES_NIMBLE
if (require_stored_bond && !nimbleBondExistsForPeer(peer)) {
requestBondedOnlyRecovery("saved peer bond is unavailable");
return false;
@@ -250,8 +265,13 @@ bool SerialBLEInterface::configureBondedOnlyAdvertising(
}
advertising->stop();
BLEAdvertisementData minimal_advertisement;
#if defined(MESH_USE_NIMBLE_ARDUINO)
minimal_advertisement.setFlags(BLE_HS_ADV_F_BREDR_UNSUP);
advertising->enableScanResponse(false);
#else
minimal_advertisement.setFlags(ESP_BLE_ADV_FLAG_BREDR_NOT_SPT);
advertising->setScanResponse(false);
#endif
advertising->setScanFilter(true, true);
advertising->setAdvertisementData(minimal_advertisement);
@@ -325,7 +345,7 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name,
}
// Create the BLE Device
#if defined(CONFIG_NIMBLE_ENABLED)
#if MESH_BLE_USES_NIMBLE
if (!BLEDevice::init(dev_name)) {
BLE_DEBUG_PRINTLN("BLEDevice::init failed");
return false;
@@ -336,9 +356,17 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name,
BLEDevice::init(dev_name);
#endif
#if defined(MESH_USE_NIMBLE_ARDUINO)
if (clear_bonds && !BLEDevice::deleteAllBonds()) {
BLE_DEBUG_PRINTLN("Could not clear NimBLE bonds for the new identity");
cleanupFailedBluetoothStart();
return false;
}
#else
if (clear_bonds) clearStoredBluetoothBonds();
#endif
#if defined(CONFIG_NIMBLE_ENABLED)
#if MESH_BLE_USES_NIMBLE
if (custom_address != nullptr) {
uint8_t native_address[mesh::companion::BLUETOOTH_MAC_BYTES];
for (size_t i = 0; i < mesh::companion::BLUETOOTH_MAC_BYTES; i++) {
@@ -348,19 +376,32 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name,
if (!BLEDevice::setOwnAddr(native_address)
|| !BLEDevice::setOwnAddrType(BLE_OWN_ADDR_RANDOM)) {
BLE_DEBUG_PRINTLN("Custom Bluetooth MAC setup failed");
BLEDevice::deinit(false);
cleanupFailedBluetoothStart();
return false;
}
}
#if defined(MESH_USE_NIMBLE_ARDUINO)
else if (!BLEDevice::setOwnAddrType(BLE_OWN_ADDR_PUBLIC)) {
cleanupFailedBluetoothStart();
return false;
}
#endif
#endif
#if !defined(MESH_USE_NIMBLE_ARDUINO)
BLEDevice::setSecurityCallbacks(this);
#endif
// ATT notifications consume three bytes of the negotiated MTU. Reserve
// that overhead so a MAX_FRAME_SIZE protocol frame fits without truncation.
BLEDevice::setMTU(MAX_FRAME_SIZE + 3);
#if defined(MESH_USE_NIMBLE_ARDUINO)
BLEDevice::setSecurityPasskey(pin_code);
BLEDevice::setSecurityIOCap(BLE_HS_IO_DISPLAY_ONLY);
BLEDevice::setSecurityAuth(true, true, true);
#else
BLESecurity sec;
#if defined(CONFIG_NIMBLE_ENABLED)
#if MESH_BLE_USES_NIMBLE
sec.setPassKey(true, pin_code);
// A passkey alone does not provide MITM protection when the controller's
// default capability is NoInputNoOutput: the peers can silently fall back
@@ -373,6 +414,7 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name,
sec.setStaticPIN(pin_code);
sec.setCapability(ESP_IO_CAP_OUT);
sec.setAuthenticationMode(ESP_LE_AUTH_REQ_SC_MITM_BOND);
#endif
#endif
//BLEDevice::setPower(ESP_PWR_LVL_N8);
@@ -381,12 +423,18 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name,
pServer = BLEDevice::createServer();
if (pServer == NULL) {
BLE_DEBUG_PRINTLN("BLEDevice::createServer failed");
BLEDevice::deinit(false);
cleanupFailedBluetoothStart();
return false;
}
#if defined(MESH_USE_NIMBLE_ARDUINO)
// The interface is owned by the application, not by the NimBLE server.
pServer->setCallbacks(this, false);
pServer->advertiseOnDisconnect(false);
#else
pServer->setCallbacks(this);
#endif
#if !defined(CONFIG_NIMBLE_ENABLED)
#if !MESH_BLE_USES_NIMBLE
if (custom_address != nullptr) {
esp_bd_addr_t native_address;
memcpy(native_address, custom_address, sizeof(native_address));
@@ -402,33 +450,41 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name,
pService = pServer->createService(SERVICE_UUID);
if (pService == NULL) {
BLE_DEBUG_PRINTLN("BLEServer::createService failed");
BLEDevice::deinit(false);
cleanupFailedBluetoothStart();
pServer = NULL;
return false;
}
// Create a BLE Characteristic
#if defined(MESH_USE_NIMBLE_ARDUINO)
uint32_t tx_properties = NIMBLE_PROPERTY::READ | NIMBLE_PROPERTY::NOTIFY |
NIMBLE_PROPERTY::READ_AUTHEN;
uint32_t rx_properties = NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_AUTHEN;
#else
uint32_t tx_properties = BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_NOTIFY;
uint32_t rx_properties = BLECharacteristic::PROPERTY_WRITE;
#if defined(CONFIG_NIMBLE_ENABLED)
#if MESH_BLE_USES_NIMBLE
// NimBLE ignores setAccessPermissions(). Authentication requirements must
// be part of the characteristic properties instead.
tx_properties |= BLECharacteristic::PROPERTY_READ_AUTHEN;
rx_properties |= BLECharacteristic::PROPERTY_WRITE_AUTHEN;
#endif
#endif
pTxCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID_TX, tx_properties);
if (pTxCharacteristic == NULL) {
BLE_DEBUG_PRINTLN("BLEService::createCharacteristic(TX) failed");
BLEDevice::deinit(false);
cleanupFailedBluetoothStart();
pServer = NULL;
pService = NULL;
return false;
}
#if !defined(MESH_USE_NIMBLE_ARDUINO)
pTxCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ_ENC_MITM);
#endif
pTxCharacteristic->setCallbacks(this);
#if !defined(CONFIG_NIMBLE_ENABLED)
#if !MESH_BLE_USES_NIMBLE
// NimBLE creates and owns the 0x2902 descriptor automatically.
pTxDescriptor = new BLE2902();
// Make notification setup start/finish pairing before the client begins its
@@ -444,16 +500,24 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name,
CHARACTERISTIC_UUID_RX, rx_properties);
if (pRxCharacteristic == NULL) {
BLE_DEBUG_PRINTLN("BLEService::createCharacteristic(RX) failed");
BLEDevice::deinit(false);
cleanupFailedBluetoothStart();
pServer = NULL;
pService = NULL;
pTxCharacteristic = NULL;
#if !defined(MESH_USE_NIMBLE_ARDUINO)
pTxDescriptor = NULL;
#endif
return false;
}
#if !defined(MESH_USE_NIMBLE_ARDUINO)
pRxCharacteristic->setAccessPermissions(ESP_GATT_PERM_WRITE_ENC_MITM);
#endif
pRxCharacteristic->setCallbacks(this);
#if defined(MESH_USE_NIMBLE_ARDUINO)
pServer->getAdvertising()->setName(dev_name);
pServer->getAdvertising()->enableScanResponse(true);
#endif
pServer->getAdvertising()->addServiceUUID(SERVICE_UUID);
if (bonded_only_peer != nullptr) {
configureBondedOnlyAdvertising(*bonded_only_peer, true);
@@ -463,6 +527,27 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name,
// -------- BLESecurityCallbacks methods
#if defined(MESH_USE_NIMBLE_ARDUINO)
uint32_t SerialBLEInterface::onPassKeyDisplay() {
_pairingRequestPending.store(true, std::memory_order_release);
return _pin_code;
}
void SerialBLEInterface::onAuthenticationComplete(NimBLEConnInfo& info) {
if (info.isEncrypted() && info.isAuthenticated() && info.isBonded()) {
deviceConnected = true;
mesh::companion::BluetoothPeerIdentity peer;
bluetoothPeerFromNimbleAddress(*info.getIdAddress().getBase(), peer);
noteSuccessfulConnection(peer);
} else {
BLEDevice::deleteBond(info.getIdAddress());
deviceConnected = false;
if (_bonded_only) requestBondedOnlyRecovery("bond authentication failed");
pServer->disconnect(info.getConnHandle());
if (advertisingAllowed()) scheduleAdvertisingRestart((uint32_t)millis());
}
}
#else
uint32_t SerialBLEInterface::onPassKeyRequest() {
BLE_DEBUG_PRINTLN("onPassKeyRequest()");
_pairingRequestPending.store(true, std::memory_order_release);
@@ -485,7 +570,7 @@ bool SerialBLEInterface::onSecurityRequest() {
return true; // allow
}
#if defined(CONFIG_NIMBLE_ENABLED)
#if MESH_BLE_USES_NIMBLE
void SerialBLEInterface::onAuthenticationComplete(ble_gap_conn_desc* desc) {
const bool success = desc != NULL && desc->sec_state.encrypted &&
desc->sec_state.authenticated
@@ -558,12 +643,31 @@ void SerialBLEInterface::onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl) {
}
#endif
#endif
// -------- BLEServerCallbacks methods
#if defined(MESH_USE_NIMBLE_ARDUINO)
void SerialBLEInterface::onConnect(BLEServer* pServer, NimBLEConnInfo& info) {
last_conn_id = info.getConnHandle();
deviceConnected = false;
oldDeviceConnected = false;
notifySucceeded = false;
notificationsEnabled.store(false, std::memory_order_release);
xQueueReset(recv_queue);
_tx_reset_pending.store(true, std::memory_order_release);
_adv_restart_pending = false;
}
void SerialBLEInterface::onMTUChange(uint16_t mtu, NimBLEConnInfo& info) {
(void)info;
BLE_DEBUG_PRINTLN("onMtuChanged(), mtu=%d", mtu);
}
#else
void SerialBLEInterface::onConnect(BLEServer* pServer) {
}
#if defined(CONFIG_NIMBLE_ENABLED)
#if MESH_BLE_USES_NIMBLE
void SerialBLEInterface::onConnect(BLEServer* pServer, ble_gap_conn_desc* desc) {
BLE_DEBUG_PRINTLN("onConnect(), conn_id=%d, mtu=%d", desc->conn_handle,
pServer->getPeerMTU(desc->conn_handle));
@@ -597,7 +701,9 @@ void SerialBLEInterface::onConnect(BLEServer* pServer, esp_ble_gatts_cb_param_t
xQueueReset(recv_queue);
_tx_reset_pending.store(true, std::memory_order_release);
_adv_restart_pending = false;
#if !defined(MESH_USE_NIMBLE_ARDUINO)
if (pTxDescriptor != NULL) pTxDescriptor->setNotifications(false);
#endif
}
void SerialBLEInterface::onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param_t* param) {
@@ -605,14 +711,25 @@ void SerialBLEInterface::onMtuChanged(BLEServer* pServer, esp_ble_gatts_cb_param
}
#endif
#endif
#if defined(MESH_USE_NIMBLE_ARDUINO)
void SerialBLEInterface::onDisconnect(BLEServer* pServer, NimBLEConnInfo& info,
int reason) {
(void)info;
(void)reason;
#else
void SerialBLEInterface::onDisconnect(BLEServer* pServer) {
#endif
BLE_DEBUG_PRINTLN("onDisconnect()");
deviceConnected = false;
notifySucceeded = false;
notificationsEnabled.store(false, std::memory_order_release);
xQueueReset(recv_queue);
_tx_reset_pending.store(true, std::memory_order_release);
#if !defined(MESH_USE_NIMBLE_ARDUINO)
if (pTxDescriptor != NULL) pTxDescriptor->setNotifications(false);
#endif
if (advertisingAllowed()) {
scheduleAdvertisingRestart((uint32_t)millis());
}
@@ -620,7 +737,11 @@ void SerialBLEInterface::onDisconnect(BLEServer* pServer) {
// -------- BLECharacteristicCallbacks methods
#if defined(CONFIG_NIMBLE_ENABLED)
#if defined(MESH_USE_NIMBLE_ARDUINO)
void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic,
NimBLEConnInfo& info) {
if (!info.isEncrypted() || !info.isAuthenticated() || !info.isBonded()) return;
#elif MESH_BLE_USES_NIMBLE
void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic,
ble_gap_conn_desc* desc) {
(void)desc;
@@ -634,8 +755,14 @@ void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic,
return;
}
#if defined(MESH_USE_NIMBLE_ARDUINO)
const auto value = pCharacteristic->getValue();
const uint8_t* rxValue = value.data();
const int len = value.size();
#else
uint8_t* rxValue = pCharacteristic->getData();
int len = pCharacteristic->getLength();
#endif
if (len > MAX_FRAME_SIZE) {
BLE_DEBUG_PRINTLN("ERROR: onWrite(), frame too big, len=%d", len);
@@ -650,11 +777,17 @@ void SerialBLEInterface::onWrite(BLECharacteristic* pCharacteristic,
}
}
#if defined(CONFIG_NIMBLE_ENABLED)
#if defined(MESH_USE_NIMBLE_ARDUINO)
void SerialBLEInterface::onSubscribe(BLECharacteristic* pCharacteristic,
NimBLEConnInfo& info, uint16_t subValue) {
(void)info;
#elif MESH_BLE_USES_NIMBLE
void SerialBLEInterface::onSubscribe(BLECharacteristic* pCharacteristic,
ble_gap_conn_desc* desc,
uint16_t subValue) {
(void)desc;
#endif
#if MESH_BLE_USES_NIMBLE
if (pCharacteristic == pTxCharacteristic) {
// NimBLE uses bit 0 for notification subscriptions.
notificationsEnabled.store((subValue & 0x0001u) != 0,
@@ -663,6 +796,7 @@ void SerialBLEInterface::onSubscribe(BLECharacteristic* pCharacteristic,
}
#endif
#if !defined(MESH_USE_NIMBLE_ARDUINO)
void SerialBLEInterface::onStatus(BLECharacteristic* pCharacteristic, Status status, uint32_t code) {
(void)pCharacteristic;
notifySucceeded = status == SUCCESS_NOTIFY;
@@ -672,6 +806,8 @@ void SerialBLEInterface::onStatus(BLECharacteristic* pCharacteristic, Status sta
}
}
#endif
// ---------- public methods
bool SerialBLEInterface::takeSuccessfulConnection(
@@ -784,7 +920,14 @@ void SerialBLEInterface::enable() {
clearBuffers();
// Start the service
#if defined(MESH_USE_NIMBLE_ARDUINO)
if (!pServer->start()) {
_isEnabled = false;
return;
}
#else
pService->start();
#endif
// Start advertising
@@ -804,7 +947,9 @@ void SerialBLEInterface::disable() {
pServer->getAdvertising()->stop();
pServer->disconnect(last_conn_id);
#if !defined(MESH_USE_NIMBLE_ARDUINO)
pService->stop();
#endif
oldDeviceConnected = deviceConnected = false;
clearBuffers();
_adv_restart_pending = false;
@@ -852,7 +997,7 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
BLE_WRITE_MIN_INTERVAL) // space the writes apart
) {
const uint16_t peer_mtu = pServer->getPeerMTU(last_conn_id);
#if defined(CONFIG_NIMBLE_ENABLED)
#if MESH_BLE_USES_NIMBLE
const bool notifications_ready =
notificationsEnabled.load(std::memory_order_acquire);
#else
@@ -870,7 +1015,13 @@ size_t SerialBLEInterface::checkRecvFrame(uint8_t dest[]) {
_last_write = now;
notifySucceeded = false;
pTxCharacteristic->setValue(send_queue[0].buf, send_queue[0].len);
#if defined(MESH_USE_NIMBLE_ARDUINO)
// NimBLE reports queue acceptance directly. Its status callback may run
// later; do not retain or duplicate an already accepted frame awaiting it.
notifySucceeded = pTxCharacteristic->notify(last_conn_id);
#else
pTxCharacteristic->notify();
#endif
if (notifySucceeded) {
BLE_DEBUG_PRINTLN("writeBytes: sz=%d, hdr=%d", (uint32_t)send_queue[0].len, (uint32_t) send_queue[0].buf[0]);
+33 -5
View File
@@ -4,19 +4,34 @@
#include "../BluetoothMac.h"
#include "../BleTxStallWatchdog.h"
#include "../UsbLogging.h"
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#if defined(MESH_USE_NIMBLE_ARDUINO)
#include <NimBLEDevice.h>
#else
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#endif
#if defined(MESH_USE_NIMBLE_ARDUINO) || defined(CONFIG_NIMBLE_ENABLED)
#define MESH_BLE_USES_NIMBLE 1
#else
#define MESH_BLE_USES_NIMBLE 0
#endif
#include <atomic>
#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>
class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLEServerCallbacks, BLECharacteristicCallbacks {
class SerialBLEInterface : public BaseSerialInterface,
#if !defined(MESH_USE_NIMBLE_ARDUINO)
public BLESecurityCallbacks,
#endif
public BLEServerCallbacks, public BLECharacteristicCallbacks {
BLEServer *pServer;
BLEService *pService;
BLECharacteristic * pTxCharacteristic;
#if !defined(MESH_USE_NIMBLE_ARDUINO)
BLE2902 *pTxDescriptor;
#endif
bool deviceConnected;
bool oldDeviceConnected;
bool notifySucceeded;
@@ -67,6 +82,16 @@ class SerialBLEInterface : public BaseSerialInterface, BLESecurityCallbacks, BLE
bool advertisingAllowed() const;
protected:
#if defined(MESH_USE_NIMBLE_ARDUINO)
uint32_t onPassKeyDisplay() override;
void onAuthenticationComplete(NimBLEConnInfo& info) override;
void onConnect(BLEServer* server, NimBLEConnInfo& info) override;
void onDisconnect(BLEServer* server, NimBLEConnInfo& info, int reason) override;
void onMTUChange(uint16_t mtu, NimBLEConnInfo& info) override;
void onWrite(BLECharacteristic* characteristic, NimBLEConnInfo& info) override;
void onSubscribe(BLECharacteristic* characteristic, NimBLEConnInfo& info,
uint16_t value) override;
#else
// BLESecurityCallbacks methods
uint32_t onPassKeyRequest() override;
void onPassKeyNotify(uint32_t pass_key) override;
@@ -98,13 +123,16 @@ protected:
void onWrite(BLECharacteristic* pCharacteristic, esp_ble_gatts_cb_param_t* param) override;
#endif
void onStatus(BLECharacteristic* pCharacteristic, Status status, uint32_t code) override;
#endif
public:
SerialBLEInterface() {
pServer = NULL;
pService = NULL;
pTxCharacteristic = NULL;
#if !defined(MESH_USE_NIMBLE_ARDUINO)
pTxDescriptor = NULL;
#endif
deviceConnected = false;
oldDeviceConnected = false;
notifySucceeded = false;
+7
View File
@@ -0,0 +1,7 @@
#pragma once
#include <stdint.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>
inline uint32_t test_millis = 100;
inline unsigned long millis() { return test_millis; }
+170
View File
@@ -0,0 +1,170 @@
#pragma once
// Test doubles for the pinned NimBLE-Arduino 2.5.1 API. Firmware builds check
// the real headers/library; this harness injects controller and bonding faults
// into the actual SerialBLEInterface implementation.
#include <stdint.h>
#include <cstring>
#include <string>
#include <vector>
#include <memory>
#include <cassert>
constexpr uint8_t BLE_ADDR_PUBLIC = 0, BLE_ADDR_RANDOM = 1;
constexpr uint8_t BLE_OWN_ADDR_PUBLIC = 0, BLE_OWN_ADDR_RANDOM = 1;
constexpr uint8_t BLE_HS_IO_DISPLAY_ONLY = 0, BLE_HS_ADV_F_BREDR_UNSUP = 4;
struct ble_addr_t { uint8_t type = 0; uint8_t val[6] = {}; };
namespace NIMBLE_PROPERTY {
constexpr uint32_t READ = 1, NOTIFY = 2, READ_AUTHEN = 4, WRITE = 8, WRITE_AUTHEN = 16;
}
struct NimBLEAddress {
ble_addr_t value;
explicit NimBLEAddress(const ble_addr_t& address) : value(address) {}
const ble_addr_t* getBase() const { return &value; }
};
struct NimBLEConnInfo {
ble_addr_t identity = {BLE_ADDR_PUBLIC, {0x8e, 0x6e, 0xf9, 0xeb, 0x27, 0xb8}};
bool encrypted = true, authenticated = true, bonded = true;
uint16_t handle = 7;
bool isEncrypted() const { return encrypted; }
bool isAuthenticated() const { return authenticated; }
bool isBonded() const { return bonded; }
uint16_t getConnHandle() const { return handle; }
NimBLEAddress getIdAddress() const { return NimBLEAddress(identity); }
};
class NimBLEServer;
class NimBLECharacteristic;
struct NimBLEServerCallbacks {
virtual ~NimBLEServerCallbacks() = default;
virtual uint32_t onPassKeyDisplay() { return 0; }
virtual void onAuthenticationComplete(NimBLEConnInfo&) {}
virtual void onConnect(NimBLEServer*, NimBLEConnInfo&) {}
virtual void onDisconnect(NimBLEServer*, NimBLEConnInfo&, int) {}
virtual void onMTUChange(uint16_t, NimBLEConnInfo&) {}
};
struct NimBLECharacteristicCallbacks {
virtual ~NimBLECharacteristicCallbacks() = default;
virtual void onWrite(NimBLECharacteristic*, NimBLEConnInfo&) {}
virtual void onSubscribe(NimBLECharacteristic*, NimBLEConnInfo&, uint16_t) {}
};
struct NimBLEAdvertisementData {
uint8_t flags = 0;
bool setFlags(uint8_t value) { flags = value; return true; }
};
namespace fake_nimble {
inline bool initialized = false, persisted = true, accept_notification = true;
inline std::string failure;
inline std::vector<std::string> calls;
inline uint8_t address[6] = {}, own_type = BLE_OWN_ADDR_PUBLIC, io_cap = 255;
inline ble_addr_t allowlisted;
inline uint16_t mtu = 179;
inline uint32_t pin = 0;
inline bool bonding = false, mitm = false, sc = false;
inline std::vector<std::vector<uint8_t>> transmitted;
inline unsigned disconnects = 0;
}
inline int ble_gap_wl_set(const ble_addr_t* peers, uint8_t count) {
assert(count == 1);
fake_nimble::allowlisted = *peers;
return fake_nimble::failure == "allowlist" ? 1 : 0;
}
struct NimBLEAdvertising {
bool active = false, scan_response = false, scan_filter = false, connect_filter = false;
std::string name, service;
bool start(uint32_t = 0, const NimBLEAddress* = nullptr) {
fake_nimble::calls.push_back("advertise"); active = true; return true;
}
bool stop() { active = false; return true; }
void enableScanResponse(bool value) { scan_response = value; }
void setScanFilter(bool scan, bool connect) { scan_filter = scan; connect_filter = connect; }
bool setAdvertisementData(const NimBLEAdvertisementData&) { name.clear(); service.clear(); return true; }
bool setName(const std::string& value) { name = value; return true; }
bool addServiceUUID(const char* value) { service = value; return true; }
};
struct NimBLECharacteristic {
std::string uuid;
uint32_t properties;
NimBLECharacteristicCallbacks* callbacks = nullptr;
std::vector<uint8_t> value;
NimBLECharacteristic(const char* id, uint32_t props) : uuid(id), properties(props) {}
void setCallbacks(NimBLECharacteristicCallbacks* cb) { callbacks = cb; }
const std::vector<uint8_t> getValue() const { return value; }
void setValue(const uint8_t* data, size_t size) { value.assign(data, data + size); }
bool notify(uint16_t handle = 0xffff) const {
assert(handle == 7);
if (!fake_nimble::accept_notification) return false;
fake_nimble::transmitted.push_back(value);
return true;
}
};
struct NimBLEService {
std::vector<std::unique_ptr<NimBLECharacteristic>> characteristics;
NimBLECharacteristic* createCharacteristic(const char* id, uint32_t props) {
if (fake_nimble::failure == (characteristics.empty() ? "tx" : "rx")) return nullptr;
characteristics.emplace_back(new NimBLECharacteristic(id, props));
return characteristics.back().get();
}
};
struct NimBLEServer {
NimBLEAdvertising advertising;
std::unique_ptr<NimBLEService> service;
NimBLEServerCallbacks* callbacks = nullptr;
bool owns_callbacks = true, auto_advertise = true, connected = false;
~NimBLEServer() { if (owns_callbacks) delete callbacks; }
void setCallbacks(NimBLEServerCallbacks* cb, bool take_ownership = true) {
callbacks = cb; owns_callbacks = take_ownership;
}
void advertiseOnDisconnect(bool value) { auto_advertise = value; }
bool start() { return true; }
uint8_t getConnectedCount() const { return connected; }
uint16_t getPeerMTU(uint16_t) const { return fake_nimble::mtu; }
bool disconnect(uint16_t) const { ++fake_nimble::disconnects; return true; }
NimBLEAdvertising* getAdvertising() { return &advertising; }
NimBLEService* createService(const char*) {
if (fake_nimble::failure == "service") return nullptr;
service.reset(new NimBLEService()); return service.get();
}
};
struct NimBLEDevice {
inline static std::unique_ptr<NimBLEServer> server;
static bool init(const std::string&) {
fake_nimble::calls.push_back("init");
return fake_nimble::initialized = fake_nimble::failure != "init";
}
static bool deinit(bool clear = false) {
fake_nimble::initialized = false;
if (clear) server.reset();
return true;
}
static bool deleteAllBonds() { fake_nimble::calls.push_back("clear-bonds"); return fake_nimble::failure != "clear-bonds"; }
static bool deleteBond(const NimBLEAddress&) { return true; }
static bool isBonded(const NimBLEAddress&) { return fake_nimble::persisted; }
static bool setOwnAddr(const uint8_t* address) {
fake_nimble::calls.push_back("address");
memcpy(fake_nimble::address, address, 6);
return fake_nimble::failure != "address";
}
static bool setOwnAddrType(uint8_t type) {
fake_nimble::calls.push_back("address-type"); fake_nimble::own_type = type;
return fake_nimble::failure != "address-type";
}
static bool setMTU(uint16_t mtu) { fake_nimble::mtu = mtu; return true; }
static void setSecurityPasskey(uint32_t pin) { fake_nimble::pin = pin; }
static void setSecurityIOCap(uint8_t value) { fake_nimble::io_cap = value; }
static void setSecurityAuth(bool bond, bool mitm, bool sc) {
fake_nimble::bonding = bond; fake_nimble::mitm = mitm; fake_nimble::sc = sc;
}
static NimBLEServer* createServer() {
if (fake_nimble::failure == "server") return nullptr;
if (!server) server.reset(new NimBLEServer());
return server.get();
}
};
#define BLEDevice NimBLEDevice
#define BLEServer NimBLEServer
#define BLEService NimBLEService
#define BLECharacteristic NimBLECharacteristic
#define BLEServerCallbacks NimBLEServerCallbacks
#define BLECharacteristicCallbacks NimBLECharacteristicCallbacks
#define BLEAddress NimBLEAddress
#define BLEAdvertising NimBLEAdvertising
#define BLEAdvertisementData NimBLEAdvertisementData
+8
View File
@@ -0,0 +1,8 @@
#pragma once
#include <stdint.h>
#include <string.h>
inline int esp_efuse_mac_get_default(uint8_t* address) {
const uint8_t factory[] = {0x44, 0x1b, 0xf6, 0x69, 0xcf, 0x98};
memcpy(address, factory, sizeof(factory));
return 0;
}
@@ -0,0 +1,4 @@
#pragma once
#include <stdint.h>
using BaseType_t = int;
constexpr BaseType_t pdTRUE = 1;
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include "FreeRTOS.h"
#include <deque>
#include <vector>
#include <cstring>
struct StaticQueue_t {
size_t capacity = 0, size = 0;
std::deque<std::vector<uint8_t>> frames;
};
using QueueHandle_t = StaticQueue_t*;
inline QueueHandle_t xQueueCreateStatic(size_t capacity, size_t size,
uint8_t*, StaticQueue_t* queue) {
queue->capacity = capacity;
queue->size = size;
queue->frames.clear();
return queue;
}
inline size_t uxQueueMessagesWaiting(QueueHandle_t queue) { return queue->frames.size(); }
inline int xQueueReset(QueueHandle_t queue) { queue->frames.clear(); return pdTRUE; }
inline int xQueueSend(QueueHandle_t queue, const void* value, int) {
if (queue->frames.size() == queue->capacity) return 0;
const auto* bytes = static_cast<const uint8_t*>(value);
queue->frames.emplace_back(bytes, bytes + queue->size);
return pdTRUE;
}
inline int xQueueReceive(QueueHandle_t queue, void* value, int) {
if (queue->frames.empty()) return 0;
memcpy(value, queue->frames.front().data(), queue->size);
queue->frames.pop_front();
return pdTRUE;
}
+192
View File
@@ -0,0 +1,192 @@
#include <helpers/esp32/SerialBLEInterface.h>
#include <algorithm>
#include <array>
#include <cassert>
#include <cstdio>
using mesh::companion::BluetoothPeerIdentity;
using namespace fake_nimble;
static void reset() {
NimBLEDevice::deinit(true);
failure.clear(); calls.clear(); transmitted.clear();
persisted = true; accept_notification = true; disconnects = 0;
test_millis = 100;
}
static void test_identity_modes() {
// The common MAC policy supplies human-order addresses for all four
// custom/random modes. The actual adapter must hand the controller exactly
// those bytes in NimBLE order before any advertisement can start.
const std::array<std::array<uint8_t, 6>, 4> addresses = {{
{{0xc2, 0x12, 0x34, 0x56, 0x78, 0x9a}},
{{0xd3, 0xff, 0x00, 0x7e, 0x01, 0x02}},
{{0xe4, 0x10, 0x20, 0x30, 0x40, 0x50}},
{{0xf5, 0x11, 0x22, 0x33, 0x44, 0x55}},
}};
for (const auto& chosen : addresses) {
reset();
SerialBLEInterface interface;
assert(interface.begin("MeshCore-", "trial", 246810, chosen.data(), true));
assert(own_type == BLE_OWN_ADDR_RANDOM);
for (size_t i = 0; i < 6; ++i) assert(address[i] == chosen[5 - i]);
auto* server = NimBLEDevice::server.get();
assert(!server->owns_callbacks && !server->auto_advertise);
assert(!server->advertising.active);
assert(pin == 246810 && bonding && mitm && sc && io_cap == BLE_HS_IO_DISPLAY_ONLY);
interface.enable();
assert(server->advertising.active);
assert(std::find(calls.begin(), calls.end(), "address-type") <
std::find(calls.begin(), calls.end(), "advertise"));
assert(server->advertising.name == "MeshCore-trial");
assert(server->advertising.service == "6E400001-B5A3-F393-E0A9-E50E24DCCA9E");
assert(server->callbacks->onPassKeyDisplay() == 246810);
assert(interface.takePairingRequest());
assert(!interface.takePairingRequest());
NimBLEDevice::deinit(true); // Must never delete the application interface.
}
reset();
own_type = BLE_OWN_ADDR_RANDOM;
SerialBLEInterface interface;
assert(interface.begin("MeshCore-", "default", 123456));
assert(own_type == BLE_OWN_ADDR_PUBLIC);
assert(std::find(calls.begin(), calls.end(), "address") == calls.end());
NimBLEDevice::deinit(true);
}
static void test_startup_failures() {
const uint8_t custom[] = {0xc2, 0x12, 0x34, 0x56, 0x78, 0x9a};
for (const char* step : {"clear-bonds", "address", "address-type", "server", "service", "tx", "rx"}) {
reset(); failure = step;
SerialBLEInterface interface;
assert(!interface.begin("MeshCore-", "trial", 123456, custom, true));
assert(!initialized && !NimBLEDevice::server);
assert(std::find(calls.begin(), calls.end(), "advertise") == calls.end());
failure.clear();
assert(interface.begin("MeshCore-", "trial", 123456, custom, true));
for (size_t i = 0; i < 6; ++i) assert(address[i] == custom[5 - i]);
NimBLEDevice::deinit(true);
}
reset();
SerialBLEInterface interface;
const uint8_t invalid[] = {0x44, 1, 2, 3, 4, 5};
assert(!interface.begin("MeshCore-", "trial", 123456, invalid));
assert(calls.empty());
}
static void authenticate(SerialBLEInterface& interface, NimBLEConnInfo& info) {
auto* server = NimBLEDevice::server.get();
server->connected = true;
server->callbacks->onConnect(server, info);
server->callbacks->onAuthenticationComplete(info);
uint8_t discarded[MAX_FRAME_SIZE];
interface.checkRecvFrame(discarded);
}
static void test_authentication_and_bond_lifecycle() {
for (unsigned missing = 0; missing < 3; ++missing) {
reset(); SerialBLEInterface interface;
assert(interface.begin("MeshCore-", "trial", 123456));
NimBLEConnInfo info;
if (missing == 0) info.encrypted = false;
if (missing == 1) info.authenticated = false;
if (missing == 2) info.bonded = false;
authenticate(interface, info);
assert(!interface.isConnected() && !interface.takeSuccessfulConnection());
assert(disconnects == 1);
NimBLEDevice::deinit(true);
}
reset(); SerialBLEInterface interface;
assert(interface.begin("MeshCore-", "trial", 123456, nullptr, false, true));
interface.enable();
NimBLEConnInfo info;
// Check an identity address, not a temporary over-the-air private address.
info.identity.type = BLE_ADDR_RANDOM;
info.identity.val[5] = 0xd6;
authenticate(interface, info);
assert(interface.isConnected());
BluetoothPeerIdentity peer;
persisted = false;
assert(!interface.takeSuccessfulConnection(&peer));
persisted = true;
assert(interface.takeSuccessfulConnection(&peer));
assert(peer.type == mesh::companion::BLUETOOTH_PEER_ADDRESS_RANDOM);
for (size_t i = 0; i < 6; ++i) assert(peer.address[i] == info.identity.val[5 - i]);
assert(!interface.takeSuccessfulConnection(&peer));
assert(interface.enableBondedOnlyAdvertising(peer));
assert(allowlisted.type == BLE_ADDR_RANDOM);
assert(memcmp(allowlisted.val, info.identity.val, 6) == 0);
auto* server = NimBLEDevice::server.get();
assert(server->advertising.scan_filter && server->advertising.connect_filter);
assert(!server->advertising.scan_response && server->advertising.name.empty());
assert(interface.isConnected());
NimBLEDevice::deinit(true);
reset(); SerialBLEInterface rebooted;
persisted = false;
assert(rebooted.begin("MeshCore-", "trial", 123456, nullptr, false, false, &peer));
rebooted.enable();
assert(!NimBLEDevice::server->advertising.active);
assert(rebooted.takeBondedOnlyRecovery());
NimBLEDevice::deinit(true);
}
static void test_real_transport_frames() {
reset(); SerialBLEInterface interface;
assert(interface.begin("MeshCore-", "trial", 123456));
interface.enable();
NimBLEConnInfo info;
authenticate(interface, info);
auto* server = NimBLEDevice::server.get();
auto* tx = server->service->characteristics[0].get();
auto* rx = server->service->characteristics[1].get();
assert(tx->properties & NIMBLE_PROPERTY::READ_AUTHEN);
assert(rx->properties & NIMBLE_PROPERTY::WRITE_AUTHEN);
uint8_t frame[MAX_FRAME_SIZE];
for (size_t i = 0; i < sizeof(frame); ++i) frame[i] = i;
frame[0] = 0x0d;
uint8_t received[MAX_FRAME_SIZE] = {};
assert(interface.writeFrame(frame, sizeof(frame)) == sizeof(frame));
interface.checkRecvFrame(received);
assert(transmitted.empty()); // Subscription is still pending.
tx->callbacks->onSubscribe(tx, info, 1);
mtu = 23;
interface.checkRecvFrame(received);
assert(transmitted.empty()); // Never truncate a full protocol frame.
mtu = 179;
accept_notification = false;
interface.checkRecvFrame(received);
assert(interface.hasPendingIO() && transmitted.empty());
test_millis += 61;
accept_notification = true;
interface.checkRecvFrame(received);
assert(transmitted.size() == 1 && transmitted[0].size() == sizeof(frame));
assert(memcmp(transmitted[0].data(), frame, sizeof(frame)) == 0);
test_millis += 61;
interface.checkRecvFrame(received);
assert(transmitted.size() == 1); // No late-status duplicate.
rx->setValue(frame, sizeof(frame));
info.authenticated = false;
rx->callbacks->onWrite(rx, info);
assert(interface.checkRecvFrame(received) == 0);
info.authenticated = true;
rx->callbacks->onWrite(rx, info);
assert(interface.checkRecvFrame(received) == sizeof(frame));
assert(memcmp(frame, received, sizeof(frame)) == 0);
uint8_t oversized[MAX_FRAME_SIZE + 1] = {};
rx->setValue(oversized, sizeof(oversized));
rx->callbacks->onWrite(rx, info);
assert(interface.checkRecvFrame(received) == 0);
NimBLEDevice::deinit(true);
}
int main() {
test_identity_modes();
test_startup_failures();
test_authentication_and_bond_lifecycle();
test_real_transport_frames();
puts("NimBLE adapter: identity, faults, bonding, stealth and full frames PASS");
}
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
"""Exercise the real ESP32 Companion adapter against the NimBLE 2.5 API."""
from pathlib import Path
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
class NimbleCompanionTests(unittest.TestCase):
def test_identity_security_and_data_path_under_sanitizers(self):
with tempfile.TemporaryDirectory(prefix="meshcore-nimble-") as temp:
binary = Path(temp) / "nimble-companion"
result = subprocess.run([
"c++", "-std=c++17", "-g", "-fsanitize=address,undefined",
"-DMESH_USE_NIMBLE_ARDUINO=1",
"-I", str(ROOT / "test/fixtures/nimble_companion/mocks"),
"-I", str(ROOT / "src"),
str(ROOT / "src/helpers/esp32/SerialBLEInterface.cpp"),
str(ROOT / "test/fixtures/nimble_companion/test_nimble_companion.cpp"),
"-o", str(binary),
], text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
result = subprocess.run([str(binary)], text=True, capture_output=True)
self.assertEqual(result.returncode, 0, result.stderr)
if __name__ == "__main__":
unittest.main()