16 · 自动化 API 方法速查

相关:第 15 章 自动化 API 总览 · 第 19 章 账号、角色与权限

这一章是什么

第 15 章讲了自动化 API 是干什么的、怎么打开工程。这一章是完整的方法清单 —— IProjectAutomation 一共 132 个方法(另有 6 个实现类辅助成员 / 异步方法,见下表标注;合计 138 条),按"你大概想干什么"分成 16 组列在这里。

用法:先在左边找到你的需求(比如"我要批量建点"),再跳到对应分组看方法名和参数。 每个方法都写了"干什么"和"什么时候用",不用去翻源码。

所有方法都返回 AutomationResult({ Ok, Message, Data }),失败不抛异常。 所以每次调用都要看 Ok,别假设一定成功。详见第 15 章。

16 组方法一览

分组 方法数 一句话
1. 打开与生命周期 5 打开/新建/切换工程,挂编译器
2. 感知查询 14 只读:看看工程里有什么
3. 画面 5 建/删/改名/复制画面、改尺寸
4. 元素 17 加/删/移动控件、绑定、改属性、分组对齐
5. 点表 9 加点、内部变量、窗口变量、导入导出
6. 通道与设备 8 通道、设备、连接参数、只读诊断
7. 脚本组织 14 全局/画面/元素脚本、脚本模块
8. 报警配置 5 按点配四级限值
9. 历史记录 5 按点配记录规则
10. 配方 10 配方增删改、导入导出
11. 报表 5 历史聚合/报警事件报表定义 + 查询
12. 多语言 12 译表、抽取、CSV 导入导出
13. 工程设置 15 启动配置、发布信息、通知配置、外观预设、皮肤
14. 持久化与整包 5 校验、保存、导出/导入、加密封包
15. 脚本编译与函数目录 3 只编不跑、编译整个工程、函数目录
16. 北向数据转发 6 出站推送 / 入站监听回写规则

1. 打开与生命周期

什么时候用:程序刚起来、要接上一个工程的时候。这一步不做,后面所有方法都用不了。

方法 干什么
ProjectAutomation.Open(dir) 静态 打开工程目录,返回实例。缺 project.json 会抛异常(不是返回失败)
CreateBlank(dir, name) 静态 在空目录里落一份最小空白工程。已有工程则拒绝,绝不覆盖
LoadProject(dir) 运行中换工程。目录里没有 project.json 就拒绝
CreateProject(dir, name) 运行中新建空工程并切过去。目录已有工程则拒绝
AttachCompiler(compiler) 注入脚本编译校验器,打开工程后必须调一次,否则 CheckScript/BuildScripts 都返回"编译器未接入"
using Hmi.Automation;

// ① 打开(这一步会抛异常,包 try)
ProjectAutomation auto;
try { auto = ProjectAutomation.Open(@"D:\我的工程"); }
catch (Exception ex) { Console.WriteLine($"打不开:{ex.Message}"); return; }

// ② 标记来源(进审计,方便日后查是谁改的)
auto.Source = "MyTool";

// ③ 挂编译器(不挂的话脚本校验用不了)
auto.AttachCompiler(new RoslynScriptChecker(auto.Loaded));

// ④ 干完活保存
var r = auto.Save();
if (!r.Ok) Console.WriteLine($"保存失败:{r.Message}");

ProjectDir / Loaded / ProjectSwitched / Audit 这四个成员不在接口里,但实现类 ProjectAutomation 上有: 当前工程目录、内存工程实例、换工程事件、审计记录。

换工程时必须重挂编译器

LoadProject / CreateProject 成功后会触发 ProjectSwitched 事件,并把内部编译器置空 (因为旧编译器指向的是旧工程)。宿主收到事件后要重新 AttachCompiler:

auto.ProjectSwitched += dir =>
{
    auto.AttachCompiler(new RoslynScriptChecker(auto.Loaded));   // 用新工程重挂
    Console.WriteLine($"已切到工程:{dir}");
};

2. 感知查询(只读)

什么时候用:想知道"这个工程里有什么" —— 有哪些画面、哪些点、哪些通道、支持哪些协议。 这一组全部只读,不产生任何副作用,可以放心随便调。

方法 返回什么
GetProject() 工程元信息:名/版本/启动画面/说明
ListScreens() 所有画面名 + 尺寸/元素数概览
GetScreen(name) 一个画面的完整模型
ListElements(screen) 画面里的元素:名/类型/几何/绑定数
ListTags() 所有点位定义
GetTag(name) 一个点的定义
ListTagTypes() 可选的点数据类型 + 每种类型哪些字段有意义(占几寄存器/字节序是否有意义/是否可配量程)
ListChannels() 所有通道/设备
ListProtocols() 可用协议(内置 + 已加载插件)+ 地址语法提示
DescribeProtocol(protocolId) 一个协议支持哪些连接方式 + 各自参数清单
DescribeElementProps(type, elementName?) 某元素类型的可写属性清单(名/类型/当前值/说明)
DescribeTypeSchema(typeName) 查嵌套复杂类型的字段结构(名/类型/枚举合法值/默认值/是否必填)。DescribeElementProps 只照到元素顶层属性,这里补它照不到的(Button.Action / Trend.Series / RealtimeTable.Columns / NavBar.Items / ScriptFragment / 报警限值项)
ListElementTypes() 所有可创建的元件类型(46 种)+ 中文名/分类/一句话用途。含 ListBox / Table / RealtimeTable 这类现成控件 —— 别再用矩形拼列表
GetProjectTree() 结构化「工程树」总览:画面→元素、点表规模、通道→设备、配方、报警/历史条数、报表、脚本组织、多语言。给 AI「先鸟瞰再钻某一枝」用(逐点/逐元素细节仍走各自的 list_/get_)

