Skip to content

validate & emulate

You don’t need an M1X plugged in to know your plugin is going to work. mixlar-sdk validate lints your manifest and widgets, and mixlar-sdk emulate runs the whole thing against a headless mock device — pressing buttons, sliding sliders, and rendering widgets to a PNG — all from the terminal.

Lints plugin.json and every bundled widgets/<id>/widget.json.

Terminal window
mixlar-sdk validate [pkg]

pkg is optional and defaults to the current directory — every CLI command resolves it by looking for plugin.json, so you’ll get a clear error rather than a silent wrong-folder operation if you run it somewhere else.

Terminal window
$ mixlar-sdk validate
[warn] no 'author' — shown on the plugin card
[ok] package valid (Weather Widget v0.1.0)

Run it any time, but it’s especially worth running before link, dev, or pack — catching a malformed widget.json here is a lot faster than debugging it on-device.

Runs a package against a headless mock device: no app, no hardware, no serial connection.

Terminal window
mixlar-sdk emulate [pkg] --widget ID --render OUT.png --data k=v --interactive
flag meaning
--widget widget id to load (default: the package’s first declared widget)
--render OUT.png render the current widget state to a PNG (needs the [render] extra)
--data k=v seed a WDATA value before rendering/inspecting (repeatable)
--interactive drop into a REPL for pressing buttons, sliding sliders, and paging live

Under the hood, emulate loads your package’s plugin instance(s), attaches a MockDevice, binds the first MixlarPlugin subclass it finds, loads the requested widget (or the manifest’s first declared one), applies any --data seeds, and prints the data table and interactive-element list — then optionally renders and/or starts the REPL.

Terminal window
$ mixlar-sdk emulate --data pomodoro.pct=40 --data pomodoro.remaining=12:34 --render preview.png
[..] loaded widget 'pomodoro' from C:\...\widgets\pomodoro\widget.json
Data snapshot:
pomodoro.pct = 40
pomodoro.remaining = 12:34
Interactive elements:
reset
start_pause
[ok] rendered preview to preview.png

--render needs Pillow, installed via the [render] extra:

Terminal window
pip install "mixlar-sdk[render]"

The renderer is a best-effort approximation of what custom_widget.ino draws on the device — not pixel-perfect, but enough to eyeball layout, colors, and live data bindings while authoring. It supports label, bar, arc (with its centered fmt label), panel, led, line, img (loads the real asset relative to the spec’s folder), qr (drawn as a placeholder box, not an actual scannable code), and btn/toggle/slider as styled boxes. Layer visibility (show) and bind/fmt templating are both honored against whatever data you seed. Unknown element types are skipped silently, matching the firmware’s own forward-compatible behavior — see the element reference in Elements for the full set.

--interactive drops you into a small command loop for poking at the widget live:

Terminal window
mixlar> press start_pause
[ok] log: <- WEVENT,pomodoro,start_pause,press
mixlar> data pomodoro.status Working
[ok] log: -> WDATA,pomodoro.status,Working
mixlar> toggle power on
mixlar> slide brightness 75
mixlar> page Controls
mixlar> snapshot
mixlar> quit
command does
press <id> simulate a tap on a btn
hold <id> simulate a long-press
toggle <id> on|off flip a toggle element
slide <id> <n> move a slider to value n
page <name> switch the mock device’s current page
data <key> <val> push a WDATA update from the plugin side
snapshot print the current live data table
help list commands
quit exit the REPL

Every one of these mirrors a real device→PC or PC→device protocol line — see Protocol for the wire format each command produces.

For anything the CLI’s flags and REPL don’t cover — assertions in a test suite, scripted sequences, inspecting _bind_keys or _element_ids — use mixlar.emulator.MockDevice directly in Python:

from mixlar.emulator import MockDevice
device = MockDevice()
device.attach() # installs itself as PluginRegistry's sender
device.bind_plugin(my_plugin) # routes events to my_plugin.on_widget_event, etc.
device.load_widget("widgets/pomodoro/widget.json")
device.press("start_pause") # simulates a tap -> WEVENT,...,press
device.toggle("power", on=True) # -> WEVENT,...,on
device.slide("brightness", 80) # -> WEVENT,...,value=80
device.switch_page("Controls") # -> WEVENT,...,_page,Controls
device.data("pomodoro.remaining") # read back a value the plugin pushed
device.snapshot() # the whole live data table, as a dict
device.render_to("preview.png") # needs Pillow (the [render] extra)
device.detach()
  1. attach() installs the device as PluginRegistry’s data/image sender — the exact seam push_widget_data/push_widget_image call through. Once attached, any plugin instance’s pushes land in this device, whether or not you explicitly bound it. detach() restores both to None and is idempotent.

  2. bind_plugin(plugin) additionally routes simulated device→PC traffic (press/hold/toggle/slide) to plugin.on_widget_event, and fires on_widget_shown/on_widget_hidden around load_widget calls. If the plugin has no active widget id set yet, it adopts plugin.widget_id — mirroring how the app selects a plugin’s bundled widget via CUSTOMWIDGET,<id> on connect.

  3. load_widget(path_or_dict) accepts a path or an already-parsed dict, scans every element for bind keys (populating _bind_keys) and interactive ids on btn/toggle/slider elements (populating _element_ids — what press/toggle/slide will accept). For multi-page specs, current_page seeds to the first non-hidden page, matching the device’s own startup behavior.

  4. Every call is logged chronologically to device.log ("-> WDATA,..." / "<- WEVENT,...") — useful for asserting exact wire traffic in a test. All dispatch into plugin code is crash-isolated, so a broken hook never raises out of a press()/toggle()/load_widget() call — matching how the real firmware/app boundary behaves.

feed(line) — acting as the PC→device sender

Section titled “feed(line) — acting as the PC→device sender”

feed is what gets installed as the data sender; it parses one or more newline-joined protocol lines and applies them: WDATA updates the data table (rejecting a new key past the 32-key budget with a logged WDATA_ERR, exactly like the firmware), WPAGE switches the current page, CUSTOMWIDGET updates the tracked widget id. Feed it manually to simulate the PC side without a plugin at all — handy for testing a hand-written widget’s bind/show wiring in isolation:

device.feed("WDATA,pomodoro.pct,40")
device.feed("WDATA,pomodoro.status,Work - Running")
from mixlar.emulator.render import render_widget, save_preview
img = render_widget("widgets/pomodoro/widget.json", data={"pomodoro.pct": "40"})
save_preview(spec_dict, "preview.png", data=device.snapshot())

This is the same renderer the emulate --render flag calls — it needs Pillow, but everything else in the emulator works without it.

  1. mixlar-sdk create scaffolds the package.
  2. mixlar-sdk validate catches manifest and widget spec mistakes early.
  3. mixlar-sdk emulate --render preview.png gives you a fast visual feedback loop with no hardware.
  4. mixlar-sdk link / mixlar-sdk dev get it into the real app for end-to-end testing.
  5. mixlar-sdk pack / mixlar-sdk sign ship it — see pack, sign & publish.

For the full widget spec these commands lint and render against, see Widget Spec and Elements. For everything else the CLI can do, see the CLI intro.