Serial protocol
Widgets are rendered entirely on the M1X device, but they’re driven live from the PC over the same serial connection every other Mixlar feature uses. This page documents the wire format: the exact lines that cross the link, the size limits enforced on both ends, and the reconcile pattern you’ll use for anything interactive.
You’ll rarely build these lines by hand — MixlarPlugin.push_widget_data and the SDK’s
mixlar.protocol helpers do it for you — but knowing the wire format makes debugging with the
CLI emulator, or hand-testing from the Dev toolkit’s command box, much easier.
Line format
Section titled “Line format”Every message is a single newline-terminated line, comma-separated, verb first:
VERB,arg1,arg2,...WDATA and a few others only split on the first comma or two, so values themselves may
safely contain commas.
PC → device
Section titled “PC → device”| line | meaning |
|---|---|
CUSTOMWIDGET,<id> |
Load /fat/widgets/<id>/widget.json and switch the panel to it. |
CUSTOMWIDGET,<name>.json |
Legacy flat-file form, used for hand-testing only. |
WDATA,<key>,<value> |
Set a live data point. Every element bound to <key> (via bind, cbind, obind, or show) refreshes immediately. |
WPAGE,<name> |
Force the active widget to jump to one of its pages. |
WIDGET_UPLOAD,<id>/<file>,<size> + raw bytes |
Install one file over serial (uses the existing FileStreamWorker handshake — see below). |
WLIST |
Request the device’s installed-widget inventory. |
Device → PC
Section titled “Device → PC”| line | meaning |
|---|---|
CUSTOMWIDGET_OK,<id>,<n> |
Widget loaded successfully; <n> is the number of elements built. |
CUSTOMWIDGET_ERR |
Load failed — the panel shows the reason. |
WDATA_ERR |
The key was rejected (bad key, or the 32-slot table is full). |
WEVENT,<widget>,<element>,<action> |
An interactive element fired. action is press, hold, on, off, value=<n> (sliders), or a page name when element is _page. |
WMACRO,<name> |
A button or toggle with a macro field was triggered — the PC should run the named saved macro. |
WLIST,<id>,<version> |
One row of the installed-widget inventory, in reply to WLIST. |
Parsing WEVENT
Section titled “Parsing WEVENT”The action field carries extra payload for slider drags: value=<n> is split out into a
separate value field by the SDK’s parser rather than left glued to the action string.
from mixlar.protocol import parse_event
evt = parse_event("WEVENT,sam_dash,vol,value=42")# Event(widget="sam_dash", element="vol", action="value", value=42)
evt = parse_event("WEVENT,sam_dash,power,on")# Event(widget="sam_dash", element="power", action="on", value=None)Page-switch announcements use the reserved element id _page, with the destination page name
as the action:
WEVENT,sam_dash,_page,ControlsLimits (enforced both ends)
Section titled “Limits (enforced both ends)”| limit | value |
|---|---|
| Canvas | 240 x 140 px |
| Elements per widget | 48 (shared across all pages) |
| Pages per widget | 6 |
Live WDATA key slots |
32 |
| Key length | ≤23 chars |
| Value length | ≤47 chars |
widget.json size |
16 KB |
| Widget id length | 2-24 chars |
Because the first comma splits key from value in a WDATA line, values themselves may contain
commas — only the key is restricted. Values are stored as text on the device and parsed with
atoi for numeric elements (bar, arc), and they’re RAM-only: a reboot clears the whole
table, so the PC re-pushes everything within seconds of reconnecting.
Namespacing with widget_id
Section titled “Namespacing with widget_id”Set widget_id on your plugin class to pair it with a bundled widget folder. Every
push_widget_data call is then sent as <widget_id>.<key> instead of the bare key, and the
firmware namespaces that widget’s binds identically — so several installed third-party plugins
can never collide with, or poison, each other’s data.
Keys starting with _ are exempt and stay global — this is how the reserved clock keys work.
def wdata(key: str, value, widget_id: str = "") -> str: k = str(key).strip().replace(",", "_") if widget_id and not k.startswith("_"): k = f"{widget_id}.{k}" k = k[:47] v = str(value).replace("\n", " ").replace(",", " ")[:47] return f"WDATA,{k},{v}"When hand-testing from the Dev toolkit’s command box, use the full namespaced key:
WDATA,my_widget.cpu,62Reserved keys — clock and date
Section titled “Reserved keys — clock and date”While a custom widget is active, the PC auto-pushes four keys every 15 seconds, with no element
wiring required beyond a bind:
| key | value |
|---|---|
_time |
12-hour time |
_time24 |
24-hour time |
_ampm |
AM / PM |
_date |
current date |
A watchface is just one label:
{ "t": "label", "s": 38, "bind": "_time" }Uploading widget files
Section titled “Uploading widget files”WIDGET_UPLOAD,<id>/<file>,<size> reuses the app’s existing FileStreamWorker handshake:
- PC sends the header line:
WIDGET_UPLOAD,<id>/<file>,<size>. - Device replies
_READYonce it’s prepared to receive. - PC streams exactly
<size>raw bytes. - Device replies
_OKon success, or_ERR,<reason>on failure (bad path, size mismatch, disk full, etc).
One file per upload call; a full widget folder is several calls in a row. Folder targets
(<id>/<file>) may overwrite an existing file, so iterating on a spec during development is
just re-upload + re-select with CUSTOMWIDGET,<id>.
The reconcile pattern
Section titled “The reconcile pattern”Sliders and toggles are drawn optimistically on the device — the UI reacts to a drag or tap before the PC confirms anything — which means the device and the PC can briefly disagree about state. Both interactive elements share the same fix: bind them, and have your plugin echo the authoritative value straight back after handling the event.
def on_widget_event(self, widget_id, element_id, action): if widget_id != "sam_dash": return if element_id == "power": on = action == "on" apply_power_state(on) # Echo back so the device's optimistic flip matches PC-side truth self.push_widget_data("power", "1" if on else "0")For toggle, the bound key always wins over the local flip on the next WDATA push — so if
apply_power_state fails, echoing back the old value snaps the switch back visually. For
slider, the bind value is authoritative between drags: a user mid-drag isn’t interrupted,
but once they release, the next WDATA on that key is what the knob settles to. See
Sliders for the full interactive-element writeup, including the
~150 ms throttling on drag events.
Where to go next
Section titled “Where to go next”- Data & events — the plugin-side API for pushing data
and handling events, including
on_widget_shown/on_widget_hiddenlifecycle callbacks. - Widget spec — the
widget.jsonformat these binds and events target. - CLI: validate & emulate — exercise this protocol locally without a physical device.