Testing without hardware
Everything in mixlar.emulator exists so you can develop and test a plugin without an M1X plugged in. It’s a headless stand-in for the device side of the serial link — it plays both directions of the wire protocol, so you can drive a plugin with fake button presses and assert on what it pushes back, all inside a normal test runner.
MockDevice
Section titled “MockDevice”MockDevice is a stdlib-only class that loads a widget spec, tracks the live data table, and can simulate both directions of traffic:
feed()plays the PC → device role, so it can be wired up as a plugin’s data sender.press/hold/toggle/slide/switch_pageplay the device → PC role, emitting the sameWEVENT/WPAGElines the firmware would and routing them to a bound plugin exactly likecustom_widget.inodoes.
from mixlar.emulator import MockDevice
device = MockDevice()device.attach() # installs itself as PluginRegistry's senderdevice.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,...,pressdevice.toggle("power", on=True) # -> WEVENT,...,ondevice.slide("brightness", 80) # -> WEVENT,...,value=80device.switch_page("Controls") # -> WEVENT,...,_page,Controls
device.data("pomodoro.remaining") # read back a value the plugin pusheddevice.snapshot() # the whole live data table, as a dictdevice.render_to("preview.png") # needs Pillow (the [render] extra)
device.detach()Wiring
Section titled “Wiring”attach() sets the process-wide PluginRegistry’s _wdata_sender/_wimg_sender to the device’s own feed/_receive_image methods. This is the exact seam push_widget_data/push_widget_image call through, so 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: it only clears them if they still point at this device.
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. It also adopts plugin.widget_id as the device’s active widget id if none is set yet, mirroring how the app selects a plugin’s bundled widget via CUSTOMWIDGET,<id> on connect.
Loading a spec
Section titled “Loading a spec”load_widget() accepts a path or an already-parsed dict. It scans every element for bind keys (so the device knows what the widget actually subscribes to) and interactive ids on btn/toggle/slider elements (what press/toggle/slide accept). For multi-page specs it seeds the current page to the first non-hidden page, matching the device’s own startup behavior. See Widget spec for the full element/page format.
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 registry’s data sender. It parses one or more newline-joined protocol lines and applies them:
| Verb | Effect |
|---|---|
WDATA,<key>,<value> |
Updates the data table. Rejects a new key past the 32-key budget with a logged WDATA_ERR, exactly like the firmware. |
WPAGE,<name> |
Switches the current page. |
CUSTOMWIDGET,<id> |
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")Writing a test
Section titled “Writing a test”Because device.data() and device.snapshot() read back whatever the plugin pushed, a plain pytest function is enough to cover a plugin’s event handling end to end:
from mixlar.emulator import MockDevicefrom my_plugin import PomodoroPlugin
def test_start_pause_toggles_running_state(): plugin = PomodoroPlugin() device = MockDevice() device.attach() device.bind_plugin(plugin) device.load_widget("widgets/pomodoro/widget.json")
device.press("start_pause")
assert device.data("pomodoro.status") == "Work - Running"
device.detach()Rendering — mixlar.emulator.render
Section titled “Rendering — mixlar.emulator.render”render_widget/save_preview draw a widget spec’s current state to a PNG so you can eyeball layout, colors, and live bindings while authoring, without hardware:
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 needs Pillow (pip install "mixlar-sdk[render]") — everything else in the emulator works without it. It’s a best-effort approximation of what custom_widget.ino draws, not pixel-perfect.
Supported element types:
| Type | Rendered as |
|---|---|
label |
Text, with fmt/bind templating and alignment |
bar |
A filled rounded-rect, horizontal or vertical depending on aspect |
arc |
An arc gauge, with its centered fmt label |
panel |
A flat rounded-rect background |
led |
A filled circle, on/off color from bind |
line |
A flat rectangle |
img |
The real asset, loaded relative to the spec’s folder |
qr |
A placeholder box, not an actual scannable code |
btn / toggle / slider |
Styled boxes approximating the on-device control |
show (layer visibility) and bind/fmt templating are both honored using whatever data dict you pass. Unknown element types are skipped silently — the same forward-compatible behavior as the firmware.
mixlar-sdk emulate
Section titled “mixlar-sdk emulate”The CLI wraps all of the above so you can poke at a plugin without writing a script:
mixlar-sdk emulate # load, print state, exitmixlar-sdk emulate --widget other_widget # pick a non-default declared widgetmixlar-sdk emulate --render preview.png # render a PNGmixlar-sdk emulate --data pct=40 --data status="Work" --render preview.pngmixlar-sdk emulate --interactive # drop into a REPLIt loads the package’s plugin instance(s), attaches a MockDevice, binds the first MixlarPlugin subclass found, loads the widget named by --widget (or the manifest’s first declared widget), applies any --data k=v seeds, prints the data table and interactive-element list, then optionally renders and/or starts the REPL. See mixlar-sdk CLI for the rest of the command surface, and Validate & emulate for how this fits alongside spec validation.
Interactive REPL
Section titled “Interactive REPL”mixlar> press start_pause[ok] log: <- WEVENT,pomodoro,start_pause,pressmixlar> data pomodoro.status Working[ok] log: -> WDATA,pomodoro.status,Workingmixlar> toggle power onmixlar> slide brightness 75mixlar> page Controlsmixlar> snapshotmixlar> quit-
press <id>/hold <id>— simulate a tap or long-press on abtn. -
toggle <id> on|off— simulate flipping atoggle. -
slide <id> <n>— simulate dragging asliderto valuen. -
page <name>— switch the active page. -
data <key> <val>— feed aWDATAline directly, as if the plugin pushed it. -
snapshot— print the current data table.helplists all commands;quitexits.
Next steps
Section titled “Next steps”- Plugin API — the
on_widget_event/on_widget_shown/on_widget_hiddenhooks the emulator drives. - Widget spec — the element and page format
load_widget()parses. - Convert a plugin — a worked example that leans on the emulator while porting an existing integration.