Skip to content

Commit f2408f2

Browse files
authored
Merge pull request #16 from MacNite/claude/hivetraffic-mac-device-names-80v2ln
Add device name suffix with BLE address for counter identification
2 parents 3213338 + 94f0ad3 commit f2408f2

8 files changed

Lines changed: 449 additions & 16 deletions

File tree

Firmware/README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -263,9 +263,9 @@ Each MCP23017 is tracked at runtime rather than only at boot:
263263

264264
## Tests
265265

266-
Two pure headers carry logic that is worth testing without hardware, so both are
267-
deliberately free of Arduino, I2C and NimBLE dependencies and both run on a host
268-
compiler:
266+
Several pure headers carry logic that is worth testing without hardware, so each
267+
is deliberately free of Arduino, I2C and NimBLE dependencies and all of them run
268+
on a host compiler:
269269

270270
```
271271
./test/run_tests.sh
@@ -301,6 +301,14 @@ No ESP32, no MCP23017 and no bee required.
301301
sized without it. The suite pins the refusal of an all-off mask, the
302302
one-based bank numbering that `gates::TABLE[].led_bank` depends on, and the
303303
masking of bits above the last physical bank.
304+
* **`include/device_name.h`** — the advertised BLE name, `HiveTraffic-AB:12`.
305+
The suffix is the last two bytes of the counter's own address, and every way
306+
of getting it wrong produces a name that still looks like a name while
307+
pointing at the wrong device: the suite pins the byte order (NimBLE stores
308+
addresses little-endian, so the two bytes are `val[1]`, `val[0]`), the
309+
two-digit uppercase rendering a scanner matches against, the fallback when no
310+
address can be read, and the refusal to truncate into a buffer that cannot
311+
hold the whole name.
304312

305313
Everything hardware-facing stays in `src/main.cpp` and is still verified on the
306314
bench with the IR-sensor console above.
@@ -319,7 +327,7 @@ Easy Bee Counter 2026 — firmware booting (BLE/GATT link)
319327
[MCP] U2 (gates 00..07) @ 0x20: OK
320328
[MCP] U3 (gates 10..17) @ 0x21: OK
321329
[MCP] U4 (gates 20..27) @ 0x22: OK
322-
[BLE] HiveTraffic 0.1.0 advertising for HiveHub
330+
[BLE] HiveTraffic-AB:12 0.3.1 advertising for HiveHub
323331
[SETUP] Entering normal counting loop (pulsed IR)
324332
```
325333