ListTagTypes 是干什么的

给"新建点"向导用的。你不需要在前端硬编码"浮点占 2 个寄存器、布尔占 1 位"这类规则 —— 调它,后端会把每种类型的字段语义告诉你,前端照着渲染就行。后端加新类型,前端零改动。

DescribeProtocol 是干什么的

给"设备向导"用的。力控式流程是:选协议 → 选网口/串口 → 按参数清单生成表单。 DescribeProtocol("ModbusTcp") 会告诉你"支持 TCP,需要 host 和 port 两个参数"。 没声明连接方式的协议,兜底成"仅 TCP(host+port)"。

DescribeElementProps —— 改属性前必须先调它

这是最容易踩的坑:SetElementProps 是按属性名反射写入的,名字写错了不会报错,直接静默跳过。

用户 2026-09-23 实际报过这个错:

set_element_props → 失败:FontWeight(该元素无此可写属性);TextAlignment(…)

正确的名字是 Bold 和 TextAlign(不是 FontWeight / TextAlignment)。

// ✅ 正确流程:先拿真名,再写
var props = auto.DescribeElementProps("Button", "启动按钮");
// → Data 里列出:Text / Background / Foreground / Bold / FontSize / Action ...

auto.SetElementProps("主画面", "启动按钮", new Dictionary<string, object?>
{
    ["Text"] = "开始",
    ["Bold"] = true,
});

⚠ 大小写差异是合法的(比较时忽略大小写),所以 Fontsize 也能用。 但"词不一样"(FontWeight ≠ Bold)就是错的。

现在写错的报错会指到正确的名字上,比以前只回一句"没有这个属性"好多了。


3. 画面

什么时候用:要程序化地建画面、删画面、改画面尺寸。

方法 干什么
CreateScreen(name, width=1280, height=720) 新建画面
DeleteScreen(name) 删画面
RenameScreen(oldName, newName) 改名
SetScreenProps(name, width?, height?, background?, gridSize?, isMaster?, master?) 改画面属性。传 null 的字段不动
DuplicateScreen(srcName, newName) 复制整张画面(深拷贝控件树 + 属性/绑定/事件),连源画面的窗口变量一并复制到新名下并改写绑定
auto.CreateScreen("监控总览", 1920, 1080);
auto.SetScreenProps("监控总览", background: "#1E1E1E", gridSize: 10);
auto.RenameScreen("监控总览", "总览");

isMaster / master 是母版画面相关:母版画面上的元素会出现在所有引用它的画面上。


4. 元素

什么时候用:在画面上摆控件、改控件外观、把控件绑到点上。

方法 干什么
AddElement(screen, type, name, x, y, w, h) 加元素
RemoveElement(screen, name) 删元素
MoveElement(screen, name, x?, y?, w?, h?, rotation?) 改几何。传 null 的不动
SetElementProps(screen, name, props) 改非几何属性(文本/颜色/字号/动作…)
SetBinding(screen, name, property, tag, format?, when?, map?, twoWay?) 设/改一条绑定(按 property 覆盖同名绑定)
RemoveBinding(screen, name, property) 删一条绑定
ApplyStyle(screen, name, presetId) 套预置样式。按元素类型分流,并校验 Id 属于该类型的合法集(内置预设镜像 ∪ Button 的自定义皮肤;非法 → Fail 并列出合法值),通过后记进元素的 StylePreset。合法 Id 先调 ListStylePresets() 查
ReorderElement(screen, name, how) 调图层次序:top / bottom / up / down
GetElement(screen, elementName) 读单个元素的完整模型(类型/属性/绑定/事件),比 GetScreen 省上下文
RenameElement(screen, oldName, newName) 元素改名,保留其绑定/事件/属性(优于删了重建)
DuplicateElement(screen, srcName, newName, dx=20, dy=20, count=1) 复制元素(深拷贝含绑定/事件),按 dx/dy 偏移摆放;count>1 连续复制并自动加序号
SetElementsProps(screen, elementNames, props) 批量改多个元素的同一组属性。先整体校验元素都在(缺则整批拒绝并列出缺哪些),再逐个套用
GroupElements(screen, elementNames) 成组(扁平分组,不嵌套),至少 2 个;成员原本属别的组也一并并入新组。data.GroupId 是新组标识
UngroupElements(screen, elementNames) 解组:传任一成员即解散它所在的整组(清空该组所有成员的 GroupId)
AlignElements(screen, elementNames, mode) 对齐到统一基准(以选中元素外包围盒为参照)。mode:left/right/top/bottom/hcenter/vcenter。只改 X/Y,不改尺寸;至少 2 个
DistributeElements(screen, elementNames, direction) 在某方向均匀分布。direction:horizontal/vertical,首尾锁定令相邻边到边间隙相等。只改 X/Y;至少 3 个
ValidateElement(elementType, props) 干跑校验:逐条判断属性能否落上(可写属性名/值可转/闭集枚举合法),不改工程。建元素前预检,避免「存进去了却不生效」

