Skip to content
miblo

SDK

The Miblo App screen: cards, frames, layers and peek

Design the Miblo App screen: screen.card with every item type, frame, layer, show, peek and clear, the flash wear rules and what to do with each error.

The App screen

Since Miblo 1.25, the gadget's screen rotation (Overview, Limits, Sessions) has the App screen, which your program draws. It can be:

  • a card: a title and up to four items (a big number, a ring, a bar, a line of text, a tiny chart), drawn by Miblo itself with its own fonts and colours;

  • a frame: a whole 240x240 image, kept in Miblo's flash;

  • a layer: the card drawn over a frame. It is the right way to a beautiful screen: the background goes once, then only the number changes.

Rules that always hold:

  • Miblo comes first. Alerts, "needs you", setup and codes always draw over the App screen.

  • Your program's name is always on the screen, so the person knows whose it is. It is the tool you pass to screen().

  • Each program has its own screen. With several apps, the App screen shows one at a time and moves to the next every 15 s (miblo apps rotation <seconds>; 0 shows only the pinned one).

  • When the apps show is the person's choice (miblo apps when, the gadget's page or the Miblo app): when nobody is working (the default: after 60 s with no AI session running or waiting), always in the rotation, or only when I ask (pinned or a peek).

screen()

const screen = miblo.screen({ tool: 'steps' });

tool follows a session's tool rules (1 to 20 characters, not a Claude Code name, no leading _). Every write carries the x-miblo-sdk-tool header; the package sends it for you. A program's screen is its declared app's (see Apps), else its name's.

card()

await screen.card({
  v: 1,
  title: 'Steps',
  icon: 'steps',
  items: [
    { t: 'big', value: '8,432', label: 'today' },
    { t: 'ring', value: 0.61, label: 'goal 14k', color: 'amber' },
  ],
});

The card's fields

FieldRules
vAlways 1.
titleOptional, up to 24 characters, at the top of the screen (your program's name follows it).
items1 to 4 items, top to bottom.
iconOptional, a name from the set below. A name outside it is drawn as none.
bgOptional, "frame:0" to "frame:3": draws the card over that frame (a layer). layer() fills it in for you.

The whole card is at most 1024 bytes. Unknown fields are dropped; an unknown item type refuses the card.

The items

tFieldsHow it shows
bigvalue (text or a whole number, up to 10 characters, required), label?, color?A big number with a caption.
ringvalue (0 to 1), label?, color?; live: id, range?, liveA ring that fills up, like the limits' rings.
barvalue (0 to 1), label?, color?; live: id, range?, liveA bar that fills up.
textvalue (up to 40 characters, required), color?A line of text.
sparkvalues (2 to 24 numbers), label?, color?; live: id, range, live (and values becomes optional)A tiny line chart.
rowitems: exactly 2 of the others (never a row)Two items side by side.
  • label: up to 16 characters.

  • id, range and live: a live chart (Miblo 1.27), fed by push(): see below.

  • color: amber, blue, green, red, white or grey. Without one, each kind has its own (big white, ring coral, bar violet, spark blue, text grey). There are no hex colours in v1.

  • icon: steps, heart, water, coffee, sun, moon, cloud, bolt, check, star, clock, calendar, chart, music, mail, code.

How text is handled

  • Runs of spaces become one, invisible, control and bidi characters are removed, the ends are trimmed and each string is cut to its limit.

  • Miblo's phrases are refused (bad_request): a string that starts like one of Miblo's notices (approved:, phone:, miblo:, allow?, task: and their Portuguese forms) or uses the gadget's alert phrases ("NEEDS YOU", "Asked permission", "Asked a question" and translations). The App screen never passes for a Miblo alert.

  • The field at fault comes in field, shaped like items[1].items[0].value.

A card's life

A card lives in the gadget's memory, not its flash: changing it every minute costs nothing. When the Miblo restarts, the bridge sends the card again within 15 minutes. A card not refreshed for 24 hours is dropped (the screen shows "sem dados" with your program's name).

Returns { ok, current, delivered, gadgets }: current says whether your screen is the one on the App screen right now. Cards: 2 a second, bursts of 5.

push()

A chart that changes all the time (CPU, audio, a sensor) has two modes since Miblo 1.27. In near real time, the value travels with each card and the item's fx animates the change on the Miblo: simple, but every card is one request that costs the gadget 40 to 400 ms, and an item with an effect reaches about 10 fps on a plain card (5 under an animation). In the fluid mode, the item declares live and the samples go through push(), at any rate you like:

await screen.card({ v: 1, title: 'CPU', items: [
  { t: 'ring', id: 'cpu', value: 0.4, range: [0, 100], label: 'now', live: { hz: 10, seconds: 3 } },
  { t: 'spark', id: 'hist', range: [0, 100], label: 'last 6 s', live: { hz: 10, seconds: 3 } },
] });
screen.push('cpu', 42.5);                // or screen.push({ cpu: 42.5, hist: 42.5 })

The package keeps the samples with their time and sends them to the bridge every 500 ms (or every 32 samples). The bridge gathers them in batches of seconds (3 s): it resamples the window to hz × seconds values, quantises each one within the range and sends one request per batch to the Miblo, which plays the batch on its own clock, at 24 to 30 fps, redrawing only the chart's rectangle.

The buffer is necessary: the Miblo plays only when it holds one whole batch and the next is already on its way. It costs a delay of 3 s, and that is compensated because the next batch is loaded behind the scenes while the current one is being shown: playback never waits for the network. A late batch holds the last sample (late); two batches in the queue drop the oldest.

  • live: { hz, seconds }: hz from 1 to 30 (default 10), seconds from 1 to 5 (default 3). On spark, ring and bar only, and at most 2 live items per card (outside or inside a row): a third is refused (live_too_many). fx on a live item is ignored.

  • id: 1 to 16 characters [a-z0-9_-], unique in the card, required with live: it is the key push() uses.

  • range: [min, max]: finite numbers, min < max; required on a spark, [0, 1] by default on a ring and a bar. The samples are quantised within it.

  • push() up to 60 times a second (faster is coalesced); one batch per Miblo every seconds, up to 1 KB. Nothing goes to the flash.

  • screen.status() carries live: one entry per id, with seq, buffered (batches in hand, 0 to 2), playing, late and real: { fps, drawMs }, what the Miblo measures. stop(), clear() and a new card without the id end the stream.

  • On a Miblo before 1.27 the bridge falls back to near real time by itself (the latest sample goes into the card every seconds) and says so in live.fallback.

The Screen animations guide has the chapter "Live charts: two modes", with a recipe and the table of which mode to pick; the store's Demo ao vivo app shows both side by side.

frame()

A 240x240 image in one of the 4 slots of Miblo's flash.

import fs from 'node:fs';
await screen.frame(0, fs.readFileSync('background.png'));