Firmware/include/device_name.h

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// ============================================================================
2+
// device_name.h — the advertised BLE name, "HiveTraffic-AB:12"
3+
// ============================================================================
4+
//
5+
// Every counter used to advertise the identical local name, so an apiary with
6+
// several in range produced a scan list of identical rows and the only way to
7+
// tell one entry from another was to open a scanner's detail view and read the
8+
// address. The name now carries the last two bytes of the counter's own BLE
9+
// address, formatted the way a scanner prints them, so the list is
10+
// self-identifying:
11+
//
12+
// HiveTraffic-AB:12 <- the node whose address ends ...:AB:12
13+
// HiveTraffic-4F:9C
14+
//
15+
// Nothing on the wire keys off the name. HiveHub connects by the MAC paired in
16+
// its portal (docs/ble-mode.md), so this is a display string for whoever is
17+
// standing at the hive with a phone — during pairing, or when working out
18+
// which of three counters on a bench is the one being flashed.
19+
//
20+
// Arduino-free, like gate_logic.h and bank_state.h, and pinned by
21+
// test/test_device_name/ for the same reason: the failure mode is quiet. A
22+
// wrong byte order or an off-by-one in the buffer produces a name that still
23+
// looks plausible in a scan list while pointing at the wrong device, and the
24+
// only way to notice on hardware is to already know the address you were
25+
// looking for. src/ble_link.cpp owns the radio and calls in here for the
26+
// string.
27+
//
28+
// Byte order
29+
// ----------
30+
// NimBLE stores an address little-endian — val[5] is the byte a scanner prints
31+
// first, val[0] the last — so the two bytes wanted here are val[1] and val[0],
32+
// in that order. Getting this backwards is the mistake this header exists to
33+
// prevent: it yields a suffix that is a real part of the address, just
34+
// reversed, so it passes a glance and fails exactly when someone tries to
35+
// match it against what the scanner shows.
36+
//
37+
// Which address ends up in the name
38+
// ---------------------------------
39+
// The one the counter advertises with. NimBLE settles its own-address type
40+
// while starting up and both the address it reports and the packets it sends
41+
// follow it, so the two always agree. In practice that is the controller's
42+
// public address — on the ESP32-C6 the factory eFuse MAC: unique per unit and
43+
// stable across reboots, reflashes and OTA updates, with nothing to provision
44+
// per device. The suffix is therefore literally the tail of the address the
45+
// scanner shows beside the entry.
46+
// ============================================================================
47+
48+
#pragma once
49+
50+
#include <stddef.h>
51+
#include <stdint.h>
52+
53+
namespace devicename {
54+
55+
// The product half of the name, without the address suffix. Also the fallback
56+
// if the address cannot be read.
57+
constexpr char BASE[] = "HiveTraffic";
58+
59+
// "-AB:12": a separator, two hex digits, a colon, two more.
60+
constexpr size_t SUFFIX_LENGTH = 6;
61+
62+
// Longest name this header produces, NUL included. sizeof(BASE) already counts
63+
// the terminator.
64+
constexpr size_t CAPACITY = sizeof(BASE) + SUFFIX_LENGTH;
65+
66+
// A legacy scan response holds 31 bytes, of which an AD structure spends two on
67+
// its length and type. ble_link.cpp puts the name in the scan response alone,
68+
// so this is the whole budget it has to fit in — checked here rather than in a
69+
// comment there, because the name is what would grow.
70+
static_assert(CAPACITY - 1 + 2 <= 31,
71+
"the advertised name does not fit a legacy scan response");
72+
73+
// Build the advertised name into `out`, returning its length (excluding the
74+
// NUL) or 0 if the buffer is too small to hold the complete name.
75+
//
76+
// `addr_val` is a NimBLE-order (little-endian) six-byte address, or nullptr
77+
// when none could be read — in which case the bare product name is used. An
78+
// unsuffixed name is a far better failure than no name, or than a plausible
79+
// "HiveTraffic-00:00" that several counters would then share.
80+
//
81+
// Refusing to truncate is deliberate: a half-written suffix is a name that
82+
// identifies the wrong device, which is worse than one that identifies no
83+
// device in particular.
84+
inline size_t build(char* out, size_t capacity, const uint8_t* addr_val) {
85+
// Lower-case deliberately: this header is compiled after Arduino.h, whose
86+
// Print.h defines `HEX` as 16. An all-caps name here is not a style choice
87+
// but a build break — see the macro block in test/test_device_name/.
88+
static const char hex_digits[] = "0123456789ABCDEF";
89+
90+
if (out == nullptr) return 0;
91+
92+
const size_t base_length = sizeof(BASE) - 1;
93+
const size_t length =
94+
base_length + (addr_val != nullptr ? SUFFIX_LENGTH : 0);
95+
if (capacity < length + 1) return 0;
96+
97+
size_t i = 0;
98+
for (; i < base_length; ++i) out[i] = BASE[i];
99+
100+
if (addr_val != nullptr) {
101+
out[i++] = '-';
102+
out[i++] = hex_digits[addr_val[1] >> 4];
103+
out[i++] = hex_digits[addr_val[1] & 0x0F];
104+
out[i++] = ':';
105+
out[i++] = hex_digits[addr_val[0] >> 4];
106+
out[i++] = hex_digits[addr_val[0] & 0x0F];
107+
}
108+
109+
out[i] = '\0';
110+
return i;
111+
}
112+
113+
} // namespace devicename

