Displays and frames¶
Frames¶
frame() returns what is on a display as a Frame: the pixels, their
geometry, and which display they came from, kept together.
from busylib import BusyBar
with BusyBar("10.0.4.20") as bb:
front = bb.frame(0)
print(front.width, front.height) # 72 16
print(front.pixel(2, 8)) # (255, 0, 0)
open("front.png", "wb").write(front.to_png())
screen() is still there and still returns the raw bytes, for anyone feeding
them somewhere that wants a flat buffer.
Two things Frame settles that a bare buffer leaves to the caller. The bytes
are RGB: the device orders colour as BGR, and that is undone before you
see it. And to_png() uses only the standard library, so writing a frame to a
file or a web page needs nothing installed — Pillow is optional and imported
only if you ask for to_pillow().
Frames also arrive on the status stream, where they are compressed and described by their own metadata:
async for state in bb.stream_status_ws():
for update in state.get("updates", []):
if "frame" in update:
frame = Frame.from_state_update(update["frame"])
from_state_update fills in the metadata protobuf leaves out. A plain
uncompressed RGB frame arrives with neither encoding nor pixel_format,
because both hold their enum's first value — reading that absence as missing
data is the mistake it exists to prevent.
busylib.frames
¶
A frame of pixels, and the few things people actually do with one.
The device hands out screen contents as a flat run of bytes, which is awkward
to work with and easy to misread - the byte order, the geometry and which
display it came from all have to be carried alongside by hand. Frame keeps
them together and answers the usual questions directly.
Nothing here needs a third-party package. PNG encoding uses zlib from the
standard library, and to_pillow imports Pillow only if you call it.
Frame
dataclass
¶
Frame(data: bytes, display: DisplaySpec)
One screen's worth of pixels, as RGB bytes with their geometry attached.
data holds three bytes per pixel in RGB order, row by row from the top
left. The device sends colour as BGR; that is already undone here, so the
bytes are ready to hand to anything that draws.
pixel
¶
pixel(x: int, y: int) -> tuple[int, int, int]
Return the RGB triple at x, y, counting from the top left.
Raises IndexError outside the display, rather than reading a
neighbouring row - a flat buffer makes that mistake silent.
to_png
¶
to_png() -> bytes
Encode the frame as a PNG, using only the standard library.
Enough to write the frame to a file or drop it into a web page, which is most of what people want from a frame, without asking anyone to install an imaging library for it.
to_pillow
¶
to_pillow() -> Image.Image
Return the frame as a Pillow image, for scaling or further drawing.
Pillow is imported here rather than at module load, so a frame can be read, inspected and saved as PNG without it installed.
from_screen
classmethod
¶
from_screen(data: bytes, display: DisplaySpecLike) -> Frame
Wrap already-decoded bytes, as returned by screen().
from_state_update
classmethod
¶
from_state_update(frame_update: dict[str, Any]) -> Frame
Build a frame from a frame update on the status stream.
The stream compresses frames and describes them with its own metadata,
and protobuf omits any field holding a default value - so a colour
frame arrives with no pixel_format and an uncompressed one with no
encoding. Both defaults are filled in here, because reading their
absence as missing data is the mistake this method exists to prevent.
Display specifications¶
Helpers describing the two physical displays and decoding the frame data the device sends back.
busylib.display
¶
WIRE_PIXEL_FORMATS
module-attribute
¶
WIRE_PIXEL_FORMATS = {'RGB888': COLOUR_FORMAT, 'L8': GREY8_FORMAT, 'L4': GREY4_FORMAT}
FRONT_DISPLAY
module-attribute
¶
FRONT_DISPLAY = DisplaySpec(name=DisplayName.FRONT, index=0, width=72, height=16, description='72x16 RGB LED matrix, ~16M colors, >800 nits')
BACK_DISPLAY
module-attribute
¶
BACK_DISPLAY = DisplaySpec(name=DisplayName.BACK, index=1, width=160, height=80, description='160x80 monochrome OLED, 16 gray scales')
DisplaySpecLike
module-attribute
¶
DisplaySpecLike: TypeAlias = 'DisplaySpec | DisplayName | int | str | None'
DisplaySpec
dataclass
¶
DisplaySpec(name: DisplayName, index: int, width: int, height: int, description: str)
get_display_spec
¶
get_display_spec(display: DisplaySpecLike) -> DisplaySpec
Resolve a display spec using explicit front/back selection.
front is used only when display is None. Any unsupported display value
raises ValueError to avoid silently rendering to the wrong screen.
rle_decode
¶
rle_decode(data: bytes, block_size: int) -> bytes | None
Decode the run-length encoding used by BSB_Frame.Frame.encoding.
A control byte with the high bit set is a literal run of
(ctrl & 0x7F) * block_size raw bytes; otherwise it is a repeat count for
the single block that follows. Returns None on truncated/malformed input.
unpack_l4_to_l8
¶
unpack_l4_to_l8(data: bytes) -> bytes
Expand packed 4-bit grayscale samples (two per byte) into one byte each.
decode_frame_data
¶
decode_frame_data(encoding: str, pixel_format: str, data: bytes) -> bytes
Decode BSB_Frame.Frame.data into RGB bytes using its own metadata.
encoding is the enum name the protobuf message reports
(PLAIN/RUN_LENGTH/DEFLATE/DEFLATE_RUN_LENGTH). pixel_format
accepts either the name the device sends or this package's own
(BGR888/L8/L4), so callers can pass a frame's metadata straight
through. Either way the result is RGB, three bytes per pixel.