Skip to content

Latest commit

ย 

History

63 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

UlanziDeck Plugin SDK

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


โšก Quick Start

1๏ธโƒฃ Create Plugin Structure

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 UI

Naming 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)

2๏ธโƒฃ Configure manifest.json

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 (.html for WebView, .js for Node.js)
  • Actions โ€” Array of action definitions
  • Type โ€” Fixed value "JavaScript"

Action Fields:

  • UUID โ€” Action unique identifier (5+ segments)
  • Controllers โ€” ["Keypad"], ["Encoder"], or both
  • Devices โ€” Target devices (empty array [] = all devices)

3๏ธโƒฃ Install SDK

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 ws

SDK Structure:

  • common-html/libs/ โ€” WebSocket client, UI utilities, i18n for Property Inspectors
  • common-node/ โ€” WebSocket server, system APIs for Main Services

4๏ธโƒฃ Implement Plugin

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 RandomPort to generate WebSocket port for Property Inspector connection
  • Both use the same $UD API for events and commands

5๏ธโƒฃ Implement Property Inspector

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-wrapper for automatic i18n and styling
  • Use data-localize attribute for automatic translation
  • Use Utils.getFormValue() and Utils.setFormValue() for form handling

6๏ธโƒฃ Add Localization (Optional)

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 Chinese
  • zh_HK โ€” Traditional Chinese (HK)
  • en โ€” English
  • ja_JP โ€” Japanese
  • de_DE โ€” German
  • ko_KR โ€” Korean
  • pt_PT โ€” Portuguese
  • es_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>

7๏ธโƒฃ Test with Simulator

The UlanziDeck Simulator allows testing plugins without the desktop application:

cd UlanziDeckSimulator
npm install
npm start

Testing Steps:

  1. Copy plugin โ€” Copy your plugin folder to UlanziDeckSimulator/plugins/

  2. Open simulator โ€” Open http://127.0.0.1:39069 in your browser

  3. Refresh plugin list โ€” Click "Refresh Plugin List" button to load your plugin

  4. Start main service โ€” For Node.js plugins, start the main service manually:

    node path/to/your/plugin/app.js
  5. Add action to key โ€” Drag your action from the plugin list to a key

  6. Open property inspector โ€” Click the key to open the configuration UI

  7. Test events โ€” Use right-click menu to manually trigger events for testing

Simulator Limitations:

  • openview and openurl cannot open local files (browser restriction)
  • Main service must be started separately for Node.js plugins
  • Actions are not auto-loaded by default

8๏ธโƒฃ Debug in Desktop App

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 --webRemoteDebug

Access Debug Tools:

  • HTML Plugins: Open localhost:9292 in browser to debug all loaded HTML plugins
  • Node.js Plugins: Open chrome://inspect in Chrome, find your plugin and click "inspect"

Important Notes:

  • Using open command on macOS may prevent Accessibility permissions, affecting hotkey functionality
  • Use ./UlanziStudio directly if hotkeys are not working

๐Ÿ“ฆ SDK Overview

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

๐Ÿ—๏ธ Architecture

Main Service Types

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.

Communication Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”         โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   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

๐Ÿ”‘ Key Concepts

UUID Rules

Main Service:  com.ulanzi.ulanzistudio.{plugin}           โ†’ 4 segments
Action:        com.ulanzi.ulanzistudio.{plugin}.{action}  โ†’ 5+ segments
Package:       com.ulanzi.{plugin}.ulanziPlugin

Context Parameter

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.


๐ŸŽฎ Devices & Controllers

Supported Devices

  • D200
  • D200H
  • Dial
  • D200X

Controller Types

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.

Encoder Layout (D200X)

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

๐Ÿš€ Core APIs

Button Icons

// 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)

Settings Persistence

// 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.

System Functions

// 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)

Event Handlers

// 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
})

Encoder Events

// 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) => {})

Cross-Page Communication

// 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) => {})

โš ๏ธ Important Notes

  1. Settings Not Saved When Inactive Settings are NOT saved when the action is inactive. Check onSetActive before relying on setSettings.

  2. Simulator Limitations

    • openview and openurl cannot open local files (browser restriction)
    • Main service must be started separately for Node.js plugins
    • Actions are not auto-loaded by default
  3. Path Handling Use Utils.getPluginPath() for cross-platform path handling:

    const pluginPath = Utils.getPluginPath();
    const configPath = `${pluginPath}/config.json`;
  4. Port Management For Node.js main services, use RandomPort to avoid port conflicts:

    const port = new RandomPort().getPort();  // Writes ws-port.js
  5. Context in clear Event For the clear event, context is in each item of the param array:

    $UD.onClear((message) => {
      if (message.param) {
        for (const item of message.param) {
          console.log('Removed:', item.context);
        }
      }
    });

๐Ÿ“š Reference Documentation

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

๐Ÿ“„ manifest.json Reference

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


๐Ÿ”— Related Links

Resource Link
Download https://www.ulanzi.com/pages/ulanzi-app
Community Forum https://bbs.ulanzistudio.com

๐Ÿ“œ License

AGPL 3.0 โ€” Services using this SDK must disclose their modified source code.


๐Ÿ“ฌ Contact

Plugin development inquiries: ustudioservice@ulanzi.com

About

Collection (?) of own plugins for the Ulanzi Dial

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages