Add configurable Companion Bluetooth addresses

This commit is contained in:
mikecarper committed 2026-09-07 18:29:00 -07:00
1 parent b25005f52f
commit 4b400ea56b
26 files changed
+2723 -1966

No files matched your search

+115
View File
@@ -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
+10 -6
View File
@@ -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
+73 -1
View File
@@ -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) {
+5 -1
View File
@@ -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;
File diff suppressed because it is too large. Load diff
+4 -3
View File
@@ -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;
+2
View File
@@ -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;
+28 -1
View File
@@ -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;
+5 -1
View File
@@ -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;