AddElement 支持哪些类型

46 种,别手抄 —— 调 ListElementTypes() 拿权威清单(返回 {Count, Types:[{Type,Name,Group,Desc}]})。

基础图形 Rectangle Ellipse Line Polygon Arrow Curve Text; 显示类 ValueDisplay Indicator Bar Gauge LED ProgressBar Slider Clock Tank; 交互类 Button GraphicButton Label Edit CheckBox RadioButton ComboBox Switch UserLogin; 列表与表格类 ListBox Table RealtimeTable RecipeTable AlarmList NavTree NavBar TabControl; 布局装饰类 GroupBox Image Video WebView Animation Timer Report AlarmBanner Trend Pipe Valve Pump SafetySign。

权威可创建集 = 后端 ElementModel 上的 [JsonDerivedType] 判别器(当前 46 个), 由 list_element_types 现算现给 —— 后端加元件,这里和前端图元库会一起跟上。 ⚠ 做列表/表格别用矩形拼 —— ListBox / Table / RealtimeTable 是现成的。

几何 / 属性 / 绑定 三条路别走混

你要改什么 用哪个方法
位置、大小、旋转 MoveElement
文本、颜色、字号、按钮动作 SetElementProps
绑到哪个点 SetBinding

⚠ Name 和 Bindings 是受保护字段,不能经 SetElementProps 改。 改名用画面级方法,改绑定用 SetBinding / RemoveBinding。

SetBinding 的四个可选参数

参数 意思
format 显示格式,如 {0:F1} 保留一位小数
when 条件表达式,满足才显示/生效
map 值映射,如 {"0":"停止","1":"运行"}
twoWay 双向:元素上改值也写回点(仅按钮/输入类有意义)
auto.SetBinding("主画面", "温度显示", "Value", "Oven.Temp",
    format: "{0:F1}", map: null, twoWay: false);

5. 点表

什么时候用:批量导入点、建内部变量、建窗口变量。

方法 干什么
AddTag(TagDef) 加点。Id 自动取当前点数(保持 0 连续)
AddInternalVariable(name, type?, initialValue?, writable?, unit?, desc?) 加全局内部变量(无 PLC 采集,可绑定/脚本读写)
AddScreenVariable(screen, name, ...) 加窗口变量(只在那张画面有效)
ScreenVariables(screen) 取某画面的全部窗口变量(按短名排序)
GlobalVariables() 取全部全局内部变量
UpdateTag(name, edit) 改点定义(按名定位,Id/Name 不在此改)
RemoveTag(name) 删点。删后自动重排剩余点 Id 为 0..N-1
ExportTags() 导出整张点表为 JSON 文本(data.Json 是内容,data.Count 是点数)。真实落文件由调用方做(自动化 API 不做文件 I/O)
ImportTags(json) 从 JSON 文本(单个点或点数组)导入并按点名合并(同名覆盖)。全有或全无:先在候选副本上合并 + 校验(点名唯一、窗口变量归属画面已存在、连接变量目标须已存在),全通过才整体换入并重排 Id 为 0..N-1;任一条不合法则点表原封不动

⭐ 点 Id 必须从 0 连续 —— 这是最容易翻车的地方

实时库用 Id 当数组下标,所以Id 必须 0、1、2… 连续无跳号。 Save() 会先校验,跳号就直接拒绝保存。

批量加点时,不要自己指定 Id,让 AddTag 自动取:

// ✅ 正确:Id 自动取,连续
foreach (var d in defs)
    auto.AddTag(new TagDef { Name = d.Name, DataType = TagDataType.Float, /* ... */ });

// ❌ 错误:自己从 100 开始编 → Save() 直接报"点 Id 必须从 0 连续无重复"

删点会重排 Id,所以别把 Id 存下来长期用。要引用点,一律用点名。 绑定、配方、报警配置全都按名引用,不受重排影响。

全局内部变量 vs 窗口变量

全局内部变量 窗口变量
作用范围 整个工程 只在指定画面
同名变量 全局唯一 不同画面可各有一份
落库点名 就叫 变量名 自动加前缀 → 画面名.变量名
auto.AddInternalVariable("系统运行状态", TagDataType.Bool, initialValue: 0);
auto.AddScreenVariable("主画面", "当前选中项", TagDataType.Int);

// 窗口变量在实时库里的真名带前缀,查询时注意
foreach (var v in auto.ScreenVariables("主画面"))
    Console.WriteLine($"{v.Name}");   // 主画面.当前选中项

窗口变量为什么要加前缀:实时库的点名必须全局唯一。两张画面各有"选中项"时, 不加前缀就撞名了。加前缀是自动的,你不用管,但读值时要写全名。


6. 通道与设备

什么时候用:配置"从哪台 PLC 采数据"。通道是物理链路(一个网口/串口),设备挂在通道下面。

方法 干什么
AddChannel(ChannelDef) 加通道
UpdateChannel(id, name?, driverName?, scanIntervalMs?, timeoutMs?, retryCount?, writeQueueCapacity?) 改通道。传 null 的不动;写队列上限必须大于 0
RemoveChannel(id) 删通道(通道下的设备一起没了)
AddDevice(channelId, DeviceDef?) 加设备
UpdateDevice(channelId, deviceId, DeviceDef?) 改设备配置
RemoveDevice(channelId, deviceId) 删设备
TestChannel(channelName) 只读连通测试(只探测,不下发)。channelName 须是已存在的通道名
ReadTagOnce(tagName) 单次读一个点的当前值(读一次,不订阅、不写)。tagName 须是已存在的点名

