Skip to content

Let a HiveTraffic counter's three emitter banks be switched individually - #170

Merged
MacNite merged 1 commit into
mainfrom
claude/hivetraffic-mosfet-toggles-4a5yqu
Aug 23, 2026
Merged

MacNite merged 1 commit into
mainfrom
claude/hivetraffic-mosfet-toggles-4a5yqu

Conversation

@MacNite

@MacNite MacNite commented Aug 22, 2026

Copy link
Copy Markdown
Owner

The HiveHub half of MacNite/HiveTraffic#15 — the dashboard controls, the config column, and the BLE write.

A counter's 48 IR emitters sit behind three MOSFETs, one per group of eight gates, and they dominate its power draw by an order of magnitude. Measured on the counter's 3.3 V rail:

Banks enabled Gates counted Draw
1 8 ~0.14 A
2 16 ~0.22 A
3 (default) 24 ~0.30 A

Roughly 80 mA per bank on top of a ~60 mA floor. For an entrance narrower than 24 gates, part of one closed for the season, or an off-grid supply that will not carry the whole board, this is the coarsest and most effective control available — and unlike night mode it applies around the clock. The two compose: a counter can be running one bank and be suspended.

Setup

Dashboard → HiveTraffic setupEmitter banks: three checkboxes, all ticked by default, sitting alongside the night window. The measured current per configuration is shown next to the boxes rather than buried in a doc — the whole point of the setting is the power, and there is no way to guess the numbers from the UI otherwise. The panel also says what a switched-off bank does to the data: those crossings are not missing, they are not happening, so the totals step down and a narrowed counter should not be compared against its own history.

Mirrored into the demo dashboard so it does not go stale.

Storage

Migration 026 adds three booleans, all defaulting to true, per device (like the night window — every counter on one hub shares an apiary). Three columns rather than one bitmask integer: the dashboard draws three checkboxes, the API is read by humans, and banks = 5 is a worse thing to find in a config dump than bank2 = false. The firmware assembles the mask.

All three off is rejected — in the dashboard, and in update_device_config with a 400. The counter refuses a mask of zero outright (it keeps the mask it had), so storing one would only leave three unticked boxes next to a counter cheerfully counting all 24 gates, with nothing anywhere saying why. A counter that should count nothing is unpaired instead.

The check needs the stored row as well as the patch: a PATCH carries only what changed, and "turn bank 3 off" is perfectly fine on its own and fatal if 1 and 2 are already off. There is deliberately no database CHECK constraint — it could not see "turn the last one off and another one on" as a single edit, and would fail a legitimate swap.

The write

The mask goes out on the connection the measurement read already has — no extra scan, no extra connect — and only when the mask the counter just reported differs from the configured one. Never against a HiveHub-side memory of what it last sent: the counter deliberately does not persist the mask, so one that browned out, watchdogged or rebooted out of an OTA comes back running all 24 gates, and the next cycle's read finds the disagreement and fixes it. A HiveHub that remembered "I already configured this one" would leave that counter wide open indefinitely; one that wrote unconditionally would spend a GATT write every ten minutes to change nothing.

Gated on the counter reporting wire revision 5. An older firmware has the control characteristic but not the opcode, and would log an "unknown opcode" line every cycle for the rest of its deployment.

The mask is persisted in NVS alongside the night window, so a hub that boots without WiFi still narrows its counters instead of running all 24 gates while it waits for a config it cannot fetch.

Wire revision 5

bee_counter_wire.h learns "banks", defaulting to all three on rather than to zero. A counter too old to report it is running its whole entrance — it has no way not to — and reading that absence as "everything is off" would write the misreading into stored history for every counter the OTA relay has not reached yet. Bits above the third bank are masked off; a 0 is stored as read, because it is not a state the counter can be in and flattening it would hide a real problem.

The value reaches storage as hives[].bee_counter.banks (via extra="allow" into raw_json), emitted on every reading including the 7 of a counter nobody has narrowed — same argument as idle_s: a bank switched off produces the same permanently flat share of the totals a dead FET does, and this is the only thing separating them.

Testing

  • New test-data/test_bee_counter_banks.py, wired into CI: the patch/stored-row merge, the last-bank refusal, that turning off an already-off bank is a no-op, the legitimate swap, and that an unrelated patch skips the check entirely. Passes under both python3 … and pytest.
  • test-data/test_bee_counter_wire.cpp gains a v5 document, a narrowed (one-bank) document, the pre-v5 default, phantom-bit masking, and the shared constants.
  • Existing suites re-run clean: firmware host tests, all four BLE decoders, night mode, and the five Python suites.
  • Both dashboards and the demo API fixture pass node --check.

