Skip to content

Night mode: a bounded sensing suspension (protocol v4) - #14

Merged
MacNite merged 3 commits into
mainfrom
claude/night-mode-review-setryb
Aug 21, 2026
Merged

MacNite merged 3 commits into
mainfrom
claude/night-mode-review-setryb

Conversation

@MacNite

@MacNite MacNite commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Adds night mode to the counter, plus the documentation fixes around it.

Pairs with MacNite/HiveHub#167, which owns the schedule. Deploy order below.

Why

The 48 IR emitters are this board's power budget: 24 series pairs behind 22 Ω ballast, all lit together for the settle+read window of every 5 ms poll. That is roughly 0.5–1.0 A peak and, at the pulsed sampler's ~35 % duty, an average an order of magnitude above the ~18 mA the C6 and the three expanders draw between them.

European honey bees are diurnal — flight requires light, and stops below roughly 10 °C regardless — so overnight that current buys nothing.

What the counter gained

One control characteristic (8e8b0103) that HiveHub writes a suspension duration to. The counter has no RTC, no NVS and no network, and this deliberately does not give it any: it is told how long to stay quiet, never when. Everything that matters falls out of that.

  • It expires. A HiveHub that dies cannot leave a counter blind.
  • It is capped at MAX_IDLE_SECONDS (1 h), several times HiveHub's upload cycle, so HiveHub re-arms per cycle and drift never accumulates. An over-long request is clamped, not refused — refusing would leave the emitters running all night because one field was too large.
  • It is never persisted. Any reset comes back counting.
  • It is refused during an OTA, which already parks the emitters itself.

Gate state machines are reset on both edges, so a half-finished pairing cannot span the gap and fabricate a crossing.

Why not deep sleep

Deep sleep was the obvious implementation and is the wrong one. Parking the emitters captures >90 % of the saving; sleeping for the rest costs:

  • the measurement read — every night row would carry bee_counter.ok=false;
  • the OTA relay path, which HiveHub's docs specifically recommend using at night;
  • the ability to cancel a schedule that turns out to be wrong;
  • the lifetime totals, which live in RAM;
  • a truthful uptime_s — every morning would look like an unexplained reboot;
  • and accuracy: the C6's deep-sleep timer runs off the temperature-dependent internal RC oscillator. Over an 8–12 h night in a hive swinging 10–25 °C, expect minutes of drift and up to ~30 minutes worst case. Re-arming a short grant from HiveHub's DS3231 (±2 ppm) means nothing accumulates.

Protocol v4

idle_s counts the suspension down and status bit 0x80 marks it. Without them a night of zeros is indistinguishable from a counter whose emitter FETs have died. The change is purely additive, so HiveHub's key-skipping parser reads v4 documents unchanged.

Loose ends fixed while in the area

  • README.md §6 claimed the emitters run continuously and could be switched with a CMD_LEDS_OFF that went away with the wired link. It also understated the emitter draw — the whole justification for this feature.
  • docs/ble-mode.md's "deep sleep is incompatible with uninterrupted counting" now says what is actually true.
  • The security note now covers the new write path: a SET_IDLE is a new way to deny counting for anyone in radio range. Strictly the lesser of the two exposures — it expires, changes nothing persistent, and anyone able to do it could already replace the firmware — but named rather than left to be discovered.

Tests

include/idle_state.h is a pure header covering the millis() rollover and the "HiveHub stopped re-arming" case, neither of which is reproducible on a hive. test_measurement_json pins the v4 document and asserts bit 0x80 does not collide.

This repo had no CI, so neither the host tests nor the ESP32 build were checked anywhere but a laptop. Added a workflow that runs both, plus the -DIR_DEBUG bench console, which has no environment of its own and would otherwise go stale unnoticed.

Verified locally: all three host suites pass. The ESP32 build is checked by the new CI workflow only — the sandbox this was written in blocks the PlatformIO registry, so the firmware has not been compiled here.

Deploy order

HiveHub's tolerant parser must be deployed before any counter emitting fw:4. In practice the parser already handled it (unknown keys are skipped), but HiveHub is what decides when to suspend, so it needs to understand idle_s first.


Generated by Claude Code

