Skip to content
Open
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: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,22 @@ JSON payload format (written to RX):

Fields: `s` = session %, `sr` = session reset (minutes), `w` = weekly %, `wr` = weekly reset (minutes), `st` = status, `ok` = success flag.

## Running the daemon tests

The Python daemons have a test suite under `daemon/tests/`. Runtime dependencies
are installed per platform by the installers, so the test extras live in their
own manifest:

```bash
python3 -m venv .venv
.venv/bin/pip install -r daemon/requirements-dev.txt
.venv/bin/python -m pytest daemon/tests
```

Two tests are marked Linux/WSL-only and skip elsewhere: on macOS, spawning the
Windows daemon initialises CoreBluetooth and trips the Bluetooth privacy gate,
which aborts the child rather than producing the scan-loop hang they assert on.

## Recompiling fonts

The `firmware/src/font_*.c` files are pre-compiled LVGL bitmap fonts.
Expand Down
19 changes: 19 additions & 0 deletions daemon/requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Dependencies for running the daemon test suite (daemon/tests/).
#
# python3 -m venv .venv
# .venv/bin/pip install -r daemon/requirements-dev.txt
# .venv/bin/python -m pytest daemon/tests
#
# Runtime deps are declared per platform and stay that way: install-mac.sh
# installs bleak/httpx into daemon/.venv, requirements-windows.txt covers the
# Windows tray build. The tests import both daemons and the icon renderer
# whatever the host is, so they need the union of those plus pytest.
#
# pystray is deliberately absent. tray_windows.py imports it inside main(), and
# test_windows_tray.py says so in its own docstring — the tests exercise the
# handlers without ever importing the top-level module, which would want a GUI
# toolkit on Linux. Adding it here would make every contributor pay for that.
pytest
bleak
httpx
Pillow
38 changes: 24 additions & 14 deletions firmware/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -372,20 +372,30 @@ void loop() {

if (ble_has_data()) {
if (parse_json(ble_get_data(), &usage)) {
int g_before = usage_rate_group();
bool session_reset = usage_rate_sample(usage.session_pct);
int g_after = usage_rate_group();
// 5-hour session limit refilled → chime so the user knows they can
// use Claude again (no-op on boards without a buzzer). Gated on the
// daemon's opt-in `chime` config; the `buzz` serial cmd ignores it.
if (session_reset && usage.chime) {
Serial.println("session reset detected — chime");
sound_hal_play_reset();
}
if (g_after != g_before) {
Serial.printf("usage rate: group %d -> %d (s=%.2f%%)\n",
g_before, g_after, usage.session_pct);
if (splash_is_active()) splash_pick_for_current_rate();
// A {"ok":false} beat is the daemon saying "no fresh data" — it
// writes that key and nothing else, so session_pct arrives as 0.0
// from the `| 0.0f` default rather than as a reading. That must not
// reach the rate ring: usage_rate_sample() treats a drop of more
// than five points as a 5-hour refill, so it would clear the
// history and report a reset, and a daemon that has merely lost its
// token would ring the chime. ui_update() guards on ok already; the
// sampler runs ahead of it and needs its own guard.
if (usage.ok) {
int g_before = usage_rate_group();
bool session_reset = usage_rate_sample(usage.session_pct);
int g_after = usage_rate_group();
// 5-hour session limit refilled → chime so the user knows they can
// use Claude again (no-op on boards without a buzzer). Gated on the
// daemon's opt-in `chime` config; the `buzz` serial cmd ignores it.
if (session_reset && usage.chime) {
Serial.println("session reset detected — chime");
sound_hal_play_reset();
}
if (g_after != g_before) {
Serial.printf("usage rate: group %d -> %d (s=%.2f%%)\n",
g_before, g_after, usage.session_pct);
if (splash_is_active()) splash_pick_for_current_rate();
}
}
ui_update(&usage);
ble_send_ack();
Expand Down