English | 简体中文
ulanzistudio-plugin-sdk-python is a Python version of the UlanziStudio plugin SDK. It wraps the WebSocket connection and event protocol used by UlanziStudio, so Python plugin services can receive host events and send button/icon/settings commands with a small API surface.
This SDK follows the same event protocol surface as
UlanziTechnology/plugin-common-node, developed for Ulanzi JS Plugin Development Protocol - V3.1.0.
For manifest.json configuration, see manifest.md.
src/ulanzi_api/
├── constants.py # Event-name constants used by the SDK
├── random_port.py # Generates a random port for a self-hosted main service and writes ws-port.js
├── utils.py # Helpers: plugin path, system type, JSON parsing, etc.
├── ulanzi_api.py # Main SDK class and WebSocket event wrapper
└── __init__.py # Exports UlanziApi, Utils, RandomPort, Events
pip install websocket-clientWhen using this repository directly inside a plugin, install it from the plugin runtime environment:
pip install .Then import:
from ulanzi_api import UlanziApi, Utils, RandomPort- The Python main service, for example
app.py, should stay connected to UlanziStudio. It handles plugin logic, receives action parameter changes, and updates icon states. - Action / PropertyInspector HTML pages should remain lightweight. Use them for configuration UI and parameter exchange.
- Plugin package naming:
com.ulanzi.{pluginName}.ulanziPlugin - The main service UUID must have exactly 4 dot-separated segments:
com.ulanzi.ulanzistudio.{pluginName} - Action UUIDs must have more than 4 segments:
com.ulanzi.ulanzistudio.{pluginName}.{actionName} - When a PropertyInspector page needs to bypass UlanziStudio and connect directly to the plugin's own Python main service, use
RandomPortto generate a random listening port for that self-hosted service and write it tows-port.jsfor the PropertyInspector page to read. Plugins that communicate only through UlanziStudio's standard event flow do not need it. - Use
Utils.getPluginPath()to locate the plugin root directory ending withulanziPlugin.
The same action can be assigned to multiple keys, so the SDK appends a unique context string to received messages.
- Format:
uuid + '___' + key + '___' + actionid - Encode:
UD.encodeContext(message) - Decode:
UD.decodeContext(context)returns{"uuid": ..., "key": ..., "actionid": ...} - For the
clearevent,contextis added to each item inmessage["param"].
RandomPort is intended for architectures where the PropertyInspector page needs to connect to the main service by itself. For example, the Python main service may start a local WebSocket / HTTP service, and the PropertyInspector page may bypass UlanziStudio mediation to exchange connection state, account information, real-time lists, or other temporary data directly with that service. In this case, call getPort() once when the main service starts, then use the returned port to start your own service. RandomPort also writes the port to ws-port.js in the plugin root directory, so the PropertyInspector HTML page can include that file, read window.__port, and connect to the service.
If the PropertyInspector and main service synchronize parameters entirely through UlanziStudio's standard events, you do not need RandomPort.
from ulanzi_api import RandomPort
random_port = RandomPort()
port = random_port.getPort() # writes ws-port.js to the plugin root
# Use this port to start your own local WebSocket / HTTP service for the PropertyInspector page.
# start_service(host="127.0.0.1", port=port)In the PropertyInspector HTML, include the generated file before connecting:
<script src="../../ws-port.js"></script>
<script>
const socket = new WebSocket(`ws://127.0.0.1:${window.__port}`);
</script>Notes:
RandomPortonly generates a port and writesws-port.js; it does not create a WebSocket / HTTP service automatically.- Call
getPort()once during main service startup, then let the PropertyInspector page connect after your service is listening. - The default range is the dynamic port range
49152-65535, which helps reduce port conflicts between plugins. To restrict the range, passRandomPort(min_port=..., max_port=...).
When launched by the host application, Python reads connection arguments from sys.argv:
sys.argv[1]-> address, default127.0.0.1sys.argv[2]-> port, default3906sys.argv[3]-> language, defaulten
from ulanzi_api import UlanziApi
UD = UlanziApi()
def connected(_):
print("Connected")
def add(message):
context = message["context"]
print("Action added:", context)
def run(message):
UD.setStateIcon(message["context"], 1, "ON")
def param_from_app(message):
print("Saved params:", message.get("param"))
UD.onConnected(connected)
UD.onAdd(add)
UD.onRun(run)
UD.onParamFromApp(param_from_app)
UD.connect("com.ulanzi.ulanzistudio.myplugin")
UD.wait()connect() starts a background WebSocket thread by default. Call wait() in a main service to keep the process alive, or pass threaded=False to run the WebSocket loop in the current thread.
Connection events:
UD.onConnected(lambda message: None)
UD.onClose(lambda message: None)
UD.onError(lambda error: None)Button / key events:
UD.onAdd(lambda message: None)
UD.onRun(lambda message: None)
UD.onKeyDown(lambda message: None)
UD.onKeyUp(lambda message: None)
UD.onSetActive(lambda message: None)
UD.onClear(lambda message: None)Dial / encoder events:
UD.onDialDown(lambda message: None)
UD.onDialUp(lambda message: None)
UD.onDialRotate(lambda message: None)
UD.onDialRotateLeft(lambda message: None)
UD.onDialRotateRight(lambda message: None)
UD.onDialRotateHoldLeft(lambda message: None)
UD.onDialRotateHoldRight(lambda message: None)Param, settings, cross-page, and dialog events:
UD.onParamFromApp(lambda message: None)
UD.onParamFromPlugin(lambda message: None)
UD.onDidReceiveSettings(lambda message: None)
UD.onDidReceiveGlobalSettings(lambda message: None)
UD.onSendToPlugin(lambda message: None)
UD.onSendToPropertyInspector(lambda message: None)
UD.onSelectdialog(lambda message: None)Python-style aliases are also available, such as on_connected, on_run, set_state_icon, and send_param_from_plugin.
Set button icon:
UD.setStateIcon(context, state, text=None)
UD.setBaseDataIcon(context, data, text=None)
UD.setPathIcon(context, path, text=None)
UD.setGifDataIcon(context, gifdata, text=None)
UD.setGifPathIcon(context, gifpath, text=None)V3.1 display content commands (UlanziStudio 3.3.0 or later):
UD.setState(context, 1)
UD.setImage(
context,
{
"isDefault": True,
"icons": [
{"state": 0, "source": "path", "path": "images/off.png"},
{"state": 1, "source": "base64", "base64": "data:image/png;base64,..."},
],
},
)
UD.setImage(
context,
{"isDefault": False, "icons": {"source": "path", "path": "images/result.png"}},
)
UD.setImage(context, {"clear": True})
UD.setTitle(context, "Ready")Each icons item supports source values path, base64, state, and clear. When source is omitted, path is used.
Encoder feedback:
UD.setFeedbackLayout(context, "$UA1")
UD.setFeedback(
context,
{"title": {"text": "Ready"}, "icon": {"value": "Images/new.png"}},
)Only built-in layout IDs are supported. Custom layout files are intentionally excluded because protocol V3.1.0 marks them as pending implementation.
Send parameters and pass-through data:
UD.sendParamFromPlugin(settings, context=None)
UD.sendToPropertyInspector(settings, context)
UD.sendToPlugin(settings)Settings persistence:
UD.setSettings(settings, context=None)
UD.getSettings(context=None)
UD.setGlobalSettings(settings, context=None)
UD.getGlobalSettings(context=None)System functions:
UD.toast(msg)
UD.showAlert(context=None)
UD.logMessage(msg, level="info")
UD.hotkey(key)
UD.openUrl(url, local=False, param=None)
UD.openView(url, width=200, height=200, x=None, y=None, param=None)
UD.selectFileDialog(file_filter=None)
UD.selectFolderDialog()Utils is a singleton exported from ulanzi_api.
Utils.getPluginPath()
Utils.getSystemType() # "windows" or "mac"
Utils.adaptLanguage("zh-CN") # "zh_CN"
Utils.parseJson('{"a": 1}')
Utils.debounce(fn, wait=150)
Utils.getProperty(obj, "list[0].name", default_value=None)Launch Ulanzi Studio with flags to enable debugging.
| Flag | Description |
|---|---|
--log |
Write logs to file |
--logLevel |
Set log verbosity |
--pluginLoad |
Enable plugin load hook |
--webRemoteDebug |
Enable WebView remote debugging for HTML plugins. Default port is 9292 |
--webRemotePort=<port> |
Override WebView debug port |
--nodeRemoteDebug |
Enable Node.js remote debugging; useful when a plugin also contains Node services |
--doubleClick |
Enable double-click detection |
Windows shortcut target example:
"C:\...\Ulanzi Studio.exe" --log --webRemoteDebug
macOS:
open /Applications/Ulanzi\ Studio.app --args --log --webRemoteDebug