The 48 IR emitters are the counter's power budget: 24 series pairs behind
22 ohm ballast, all lit together for the settle+read window of every 5 ms
poll — roughly 0.5-1.0 A peak and, at the pulsed sampler's ~35% duty, an
average an order of magnitude above the ~18 mA the C6 and the three
expanders draw between them. European honey bees are diurnal (flight needs
light, and stops below roughly 10 C regardless), so every night that draw
buys nothing.

Add a control characteristic (8e8b0103) that HiveHub writes a suspension
DURATION to. The counter has no RTC, no NVS and no network, and this change
deliberately does not give it any: it is told how long to stay quiet, never
when. Everything that matters falls out of that.

  * It expires. A HiveHub that dies cannot leave a counter blind.
  * It is capped at MAX_IDLE_SECONDS (1 h), several times HiveHub's upload
    cycle, so HiveHub re-arms per cycle and nothing accumulates. An
    over-long request is clamped, not refused — refusing would leave the
    emitters running all night because one field was too large.
  * It is never persisted, so any reset comes back counting.
  * It is refused during an OTA, which already parks the emitters itself.

Not deep sleep, which was the obvious implementation. Parking the emitters
captures >90% of the saving; sleeping for the rest costs the measurement
read (every night row would carry ok=false), the OTA relay path that is
specifically recommended for night use, the ability to cancel, the RAM
totals, and a truthful uptime_s — and the C6's deep-sleep timer runs off the
temperature-dependent internal RC oscillator, drifting minutes to half an
hour over a 8-12 h night.

The deadline arithmetic is a pure header (idle_state.h) with host tests
covering the millis() rollover and the "HiveHub stopped re-arming" case,
neither reproducible on a hive. Gate state machines are reset on both edges
so a half-finished pairing cannot span the gap and fabricate a crossing.

Protocol v4 reports it: idle_s counts the suspension down and status bit
0x80 marks it. Without them a night of zeros is indistinguishable from a
counter whose emitter FETs have died. The addition is purely additive, so
HiveHub's key-skipping parser reads v4 documents unchanged.

Also fixes documentation that has gone stale around this area: README's
power budget still claimed the emitters run continuously and could be
switched with a CMD_LEDS_OFF that went away with the wired link, and it
understated the emitter draw that is the whole reason this feature exists.
docs/ble-mode.md's "deep sleep is incompatible" line now says what is
actually true.

Adds a CI workflow — the repo had none, so neither the host tests nor the
ESP32 build were checked anywhere but a laptop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gFpezSuXW3sHMd1xaSYnd
claude added 2 commits August 19, 2026 09:32
The new CI workflow's first run failed with "This board doesn't support
arduino framework!". Not a regression from night mode — a latent breakage
this repo had no CI to notice.

`platform = espressif32` was unpinned, so it resolves to the official
Espressif platform, which at 7.0.1 still bundles Arduino core 2.0.17. That
core has no ESP32-C6 Arduino support; C6 support arrived in Arduino-ESP32
3.x. Anyone running the documented `pio run` on a fresh install has been
getting that error rather than a firmware image.

Pin the pioarduino release HiveHub already builds this same chip with
(55.03.39 = Arduino-ESP32 3.3.9 / ESP-IDF 5.5.4), and pin the libraries
exactly while here — including NimBLE at 2.5.1. The caret range let it
float, and the 2.x releases below 2.5.0 each trip a different C6 failure
(a libbt.a link error, an init crash on core 3.3.7+, a scan crash on the
C6), so "some 2.x" is not a safe thing to ask for on this board.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gFpezSuXW3sHMd1xaSYnd
Companion to the HiveHub fix. These host tests build the pure headers
against a bare compiler, where Arduino's few hundred all-caps macros do not
exist — so a constant in the shared counter_protocol.h that collides with
one of them passes every check here and fails only in the firmware build.
HiveHub hit exactly that with an enumerator named DISABLED meeting
esp32-hal-gpio.h's `#define DISABLED 0x00`.

Define the macros that actually bite before the include, so the collision
surfaces in seconds on a host compiler instead of partway through a
firmware build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gFpezSuXW3sHMd1xaSYnd
@MacNite
MacNite merged commit 7f463ed into main Aug 21, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants