Skip to content

Drawing on the displays

A BUSY Bar has two displays, and every draw call targets one of them explicitly:

Display Size Kind
DisplayName.FRONT 72 × 16 RGB LED matrix, ~16M colours
DisplayName.BACK 160 × 80 Monochrome OLED, 16 shades of grey

Those numbers matter: an element positioned outside its display is accepted by the device but never becomes visible. The client checks for this and logs a warning rather than failing, so watch for it while developing:

Element status y=20 exceeds front height=16

You can read the specs from the library instead of hardcoding them:

from busylib import display

spec = display.get_display_spec(display.DisplayName.FRONT)
print(spec.width, spec.height)

Expected output:

72 16

This is the front display's drawable width and height in pixels; coordinates must stay within those bounds to be visible.

Elements

display_draw takes a DisplayElements payload holding a list of elements. Every element carries an id, a type, coordinates, and the display it belongs to.

from busylib import types

payload = types.DisplayElements(
    application_name="my-app",
    elements=[...],
    priority=None,
    led_notification_color=None,
)

application_name groups everything your app draws, which is what display_clear and assets_delete operate on.

Creating this payload prints nothing and does not update the bar. It becomes visible only after you pass it to display_draw.

Text

types.TextElement(
    id="status",
    type="text",
    x=2,
    y=4,
    text="BUILDING",
    font="small",
    display=types.DisplayName.FRONT,
)

Fonts: tiny, small, normal, condensed, bold, large, extra_large, global.

This creates a local element model only. It has no output or device effect until it is included in a DisplayElements payload and drawn.

Alignment (align) accepts top_left, top_mid, top_right, mid_left, center, mid_right, bottom_left, bottom_mid, bottom_right.

Text longer than its width can scroll, controlled by scroll_rate, scroll_start_delay, and scroll_repeat_delay. Use timeout or display_until to make an element disappear on its own.

Images

types.ImageElement(
    id="icon",
    type="image",
    x=0,
    y=0,
    path="icon.png",
    display=types.DisplayName.BACK,
)

path refers to a file already uploaded for your application — see Assets and storage. Use stock_path instead to reference artwork that ships with the device. opacity blends the image against what's underneath.

This also only creates a payload. Once drawn, the image is rendered on the back display at (0, 0) using the uploaded file named icon.png.

Replacing versus adding

By default a draw call adds to what's on screen. To clear the display before drawing in one step:

bb.display_draw(payload, clear_before_draw=True)

To wipe it entirely:

bb.display_clear(application_name="my-app")

Expected result: neither call prints output. The first call clears the whole display before drawing payload; the second removes only my-app's elements and leaves other applications' drawings alone.

Text that came from somewhere else

Text arriving from an API, a commit message, or a chat room can contain emoji and control characters the firmware won't render. Let the client strip them:

bb.display_draw(payload, sanitize_text=True)

Each substitution is logged, so you can see what was removed and why. The call itself prints nothing; the bar receives the cleaned text rather than characters the firmware cannot render.

Reading the screen back

frame() returns the current contents of a display as a Frame, which is how the remote example mirrors the bar in a terminal:

frame = bb.frame(0)  # 0 = front, 1 = back
print(len(frame.data))

Expected output for the front display:

3456

That is 72 * 16 * 3: three bytes per front-display pixel. The back display produces 160 * 80 * 3, or 38400 bytes.

The bytes are RGB. The device orders colour blue-first and its own protobuf calls that format RGB888 anyway, so the library reorders on the way out rather than passing the firmware's name along — see Displays and frames. screen() still returns the same bytes without the wrapper.

The HTTP endpoint returns base64-encoded, uncompressed framebuffer data despite advertising Content-Type: image/bmp; the client decodes that for you. Live frames also arrive in the device state stream — see Device state.