English ยท ็ฎไฝไธญๆ
Official development kit for UlanziStudio programmable macro keypad platform
SDK Version: Based on Ulanzi JS Plugin Development Protocol V2.1.2 Compatible: Ulanzi Studio 3.0.11
Create a new folder with the naming convention com.ulanzi.{pluginName}.ulanziPlugin:
com.ulanzi.myplugin.ulanziPlugin/
โโโ manifest.json # Plugin configuration (required)
โโโ libs/ # SDK libraries
โโโ resources/ # Icons and assets
โโโ plugin/
โ โโโ app.js # Main service entry
โโโ property-inspector/
โโโ inspector.html # Configuration UINaming Convention:
- Plugin folder:
com.ulanzi.{pluginName}.ulanziPlugin - Main service UUID:
com.ulanzi.ulanzistudio.{pluginName}(4 segments) - Action UUID:
com.ulanzi.ulanzistudio.{pluginName}.{actionName}(5+ segments)
Create manifest.json in the plugin root directory:
{
"Author": "Your Name",
"Name": "My Plugin",
"Description": "Plugin description",
"Icon": "resources/icon.png",
"Version": "1.0.0",
"UUID": "com.ulanzi.ulanzistudio.myplugin",
"Type": "JavaScript",
"CodePath": "plugin/app.js",
"Actions": [
{
"Name": "My Action",
"Icon": "resources/action-icon.png",
"UUID": "com.ulanzi.ulanzistudio.myplugin.action",
"PropertyInspectorPath": "property-inspector/inspector.html",
"States": [
{ "Name": "Default", "Image": "resources/action-icon.png" }
],
"Controllers": ["Keypad"]
}
],
"OS": [
{ "Platform": "windows", "MinimumVersion": "10" },
{ "Platform": "mac", "MinimumVersion": "10.11" }
]
}Required Fields:
UUIDโ Plugin unique identifier (exactly 4 segments)CodePathโ Main entry point (.htmlfor WebView,.jsfor Node.js)Actionsโ Array of action definitionsTypeโ Fixed value"JavaScript"
Action Fields:
UUIDโ Action unique identifier (5+ segments)Controllersโ["Keypad"],["Encoder"], or bothDevicesโ Target devices (empty array[]= all devices)
Copy the SDK libraries to your plugin project:
# For HTML Property Inspectors
cp -r common-html/libs ./libs/
# For Node.js Main Services
cp -r common-node ./plugin-common-node/
# Install WebSocket dependency
npm install wsSDK Structure:
common-html/libs/โ WebSocket client, UI utilities, i18n for Property Inspectorscommon-node/โ WebSocket server, system APIs for Main Services
Plugins can use either HTML or Node.js as the main service. Choose based on your needs:
- HTML โ Best for UI-rich plugins, Canvas drawing, simpler logic
- Node.js โ Best for system integration, file access, complex logic
HTML Main Service (plugin/app.html):
<!DOCTYPE html>
<html>
<head>
<script src="../libs/js/constants.js"></script>
<script src="../libs/js/eventEmitter.js"></script>
<script src="../libs/js/timers.js"></script>
<script src="../libs/js/utils.js"></script>
<script src="../libs/js/ulanziApi.js"></script>
</head>
<body>
<script>
$UD.connect('com.ulanzi.ulanzistudio.myplugin');
$UD.onConnected(() => {
console.log('Connected to UlanziStudio');
});
$UD.onAdd((message) => {
console.log('Action added:', message.context);
});
$UD.onRun((message) => {
// Main action trigger logic
console.log('Action triggered:', message.context);
$UD.toast('Hello from plugin!');
});
</script>
</body>
</html>Node.js Main Service (plugin/app.js):
import UlanziApi, { Utils, RandomPort } from './plugin-common-node/index.js';
const $UD = new UlanziApi();
// Generate random port for Property Inspector connection (Node.js only)
const port = new RandomPort().getPort();
// Connect to UlanziStudio
$UD.connect('com.ulanzi.ulanzistudio.myplugin');
$UD.onConnected(() => {
console.log('Connected to UlanziStudio');
});
$UD.onAdd((message) => {
console.log('Action added:', message.context);
});
$UD.onRun((message) => {
// Main action trigger logic
console.log('Action triggered:', message.context);
$UD.toast('Hello from plugin!');
});Key Differences:
- HTML uses
<script>tags to load SDK; Node.js uses ES modules (import) - Node.js requires
RandomPortto generate WebSocket port for Property Inspector connection - Both use the same
$UDAPI for events and commands
Create property-inspector/inspector.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="../../libs/css/uspi.css">
<script src="../../libs/js/constants.js"></script>
<script src="../../libs/js/eventEmitter.js"></script>
<script src="../../libs/js/timers.js"></script>
<script src="../../libs/js/utils.js"></script>
<script src="../../libs/js/ulanziApi.js"></script>
</head>
<body>
<div class="uspi-wrapper">
<form id="property-inspector">
<div class="uspi-item">
<div class="uspi-item-label" data-localize>Message</div>
<input type="text" class="uspi-item-value" name="message" value="">
</div>
</form>
</div>
<script>
$UD.connect('com.ulanzi.ulanzistudio.myplugin.action');
// Load saved settings when action is added
$UD.onAdd((message) => {
Utils.setFormValue(message.param, '#property-inspector');
});
// Send settings to main service when form changes
document.getElementById('property-inspector').addEventListener('change', () => {
const params = Utils.getFormValue('#property-inspector');
$UD.sendParamFromPlugin(params);
});
</script>
</body>
</html>Key Points:
- Wrap content in
.uspi-wrapperfor automatic i18n and styling - Use
data-localizeattribute for automatic translation - Use
Utils.getFormValue()andUtils.setFormValue()for form handling
Create localization JSON files in the plugin root directory:
zh_CN.json:
{
"Localization": {
"Message": "ๆถๆฏ",
"Save": "ไฟๅญ",
"Hello": "ไฝ ๅฅฝ"
}
}en.json:
{
"Localization": {
"Message": "Message",
"Save": "Save",
"Hello": "Hello"
}
}Supported Languages:
zh_CNโ Simplified Chinesezh_HKโ Traditional Chinese (HK)enโ Englishja_JPโ Japanesede_DEโ Germanko_KRโ Koreanpt_PTโ Portuguesees_ESโ Spanish
Usage in HTML:
<!-- Automatic translation by element text content -->
<div class="uspi-item-label" data-localize>Message</div>
<!-- Automatic translation by attribute value -->
<option value="save" data-localize="Save">Save</option>
<!-- Manual translation in JavaScript -->
<script>
const label = $UD.t('Message'); // Returns localized string
</script>The UlanziDeck Simulator allows testing plugins without the desktop application:
cd UlanziDeckSimulator
npm install
npm startTesting Steps:
-
Copy plugin โ Copy your plugin folder to
UlanziDeckSimulator/plugins/ -
Open simulator โ Open http://127.0.0.1:39069 in your browser
-
Refresh plugin list โ Click "Refresh Plugin List" button to load your plugin
-
Start main service โ For Node.js plugins, start the main service manually:
node path/to/your/plugin/app.js
-
Add action to key โ Drag your action from the plugin list to a key
-
Open property inspector โ Click the key to open the configuration UI
-
Test events โ Use right-click menu to manually trigger events for testing
Simulator Limitations:
openviewandopenurlcannot open local files (browser restriction)- Main service must be started separately for Node.js plugins
- Actions are not auto-loaded by default
Launch UlanziStudio with debugging flags enabled:
Available Flags:
| Flag | Description |
|---|---|
--log |
Write logs to file |
--logLevel |
Set log verbosity |
--webRemoteDebug |
Enable HTML plugin debugging at port 9292 |
--nodeRemoteDebug |
Enable Node.js plugin debugging |
Windows:
Right-click UlanziStudio shortcut โ Properties โ append to Target:
"C:\...\Ulanzi Studio.exe" --log --webRemoteDebug --nodeRemoteDebug
macOS:
open /Applications/Ulanzi\ Studio.app --args --log --webRemoteDebugAccess Debug Tools:
- HTML Plugins: Open
localhost:9292in browser to debug all loaded HTML plugins - Node.js Plugins: Open
chrome://inspectin Chrome, find your plugin and click "inspect"
Important Notes:
- Using
opencommand on macOS may prevent Accessibility permissions, affecting hotkey functionality - Use
./UlanziStudiodirectly if hotkeys are not working
| Package | Purpose |
|---|---|
common-html |
HTML plugins (WebView) โ WebSocket client, UI utilities, i18n, Canvas helpers |
common-node |
Node.js plugins โ WebSocket server, system APIs, file system access |
UlanziDeckSimulator |
Browser simulator for testing |
demo |
Example plugins |
Plugins can use either HTML or Node.js as the main service:
| Type | Entry | Loading | SDK | Use Case |
|---|---|---|---|---|
| HTML Plugin | .html |
WebView | common-html |
UI-rich plugins, Canvas drawing, simple logic |
| Node.js Plugin | .js |
Node.js v20 | common-node |
System integration, file access, complex logic |
Property Inspector (optional) is always HTML using common-html, opened when user configures an action.
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
โ Property โ WebSocketโ UlanziStudio โ WebSocketโ Main โ
โ Inspector โ โโโโโโโโโ (Bridge) โ โโโโโโโโโ Service โ
โ (HTML) โ โ โ โ (Node.js/HTML)โ
โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
Plugin Lifecycle:
- Main Service โ Long-running background process (Node.js or HTML)
- Property Inspector โ HTML page created when user configures a key, destroyed on navigate away
Main Service: com.ulanzi.ulanzistudio.{plugin} โ 4 segments
Action: com.ulanzi.ulanzistudio.{plugin}.{action} โ 5+ segments
Package: com.ulanzi.{plugin}.ulanziPlugin
Each key instance has a unique context string:
// Format: uuid + '___' + key + '___' + actionid
const context = $UD.encodeContext(msg) // Encode to context string
const decoded = $UD.decodeContext(context) // Decode โ { uuid, key, actionid }Important: The same action can be assigned to multiple keys. The context parameter uniquely identifies each instance.
- D200
- D200H
- Dial
- D200X
Actions specify which controller types they support:
| Controller | Description | Events |
|---|---|---|
Keypad |
Standard button press | onRun, onKeyDown, onKeyUp |
Encoder |
Rotary dial | onDialDown, onDialUp, onDialRotate* |
Example Configuration:
{
"Controllers": ["Keypad", "Encoder"],
"Devices": ["D200X"]
}This action supports both keypad buttons and rotary dial on the D200X device.
The D200X dial area has an independent layout canvas for customizing display content:
"Encoder": {
"layout": "$UA1" // Built-in layout: icon + text
// or "layout": "layout.json" // Custom layout file
}Built-in Layouts:
$UA1โ Icon + Text layout$UA2โ Text + Text layout
// Use state index from manifest.json States array
$UD.setStateIcon(context, stateIndex, text)
// Use local image file path (relative to plugin root)
$UD.setPathIcon(context, 'resources/icon.png', text)
// Use base64-encoded image data
$UD.setBaseDataIcon(context, base64Data, text)
// Use local GIF file for animation
$UD.setGifPathIcon(context, 'anim.gif', text)
// Use base64-encoded GIF data
$UD.setGifDataIcon(context, base64GifData, text)// Save action-specific settings (only saves when action is active)
$UD.setSettings(data, context)
// Request saved action settings (response via onDidReceiveSettings)
$UD.getSettings(context)
// Save plugin-wide global settings
$UD.setGlobalSettings(data)
// Request global settings (response via onDidReceiveGlobalSettings)
$UD.getGlobalSettings()Important: Settings are NOT saved when the action is inactive.
// Show toast notification on UlanziStudio
$UD.toast('Hello World')
// Trigger system-level hotkey
$UD.hotkey('Ctrl+C') // Windows
$UD.hotkey('โC') // macOS
// Open URL in system browser
$UD.openUrl('https://example.com')
// Open local HTML file in popup window
$UD.openView('popup.html', 400, 300)
// Open file picker dialog
$UD.selectFileDialog('image(*.png *.jpg)')
// Open folder picker dialog
$UD.selectFolderDialog()
// Write to plugin log file
$UD.logMessage('Debug info', 'debug')
// Show error indicator on button
$UD.showAlert(context)// Action added to key (drag-and-drop)
$UD.onAdd((message) => {
console.log('Action added:', message.context);
})
// Key triggered (single click confirmed) โ main entry point
$UD.onRun((message) => {
console.log('Action triggered:', message.context);
})
// Key press started (fires before run, useful for long-press detection)
$UD.onKeyDown((message) => {})
// Key released
$UD.onKeyUp((message) => {})
// Action active state changed
$UD.onSetActive((message) => {
console.log('Active:', message.active); // true/false
})
// Action removed from one or more keys
$UD.onClear((message) => {
// message.param is array, each item has .context
})// Dial pressed
$UD.onDialDown((message) => {})
// Dial released
$UD.onDialUp((message) => {})
// Dial rotated (any direction)
$UD.onDialRotate((message) => {
// message.rotateEvent: 'left' | 'right' | 'hold-left' | 'hold-right'
})
// Rotated left (not held)
$UD.onDialRotateLeft((message) => {})
// Rotated right (not held)
$UD.onDialRotateRight((message) => {})
// Rotated left while pressed
$UD.onDialRotateHoldLeft((message) => {})
// Rotated right while pressed
$UD.onDialRotateHoldRight((message) => {})// Main Service โ Property Inspector (pass-through, not saved by host)
$UD.sendToPropertyInspector(data, context)
// Property Inspector โ Main Service (pass-through, not saved by host)
$UD.sendToPlugin(data)Event Handlers:
// In Main Service: receive from Property Inspector
$UD.onSendToPlugin((message) => {})
// In Property Inspector: receive from Main Service
$UD.onSendToPropertyInspector((message) => {})-
Settings Not Saved When Inactive Settings are NOT saved when the action is inactive. Check
onSetActivebefore relying onsetSettings. -
Simulator Limitations
openviewandopenurlcannot open local files (browser restriction)- Main service must be started separately for Node.js plugins
- Actions are not auto-loaded by default
-
Path Handling Use
Utils.getPluginPath()for cross-platform path handling:const pluginPath = Utils.getPluginPath(); const configPath = `${pluginPath}/config.json`;
-
Port Management For Node.js main services, use
RandomPortto avoid port conflicts:const port = new RandomPort().getPort(); // Writes ws-port.js
-
Context in clear Event For the
clearevent,contextis in each item of theparamarray:$UD.onClear((message) => { if (message.param) { for (const item of message.param) { console.log('Removed:', item.context); } } });
| Topic | Link |
|---|---|
| manifest.json Full Reference | manifest.md |
| common-html SDK (Property Inspector) | common-html/README.md |
| common-node SDK (Main Service) | common-node/README.md |
| Simulator Guide | UlanziDeckSimulator/README.md |
| Localization (i18n) | See common-html/README.md ยป Localization |
| Debugging Guide | See Step 8 above or common-html/README.md ยป Debugging |
| Field | Required | Description |
|---|---|---|
UUID |
โ | Plugin ID (exactly 4 segments) |
CodePath |
โ | Entry point (.html or .js) |
Actions |
โ | Action definitions array |
Type |
โ | Fixed value "JavaScript" |
Author |
โ | Developer name |
Name |
โ | Plugin name |
Icon |
โ | Plugin icon path |
Version |
โ | Plugin version |
Controllers |
["Keypad"], ["Encoder"], or both |
|
Devices |
[] = all devices, ["D200X"] = specific |
|
OS |
Supported operating systems |
Full reference: manifest.md
| Resource | Link |
|---|---|
| Download | https://www.ulanzi.com/pages/ulanzi-app |
| Community Forum | https://bbs.ulanzistudio.com |
AGPL 3.0 โ Services using this SDK must disclose their modified source code.
Plugin development inquiries: ustudioservice@ulanzi.com