Reading device state¶
A one-off snapshot¶
For a quick look at everything at once, collect_device_snapshot gathers the
individual endpoints concurrently and tolerates partial failures:
from busylib.features import collect_device_snapshot
snapshot = await collect_device_snapshot(client)
print(f"Name: {snapshot.name or 'unknown'}")
print(f"Battery: {snapshot.power.battery_charge if snapshot.power else 'unknown'}")
print(f"Wi-Fi: {snapshot.wifi.state if snapshot.wifi else 'unknown'}")
print(f"Field errors: {snapshot.field_errors}")
Example output:
Name: Front desk
Battery: 88
Wi-Fi: connected
Field errors: {}
{} means every snapshot endpoint responded. A non-empty field_errors
dictionary identifies only the unavailable fields, while usable values remain
available in the same snapshot.
A field that fails lands in field_errors instead of aborting the whole
snapshot, so one unavailable endpoint doesn't cost you the rest.
The underlying calls are available individually too: version(), status(),
display_brightness(), audio_volume(), wifi_status(), ble_status(),
storage_status(), time(), name().
Streaming updates¶
/api/status/ws pushes protobuf state messages as things change, which is far
better than polling:
async for message in client.stream_status_ws():
if isinstance(message, dict):
print(message)
Example output after a power update:
{'updates': [{'power': {'known': {'battery_charge_percent': 88}}}]}
The exact payload changes with the device event. Each member of updates is a
delta, not a full device snapshot, so retain earlier state for fields absent
from that message.
Messages are decoded from the BSB_State.State schema into dictionaries with
the original proto field names. Each one carries a list of updates, where
every entry is one changed thing: device_name, power, brightness,
audio_volume, wifi, timezone, ble, update_state, input, timer, or
frame.
Keeping a snapshot in sync¶
DeviceStateStore applies those deltas onto a snapshot for you and notifies
subscribers:
from busylib.features import DeviceStateStore, collect_device_snapshot
store = DeviceStateStore(await collect_device_snapshot(client))
store.on_state(lambda snapshot: print("now:", snapshot.name))
store.on_diff(lambda changed, snapshot: print("changed:", changed))
async for message in client.stream_status_ws():
if isinstance(message, dict):
store.apply_stream_message(message)
Example output after a battery update:
now: Front desk
changed: {'power'}
The first callback receives the complete merged snapshot; the second names the top-level fields changed by this particular stream message.
on_state receives the full updated snapshot; on_diff also receives the set
of top-level fields that actually changed, which is handy for redrawing only
what moved.
Screen frames arrive here too¶
Current firmware has no separate screen WebSocket. Instead, front-display
frames come through this same stream as frame updates, carrying their own
width, height, encoding, and pixel_format. The store decodes them into
Frame objects on DeviceSnapshot.screen_front and screen_back:
def on_diff(changed, snapshot):
if "screen_front" in changed and snapshot.screen_front is not None:
render(snapshot.screen_front)
store.on_diff(on_diff)
Expected result: render receives a decoded Frame whenever the front
screen changes, carrying RGB bytes with their geometry. This fragment intentionally has no terminal output; the
observable result is the updated image in your renderer.
Because the frame describes its own encoding, no guessing by payload size is
involved — see busylib.display.decode_frame_data. A decoded frame whose size
doesn't match the target display is logged and dropped rather than stored, so a
malformed update can't reach a renderer.