Skip to content

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.

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.

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.
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.

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,Controls
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.

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,62

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" }

WIDGET_UPLOAD,<id>/<file>,<size> reuses the app’s existing FileStreamWorker handshake:

  1. PC sends the header line: WIDGET_UPLOAD,<id>/<file>,<size>.
  2. Device replies _READY once it’s prepared to receive.
  3. PC streams exactly <size> raw bytes.
  4. Device replies _OK on 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>.

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.

  • Data & events — the plugin-side API for pushing data and handling events, including on_widget_shown / on_widget_hidden lifecycle callbacks.
  • Widget spec — the widget.json format these binds and events target.
  • CLI: validate & emulate — exercise this protocol locally without a physical device.