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.
Pushing data with push_widget_data
Section titled “Pushing data with push_widget_data”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.
Pairing and key namespacing
Section titled “Pairing and key namespacing”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.
Bundling the widget inside the plugin
Section titled “Bundling the widget inside the plugin”The widget ships inside the plugin folder, next to the plugin file:
my_plugin/ my_plugin.py widgets/ my_widget/ widget.json logo.pngself.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.
Streaming images
Section titled “Streaming images”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 |
Receiving events — on_widget_event
Section titled “Receiving events — on_widget_event”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 stateaction 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 |
The reconcile pattern
Section titled “The reconcile pattern”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")Shown / hidden lifecycle
Section titled “Shown / hidden lifecycle”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."""- User swipes/selects to your widget’s panel on the device.
- The device switches the custom-widget panel and the app calls
on_widget_shown(widget_id)on your plugin. - 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.
- Start any polling timer your plugin needs while visible.
- 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.”
Worked examples
Section titled “Worked examples”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().
Where to go next
Section titled “Where to go next”- Elements — every field on every element type, including
bind,cbind,obind, andshow. - Protocol — the raw
WDATA/WEVENT/WPAGE/WMACROwire format, if you’re building tooling outside a plugin. - Builder — the Python
Widgetclass for generatingwidget.jsoninstead of hand-writing it. - Plugin API — the full plugin hook list, including
on_widget_eventin context with everything else a plugin can override.