# Mixlar Plugin & Widget SDK A reference for building plugins and device widgets for **Mixlar Control** — the Windows desktop app that drives the Mixlar One (a USB mixer: ESP32-S3, 480×320 touchscreen, 4 faders, 6 macro buttons, rotary encoder). Hand this whole file to an AI (Claude, ChatGPT, …) and it will understand how to write a plugin. --- ## 1. Package layout A plugin is a **folder** dropped into `%APPDATA%\Mixlar\config\plugins\`: ``` My Plugin/ plugin.json manifest (v1) — read before any code runs my_plugin.py one MixlarPlugin subclass (the "entry") widgets// optional device-widget bundles (widget.json + assets) icons/ optional PNGs for the app UI + widget image assets ``` ## 2. plugin.json (manifest v1) ```json { "manifest": 1, "id": "my_plugin", "name": "My Plugin", "version": "1.0.0", "author": "You", "description": "What it does.", "entry": "my_plugin.py", "icon": "fa5s.cube", "icon_color": "#FF4D14", "widgets": [ { "id": "my_widget", "name": "My Widget", "version": "1.0.0" } ] } ``` - `entry` is the `.py` filename (no path). `id` should match the class's `plugin_id`. - `icon` is a [qtawesome](https://github.com/spamap/qtawesome) name (`fa5s.*`). - `widgets[]` lists device widgets shipped in `widgets//`; each `id` must match a folder and (ideally) the `version` inside that `widget.json`. ## 3. The MixlarPlugin base class Subclass `MixlarPlugin` — **the loader injects it; do NOT import it.** Class attributes double as manifest fallbacks (the manifest wins): ```python class MyPlugin(MixlarPlugin): # noqa: F821 (injected by the loader) plugin_id = "my_plugin" plugin_name = "My Plugin" plugin_version = "1.0.0" plugin_author = "You" plugin_description = "What it does." plugin_icon = "fa5s.cube" plugin_icon_color = "#FF4D14" widget_id = "my_widget" # if it ships exactly one widget ``` ### Lifecycle - `on_load(self, settings)` — once at startup. **Call `super().on_load(settings)`.** Start background threads here (`threading.Thread(daemon=True)` + a `threading.Event` to stop them). - `on_unload(self)` — app shutdown / live reload. **Stop your threads here** — the app warm-reloads plugins, so a leaked thread survives a reload. ### Macro actions (device buttons) - `get_macro_actions(self)` → `[(action_id, "Label"), ...]` - `get_macro_action_icons(self)` → `{action_id: "fa5s.icon"}` - `get_macro_action_groups(self)` → `[("Group", [(id, "Label"), ...]), ...]` (tabbed grid) - `execute_macro_step(self, step)` — runs the action. `step` is a dict; `step.get("_action")` tells you which action fired. Runs on a background thread. - `get_macro_step_defaults(self)` → `{key: ""}` — default fields for a new step. - `build_macro_editor(self, step, row, editor_lay, upd, upd_refresh, lbl)` — add PyQt widgets (QLineEdit/QCheckBox/QComboBox) so the user configures per-step fields; call `upd(row, key, value)` on change. - `describe_macro_step(self, step)` → short human summary for the step list. ### Slider modes (faders) - `get_slider_mode(self)` → `(mode_id, "Label")` - `handle_slider_value(self, idx, value, pct)` — `value` 0–100, `pct` 0.0–1.0. - `get_slider_color(self, idx)` → `"#hex"`. ### Device widgets - `push_widget_data(self, key, value, widget=None)` — live-update any widget element bound to `key`. Thread-safe; no-op when no device is connected. - `push_widget_image(self, key, source, w, h, widget=None)` — push a runtime image (album art, camera) to an `img` element bound to `key`. - `on_widget_shown(self, widget_id)` / `on_widget_hidden(self, widget_id)` — your paired widget appeared/left the screen. Push full state on shown; go quiet on hidden. - `on_widget_event(self, widget_id, element_id, action)` — a button/toggle/ slider on the device widget fired (`action` e.g. `"press"`). Broadcast to all plugins; filter by the ids you own. ### Settings - `self._pget(key, default="")` / `self._pset(key, value)` — persist plugin config. - `build_settings_ui(self, layout, settings)` — the panel behind the plugin card. - `has_settings_ui(self)` → `True` if you override the above. ## 4. Available Python libraries (COMPLETE LIST) The app is Windows-only, Python 3.13. You may import from the **full standard library** and the packages below — **and nothing else.** Do not import any package not on this list; it will not be installed on the user's machine. **Standard library** (always available): `threading`, `time`, `datetime`, `json`, `os`, `sys`, `subprocess`, `socket`, `urllib`, `http`, `ctypes`, `winreg`, `math`, `random`, `re`, `pathlib`, `tempfile`, `shutil`, `base64`, `hashlib`, `hmac`, `uuid`, `collections`, `functools`, `itertools`, `webbrowser`, `queue`, `struct`, `wave`, `csv`, `sqlite3`, `logging`, etc. **Bundled third-party packages** — `import` name → package → what it's for: | import | package | use | |---|---|---| | `requests` | requests | HTTP GET/POST to any web API | | `websocket` | websocket-client | WebSocket clients (OBS, G HUB, etc.) | | `pynput` | pynput | send keystrokes / mouse; global hotkeys | | `PyQt5` | PyQt5 | Qt widgets for settings/editor UIs (QLineEdit, QCheckBox, QComboBox, QLabel…) | | `qtawesome` | qtawesome | Font Awesome icons in Qt (`fa5s.*`) | | `comtypes` | comtypes | Windows COM (audio sessions, SMTC internals) | | `pycaw` | pycaw | Windows Core Audio — per-app volume, mute, devices | | `mido` | mido | MIDI messages (note/CC) | | `rtmidi` | python-rtmidi | mido's backend — actually opens MIDI ports | | `serial` | pyserial | serial ports (talk to other hardware) | | `psutil` | psutil | CPU / RAM / disk / net / process stats | | `GPUtil` | GPUtil | NVIDIA GPU load / temp / memory | | `cpuinfo` | py-cpuinfo | CPU model / frequency details | | `keyring` | keyring | store secrets in Windows Credential Manager | | `PIL` | Pillow | image load / resize / convert (for `push_widget_image`) | | `numpy` | numpy | arrays / math (audio, image, data) | | `winrt` | winrt-runtime + Windows.Media.Control + Windows.Storage.Streams | Windows SMTC — now-playing title/artist/art from any media app | | `yfinance` | yfinance | stock / index quotes | | `curl_cffi` | curl_cffi | browser-impersonating HTTP (yfinance backend; anti-bot sites) | | `syncedlyrics` | syncedlyrics | fetch synced song lyrics | | `qrcode` | qrcode | generate QR codes (device pairing, links) | | `packaging` | packaging | version parsing / comparison | Prefer lazy imports inside methods (e.g. `import requests` at the top of the method that uses it) so a missing optional dep never blocks the whole plugin from loading. ## 5. Device widgets (widget.json) A declarative 240×140 panel rendered on the device. `widgets//widget.json`. **IMPORTANT — elements use SHORT keys** (this is the real device/simulator schema; long keys like `type`/`font`/`color` will NOT render): - `t` — element type (NOT `type`): `label, bar, arc, led, panel, line, img, qr, btn, toggle, slider` - `x`, `y` — position; `w`, `h` — size; `d` — diameter (arc/led/qr) - `s` — font size (NOT `font`); `c` — colour hex without `#` (NOT `color`); `bgc` — background colour - `text` — static/fallback text; `sym` — an icon glyph name - `bind` — a data key; the PC pushes its value with `push_widget_data("key", value)` - `fmt` — format template for a bound value, `{v}` = the value (e.g. `"{v}%"`, `"{v} °C"`) - `min`/`max` — range for bar/arc/slider; `id` — element id for btn/toggle/slider events - `cbind` — bind a colour; `obind` — bind opacity; `offc` — off colour (led/toggle) ```json { "version": "1.0.0", "elements": [ { "t": "label", "x": 8, "y": 6, "s": 14, "c": "8C8478", "text": "CPU" }, { "t": "label", "x": 8, "y": 24, "s": 34, "c": "FFFFFF", "bind": "cpu", "fmt": "{v}%", "text": "--" }, { "t": "bar", "x": 8, "y": 72, "w": 224, "h": 12, "c": "FF4D14", "bgc": "2C2C2E", "bind": "cpu", "min": 0, "max": 100 }, { "t": "arc", "x": 170,"y": 10, "d": 56, "w": 8, "c": "1DB954", "bgc": "2C2C2E", "bind": "temp", "min": 0, "max": 100, "fmt": "{v}°" }, { "t": "led", "x": 222,"y": 6, "d": 12, "c": "1DB954", "offc": "FF3B30", "bind": "alert" }, { "t": "btn", "x": 8, "y": 108,"w": 90, "h": 26, "r": 6, "c": "2C2C2E", "tc": "FFFFFF", "text": "Go", "id": "btn_go" } ] } ``` A label shows `text` normally; if it has a `bind`, it shows the pushed value (run through `fmt` if given), falling back to `text` when there's no value yet. So "Temperature: 21 °C" = `{ "t":"label", "bind":"temp", "fmt":"{v} °C", "text":"--" }` plus `push_widget_data("temp", 21)`. Do NOT put `{value:key}` inside `text` — that isn't a thing; use `bind` + `fmt`. Interactive elements (`btn/toggle/slider`) send events back via `on_widget_event`. Colours are hex without `#`. Design widgets visually in the **Widget view** of the Plugin Builder. Widget buttons can also run an existing macro with **zero code** (WMACRO) — the `on_widget_event` hook is only for custom PC-side logic. ## 6. Minimal example ```python class LightToggle(MixlarPlugin): # noqa: F821 plugin_id = "light_toggle" plugin_name = "Light Toggle" plugin_version = "1.0.0" plugin_icon = "fa5s.lightbulb" def get_macro_actions(self): return [("toggle", "Toggle Light")] def execute_macro_step(self, step): import requests requests.post(f"http://{self._pget('host', '192.168.1.50')}/toggle", timeout=5) def build_settings_ui(self, layout, settings): from PyQt5.QtWidgets import QLabel, QLineEdit layout.addWidget(QLabel("Device address")) inp = QLineEdit(str(self._pget("host", "192.168.1.50"))) inp.textChanged.connect(lambda t: self._pset("host", t)) layout.addWidget(inp) def has_settings_ui(self): return True ``` ## 7. Install & test - Drop the folder into `%APPDATA%\Mixlar\config\plugins\` — the app hot-loads it live (no restart) and prompts to install any bundled widgets. - Or use the Plugin Builder's **Send to app** button (mixlar.net/studio) to push it straight in for testing. - The app **warm-reloads** on re-send, so edit → send → see with no restart (make sure `on_unload` stops your threads).