Add configurable Companion Bluetooth addresses
This commit is contained in:
1 parent
b25005f52f
commit
4b400ea56b
26 files changed
+2723
-1966
No files matched your search
@@ -0,0 +1,115 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
namespace mesh {
|
||||
namespace companion {
|
||||
|
||||
static const size_t BLUETOOTH_MAC_BYTES = 6;
|
||||
static const size_t BLUETOOTH_MAC_TEXT_SIZE = 18;
|
||||
|
||||
enum BluetoothMacMode : uint8_t {
|
||||
BLUETOOTH_MAC_DEFAULT = 0,
|
||||
BLUETOOTH_MAC_CUSTOM = 1,
|
||||
BLUETOOTH_MAC_RANDOM_SAVED = 2,
|
||||
BLUETOOTH_MAC_RANDOM_EVERY_BOOT = 3,
|
||||
};
|
||||
|
||||
inline bool isValidBluetoothMacMode(uint8_t mode) {
|
||||
return mode <= BLUETOOTH_MAC_RANDOM_EVERY_BOOT;
|
||||
}
|
||||
|
||||
inline bool bluetoothMacModeUsesSavedAddress(uint8_t mode) {
|
||||
return mode == BLUETOOTH_MAC_CUSTOM
|
||||
|| mode == BLUETOOTH_MAC_RANDOM_SAVED;
|
||||
}
|
||||
|
||||
inline bool hasCustomBluetoothMac(
|
||||
const uint8_t address[BLUETOOTH_MAC_BYTES]) {
|
||||
if (address == NULL) return false;
|
||||
for (size_t i = 0; i < BLUETOOTH_MAC_BYTES; i++) {
|
||||
if (address[i] != 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// User-selected BLE identities are random-static addresses, not IEEE-assigned
|
||||
// public addresses. The two most-significant bits must be one, while the
|
||||
// remaining 46 bits may not be all zero or all one.
|
||||
inline bool isValidBluetoothMac(
|
||||
const uint8_t address[BLUETOOTH_MAC_BYTES]) {
|
||||
if (address == NULL || (address[0] & 0xC0u) != 0xC0u) return false;
|
||||
|
||||
bool payload_all_zero = (address[0] & 0x3Fu) == 0;
|
||||
bool payload_all_one = (address[0] & 0x3Fu) == 0x3Fu;
|
||||
for (size_t i = 1; i < BLUETOOTH_MAC_BYTES; i++) {
|
||||
payload_all_zero = payload_all_zero && address[i] == 0;
|
||||
payload_all_one = payload_all_one && address[i] == 0xFFu;
|
||||
}
|
||||
return !payload_all_zero && !payload_all_one;
|
||||
}
|
||||
|
||||
// Convert six random bytes into a valid BLE random-static identity. This is
|
||||
// deterministic for a given input so both saved-random and per-boot modes can
|
||||
// use the platform's existing entropy source without platform-specific rules.
|
||||
inline void makeRandomStaticBluetoothMac(
|
||||
uint8_t address[BLUETOOTH_MAC_BYTES]) {
|
||||
if (address == NULL) return;
|
||||
address[0] |= 0xC0u;
|
||||
if (!isValidBluetoothMac(address)) address[5] ^= 0x01u;
|
||||
}
|
||||
|
||||
inline int bluetoothMacHexDigit(char value) {
|
||||
if (value >= '0' && value <= '9') return value - '0';
|
||||
if (value >= 'a' && value <= 'f') return value - 'a' + 10;
|
||||
if (value >= 'A' && value <= 'F') return value - 'A' + 10;
|
||||
return -1;
|
||||
}
|
||||
|
||||
inline bool parseBluetoothMac(
|
||||
const char* text, uint8_t output[BLUETOOTH_MAC_BYTES]) {
|
||||
if (text == NULL || output == NULL || strlen(text) != 17) return false;
|
||||
const char separator = text[2];
|
||||
if (separator != ':' && separator != '-') return false;
|
||||
|
||||
uint8_t parsed[BLUETOOTH_MAC_BYTES];
|
||||
for (size_t i = 0; i < BLUETOOTH_MAC_BYTES; i++) {
|
||||
const size_t offset = i * 3;
|
||||
const int high = bluetoothMacHexDigit(text[offset]);
|
||||
const int low = bluetoothMacHexDigit(text[offset + 1]);
|
||||
if (high < 0 || low < 0
|
||||
|| (i + 1 < BLUETOOTH_MAC_BYTES
|
||||
&& text[offset + 2] != separator)) {
|
||||
return false;
|
||||
}
|
||||
parsed[i] = static_cast<uint8_t>((high << 4) | low);
|
||||
}
|
||||
if (!isValidBluetoothMac(parsed)) return false;
|
||||
memcpy(output, parsed, sizeof(parsed));
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool formatBluetoothMac(
|
||||
const uint8_t address[BLUETOOTH_MAC_BYTES], char* output,
|
||||
size_t output_size) {
|
||||
if (address == NULL || output == NULL
|
||||
|| output_size < BLUETOOTH_MAC_TEXT_SIZE) {
|
||||
if (output != NULL && output_size != 0) output[0] = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
static const char HEX_DIGITS[] = "0123456789ABCDEF";
|
||||
size_t cursor = 0;
|
||||
for (size_t i = 0; i < BLUETOOTH_MAC_BYTES; i++) {
|
||||
output[cursor++] = HEX_DIGITS[address[i] >> 4];
|
||||
output[cursor++] = HEX_DIGITS[address[i] & 0x0Fu];
|
||||
if (i + 1 < BLUETOOTH_MAC_BYTES) output[cursor++] = ':';
|
||||
}
|
||||
output[cursor] = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace companion
|
||||
} // namespace mesh
|
||||
@@ -18,7 +18,7 @@
|
||||
// separately, see wcIsAdminPasswordKey below.
|
||||
static const char* const WC_ALLOWED_SET_KEYS[] = {
|
||||
// NodePrefs (radio / node)
|
||||
"name", "bluetooth.name", "lat", "lon", "radio", "tx", "af", "rxdelay", "txdelay",
|
||||
"name", "bluetooth.name", "bluetooth.mac", "lat", "lon", "radio", "tx", "af", "rxdelay", "txdelay",
|
||||
"cad", "radio.rxgain", "radio.fem.rxgain", "radio.rxps", "powersaving",
|
||||
"repeat",
|
||||
"advert.interval", "flood.advert.interval",
|
||||
@@ -57,14 +57,18 @@ static inline bool wcIsAllowedSetKey(const char* key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Changing the primary ESP-NOW channel cannot be applied live: every peer and
|
||||
// any infrastructure-WiFi association must stay on the boot channel until the
|
||||
// node restarts. The server enforces this even when a crafted client omits the
|
||||
// browser's reboot flag.
|
||||
static inline bool wcSetKeyRequiresReboot(const char* key) {
|
||||
static inline bool wcIsEspNowChannelKey(const char* key) {
|
||||
return key != NULL && strcmp(key, "espnow.channel") == 0;
|
||||
}
|
||||
|
||||
// These settings cannot be applied to a running transport. The server enforces
|
||||
// a reboot even when a crafted client omits the browser's reboot flag.
|
||||
static inline bool wcSetKeyRequiresReboot(const char* key) {
|
||||
return key != NULL
|
||||
&& (wcIsEspNowChannelKey(key)
|
||||
|| strcmp(key, "bluetooth.mac") == 0);
|
||||
}
|
||||
|
||||
// The admin password maps to the top-level `password` command, not a setter, so
|
||||
// it is classified apart from the `set` allowlist. It is the only key that gets
|
||||
// this treatment, which is what keeps the allowlist the sole route to `set` and
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include "SerialBLEInterface.h"
|
||||
#include "../BluetoothMac.h"
|
||||
#include "../CompanionFrameQueue.h"
|
||||
#include "esp_mac.h"
|
||||
#include <stdlib.h>
|
||||
#if defined(CONFIG_BLUEDROID_ENABLED)
|
||||
#include "esp_gap_ble_api.h"
|
||||
#endif
|
||||
@@ -26,9 +28,48 @@ extern "C" bool bleInUse(void) {
|
||||
|
||||
#define ADVERT_RESTART_DELAY 1000 // millis
|
||||
|
||||
bool SerialBLEInterface::begin(const char* prefix, const char* name, uint32_t pin_code) {
|
||||
static void clearStoredBluetoothBonds() {
|
||||
#if defined(CONFIG_NIMBLE_ENABLED)
|
||||
const int result = ble_store_clear();
|
||||
if (result != 0) {
|
||||
BLE_DEBUG_PRINTLN("Could not clear NimBLE bonds: %d", result);
|
||||
}
|
||||
#else
|
||||
int count = esp_ble_get_bond_device_num();
|
||||
if (count <= 0) return;
|
||||
|
||||
esp_ble_bond_dev_t* devices = static_cast<esp_ble_bond_dev_t*>(
|
||||
malloc(sizeof(esp_ble_bond_dev_t) * count));
|
||||
if (devices == NULL) {
|
||||
BLE_DEBUG_PRINTLN("Could not allocate Bluetooth bond list");
|
||||
return;
|
||||
}
|
||||
if (esp_ble_get_bond_device_list(&count, devices) != ESP_OK) {
|
||||
BLE_DEBUG_PRINTLN("Could not read Bluetooth bond list");
|
||||
free(devices);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < count; i++) {
|
||||
if (esp_ble_remove_bond_device(devices[i].bd_addr) != ESP_OK) {
|
||||
BLE_DEBUG_PRINTLN("Could not remove Bluetooth bond %d", i);
|
||||
}
|
||||
}
|
||||
free(devices);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool SerialBLEInterface::begin(const char* prefix, const char* name,
|
||||
uint32_t pin_code,
|
||||
const uint8_t* custom_address,
|
||||
bool clear_bonds) {
|
||||
_pin_code = pin_code;
|
||||
|
||||
if (custom_address != nullptr
|
||||
&& !mesh::companion::isValidBluetoothMac(custom_address)) {
|
||||
BLE_DEBUG_PRINTLN("Custom Bluetooth MAC is invalid");
|
||||
return false;
|
||||
}
|
||||
|
||||
char resolved_name[32];
|
||||
const char* suffix = name;
|
||||
if (strcmp(name, "@@MAC") == 0) {
|
||||
@@ -59,6 +100,25 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name, uint32_t pi
|
||||
// failure is a null server below, which is handled without dereferencing it.
|
||||
BLEDevice::init(dev_name);
|
||||
#endif
|
||||
|
||||
if (clear_bonds) clearStoredBluetoothBonds();
|
||||
|
||||
#if defined(CONFIG_NIMBLE_ENABLED)
|
||||
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++) {
|
||||
native_address[i] = custom_address[
|
||||
mesh::companion::BLUETOOTH_MAC_BYTES - 1 - i];
|
||||
}
|
||||
if (!BLEDevice::setOwnAddr(native_address)
|
||||
|| !BLEDevice::setOwnAddrType(BLE_OWN_ADDR_RANDOM)) {
|
||||
BLE_DEBUG_PRINTLN("Custom Bluetooth MAC setup failed");
|
||||
BLEDevice::deinit(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
BLEDevice::setSecurityCallbacks(this);
|
||||
// ATT notifications consume three bytes of the negotiated MTU. Reserve
|
||||
// that overhead so a MAX_FRAME_SIZE protocol frame fits without truncation.
|
||||
@@ -91,6 +151,18 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name, uint32_t pi
|
||||
}
|
||||
pServer->setCallbacks(this);
|
||||
|
||||
#if !defined(CONFIG_NIMBLE_ENABLED)
|
||||
if (custom_address != nullptr) {
|
||||
esp_bd_addr_t native_address;
|
||||
memcpy(native_address, custom_address, sizeof(native_address));
|
||||
// Arduino-ESP32 2.x returns void here, while newer Bluedroid wrappers
|
||||
// return bool. Both configure the advertising address type and report any
|
||||
// controller failure through the BLE library log.
|
||||
pServer->getAdvertising()->setDeviceAddress(
|
||||
native_address, BLE_ADDR_TYPE_RANDOM);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Create the BLE Service
|
||||
pService = pServer->createService(SERVICE_UUID);
|
||||
if (pService == NULL) {
|
||||
|
||||
@@ -108,8 +108,12 @@ public:
|
||||
* @param prefix a prefix for the device name
|
||||
* @param name a name for the device (combined with prefix); "@@MAC" uses the hardware address
|
||||
* @param pin_code the BLE security pin
|
||||
* @param custom_address optional human-order BLE random-static address
|
||||
* @param clear_bonds clear saved peer bonds before accepting connections
|
||||
*/
|
||||
bool begin(const char* prefix, const char* name, uint32_t pin_code);
|
||||
bool begin(const char* prefix, const char* name, uint32_t pin_code,
|
||||
const uint8_t* custom_address = nullptr,
|
||||
bool clear_bonds = false);
|
||||
|
||||
// BaseSerialInterface methods
|
||||
void enable() override;
|
||||
|
||||
+1960
-1944
File diff suppressed because it is too large.
Load diff
@@ -205,7 +205,7 @@ static bool isAllowedSetKey(const char* key, bool has_mqtt) {
|
||||
#if !defined(MESH_PRIMARY_ESPNOW) || !MESH_PRIMARY_ESPNOW
|
||||
// The shared source-level allowlist is host-tested, but only a target whose
|
||||
// mesh transport is ESP-NOW has a meaningful primary-radio channel to set.
|
||||
if (wcSetKeyRequiresReboot(key)) return false;
|
||||
if (wcIsEspNowChannelKey(key)) return false;
|
||||
#endif
|
||||
const bool mqtt_only = strncmp(key, "mqtt.", 5) == 0
|
||||
|| strncmp(key, "mqtt", 4) == 0
|
||||
@@ -1418,7 +1418,7 @@ void WebConfigServer::drainBatch(uint32_t now) {
|
||||
if (strcmp(_batch[i].key, "wifi.ssid") == 0
|
||||
|| strcmp(_batch[i].key, "wifi.pwd") == 0) {
|
||||
wifi_credentials_changed = true;
|
||||
} else if (wcSetKeyRequiresReboot(_batch[i].key)) {
|
||||
} else if (wcIsEspNowChannelKey(_batch[i].key)) {
|
||||
espnow_channel_changed = true;
|
||||
}
|
||||
}
|
||||
@@ -1796,6 +1796,7 @@ void WebConfigServer::handleConfigGet(AsyncWebServerRequest* req) {
|
||||
radio["loop_detect"] = LOOP_MODES[node.loop_detect <= 3 ? node.loop_detect : 0];
|
||||
radio["name"] = (const char*)node.name;
|
||||
radio["bluetooth_name"] = (const char*)node.bluetooth_name;
|
||||
radio["bluetooth_mac"] = (const char*)node.bluetooth_mac;
|
||||
radio["lat"] = node.lat;
|
||||
radio["lon"] = node.lon;
|
||||
radio["advert_interval"] = node.advert_interval;
|
||||
@@ -1942,7 +1943,7 @@ void WebConfigServer::handleConfigPost(AsyncWebServerRequest* req) {
|
||||
req->send(400, "application/json", out);
|
||||
return;
|
||||
}
|
||||
if (wcSetKeyRequiresReboot(key)) {
|
||||
if (wcIsEspNowChannelKey(key)) {
|
||||
uint8_t channel = 0;
|
||||
if (!mesh::wifi::parseEspNowChannel(val, channel)) {
|
||||
StaticJsonDocument<128> err;
|
||||
|
||||
@@ -84,6 +84,7 @@ public:
|
||||
CAP_POWER_SAVING = 1UL << 13,
|
||||
CAP_BLUETOOTH_NAME = 1UL << 14,
|
||||
CAP_ESPNOW_CHANNEL = 1UL << 15,
|
||||
CAP_BLUETOOTH_MAC = 1UL << 16,
|
||||
CAP_DELAYS = CAP_RX_DELAY | CAP_TX_DELAY,
|
||||
};
|
||||
|
||||
@@ -93,6 +94,7 @@ public:
|
||||
struct NodeSnapshot {
|
||||
char name[32];
|
||||
char bluetooth_name[32]; // empty = transport's default device name
|
||||
char bluetooth_mac[24]; // empty = chipset factory address
|
||||
char admin_password[32]; // empty = trusted LAN/no login prompt
|
||||
double lat;
|
||||
double lon;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "SerialBLEInterface.h"
|
||||
#include "../BluetoothMac.h"
|
||||
#include "../CompanionFrameQueue.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
@@ -238,7 +239,10 @@ bool SerialBLEInterface::removeStoredBondForPeer(const char* cause) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SerialBLEInterface::begin(const char* prefix, const char* name, uint32_t pin_code) {
|
||||
bool SerialBLEInterface::begin(const char* prefix, const char* name,
|
||||
uint32_t pin_code,
|
||||
const uint8_t* custom_address,
|
||||
bool clear_bonds) {
|
||||
instance = this;
|
||||
|
||||
char charpin[20];
|
||||
@@ -252,6 +256,29 @@ bool SerialBLEInterface::begin(const char* prefix, const char* name, uint32_t pi
|
||||
BLE_DEBUG_PRINTLN("Bluefruit.begin failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (clear_bonds) Bluefruit.Periph.clearBonds();
|
||||
|
||||
if (custom_address != nullptr) {
|
||||
if (!mesh::companion::isValidBluetoothMac(custom_address)) {
|
||||
instance = nullptr;
|
||||
BLE_DEBUG_PRINTLN("Custom Bluetooth MAC is invalid");
|
||||
return false;
|
||||
}
|
||||
|
||||
ble_gap_addr_t address = {};
|
||||
address.addr_type = BLE_GAP_ADDR_TYPE_RANDOM_STATIC;
|
||||
for (size_t i = 0; i < mesh::companion::BLUETOOTH_MAC_BYTES; i++) {
|
||||
address.addr[i] = custom_address[
|
||||
mesh::companion::BLUETOOTH_MAC_BYTES - 1 - i];
|
||||
}
|
||||
const uint32_t address_error = sd_ble_gap_addr_set(&address);
|
||||
if (address_error != NRF_SUCCESS) {
|
||||
instance = nullptr;
|
||||
BLE_DEBUG_PRINTLN("Custom Bluetooth MAC failed: %lu", address_error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
char resolved_name[32];
|
||||
const char* suffix = name;
|
||||
|
||||
@@ -94,8 +94,12 @@ public:
|
||||
* @param prefix a prefix for the device name
|
||||
* @param name a name for the device (combined with prefix); "@@MAC" uses the hardware address
|
||||
* @param pin_code the BLE security pin
|
||||
* @param custom_address optional human-order BLE random-static address
|
||||
* @param clear_bonds clear saved peer bonds before accepting connections
|
||||
*/
|
||||
bool begin(const char* prefix, const char* name, uint32_t pin_code);
|
||||
bool begin(const char* prefix, const char* name, uint32_t pin_code,
|
||||
const uint8_t* custom_address = nullptr,
|
||||
bool clear_bonds = false);
|
||||
|
||||
void disconnect();
|
||||
void enable() override;
|
||||
|
||||
Reference in new issue
Block a user