Backend to 0.3.2, firmware to 0.24.19. Requires HiveTraffic 0.3.0 on the counter; older counters keep all three banks on.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TCTPLvKrrm1RmiaVHGxz4D


Generated by Claude Code

The counter's 48 IR emitters sit behind three MOSFETs, one per group of eight
gates, and they dominate its power draw by an order of magnitude. Measured on
the counter's 3.3 V rail, one bank draws ~0.14 A, two ~0.22 A and three ~0.30 A
— roughly 80 mA per bank on top of a ~60 mA floor. For an entrance narrower
than 24 gates, part of one closed for the season, or an off-grid supply that
will not carry the whole board, switching a bank off is the coarsest and most
effective control available, and unlike night mode it applies around the clock.
The two compose: a counter can be running one bank and be suspended.

Dashboard → HiveTraffic setup → Emitter banks: three checkboxes, all ticked by
default, alongside the night window and with the measured current per
configuration spelled out — the point of the setting is the power, so the
numbers belong next to the boxes rather than in a doc.

Stored per device as three booleans (migration 026, all defaulting to true) and
assembled by the firmware into the bitmask HiveTraffic's SET_BANKS opcode takes.
Three columns rather than one integer because the dashboard draws three
checkboxes and "banks = 5" is a worse thing to find in a config dump than
"bank2 = false".

All three off is rejected — in the dashboard, and in update_device_config with
a 400. The counter refuses a mask of zero outright (it keeps the mask it had),
so storing one would only leave three unticked boxes next to a counter
cheerfully counting all 24 gates with nothing saying why. The check needs the
stored row as well as the patch, because a PATCH carries only what changed and
"turn bank 3 off" is fine on its own and fatal if 1 and 2 are already off;
that merge is what test_bee_counter_banks.py pins. There is deliberately no
database CHECK: a constraint could not see "turn the last one off and another
one on" as a single edit.

The mask is written on the connection the measurement read already has, and
only when the mask the counter just REPORTED differs from the configured one —
never against a HiveHub-side memory of what it last sent. The counter does not
persist the mask, so one that browned out or rebooted out of an OTA comes back
running all 24 gates and the next cycle's read finds and fixes it; a HiveHub
that remembered "already configured" would leave it wide open indefinitely.

bee_counter_wire.h learns wire revision 5's "banks" field, defaulting to all
three on rather than to zero — a counter too old to report it is running its
whole entrance, and reading that absence as "everything is off" would write the
misreading into stored history. Bits above the third bank are masked off.

Backend to 0.3.2, firmware to 0.24.19.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TCTPLvKrrm1RmiaVHGxz4D

MacNite commented Aug 22, 2026

Copy link
Copy Markdown
Owner Author

Heads up on the red firmware-build jobs — re-running won't clear them, and it isn't this PR.

Both environments die during dependency install, before a single source file is compiled:

Library Manager: Installing bogde/HX711 @ 0.7.5
Library Manager: HX711@0.7.5 has been installed!
Library Manager: Installing adafruit/Adafruit SHT4x Library @ 1.0.4
UnknownPackageError: Could not find the package with 'adafruit/Adafruit SHT4x Library @ 1.0.4' requirements

The PlatformIO registry has stopped resolving that package. Evidence it's upstream and not the diff:

  • I dispatched CI on main at 6c8d274 — the current base, green ~14 h ago, containing none of these changes — and both firmware builds failed there byte-for-byte identically (run 32594480578).
  • git diff main...HEAD -- firmware/platformio.ini is empty. That pin is pre-existing and untouched here.
  • Three re-runs so far (19:33, 19:44, 19:51 UTC) all fail at the same line.

Every other check on this PR is green: server-tests, firmware-host-tests, website-js-check, and the backend image build.

I'm deliberately not "fixing" this by loosening the pin. platformio.ini states the policy explicitly — every dependency pinned to an exact, immutable reference — so re-pointing Adafruit SHT4x Library@1.0.4 to get a green tick would turn this PR into a dependency change and quietly drop that guarantee for both build environments.

Worth knowing separately: main cannot build firmware right now either, so this blocks any HiveHub firmware release until the registry recovers, independent of this PR.

I'm watching and will re-run the failed jobs once the registry is back. If it stays broken and the build needs unblocking sooner, the standard escape hatch is repinning that one library to an immutable git SHA — but that belongs in its own change, not folded in here.


Generated by Claude Code

@MacNite
MacNite merged commit 1f9de2a into main Aug 23, 2026
28 of 46 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.

1 participant