# 工程 JSON 1.0 格式说明 ## 格式策略 当前开发阶段只有一个有效工程格式:`1.0`。代码直接读写本文描述的结构,不为早期开发草稿增加同版本默认值、字段推断、迁移分支或双写逻辑。缺少必填字段、字段类型错误、引用不存在或领域校验失败时,加载必须失败且不得替换当前工程。 ## 顶层结构 ```json { "formatVersion": "1.0", "id": "project-1", "name": "包装线", "hmiPages": [], "initialHmiPageId": "", "alarmDefinitions": [], "registerComments": [], "controlLogics": [] } ``` 所有顶层字段必填。空工程允许 `hmiPages`、`alarmDefinitions`、`registerComments` 和 `controlLogics` 为空,此时 `initialHmiPageId` 必须为空。存在页面时,`initialHmiPageId` 必须引用 `hmiPages` 中的页面 ID。 `hmiPages` 与 `controlLogics` 都是有序数组。页面顺序用于工程树显示;控制逻辑顺序是离线扫描顺序。对象 ID 稳定且唯一,名称可修改且在同类对象中唯一。 编辑器的当前页面 ID 和当前逻辑 ID 是主窗口会话状态,不进入工程 JSON。每次进入运行态时,HMI 从 `initialHmiPageId` 重新启动。 ## HMI 页面与控件 ```json { "id": "page-1", "name": "主操作页面", "width": 800, "height": 400, "controls": [] } ``` 每个控件都要求 `id`、`type`、`bounds`、`text`、`binding` 和 `properties`。`bounds` 包含整数 `x`、`y`、`width`、`height`,矩形必须完整位于页面内。`binding` 为寄存器对象或 JSON `null`。 页面尺寸是当前 PC HMI 编辑器的业务尺寸,宽度必须为 `320~1600`,高度必须为 `200~800`。默认页面尺寸为 `800×400`;超出范围的 JSON 页面直接加载失败。 `properties` 保存控件外观扩展属性。当前保留以下 HMI 外观属性名: | 属性名 | 值格式 | 说明 | | --- | --- | --- | | `textColor` | `#RRGGBB` 字符串 | 控件普通文字颜色,缺失时使用控件默认颜色 | | `fontSize` | `6~72` 的十进制数字字符串 | 控件文字字号 | | `fontBold` | `true` 或 `false` 字符串 | 是否使用粗体 | | `fontItalic` | `true` 或 `false` 字符串 | 是否使用斜体 | 字体和颜色属于静态显示属性,不参与 M/D 绑定、PLC 轮询和离线控制逻辑。未知扩展属性仍按字符串保留。 | `type` | 绑定 | 额外必填字段 | | --- | --- | --- | | `button` | M 地址或 `null` 草稿 | `buttonOperation` | | `indicator` | M 地址或 `null` 草稿 | 无 | | `numericDisplay` | D 地址或 `null` 草稿 | 无 | | `numericInput` | D 地址或 `null` 草稿 | 无 | | `label` | 必须为 `null` | 无 | | `pageJump` | 必须为 `null` | `targetPageId` | | `alarmList` | 必须为 `null` | 无 | `type` 只接受表中列出的控件类型。其他类型按严格 `1.0` 规则直接拒绝加载,不做兼容迁移。 按钮操作字符串为 `setOn`、`setOff`、`toggle` 或 `momentaryOn`。PageJump 使用目标页面稳定 ID,不使用名称或数组位置: ```json { "id": "page-jump-1", "type": "pageJump", "bounds": {"x": 20, "y": 20, "width": 120, "height": 40}, "text": "参数设置", "binding": null, "properties": {}, "targetPageId": "page-2" } ``` 编辑草稿允许 `targetPageId` 为空字符串,但进入运行态前必须配置为现存页面 ID。任何页面被 PageJump 引用时不得删除。 AlarmList 只负责显示项目级当前报警,不保存独立触发逻辑,也不直接绑定单个寄存器。编辑界面的标题输入上限为 12 个字符。多个页面放置 AlarmList 时共享同一组当前报警和确认状态。运行态没有报警时控件隐藏;有报警时按当前页的实际记录数收缩高度,每页最多显示 5 条。超过 5 条时通过表头翻页按钮查看,记录不会被丢弃。标题和报警文本按单行绘制,空间不足时显示省略号。报警恢复后对应记录立即移除,最后一条报警恢复后控件再次隐藏。 ## 报警定义 ```json { "id": "alarm-1", "address": {"area": "M", "index": 0}, "condition": "mOn", "threshold": 0, "message": "急停已按下" } ``` `alarmDefinitions` 是项目级有序数组,报警 ID 必须唯一。触发条件只包含当前业务需要的三种: | `condition` | 地址区域 | 触发规则 | | --- | --- | --- | | `mOn` | M | 位值为 ON | | `dHigh` | D | 字值大于等于 `threshold` | | `dLow` | D | 字值小于等于 `threshold` | `threshold` 使用有符号 16 位范围;M 报警仍保留该必填字段但不参与判断。报警文本不能为空,编辑界面的输入上限为 20 个字符。运行界面按单行绘制,超出当前消息列宽度时显示省略号。发生时间、活动状态、确认状态和清除时间属于运行会话,不进入工程 JSON。 ## 地址注释与网络注释 `registerComments` 是工程级 M/D 地址元数据数组。每个元素必须包含有效的 `address` 和非空 `text`,同一地址只能出现一次。软元件注释只能使用单行文本,不能包含回车或换行,且不能超过 64 个 UTF-8 字节: ```json { "address": {"area": "M", "index": 0}, "text": "启动按钮" } ``` 注释不保存寄存器当前值,也不自动把地址加入 Modbus 轮询。网络注释属于 `LadderRung`,与网络 ID、名称、条件和输出并列保存。网络注释允许为空;非空时只能使用单行文本,不能包含回车或换行,且不能超过 128 个 UTF-8 字节: ```json { "id": "rung-1", "name": "启动网络", "comment": "启动按钮接通后延时启动", "condition": null, "output": null } ``` ## 控制逻辑 ```json { "id": "logic-1", "name": "主控制逻辑", "enabled": true, "rungs": [] } ``` `enabled` 决定离线执行器是否扫描该逻辑。所有启用逻辑按 `controlLogics` 数组顺序执行;禁用逻辑仍要求 ID、名称、网络和表达式结构合法,但允许保留未完成节点作为草稿。 控制逻辑初始可以没有网络,`rungs` 为空不是错误。编辑器第一次添加条件、横线或输出时才创建网络;删除最后一个网络后可以再次保存为空逻辑。没有输出的空网络草稿允许 `condition` 和 `output` 同时为 `null`。存在输出时,`condition` 不能为空,并且必须显式占满前 10 列。 网络、`Node / Wire / Gap / Series / Parallel` 条件表达式、M 触点、M 边沿、D 比较、M 线圈和字操作指令字段沿用当前结构化梯形图模型。条件区中每一段可见、可编辑横线都保存为恒真的 `Wire` 叶子: ```json {"id": "wire-1", "kind": "wire", "columnSpan": 2} ``` 明确断开的连续空白网格保存为 `Gap` 叶子: ```json {"id": "gap-1", "kind": "gap", "columnSpan": 1} ``` `Wire` 和 `Gap` 的 `columnSpan` 都是必填整数,范围为 `1~10`,表示占用的条件区逻辑网格列数,不是像素长度。删除一格横线或触点时,该格改为 `Gap(1)`;多格横线只断开被删除的格,其余部分仍是 `Wire`。`Gap` 可以保存和重新加载,但运行校验必须拒绝包含 `Gap` 的启用网络。 单个网络前 10 列是条件区,第 11 列固定放输出指令。存在输出时,前 10 列必须由 `Node / Wire / Gap` 精确占满;直接添加输出保存为 `Wire(10) + Output`,一个触点后添加输出保存为 `Node + Wire(9) + Output`。并联支路必须显式等宽,短支路的补线也保存为真实 `Wire`。左右母线、节点和输出自身端子、并联竖线不保存为独立对象,而是由表达式结构派生。画布像素坐标、自由连线和运行轨迹是 UI 或运行会话投影,不进入 JSON;横线和断路不绑定寄存器,也不产生 M/D 轮询地址。节点配置的类型和值域如下: ```json {"type": "contact", "address": {"area": "M", "index": 0}, "mode": "normallyOpen"} {"type": "edgeContact", "address": {"area": "M", "index": 0}, "mode": "rising"} {"type": "edgeContact", "address": {"area": "M", "index": 0}, "mode": "falling"} {"type": "compare", "address": {"area": "D", "index": 0}, "comparison": "greaterThanOrEqual", "value": 10} {"type": "coil", "address": {"area": "M", "index": 1}, "mode": "normal"} {"type": "move", "source": {"kind": "register", "address": {"area": "D", "index": 1}}, "destination": {"area": "D", "index": 2}} {"type": "arithmetic", "operation": "add", "left": {"kind": "register", "address": {"area": "D", "index": 2}}, "right": {"kind": "constant", "value": 1}, "destination": {"area": "D", "index": 2}} ``` 当前 `LogicNodeConfig` 只接受 `contact`、`edgeContact`、`coil`、`compare`、`move` 和 `arithmetic`。`timerContact`、`counterContact`、`ton`、`counter` 等未支持类型会使工程加载失败,不会被忽略或自动迁移。触点模式为 `normallyOpen / normallyClosed`,边沿模式为 `rising / falling`,线圈模式为 `normal / set / reset`;比较运算支持 `equal / notEqual / lessThan / lessThanOrEqual / greaterThan / greaterThanOrEqual`,算术运算使用 `add / subtract`。上升沿和下降沿在离线执行器中产生一次扫描脉冲。 MOVE、ADD 和 SUB 只能把结果写入 D 地址,源操作数和算术左右操作数可以是有符号 16 位常量或 D 地址。指令仅在所在网络条件成立时执行;ADD/SUB 使用更宽中间类型计算,结果超出 `-32768~32767` 时饱和到对应边界,并在离线运行轨迹中记录 `overflow: true`,不会中止扫描。 ## 校验与读写 保存顺序为领域校验、JSON 序列化、`QSaveFile` 原子提交。加载顺序为 JSON 语法、`formatVersion`、必填字段和类型、领域关系校验,全部成功后才替换当前工程。 工程保存允许未绑定的寄存器控件和未完成梯形图草稿。进入离线运行还必须通过运行校验:所有 HMI 寄存器控件完成绑定、所有 PageJump 目标有效、所有报警定义合法、所有启用逻辑包含输出指令,并且条件区中不存在 `Gap`;禁用草稿逻辑不阻止运行。左侧电源线直接接通的输出必须保存为 `Wire(10) + Output`,不能省略 `condition`。 当前实现只读写本文定义的严格 `1.0`。早期同为 `1.0`、但依赖隐式输出连线或隐式并联补线的草稿不会自动迁移,结构不符合当前领域校验时直接加载失败。