Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions Firmware/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,9 +263,9 @@ Each MCP23017 is tracked at runtime rather than only at boot:

## Tests

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

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

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

Expand Down
113 changes: 113 additions & 0 deletions Firmware/include/device_name.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// ============================================================================
// device_name.h — the advertised BLE name, "HiveTraffic-AB:12"
// ============================================================================
//
// Every counter used to advertise the identical local name, so an apiary with
// several in range produced a scan list of identical rows and the only way to
// tell one entry from another was to open a scanner's detail view and read the
// address. The name now carries the last two bytes of the counter's own BLE
// address, formatted the way a scanner prints them, so the list is
// self-identifying:
//
// HiveTraffic-AB:12 <- the node whose address ends ...:AB:12
// HiveTraffic-4F:9C
//
// Nothing on the wire keys off the name. HiveHub connects by the MAC paired in
// its portal (docs/ble-mode.md), so this is a display string for whoever is
// standing at the hive with a phone — during pairing, or when working out
// which of three counters on a bench is the one being flashed.
//
// Arduino-free, like gate_logic.h and bank_state.h, and pinned by
// test/test_device_name/ for the same reason: the failure mode is quiet. A
// wrong byte order or an off-by-one in the buffer produces a name that still
// looks plausible in a scan list while pointing at the wrong device, and the
// only way to notice on hardware is to already know the address you were
// looking for. src/ble_link.cpp owns the radio and calls in here for the
// string.
//
// Byte order
// ----------
// NimBLE stores an address little-endian — val[5] is the byte a scanner prints
// first, val[0] the last — so the two bytes wanted here are val[1] and val[0],
// in that order. Getting this backwards is the mistake this header exists to
// prevent: it yields a suffix that is a real part of the address, just
// reversed, so it passes a glance and fails exactly when someone tries to
// match it against what the scanner shows.
//
// Which address ends up in the name
// ---------------------------------
// The one the counter advertises with. NimBLE settles its own-address type
// while starting up and both the address it reports and the packets it sends
// follow it, so the two always agree. In practice that is the controller's
// public address — on the ESP32-C6 the factory eFuse MAC: unique per unit and
// stable across reboots, reflashes and OTA updates, with nothing to provision
// per device. The suffix is therefore literally the tail of the address the
// scanner shows beside the entry.
// ============================================================================

#pragma once

#include <stddef.h>
#include <stdint.h>

namespace devicename {

// The product half of the name, without the address suffix. Also the fallback
// if the address cannot be read.
constexpr char BASE[] = "HiveTraffic";

// "-AB:12": a separator, two hex digits, a colon, two more.
constexpr size_t SUFFIX_LENGTH = 6;

// Longest name this header produces, NUL included. sizeof(BASE) already counts
// the terminator.
constexpr size_t CAPACITY = sizeof(BASE) + SUFFIX_LENGTH;

// A legacy scan response holds 31 bytes, of which an AD structure spends two on
// its length and type. ble_link.cpp puts the name in the scan response alone,
// so this is the whole budget it has to fit in — checked here rather than in a
// comment there, because the name is what would grow.
static_assert(CAPACITY - 1 + 2 <= 31,
"the advertised name does not fit a legacy scan response");

// Build the advertised name into `out`, returning its length (excluding the
// NUL) or 0 if the buffer is too small to hold the complete name.
//
// `addr_val` is a NimBLE-order (little-endian) six-byte address, or nullptr
// when none could be read — in which case the bare product name is used. An
// unsuffixed name is a far better failure than no name, or than a plausible
// "HiveTraffic-00:00" that several counters would then share.
//
// Refusing to truncate is deliberate: a half-written suffix is a name that
// identifies the wrong device, which is worse than one that identifies no
// device in particular.
inline size_t build(char* out, size_t capacity, const uint8_t* addr_val) {
// Lower-case deliberately: this header is compiled after Arduino.h, whose
// Print.h defines `HEX` as 16. An all-caps name here is not a style choice
// but a build break — see the macro block in test/test_device_name/.
static const char hex_digits[] = "0123456789ABCDEF";

if (out == nullptr) return 0;

const size_t base_length = sizeof(BASE) - 1;
const size_t length =
base_length + (addr_val != nullptr ? SUFFIX_LENGTH : 0);
if (capacity < length + 1) return 0;

size_t i = 0;
for (; i < base_length; ++i) out[i] = BASE[i];

if (addr_val != nullptr) {
out[i++] = '-';
out[i++] = hex_digits[addr_val[1] >> 4];
out[i++] = hex_digits[addr_val[1] & 0x0F];
out[i++] = ':';
out[i++] = hex_digits[addr_val[0] >> 4];
out[i++] = hex_digits[addr_val[0] & 0x0F];
}

out[i] = '\0';
return i;
}

} // namespace devicename
2 changes: 1 addition & 1 deletion Firmware/include/version.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@
// and it is the only way to confirm afterwards that an update actually took.
//
// Bump this on every released image.
#define HIVETRAFFIC_FW_VERSION "0.3.0"
#define HIVETRAFFIC_FW_VERSION "0.3.1"
71 changes: 65 additions & 6 deletions Firmware/src/ble_link.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@
#include <stdio.h>

