Skip to content

Data & events

A widget spec (see Elements) is static JSON — it does nothing on its own until a plugin starts feeding it data and reacting to what the user does on the panel. This page covers that half of the picture: pushing values, pairing a plugin with a bundled widget, handling press/hold/on/off/value events, and the shown/hidden lifecycle that lets many installed plugins stay quiet unless their widget is actually on screen.

If you haven’t read the wire protocol yet, skim Protocol first — this page assumes you know what WDATA and WEVENT are.

Any plugin can feed a widget with one call. It doesn’t need to know — or care — whether a widget is even installed:

class MyPlugin(MixlarPlugin):
plugin_id = "my_plugin"
def some_update(self):
self.push_widget_data("room_temp", 23) # → WDATA,room_temp,23
self.push_widget_data("printer_pct", progress) # bar/arc binds
self.push_widget_data("status", "Printing…") # label binds
  • Thread-safe. Call it from a worker thread, a Qt timer, wherever your update comes from.
  • Returns False (a no-op) when no device is connected — safe to call unconditionally.
  • Keys are capped at 47 characters, and commas/newlines are stripped, matching the firmware’s on-device limits exactly (mixlar.protocol.VALUE_MAX_LEN).
  • Every element in the active widget with a matching "bind" field refreshes immediately.

Set widget_id on your plugin class to pair it with a bundled widget folder:

class MyPlugin(MixlarPlugin):
plugin_id = "my_plugin"
widget_id = "my_widget"

Once paired, every push_widget_data key is sent as <widget_id>.<key> — so self.push_widget_data("cpu", 42) actually goes over the wire as WDATA,my_widget.cpu,42. The firmware namespaces that widget’s bind values identically, so among ten installed third-party plugins none of them can feed — or poison — anyone else’s widget.

key form behavior
"cpu_load" (no prefix) namespaced automatically to <widget_id>.cpu_load
"_time", "_date", any _-prefixed key stays global — not namespaced, shared across all widgets
push_widget_data(key, value, widget="other_id") overrides namespacing to feed a different widget your plugin also owns

Dev-box testing always uses the full key, since the box isn’t a plugin: WDATA,my_widget.cpu,62.

The widget ships inside the plugin folder, next to the plugin file:

my_plugin/
my_plugin.py
widgets/
my_widget/
widget.json
logo.png

self.widget_id = "my_widget" combined with a widgets/my_widget/widget.json next to your plugin is all that’s needed — widget_bundle_dir() resolves that path for you. The app auto-installs the bundle onto the device a few seconds after connect, and re-uploads it whenever the bundle changes, so installing the plugin installs the widget: one download, zero manual steps for the user. Plugins with a widget_id show a Widget capability pill on their card in the plugin browser, alongside Macros/Slider.

A plugin can also own more than one bundled widget — widget_bundle_dirs() returns every (widget_id, dir) pair declared in the plugin’s manifest, each installed the same way.

For img elements, use push_widget_image instead of pushing a filename:

self.push_widget_image("cover_art", source, w, h)
param meaning
key the bind key an img element is watching
source image source (path, bytes, or PIL-compatible object depending on context)
w, h target size — the image is resized and RGB565-packed for the panel
widget optional, same override as push_widget_data

Interactive elements (btn, slider, toggle, and page switches) fire WEVENT lines back from the device. Override on_widget_event to receive them — it’s broadcast to every installed plugin, crash-isolated, and runs on the GUI thread, so filter by the widget ids you actually own:

def on_widget_event(self, widget_id, element_id, action):
if widget_id != "sam_dash":
return
if element_id == "ping" and action == "press":
self.push_widget_data("ping_status", "pong!")
elif element_id == "power": # toggle
on = action == "on"
self.push_widget_data("power", "1" if on else "0") # confirm state

action takes one of these forms, matching the device-side WEVENT actions:

action fired by notes
press btn tap and release
hold btn with "hold": 1 600 ms long-press, instead of press
on / off toggle fires immediately after the optimistic flip
value slider throttled (~150 ms) while dragging, plus one final call on release
_page any page switch element_id is _page; action carries the new page’s name

toggle and slider flip or move optimistically on the device before your plugin ever hears about it — the user sees an instant response even with nothing connected on the other end. If the element also has a bind, your plugin is expected to echo the authoritative state back with push_widget_data once it’s processed the event, so the device’s local guess and your plugin’s real state converge:

elif element_id == "power":
on = action == "on"
apply_real_power_state(on)
self.push_widget_data("power", "1" if on else "0")

A paired plugin also gets visibility callbacks, so ten installed plugins can all stay silent except the one whose widget is actually on the panel:

def on_widget_shown(self, widget_id):
"""Your paired widget came on screen — push full state, start timers."""
def on_widget_hidden(self, widget_id):
"""Your paired widget left the screen — go quiet, stop timers."""
  1. User swipes/selects to your widget’s panel on the device.
  2. The device switches the custom-widget panel and the app calls on_widget_shown(widget_id) on your plugin.
  3. Push a full snapshot of current state here (not just deltas) — the device has no memory of what it showed last time, and stored values are RAM-only on the device and reset on reboot.
  4. Start any polling timer your plugin needs while visible.
  5. User swipes away. The app calls on_widget_hidden(widget_id) — stop the timer and go quiet until shown again.

Only one custom widget is visible at a time (it owns the shared widget panel), which is exactly why this callback pair exists: it’s the cue for “should I even bother pushing right now.”

The SDK ships full source for these under PC Software/Plugin Examples/:

example shows
Mouse Battery the canonical full bundle: widget_id pairing, bundled auto-installing widget, shown/hidden poll cadence, cached resync-on-shown, cbind color coding, show + pulse low-battery alert
Custom Widget Demo / demo_stats passive dashboard (CPU/RAM via psutil)
Custom Widget Demo / sam_dash two pages, buttons + toggle + event handling
Custom Widget Demo / test_lab every interactive feature: tap/hold, toggles, layers, slider round-trip, speedo arc
Custom Widget Demo / menu_demo themed built-in menu vs a hand-built btn menu page
Custom Widget Demo / qr_hub QR, sym icon strip, clock keys, hidden pages

There’s also a complete widget + plugin pairing in the walkthrough guide — see Convert a plugin and the Pomodoro guide for an arc countdown, status label, and Start/Pause/Reset buttons driven end-to-end from push_state().

  • Elements — every field on every element type, including bind, cbind, obind, and show.
  • Protocol — the raw WDATA/WEVENT/WPAGE/WMACRO wire format, if you’re building tooling outside a plugin.
  • Builder — the Python Widget class for generating widget.json instead of hand-writing it.
  • Plugin API — the full plugin hook list, including on_widget_event in context with everything else a plugin can override.