node-red-contrib-room-tools 1.9.3 → 1.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTROL_PANEL_AI_GUIDE.md +58 -18
- package/README.md +28 -6
- package/control-bound-protocol.js +25 -7
- package/control-catalog.js +26 -3
- package/control-panel.html +10 -36
- package/control-panel.js +37 -9
- package/control-policy.js +34 -5
- package/control-type-model.js +73 -11
- package/device-availability.js +104 -0
- package/gateway.cjs +36 -12
- package/managed-native-bridge.js +3 -1
- package/meter-frame.js +7 -1
- package/native-binding-plan.js +51 -12
- package/npm-shrinkwrap.json +2 -2
- package/package.json +6 -1
- package/panel-binding-editor.js +311 -62
- package/panel-meter.js +4 -4
- package/panel-momentary.js +7 -1
- package/panel-telemetry-send.js +13 -0
- package/panel-template-instrumentation.js +1 -0
- package/panel-visual-apply.js +86 -27
- package/panel-visual-bindings.js +26 -11
- package/panel-xy-series.js +16 -7
- package/xy-series-frame.js +8 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ControlPanel 前端页面编写指南
|
|
2
2
|
|
|
3
|
-
适用基线:当前统一工程中的 `node-red-contrib-room-tools@1.9.
|
|
3
|
+
适用基线:当前统一工程中的 `node-red-contrib-room-tools@1.9.4` 源码、Dashboard 2(已核对 `1.31.0`)和 Vue Options API。文档基线不代表该源码已发布或已安装到每台主机;本轮设备不可用处理尚未测试、发布。支持范围以当前识别器和运行时代码为准,不代表完整 Vue/Vuetify 语法都可自动绑定。
|
|
4
4
|
|
|
5
5
|
本文件是给页面编写 AI 的实现契约,不是完整的 Vue 或 Vuetify 手册。目标是:页面作者只写普通 HTML、CSS 和 Vue 页面;设备、驱动、映射表、反馈订阅和通信细节全部由 `ControlPanel` 节点处理。
|
|
6
6
|
|
|
@@ -23,15 +23,34 @@
|
|
|
23
23
|
|
|
24
24
|
不要在页面里实现第二套设备通信。不要写设备轮询、定时回写滑杆、反馈回写命令、重复发送监听器或自定义发送函数。未绑定的控件不会自动控制设备。
|
|
25
25
|
|
|
26
|
+
### 设备不可用时的页面行为
|
|
27
|
+
|
|
28
|
+
设备连接、有效回复超时、重新连接和新反馈就绪由公共驱动框架与节点处理,
|
|
29
|
+
页面 AI 不写掉线判断、轮询、禁用绑定或恢复逻辑。驱动已声明物理设备健康
|
|
30
|
+
语义、绑定已重新 Apply 后,节点自动把对应设备的绑定控件灰掉并禁止操作,
|
|
31
|
+
冻结最后值,不弹出提示;重新上线、就绪并收到新反馈后自动恢复。
|
|
32
|
+
同一页面其他正常设备不受影响。绑定编辑模式仍能选中不可用控件。
|
|
33
|
+
|
|
34
|
+
按钮灰掉期间仍保留最后的真实反馈状态;灰色不等于“关闭”或“取消静音”,
|
|
35
|
+
不能改写布尔模型。节点同时在服务端拒绝不可用设备的操作,不只是改变 CSS。
|
|
36
|
+
页面自身的业务 disabled 状态在设备恢复后仍保留;不要自行去掉节点施加的
|
|
37
|
+
禁用状态。没有健康声明的旧驱动/旧绑定不会凭名称猜测掉线;框架升级后由
|
|
38
|
+
驱动与绑定维护者完成接入,这不是页面作者的额外任务。
|
|
39
|
+
|
|
40
|
+
实时 Meter/XY-series 还检查有效采样与过期,不把连上 socket 当作新采样。
|
|
41
|
+
有健康声明且设备不可用时保留最后显示值;普通 XY-series 采样无效/过期但
|
|
42
|
+
设备仍可用时,继续沿用空数组语义。掉线识别存在协议相关的检测窗口,
|
|
43
|
+
不是拔电即刻必然通知网页。纯页面导航或未绑定按钮不因设备掉线而禁用。
|
|
44
|
+
|
|
26
45
|
页面里的 `volume` 和 `meter` 是两个不同语义:
|
|
27
46
|
|
|
28
47
|
- `volume` 等普通数值字段用于音量/增益等可写连续控制,并可接收普通真实值反馈。
|
|
29
|
-
- `meter`
|
|
48
|
+
- `meter` 等字段用于只读数值指示,可接收原生只读连续数值参数,或带 Meter 流元数据的实时点位;不能用来设置音量。
|
|
30
49
|
- `series`、`spectrum` 等数组字段用于只读实时二维点列,只能接收驱动声明了 XY-series 元数据的点位。
|
|
31
50
|
|
|
32
51
|
不要把同一个模型字段同时用于不同设备点位,尤其不要让 Meter 和 Volume 共用一个模型。
|
|
33
52
|
|
|
34
|
-
`volume`、`level`、`slider`、`dimmer` 和 `position`
|
|
53
|
+
`volume`、`level`、`slider`、`dimmer` 和 `position` 都属于普通连续数值控制机制;名称不决定设备匹配。布尔开关、单选枚举和文本也有独立适配。带流元数据的 Meter 和 XY-series 使用专门的只读实时帧机制;原生只读数值指示复用设备参数反馈。当前没有自动绑定任意 Command 动作或 Mixer 矩阵的适配器。
|
|
35
54
|
|
|
36
55
|
## 2. 页面格式与 Vue 约束
|
|
37
56
|
|
|
@@ -53,7 +72,7 @@ export default {
|
|
|
53
72
|
|
|
54
73
|
不要使用 `<script setup>`、TypeScript、组件导入、自行创建 Vue 应用或依赖外部构建的运行时。普通绑定模型应是 `data()` 中的顶层字段,或本指南 3.2 节规定的列表模型。计算属性、函数调用、取反、条件表达式和任意动态索引不能作为绑定模型;它们仍可用于不绑定设备的显示逻辑。
|
|
55
74
|
|
|
56
|
-
不要把节点注入的 `boundState`、`boundPending`、`boundTargets`、`boundUnconfirmed`、`boundAuthenticated`、`boundFeedbackConnected` 等内部字段,或 `__bound*`、`__panel*`、`__attachButtonPress`、`__attachMomentaryButton`、`sendBoundControl` 等方法用于页面模型或自定义方法。使用自己命名的普通字段,避免合并时冲突。
|
|
75
|
+
不要把节点注入的 `boundState`、`boundPending`、`boundTargets`、`boundUnconfirmed`、`boundAuthenticated`、`boundFeedbackConnected`、`boundAvailability` 等内部字段,或 `__bound*`、`__panel*`、`__deviceAvailable`、`__attachButtonPress`、`__attachMomentaryButton`、`sendBoundControl` 等方法用于页面模型或自定义方法。使用自己命名的普通字段,避免合并时冲突。
|
|
57
76
|
|
|
58
77
|
不要手写任何 `data-cp-*` 属性。节点会在渲染副本中自动加入内部标记,页面源码不应依赖这些标记;手写此类属性会被拒绝。
|
|
59
78
|
|
|
@@ -77,7 +96,7 @@ export default {
|
|
|
77
96
|
| 用途 | 原生 HTML | Vuetify | 模型与行为 |
|
|
78
97
|
| --- | --- | --- | --- |
|
|
79
98
|
| 连续数值控制 | `input[type=range]`、`input[type=number]` | `v-slider` | 数字;原生控件在 input 时发送中间值、change 时提交最终值;`v-slider` 根据模型变化发送,停止变化后补交最终值。 |
|
|
80
|
-
|
|
|
99
|
+
| 两态控制 | `input[type=checkbox]`、带语义属性的 `button` | `v-switch`、`v-checkbox` | 页面字段始终为布尔值;驱动原始两态值由绑定映射转换。原生语义按钮还可在绑定窗口选择“按住生效”。 |
|
|
81
100
|
| 单选枚举 | `select` | `v-select`、`v-radio-group` | 单个字符串或数字,必须是设备允许的枚举值。 |
|
|
82
101
|
| 文本控制 | `input[type=text]`、`textarea` | `v-text-field`、`v-textarea` | 字符串;原生控件在 change 时提交,Vuetify 文本框在 change、失焦时提交,`v-text-field` 也支持 Enter 提交。 |
|
|
83
102
|
| 普通只读反馈 | `span`、`strong`、`b`、`output`、`h1`、`h2`、`h3` | 暂无专用组件 | 单个简单字段插值,或静态文本由节点替换;不发送命令。普通 `div`/`p` 文本不属于此适配器。 |
|
|
@@ -109,6 +128,8 @@ XY-series 使用数组字段初始化,例如 `spectrum: []`。推荐使用标
|
|
|
109
128
|
|
|
110
129
|
数组以整体替换方式更新,绘图通常使用浅层 `watch` 即可,无需 `deep: true` 遍历每个点。只观察数组来绘制,不改写接收数组;多图表需要时可在页面自己的 `requestAnimationFrame` 中合并绘制,并在卸载时清理绘图资源。这是纯显示逻辑,不是第二套设备通信。
|
|
111
130
|
|
|
131
|
+
无关 DOM 变化不会反复向同一个目标模型写入上一帧数组;容器重新出现时由节点恢复最新快照,不要手动补绑。大量频谱点建议在 Canvas 中批量绘制,避免为每个采样点创建 DOM 元素。多个显示终端会增加实际网络和绘制负载,不能把单帧点数上限理解为所有图表都能同时维持 30 Hz。
|
|
132
|
+
|
|
112
133
|
### 3.1 普通控件写法
|
|
113
134
|
|
|
114
135
|
```html
|
|
@@ -262,16 +283,27 @@ export default {
|
|
|
262
283
|
|
|
263
284
|
## 4. Meter 与普通数值反馈
|
|
264
285
|
|
|
265
|
-
Meter
|
|
286
|
+
Meter 是只读数值指示,不是“另一个滑杆”。页面作者只声明普通进度/电平控件,原生参数反馈和批量实时流使用相同的前端写法,不需要为不同设备改页面。
|
|
266
287
|
|
|
267
288
|
- 一个 Meter 绑定只能使用 `read` 模式,不能配置 Write point。
|
|
268
|
-
-
|
|
269
|
-
-
|
|
270
|
-
-
|
|
289
|
+
- 原生 `kind=param`、`access=ro` 的 integer/float 参数,没有枚举或两态域时,可绑定 Meter。既不要求名称包含 Meter,也不要求厂商提供自定义流元数据。它走原生 Status/Parameter Event 反馈,不生成虚构的 manifest/frame 路径;相同设备和路径只订阅一次。
|
|
290
|
+
- 原生只读数字可能是电平、温度或其他测量值。通用识别器根据权限和数值结构判断“只读指示”,不能仅凭这些描述确定物理含义。绑定人员仍须选择正确点位;字段名不会把一个可写音量参数变成 Meter。
|
|
291
|
+
- 可写的连续参数供音量滑杆等普通数值控件使用;只读数值指示和带 `.metadata.meter` 的点位不能作为滑杆的 Write 或 Feedback point。原生只读数字仍可绑定文本展示。若设备把写入增益和只读增益反馈拆为两个点,当前不会猜测其关联;需要明确的后续适配,不能把只读电平误当作增益反馈。
|
|
292
|
+
- 带有效 `.metadata.meter` 的只读整数/浮点点位继续使用批量实时通道:自动订阅 manifest/frame、同帧投影、过期判断和重连。同设备、同 stream 的多个 Meter 取自同一多通道帧;不同设备或不同 stream 独立接收。Room 聚合各流的最新值,不保证不同流采样同步。页面不编写任何批处理或 socket 代码。
|
|
293
|
+
- 原生参数反馈没有 stream 的 epoch/seq、采样时间戳或多通道原子帧保证,更新速度取决于设备。不能把原生参数兼容性当成批量高频流性能或采样过期检测的承诺;断线后沿用普通反馈重连和最新快照机制。
|
|
271
294
|
- Meter 需要独立字段,例如 `meter1`、`meter2`。不要与音量滑杆的 `volume` 共用字段,也不要让不同点位共用一个 Meter 模型。
|
|
272
295
|
- 未收到有效采样时模型保持初值;断线、无效采样和过期后保留最后值,控件的标准 `aria-invalid` 标为 `true`。数值模型本身不会变成特殊的“无效值”,不能把保留值当成新采样;需要时可用标准属性选择器显示无效样式,不依赖私有标记,也不添加轮询或回写。
|
|
273
296
|
- 原生 `<meter>`/`<progress>` 默认页面范围是 0 到 1;Vuetify 进度组件分别使用固定的 0 到 max 或 0 到 100。应按页面显示范围声明固定量程。
|
|
274
297
|
|
|
298
|
+
以百分比显示的 Meter,旁边的数值标签统一显示整数百分比,不直接输出实时反馈的小数。绑定控件仍使用简单的原始模型字段,只在独立的展示标签中使用普通 Vue 表达式取整,不能为显示格式改写反馈模型或降低采样精度:
|
|
299
|
+
|
|
300
|
+
```html
|
|
301
|
+
<meter id="signal-meter" :value="meter" min="0" max="100" aria-label="实时信号电平"></meter>
|
|
302
|
+
<span class="meter-percent" data-display-only="true">{{ Math.round(meter) }}%</span>
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
示例的 `meter` 在 `data()` 中初始化为 `0`;控件量程为 0–100,`66.7` 显示为 `67%`。`data-display-only="true"` 是普通 HTML 数据属性,明确告诉识别器这个元素只是展示格式,不是第二个设备绑定;所有取整、单位和拼接文本都放在这种展示元素中。若页面量程为 0–1,标签使用 `{{ Math.round(meter * 100) }}%`。为标签预留 `100%` 的宽度,可使用 `font-variant-numeric: tabular-nums` 和 `white-space: nowrap`,避免数字更新挤占相邻控件。温度、分贝等物理测量不要误标为百分比,按其实际单位格式化。
|
|
306
|
+
|
|
275
307
|
量程适配发生在网页侧:普通数值设置从页面范围换算到驱动范围,数值反馈和 Meter 从驱动范围换算到页面范围,不修改驱动或协议。两端都必须是有限且 max > min 的数字;否则按原值传递,不能猜测量程。换算会钳制到范围内,整数驱动值会取整,反馈会按有效的页面 step 对齐。XY-series 不使用这种标量量程换算。
|
|
276
308
|
|
|
277
309
|
普通只读反馈使用文本展示即可:
|
|
@@ -298,7 +330,7 @@ XY-series 是 Meter 的独立数组版本,不是多个 Meter 的替代写法
|
|
|
298
330
|
|
|
299
331
|
## 5. 按钮必须分开表达按压与设备状态
|
|
300
332
|
|
|
301
|
-
这是页面编写 AI
|
|
333
|
+
这是页面编写 AI 的必选模式,不是可选装饰。凡是控制静音、电源、启停等两态设备状态的按钮,都使用本节写法;默认点击切换和按住生效只改变命令行为,不改变状态显示规则。页面字段始终是布尔值,无需知道设备使用 true/false、0/1、Off/On、A/B 或其他两个值;转换在绑定配置和节点内部完成,不在前端编写。
|
|
302
334
|
|
|
303
335
|
| 状态 | 表达方式 | 不能表示什么 |
|
|
304
336
|
| --- | --- | --- |
|
|
@@ -390,9 +422,11 @@ export default {
|
|
|
390
422
|
|
|
391
423
|
1. 将 Template 保存到 `ui-control-panel`,配置 Dashboard 页面、Room ID、普通登录账号和独立编辑账号,然后保存并 Deploy。
|
|
392
424
|
2. 重新打开节点,点击 `Open binding editor`。编辑页登录使用编辑账号;进入后 Select 默认激活。`Open page` 打开普通控制页面,普通页面必须使用普通账号登录。
|
|
393
|
-
3. 在 Select 模式选择控件,选择设备、模式、Write point、Feedback point 和编号方式。Meter、XY-series
|
|
394
|
-
4. 点 Apply。节点验证当前模板、设备目录和映射,备份当前 Flow,并通过运行时 Flow API
|
|
395
|
-
5. Apply
|
|
425
|
+
3. 在 Select 模式选择控件,选择设备、模式、Write point、Feedback point 和编号方式。Meter、XY-series 和文本展示的模式选项只有 `read`;无需手动从 `both` 改为 `read`。已绑定控件在编辑模式显示亮紫色边框;这只是编辑提示。
|
|
426
|
+
4. 点 Apply。节点验证当前模板、设备目录和映射,备份当前 Flow,并通过运行时 Flow API 以 `nodes` 部署方式更新当前面板的映射和受管理桥接。更新会使受影响节点重启;浏览器同步脚本不点击 Deploy,也不会向画布导入、清除或追加节点。
|
|
427
|
+
5. Apply 成功后会重新加载当前编辑页,使用仍有效的编辑会话继续进入;会话过期或登录配置改变时可能要求重新登录。设备目录或运行时加载失败不代表会话失效,此时显示错误与 Retry,重试不需要再次输入密码。配置和设备目录并行加载。Bindings 列表可以 Edit 或 Remove,从列表进入编辑时取消/保存后回到列表。点 Exit 退出编辑并回到普通页面登录页。
|
|
428
|
+
|
|
429
|
+
未改变绑定、生成桥接或其他节点配置的 Apply 仍执行模板、目录、映射和版本校验,但不重复备份、写入或重启 Flow;编辑页仍重新加载。真实绑定变化保留原有 `nodes` 部署方式,因此耗时还取决于当前运行环境的目录查询、Flow 存储和受影响节点启动速度。不能承诺本地 Automator 与 MUSE 耗时相同。
|
|
396
430
|
|
|
397
431
|
Automator 的同步脚本监听 `notification/runtime-deploy` 的新版本通知,并在当前页面最多触发一次浏览器刷新;它不读取、导入、合并、清除或再次提交 Flow,也不额外点击 Deploy。监听不限于本面板 Apply,其他运行时版本变化也可能触发刷新。当前实现不判断画布是否有未 Deploy 改动,也不会合并画布草稿;在 Apply 前应先处理好 Automator 中未保存的画布改动。没有打开 Automator 编辑页或不支持该通知接口时,不能承诺自动刷新。
|
|
398
432
|
|
|
@@ -400,21 +434,27 @@ Template 修改可能使已有映射失效。重新 Deploy 后,应重新打开
|
|
|
400
434
|
|
|
401
435
|
## 7. 点位匹配与量程
|
|
402
436
|
|
|
403
|
-
本节供绑定使用者和排查人员参考,不要求页面编写 AI 在前端填写这些描述。编辑器根据 AMX 参数描述推导可匹配类型,不根据页面文字,也不把驱动作者写入的旧式显式 `controlType` 当作页面契约。主要依据是 `.kind`、`.type`、`.access`、`.min`、`.max`、`.enums` 和 `.metadata
|
|
437
|
+
本节供绑定使用者和排查人员参考,不要求页面编写 AI 在前端填写这些描述。编辑器根据 AMX 参数描述推导可匹配类型,不根据页面文字,也不把驱动作者写入的旧式显式 `controlType` 当作页面契约。主要依据是 `.kind`、`.type`、`.access`、`.min`、`.max`、`.enums` 和 `.metadata`。批量 Meter 的 `.metadata.meter` 和 XY-series 的 `.metadata.xySeries` 是各自实时流通道所需的特殊元数据;原生只读数值指示不需要这些元数据。
|
|
404
438
|
|
|
405
439
|
| 参数描述 | 可匹配控件 |
|
|
406
440
|
| --- | --- |
|
|
407
|
-
| boolean
|
|
441
|
+
| boolean | 布尔开关;默认 false/true,可反转对应关系。 |
|
|
442
|
+
| 描述明确列出两个不同、合法的标量枚举值 | 两态开关,不依赖文字含义;enum、带枚举的 integer/float/string/dstring/boolean 保留原始值。enum 也仍可绑定选择控件。 |
|
|
443
|
+
| 没有枚举、合法整数范围恰好只有两个相邻整数的 integer | 两态开关,例如 0/1、1/2、-2/-1;发送与反馈转换为绑定所选对应关系。不能依据当前值猜测合法状态。 |
|
|
408
444
|
| 普通可写 integer/float,未被识别为布尔或枚举 | 数值 input 或 `v-slider`;用于音量/增益等普通连续控制。 |
|
|
409
|
-
| 普通只读 integer/float
|
|
445
|
+
| 普通只读 integer/float,没有枚举或两态域,也没有 Meter 流元数据 | 原生 `meter`/`progress`、Vuetify 进度组件或文本展示;走普通参数反馈,不能映射到音量滑杆。 |
|
|
410
446
|
| 只读 integer/float,带有效 `.metadata.meter` | Meter、原生 `meter`/`progress` 或 Vuetify 进度组件;只能读。 |
|
|
411
447
|
| 只读参数,带有效 `.metadata.xySeries` | XY-series;使用标准 Canvas 的 `:data-points`/`:data-series`,或地图容器的 `:data-coordinates` 数组字段;只能读。 |
|
|
412
|
-
| enum,或带枚举描述的 integer/string |
|
|
448
|
+
| enum,或带枚举描述的 integer/string | 单选枚举;恰为两个合法选项时也支持两态开关。 |
|
|
413
449
|
| 没有枚举但路径以 `/source` 或 `/preset` 结尾的 integer | 选择类参数,仍需确认合法选项。 |
|
|
414
450
|
| string/dstring | 文本输入或文本展示。 |
|
|
415
451
|
|
|
416
452
|
只有 `kind=param` 且具备相应读/写权限的点位参与绑定。普通 Command 动作、数组容器和未知描述不能直接绑定。普通数值、布尔、枚举或文本控件也可以只读绑定;若需要阻止用户本地操作,页面另用标准 `disabled` 等属性表达只读交互。
|
|
417
453
|
|
|
454
|
+
两态绑定只提供一个 `State reverse` 复选框,统一作用于写入和反馈。默认不勾选:驱动状态列表第一项对应页面 false,第二项对应 true;勾选后两个方向一起反转,不再提供独立的写入/反馈映射选项。设置会在切换点位、模式、保存草稿、重新打开和 Apply 后保留。读写点可以具有不同的原始类型和值,但必须采用同一个默认/反转方向,不能复制另一点的原始值。旧绑定在下次 Apply 时迁移:可写绑定以 Write 映射为准,只读绑定以 Feedback 映射为准;明确的布尔 `stateReverse` 优先。仅安装新版不会重写现有 Flow。前端作者仍只维护普通布尔字段,不需要处理反转或原始驱动值。已有 boolean 和整数 0/1 绑定保持兼容。普通 float 范围 0..1 仍然有无限多个合法值,不视为两态;不完整枚举、重复值、非有限数、三项以上枚举也不会被强行识别为两态。未知反馈值会报错,不会被猜成 true/false。枚举对象使用其明确的 `value`,而非 label 或数组索引;只支持原生参数通道能够传输的 boolean、有限 number 和 string 值。
|
|
455
|
+
|
|
456
|
+
按住生效也使用该映射:按下发送 true 对应值,松开或租约释放发送 false 对应值。映射不代表设备已执行,激活样式仍来自真实反馈。同一面板若同时按住同一点的两个按钮,不能配置相反的激活方向;冲突按压会被拒绝。设备节点名称不是控制语义证明,例如矩阵交叉点 On_Off 不应未经确认就当成输出静音。
|
|
457
|
+
|
|
418
458
|
当前一个面板的所有可视绑定合计最多 256 项,包含普通控件、Meter 和 XY-series,并非每种类型各有 256 个独立名额。某些不推荐的字段共享写法未必被校验器全部拦截,不能把 Apply 成功当成模型设计无冲突的证明。
|
|
419
459
|
|
|
420
460
|
编辑弹窗中,`read` 禁用并灰显 Write point,`write` 禁用并灰显 Feedback point,`both` 两者均可选择。切换模式不清空弹窗内已有选择;保存草稿时只保留当前模式需要的点位。在 `both` 模式下,Write point 改动时 Feedback point 会跟随;单独改 Feedback point 不会反向改 Write point。Meter 和 XY-series 只能使用 Feedback point。编号方式默认 Zero-based;如果目录点位带有 `.onebased`,保存时以目录描述为准,否则使用编辑页选择。编号方式只影响驱动点位编号,不会给网页值加减 1。
|
|
@@ -426,7 +466,7 @@ Template 修改可能使已有映射失效。重新 Deploy 后,应重新打开
|
|
|
426
466
|
| 控件不能选择 | 是否在支持清单;是否有事件指令、复杂模型、动态量程、重复/不唯一 key、重复 ID 或自闭合 Vuetify 标签。 |
|
|
427
467
|
| `v-for` 实例没有出现 | 来源是否是顶层数组;key 是否严格符合对象/数组规则;列表项是否实际渲染且 key 唯一。 |
|
|
428
468
|
| 设备有了但点位为空 | 设备在线状态、AMX 参数目录、`kind/type/access` 和读写权限;不是所有命令都属于可绑定参数。 |
|
|
429
|
-
| Meter 可以选但无值 |
|
|
469
|
+
| Meter 可以选但无值 | 原生点位检查 Status/Parameter Event 是否实际反馈;带 `.metadata.meter` 的点位检查 manifest/frame;两者都检查是否与 Volume 使用独立模型,以及设备量程与网页范围。 |
|
|
430
470
|
| XY-series 可以选但无值 | 点位是否带 `.metadata.xySeries`;驱动是否发布对应 manifest/frame;数组字段是否为顶层数组;检查点数、帧大小和过期状态。 |
|
|
431
471
|
| 页面一直 Loading | 查看模板编译错误,重点检查原生 Meter/Progress 的 v-model 写法、自闭合 Vuetify、动态表达式和不支持的组件。设备离线不应被页面作者通过轮询“修复”。 |
|
|
432
472
|
| 滑杆动不了 | 是否仍处于 Select 编辑模式;模型是否为数字;是否给绑定控件添加了自己的事件或回写逻辑。 |
|
package/README.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
This package provides `ui-control-panel` for Node-RED and MUSE Automator.
|
|
4
4
|
|
|
5
|
+
Current source includes unpublished, untested physical-device availability
|
|
6
|
+
support. Drivers annotate readonly boolean health parameters with
|
|
7
|
+
`.metadata.availabilityRole: "online"` / `"ready"` at the physical-device scope.
|
|
8
|
+
Re-Apply bindings after updating the driver descriptor to add deduplicated
|
|
9
|
+
Status/Event health subscriptions. Without this metadata old bindings retain
|
|
10
|
+
their existing behavior; this cannot infer physical health from a provider's
|
|
11
|
+
virtual-device online status. Managed controls gray out, become inert/aria-disabled
|
|
12
|
+
and freeze while unavailable. Commands are rejected server-side too. Ordinary
|
|
13
|
+
controls resume only after readiness and fresh point feedback; streams also
|
|
14
|
+
require valid fresh frames. Binding editor selection remains available. The
|
|
15
|
+
page author supplies no MUSE-specific health code, and no alert is displayed.
|
|
16
|
+
|
|
5
17
|
## ControlPanel
|
|
6
18
|
|
|
7
19
|
The page author writes ordinary HTML/CSS/Vue and pastes it into the `ui-control-panel` Template. After configuring a Dashboard page, room ID, control login, and separate editor login, save and Deploy. Reopen the node and select **Open binding editor**. Sign in with the editor account, right-click a supported control, choose its device points, and Apply. **Open page** opens the normal control page with its separate user login.
|
|
@@ -10,15 +22,17 @@ Both logins work over HTTP on a trusted local network. HTTPS is recommended for
|
|
|
10
22
|
|
|
11
23
|
Bindings are stored in `visualBindings` on the node. The runtime reads that mapping to authenticate commands, route feedback, and generate a native bridge. The page author does not put device paths, `data-cp-*` attributes, or device-send code in the Template. Author-provided `data-cp-*` attributes are rejected.
|
|
12
24
|
|
|
13
|
-
Supported controls include native range, checkbox, semantic pressed button, select, text input, meter, progress, and simple text display. Give
|
|
25
|
+
Supported controls include native range, checkbox, semantic pressed button, select, text input, meter, progress, and simple text display. Give each template control a stable standard ID or name; repeated instances also need stable unique keys. Simple top-level Vue `v-model` works, including object-list models such as `item.volume`, `item.audio.volume`, or `items[index].volume`, primitive array models such as `values[index]`, and an inner `<template v-for>` for multiple controls per row. During Vue re-rendering, a temporary duplicate list key is ignored until the key becomes unique again, so bindings do not attach twice. Detached or disposed controls release their event listeners and cannot send commands. Controls with existing send handlers or ambiguous dynamic behavior are rejected until a suitable adapter exists. See `CONTROL_PANEL_AI_GUIDE.md` for examples and limits.
|
|
26
|
+
|
|
27
|
+
Native semantic buttons support a binding-editor **Button behavior** setting: **Click to toggle** (the default for existing bindings) or **Hold to activate**. Momentary buttons require `both` mode, send the mapped active value on press and inactive value on release, and always display real device feedback rather than a predicted local state. Pointer cancellation, keyboard release, focus/visibility loss, DOM removal, and disconnect release the hold. A 3-second server lease attempts release if the browser disappears; lease renewal does not repeat device writes. Concurrent holders of the same Write point within one panel are combined until the last release; opposite state mappings are rejected while held. Other panels and external controllers are not coordinated. Device or bridge outages can prevent physical release, so feedback remains the authority. Drivers keep their original parameter values; page authors do not implement hold events or lease traffic.
|
|
14
28
|
|
|
15
|
-
|
|
29
|
+
Two-state controls do not depend on device labels or a legacy explicit controlType. A boolean, an integer domain containing exactly two adjacent values, or a descriptor listing two distinct valid scalar options can match a switch. The editor offers one **State reverse** checkbox for both command and feedback conversion. Unchecked means the first value is false and the second is true; checked reverses both directions. This setting persists across point changes, mode changes, draft reopening and Apply. Read and write points may use different raw types/values but share the same normal/reversed order. On the next Apply, legacy separate mappings are migrated using the Write mapping when writable, otherwise the Feedback mapping; an explicit boolean stateReverse takes precedence. Existing running bindings are not rewritten merely by installing an update. Original string/number/boolean payloads are preserved, including enum Off/On, arbitrary A/B labels and integer 1/2, while the ordinary Vue model stays boolean. Enum targets remain available to select controls. Continuous float ranges, unknown domains, duplicate options and nonfinite values are not guessed into switches; unexpected feedback is rejected. Existing boolean/integer01 bindings keep their old behavior.
|
|
16
30
|
|
|
17
31
|
Writable native semantic buttons in either behavior automatically show a separate physical press effect (2px downward translation and inset shadow) for pointer/touch and Space/Enter. Release, cancellation, focus/visibility loss, disablement, DOM removal and loss of the authenticated feedback connection clear it. This effect changes neither the Vue model nor device-feedback ARIA, colors or icons; device feedback remains the sole source of the indicated mute/power state. Read-only buttons and editor mode do not show this effect. Page authors need no extra event handlers or internal press attributes.
|
|
18
32
|
|
|
19
|
-
|
|
33
|
+
Meter delivery does not change the page-authoring contract. Authors use ordinary `meter`, `progress`, `v-progress-linear`, or `v-progress-circular` controls with stable IDs and simple Vue fields. They do not declare devices, point paths, stream IDs, sockets, subscriptions, or frame formats. A `volume` binding is a writable gain setting with ordinary feedback. Readonly numeric parameters without enums or a two-state domain can bind to an indicator through native Status/Parameter Event feedback, without custom metadata or vendor/name heuristics. These can represent signal levels or other measurements; numeric structure alone cannot establish their physical meaning. They cannot bind to a volume slider, even as its feedback. Writable numeric controls cannot bind to a Meter. Split write-gain/read-gain parameters are not inferred as a pair. Native readonly values can still bind to text displays. When a target advertises Meter stream metadata, ControlPanel continues to share that stream, project complete frames, apply webpage-side range conversion, and handle stale/reconnected data. Native scalar events do not provide stream sequencing, sampling timestamps, atomic multichannel frames, or the same high-rate performance guarantees.
|
|
20
34
|
|
|
21
|
-
Readonly XY-series telemetry follows the same page boundary. Use a standard canvas with one simple Vue array binding, for example `<canvas id="spectrum" :data-points="spectrum"></canvas>`, or a map container with `<div id="room-map" :data-coordinates="locations"></div>`.
|
|
35
|
+
Readonly XY-series telemetry follows the same page boundary. Use a standard canvas with one simple Vue array binding, for example `<canvas id="spectrum" :data-points="spectrum"></canvas>`, or a map container with `<div id="room-map" :data-coordinates="locations"></div>`. Use `[longitude, latitude]` for geographic coordinates or `[x, y]` for local planar coordinates; the page owns the projection. The page receives complete `[[x, y], ...]` snapshots; ControlPanel owns subscription, latest-frame replacement, stale handling, and reconnection. The page does not declare a device, stream, socket, or frame protocol. XY-series is independent from Meter and Volume: it never sends commands and does not represent a single scalar value.
|
|
22
36
|
|
|
23
37
|
Different XY-series streams arrive independently. Room delivery aggregates their latest snapshots, not synchronized samples from a shared acquisition frame. The public framework defaults to 2048 points per stream and allows a configured maximum of 8192; the receiver enforces the manifest's `maxPoints`. A panel supports at most 256 XY-series bindings. These limits do not guarantee throughput at every frame rate or concurrency level; validate performance in the target environment.
|
|
24
38
|
|
|
@@ -26,7 +40,9 @@ Vuetify adapters cover `v-slider`, `v-switch`, `v-checkbox`, `v-select`, `v-radi
|
|
|
26
40
|
|
|
27
41
|
Use paired Vuetify tags in Dashboard templates, such as `<v-slider></v-slider>`; self-closing component tags can absorb the following controls during Dashboard rendering and are rejected before deployment.
|
|
28
42
|
|
|
29
|
-
After clicking Apply in the page binding editor, the package validates the mapping, backs up the active Flow, and
|
|
43
|
+
After clicking Apply in the page binding editor, the package validates the mapping, backs up the active Flow, and updates the panel and generated native bridge through the runtime Flow API with deployment type `nodes`. Affected runtime nodes restart. Separately, when Automator reports the external Flow change, its editor listener reloads the browser page once: that listener does not import, merge, clear or deploy Flow data. It does not preserve undeployed canvas drafts, so handle those before Apply. The bridge revision must match the saved mapping before a command can reach a device. A Node-RED simulation verifies software behavior, while live device operation requires a separate check.
|
|
44
|
+
|
|
45
|
+
An unchanged Apply still validates the draft and current revision, but skips the backup and Flow write when the resulting nodes are identical, regardless of their array order. The binding page still reloads and restores its valid session. Editor configuration and device discovery load concurrently. Transient runtime/catalog failures show an error with Retry instead of treating valid authentication as expired; only a 401 response requests credentials again. Ordinary user-page session restoration follows the same distinction. Real updates still depend on the host's catalog, storage and node restart performance.
|
|
30
46
|
|
|
31
47
|
The current runtime has one panel-scoped control path. The retired topic-based region gateway, fixed demo scenes, hand-written control tags, and `sendControl` authoring API are not supported. The page editor and server use the same target compatibility rules.
|
|
32
48
|
|
|
@@ -40,4 +56,10 @@ Node.js 18.16.0 or newer is required. Dashboard 2 and the AMX Command/Status/Eve
|
|
|
40
56
|
|
|
41
57
|
The release includes `npm-shrinkwrap.json` to install the tested dependency versions. Deploy/Push must reference the intended registry version or a controller-accessible tarball; a `file:` dependency pointing at a Mac path is not portable to MUSE. Restart the controller runtime after replacing module code.
|
|
42
58
|
|
|
43
|
-
For development, run `
|
|
59
|
+
For development, run `npm ci --ignore-scripts` and `npm test` in this source directory with Node.js >=18.16. Tests need the local pinned dependencies; do not depend on another machine's installed Automator modules. The parent project's `local-validation/test-source.sh` also covers the Python framework, drivers, and offline Flow helpers. Its optional isolated runtime test uses a temporary user directory and random loopback port; it never deploys to the live Automator or MUSE.
|
|
60
|
+
|
|
61
|
+
Ordinary feedback, including native readonly numeric indicators, uses authenticated SSE; stream-backed Meter and XY-series use Dashboard Socket.IO. SSE feedback is serialized once per broadcast; a temporary backpressured write may drain, while a persistently blocked client is disconnected and reconnects to the latest snapshot rather than buffering history. Each panel allows at most 100 simultaneous SSE clients.
|
|
62
|
+
|
|
63
|
+
Realtime fan-out uses one volatile namespace broadcast to explicit authenticated socket IDs when the Dashboard socket exposes that API; other hosts keep the per-socket fallback. This avoids repeated server encoding without changing frame formats, authoring syntax, authorization or each viewer's network traffic. Shared streams are checked once when deciding whether a panel needs a new frame. Browser control lookup uses a DOM index invalidated on structure or binding-key changes; unrelated DOM mutations do not replay an XY array into the same model. Target recreation still restores the latest snapshot.
|
|
64
|
+
|
|
65
|
+
The parent project's `local-validation/stress-panel.cjs` adds opt-in multi-room, maximum-point, multi-viewer and SSE pressure scenarios. These use real HTTP/Socket.IO with simulated DOM/animation frames, not live Automator, MUSE or actual browser paint; target-device capacity must be verified separately.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { visualRoutes } = require('./panel-visual-bindings')
|
|
2
|
+
const { switchMapping } = require('./control-type-model')
|
|
2
3
|
|
|
3
4
|
function configuredRoutes (config) {
|
|
4
5
|
return visualRoutes(config)
|
|
@@ -11,7 +12,9 @@ function resolveBoundCommand (routes, message) {
|
|
|
11
12
|
}
|
|
12
13
|
const route = routes.get(request.id)
|
|
13
14
|
if (!route) throw new Error(`Unknown control ID: ${request.id}`)
|
|
14
|
-
if (route.control.mode === 'read'
|
|
15
|
+
if (route.control.mode === 'read' || ['meter', 'xy-series'].includes(route.control.type)) {
|
|
16
|
+
throw new Error(`Read-only control: ${request.id}`)
|
|
17
|
+
}
|
|
15
18
|
if (!route.binding?.device || !route.binding.writePath) {
|
|
16
19
|
throw new Error(`Incomplete command binding: ${request.id}`)
|
|
17
20
|
}
|
|
@@ -41,12 +44,15 @@ function resolveBoundCommand (routes, message) {
|
|
|
41
44
|
if (type === 'text' && (typeof value !== 'string' || value.length > 4096)) {
|
|
42
45
|
throw new Error('Text value must be a string under 4096 characters')
|
|
43
46
|
}
|
|
47
|
+
const mapping = ['mute', 'power'].includes(type) && metadata?.booleanMapping
|
|
48
|
+
? switchMapping(metadata, metadata.booleanMapping) : null
|
|
49
|
+
const payload = mapping ? (value ? mapping.trueValue : mapping.falseValue)
|
|
50
|
+
: ['mute', 'power'].includes(type) && metadata?.booleanEncoding === 'integer01'
|
|
51
|
+
? (value ? 1 : 0) : value
|
|
44
52
|
if (Array.isArray(metadata?.enums) && metadata.enums.length &&
|
|
45
|
-
!metadata.enums.some(entry => Object.is(entry,
|
|
53
|
+
!metadata.enums.some(entry => Object.is(entry, payload) || Object.is(entry?.value, payload))) {
|
|
46
54
|
throw new Error('Value is not a permitted device option')
|
|
47
55
|
}
|
|
48
|
-
const payload = ['mute', 'power'].includes(type) && metadata?.booleanEncoding === 'integer01'
|
|
49
|
-
? (value ? 1 : 0) : value
|
|
50
56
|
return {
|
|
51
57
|
kind: 'bound-command',
|
|
52
58
|
controlId: request.id,
|
|
@@ -66,15 +72,27 @@ function resolveBoundFeedback (routes, message) {
|
|
|
66
72
|
const matches = []
|
|
67
73
|
for (const [id, route] of routes) {
|
|
68
74
|
if (route.control.mode === 'write' || route.binding?.device !== message.device ||
|
|
69
|
-
route.binding.readPath !== message.path || route.binding.readMeta?.meter
|
|
75
|
+
route.binding.readPath !== message.path || route.binding.readMeta?.meter ||
|
|
76
|
+
route.binding.readMeta?.xySeries) continue
|
|
70
77
|
const type = route.control.type
|
|
71
78
|
const metadata = route.binding.readMeta
|
|
72
79
|
let value = message.payload
|
|
73
80
|
if (['mute', 'power'].includes(type)) {
|
|
74
|
-
if (
|
|
81
|
+
if (metadata?.booleanMapping) {
|
|
82
|
+
const mapping = switchMapping(metadata, metadata.booleanMapping)
|
|
83
|
+
// Integer/float Event nodes can deliver numeric values as strings.
|
|
84
|
+
if (['integer', 'float'].includes(metadata.type) && typeof value === 'string' && value.trim()) value = Number(value)
|
|
85
|
+
if (metadata.type === 'boolean') {
|
|
86
|
+
if (value === 'true' || value === 1 || value === '1') value = true
|
|
87
|
+
else if (value === 'false' || value === 0 || value === '0') value = false
|
|
88
|
+
}
|
|
89
|
+
if (Object.is(value, mapping.trueValue)) value = true
|
|
90
|
+
else if (Object.is(value, mapping.falseValue)) value = false
|
|
91
|
+
else throw new Error(`Invalid ${type} feedback: ${id}`)
|
|
92
|
+
} else if (value === 'true' || value === 1 || value === '1') value = true
|
|
75
93
|
else if (value === 'false' || value === 0 || value === '0') value = false
|
|
76
94
|
if (typeof value !== 'boolean') throw new Error(`Invalid ${type} feedback: ${id}`)
|
|
77
|
-
} else if (['level', 'dimmer', 'position'].includes(type)) {
|
|
95
|
+
} else if (['level', 'dimmer', 'position', 'meter'].includes(type)) {
|
|
78
96
|
if (typeof value === 'string' && value.trim() !== '') value = Number(value)
|
|
79
97
|
if (typeof value !== 'number' || !Number.isFinite(value) ||
|
|
80
98
|
(metadata?.type === 'integer' && !Number.isInteger(value)) ||
|
package/control-catalog.js
CHANGED
|
@@ -2,6 +2,7 @@ const { schemaVersion } = require('./control-type-contract.json')
|
|
|
2
2
|
const { runtimeJsonRequest } = require('./panel-http')
|
|
3
3
|
const { meterRoute } = require('./meter-route')
|
|
4
4
|
const { xySeriesRoute } = require('./xy-series-route')
|
|
5
|
+
const { isMeterTarget } = require('./control-type-model')
|
|
5
6
|
|
|
6
7
|
function catalogBase (settings) {
|
|
7
8
|
const port = Number(settings?.uiPort)
|
|
@@ -58,7 +59,10 @@ function inferredControl (path, properties, type, min, max, enums) {
|
|
|
58
59
|
if (type === 'integer' && /\/(preset|source)$/.test(path.toLowerCase())) {
|
|
59
60
|
return { controlType: 'source' }
|
|
60
61
|
}
|
|
61
|
-
if (['integer', 'float'].includes(type))
|
|
62
|
+
if (['integer', 'float'].includes(type)) {
|
|
63
|
+
return { controlType: isMeterTarget({ type, access: properties['.access'], min, max, enums })
|
|
64
|
+
? 'meter' : 'slider' }
|
|
65
|
+
}
|
|
62
66
|
if (['string', 'dstring'].includes(type)) return { controlType: 'text' }
|
|
63
67
|
return { controlType: '' }
|
|
64
68
|
}
|
|
@@ -77,6 +81,7 @@ async function listCatalogTargets (base, device, fetcher = fetch) {
|
|
|
77
81
|
const enums = properties['.enums']
|
|
78
82
|
const meter = properties['.metadata']?.meter ? meterRoute(properties['.metadata'].meter) : null
|
|
79
83
|
const xySeries = properties['.metadata']?.xySeries ? xySeriesRoute(properties['.metadata'].xySeries) : null
|
|
84
|
+
const availabilityRole = properties['.metadata']?.availabilityRole
|
|
80
85
|
return {
|
|
81
86
|
path: item.path,
|
|
82
87
|
kind: properties['.kind'] || '',
|
|
@@ -89,7 +94,9 @@ async function listCatalogTargets (base, device, fetcher = fetch) {
|
|
|
89
94
|
controlSchemaVersion: schemaVersion,
|
|
90
95
|
unit: properties['.metadata']?.unit || '',
|
|
91
96
|
...(meter ? { meter } : {}),
|
|
92
|
-
...(xySeries ? { xySeries } : {})
|
|
97
|
+
...(xySeries ? { xySeries } : {}),
|
|
98
|
+
...(['online', 'ready'].includes(availabilityRole) && type === 'boolean' &&
|
|
99
|
+
properties['.access'] === 'ro' ? { availabilityRole } : {})
|
|
93
100
|
}
|
|
94
101
|
})
|
|
95
102
|
const unique = new Map()
|
|
@@ -100,7 +107,23 @@ async function listCatalogTargets (base, device, fetcher = fetch) {
|
|
|
100
107
|
}
|
|
101
108
|
unique.set(target.path, target)
|
|
102
109
|
}
|
|
103
|
-
|
|
110
|
+
const result = [...unique.values()]
|
|
111
|
+
// Explicit semantic metadata, never brand names or guesses such as "online".
|
|
112
|
+
// The nearest physical-device ancestor wins over any aggregate/root alias.
|
|
113
|
+
const scopes = new Map()
|
|
114
|
+
for (const point of result.filter(target => target.availabilityRole)) {
|
|
115
|
+
const scope = point.path.slice(0, point.path.lastIndexOf('/') + 1)
|
|
116
|
+
if (!scopes.has(scope)) scopes.set(scope, {})
|
|
117
|
+
const roles = scopes.get(scope)
|
|
118
|
+
if (roles[point.availabilityRole]) throw Error('Ambiguous device availability scope: ' + scope)
|
|
119
|
+
roles[point.availabilityRole] = { path: point.path, onebased: point.onebased }
|
|
120
|
+
}
|
|
121
|
+
for (const target of result) {
|
|
122
|
+
const scope = [...scopes.keys()].filter(prefix => target.path.startsWith(prefix) && scopes.get(prefix).online)
|
|
123
|
+
.sort((a, b) => b.length - a.length)[0]
|
|
124
|
+
if (scope) target.availability = scopes.get(scope)
|
|
125
|
+
}
|
|
126
|
+
return result
|
|
104
127
|
}
|
|
105
128
|
|
|
106
129
|
module.exports = { catalogBase, listCatalogDevices, listCatalogTargets }
|
package/control-panel.html
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
function c_ (x) {
|
|
5
5
|
return RED._('@flowfuse/node-red-dashboard/ui-template:ui-template.' + x)
|
|
6
6
|
}
|
|
7
|
-
// TODO: move this to util.js for reuse & unit testing
|
|
8
7
|
function hasProperty (obj, prop) {
|
|
9
8
|
return Object.prototype.hasOwnProperty.call(obj, prop)
|
|
10
9
|
}
|
|
@@ -346,20 +345,12 @@
|
|
|
346
345
|
})()
|
|
347
346
|
</script>
|
|
348
347
|
|
|
349
|
-
<style>
|
|
350
|
-
.control-binding-hint { margin: 4px 0; color: var(--red-ui-secondary-text-color, #666); overflow-wrap: anywhere; }
|
|
351
|
-
.control-binding-bridge-option { display: flex; align-items: flex-start; gap: 8px; }
|
|
352
|
-
.control-binding-bridge-option > label { flex: 0 0 105px; }
|
|
353
|
-
.control-binding-bridge-option > div { flex: 1; min-width: 0; }
|
|
354
|
-
.control-binding-bridge-option small { display: block; margin-top: 3px; }
|
|
355
|
-
</style>
|
|
356
|
-
|
|
357
348
|
<script type="text/html" data-help-name="ui-control-panel">
|
|
358
349
|
<p>This node renders a custom Vue Template and automatically injects control traffic and reported-device-state APIs.</p>
|
|
359
350
|
<p>普通 HTML/Vue 控件由页面绑定编辑器自动识别,并通过面板的 authenticated session 发送控制。开发者不需要添加标签、调用自定义节点 API,或在 Template 中实现 throttling、latest-wins、队列和设备协议。</p>
|
|
360
351
|
<p>页面只需按普通 H5/Vue 方式编写控件;编辑模式下在控件上打开绑定菜单,选择设备参数即可。登录、发送、反馈和状态同步由节点负责,Template 不应主动查询设备或把反馈回发成命令。</p>
|
|
361
352
|
<p>See <code>CONTROL_PANEL_AI_GUIDE.md</code> in the node package for the complete AI authoring contract and examples.</p>
|
|
362
|
-
<p>Save and Deploy the panel, then use Open binding editor. Apply
|
|
353
|
+
<p>Save and Deploy the panel, then use Open binding editor. Apply validates bindings, backs up the Flow, and updates the panel and native bridge through the runtime Flow API. When Node-RED reports a newer revision, the Automator editor page reloads once; that editor listener never imports nodes or clicks Deploy. Ordinary Deploy alone does not apply a binding draft. Check targets and numbering before using real hardware.</p>
|
|
363
354
|
</script>
|
|
364
355
|
|
|
365
356
|
<script type="text/html" data-template-name="ui-control-panel">
|
|
@@ -377,48 +368,40 @@
|
|
|
377
368
|
<option value="widget:ui" data-i18n="ui-template.label.widget-ui"></option>
|
|
378
369
|
<option value="site:style" data-i18n="ui-template.label.site-style"></option>
|
|
379
370
|
<option value="page:style" data-i18n="ui-template.label.page-style"></option>
|
|
380
|
-
<!-- FUTURE? <option value="site:script" data-i18n="ui-template.label.site-script"></option>
|
|
381
|
-
<option value="page:script" data-i18n="ui-template.label.page-script"></option> -->
|
|
382
371
|
</select>
|
|
383
372
|
</div>
|
|
384
373
|
<div id="template-row-ui" class="form-row">
|
|
385
|
-
<label for="node-input-ui"><i class="fa fa-bookmark"></i> <span data-i18n="ui-template.label.ui"></label>
|
|
374
|
+
<label for="node-input-ui"><i class="fa fa-bookmark"></i> <span data-i18n="ui-template.label.ui"></span></label>
|
|
386
375
|
<input type="text" id="node-input-ui">
|
|
387
376
|
</div>
|
|
388
377
|
<div id="template-row-page" class="form-row">
|
|
389
|
-
<label for="node-input-page"><i class="fa fa-bookmark"></i> <span data-i18n="ui-template.label.page"></label>
|
|
378
|
+
<label for="node-input-page"><i class="fa fa-bookmark"></i> <span data-i18n="ui-template.label.page"></span></label>
|
|
390
379
|
<input type="text" id="node-input-page">
|
|
391
380
|
</div>
|
|
392
381
|
<div id="template-row-group" class="form-row">
|
|
393
|
-
<label for="node-input-group"><i class="fa fa-table"></i> <span data-i18n="ui-template.label.group"></label>
|
|
382
|
+
<label for="node-input-group"><i class="fa fa-table"></i> <span data-i18n="ui-template.label.group"></span></label>
|
|
394
383
|
<input type="text" id="node-input-group">
|
|
395
384
|
</div>
|
|
396
385
|
<div class="form-row nr-db-ui-element-hide-in-subflow">
|
|
397
|
-
<label><i class="fa fa-object-group"></i> <span data-i18n="ui-base.label.size">Size</label>
|
|
386
|
+
<label><i class="fa fa-object-group"></i> <span data-i18n="ui-base.label.size">Size</span></label>
|
|
398
387
|
<button class="editor-button" id="node-input-size"></button>
|
|
399
388
|
</div>
|
|
400
389
|
<div class="form-row nr-db-ui-element-show-in-subflow">
|
|
401
|
-
<label><i class="fa fa-arrows-h"></i> <span data-i18n="ui-base.label.width">Width</label>
|
|
390
|
+
<label><i class="fa fa-arrows-h"></i> <span data-i18n="ui-base.label.width">Width</span></label>
|
|
402
391
|
<input type="hidden" id="node-input-width">
|
|
403
392
|
</div>
|
|
404
393
|
<div class="form-row nr-db-ui-element-show-in-subflow">
|
|
405
|
-
<label><i class="fa fa-arrows-v"></i> <span data-i18n="ui-base.label.height">Height</label>
|
|
394
|
+
<label><i class="fa fa-arrows-v"></i> <span data-i18n="ui-base.label.height">Height</span></label>
|
|
406
395
|
<input type="hidden" id="node-input-height">
|
|
407
396
|
</div>
|
|
408
397
|
<div class="form-row nr-db-ui-element-show-in-subflow">
|
|
409
|
-
<label><i class="fa fa-arrows"></i> <span data-i18n="ui-base.label.order">Order</label>
|
|
398
|
+
<label><i class="fa fa-arrows"></i> <span data-i18n="ui-base.label.order">Order</span></label>
|
|
410
399
|
<input type="text" id="node-input-order">
|
|
411
400
|
</div>
|
|
412
|
-
<!--<div class="form-row" id="template-row-size">
|
|
413
|
-
<label><i class="fa fa-object-group"></i> <span data-i18n="ui-template.label.size"></span></label>
|
|
414
|
-
<input type="hidden" id="node-input-width">
|
|
415
|
-
<input type="hidden" id="node-input-height">
|
|
416
|
-
<button class="editor-button" id="node-input-size"></button>
|
|
417
|
-
</div>-->
|
|
418
401
|
<div class="form-row">
|
|
419
|
-
<label for="node-input-className"><i class="fa fa-code"></i> <span data-i18n="ui-template.label.className"></label>
|
|
402
|
+
<label for="node-input-className"><i class="fa fa-code"></i> <span data-i18n="ui-template.label.className"></span></label>
|
|
420
403
|
<div style="display: inline;">
|
|
421
|
-
<input style="width: 70%;" type="text" id="node-input-className" data-i18n="[placeholder]ui-template.label.classNamePlaceholder"
|
|
404
|
+
<input style="width: 70%;" type="text" id="node-input-className" data-i18n="[placeholder]ui-template.label.classNamePlaceholder">
|
|
422
405
|
<a
|
|
423
406
|
data-html="true"
|
|
424
407
|
title="Dynamic Property: Send msg.class to append new classes to this widget. NOTE: classes set at runtime will be applied in addition to any class(es) set in the nodes class field."
|
|
@@ -480,13 +463,4 @@
|
|
|
480
463
|
<label for="node-input-passthru" style="width:70%;"> <span data-i18n="ui-template.label.pass-through"></span></label>
|
|
481
464
|
</div>
|
|
482
465
|
</div>
|
|
483
|
-
<!--<div class="form-row" style="margin-bottom:0px;">
|
|
484
|
-
<input type="checkbox" id="node-input-storeOutMessages" style="display:inline-block; margin-left:8px; width:auto; vertical-align:top;">
|
|
485
|
-
<label for="node-input-storeOutMessages" style="width:70%;"> <span data-i18n="ui-template.label.store-state"></span></label>
|
|
486
|
-
</div>
|
|
487
|
-
<div class="form-row" style="margin-bottom:0px;">
|
|
488
|
-
<input type="checkbox" id="node-input-resendOnRefresh" style="display:inline-block; margin-left:8px; width:auto; vertical-align:top;">
|
|
489
|
-
<label for="node-input-resendOnRefresh" style="width:70%;"> <span data-i18n="ui-template.label.resend"></span></label>
|
|
490
|
-
</div>
|
|
491
|
-
</div> -->
|
|
492
466
|
</script>
|