Firmware/include/version.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@
1414
// and it is the only way to confirm afterwards that an update actually took.
1515
//
1616
// Bump this on every released image.
17-
#define HIVETRAFFIC_FW_VERSION "0.3.0"
17+
#define HIVETRAFFIC_FW_VERSION "0.3.1"

Firmware/src/ble_link.cpp

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,20 @@
77
#include <stdio.h>
88

99
#include "counter_protocol.h"
10+
#include "device_name.h"
1011
#include "measurement_json.h"
1112
#include "version.h"
1213

1314
namespace ble {
1415
namespace {
1516

16-
constexpr char BLE_DEVICE_NAME[] = "BeeCounter";
17+
// The advertised local name, "HiveTraffic-AB:12", built once in begin() from
18+
// this counter's own BLE address so several in range are distinguishable in a
19+
// scan list (include/device_name.h). File scope rather than a local in begin()
20+
// because the log line below reads it too, and because it is the one place the
21+
// name exists — nothing should reconstruct it.
22+
char deviceName[devicename::CAPACITY];
23+
1724
constexpr char SVC_BEECOUNTER[] = "8e8b0101-7a1c-4b9e-9a2f-1d6e0b9c1a01";
1825
constexpr char CHR_MEASUREMENT[] = "8e8b0102-7a1c-4b9e-9a2f-1d6e0b9c1a01";
1926
// Night mode: HiveHub writes a suspension DURATION here, and reads back the
@@ -352,6 +359,49 @@ class ServerCallbacks : public NimBLEServerCallbacks {
352359
}
353360
};
354361

362+
// Fill deviceName with "HiveTraffic-AB:12", the product name suffixed with the
363+
// last two bytes of this counter's own BLE address.
364+
//
365+
// Must run AFTER NimBLEDevice::init(): the address comes from the controller,
366+
// and init() is what starts it — it blocks until the host and controller have
367+
// synced, so the address is readable the moment it returns, but not one line
368+
// earlier.
369+
//
370+
// The address NimBLE reports here is the one it goes on to advertise with:
371+
// both read the own-address type that init() settled, which is the controller's
372+
// public address whenever it has one — on the ESP32-C6 the factory eFuse MAC.
373+
// The suffix is therefore literally the tail of the address a scanner shows
374+
// beside the entry, needs no provisioning, and survives reboots, reflashes and
375+
// OTA updates.
376+
//
377+
// If the address cannot be read, the bare product name is advertised. That is
378+
// a far better failure than not advertising at all: a counter HiveHub cannot
379+
// see is invisible to the measurement read AND to the OTA relay, and HiveHub
380+
// finds it by the paired MAC regardless of what the name says.
381+
void buildDeviceName() {
382+
const NimBLEAddress address = NimBLEDevice::getAddress();
383+
const bool haveAddress = !address.isNull();
384+
385+
if (!devicename::build(deviceName, sizeof(deviceName),
386+
haveAddress ? address.getVal() : nullptr)) {
387+
// Only reachable if CAPACITY and the name it sizes ever disagree, which
388+
// the header's static_assert and test/test_device_name/ both rule out.
389+
// Left as a hard fallback rather than an assert: an unnamed counter
390+
// still counts bees and still relays firmware.
391+
deviceName[0] = '\0';
392+
}
393+
if (!haveAddress) {
394+
Serial.println(F("[BLE] no address available; advertising unsuffixed name"));
395+
}
396+
397+
// Keep the GAP Device Name characteristic in step with the advertised one,
398+
// so a client that connects and reads it — rather than trusting the scan
399+
// response — sees the same identity.
400+
if (!NimBLEDevice::setDeviceName(deviceName)) {
401+
Serial.println(F("[BLE] GAP device name not updated"));
402+
}
403+
}
404+
355405
// NimBLE stores these pointers for the lifetime of the server and never frees
356406
// them: NimBLECharacteristic::setCallbacks() takes no ownership at all, so the
357407
// old `new X(), true` form both fails to compile against NimBLE 2.5.x (the
@@ -367,7 +417,13 @@ ServerCallbacks serverCallbacks;
367417
} // namespace
368418

