mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-09-17 11:04:20 +00:00
Add opt-in NimBLE Full Companion trials for V4 and XIAO S3
This commit is contained in:
@@ -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.
|
||||
@@ -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}
|
||||
@@ -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]);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user