⚠ TestChannel / ReadTagOnce 是「只组态不控 PLC」硬边界下的只读 seam。 组态态无头宿主默认不接真实设备,此时两者只做组态侧存在性校验,然后明确返回「未接入在线诊断」 —— 绝不伪造连通结果或点值。接真实设备要宿主侧注入 IDiagnosticReadProvider(见 AttachDiagnosticReader)。

⚠ 设备 Id 不可改

UpdateDevice 是整体替换可配置字段(描述/站号/连接方式与参数/采集周期/故障恢复), 但 Id 不可改 —— 点表是按 DeviceId 引用设备的,换了 Id 会让已有点全变成孤儿。 载荷里带了不同的 Id 会被直接拒绝。

AddDevice 的 device 参数可以为 null

这不是笔误 —— 路由侧从载荷里反序列化时,载荷没带 device 字段就是 null。 实现里显式判空并回一个可读的失败信息(而不是抛空引用)。


7. 脚本组织

什么时候用:程序化地写脚本 —— 全局脚本、画面脚本、元素事件脚本、脚本模块。

7.1 工程级脚本(全局)

方法 干什么
GetProjectScripts() 读工程级脚本:OnStartup / OnShutdown / Conditions / StartupCalls / ShutdownCalls / Periodics
SetProjectScripts(scripts) 整体保存。缺的字段保留原值(保护 Periodics 不被老前端抹掉)
ListProjectPeriodics() 列后台定时脚本(含触发方式 At/IntervalMs 与中文描述 Trigger)
SetProjectPeriodic(name, intervalMs, code?, remark?, at?) 加/改一条后台定时脚本
RemoveProjectPeriodic(name) 删一条

后台定时脚本 = 软件一启动就常驻执行,和当前在哪个画面无关。适合做后台轮询、数据采集汇总这类事。

触发方式二选一: - 按间隔 —— intervalMs > 0(如 SetProjectPeriodic("巡检", 2000, code)); - 按时刻 —— at 非空,墙钟走到表上的时刻才跑(如 SetProjectPeriodic("整点存盘", 0, code, null, "*:00") 即每小时整点)。at 语法 = 逗号分隔的 HH:mm,HH 可用 * 表示每个小时: *:00 每小时整点、*:30 每半点、08:00,20:00 每天早八晚八。

两者互斥:填了 at,intervalMs 会被归一为 0。at 传 null = 不改现有触发方式(老调用方安全), 传空串 = 显式改回按间隔。画面周期脚本不支持 at(画面没打开就不会跑,与"到点就跑"语义冲突)。

7.2 画面脚本

方法 干什么
GetScreenScripts(screen) 读画面脚本:OnEnter / OnExit / Periodic
SetScreenScripts(screen, scripts) 整体保存

画面脚本只在当前打开这张画面时才跑。切走了就停。

7.3 元素事件脚本

方法 干什么
GetElementEvents(screen, elementName) 读元素的事件脚本集
SetElementEvents(screen, elementName, events) 整体保存(会校验事件名合法)

元素事件是 12 个固定字面量:Click / DoubleClick / MouseDown / MouseUp / RightClick / RightDoubleClick / RightMouseDown / RightMouseUp / MouseEnter / MouseHover / MouseMove / MouseLeave。 写别的名字会被拒。详见第 12 章。

7.4 脚本模块(易语言式程序集)

方法 干什么
ListModules() 列模块(名/备注/函数数)
GetModule(name) 取一个模块的完整内容
AddModule(name) 新建空模块
SaveModule(name, module) 整体保存
DeleteModule(name) 删模块

模块 = 你自己写的一堆子程序打包在一起,别的脚本可以调。 名字和画面名相同的模块 = 那个画面的程序集,随窗口生死。


8. 报警配置

什么时候用:给某个点配上"超过多少就报警"。

方法 干什么
ListAlarms() 列所有点的报警配置概览
GetAlarm(tagName) 取一个点的完整配置(该点没配置返回失败)
SetAlarm(config) 设/改(同点名整体覆盖)。限值越出点量程照收,成功消息里带 ⚠ 告警
RemoveAlarm(tagName) 删配置
SetAlarmsBulk(configs) 批量设/改(一次多条,省逐条往返)。全有或全无:任一条不合法则整批拒绝、现有配置不动;全合法则逐条按点名去重覆盖

模拟量四级 + 开关量

点类型 配什么
模拟量(温度/压力…) 四级限值 HiHi / Hi / Lo / LoLo —— 配了才检测
开关量 Digital(0 或 1 触发)

校验规则:点必须存在、级别名合法(Info/Warn/Alarm/Critical)、回差/延时非负。

auto.SetAlarm(new AlarmConfigModel
{
    TagName = "Oven.Temp",
    Group   = "烤箱",          // 分组:报警窗按它过滤
    Deadband = 2,              // 回差:升到 100 报警后,要降回 98 才恢复(防在 100 附近抖动)
    DelaySeconds = 3,          // 延时秒:持续超限 3 秒才算真报警(滤瞬时毛刺)

    // 四级限值:每一级是个**对象**,不是数字。不配就留 null(该级不检测)。
    HiHi = new AnalogLimitModel { Limit = 120, Level = "Critical", Message = "温度超高高限" },
    Hi   = new AnalogLimitModel { Limit = 100, Level = "Alarm" },
    Lo   = new AnalogLimitModel { Limit = 20,  Level = "Alarm" },
    LoLo = new AnalogLimitModel { Limit = 10,  Level = "Critical" },
});