369419
void begin() {
370-
NimBLEDevice::init(BLE_DEVICE_NAME);
420+
// init() takes a name because it must set one before the GATT server
421+
// exists; the address it is built from is only available once init() has
422+
// synced the host and controller, so the suffixed name is applied
423+
// immediately afterwards by buildDeviceName().
424+
NimBLEDevice::init(devicename::BASE);
425+
buildDeviceName();
426+
371427
NimBLEServer* server = NimBLEDevice::createServer();
372428
// false: never delete a statically allocated callback object.
373429
server->setCallbacks(&serverCallbacks, false);
@@ -404,18 +460,19 @@ void begin() {
404460
//
405461
// flags 3 (added by NimBLE at start())
406462
// 128-bit service UUID 18 (2 + 16)
407-
// "BeeCounter" 12 (2 + 10) -> 33 > 31
463+
// "HiveTraffic-AB:12" 19 (2 + 17) -> 40 > 31
408464
//
409465
// NimBLE 2.x leaves scan response DISABLED by default and does not silently
410466
// relocate the name, so setting all three on the advertisement overflows and
411467
// something is dropped — potentially advertising itself. A counter that does
412468
// not advertise is invisible to BOTH the measurement read and the OTA relay,
413469
// which locates it by a scan first (HiveHub ble_sensor.cpp::otaBegin).
414470
// Splitting them keeps the advertisement at 21 bytes and the scan response
415-
// at 12, with room to spare on each.
471+
// at 19, with room to spare on each. The name is the element that grows, so
472+
// device_name.h static_asserts its own longest form against that 31.
416473
advertising->addServiceUUID(service->getUUID());
417474
NimBLEAdvertisementData scanResponse;
418-
scanResponse.setName(BLE_DEVICE_NAME);
475+
scanResponse.setName(deviceName);
419476
advertising->setScanResponseData(scanResponse);
420477
advertising->enableScanResponse(true);
421478
advertising->setMinInterval(ADV_INTERVAL_UNITS);
@@ -426,7 +483,9 @@ void begin() {
426483
Serial.println(F("[BLE] ERROR: advertising failed to start"));
427484
return;
428485
}
429-
Serial.printf("[BLE] HiveTraffic %s advertising for HiveHub\n",
486+
// The name is logged, not just the version: it is what someone comparing
487+
// the serial console with a scan list on their phone needs to match up.
488+
Serial.printf("[BLE] %s %s advertising for HiveHub\n", deviceName,
430489
HIVETRAFFIC_FW_VERSION);
431490
}
432491

Firmware/test/run_tests.sh

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
# the millis() rollover and the "HiveHub stopped
1414
# re-arming" case;
1515
# * include/bank_state.h — the emitter-bank enable mask, whose mistakes
16-
# are eight gates that silently stop counting.
16+
# are eight gates that silently stop counting;
17+
# * include/device_name.h — the advertised BLE name, whose address suffix
18+
# is how a person tells two counters apart.
1719
#
1820
# Everything hardware-facing stays in src/main.cpp and is still verified on the
1921
# bench (see the IR_DEBUG console in the README).
@@ -56,3 +58,10 @@ trap 'rm -rf "$OUT"' EXIT
5658
-o "$OUT/test_bank_state"
5759

5860
"$OUT/test_bank_state"
61+
62+
"$CXX" -std=c++11 -Wall -Wextra -Werror \
63+
-I include \
64+
test/test_device_name/test_device_name.cpp \
65+
-o "$OUT/test_device_name"
66+
67+
"$OUT/test_device_name"

0 commit comments

Comments
 (0)