Skip to content

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.

BYTES_PER_PIXEL module-attribute

BYTES_PER_PIXEL = 3

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.

data instance-attribute

data: bytes

display instance-attribute

display: DisplaySpec

width property

width: int

Pixels across, taken from the display this frame came from.

height property

height: int

Pixels down, taken from the display this frame came from.

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.

rows

rows() -> list[bytes]

Split the frame into one bytes object per row of pixels.

pixels

pixels() -> list[tuple[int, int, int]]

Every pixel as an RGB triple, in reading order.

is_blank

is_blank() -> bool

Whether every pixel is black.

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

COLOUR_FORMAT module-attribute

COLOUR_FORMAT = 'BGR888'

GREY8_FORMAT module-attribute

GREY8_FORMAT = 'L8'

GREY4_FORMAT module-attribute

GREY4_FORMAT = 'L4'

WIRE_PIXEL_FORMATS module-attribute

WIRE_PIXEL_FORMATS = {'RGB888': COLOUR_FORMAT, 'L8': GREY8_FORMAT, 'L4': GREY4_FORMAT}

DEFAULT_PIXEL_FORMAT module-attribute

DEFAULT_PIXEL_FORMAT = COLOUR_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)

name instance-attribute

name: DisplayName

index instance-attribute

index: int

width instance-attribute

width: int

height instance-attribute

height: int

description instance-attribute

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.