feat: add customizable companion Bluetooth names

This commit is contained in:
mikecarper committed 2026-08-25 20:51:18 -07:00
1 parent 42b6a0df60
commit 65e40ff05f
20 files changed
+2060 -1737

No files matched your search

+13
View File
@@ -221,6 +221,7 @@ catalog; bytes `0x2C`-`0x31` are parked and `0x35` is unused.
| `0x42` / `0x43` | `CMD_GET_RADIO_FEM_RXGAIN` / `CMD_SET_RADIO_FEM_RXGAIN` | Read or set FEM receive gain. |
| `0x44` / `0x45` | `CMD_GET_RADIO_RXGAIN` / `CMD_SET_RADIO_RXGAIN` | Read or set the radio chip's boosted receive-gain mode. |
| `0x46` / `0x47` | `CMD_GET_WIFI_POWER_SAVE` / `CMD_SET_WIFI_POWER_SAVE` | Read or set ESP32 Companion WiFi modem sleep. |
| `0x48` / `0x49` | `CMD_GET_BLUETOOTH_NAME` / `CMD_SET_BLUETOOTH_NAME` | Read or set the independent Bluetooth device name. |
The sections below detail the most common frames. Refer to the source named
above for command bodies that are not expanded here.
@@ -260,6 +261,18 @@ coexistence requires modem sleep. A storage failure also returns
WiFi driver, SET still returns `RESP_CODE_OK` and the mode is applied on the
next connection. Device `powersaving` does not overwrite this setting.
The Bluetooth-name pair is available on Companion builds regardless of the
currently selected transport, so a host can configure a BLE-capable image over
USB or TCP before rebooting into it. `CMD_GET_BLUETOOTH_NAME` has no body and
replies with `RESP_CODE_OK`, a one-byte mode (`0` for the default name, `1` for
a custom name), then the effective UTF-8 device name without a terminator.
`CMD_SET_BLUETOOTH_NAME` is followed by zero to 31 UTF-8 bytes. A non-empty
body stores that exact complete Bluetooth name; a zero-length body clears the
override and restores `MeshCore-<advert name>`. A successful SET replies with
`RESP_CODE_OK`, and the new label takes effect after reboot. Embedded NULs,
control characters, malformed UTF-8, or oversized values return
`ERR_CODE_ILLEGAL_ARG`; a storage failure returns `ERR_CODE_BAD_STATE`.
### 1. App Start
**Purpose**: Initialize communication with the device. Must be sent first after connection.
+8
View File
@@ -226,6 +226,14 @@ saved BLE PIN, display builds generate a new PIN at boot; builds without a
physical display use `123456`. A PIN saved through the Companion protocol takes
effect after reboot.
The Bluetooth device name is independently configurable. In the text terminal,
use `get bluetooth.name` and `set bluetooth.name <name>`; use
`set bluetooth.name default` to restore `MeshCore-<node name>`. The WebConfig
Node card exposes the same optional field on Bluetooth-capable ESP32 builds.
Custom names replace the complete Bluetooth label rather than inheriting the
prefix, accept up to 31 valid UTF-8 bytes, and take effect after reboot. This
does not change the node's mesh advertisement name.
ESP32 ports 5000, 5001, 5002, and WebConfig have no independent login layer.
Expose them only on a trusted LAN or temporary setup network. See
[WiFi setup](./WiFi.md) for credential setup and reconnect behavior.
+9
View File
@@ -66,6 +66,15 @@ set name {name}
```
Sets your advertisement name.
```
get bluetooth.name
set bluetooth.name {name|default}
```
Shows or changes the complete Bluetooth device name without changing the mesh
advertisement identity. A custom name may contain spaces and is limited to 31
valid UTF-8 bytes. `default` restores `MeshCore-<node name>`. The saved change
takes effect after reboot. `get ble.name` and `set ble.name` are short aliases.
```
set lat {latitude}
```
+56
View File
@@ -0,0 +1,56 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <helpers/UTF8Helpers.h>
namespace mesh {
namespace companion {
// Bluetooth names are stored as UTF-8 bytes. Keeping the override the same
// size as the node name makes its limit predictable on every Companion target.
static constexpr size_t BLUETOOTH_NAME_SIZE = 32;
static constexpr size_t BLUETOOTH_NAME_MAX_BYTES = BLUETOOTH_NAME_SIZE - 1;
inline bool isValidBluetoothName(const char* name) {
if (name == nullptr) return false;
const size_t length = strnlen(name, BLUETOOTH_NAME_SIZE);
if (length == 0 || length >= BLUETOOTH_NAME_SIZE
|| validUtf8PrefixLength(name, length) != length) {
return false;
}
bool has_visible_character = false;
for (size_t i = 0; i < length; i++) {
const uint8_t byte = static_cast<uint8_t>(name[i]);
if (byte < 0x20 || byte == 0x7F) return false;
if (byte != 0x20) has_visible_character = true;
}
return has_visible_character;
}
inline bool hasCustomBluetoothName(const char* name) {
return name != nullptr && name[0] != '\0' && isValidBluetoothName(name);
}
inline void formatBluetoothName(char* output, size_t output_size,
const char* custom_name,
const char* default_prefix,
const char* node_name) {
if (output == nullptr || output_size == 0) return;
if (hasCustomBluetoothName(custom_name)) {
snprintf(output, output_size, "%s", custom_name);
} else {
snprintf(output, output_size, "%s%s",
default_prefix == nullptr ? "" : default_prefix,
node_name == nullptr ? "" : node_name);
}
}
} // namespace companion
} // namespace mesh
+6
View File
@@ -352,6 +352,10 @@ void DataStore::loadPrefsInt(const char *filename, CompanionNodePrefs& _prefs, d
if (file.available() >= (int)sizeof(_prefs.usb_logging_enabled)) {
file.read((uint8_t *)&_prefs.usb_logging_enabled,
sizeof(_prefs.usb_logging_enabled)); // 140
if (file.available() >= (int)sizeof(_prefs.bluetooth_name)) {
file.read((uint8_t *)_prefs.bluetooth_name,
sizeof(_prefs.bluetooth_name)); // 141
}
}
}
}
@@ -428,6 +432,8 @@ bool DataStore::savePrefs(const CompanionNodePrefs& _prefs, double node_lat, dou
success = success && file.write((uint8_t *)&_prefs.usb_logging_enabled,
sizeof(_prefs.usb_logging_enabled))
== sizeof(_prefs.usb_logging_enabled); // 140
success = success && file.write((uint8_t *)_prefs.bluetooth_name,
sizeof(_prefs.bluetooth_name)) == sizeof(_prefs.bluetooth_name); // 141
#if defined(NRF52_PLATFORM)
success = file.commit(success);
+146 -2
View File
@@ -20,8 +20,8 @@
#include <helpers/ESP32TrueRandom.h>
#endif
#ifdef ENABLE_USB_INTERFACE
#include <helpers/CLICommandUtils.h>
#ifdef ENABLE_USB_INTERFACE
#include <helpers/TracePathHelpers.h>
#endif
@@ -122,6 +122,8 @@ static const uint32_t COMMAND_RADIO_APPLY_TIMEOUT_MS = 5000UL;
#define CMD_SET_RADIO_RXGAIN 69
#define CMD_GET_WIFI_POWER_SAVE 70
#define CMD_SET_WIFI_POWER_SAVE 71
#define CMD_GET_BLUETOOTH_NAME 72
#define CMD_SET_BLUETOOTH_NAME 73
#if defined(RADIO_FEM_RXGAIN) && (RADIO_FEM_RXGAIN == 0)
static constexpr uint8_t DEFAULT_FEM_RX_GAIN = 0;
@@ -1474,6 +1476,7 @@ MyMesh::MyMesh(mesh::Radio &radio, mesh::RNG &rng, mesh::RTCClock &rtc, SimpleMe
_prefs.powersaving_enabled = 1;
_prefs.powersaving_policy_version = 0;
_prefs.wifi_enabled = 1;
memset(_prefs.bluetooth_name, 0, sizeof(_prefs.bluetooth_name));
#if defined(MESH_DUAL_CDC_LOGGING)
// Keep the primary CDC stream exclusively framed unless the owner opts in
// to the separate plaintext logging interface and reboots.
@@ -1552,6 +1555,18 @@ void MyMesh::begin(bool has_display, bool radio_available) {
const bool power_saving_default_migrated =
migrateCompanionPowerSavingDefault(_prefs);
_prefs.node_name[sizeof(_prefs.node_name) - 1] = 0;
const bool bluetooth_name_had_terminator =
memchr(_prefs.bluetooth_name, 0, sizeof(_prefs.bluetooth_name)) != NULL;
_prefs.bluetooth_name[sizeof(_prefs.bluetooth_name) - 1] = 0;
const bool bluetooth_name_repaired =
!bluetooth_name_had_terminator
|| (_prefs.bluetooth_name[0] != 0
&& !mesh::companion::isValidBluetoothName(_prefs.bluetooth_name));
if (bluetooth_name_repaired) {
memset(_prefs.bluetooth_name, 0, sizeof(_prefs.bluetooth_name));
}
// sanitise bad pref values
_prefs.rx_delay_base = constrain(_prefs.rx_delay_base, 0, 20.0f);
_prefs.airtime_factor = constrain(_prefs.airtime_factor, 0, 9.0f);
@@ -1585,7 +1600,7 @@ void MyMesh::begin(bool has_display, bool radio_available) {
recalcRxPowerSavingFromLevel(_prefs.rx_ps_level, _prefs.sf, _prefs.bw,
_prefs.rx_ps_preamble, &_prefs.rx_ps_rx_us,
&_prefs.rx_ps_sleep_us);
if (power_saving_default_migrated) {
if (power_saving_default_migrated || bluetooth_name_repaired) {
_store->savePrefs(_prefs, sensors.node_lat, sensors.node_lon);
}
#if MESH_USB_LOGGING_AVAILABLE
@@ -1848,6 +1863,38 @@ bool MyMesh::handleLocalControlCommand(const char* command, char* reply,
if (!command || !reply || reply_size == 0) return false;
while (*command == ' ') command++;
if (strcmp(command, "get bluetooth.name") == 0
|| strcmp(command, "get ble.name") == 0) {
formatBluetoothNameStatus(reply, reply_size);
return true;
}
const char* bluetooth_name_value = NULL;
if (strncmp(command, "set bluetooth.name", 18) == 0
&& (command[18] == 0 || command[18] == ' '
|| command[18] == '\t')) {
bluetooth_name_value = command + 18;
} else if (strncmp(command, "set ble.name", 12) == 0
&& (command[12] == 0 || command[12] == ' '
|| command[12] == '\t')) {
bluetooth_name_value = command + 12;
}
if (bluetooth_name_value != NULL) {
while (*bluetooth_name_value == ' ' || *bluetooth_name_value == '\t') {
bluetooth_name_value++;
}
if (bluetooth_name_value[0] == 0) {
snprintf(reply, reply_size,
"Error: use set bluetooth.name <name|default>");
} else {
const bool clear_override = strcmp(bluetooth_name_value, "default") == 0
|| strcmp(bluetooth_name_value, "clear") == 0;
applyAndSaveBluetoothName(clear_override ? "" : bluetooth_name_value,
reply, reply_size);
}
return true;
}
#if defined(ESP32) && defined(WIFI_SSID)
#ifdef WITH_WEBCONFIG
if (strncmp(command, "get ", 4) == 0) {
@@ -2221,6 +2268,8 @@ static bool wcCopyValue(char* dest, size_t dest_size, const char* value) {
void MyMesh::getNodeSnapshot(WebConfigServer::NodeSnapshot& s) {
memset(&s, 0, sizeof(s));
wcCopyValue(s.name, sizeof(s.name), _prefs.node_name);
wcCopyValue(s.bluetooth_name, sizeof(s.bluetooth_name),
_prefs.bluetooth_name);
s.lat = sensors.node_lat;
s.lon = sensors.node_lon;
s.freq = _prefs.freq;
@@ -2241,6 +2290,9 @@ void MyMesh::getNodeSnapshot(WebConfigServer::NodeSnapshot& s) {
s.repeat = _prefs.client_repeat != 0;
s.capabilities = WebConfigServer::CAP_LOCATION | WebConfigServer::CAP_AIRTIME
| WebConfigServer::CAP_RX_DELAY | WebConfigServer::CAP_POWER_SAVING;
#ifdef BLE_PIN_CODE
s.capabilities |= WebConfigServer::CAP_BLUETOOTH_NAME;
#endif
#if defined(ESP32) && defined(WIFI_SSID)
s.capabilities |= WebConfigServer::CAP_WIFI_POWER_SAVE;
#endif
@@ -2342,6 +2394,11 @@ void MyMesh::onConfigBatchEnd() {
void MyMesh::execCommand(char* cmd, char* reply) {
reply[0] = 0;
if (cmd && (strcmp(cmd, "get bluetooth.name") == 0
|| strcmp(cmd, "get ble.name") == 0)) {
formatBluetoothNameStatus(reply, 160);
return;
}
#if defined(ESP32) && defined(WIFI_SSID)
if (cmd && strcmp(cmd, "get wifi.powersave") == 0) {
formatWiFiPowerSaving(reply, 160);
@@ -2370,6 +2427,10 @@ void MyMesh::execCommand(char* cmd, char* reply) {
}
return;
}
if (strcmp(key, "bluetooth.name") == 0) {
applyAndSaveBluetoothName(value, reply, 160);
return;
}
if (strcmp(key, "lat") == 0 || strcmp(key, "lon") == 0) {
double parsed;
const bool latitude = key[1] == 'a';
@@ -3738,6 +3799,31 @@ void MyMesh::handleCmdFrame(size_t len) {
#else
writeErrFrame(ERR_CODE_UNSUPPORTED_CMD);
#endif
} else if (cmd_frame[0] == CMD_GET_BLUETOOTH_NAME) {
out_frame[0] = RESP_CODE_OK;
out_frame[1] = mesh::companion::hasCustomBluetoothName(
_prefs.bluetooth_name) ? 1 : 0;
char* effective_name = reinterpret_cast<char*>(&out_frame[2]);
mesh::companion::formatBluetoothName(
effective_name, MAX_FRAME_SIZE - 1, _prefs.bluetooth_name,
BLE_NAME_PREFIX, _prefs.node_name);
_serial->writeFrame(out_frame, 2 + strlen(effective_name));
} else if (cmd_frame[0] == CMD_SET_BLUETOOTH_NAME) {
const size_t name_len = len - 1;
if (name_len > mesh::companion::BLUETOOTH_NAME_MAX_BYTES
|| memchr(&cmd_frame[1], 0, name_len) != NULL) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else {
char name[mesh::companion::BLUETOOTH_NAME_SIZE] = {0};
if (name_len != 0) memcpy(name, &cmd_frame[1], name_len);
if (name_len != 0 && !mesh::companion::isValidBluetoothName(name)) {
writeErrFrame(ERR_CODE_ILLEGAL_ARG);
} else if (!saveBluetoothNameOverride(name)) {
writeErrFrame(ERR_CODE_BAD_STATE);
} else {
writeOKFrame();
}
}
} else if (cmd_frame[0] == CMD_GET_ADVERT_PATH && len >= PUB_KEY_SIZE+2) {
// FUTURE use: uint8_t reserved = cmd_frame[1];
uint8_t *pub_key = &cmd_frame[2];
@@ -3971,6 +4057,62 @@ bool MyMesh::applyAndSaveRxBoostedGain(bool enabled) {
return true;
}
bool MyMesh::saveBluetoothNameOverride(const char* name) {
if (name == NULL) return false;
char previous[sizeof(_prefs.bluetooth_name)];
memcpy(previous, _prefs.bluetooth_name, sizeof(previous));
memset(_prefs.bluetooth_name, 0, sizeof(_prefs.bluetooth_name));
if (name[0] != 0) {
StrHelper::strncpy(_prefs.bluetooth_name, name,
sizeof(_prefs.bluetooth_name));
}
if (savePrefs()) return true;
memcpy(_prefs.bluetooth_name, previous, sizeof(_prefs.bluetooth_name));
return false;
}
bool MyMesh::applyAndSaveBluetoothName(const char* value, char* reply,
size_t reply_size) {
if (reply == NULL || reply_size == 0) return false;
const bool use_default = value == NULL || value[0] == 0;
if (!use_default && !mesh::companion::isValidBluetoothName(value)) {
snprintf(reply, reply_size,
"Error: Bluetooth name must be 1-%u valid UTF-8 bytes without control characters",
(unsigned)mesh::companion::BLUETOOTH_NAME_MAX_BYTES);
return false;
}
if (!saveBluetoothNameOverride(use_default ? "" : value)) {
snprintf(reply, reply_size, "Error: Bluetooth name save failed");
return false;
}
if (use_default) {
snprintf(reply, reply_size,
"OK - Bluetooth name follows %s<node name>; reboot required",
BLE_NAME_PREFIX);
} else {
snprintf(reply, reply_size,
"OK - Bluetooth name saved as '%s'; reboot required", value);
}
return true;
}
void MyMesh::formatBluetoothNameStatus(char* reply, size_t reply_size) const {
if (reply == NULL || reply_size == 0) return;
char effective_name[64];
mesh::companion::formatBluetoothName(
effective_name, sizeof(effective_name), _prefs.bluetooth_name,
BLE_NAME_PREFIX, _prefs.node_name);
snprintf(reply, reply_size, "> %s (%s)", effective_name,
mesh::companion::hasCustomBluetoothName(_prefs.bluetooth_name)
? "custom" : "default from node name");
}
bool MyMesh::applyAndSavePowerSaving(const char* value, char* reply) {
bool enabled;
if (strcmp(value, "on") == 0) {
@@ -5276,6 +5418,8 @@ void MyMesh::handleTerminalCommand(char* command) {
} else if (strcmp(command, "help") == 0) {
terminalOutput().print("Commands:\r\n");
terminalOutput().print(" set {name|lat|lon|freq|tx|af} {value}\r\n");
terminalOutput().print(" get bluetooth.name\r\n");
terminalOutput().print(" set bluetooth.name <name|default>\r\n");
terminalOutput().print(" powersaving [on|off]\r\n");
#if MESH_USB_LOGGING_AVAILABLE
terminalOutput().print(" get usb.logging\r\n");
+4
View File
@@ -311,6 +311,10 @@ private:
bool applyAndSaveFemRxGain(bool enabled);
bool applyAndSaveFemTxGain(bool enabled);
bool applyAndSaveRxBoostedGain(bool enabled);
bool saveBluetoothNameOverride(const char* name);
bool applyAndSaveBluetoothName(const char* value, char* reply,
size_t reply_size);
void formatBluetoothNameStatus(char* reply, size_t reply_size) const;
bool applyAndSavePowerSaving(const char* value, char* reply);
bool applyAndSaveRxPowerSaving(const char* value, char* reply);
void appendRxPowerSavingAdjustmentNote(char* reply, size_t reply_size,
+3
View File
@@ -1,6 +1,7 @@
#pragma once
#include <Arduino.h>
#include <cstdint> // For uint8_t, uint32_t
#include "BluetoothName.h"
#define TELEM_MODE_DENY 0
#define TELEM_MODE_ALLOW_FLAGS 1 // use contact.flags
@@ -54,6 +55,8 @@ struct CompanionNodePrefs { // persisted to file
uint8_t wifi_enabled; // Companion WiFi radio and services
uint8_t powersaving_policy_version; // one-time default migration marker
uint8_t usb_logging_enabled; // live USB packet/debug output
char bluetooth_name[mesh::companion::BLUETOOTH_NAME_SIZE];
// exact BLE name; empty uses BLE_NAME_PREFIX + node_name
// Keep the upstream repeat API while retaining the existing binary prefs
// layout used by this branch.
+7 -2
View File
@@ -982,8 +982,13 @@ void halt() {
scheduleCompanionBluetoothRetry();
return;
}
if (!bluetooth_interface.begin(BLE_NAME_PREFIX,
the_mesh.getNodePrefs()->node_name,
CompanionNodePrefs* prefs = the_mesh.getNodePrefs();
const bool custom_bluetooth_name =
mesh::companion::hasCustomBluetoothName(prefs->bluetooth_name);
if (!bluetooth_interface.begin(custom_bluetooth_name ? "" : BLE_NAME_PREFIX,
custom_bluetooth_name
? prefs->bluetooth_name
: prefs->node_name,
the_mesh.getBLEPin())) {
interface_manager.removeInterface(&bluetooth_interface);
mesh::usbLoggingPort().println(
+1 -1
View File
@@ -18,7 +18,7 @@
// separately, see wcIsAdminPasswordKey below.
static const char* const WC_ALLOWED_SET_KEYS[] = {
// NodePrefs (radio / node)
"name", "lat", "lon", "radio", "tx", "af", "rxdelay", "txdelay",
"name", "bluetooth.name", "lat", "lon", "radio", "tx", "af", "rxdelay", "txdelay",
"cad", "radio.rxgain", "radio.fem.rxgain", "radio.rxps", "powersaving",
"repeat",
"advert.interval", "flood.advert.interval",
+13 -4
View File
@@ -26,18 +26,27 @@ extern "C" bool bleInUse(void) {
#define ADVERT_RESTART_DELAY 1000 // millis
bool SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) {
bool SerialBLEInterface::begin(const char* prefix, const char* name, uint32_t pin_code) {
_pin_code = pin_code;
char resolved_name[32];
const char* suffix = name;
if (strcmp(name, "@@MAC") == 0) {
uint8_t addr[8];
memset(addr, 0, sizeof(addr));
esp_efuse_mac_get_default(addr);
sprintf(name, "%02X%02X%02X%02X%02X%02X", // modify (IN-OUT param)
addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]);
snprintf(resolved_name, sizeof(resolved_name),
"%02X%02X%02X%02X%02X%02X",
addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]);
suffix = resolved_name;
}
char dev_name[32+16];
sprintf(dev_name, "%s%s", prefix, name);
const int dev_name_len = snprintf(dev_name, sizeof(dev_name), "%s%s",
prefix, suffix);
if (dev_name_len < 0 || dev_name_len >= (int)sizeof(dev_name)) {
BLE_DEBUG_PRINTLN("Bluetooth device name is too long");
return false;
}
// Create the BLE Device
#if defined(CONFIG_NIMBLE_ENABLED)
+2 -2
View File
@@ -106,10 +106,10 @@ public:
/**
* init the BLE interface.
* @param prefix a prefix for the device name
* @param name IN/OUT - a name for the device (combined with prefix). If "@@MAC", is modified and returned
* @param name a name for the device (combined with prefix); "@@MAC" uses the hardware address
* @param pin_code the BLE security pin
*/
bool begin(const char* prefix, char* name, uint32_t pin_code);
bool begin(const char* prefix, const char* name, uint32_t pin_code);
// BaseSerialInterface methods
void enable() override;
File diff suppressed because it is too large. Load diff
+1
View File
@@ -1512,6 +1512,7 @@ void WebConfigServer::handleConfigGet(AsyncWebServerRequest* req) {
static const char* const LOOP_MODES[] = { "off", "minimal", "moderate", "strict" };
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["lat"] = node.lat;
radio["lon"] = node.lon;
radio["advert_interval"] = node.advert_interval;
+2
View File
@@ -72,6 +72,7 @@ public:
CAP_FEM_RX_GAIN = 1UL << 11,
CAP_RX_POWER_SAVING = 1UL << 12,
CAP_POWER_SAVING = 1UL << 13,
CAP_BLUETOOTH_NAME = 1UL << 14,
CAP_DELAYS = CAP_RX_DELAY | CAP_TX_DELAY,
};
@@ -80,6 +81,7 @@ public:
// assumes their member offsets match.
struct NodeSnapshot {
char name[32];
char bluetooth_name[32]; // empty = transport's default device name
char admin_password[32]; // empty = trusted LAN/no login prompt
double lat;
double lon;
+16 -5
View File
@@ -199,7 +199,7 @@ bool SerialBLEInterface::removeStoredBondForPeer(const char* cause) {
return true;
}
bool SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code) {
bool SerialBLEInterface::begin(const char* prefix, const char* name, uint32_t pin_code) {
instance = this;
char charpin[20];
@@ -214,15 +214,26 @@ bool SerialBLEInterface::begin(const char* prefix, char* name, uint32_t pin_code
return false;
}
char dev_name[32+16];
char resolved_name[32];
const char* suffix = name;
if (strcmp(name, "@@MAC") == 0) {
ble_gap_addr_t addr;
if (sd_ble_gap_addr_get(&addr) == NRF_SUCCESS) {
sprintf(name, "%02X%02X%02X%02X%02X%02X", // modify (IN-OUT param)
addr.addr[5], addr.addr[4], addr.addr[3], addr.addr[2], addr.addr[1], addr.addr[0]);
snprintf(resolved_name, sizeof(resolved_name),
"%02X%02X%02X%02X%02X%02X",
addr.addr[5], addr.addr[4], addr.addr[3], addr.addr[2],
addr.addr[1], addr.addr[0]);
suffix = resolved_name;
}
}
sprintf(dev_name, "%s%s", prefix, name);
char dev_name[32+16];
const int dev_name_len = snprintf(dev_name, sizeof(dev_name), "%s%s",
prefix, suffix);
if (dev_name_len < 0 || dev_name_len >= (int)sizeof(dev_name)) {
instance = nullptr;
BLE_DEBUG_PRINTLN("Bluetooth device name is too long");
return false;
}
// Connection interval units: 1.25ms, supervision timeout units: 10ms
ble_gap_conn_params_t ppcp_params;
+2 -2
View File
@@ -73,10 +73,10 @@ public:
/**
* init the BLE interface.
* @param prefix a prefix for the device name
* @param name IN/OUT - a name for the device (combined with prefix). If "@@MAC", is modified and returned
* @param name a name for the device (combined with prefix); "@@MAC" uses the hardware address
* @param pin_code the BLE security pin
*/
bool begin(const char* prefix, char* name, uint32_t pin_code);
bool begin(const char* prefix, const char* name, uint32_t pin_code);
void disconnect();
void enable() override;
@@ -96,6 +96,47 @@ TEST(CompanionNodePrefs, UsbLoggingStateIsIndependentFromTransports) {
EXPECT_EQ(1, prefs.powersaving_enabled);
}
TEST(CompanionNodePrefs, BluetoothNameOverrideIsIndependentFromNodeName) {
CompanionNodePrefs prefs = {};
strcpy(prefs.node_name, "RidgeNode");
strcpy(prefs.bluetooth_name, "Workshop Companion");
char effective[64];
mesh::companion::formatBluetoothName(
effective, sizeof(effective), prefs.bluetooth_name, "MeshCore-",
prefs.node_name);
EXPECT_STREQ("Workshop Companion", effective);
strcpy(prefs.bluetooth_name, "default");
mesh::companion::formatBluetoothName(
effective, sizeof(effective), prefs.bluetooth_name, "MeshCore-",
prefs.node_name);
EXPECT_STREQ("default", effective);
prefs.bluetooth_name[0] = 0;
mesh::companion::formatBluetoothName(
effective, sizeof(effective), prefs.bluetooth_name, "MeshCore-",
prefs.node_name);
EXPECT_STREQ("MeshCore-RidgeNode", effective);
}
TEST(CompanionNodePrefs, BluetoothNameRequiresBoundedValidUtf8) {
EXPECT_TRUE(mesh::companion::isValidBluetoothName("Field Unit 4"));
EXPECT_TRUE(mesh::companion::isValidBluetoothName(
"Field Unit \xF0\x9F\x93\xA1"));
EXPECT_FALSE(mesh::companion::isValidBluetoothName(""));
EXPECT_FALSE(mesh::companion::isValidBluetoothName(" "));
EXPECT_FALSE(mesh::companion::isValidBluetoothName("bad\nname"));
char too_long[mesh::companion::BLUETOOTH_NAME_SIZE + 1];
memset(too_long, 'A', sizeof(too_long));
too_long[sizeof(too_long) - 1] = 0;
EXPECT_FALSE(mesh::companion::isValidBluetoothName(too_long));
const char malformed[] = {'A', static_cast<char>(0x80), 0};
EXPECT_FALSE(mesh::companion::isValidBluetoothName(malformed));
}
TEST(CompanionNodePrefs, MigratesRegressedPowerSavingDefaultOnce) {
CompanionNodePrefs prefs = {};
prefs.powersaving_enabled = 0;
@@ -8,6 +8,7 @@
TEST(WebConfigKeys, AllowsKnownScalarKeys) {
EXPECT_TRUE(wcIsAllowedSetKey("name"));
EXPECT_TRUE(wcIsAllowedSetKey("bluetooth.name"));
EXPECT_TRUE(wcIsAllowedSetKey("radio"));
EXPECT_TRUE(wcIsAllowedSetKey("radio.rxps"));
EXPECT_TRUE(wcIsAllowedSetKey("powersaving"));
+5 -1
View File
@@ -345,6 +345,9 @@ body.tab-cli{padding-bottom:0}
<div class="card">
<h2>Node</h2>
<div class="f"><label>Name</label><input type="text" data-k="name" maxlength="31"></div>
<div class="f" data-cap="16384"><label>Bluetooth name</label>
<input type="text" data-k="bluetooth.name" maxlength="31" placeholder="Use MeshCore-&lt;node name&gt;">
<div class="hint">Optional complete Bluetooth device name. Leave blank to follow the node name.</div></div>
<div class="row" data-cap="1">
<div class="f"><label>Latitude</label><input type="number" data-k="lat" step="any"></div>
<div class="f"><label>Longitude</label><input type="number" data-k="lon" step="any"></div>
@@ -734,7 +737,8 @@ function cfgVal(k){ // map a `set` key to its current value string, from st.cfg
var c=st.cfg,m,r=c.radio,q=c.mqtt||{slots:[]},w=c.wifi;
if((m=k.match(/^mqtt([1-9])\.(\w+)$/))){var s=(q.slots||[])[+m[1]-1]||{};var v=s[m[2]];return v==null?"":String(v)}
switch(k){
case"name":return r.name;case"lat":return String(r.lat);case"lon":return String(r.lon);
case"name":return r.name;case"bluetooth.name":return r.bluetooth_name||"";
case"lat":return String(r.lat);case"lon":return String(r.lon);
case"tx":return String(r.tx);case"af":return String(r.af);
case"rxdelay":return String(r.rxdelay);case"txdelay":return String(r.txdelay);
case"cad":return r.cad?"on":"off";case"radio.rxgain":return r.rxgain?"on":"off";