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
112 changes: 107 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Per-integration extras (e.g., macOS Calendar access for `calendar_countdown`) ar

1. Clone the repo:
```bash
git clone https://github.com/your-org/busybar-integrations.git
git clone https://github.com/sumitake/busybar-integrations.git
cd busybar-integrations
```

Expand All @@ -44,6 +44,105 @@ Per-integration extras (e.g., macOS Calendar access for `calendar_countdown`) ar
uv run python -m ci_status.main --once --dry-run
```

## Firmware 1.2.3 support

The local API is capability-probed through `GET /api/version` (`api_semver`).
API **27.5.0+** enables selective element cleanup, explicit drawing order
(`z_index`), and small inline XPM2 CI icons. Older/unknown firmware and cloud
relay retain the ordinary text/shape payloads. No new configuration is needed
for these display improvements; `[ci_status] bitmap_icons = false` disables
the cosmetic icons.

Calendar and CI still send complete, expiring frames on their established
cadence. At a same-priority layout change, selective cleanup removes obsolete
IDs while keeping common content visible. Type changes and priority reductions
use an app-scoped full clear. Neither path is an atomic frame transaction;
timeouts and subsequent full redraws provide recovery after preemption or a
failed request. Firmware 1.2.3 has an application-name parsing bug in the
selective DELETE body, so this client always puts ownership in the query.

The large calendar countdown and device-native Nyan animation remain in use.
The firmware's native countdown font is too small for the existing calendar
layout; Nyan already plays its uploaded animation on the bar. Firmware fixes
for Wi-Fi status streaming and networking benefit the existing local API
without adding another background listener.

### Inspect the device

```bash
uv run python -m busybar diagnose
uv run python -m busybar diagnose --host 10.0.4.20 --screen screen.bmp
```

If a macOS editable install reports `No module named busybar` (Python can
ignore a `.pth` file marked hidden), run from the repository root with
`PYTHONPATH=src uv run python -m busybar diagnose`. This uses the same source
modules without relying on the editable-install file.

Diagnostics read firmware/API versions, local transport, power and BUSY
snapshot availability. They never dump tokens/configuration, play audio,
start timers, or write device logs. An incomplete report exits nonzero.
Screen capture converts firmware 1.2.3's base64 BGR framebuffer into a standard
BMP; the endpoint's `image/bmp` header does not describe its actual wire data.

### Local tokens and USB/Wi-Fi recovery

```toml
[device]
host = "10.0.4.20"
fallback_hosts = ["192.0.2.20"] # replace with your bar's Wi-Fi address
local_token = "" # preferably supply BUSYBAR_LOCAL_TOKEN instead
transport = "auto"
discover = false
# device_id = "001122aabbcc" # USB MAC with colons removed, for opt-in discovery
```

Local tokens use `X-API-Token`; cloud tokens use `Authorization: Bearer`.
Redirects are disabled and credentials are kept separate. Token creation or
revocation is not automatic. Supply only addresses for the same trusted device;
mDNS and local HTTP are not a cryptographic device identity check.

Explicit local alternatives work without extra dependencies. Optional discovery
uses the firmware's actual HTTP service registration, not a proprietary service:
`busybar-<USB MAC>._http._tcp.local.` on port 80.

```bash
uv sync --extra discovery
uv run python -m busybar discover --timeout 3
```

Use the returned bare `device_id` with `discover = true`. Discovery scans are
short, close their resources, and refresh no more than once per minute. The
client tries at most four local addresses per operation, then the configured
cloud route for supported operations. A working fallback remains preferred
between recovery probes. Missing discovery support or a failed scan leaves
explicit hosts usable; the CLI distinguishes an unavailable scan from a
successful scan that found no devices.

HTTP rejections (including authentication errors and a higher-priority canvas)
do not trigger failover. Reads and display updates can use bounded fallback.
Audio and timer starts are not replayed after an uncertain send/read failure;
only a definite connection timeout permits another route. Calendar chirps are
attempted once per event edge, and `auto_busy` requires a positively observed
nested `NOT_STARTED` snapshot rather than treating unavailable state as idle.

### Platform examples

- [Home Assistant](examples/home_assistant/README.md): built-in REST sensors
for the nested BUSY snapshot and an expiring notification command with a
secret placeholder. Notices stay below urgent calendar and BUSY-session
priority and expire within 1–60 seconds. This is optional YAML, not a custom
integration or an automatic HA installation.
- [On-device JavaScript](examples/javascript/README.md): a finite 30-second
health demo using fetch, timers, and one persisted run counter. Scripts can
be uploaded into app asset subdirectories with `upload_asset(app,
"scripts/main.js", data)`. The firmware runner is experimental; this example
does not replace the host integrations or install persistent autostart.

Protocol references: [firmware 1.2.3 release](https://github.com/busy-app/busybar-firmware/releases/tag/1.2.3),
[display API](https://github.com/busy-app/busybar-firmware/blob/2cd7ec8abf8479ba3398241e99d291ec24f2a96f/applications/services/web_server/openapi/assets.yaml),
[HTTP service registration](https://github.com/busy-app/busybar-firmware/blob/2cd7ec8abf8479ba3398241e99d291ec24f2a96f/applications/services/web_server/web_server.c).

## How it works

The display is a shared 72×16 canvas. Each integration publishes text, shapes, or status via the `busybar.client.BusyBarClient` API (see [`src/busybar/client.py`](src/busybar/client.py)). The display arbitrates by **priority**, through the shared ladder in [`src/busybar/display.py`](src/busybar/display.py):
Expand Down Expand Up @@ -127,10 +226,13 @@ revoke the old one, rather than revoking first.

Continuous status streaming (`/api/status/ws`) is local-only by design —
the cloud API has no equivalent, so a caller relying on the status
WebSocket will not get a cloud fallback for it. Everything else this
client uses (`draw`, `clear`, `status`, `get_busy`, `set_busy_simple`,
`play_audio`) is a synchronous request/response call and mirrors 1:1
over cloud.
WebSocket will not get a cloud fallback for it. Selective deletion, asset
uploads, capability probes, and the diagnostic screen are also local-only.
Ordinary `draw`, `clear`, `status`, and `get_busy` calls retain cloud support;
new bitmap/layer fields are omitted from cloud drawings. Bitmap-only frames
require verified modern local firmware and never fall back to an empty cloud
draw. `set_busy_simple` and `play_audio` can use cloud directly, but an uncertain
local send is not replayed through the relay.

### Verified against the live cloud API

Expand Down
9 changes: 9 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ cloud_base_url = "https://api.busy.app/busybar"
transport = "auto" # "auto" (local, fall back to cloud) | "local" | "cloud" (forced --
# mainly for deliberately testing the cloud path)

# Local API-token and route discovery are opt-in. The local token is sent only
# to the device as X-API-Token; it is never sent to BUSY's cloud relay. Prefer
# BUSYBAR_LOCAL_TOKEN in your environment over writing a token in config.toml.
local_token = ""
fallback_hosts = [] # at most three explicit LAN fallback hosts
discover = false # mDNS convenience on a trusted LAN; not authentication
device_id = "" # exact 12-hex USB MAC without colons when discover = true

[calendar_countdown]
poll_seconds = 10 # ambient-tier redraw cadence (default: 10) -- matches the running-CI
# overlay's 10s dwell gap so this app's redraws reliably land inside
Expand Down Expand Up @@ -63,6 +71,7 @@ running_poll_seconds = 20 # poll interval while a run is active (shortened fr
show_quota = true # GraphQL/REST quota frames join the overlay rotation while a run
# is active (no effect if show_running is false)
running_spinner = true # animated 8x8 spinner on the running badge
bitmap_icons = true # use small XPM2 accents on current local firmware; text layout remains the fallback

# Account-wide watching (v1.5.1) -- off by default. When on, the watch list
# becomes auto-discovered account repos UNION `repos` above, MINUS
Expand Down
102 changes: 102 additions & 0 deletions docs/firmware-1.2.3-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Firmware 1.2.3 qualification

Qualification date: 2026-09-08. Source baseline: `097e1bb`.
Device: firmware **1.2.3**, API **27.5.0**, build **2026-09-03**, firmware
commit `2cd7ec8abf8479ba3398241e99d291ec24f2a96f`.

## Automated checks

- Existing baseline: 374 passing tests.
- Implementation: 433 passing tests, including existing calendar/CI/Nyan
behavior and new transport, discovery, presentation and diagnostic cases.
- Source distribution and wheel built successfully with `uv build`.
- Python compilation and `git diff --check` passed.
- Optional discovery dependency installed and exercised; ordinary client
operation does not require it.
- Home Assistant YAML parsed with a `!secret` placeholder constructor. Jinja
rendering checked malformed/unknown/valid snapshots, quoted/multiline
notification text, and TTL inputs `0`, negative, malformed, normal and
oversized. The notice is bounded to 1–60 seconds.

Tests cover local/cloud credential separation, redirect suppression, no
failover on HTTP rejection, no replay of uncertain audio/timer requests,
priority step-down, selective-delete fallback, expired/preempted elements,
bitmap-only local drawing, legacy/cloud cosmetic fallback, unknown BUSY
state, discovery cleanup/failure reporting, and screen conversion.

## Live device checks

| Capability | Positive observation |
|---|---|
| API and firmware detection | Local diagnostic report returned 1.2.3 / 27.5.0 with `complete: true`. |
| Explicit z-order | Overlapping green/red rectangles rendered the higher `z_index` layer despite reverse payload order. Pixel readback was `(0, 255, 0)`. |
| Selective cleanup | Deleting only the green rectangle exposed the remaining red rectangle: `(255, 0, 0)`. |
| Ownership guard | A different app name could not delete the remaining element; the red pixel remained. |
| Inline XPM2 icon | The fixed CI icon rendered; its expected white pixel read `(255, 255, 255)`. |
| Cleanup and BUSY state | Temporary probes used unique ownership, priority 80, five-second TTLs and scoped cleanup. Cleanup succeeded; BUSY remained `NOT_STARTED` before and after. |
| Discovery | The bar advertised one persistent USB-MAC-derived HTTP service with both USB and Wi-Fi IPv4 addresses. |
| Local route recovery | An unavailable primary address fell back to the explicit Wi-Fi route; `/api/transport` positively reported `wifi`. |
| Screen capture | Base64 BGR24 response decoded into a valid standard 72×16 BMP. |
| Asset subdirectory | `scripts/main.js` uploaded and exact file bytes matched an HTTP readback. |
| JavaScript runtime | USB-network TCP CLI ran the demo: three successful API-version polls, a 30-second stop message, and return to the shell prompt. |
| JavaScript persistence | The demo's own localStorage file retained `run_count: "1"`; read back through the CLI. |

No BUSY timer was started, no audio was played, and no authentication,
brightness, charging, Wi-Fi or Home Assistant configuration was changed by
these checks. The example JS file and its counter are the only retained demo
assets; it has no autostart and is no longer running.

## Corrections established during qualification

- Firmware advertises `busybar-<USB MAC>._http._tcp.local.` on port 80.
The earlier `_busybar._tcp` assessment was incorrect.
- The API version field is `api_semver`.
- Selective DELETE uses `/api/display/draw`, with ownership in the query;
the 1.2.3 body parser has an app-name shadowing bug.
- `/api/screen` claims `image/bmp` but actually sends base64 BGR24 pixels.
- The stock CLI is TCP port 23 over USB Ethernet, not a USB serial modem.
- The storage HTTP path buffer permits at most 63 characters. The demo's
localStorage filename is longer; direct HTTP read returned 400 even though
the file existed. CLI read positively confirmed its stored contents.

## Review and remaining activation boundary

Terra implemented and tested connectivity; Luna implemented diagnostics and
platform examples. Astra reviewed the combined code. Its concrete findings
were fixed: modern bitmap-only support without empty cloud fallback, honest
unavailable discovery reporting, and positive finite HA notification TTLs.
Primary review also corrected firmware wire contracts against source and live
responses and rejected a broker/transaction layer as unnecessary.

The first complete GitHub review batch on PR #22 (head `db05fce`, inventory
cutoff 2026-09-08 21:56 UTC) contained two actionable P2 findings. Both were
fixed together: explicit diagnostic `--host` now forces that sole local target,
and discovery may append newly found addresses during the current operation
without exceeding four total attempts or replaying an uncertain write. Tests
and live reads verified both corrections. Formal reviews, all inline threads,
issue comments and applicable check annotations were inventoried before the
patch; CodeRabbit's skipped review was not counted as approval.

The second complete GitHub batch (head `15acd5e`, inventory cutoff
2026-09-08 22:07 UTC) identified two further route-selection defects. The
primary recovery interval now starts at the actual primary attempt, so an
immediate one-shot operation uses the known-working fallback. A full four-route
configuration reserves its last attempt for an untried discovery candidate,
while preserving the fourth configured route when discovery has no candidate.
The preferred address uses the existing successful endpoint rather than a list
index. Astra reviewed this bounded design; no new retry service or state
machine was added. The four-attempt limit and uncertain-write stop remain.

A logical Gemini final repository advisory returned **PROCEED**. The earlier
follow-up design call was unavailable because of nested host sandbox failure;
that failed call was not counted as approval or replayed. A fresh final review
used approved host execution with native sandboxing preserved. The runtime
returned no native model identity, so the advisory is recorded as such rather
than claimed as independently attested model lineage.

This report qualifies the implementation and the bounded device probes. The
three long-running host integrations have not yet been switched to this branch.
Cloud and local-token behavior is covered by tests, not a new live credential
rotation. The HA example has not been loaded into a running HA instance. The
firmware JS runner remains experimental and this demo is not a replacement for
the host integrations.
40 changes: 40 additions & 0 deletions examples/home_assistant/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Home Assistant example

`busybar.yaml` uses Home Assistant's built-in REST sensor and `rest_command`
facilities. Replace `BUSY_BAR_IP` with your device address. Add the local device token to `secrets.yaml` as
`busybar_api_token`; the YAML sends it in the firmware's `X-API-Token` header.
The cloud relay uses a different `Authorization: Bearer` contract and is not
used here.

The sensor preserves the firmware 1.2.3 BUSY snapshot shape:

```json
{
"snapshot": {
"type": "SIMPLE",
"card_id": "...",
"time_left_ms": 90000,
"is_paused": false,
"busy_bar_settings": {}
},
"snapshot_timestamp_ms": 1700000000000
}
```

This is the nested snapshot contract verified on firmware 1.2.3; nesting is
not claimed as a new 1.2.3 feature. Unknown responses should not be converted
to an idle timer by guessing alternative field paths.

Malformed snapshots and snapshots without the expected fields become
`unknown` or unavailable. They are never rendered as `NOT_STARTED` or an
idle state. The commented automation is a light cue on a state change; it
does not start a timer. Notice TTLs are clamped to 1-60 seconds so a bad
automation value cannot create a permanent canvas element. The example does not claim that an official Home
Assistant core BUSY Bar integration exists.

If local authentication is disabled, remove the `X-API-Token` header lines
instead of creating an unnecessary token. Merge the YAML into your existing
REST/REST-command configuration, or include it as an HA package; do not add
duplicate top-level keys. Validate the configuration in Home Assistant before
reloading it. References: [REST sensors](https://www.home-assistant.io/integrations/sensor.rest/)
and [REST commands](https://www.home-assistant.io/integrations/rest_command/).
Loading
Loading