⚠ 三个最容易写错的地方

写错的样子 正确写法 说明
HiHi = 120 HiHi = new AnalogLimitModel { Limit = 120 } HiHi/Hi/Lo/LoLo 是 AnalogLimitModel?,不是 double
DelayMs = 3000 DelaySeconds = 3 真名是 DelaySeconds,单位是秒不是毫秒
Level = "Warning" Level = "Warn" 枚举里没有 Warning

AnalogLimitModel 就三个字段:

字段 类型 说明
Limit double 限值(工程量)
Level string Info / Warn / Alarm / Critical,默认 "Alarm"
Message string 报警文本,留空则运行期用默认模板

开关量报警(Digital)同理,是 DigitalAlarmModel:字段 Trigger(long,不是 bool)、Level、Message。

回差(Deadband) 是防抖动用的:超 100 报高,但必须降到 98 以下才恢复。 延时(DelaySeconds) 是防毛刺用的:瞬时超一下不算,得持续超一段时间。 详见第 14 章。


9. 历史记录

什么时候用:给某个点配"值变了就记一笔"或"每 5 秒记一笔"。

方法 干什么
ListHistoryRules() 列所有点的历史规则
GetHistoryRule(tagName) 取一个点的规则(该点没配置返回失败)
SetHistoryRule(rule) 设/改(同点名整体覆盖)
RemoveHistoryRule(tagName) 删规则
SetHistoryRulesBulk(rules) 批量设/改(一次多条)。全有或全无:任一条不合法则整批拒绝;全合法则逐条按点名去重覆盖

三种记录模式

模式 什么时候记
OnChange 值变化才记(带死区,变化小于死区不记)
Timed 每 N 毫秒记一次(不管变没变),周期看 TimedIntervalMs
Both 两者都要
auto.SetHistoryRule(new HistoryConfigModel
{
    TagName = "Oven.Temp",
    Enabled = true,          // 默认就是 true
    Mode = "OnChange",       // ⚠ 是**字符串**,不是枚举
    Deadband = 0.5,          // 变化小于 0.5℃ 不记,省空间
    TimedIntervalMs = 1000,  // Timed / Both 模式下每 1 秒记一次
    RetentionDays = 90,      // 保留 90 天,超期自动清;0 = 不自动清理
});

⚠ HistoryConfigModel 的 Mode 是 string("OnChange" / "Timed" / "Both")—— 写 Mode = TagHistoryRule.OnChange 或 HistoryRecordMode.OnChange 都编不过。

为什么会这样:HistoryConfigModel 是工程模型(要和磁盘上 project.json 的字符串对得上), 而 TagHistoryRule(带 HistoryRecordMode 枚举)是 Hmi.Historian 的运行期模型。 两者靠 HistoryConfigMapper 桥接。报警的 Level 也是同一个套路(工程侧字符串、运行侧枚举)。

校验:点必须存在、模式名合法、死区非负、保留天数非负; 定时周期只在 Timed / Both 模式下要求 > 0 —— OnChange 模式根本不读它(记不记只看变化)。


10. 配方

什么时候用:管理"一套参数组合"(比如不同产品的温度/时间/压力配方)。

方法 干什么
ListRecipes() 列配方(名/备注/项数)
GetRecipe(name) 取完整内容(含各项)
CreateRecipe(name, remark?) 建空配方(名工程内唯一)
DeleteRecipe(name) 删
SetRecipeItem(recipe, tagName, value, remark?) 设一项点目标(点名 → 目标值)
SetRecipeElementItem(recipe, elementTarget, value, remark?) 设一项编辑框目标(窗口.组件[.属性] → 目标值)
RemoveRecipeItem(recipe, target) 删一项(target 可以是点名或组件目标)
SetRecipeRemark(name, remark) 设备注
ExportRecipe(name) 导出为 JSON 文本
ImportRecipes(json) 从 JSON 文本导入并合并(同名覆盖)

两种配方项

类型 目标是 例子
点目标 点名 Oven.Temp → 180
编辑框目标 窗口.组件[.属性] 参数画面.温度输入.Text → 180

配方下发是运行态的事(要真写进 PLC),不在自动化 API 里 —— 这里只管配方的定义。

ExportRecipe 只返回文本,不落文件

它返回的 Data 是 JSON 字符串。要真的存成文件、写进 U 盘,由宿主侧做 (自动化 API 刻意不做文件 I/O,保持纯粹)。

var r = auto.ExportRecipe("产品A");
if (r.Ok) File.WriteAllText(@"D:\配方导出.json", r.Data!.ToString()!);

11. 报表

什么时候用:定义"每天出一张温度曲线报表"这类东西。

方法 干什么
ListReports() 列报表定义(名/标题/源/窗口/点数)
GetReport(name) 取完整定义
SetReport(report) 设/改(同名整体覆盖)
RemoveReport(name) 删
QueryReport(name, fromUtc?, toUtc?) 查一张真实表格(历史聚合 / 报警事件)。fromUtc/toUtc 都给则用绝对范围(ISO 8601),否则用定义里的近 N 分钟窗口。纯只读;未接入报表数据源时返回「未接入」而非伪造行值

