admin管理员组

文章数量:1794759

从零开始:UlanziDeck插件开发之旅

UlanziDeck介绍

Ulanzi StreamDeck

在数字时代,个性化和效率是我们追求的核心。Ulanzi Stream Deck作为一款可视化键盘,拥有强大的桌面控制设备,通过其插件系统,为开发者和爱好者提供了无限的创造空间。本文将带你从零开始,探索Ulanzi Deck插件的开发过程,让你的桌面控制体验更加个性化和高效。

环境准备

在开始之前,请确保你已经安装了以下工具和环境:

  • Node.js版本20或更高。
  • Ulanzi Deck 软件
  • 集成开发环境,VS Code、WebStorm等任意。
  • Ulanzi Deck Plugin SDK

插件展示

下载完UlanziDeck软件后,打开右上角应用市场,下载模拟时钟,让我们看看模拟时钟的效果

如图所示,我们可以看到时钟正在运行,下面有着参数设置的区域

接下来我们来探索一下插件实现的过程

插件实现

1.插件文件夹

C:\Users\用户\AppData\Roaming\Ulanzi\UlanziDeck\Plugins下

在该位置下我们看到了模拟时钟的插件文件夹,我们来看下文件夹的结构

代码语言:txt复制
com.ulanzi.analogclock.ulanziPlugin
├── assets         //主要用于存放上位机icon的展示和action状态的切换
│   └── icons      
│       └── icon.png
├── libs    //插件通用库,此处不做具体介绍,可前往Ulanzi Deck Plugin SDK查看。
│  
├── plugin  //js主要功能模块,包括action的处理
│   ├── actions   //处理具体action逻辑
│   ├── app.html  //主服务html,作为入口
│   └── app.js    //主服务js
├── property-inspector // 配置项html和form表单的读写
│   └── clock      //action的名称
│       ├── inspector.html  //配置项html
│       └── inspector.js  //配置项js,用于做socket连接和form表单的处理
├── manifest.json         //具体配置
├── zh_CN.json      //中文翻译文件
├── en.json         //英文翻译文件

2.Manifest.json 插件配置解读

首先让我们来看下插件配置文件

代码语言:json复制
manifest.json
{
    "Version": "1.0.4",
    "Author": "Ulanzi",
    "Name": "Analog Clock",
    "Description": "Always be on time.",
    "Icon": "assets/icons/icon.png",
    "Category": "Analog Clock",
    "CategoryIcon": "assets/icons/categoryIcon.png",
    "CodePath": "plugin/app.html", 
    "Type": "JavaScript",
    "SupportedInMultiActions": false,
    "PrivateAPI": true,
    "UUID": "com.ulanzi.ulanzideck.analogclock",
    "Actions": [
      {
        "Name": "clock",
        "Icon": "assets/icons/actionIcon.png",
        "PropertyInspectorPath": "property-inspector/clock/inspector.html",
        "state": 0,
        "States": [
          {
            "Name": "clock",
            "Image": "assets/icons/icon.png"
          }
        ],
        "Tooltip": "Show a nice analog clock",
        "UUID": "com.ulanzi.ulanzideck.analogclock.clock",
        "SupportedInMultiActions": false
      }
    ],
    "OS": [
      {
        "Platform": "mac",
        "MinimumVersion": "10.11"
      },
      {
        "Platform": "windows",
        "MinimumVersion": "10"
      }
    ],
    "Software": {
      "MinimumVersion": "6.1"
    }
  }

Icon/Name:

在软件左侧列表显示的插件图标和插件名称

Category/CategoryIcon/Author/Description/Version

在设置->插件中右侧区域显示的名称、图标、作者、描述、版本信息

CodePath

插件的主程序,可以为".html"和".js",".html"时使用QWebEngineView加载,".js时"使用Node(版本为20.12.2)加载

UUID

插件的主程序的唯一标识

Actions -> Name/Icon

插件展开的配置项对应的名称和图标

