Obsoleto desde Miblo 1.45. La app del Desk estándar ahora es una página web, que Miblo Hub dibuja en el computador y el Miblo Desk solo pinta (apps del Desk de página (/es/sdk/paginas)). Las animaciones de abajo siguen funcionando, pero una app del Desk nueva debe animar una página.
The arithmetic behind this guide (what a scene costs in RAM, what Miblo lends to the network and how to read the diagnostics when a scene does not load) is in Miblo's memory (in the repository, docs/miblo-memory.en.md).
This guide explains how Miblo animates its screen and how to get the most out of it in your app. The rule behind everything: the SDK slices, Miblo animates. Your program sends the art once and a few numbers; the device computes every frame itself. Nothing streams while an animation plays.
There are three levels, and only three:
A built-in effect on a card item (
fx): one word, no art. Start here.screen.animate('art.gif', { fps }): the SDK does everything else.The same call with options (
fps,loop,region,fx,speed,amplitude,zoom,period,centre,dir,scene,slot): per-line perspective and rotation effects and scenes that stay in the flash. 3D is a card item ({ t: 'mesh' }).
Slots, formats, steps and tiles never appear in your program. A program has one animation on Miblo at a time: the next call replaces the previous one, and the card is drawn over it. To stop your animation: screen.stop().
A chart that changes all the time (CPU, audio, a sensor) is another matter: it has two modes, and chapter 7 (Live charts: two modes (#7-live-charts-two-modes)) says which one to pick.
Numbers marked (measured on Amon, firmware 1.26.0) were measured on a real Miblo with the Demo app; those marked (estimated, measured on the device before release) come from the design and the arithmetic.
1. How Miblo draws
No frame buffer
The screen has 240x240 pixels of 16 bits: one full picture takes 115,200 bytes. The ESP8266's 80 KB of application RAM are shared by the firmware, the network and the apps' state; how much is free varies with the scene and the connections. There is no copy of the screen in memory: Miblo draws straight to the panel, in strips, and whatever is on the glass stays there until something covers it. Everything below follows from that.
The cost is the bus
Every pixel travels over the SPI bus to the panel. The cost of drawing is, almost entirely, how many pixels go through it:
| Area redrawn | Pixels | Time at 40 MHz | Time at 80 MHz |
|---|---|---|---|
| One 8x8 tile | 64 | ~0.04 ms | ~0.02 ms |
| One 16x16 sprite | 256 | ~0.1 ms | ~0.05 ms |
| One 32x32 sprite | 1,024 | ~0.4 ms | ~0.2 ms |
| 100 tiles (a typical animation) | 6,400 | ~4 ms | ~2 ms |
| A 120x120 region | 14,400 | ~7 to 10 ms | ~4 to 6 ms |
| The whole screen, 240x240 | 57,600 | ~25 to 40 ms | ~13 to 20 ms |
(estimated, measured on the device before release)
The takeaway: animating the whole screen is expensive; animating what changes is cheap. An effect that touches a few tiles runs at 30 frames per second; the same effect over the whole screen, at 4 to 8.
One loop, shared with Wi-Fi and the pet
Miblo has a single main loop. Wi-Fi, the pet, your session alerts, the clock and your animation all run in it. That is why drawing happens in strips and hands control back between one strip and the next: Wi-Fi never waits for a whole screen.
The frame governor
Everything that animates on Miblo (the items' effects, a 3D model, the transition between screens, a live chart, your animation) goes through a governor, which measures how long each frame took to draw. The fps you ask for is a target, and there is one rule: fps asked × measured cost. Each kind draws again when its interval has passed, and that interval is the larger of the one asked (1000 / fps) and the frame's measured cost plus 2 ms, for the loop to serve the network. While drawMs + 2 ms fits the interval asked, you get the fps you asked for. When everything animating together passes 90 % of the loop, every interval grows in the same proportion; when the measure drops, the interval drops with it, with no ramp. There is no rate floor by the area that changed and no cap of a third of the loop per kind: a cheap frame is a fast frame.
The caps on what is asked: item effects and wireframe meshes 30 fps, filled meshes 15, live charts 30, your animation 60 (an animation file's steps last 20 ms at least, so 50 fps; card frames, 100 ms, 10 fps). An animation with the card's items over it is measured once, as a single composition. Wi-Fi, the pet and an alert never wait for an animation. If your animation looks slower than the fps you asked for, it was the frame's cost: see Debugging with drawMs (#debugging-with-drawms).
2. Tiles, maps and sprites
A 240x240 screen driven by a small chip with 80 KB of RAM for everything (the firmware, Wi-Fi, your sessions) and no graphics hardware pushed us into the same kind of constraints the classic game consoles faced. We took inspiration from how that era thought about memory, and developed our own techniques for this hardware: tiles and maps, sprites, sub-palettes, effects computed row by row, the governor, the scenes kept in the flash, the compacted resident tiles and live charts with the 3 s buffer. It all rests on three pieces.
Tiles
A tile is an 8x8 drawing stored once. Each pixel is a 4-bit index into a 16-colour sub-palette (taken from the animation's 64): 32 bytes per tile. A tile set holds up to 256 tiles (up to 8 KB), lives in a flash slot and is copied to RAM while the animation plays.
In RAM, Miblo keeps each tile in the shortest form that reproduces its pixels exactly: a one-colour tile takes 1 byte, a two-colour one 9, three or four colours 18, and only a tile with more colours stays at the file's 32 bytes (plus a 2-byte index per tile; the compact form is used only when the total comes out smaller than the file). No colour is dropped and nothing changes in the file or in your program: the Radar, with 237 tiles, went from 7.6 KB to 3.8 KB of resident tiles. That is the number that enters a scene's memory arithmetic (see What a scene costs in RAM (#what-a-scene-costs-in-ram)): art with few colours per tile is art that loads on more Miblos.
Maps
The screen is a 30x30 grid of tiles (240 / 8). A map says which tile goes in each cell: 900 cells, 1,800 bytes, against 115 KB for a picture. An animation is a sequence of up to 16 maps. Between one map and the next, Miblo redraws only the cells that changed.
Sprites
A sprite is an object that moves over the map: up to 16 per step, squares of 8, 16 or 32 px, with a transparent colour. At every step Miblo restores the tiles under the old position and draws the sprite at the new one: about 0.1 ms per sprite (estimated, measured on the device before release). The background is never redrawn whole.
Palettes
The animation uses up to 64 colours. Each tile picks one of up to 8 sub-palettes of 16 of them: 4 bits per pixel, yet the screen as a whole can show all 64.
More from the format
Flips: a tile can be drawn mirrored horizontally or vertically. A symmetric drawing needs half the tiles.
Priority: the format has a bit for a tile or a sprite to sit in front of the card's items. The SDK does not use it: your animation always sits behind the card.
Sprites up to 32x32. The format also allows a half-transparent sprite; the SDK does not make one for now.
Scrolling: a background that scrolls across the GIF's frames costs no new tiles (the same tiles in other cells), but every cell that changes is redrawn. That is why scrolling is the job of the
scrolleffect, which Miblo computes row by row with no frame at all.
How many fps this gives here (measured)
On Amon, a step of rain.gif (8 frames, 23 tiles, 1 sprite, the whole screen, 8 fps asked) costs 55 ms under a minimal card (the title and a one-character item) and 62 ms under a card with a sentence; a 32x32 sprite crossing a still map, 62 ms. Under 1.26.0's policy, which let no kind of animation take more than a third of the loop, those steps came at 6 fps (minimal card) and 5 fps (one item) (measured on Amon, firmware 1.26.0). Since 1.28.1 the governor charges only the measured cost: 55 ms + 2 ms fit the 125 ms of 8 fps, so the 8 fps asked fit the arithmetic; the measurement under the new policy goes here once taken (estimated, measured on the device before release). And nothing comes from the flash on the critical path: tiles and maps are already in RAM (a step that only moves sprites does not even read the map again).
3. What the SDK does with the art
You send an animated GIF, an APNG, a PNG, a list of PNGs or a sprite sheet ({ sheet, cell, count }), up to 8 MiB. The SDK, in the bridge on your computer, does all the work of preparing the art:
Decodes the art (the bridge's GIF decoder has no dependencies; up to 256 frames).
Fits every frame into the
region(the rest of the screen is the App screen's background) and merges repeated frames.Quantises to 64 colours (the Miblo palette's colours stay exact) and picks up to 8 sub-palettes of 16 for the tiles.
Cuts it into 8x8 tiles and drops the duplicates across all frames, including those that mirror another. Pixel art and UI repeat a lot. A few tiles over 256 are merged with their nearest.
Builds one map per distinct frame (up to 16: the 16 most different stay).
Finds the sprites: objects of up to 32x32 that appear in two frames or more in different places become sprites (up to 16 shapes), not new tiles, when that saves tiles; and, at the same tiles and the same pixels, when the file gets smaller (fewer maps: a moving sprite changes no map, and a step that only moves sprites does not even read the map again on Miblo).
Picks the path, without you asking:
| The art | The path | Why |
|---|---|---|
| Pixel art, UI, a repeating background (fits in 256 tiles) | Tiles and maps, up to 50 fps | Only the tiles that change are redrawn |
| Small objects moving over a still background | Sprites over the map | The background is never redrawn |
| Over 256 tiles, but only part of the screen changes (up to 120x120) | A base frame and one rectangle per step, up to 10 fps | Pays only for the area that changes |
| Over 256 tiles and big changes (a photo) | Whole frames, up to 8, up to 10 fps | Cost depends on frame size and differences |
One picture with fx | A per-line effect: a tile layer, the motion computed on Miblo | No frame at all |
fx on a card item, or a mesh item | A firmware effect | No art at all |
Converts, picks the slots and sends each part. A file Miblo already holds in a slot is not written again: the bridge keeps the SHA-256 of what each slot of each gadget holds (in its data folder, checked against the gadget) and plays it from there, also after a restart. Sending the same art again costs no flash (see the scenes, in chapter 6).
Checks the limits (the slots, the fps, the day's writes) before anything reaches Miblo, and explains every reduction or refusal in plain words, in English and Portuguese (
warnings: [{ code, en, pt }]; see Reading the SDK's messages (#reading-the-sdks-messages)). WithdryRun: trueit answers the plan and the warnings only, sending nothing.
4. Drawing art that animates well
The right art is the difference between a 2 ms step and a 40 ms one, and between a scene that loads on any Miblo and one that waits for memory: the governor charges what each frame costs (how many pixels change), and the RAM charges how many distinct tiles there are and how many colours each one uses. The rules:
Draw on the 8-px grid. Edges, bands and objects aligned to multiples of 8 become whole, repeated tiles. A line crossing a tile in the middle creates new tiles.
Flat areas. A plain colour is a single tile, repeated as often as needed.
Repeat. Grass, water, stars, tarmac: a pattern of 8, 16 or 32 px that repeats costs few tiles.
Few gradients. A smooth (or dithered) gradient makes a different tile in every cell. Use bands of 2 or 3 shades.
A still background, a few moving objects. A fixed scene with one or two small objects moving is the perfect case: the background goes once, the objects become sprites.
Up to 16 colours per tile. The screen can show 64, but each 8x8 tile uses 16 at most.
Photos are not animations. A photo has a different tile in every cell: it ends up as whole frames targeting up to 10 fps, with higher read and draw costs. For a still photo, use a background (
bg: 'frame:N').
The Demo follows all of this: the road (128x128) is 224 bytes, the planet (112x112) 643 bytes, the water (240x120) 285 bytes. Its three pictures are drawn by code in apps/scripts/demo-assets.mjs.
5. Built-in effects, per-line effects and 3D
Effects on a card item (fx)
A card item may carry fx. The firmware computes the effect itself: no frame, no upload, no flash wear. It is the simplest way to animate, but not a free one: an item with an effect is redrawn whole at every step, about 34 ms a step (measured on Amon, firmware 1.26.0) and up to 62 ms in the 1.30 measurements the bridge's budget uses. In practice that is about 10 to 15 fps on a plain card and 5 fps while an animation plays.
fx | On which item | What it does | When it costs |
|---|---|---|---|
sweep | ring, bar | The ring or bar fills to the new value | When the value changes |
count | big | The number counts to the new value | When the value changes |
slide | spark | The chart slides when a new point arrives | When a point arrives |
pulse | big, ring, bar, text, spark | A short glow when the value changes | When the value changes |
blink | big, ring, bar, text, spark | Blinks, to draw attention | All the time |
orbit | ring | A dot circles the ring; speed in deg/s (0 to 360) | All the time |
rain | big, ring, bar, text, spark | Drops falling over the item | All the time |
ticker | text | The text scrolls once, when it does not fit | When the text changes |
A row and a mesh take none of these fx. An effect that runs all the time (blink, orbit, rain) shares the Miblo's loop with your animation and live charts: miblo screen status and screen.budget() say what the whole screen costs and the fps it really reaches.
Effects run on every card update: send the new value, Miblo animates the change. Nothing changes in the API.
One picture in motion: per-line effects
These are the per-line perspective and rotation effects. With fx on screen.animate, the picture (the first frame, if the art has several) becomes a tile layer in RAM (up to 8 KB of tiles and a map) and Miblo computes every row with integer math. Only the art (once) and a few numbers leave the computer: it is the safest animation there is. The picture has to fit in 256 tiles (flat colours and repeating patterns fit).
fx | Numbers | What it does |
|---|---|---|
scroll | speed px/s; dir: left, right, up, down or degrees (0 = right, 90 = down) | The picture scrolls that way and wraps around |
rotate | speed deg/s; zoom in thousandths (1000 = natural size); centre: [x, y] | Turns the picture around the centre |
zoom | zoom (the mean size); amplitude in thousandths (the swing); speed deg/s of the swing; centre | The scale swings back and forth around the mean size |
perspective | speed px/s (how fast the ground comes at you); zoom; centre (the vanishing column) | Tilts the picture to the horizon, at the top of the region: a road |
wave | amplitude px (0 to 64); period rows (8 to 240); speed deg/s of the phase | Shifts each row sideways on a sine: water, heat haze |
The limits: speed -720 to 720, amplitude 0 to 900 (wave: 0 to 64), zoom 250 to 4000, period 8 to 240, centre on the screen, dir 0 to 359. A value outside them is brought to the limit and said (clamped); a number that is not a number is refused (bad_option). Without the number, each effect has its default: rotate 45 deg/s, zoom a 90 deg/s swing of 500 thousandths, perspective 60 px/s, wave 6 px with period 64 at 180 deg/s, scroll 30 px/s to the right.
To change the numbers later, without sending the art again: screen.mode7({ fx, speed, ... }) (fps too, 1 to 60; 30 when left out).
The smaller the region, the higher the fps: whole screen ~4 to 8 fps, a 120x120 region ~15 fps (estimated, measured on the device before release). The governor applies here too.
3D: the mesh item
A 3D model is a card item, like big or ring: { t: 'mesh', mesh, fx, speed, shading, light }. It takes the item's cell (there is no region) and is turned by the firmware with integer math.
mesh:'cube','icosphere','miblo'(the logo) or a model of your own,{ v: [[x, y, z], ...], f: [[a, b, c], ...] and/or e: [[a, b], ...] }(the SDK also takes{ vertices, faces }): coordinates from -1 to 1, up to 64 vertices, 128 faces and 128 edges. A model of your own has to fit the card's 1 KB: about 16 vertices. An.objgiven toscreen.mesh()is reduced by the bridge until it fits, and it says so.fx:spin(turns around one axis) ortumble(rolls on two).speedin deg/s, 0 to 360.shading: 'wire'(lines; only the tiles under the old and new lines are redrawn: ~90 ms a step for the Miblo logo, 2 to 3 fps (measured on Amon, firmware 1.26.0)) or'flat'(filled triangles with a simple light,light: [x, y, z]; needs faces; the cell is composed off-screen and painted whole, in the background colour: 10 to 15 fps) (estimated, measured on the device before release).A card has at most one
mesh, and never inside arow.
The shortcut screen.mesh('cube', { fx, speed, shading, light, title }) (or screen.animate({ mesh }, opts)) builds a card with that one item and shows it in place of yours. To put the 3D next to other items, place the item in your own screen.card, as the Demo does.
Examples in motion
Every effect above shows in the Demo app (store, Delights category): perspective on the road, rotate on the planet, wave on the water with rain on an item over it, the card effects on the dashboard and a mesh with spin on the logo. Each scene's code is in apps/lib/demo-scenes.js, and miblo screen preview shows any effect on your computer before you send it.
6. Composing a scene
One animation per program
Miblo plays one animation per program. A new screen.animate replaces the previous one on the screen (its files stay in the slots: see the scenes, below). The animation plays while your program's screen is the one on the App screen: sent while another app's turn is on, it is kept (queued: true) and plays when your turn comes — no need to send it again. Only while another program holds the screen right now (pinned, or a peek) the answer is 409 anim_taken with its name (owner) and retryAfter. screen.stop() takes yours off the screen; the files stay in the slots and the card stays.
So a scene is not a stack of calls: it is one piece of art holding the whole scene plus the card on top.
The art carries the moving parts
One GIF (or APNG, or PNGs) with everything in it. Draw the background and the objects in the same file. The SDK lifts out as sprites, by itself, the objects of up to 32x32 that appear in two frames or more in different places; the background becomes tiles and is never redrawn whole. A car driving over a still road, a fish over the water: a single GIF. With
loop: falseit plays once.One picture, with
fx. The motion is computed on Miblo (a per-line effect). To vary the scene without sending anything:screen.mode7()changes the numbers.
The card on top
Miblo draws your card over the animation: the title band (the top 24 px, always Miblo's) and every item. An item with fx (sweep, count, slide, pulse, blink, orbit, rain, ticker) and a mesh item animate right there, computed by the device itself: no upload, no write. Updating the card leaves the animation alone; and an animation with no card at all is the screen.
A still item (big, text or bar with no fx) never flickers over the animation: Miblo composes it in memory with the animation's pixels, strip by strip of 240x16 and only on the rows where it has ink, and sends it to the panel in one push, the first time it appears, when its value changes and at every frame moving under it. When memory is to spare, the item's capture is kept and not even that is redone; when it is not, the strip is temporary and goes back before the network needs it.
Two cost rules: start the region at y ≥ 24, since whatever sits under the title band never shows; and an item with fx, a ring, a spark or a mesh over an animation still repaints its whole box at every step (the animation's pixels first, then the item), so prefer small items and few effects over the art.
What a scene costs in RAM
A tile scene takes, in Miblo's RAM, the compacted resident tiles (1 to 32 bytes a tile, chapter 2 (#tiles)) plus the player (3.1 KB) and the palette cache (0.5 KB), and loads only when that sum fits beside an 8 KB floor, in one whole block. The Radar (237 tiles) costs 7.4 KB and loads with about 15.6 KB free; a small scene with flat colours costs 4 to 5 KB and loads with about 12 to 13 KB. On a paired Miblo with a card set and the bridge's stream up, about 17 to 19 KB are left between one request and the next. The fps does not change this arithmetic; the number of distinct tiles and of colours per tile does.
Frames (rectangle or whole) leave no player in RAM: at each step Miblo opens a decoder (0.3 KB) and a strip of 8 rows of 240 px (3.8 KB), released as soon as the step is drawn, and keeps the frames' files open (0.3 KB each: the base and the step for rectangles, the step alone for whole frames). That is 4.6 KB for rectangles and 4.4 KB for whole frames, whatever the rectangle's size; without room for the strip, the step is drawn one row at a time (0.5 KB), more slowly.
The scene is a cache of the flash: when a large request needs the memory, Miblo lends the scene's (the picture stays on the glass) and loads it again where it was, at the same step, 150 ms later. The order of the loans, the waits and what each "not loaded" reason means are in Miblo's memory.
Scenes: upload once, replay for free
The flash takes one write per slot every 10 s and 100 per slot a day; the SDK refuses with 429 and retryAfter (seconds) before that. Playing an animation only reads the flash. So an animation's files stay in their slots when it stops or when another scene of the same program plays, and playing again what Miblo already holds writes nothing: no wear, no wait, no limit. What makes the replay free is the content: the bridge knows, by SHA-256, what each slot of each gadget holds (in its data folder, checked against the gadget), so it holds even after a restart.
Two hints on screen.animate steer where a new file goes:
scene: 'name'(1 to 32 letters, digits,-or_): the same scene always comes back to the same slots, and the program's other scenes stay in theirs.slot: nor[n, ...](0 to 47): the slots to prefer.scene: 'moment': a moment (a goal, a build gone green), played for seconds. It does not go to the program's slots but to a moments' pool the whole Miblo shares (4 slots, two tile scenes, plus the slots left free), the least recently played giving way. A moment never takes another app's resident scene; the same moment again, still in the pool, writes nothing.
A Miblo has 48 slots (it says slotCount in GET /api/screen) and the bridge uses the smallest count among the paired Miblos. The real limit is the flash's space: every write leaves 350 KB free (else 507 no_space, with the free bytes), and an app's scene takes about 18 KB, so the resident scenes of every animated app today (18 apps, 36 slots) fit with the moments' pool.
A new file goes to the hinted slots; else to the ones the scene had; else to a free slot (one the flash may write now first; the moments' pool's share stays free); else to this program's least recently played slot; else to a pool slot past the pool's share; else to one of an app turned off or hidden; else to one of an app that has not played for 6 hours. Never the app on screen's, and, in a rotation, never another app's: the app past the capacity stays static (no_free_slots, its card stays) rather than apps taking turns writing the flash. Slots 4 and up are the animation's; a scene kept in 0 to 3 (and not playing) gives way to another program's screen.frame. The answer says writes (files written) and slots (the ones used). A program's slots go with its screen (screen.clear(), or a day with nothing written).
That is what the Demo does: three scenes (scene: 'road', 'planet', 'water') in six slots, written on the first round only; after that every round plays for free (the test apps/test/demo.test.js proves a cycle writes at most six files and the later ones none). In a scene that changes, vary the card and the effect's numbers, not the art.
Recipes
Each recipe is 15 lines at most and runs as is (Node 20+, @miblo/status). The road recipe needs road.png in the folder you run it from: it ships in the SDK package (node_modules/@miblo/status/examples/road.png). The water recipe takes any picture of your own (water.png, about 240x120 with a few flat colours); the Demo's own water.png is not distributed.
Rain
// recipe: rain
import { MibloStatus } from '@miblo/status';
const screen = new MibloStatus().screen({ tool: 'Weather' });
// Rain is a firmware effect over the item: no art, no upload.
await screen.card({
v: 1, title: 'London', icon: 'cloud',
items: [
{ t: 'big', value: '12°', label: 'heavy rain', color: 'blue' },
{ t: 'text', value: 'rain until 4 pm', fx: 'rain' },
],
});Orbit
// recipe: orbit
import { MibloStatus } from '@miblo/status';
const screen = new MibloStatus().screen({ tool: 'ISS' });
// A dot circles the ring: the station going round the Earth.
await screen.card({
v: 1, title: 'ISS', icon: 'star',
items: [
{ t: 'ring', value: 0.62, label: 'this orbit', color: 'blue', fx: 'orbit', speed: 60 },
{ t: 'text', value: 'over the Atlantic Ocean' },
],
});Pulse
// recipe: pulse
import { MibloStatus } from '@miblo/status';
const screen = new MibloStatus().screen({ tool: 'Sales' });
// Glows once on every new value: send the card again when the number changes.
await screen.card({
v: 1, title: 'Sales', icon: 'chart',
items: [{ t: 'big', value: '128', label: 'today', color: 'green', fx: 'pulse' }],
});Road
// recipe: road
import { MibloStatus } from '@miblo/status';
const screen = new MibloStatus().screen({ tool: 'Road' });
// One picture holding the whole scene; Miblo computes the perspective and the motion.
await screen.animate('road.png', { scene: 'road', fx: 'perspective', speed: 60, region: [0, 112, 240, 128] });
// The card is drawn on top by Miblo itself: the number counts to the new value.
await screen.card({
v: 1, title: 'Road', icon: 'star',
items: [{ t: 'big', value: '75', label: 'mph', color: 'amber', fx: 'count' }],
});
// To speed up without sending the art again: screen.mode7({ fx: 'perspective', speed: 120 }).Water
// recipe: water
import { MibloStatus } from '@miblo/status';
const screen = new MibloStatus().screen({ tool: 'Aquarium' });
// The wave shifts each row of the water, computed on Miblo. The scene stays in the flash.
await screen.animate('water.png', { scene: 'water', fx: 'wave', amplitude: 4, period: 32, speed: 180, region: [0, 120, 240, 120] });
// Rain is an item effect, drawn over the water: no extra art.
await screen.card({
v: 1, title: 'Aquarium', icon: 'water',
items: [{ t: 'text', value: 'rain over the aquarium', fx: 'rain' }],
});Counter
// recipe: counter
import { MibloStatus } from '@miblo/status';
const screen = new MibloStatus().screen({ tool: 'Downloads' });
// Send only the new value: Miblo counts from the previous one to it.
const total = 48213;
await screen.card({
v: 1, title: 'Downloads', icon: 'chart',
items: [{ t: 'big', value: total.toLocaleString('en-US'), label: 'this week', fx: 'count' }],
});Ticker
// recipe: ticker
import { MibloStatus } from '@miblo/status';
const screen = new MibloStatus().screen({ tool: 'Headlines' });
// A text that does not fit scrolls once, from start to end.
await screen.card({
v: 1, title: 'Headlines', icon: 'mail',
items: [{ t: 'text', value: 'Central bank holds rates, hints at cuts', fx: 'ticker' }],
});Cube
// recipe: cube
import { MibloStatus } from '@miblo/status';
const screen = new MibloStatus().screen({ tool: 'Cube' });
// 3D is a card item: Miblo turns the model and redraws only the tiles under the lines.
await screen.card({
v: 1, title: 'Cube', icon: 'star',
items: [
{ t: 'mesh', mesh: 'cube', fx: 'spin', speed: 90, shading: 'wire' },
{ t: 'text', value: 'wireframe, turned by Miblo' },
],
});
// The same in a card with the cube alone: screen.mesh('cube', { fx: 'spin', speed: 90 }).7. Live charts: two modes
Everything so far is art and effects: Miblo computes the motion itself, from a few numbers. A chart that changes all the time is different: the numbers come from your program, without end (the CPU, the audio level, a sensor, the speed). Since Miblo 1.27 a spark, a ring or a bar has two modes for that.
Near real time (what every card already does)
The value travels with each card, and the item's fx animates the change on Miblo itself: sweep on the ring and the bar, slide on the chart, count on the number. Simple, nothing new to learn, no delay beyond your card's own cadence.
The limits are the card's. Every update is one HTTP request, which costs Miblo 40 to 400 ms and a TCP connection; the bridge takes 2 cards a second (bursts of 5). And an item with fx is redrawn whole at every step, about 34 ms: in practice it reaches ~10 fps on a plain card and 5 fps under an animation (measured on Amon, firmware 1.26.0). Sub-second updates from the computer do not fit this hardware. For a number that changes every few seconds it is the right mode; for a chart that has to flow, it is not.
Fluid animations (live)
In the fluid mode the item declares live, and the samples go through screen.push, at any rate you like:
await screen.card({ v: 1, title: 'CPU', items: [
{ t: 'spark', id: 'cpu', range: [0, 100], live: { hz: 10, seconds: 3 }, label: 'now', color: 'green' },
] });
screen.push('cpu', 42.5); // or screen.push({ cpu: 42.5, net: 1200 })What happens behind it:
The SDK keeps every sample with its time and sends them all to the bridge every 500 ms (or every 32 samples), in
POST /sdk/v1/screen/live.The bridge gathers the samples in batches of
seconds(3 s by default): it resamples the window to exactlyhz × secondsvalues at a fixed rate (holds the last value over gaps, interpolates between samples), quantises each one to 0..1000 within therangeand sends one request per batch to Miblo, with every live item of the card at once. A window with no sample at all sends nothing, and Miblo holds what it had. Batches go only while your program's screen is the one on the App screen; when it comes up (the rotation, apeek, a pin), the bridge sends the latest complete window at once, and the chart fills within about a second. Your program need not know: it pushes whenever it runs.Miblo plays the batch on its own clock, at 24 to 30 fps (estimated, measured on the device before release), interpolating between one sample and the next and redrawing only the chart's rectangle (the
spark's plot, the ring's band, the bar), never the whole card. Chapter 1's governor applies here too (kindLive, capped at 30 fps).
The buffer is necessary: Miblo starts playing only when it holds one whole batch and the next is already on its way. It costs a delay of 3 s (seconds), 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. If a batch is late all the same, Miblo holds the last sample (a flat line) and marks late; if two batches pile up behind the one playing, it drops the oldest and catches up with the program.
More of the mode:
A live
sparkshows the lasthz × seconds × 2samples (two batches), scrolling; the card'svaluesbecome optional (they are the first picture, before the first batch).range: [min, max]says what the samples mean: required on aspark; on aringand abarthe default is[0, 1]. A sample outside therangeis brought into it.fxon a live item is ignored: the playback is the animation.screen.stop()andscreen.clear()end the streams; a new card without theidends that item's.Nothing goes to the flash, and nothing survives a bridge restart: the stream restarts with the program.
Limits
2 live items per card, at the top level or inside a
row; a third is refused (live_too_many).live.hzfrom 1 to 30 (default 10);live.secondsfrom 1 to 5 (default 3).id: 1 to 16 characters[a-z0-9_-], unique in the card, required withlive; it is the keypushuses.range: two finite numbers,min < max; required on aspark.One batch per card: every live item of a card has the same
live.seconds(a different one is refused), and theirhz × secondsadd up to 180 at most (two items at 30 Hz × 3 s fit; two at 30 Hz × 5 s do not).pushup to 60 times a second perid(faster is coalesced); the SDK sends 1 to 128 samples a request, each at most 60 s old, and the bridge takes 10 such requests a second (bursts of 20), answering{ ok, accepted, unknown }(unknown: ids the current card has no live item for). One batch per Miblo everyseconds, up to 1 KB; Miblo answers429to a second batch within 1 s.pushnever throws: a refused flush goes toscreen({ tool, onPushError }), else it is dropped;await screen.flush()sends now and throws it.A bad
live,idorrangeis refused like any card field (bad_requestwith thefield).
Recipe: live CPU
// recipe: live
import os from 'node:os';
import { MibloStatus } from '@miblo/status';
const screen = new MibloStatus().screen({ tool: 'CPU' });
// Fluid mode: the ring and the chart get samples by push; Miblo plays them at 24-30 fps.
await screen.card({
v: 1, title: 'CPU', icon: 'chart',
items: [{ t: 'ring', id: 'cpu', value: 0, range: [0, 100], label: 'now', live: { hz: 10, seconds: 3 } },
{ t: 'spark', id: 'hist', values: [0, 0], range: [0, 100], label: 'last 6 s', live: { hz: 10, seconds: 3 } }],
});
const cpu = () => Math.min(100, (os.loadavg()[0] / os.cpus().length) * 100);
// Ten samples a second (in your program, for as long as it runs): the SDK batches and sends every 500 ms.
for (let i = 0; i < 10; i++) { await screen.push({ cpu: cpu(), hist: cpu() }); await new Promise((r) => setTimeout(r, 100)); }Which mode to pick
| You want | The mode | Why |
|---|---|---|
| A number that changes every few seconds (steps, a price, downloads) | Near real time, with fx | No delay; the card already does it |
| A chart that has to flow (CPU, audio, a sensor, a speedometer) | live | 24 to 30 fps on Miblo, with one request every 3 s |
| The value on the screen the instant it happens (a goal, an alert) | Near real time, and a peek | The fluid mode arrives 3 s later, because of the buffer |
| More than two live charts on the same card | Near real time | live is 2 per card |
The live demo
The Demo ao vivo app (store, Delights category) shows both modes: a heartbeat on a spark at 30 samples a second, two music meters with the average coming by card, the same wave drawn both ways side by side (the difference is the point), a speedometer and the explanation. The last line of every screen is what Miblo measures: "fluid · 24 real fps · 2 batches". The scenes' code is in apps/lib/demo-ao-vivo-scenes.js, and My computer shows the CPU this way (5 samples a second, batches of 3 s).
8. Limits, messages and debugging
Limits
| What | Limit |
|---|---|
| Animations | 1 per program, played on its turn on the App screen (queued meanwhile); the next replaces it (the files stay in the slots); 409 anim_taken only while another program is pinned or peeking |
| Flash slots | 48 (slotCount in GET /api/screen): a tiles or per-line effect animation takes 2, frames take 1 each (up to 8 frames); slots 0 to 3 belong to the programs' screen.frame, an animation takes 4 and up first; each app keeps its resident scene in its own slots, moments (scene: 'moment') share a pool of 4 slots (plus the free ones); a scene gives way only when its app is off, hidden or has not played for 6 h, never the app on screen's; scene and slot say where a new file goes |
| Flash space | every write leaves 350 KB free (507 no_space, with the free bytes); an app's scene is ~18 KB (tiles 0.2 to 8.5 KB, animation 2 to 37 KB); an app whose files do not fit stays static for 1 h, rewriting nothing |
| The art | GIF, APNG, PNG, a list of PNGs or a sprite sheet, up to 8 MiB; up to 256 frames read |
| Tiles and maps | up to 256 tiles of 8x8, 8 sub-palettes of 16 colours, 64 colours; up to 16 maps (the 16 most different frames), 20 to 2000 ms each, up to 50 fps asked; the real rate is the measured cost (chapter 1's governor). In RAM, each tile takes 1 to 32 bytes by its colours, plus the player (3.1 KB) and the cache (0.5 KB), beside an 8 KB floor. Measured on Amon (firmware 1.26.0, under the third-of-the-loop policy of the time), whole screen under a card: 55 ms a step with a minimal card (6 fps), 62 ms with one item (5 fps) |
| Sprites | up to 16 shapes of 8, 16 or 32 px, 16 per step |
| Rectangle frames | what changes fits in 120x120; one base frame and up to 7 rectangles, 100 to 2000 ms, up to 10 fps; in RAM, 4.6 KB while playing (decoder, 8-row strip and two open files) |
| Whole frames | up to 8, 100 to 2000 ms, targeting up to 10 fps; a frame with over 60 KB of data is refused (too_detailed); over 12 KB the SDK warns about read and draw cost; in RAM, 4.4 KB while playing |
The fps asked | 0.5 to 60; above what the path holds it is lowered and said |
Per-line effects (fx on screen.animate) | one picture (the first frame), up to 256 tiles; speed -720 to 720, amplitude 0 to 900 (wave: 0 to 64), zoom 250 to 4000, period 8 to 240, centre on the screen, dir 0 to 359; outside that it is clamped and said; whole screen ~4 to 8 fps, 120x120 region ~15 fps (estimated, measured on the device before release). Measured on Amon with the card's items over it: 112x112 rotate 3 fps (101 ms), 240x120 wave 2 fps (143 ms), 240x128 perspective 1 to 2 fps (158 to 262 ms) |
Item fx | sweep (ring, bar), count (big), slide (spark), orbit (ring; speed 0 to 360), ticker (text), pulse, blink and rain (big, ring, bar, text, spark). Measured on Amon: ~34 ms a step (firmware 1.26.0), up to 62 ms in the 1.30 measurements the budget uses; about 10 to 15 fps on a plain card, 5 fps while an animation plays |
mesh | 1 per card, outside a row; up to 64 vertices, 128 faces and 128 edges, coordinates from -1 to 1; a model of your own fits the card's 1 KB (about 16 vertices); fx spin or tumble, speed 0 to 360; flat needs faces. Measured on Amon: wire ~90 ms a step for the Miblo logo (2 to 3 fps) |
Live charts (live) | on spark, ring and bar; 2 per card (outside or inside a row), sharing one seconds, their hz × seconds adding up to 180 at most; id of 1 to 16 [a-z0-9_-], unique in the card; a finite range with min < max (required on a spark; ring and bar [0, 1]); hz 1 to 30, seconds 1 to 5; push up to 60 a second; one batch per Miblo every seconds, up to 1 KB; Miblo plays at 24 to 30 fps redrawing only the chart, with a delay of seconds; fx ignored on the live item; nothing in the flash |
| Flash writes | 1 per slot every 10 s, 100 per slot per day; a file Miblo already holds does not write (nor wait) |
| Requests | one writing animate every 2 s (bursts of 4); playing a kept scene counts as a card; the same file with the same options is not decoded again; one mode7 a second; push up to 60 a second, batched by the SDK every 500 ms or 32 samples |
Reading the SDK's messages
The SDK checks everything before sending and says what it did in plain words (warnings, each with code, en and pt; a refusal comes as an error with code and message). The messages read like this:
| The message says | What happened | What to do |
|---|---|---|
"20 frames and 180 colours: kept 16 frames and 64 colours" (reduced) | The art had too many frames or colours | Fewer frames or colours, or accept the reduction |
"animation files use a minimum of 20 ms per step: 60 fps adjusted to 50 fps" (fps_capped) | The path chosen does not reach the fps asked | Ask for an fps within the table, or a smaller region |
"too detailed for tiles (300 different 8x8 blocks, photo-like): whole frames at 10 fps at most" (whole_frames) | More than 256 different tiles | More flat areas and repetition, or a smaller region |
"300 different 8x8 blocks (over 256), but only 80x60 pixels move: rectangle frames, up to 10 fps" (rect_frames) | More than 256 tiles, but the change is small | Nothing, or flatter art to get back to tiles |
"a frame is 20 KB: check drawMs and the achieved rate on the device" (heavy) | A whole frame is over 12 KB | Shrink the region or what changes |
"12 similar 8x8 blocks were merged to fit 256" (tiles_merged) | A few tiles over 256 | Nothing, or check the preview |
"3 blocks needed more colours than a 16-colour sub-palette: the nearest were used" (colours_near) | A tile with more than 16 colours | Fewer colours per tile |
"speed 900 is out of range for rotate: 720 (-720 to 720)" (clamped) | An effect's number was brought to its limit | Ask for a value within the table |
"too detailed for rotate: 400 different 8x8 blocks, at most 256 (flat colours and repeating patterns fit)" (error too_detailed) | The effect's picture does not fit in tiles | Flatter, more repeated art, or a smaller region |
"Weather holds Miblo's screen right now: try again in 8 s" (error anim_taken) | Another program is pinned or peeking | Try again after retryAfter |
"every frame slot is held by other programs: clear one" (error no_free_slots) | The slots hold other playing apps' frames and scenes | Free one (their screen.clear(), or miblo screen clear), or turn an app off |
"Miblo's flash is full: this animation waits (the app shows its card meanwhile)" (error no_free_slots) | The Miblo answered 507 no_space for this app's files | Nothing: the scene is tried again in 1 h; apps turned off free space |
"Miblo's flash was written moments ago: wait 7 s (a slot takes one write every 10 s)" (error rate_limited, retryAfter) | The slot the new file would take was written less than 10 s ago | Wait retryAfter seconds (the Demo does) |
"Miblo's flash has taken today's writes for that slot (100 a day): try again in 3 h, or play a scene it already holds" (error rate_limited, daily: true) | The slot reached its 100 writes of the day | Play the scenes already there; do not send new art every round |
miblo screen preview
Before sending anything, see what Miblo will show:
miblo screen preview road.png --fx perspective --region 0,112,240,128 -o road-preview.pngWith a file, the preview converts on your computer, without the bridge, prints the plan and the warnings and writes a PNG of the step asked (--step N; with fx: --ms N). Without a file, it shows the animation on the App screen (screen.preview(), GET /sdk/v1/screen/preview). The drawing is the bridge's renderer with the firmware's rules: tiles and sprites match the firmware's sample pixel for pixel; the per-line effects mirror its integer math, not yet proven on the device; the card (the title band and the items) does not appear in the preview.
Debugging with drawMs
screen.status() (GET /sdk/v1/screen) carries your animation in anim: the path (kind: tiles, mode7, rect or frames), the planned fps, the slots, the current step, the fx and the effect's numbers, the frames, the tiles and the colours. In anim.real ({ fps, drawMs }) comes the device's own measure: the fps it achieves and the time of the last draw. While the animation plays on a Miblo 1.26, the bridge asks the device for it every 5 s; until the first answer both values are null. The Demo shows both numbers on the card's last line ("-" while there is no measure).
const st = await screen.status();
console.log(st.anim?.fps, 'fps planned;', st.anim?.real?.fps ?? '-', 'fps real,', st.anim?.real?.drawMs ?? '-', 'ms');On a Miblo 1.28.1 the anim also says whether the scene is loaded on the device (loaded), whether its memory is lent to a request (yielded, and yields: how many times it was) and, while it does not load, why (why: the app's screen off the panel, a request in progress, the wait after a loan, free memory or a whole block missing, the file). miblo screen status turns that into a sentence: "Animação: blocos de Clima · 8 fps pedidos · — fps real · — ms · não carregada (sem memória livre: 7,9 KB livres)". What each reason means and what to do is in Miblo's memory.
A live chart has its measure in live: one entry per id, with seq (the batch playing), buffered (how many batches Miblo holds, 0 to 2), playing, late (the next batch did not come in time) and the same real: { fps, drawMs }. miblo screen status prints "Ao vivo: cpu · 2 lotes · 24 fps real · 6 ms". A frequent late is the network (Miblo's Wi-Fi, a busy bridge); a low real fps with a high drawMs is a chart too large: shrink the item or take other effects off the card.
How to read them:
drawMsfits the budget (below 1000 / fps): all good.The real fps is lower than the planned one: the frame costs more than the interval asked, and the governor charges the cost. Shrink the
region, trade frames for tiles (flatter, more repeated art) or ask for a lower fps.loaded: falsefor more than a few seconds with the app's screen on Miblo: the scene does not fit beside the 8 KB floor. Fewer distinct tiles and fewer colours per tile (the resident tiles get smaller), or fewer things open on the Miblo.drawMsnear 25 to 40 ms: the whole screen is being redrawn every frame. Look for what changes needlessly: a gradient, noise, a photo.Occasional spikes: Wi-Fi or the pet taking their turn. That is expected; the governor handles it.