两种报表源

源 数据从哪来 校验什么
Historian 历史库,按聚合函数算 引用的点必须存在 + 聚合函数名合法
Alarms 报警事件 级别名合法

运行期真正生成报表走的是 ReportGenerator,不在自动化 API 里 —— 这里只管定义。


12. 多语言

什么时候用:工程要做成中英双语、中日双语。

方法 干什么
ListLanguages() 列语言:默认(源)语言 + 目标语言 + 译条数
GetI18n() 读整份多语言表
SetI18n(table) 整份覆盖(null 按空表处理)
AddLanguage(code) 加目标语言(如 en / ja)
SetDefaultLanguage(code) 设默认(源)语言
ExtractTranslations() 从所有画面抽取可翻译文本,补进译表
SetTranslation(sourceText, language, translation) 设某源文本在某语言下的译文
GetTranslation(sourceText, language) 查译文(缺译回退原文)
ExportTranslationsCsv() 导成 CSV 文本(Excel 可编辑)
ImportTranslationsCsv(csv) 从 CSV 导入译文
RemoveLanguage(code) 删一个目标语言:从语言列表移除,并清掉每条译条里该语言的译文(源文本保留)。默认(源)语言不可删
RemoveTranslation(sourceText, language?) 删译文(与 SetTranslation 对称):language 留空 = 删整条译条;给定语言 = 只清该语言下的译文

典型流程

auto.AddLanguage("en");                       // ① 加目标语言
auto.ExtractTranslations();                   // ② 从画面里把所有中文抽出来
var csv = auto.ExportTranslationsCsv();       // ③ 导出 CSV 交给翻译
// ...人工翻译,改 CSV...
auto.ImportTranslationsCsv(translatedCsv);    // ④ 导回来

源文本是 key:译表按"中文原文"匹配。所以原文改了,译文就对不上了 —— 改文案后要重新抽取核对。


13. 工程设置

什么时候用:配启动画面、配发布信息、配邮件通知、配按钮皮肤、配数据库连接。

13.1 启动配置

方法 干什么
GetStartupConfig() 读:启动画面 + 启动/退出时要调的全局子程序清单
SetStartupConfig(startScreen?, startupCalls?, shutdownCalls?) 写。三个参数给了才改(null = 不动)

⚠ 清单里的子程序必须真的存在 —— 认不出会直接拒绝,不静默丢弃。 启动画面也必须存在。

13.2 交付发布配置

方法 干什么
GetPublishProfile() 读:exe 文件名/图标/产品信息(含留空回退值)
SetPublishProfile(exeName?, productName?, company?, fileVersion?, copyright?, description?, iconFile?, readmeNote?, author?) 写。每个字段给了才改(空串 = 清空回退默认)
GetProjectBuild() 读「生成信息」—— 同一份配置的另一种叫法
SetProjectBuild(productName?, fileName?, version?, company?, author?, copyright?, description?) 写「生成信息」,内部映射到 PublishProfile
SetProjectInfo(name?, description?) 改工程元信息:工程名 / 说明。两者都给了才改(null = 不动;说明给空串 = 清空)。不含 Version(那是文件格式版本,非产品版本)与 StartScreen(走 SetStartupConfig);产品版本/作者走 PublishProfile

为什么有两套:前端不同地方用了不同的词表。 SetProjectBuild 的 FileName ↔ SetPublishProfile 的 ExeName,Version ↔ FileVersion。 底层是同一份数据,改哪个都行。

13.3 通知配置(邮件)

方法 干什么
GetNotifyConfig() 读配置。口令只回"有没有",不回明文
SetNotifyConfig(config?, password?, clearPassword?) 写(整份替换)
SendTestMailAsync(to?, ct?) 发一封测试邮件
// 口令语义:三态
auto.SetNotifyConfig(config);                        // password=null → 不改口令
auto.SetNotifyConfig(config, password: "smtp口令");   // 设新口令
auto.SetNotifyConfig(config, clearPassword: true);    // 删掉本机保存的口令

⭐ password = null 的意思是"不改",不是"设成空" —— 因为工程文件里不存明文口令(存在本机),所以"不改"必须靠这个语义区分。

⭐ 测试邮件不受总开关限制 —— 配置配到一半正是最需要验证的时候。 收件地址留空则用配置里第一个启用的收件人。

13.4 自定义按钮皮肤

方法 干什么
ListStylePresets() 列内置外观预设的 Id/中文名/分类(button/indicator/bar 三类),供 ApplyStyle 选 presetId。先查再写
ListButtonSkins() 列皮肤(Id/名字/分组 + 完整 Svg/States)
AddButtonSkin(skin) 加。Id 工程内唯一;SVG 非空且不含活动内容(脚本/事件)
UpdateButtonSkin(skin) 整体覆盖(按 Id 定位)
RemoveButtonSkin(id) 删。已引用它的按钮回退到显式外观

⚠ SVG 里不许有脚本/事件 —— 会被安全校验拦下。这是防 XSS。

13.5 数据库连接(脚本 Db.* 的连接名来源)

