# RETRO-INK OS API An OS is one Lua script. The driver runs it, calls its event functions, and gives it the tables below. This file is written to be handed to the model that generates an OS. ## The device - E-paper display, 600 × 400, six colours: `black`, `white`, `red`, `yellow`, `blue`, `green`. No greys, no blending. - A refresh takes about 19 s and redraws the whole panel. Refreshes start at least 30 s apart. Design screens that change rarely; nothing can animate. - 8 toggle switches (1–4 left of the display, 5–8 right), 2 rotary knobs with push (1 left, 2 right), one large key, one small button, one LED. - The key belongs to the driver when held: holding it records a spoken request for a new OS. Only a short tap reaches the OS. ## Rules - Lua 5.4 subset: `string`, `table`, `math` and the basic functions. No `io`, `os`, `require`, `debug`, coroutines. - Everything is event driven. Every function must return within about 300 ms; there is no `sleep` or blocking call. Use `timer` and callbacks. - 1 MB of memory. - An error in a callback is logged and the OS keeps running. An error while loading or in `on_start` rejects the OS and the previous one stays. - Globals do not survive a restart or deep sleep. Use `store` for what must. ## Events (define the ones you need, as globals) | Function | Called when | | --- | --- | | `on_start()` | the OS starts, also after reset and after deep sleep | | `on_toggle(i, on)` | toggle `i` (1–8) moved; `on` is a boolean | | `on_knob(i, delta)` | knob `i` (1–2) turned; `delta` is +1 per click clockwise, −1 counter-clockwise | | `on_knob_press(i)` | knob `i` pressed | | `on_key()` | the large key was tapped | | `on_button()` | the small button was pressed | | `on_display_done()` | a refresh finished | ## display Drawing goes into a frame buffer; nothing shows until `display.update()`. Coordinates are pixels, origin top left. Colour arguments are the names above and may be omitted (black; white for `clear`). | Call | Effect | | --- | --- | | `display.clear(color)` | fill the frame buffer | | `display.rect(x, y, w, h, color, fill)` | rectangle; filled unless `fill` is `false` | | `display.line(x1, y1, x2, y2, color, width)` | line, `width` in pixels (default 1) | | `display.circle(x, y, r, color, fill)` | circle around (x, y); filled unless `fill` is `false` | | `display.text(x, y, str, opts)` | text with its top at `y`; returns the width in pixels | | `display.text_width(str, size)` | width without drawing | | `display.image(x, y, rows, scale)` | pixel art, see below | | `display.update()` | show the frame buffer; returns at once | | `display.busy()` | `true` while a refresh is running or waiting | | `display.width`, `display.height` | 600, 400 | Text options: `{ size = "s" | "m" | "l" | "xl", color = "black", align = "left" | "center" | "right" }`. `x` is the left edge, centre or right edge according to `align`. `\n` starts a new line. The text does not wrap by itself. | Size | Line height | Roughly fits across the display | | --- | --- | --- | | `s` | 22 px | 75 characters | | `m` (default) | 32 px | 50 characters | | `l` | 47 px | 33 characters | | `xl` | 84 px | 18 characters | The font covers ASCII, Latin-1 (ä, é, ñ, °, …) and `– — ‘ ’ “ ” • … € ← ↑ → ↓ ✓`. Anything else, emoji included, draws as `?`. `display.image` takes a table of strings, one per row, one character per pixel: `k` black, `w` white, `r` red, `y` yellow, `b` blue, `g` green; `.` or a space leaves the pixel alone. `scale` (1–16) enlarges it. ```lua display.image(20, 20, { "..rr..", ".rrrr.", "rrrrrr", ".rrrr.", "..rr.." }, 8) ``` Calling `update()` during a refresh is fine: one more refresh follows with whatever the frame buffer holds by then. ## Inputs - `toggle.get(i)` → boolean - `knob.get(i)` → clicks turned since the OS started (integer, can be negative) ## timer - `timer.after(ms, fn)` → id; calls `fn` once - `timer.every(ms, fn)` → id; calls `fn` repeatedly (`ms` ≥ 10) - `timer.cancel(id)` At most 64 timers at a time. ## http - `http.get(url, function(status, body) … end)` - `http.post(url, body, function(status, body) … end, content_type)`: a table `body` is sent as JSON `status` is the HTTP status, or 0 when the request failed (then `body` is the reason). Bodies are cut at 256 KB. ## json - `json.encode(value)` → string - `json.decode(str)` → value, or `nil, message` if `str` is not JSON. JSON `null` becomes `nil`. ## ha (Home Assistant) - `ha.state(entity_id, function(state, attributes) … end)`: `state` is a string, or `nil` if the entity does not exist - `ha.call(service, data)`: for example `ha.call("light.turn_on", { entity_id = "light.kitchen", brightness = 120 })` ## store Small values that survive restarts, deep sleep and a change of OS. - `store.set(key, value)`: `value` is anything `json.encode` accepts; `nil` deletes - `store.get(key)` → value or `nil` ## Device, time, log - `led.set(on)` - `device.battery()` → percent, `device.usb()` → boolean - `device.sleep(seconds)`: deep sleep once the display is idle. The OS stops; after the time, or a tap on the key, it starts again from `on_start()`. Timers are lost. Toggles and knobs do not wake it. - `time.now()` → Unix time in seconds - `time.date(t)` → `{ year, month, day, hour, min, sec, wday }` in local time, `wday` 1 = Sunday; `t` defaults to now - `log(...)` (also `print`) ## Example ```lua local count = store.get("count") or 0 local function draw() display.clear() display.text(300, 150, tostring(count), { size = "xl", align = "center" }) display.text(300, 250, "turn knob 1", { align = "center", color = "blue" }) display.update() end function on_start() draw() end function on_knob(i, delta) if i ~= 1 then return end count = count + delta store.set("count", count) draw() end ```