What you can send:

  • a PNG (8-bit, RGB or RGBA, not interlaced, up to 4096 a side): it is fitted into 240x240 keeping its proportions;

  • raw RGBA pixels: { rgba, w, h } (TS, with a Uint8Array) or {"rgba": bytes, "w": ..., "h": ...} (Python);

  • a ready MFRM1 file (Miblo's format).

The computer converts the image to at most 64 colours, encodes it by rows and checks it all before sending; no other software is needed. SVG or HTML you render yourself (a canvas, sharp, resvg...) and send the PNG. A background of flat colours and text takes 5 to 30 KB; a photo usually goes over 60000 bytes after the reduction and is refused with its size (too_many_colours).

Returns { ok, slot, bytes, colours, delivered, gadgets }. An upload may take up to 60 s (the file goes to each gadget in chunks).

Flash wears out

Miblo's flash takes a limited number of writes, and a frame is written to it on every upload. So:

  • each slot takes 1 write every 10 s and 100 a day (rate_limited with retryAfter, checked on the computer before anything is converted or sent); the count survives a restart;

  • what changes goes on the card, not the frame. A layer redraws the card without writing anything;

  • a damaged or incomplete frame is deleted, never drawn.

Slots are shared

The 4 slots belong to every program. A slot is the program's that wrote it until it lets go (removeFrame(), clear()) or its screen goes. Writing another program's slot gives slot_taken, with the owner's name in owner.

layer()

The card drawn over a frame: write the background once, then only send cards.

await screen.layer(0, { v: 1, title: 'Steps', items: [{ t: 'big', value: '8,432', label: 'today' }] });

It is card() with bg: "frame:0". Design the background leaving room for the items: the card's layout is fixed per item count.

show()

Pins your screen: it stays on the App screen (no rotation) and the gadget stays on the App screen, like miblo mode app. show(false) unpins it.

await screen.show(true);

Returns { ok, shown, delivered, gadgets }. Once every 10 s.

peek()

"Look now": the App screen comes to the front for 1 to 10 seconds (default 10), unless Miblo is alerting. For the moment that matters (bitcoin moved 5 %, the build broke).

await screen.peek(10);
  • It needs a card first (no_card).

  • Once every 10 minutes per program, and at most one every 10 s (bursts of 3) for all programs together (rate_limited with retryAfter).

  • It brings your screen forward first, unless another one is pinned.

Returns { ok, seconds, delivered, gadgets }.

clear(), removeFrame() and status()

TypeScriptPythonWhat it does
screen.removeFrame(slot)screen.remove_frame(slot)Deletes one of your slots: { ok, slot, delivered, gadgets }.
screen.clear()screen.clear()Takes your screen off: its card, your slots and your pin. miblo screen clear takes all of them.
screen.status()screen.status()Your screen: { card, frames: [{ slot, bytes, at }], shown, current, rotation, anim, live }.

What the gadget does with each call

CallOn the computerOn the gadget
card() / layer()Validates, cleans the strings, keeps itRedraws only what changed; kept in memory
frame()Checks the wear, converts to 64 colours, encodesWrites the whole slot to flash and checks it before use
show()Marks your screen as pinnedStays on the App screen, no rotation
peek()Checks the limitsShows the App screen for a few seconds, unless alerting
clear()Forgets your screen and your slotsTakes the card off and deletes your frames

A gadget that is off or busy is brought up to date by the bridge later on its own. A Miblo on a firmware before 1.25 answers unsupported in gadgets. The Miblo app and the phone draw the same card, pixel for pixel, with the gadget's own code.

Errors and what to do

StatuscodeWhenWhat to do
400bad_request (+ field)A card or name the rules refuseSee field; shorten the text or change the phrase
400bad_frameNot a PNG or MFRM1 the converter takes (e.g. an interlaced PNG)Save as an 8-bit, non-interlaced PNG
409slot_taken (+ owner)The slot is another program'sUse another slot
409no_cardA peek before any cardSend a card first
409too_many_screensThe bridge already holds 64 program screensscreen.clear() the ones you no longer use, or miblo screen clear; a screen nothing wrote for a day goes by itself
413too_large (+ max)A card over 1024 bytes, a frame over 1 MiB, an MFRM1 over 60000 bytes of imageShorten the card; shrink the image
415unsupported_media_typeA frame that is neither octet-stream nor JSONUse the package's frame()
422too_many_colours (+ bytes)The image is too detailed for a frameUse flat colours and fewer gradients
429rate_limited (+ retryAfter)Cards, frames (the wear), show or peek over the limitWait retryAfter seconds; change the card, not the frame

Complete example: a steps app

The background goes once; then, every minute, only the number changes (no flash writes).

// MIBLO_TOKEN=$(miblo sdk token) node steps.mjs
import fs from 'node:fs';
import { MibloStatus } from '@miblo/status';

const screen = new MibloStatus().screen({ tool: 'steps' });
await screen.frame(0, fs.readFileSync(new URL('./steps-background.png', import.meta.url)));
await screen.show(true);                       // or: miblo mode app

let steps = 8432;
for (;;) {
  await screen.layer(0, { v: 1, title: 'Steps', icon: 'steps', items: [
    { t: 'big', value: steps.toLocaleString('en-US'), label: 'today' },
    { t: 'ring', value: Math.min(1, steps / 14000), label: 'goal 14k', color: 'amber' },
  ] });
  await new Promise((r) => setTimeout(r, 60_000));
  steps += Math.floor(Math.random() * 120);
}

The TypeScript example and its background image ship inside the @miblo/status package (examples/steps.mjs and examples/steps-background.png).