方法 干什么
ListDbConnections() 读:连接清单 + 可用驱动清单(Available 标该驱动是否已接通)
SetDbConnection(conn) 写:新增 / 修改一条连接(按连接名匹配,忽略大小写:同名 = 改,否则 = 新增)
DeleteDbConnection(name) 删一条连接(不动物理 .db 文件 —— 那是用户数据)
auto.SetDbConnection(new DbConnectionDef { Name = "记录库", Provider = "sqlite" });
// 之后脚本里就能用:Db.Exec("记录库", "CREATE TABLE IF NOT EXISTS t(a TEXT)");

⭐ Password 留空 = 保持原密码,不是清空 —— 读接口从不回明文(只回 HasPassword),编辑时留空是常态。 ⭐ 当前只内置 SQLite:Provider 填 mysql / postgres 也存得下,但驱动未接,运行态会明确报「驱动未接入」。 SQLite 的 File 是相对工程 data/ 的文件名(留空用「连接名.db」;:memory: 走内存库)。


14. 持久化与整包

什么时候用:保存、校验、备份、还原、加密分发。

方法 干什么
Validate() 整份工程校验(见下)
Save() 校验 10 项全过才写回目录(任一项红都不写)
ExportProject(archivePath) 导出 .hmiproj 归档(先保存;排除运行态历史库产物)
ImportProject(archivePath, targetDir, overwrite?) 从归档还原到目标目录(不改当前工程;目标非空需 overwrite)
PackProject(packagePath, password?) 封成加密运行封包 .hmipkg(先保存)

Validate() 具体查什么

Data 回 [{Check, Ok, Details}] 逐项清单,全部跑完再汇总(不首错即返)。 10 项全部是阻断项 —— 任一项红则 Ok=false、Save() 拒写。

检查项 说明
点 Id 连续 必须 0、1、2… 无断裂无重复
点名唯一
画面键名一致 字典键与 Name 必须一致
元素名画面内唯一
绑定引用存在且双向可写 引用点必须存在;TwoWay 要求目标点 Writable
配方名唯一
配方项引用有效 组件项:目标画面/组件存在;点项:点名存在
连接变量有效 见下
报警限值在量程内 限值越出点工程量程 → 运行期永不触发;写入口 SetAlarm(s) 直接拒
点字段与类型相符 与类型无关的字段被填(Bool 点的 Decimals/量程、单寄存器点的 ByteOrder)→ 运行期不生效;写入口 AddTag/UpdateTag 直接拒

最后两项 2026-09-29 从「只告警」改为硬校验(用户拍板:不为存量工程保留兼容)。 这两类配置运行期必然失效 —— 放行等于在工程里留一个"看着配了、实际不动作"的坑。

连接变量(MCGS 式)的三条规则

规则 违反时
内部变量不该设连接变量 报错
不能连自己 报错
连接目标必须是存在的全局内部变量 报错

连接变量是干嘛的:把一个采集点连到一个全局内部变量上。 典型用途是"我想在界面上手动改这个值,但也要 PLC 能写" —— 两边映射到同一个变量。

.hmiproj vs .hmipkg

.hmiproj .hmipkg
用途 备份/还原/整包分发 加密运行封包
内容 完整工程(可再编辑) 加密后的运行包
口令 无 空 = 内置密钥(混淆级);非空 = 口令保护

⚠ .hmipkg 的"内置密钥"只是混淆级,不是强加密 —— 别拿它当保密手段。


15. 脚本编译与函数目录

什么时候用:做脚本编辑器 —— 用户点"编译"时你替他编一下,看有没有错。

方法 干什么
CheckScript(code, screen?, asCondition?) 只编不跑一段脚本。需已 AttachCompiler
BuildScripts() 编译校验整份工程的所有脚本(「编译工程」按钮用)
ListFunctionCatalog() 列内置脚本函数库目录(15 库 / 176 函数,含签名 + 中文说明 + 插入片段)
// 编辑器里用户点了"编译"
var r = auto.CheckScript(userCode, screen: "主画面");
if (!r.Ok) ShowError(r.Message);   // 编译错误信息

asCondition: true = 按布尔条件式校验(条件脚本必须能转成 bool)。

ListFunctionCatalog 和 第 13 章 同源

章 13 里列的那 15 个库、176 个函数,就是这个方法返回的内容。 组态器里「快速插入函数」面板调的也是它。所以后端加一个函数,文档和面板都要跟着更新。


16. 北向数据转发

什么时候用:把工程里的点往外推(出站:给 MES / 上级 SCADA 读),或接别处推来的值 (入站:监听一个端口,收到就写回内部变量)。

方法 干什么
ListForwards() 列所有转发规则(名/启用/方向/协议/点数/出入站端点/鉴权模式)
GetForward(name) 取一个规则的完整定义。不存在返回失败
SetForward(forward) 设/改(同名整体覆盖)
RemoveForward(name) 删规则
SetForwardSecret(authKeyRef, value) 写入入站鉴权密钥的真值到本机 %APPDATA%(不进工程、不置脏)。value 为空 = 清除
ClearForwardSecret(authKeyRef) 清除某入站鉴权密钥(本机)

⚠ 三条安全纪律(违反直接拒绝)

纪律 为什么
入站监听非回环 + 无鉴权 → 拒绝 对外开一个无鉴权的写入口 = 把 PLC 交给整个局域网
入站写点范围 IncludeChannel(写真实 PLC 通道点)一期拒绝 触及「组态不写 PLC」的红线,阶段六才开
密钥永不回吐 SetForwardSecret 的返回信息、GetForward 都只回 AuthKeyRef,不含真密钥