Actions -> PropertyInspectorPath

插件对应的参数配置html,会由桌面软件加载在下方的红色区域

Actions -> UUID

插件的某个配置项的唯一标识

Actions -> SupportedInMultiActions

插件配置项是否支持多项操作,true代表点击进入多项操作后,支持拖入该插件配置进入多项操作

Actions -> States/state

两个按键位置的默认图标,支持复数个图标,state表示图标数组中的坐标位置,可以通过协议切换坐标位置来切换图标。因为模拟时钟会通过绘制表图更新按键位置,所以不会见到上方箭头按键的默认图标

好了,以上是比较重要的Manifest属性,下面我们介绍一下通用的约定,方便我们快速上手插件开发

通用约定

为了统一管理,我们的插件包的名称为 com.ulanzi.插件名.ulanziPlugin

为了SDK的正常使用,主服务连接的uuid我们约定长度是4。例:com.ulanzi.ulanzideck.插件名

配置项连接的uuid要大于4用于区分。例:com.ulanzi.ulanzideck.插件名.插件action

为了UI字体的统一,我们已经在udpi.css设置了开源字体思源黑体(Source Han Sans),在app.html也同样需要引用字体库。请大家在绘制icon时,统一使用'Source Han Sans'。

上位机的背景颜色为 '#282828',通用css(udpi.css)已经设定了'--udpi-bgcolor: #282828;'。若要自定义action的背景颜色应与上位机背景色相同,避免插件背景颜色过于突兀。

通过以上介绍,我们应该了解了插件Manifest的配置,并提及了

插件主程序(CodePath)

Action和Action参数配置Html(PropertyInspectorPath)两个概念,接下来给大家更详细地介绍一下这两个部分。

以下我们会将插件主程序称为app 或 app.js/html, 将配置项称为action


3.插件主程序、Action/PropertyInspector加载

插件主程序

刚刚我们提到了桌面软件会将插件Manifest中的CodePath,作为插件的主程序加载。那么主程序需要通过ws client,与上位软件建立常态通信,主程序的生命周期为:桌面软件的启动至退出,或是插件的安装至卸载。我们来看下主程序的具体加载过程:

使用node加载 app.js 时,会传递Websocket服务端地址及端口参数:

代码语言:powershell复制
 node.exe "app.js" "127.0.0.1" "3906" "en-US" 

加载app.html时,Url添加键值对参数:<"address", "127.0.0.1">,<"port","3906">,<"language","en-US">, <"uuid", "manifest中UUID">, <"actionId", "">, <"key", "0_0">

代码语言:javascript代码运行次数:0运行复制
app.html?address=127.0.0.1          //ws server链接
                &port=3906          //ws server端口     
                &language=en-US
                &uuid="com.xxx.xxx" //主服务的UUID

Action/PropertyInspector

当拖拽设置时钟至按键区域后,下方显示的是Manifest中的Actions -> PropertyInspectorPath,PropertyInspector的生命周期是在下面区域显示至从下方区域消失

具体加载的方式如上

代码语言:javascript代码运行次数:0运行复制
PropertyInspecto.html?address=127.0.0.1    //ws server链接
                &port=3906                 //ws server端口     
                &language=en-US
                &uuid="com.xxx.xxx" //Action的UUID
                &actionId="" //每个Action的唯一ID,区分不同页的Action
                &key="0_0"   //Action在当前页的坐标,在5*3方格区域里13个可设置区域,左上是0_0,任意位置为“行_列”


4.服务端、主程序、Action之间的通讯

服务端事件

以UlanziDeck Plugin SDK中为例,可以看到,在实现插件 app.html/js 和 propertyInspector.html时,应当首先与服务端建立连接: $UD.connect(UUID) ,随后通过监听服务端传来的事件,完成插件的功能。


插件端请求

接下来是可以向服务端发起的请求

通过上面两部分的消息接收/发送,我们应该可以理解

服务端 <=> app.html/js

服务端 <=> propertyInspector.html

的通讯过程,那么随之而来的问题是,app.html/js 是怎么与 propertyInspector.html 进行通讯的

以模拟时钟选中数字为例,来介绍下app与propertyInspector的通讯


服务端的消息转发

我们来看下propertyInspector的实现,通过监听html元素的表单变化,当“数字”选项发生变化时,会将参数更新给服务端:sendParamFromPlugin

服务端在接收到Action发来的参数更新时,会将带参数消息直接转发给Action的主服务;

反之同理,当主服务发起参数更新消息时,也会将消息转发给参数当前显示的propertyInspector.html

代码语言:javascript代码运行次数:0运行复制
propertyInspector.js
$UD.connect('com.ulanzi.ulanzideck.analogclock.clock')
$UD.onConnected(conn => {
  //获取表单
  form = document.querySelector('#property-inspector');

  //渲染option
  const oClockSelector = document.querySelector(".clockSelector");
  Object.keys(clockfaces).map(e => {
    let option = document.createElement('option');
    option.setAttribute('value', e);
    option.setAttribute('label', clockfaces[e].name);
    option.setAttribute('data-localize', clockfaces[e].name);
    oClockSelector.appendChild(option);
  });


  //连接上socket,显示配置项
  const el = document.querySelector('.udpi-wrapper');
  el.classList.remove('hidden');


  //监听表单变化,发送参数到上位机
  form.addEventListener(
    'input',
    Utils.debounce(() => {
        const value = Utils.getFormValue(form);
        ACTION_SETTING = value
        if(!ACTION_SETTING.clock_type) ACTION_SETTING.clock_type = 'analog'
        $UD.sendParamFromPlugin(ACTION_SETTING);   //发起参数更新
    })
  );
});

在app端接收来自action的参数更新消息,并刷新绘制

代码语言:javascript代码运行次数:0运行复制
app.js
//监听插件功能配置信息变化
$UD.onParamFromPlugin(jsn =>{
  onSetSettings(jsn)  //接收到propertyInspector发起的参数更新消息
})


//更新参数
function onSetSettings(jsn){
  const settings = jsn.param ||  {}
  const context = jsn.context   //context中含有action的UUID信息
  const clock = ACTION_CACHES[context];  //找到具体按键位置action的实例
  if(!settings || !clock) return;

  if(settings.hasOwnProperty('clock_index')) { 
      const clockIdx = Number(settings.clock_index);
      clock.setClockFaceNum(clockIdx);
      ACTION_CACHES[context] = clock;
  }
  if(settings.hasOwnProperty('clock_type')) {
    clock.setClockType(settings.clock_type); //更新“数字”参数到实例
  }
}


5.Action的active状态

在服务端事件中我们可以看到onSetActive事件,因为按键区域存在着 配置文件 和 页码 的切换,在切换过程中,非当前页的action会被设置为非active状态,例如切换到第二页时,第一页的时钟会接收到非active的状态通知,这时我们需要去暂停资源,停止定时请求等节省消耗,并等待active状态的通知重新恢复。

代码语言:javascript代码运行次数:0运行复制
//插件功能活跃状态设置
$UD.onSetActive(jsn =>{
  const context = jsn.context
  const instance = ACTION_CACHES[context];
  if (instance) {
    instance.setActive(jsn.active)
  }
})


6.Action的移除

Action的移除伴随着服务端onClear的事件通知,移除会有三种触发方式

1)按键的移除 2)页面的删除 3)配置的删除

按键的移除
页面的删除
配置的删除

总结:

以上,我们介绍了从插件文件的创建,插件加载,以及服务端和插件的通讯流程,有任何问题和错误,请及时联系我嘿嘿。代码的详细实现和更详细的通讯接口,可以在Ulanzi Deck Plugin SDK上查看,下篇文章会给大家展示插件的调试流程,和node方式的插件解读,敬请期待~

本文标签: 从零开始UlanziDeck插件开发之旅