#include "counter_protocol.h"
#include "device_name.h"
#include "measurement_json.h"
#include "version.h"

namespace ble {
namespace {

constexpr char BLE_DEVICE_NAME[] = "BeeCounter";
// The advertised local name, "HiveTraffic-AB:12", built once in begin() from
// this counter's own BLE address so several in range are distinguishable in a
// scan list (include/device_name.h). File scope rather than a local in begin()
// because the log line below reads it too, and because it is the one place the
// name exists — nothing should reconstruct it.
char deviceName[devicename::CAPACITY];

constexpr char SVC_BEECOUNTER[] = "8e8b0101-7a1c-4b9e-9a2f-1d6e0b9c1a01";
constexpr char CHR_MEASUREMENT[] = "8e8b0102-7a1c-4b9e-9a2f-1d6e0b9c1a01";
// Night mode: HiveHub writes a suspension DURATION here, and reads back the
Expand Down Expand Up @@ -352,6 +359,49 @@ class ServerCallbacks : public NimBLEServerCallbacks {
}
};

// Fill deviceName with "HiveTraffic-AB:12", the product name suffixed with the
// last two bytes of this counter's own BLE address.
//
// Must run AFTER NimBLEDevice::init(): the address comes from the controller,
// and init() is what starts it — it blocks until the host and controller have
// synced, so the address is readable the moment it returns, but not one line
// earlier.
//
// The address NimBLE reports here is the one it goes on to advertise with:
// both read the own-address type that init() settled, which is the controller's
// public address whenever it has one — on the ESP32-C6 the factory eFuse MAC.
// The suffix is therefore literally the tail of the address a scanner shows
// beside the entry, needs no provisioning, and survives reboots, reflashes and
// OTA updates.
//
// If the address cannot be read, the bare product name is advertised. That is
// a far better failure than not advertising at all: a counter HiveHub cannot
// see is invisible to the measurement read AND to the OTA relay, and HiveHub
// finds it by the paired MAC regardless of what the name says.
void buildDeviceName() {
const NimBLEAddress address = NimBLEDevice::getAddress();
const bool haveAddress = !address.isNull();

if (!devicename::build(deviceName, sizeof(deviceName),
haveAddress ? address.getVal() : nullptr)) {
// Only reachable if CAPACITY and the name it sizes ever disagree, which
// the header's static_assert and test/test_device_name/ both rule out.
// Left as a hard fallback rather than an assert: an unnamed counter
// still counts bees and still relays firmware.
deviceName[0] = '\0';
}
if (!haveAddress) {
Serial.println(F("[BLE] no address available; advertising unsuffixed name"));
}

// Keep the GAP Device Name characteristic in step with the advertised one,
// so a client that connects and reads it — rather than trusting the scan
// response — sees the same identity.
if (!NimBLEDevice::setDeviceName(deviceName)) {
Serial.println(F("[BLE] GAP device name not updated"));
}
}

// NimBLE stores these pointers for the lifetime of the server and never frees
// them: NimBLECharacteristic::setCallbacks() takes no ownership at all, so the
// old `new X(), true` form both fails to compile against NimBLE 2.5.x (the
Expand All @@ -367,7 +417,13 @@ ServerCallbacks serverCallbacks;
} // namespace

void begin() {
NimBLEDevice::init(BLE_DEVICE_NAME);
// init() takes a name because it must set one before the GATT server
// exists; the address it is built from is only available once init() has
// synced the host and controller, so the suffixed name is applied
// immediately afterwards by buildDeviceName().
NimBLEDevice::init(devicename::BASE);
buildDeviceName();

NimBLEServer* server = NimBLEDevice::createServer();
// false: never delete a statically allocated callback object.
server->setCallbacks(&serverCallbacks, false);
Expand Down Expand Up @@ -404,18 +460,19 @@ void begin() {
//
// flags 3 (added by NimBLE at start())
// 128-bit service UUID 18 (2 + 16)
// "BeeCounter" 12 (2 + 10) -> 33 > 31
// "HiveTraffic-AB:12" 19 (2 + 17) -> 40 > 31
//
// NimBLE 2.x leaves scan response DISABLED by default and does not silently
// relocate the name, so setting all three on the advertisement overflows and
// something is dropped — potentially advertising itself. A counter that does
// not advertise is invisible to BOTH the measurement read and the OTA relay,
// which locates it by a scan first (HiveHub ble_sensor.cpp::otaBegin).
// Splitting them keeps the advertisement at 21 bytes and the scan response
// at 12, with room to spare on each.
// at 19, with room to spare on each. The name is the element that grows, so
// device_name.h static_asserts its own longest form against that 31.
advertising->addServiceUUID(service->getUUID());
NimBLEAdvertisementData scanResponse;
scanResponse.setName(BLE_DEVICE_NAME);
scanResponse.setName(deviceName);
advertising->setScanResponseData(scanResponse);
advertising->enableScanResponse(true);
advertising->setMinInterval(ADV_INTERVAL_UNITS);
Expand All @@ -426,7 +483,9 @@ void begin() {
Serial.println(F("[BLE] ERROR: advertising failed to start"));
return;
}
Serial.printf("[BLE] HiveTraffic %s advertising for HiveHub\n",
// The name is logged, not just the version: it is what someone comparing
// the serial console with a scan list on their phone needs to match up.
Serial.printf("[BLE] %s %s advertising for HiveHub\n", deviceName,
HIVETRAFFIC_FW_VERSION);
}

Expand Down
11 changes: 10 additions & 1 deletion Firmware/test/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
# the millis() rollover and the "HiveHub stopped
# re-arming" case;
# * include/bank_state.h — the emitter-bank enable mask, whose mistakes
# are eight gates that silently stop counting.
# are eight gates that silently stop counting;
# * include/device_name.h — the advertised BLE name, whose address suffix
# is how a person tells two counters apart.
#
# Everything hardware-facing stays in src/main.cpp and is still verified on the
# bench (see the IR_DEBUG console in the README).
Expand Down Expand Up @@ -56,3 +58,10 @@ trap 'rm -rf "$OUT"' EXIT
-o "$OUT/test_bank_state"

"$OUT/test_bank_state"

"$CXX" -std=c++11 -Wall -Wextra -Werror \
-I include \
test/test_device_name/test_device_name.cpp \
-o "$OUT/test_device_name"

"$OUT/test_device_name"
Loading
Loading