密钥为什么不进工程:工程目录是要拷给别人的。真密钥存本机 %APPDATA%, 工程里只留一个引用名(AuthKeyRef)—— 与邮件口令同一套路。


完整调用示例:批量生成 100 个点 + 一张画面

把前面几组串起来,做一个真实场景:给一条产线生成 100 个温度点,并做一张显示画面。

using Hmi.Automation;
using Hmi.Core.Models;
using Hmi.Project.Model;
using Hmi.Project.History;

var auto = ProjectAutomation.Open(@"D:\产线工程");
auto.Source = "导入工具";

// ① 建通道 + 设备(100 个点都挂在这台 PLC 上)
auto.AddChannel(new ChannelDef
{
    Id = 0, Name = "产线PLC", DriverName = "ModbusTcp",
    ScanIntervalMs = 1000, TimeoutMs = 3000, RetryCount = 3,
    WriteQueueCapacity = 4096,
});

auto.AddDevice(0, new DeviceDef
{
    Id = 0, Name = "1号炉",
    Description = "1号加热炉",
    StationAddress = 1,                  // 从站号(不是 StationNo)
    Connection = new ConnectionDef       // 连接参数挂在 Connection 里(不是 Props)
    {
        Kind = "Tcp",
        Params = new Dictionary<string, string> { ["host"] = "192.168.1.10", ["port"] = "502" },
    },
});

// ② 批量加点(Id 自动取,不要自己编)
for (int i = 1; i <= 100; i++)
{
    var r = auto.AddTag(new TagDef
    {
        Name    = $"1号炉.Temp{i:D2}",
        Type    = TagDataType.Float,          // 字段名是 Type(不是 DataType)
        DeviceId = 0,
        // 地址是**纯十进制字符串**。Float 占 2 个寄存器,所以每个点间隔 2。
        Address = (40001 + (i - 1) * 2).ToString(),   // 40001 / 40003 / 40005 …
        Unit    = "℃",
        Desc    = $"第 {i} 区温度",
    });
    if (!r.Ok) { Console.WriteLine($"第 {i} 个点失败:{r.Message}"); break; }
}

// ③ 建画面 + 摆几个显示框
auto.CreateScreen("1号炉总览", 1920, 1080);
for (int i = 1; i <= 8; i++)      // 前 8 个点上画面
{
    auto.AddElement("1号炉总览", "ValueDisplay", $"温度{i}", 40, 40 + (i - 1) * 60, 200, 40);
    auto.SetBinding("1号炉总览", $"温度{i}", "Value", $"1号炉.Temp{i:D2}", format: "{0:F1}");
}

// ④ 给每个点配历史记录(值变就记,保留 30 天)
for (int i = 1; i <= 100; i++)
    auto.SetHistoryRule(new HistoryConfigModel
    {
        TagName = $"1号炉.Temp{i:D2}",
        Mode = "OnChange",       // ⚠ 字符串,不是枚举
        Deadband = 0.5,
        RetentionDays = 30,
    });

// ⑤ 校验 + 保存
var v = auto.Validate();
if (!v.Ok) { Console.WriteLine($"校验没过:{v.Message}"); return; }
Console.WriteLine(auto.Save().Message);

// ⑥ 导出备份
auto.ExportProject(@"D:\备份\产线工程.hmiproj");

这个示例里埋了几个坑,注意看:

  1. 地址必须留宽度 —— Float 占 2 个寄存器,所以地址是 40001 / 40003 / 40005…。 连着写 40001 / 40002 / 40003 会互相覆盖。
  2. 地址是纯十进制字符串 —— 解析器走 long.TryParse,写 "40001+2" 会抛"地址无法解析"。 要算偏移就在 C# 里先算好再 ToString()。
  3. AddTag 不传 Id —— 让它自动取,保证 0 连续。
  4. 绑定点名用 {i:D2} 补零 —— 因为点名就是 Temp01 这种格式,和 AddTag 时一致。
  5. 每步都查 Ok —— 失败不抛异常,不查就静默错过去了。
  6. ExportProject 内部会先 Save() 一次 —— 所以第 ⑤ 步的 Save 主要是为了拿校验结果。
  7. ExportProject 内部会先 Save 一次 —— 所以第 ⑤ 步的 Save 其实是给校验用的。

七个最容易踩的坑

# 坑 怎么避
1 点 Id 跳号 → Save() 拒绝 别自己指定 Id,让 AddTag 自动取;删点后别复用旧 Id
2 属性名写错 → 静默跳过,以为改成功了 改前先 DescribeElementProps 拿真名
3 忘了 AttachCompiler → 脚本校验全返回"编译器未接入" 打开工程后立刻挂;ProjectSwitched 后重挂
4 不看 Ok → 失败了也不知道 每个调用都查 Ok,!Ok 时打 Message
5 SetElementProps 传空表 → 被拒 空/null 会明确拒绝,不静默当"改成功"
6 用 Id 引用点 → 删点重排后全错 一律用点名引用
7 忘了 Save() → 改了半天磁盘上没变 一组操作做完统一 Validate() + Save()

方法清单以 src/Hmi.Automation/IProjectAutomation.cs 为准。 发现手册缺了方法,回头补上。