13 · 脚本函数库速查
这一章是什么
脚本里能直接调用的全部函数,一共 15 个库、176 个函数。
写脚本时不用背 —— 找到你要做的事对应的库,查一下函数名和参数就行。
组态器里也有「快速插入函数」面板,内容和这里同源。
💡 每个库开头都写了"这个库是干什么的、什么时候用",先看那一段,再看具体函数。
怎么用
脚本里直接写 库名.函数名(...),不用 using、不用 new:
// 读一个温度点,超过 80 就报警并写日志
double t = #PLC1.烤箱温度.实时值; // 首选:点位引用,读回来直接就能比较 / 运算
if (#PLC1.烤箱温度.质量正常 && t > 80) // 质量正常 = 该点质量良好(等价 Tag.IsGood)
{
Log.Warn($"烤箱温度偏高:{t}℃"); // ⚠ 字符串里不能写 #,先取到变量再拼
Alarm.Trigger("烤箱温度过高", level: 2);
}
15 个库总览
| 库 | 干什么 | 函数数 | 可用 |
|---|---|---|---|
Tag |
点位读写(最常用) | 23 | ✅ |
Math2 |
工控数学 | 30 | ✅ |
Str |
字符串处理 | 28 | ✅ |
Bit |
位运算与字节拆装 | 18 | ✅ |
Fs |
受限文件读写 | 13 | ✅ |
Csv |
CSV 整表读写(增删改查) | 15 | ✅ |
Alarm |
触发/确认/清除报警 | 6 | ✅ |
History |
查历史数据 | 4 | ✅ |
Report |
报表 / 实时表 | 19 | ✅ |
Log |
写日志 | 3 | ✅ |
Sys |
时间与当前操作者 | 4 | ✅ |
Timer |
命名定时器 | 2 | ✅ |
Comm |
查设备在线/重连通道 | 2 | ✅ |
Ui |
开关画面、改元素属性 | 6 | ⚠ 需运行态 |
Db |
参数化 SQL(内置 SQLite) | 3 | ✅ |
⚠ 标"需运行态"的库,在运行态就绪前调用会抛
ScriptApiUnavailableException。
这个异常的意思是"宿主没接上这个能力",不是你的脚本写错了。
(Db已内置 SQLite,不需要额外就绪;MySQL / PostgreSQL 的驱动尚未接入,填了会明确报「驱动未接入」。)
Tag —— 点位读写
什么时候用:任何要读现场数据、或者写值的场合。这是用得最多的库。
首选写法:点位引用 #作用域.点名.实时值
把点当成一个可以直接读写的对象,读写两用:
#PLC1.锅炉温度.实时值 = 80; // 写:等价 Tag.Write("锅炉温度", 80)
double t = #PLC1.锅炉温度.实时值; // 读:等价 Tag.Get("锅炉温度")
if (#PLC1.锅炉温度.实时值 > 80) { … } // 可直接参与比较 / 算术
| 作用域(首段) | 写法 | 解析成 |
|---|---|---|
| 设备通道点 | #<设备名>.<点名>.实时值 |
该设备下名为该点名的点 |
| 全局内部变量 | #全局变量.<点名>.实时值 |
裸名 |
| 画面(窗口)变量 | #<画面名>.<短名>.实时值 |
全名 画面名.短名 |
设备 / 全局变量 作用域还能直接取只读元数据叶子:.质量 .质量正常 .单位 .量程下限 .量程上限 .初始值(如 #PLC1.锅炉温度.质量正常、#PLC1.锅炉温度.单位)。
外加 .点名 叶子:#PLC1.锅炉温度.点名 产出该点的名字符串(如 "锅炉温度"),给「按名收点」的函数当实参(History.Log(#烤箱温度.点名, 0))—— 点位是变量,不必手写双引号,改名/换作用域随手改成对的。它不跟任何界面属性同名,所以画面作用域也照常消费(不像上面那几个元数据叶子,在画面作用域会被门控成控件属性)。
⚠
#不能写在字符串 / 注释里 —— 那些地方的#不会被识别。要在插值串里显示实时值,先取到变量再拼:double t = #PLC1.锅炉温度.实时值; Log.Info($"当前 {t}℃");
⚠ 首段是画面名时,只有实时值/.点名两个叶子按点位(窗口变量)算(如#主控总览.设定值.实时值、#主控总览.设定值.点名);其余叶子(文本/可见/单位…)一律当控件属性(见下面Ui章)—— 两套叶子别混着写。
⚠ 重名(画面名=设备名、设备跨通道重名)一律直接报错要求改名消歧,不猜优先级。 ⚠ 点名自己带.(如进料阀.关/Valve.Open)也照写不误 —— 解析从右往左:末段是叶子、首段是作用域,中间整段都算点名。首段不是画面/设备名时还能省掉,写#进料阀.关.实时值(按点名规范名直取)。
点位有类型:编译期就查「类型混用」
每个点在点表里定了类型(Bool 开关 / Float Int 数值 / 字符串 / 数组…),#….实时值 读回来的就是它:开关点读回 true/false,数值点读回数字。所以「开关点跟 0 比」「数值点直接当 if 条件」这类混用,编译(F7 / Shift+F7)那一刻就报错,不用等运行期才炸:
if (#PLC1.锅炉温度.实时值) { … } // ❌ 数值点不能直接当开关 —— 要判断请和门槛比:#PLC1.锅炉温度.实时值 > 80
if (#PLC1.急停.实时值 != 0) { … } // ❌ 开关点不用和 0 比 —— 它本身就是布尔值,直接 if(#PLC1.急停.实时值)
double x = #PLC1.急停.实时值 + 1; // ❌ 开关值不能做加减乘除
bool b = #PLC1.锅炉温度.实时值; // ❌ 数字不能赋给 bool 变量
#PLC1.急停.实时值 = 1; // ❌ 开关点只能写 true / false,不能写数字
诊断 Id
PTS0002,消息里带点名和该改成什么写法。 判据是点表里声明的类型:老工程类型没填对的点检查不到(不误报);var、方法返回值、#{}动态点名这类静态判不出类型的也不报(宁可漏报,绝不误报)。 字符串点拼句子("急停=" + #PLC1.急停.实时值)和Tag.GetBool("数值点")这种有意的强制转换都不受影响 —— 后者正是「数值点要非 0 即真」的正规出口。
点名是动态的:#{"全名" + 拼接}
点名在编译期还不知道、要运行期才算得出来时(比如窑 1~窑 8、批次 1~批量 N),静态的 #作用域.点名 写不出来 —— 把整条引用(作用域 + 点名 + 叶子)写成表达式放进 #{},变量裸写在双引号外、双引号里只放死文本:
string 点名 = $"PLC1.设备{i}.温度"; // 名字里**带作用域**(设备名/画面名/全局变量)
double v = #{点名 + ".实时值"}; // 等价 #….实时值 的运行期版,读 / 写都行
#{"PLC1.批次" + i + ".实时值"} = 0; // 字面量拼接直接写进去也行
if (#{"PLC1.窑" + i + ".开.质量正常"}) { … } // 叶子和静态形态同一张表(写在大括号里)
string 规范名 = #{点名 + ".点名"}; // 运行期把别名叫回**规范名**
#{}里是任意 C# 表达式(变量、拼接、下标都行),运行期算出点名再取点;表达式从#{到最后一个}收尾。- 二形态,没有第三种:要么整段都没有大括号(
#作用域.点名.叶子),要么把作用域、点名、叶子一起包进大括号(#{"作用域.点名" + 拼接 + ".叶子"})。#设备名.{…}.实时值这种把作用域留在括号外的混搭不认。 - ⚠ 双引号里只放纯文本:变量/下标一律裸写在引号外(
+ i +),引号中出现"变量"是误读 —— 这样读代码的人一眼看出哪段是死的、哪段是活的。 - ⚠ 叶子必须写在大括号里(紧跟引号段)、且必须是点位专用叶子(实时值 / 点名 / 质量 / 质量正常 / 单位 / 量程下限 / 量程上限 / 初始值):
#{点名 + ".长度"}这种编译期直接报错(响但不哑,不猜你想干啥);#{点名}.实时值这种把叶子留在括号外的写法同样不认。 - 括号里是一个完整的字符串字面量时(如
#{"PLC1.锅炉温度.实时值"}),编译期就校验并收敛成规范名;不含.的裸名一律拒绝(分不清这个点挂在哪个设备/画面下)。 - 因为点名运行期才知道(含变量时),编译期不校验点是否存在,拼错了不会报错(读回来是 0 / 质量坏)—— 想确认先
Tag.Exists(点名)。 - 点名本身带
.时(如进料阀.关)不用绕#{}—— 静态形态从右往左切,直接写#PLC1.进料阀.关.实时值;首段不是画面/设备名时还能省,写#进料阀.关.实时值。#{}只留给「点名要运行期才算得出来」那种(如#{"PLC1.窑" + i + ".开.实时值"})。 - ⚠ 表达式里不能出现
}(改写器扫到第一个}就收尾),要拼接请用字符串拼接或string.Format。
兜底写法:按名 Tag.*
凡是能用 # 表达的,一律用 # —— 静态点名写 #作用域.点名.实时值、动态点名写 #{"作用域.点名" + 拼接 + ".叶子"}。只有 # 真的表达不了的时候才按名调:# 一次只取一个点,所以批量读写没有 # 形态;# 的叶子只有上面那几个,取整个元数据对象、探存在、FIFO / 数组操作也都没有 # 形态。
「按名收点」的函数(History.* 这类)也不用再手写双引号 —— 实参传 #….点名(如 #烤箱温度.点名)就是传点名字符串,是变量形态。
换句话说:按名调用里,点名参数通常是"运行期算出来的变量"。代码里出现字面量点名的按名调用(如
Tag.GetFloat("锅炉温度")),基本说明上面那行#写法被漏了。
读值有四个类型化方法,别用 Tag.Get(那个不转换,一般用不上)。
| 函数 | 干什么 |
|---|---|
Tag.GetFloat(name) → double |
读浮点值(温度/压力/液位这类模拟量) |
Tag.GetInt(name) → int |
读整数(计数、档位、状态码) |
Tag.GetBool(name) → bool |
开关点请直接写 #PLC1.急停.实时值(Bool 点读回就是 bool,本函数不用于开关点);只给「数值点要非 0 即真」用 |
Tag.GetString(name) → string |
强制转文本(拼提示语/写日志/下发字符串);Bool 给英文 True/False;数组·结构·FIFO 点直接抛错 |
Tag.Get(name) → object |
读原始值不转换(很少用) |
Tag.Write(name, value) |
写一个点(过写安全校验) |
Tag.WriteMany(("锅炉温度",80), ("水泵1",1)) |
一次写多个点(# 没有批量形态) |
Tag.GetFloats("锅炉温度","冷凝压力") → double[] |
一次读多个点成数组(同上) |
Tag.IsGood(name) → bool |
质量好不好(首选 #作用域.点名.质量正常;联锁判断前必查) |
Tag.QualityOf(name) → Quality |
读质量枚举(首选 #作用域.点名.质量;要区分坏在哪时用) |
Tag.Exists(name) → bool |
点是否存在(拼动态点名时先探) |
Tag.TimeOf(name) → DateTime |
最后一次更新时刻(UTC,判断数据新鲜度) |
Tag.Info(name) → TagMeta |
读点定义(单取首选 #….单位 / .量程下限 / .量程上限 / .初始值;整份对象才用函数) |
Tag.WriteString(name, "文本") |
给字符串点写文本(点名和值都是字符串) |
Tag.GetArray(name) → double[] |
读数组 / 结构点成浮点数组(档二非标量) |
Tag.GetIntArray(name) → int[] |
同上,要整数用这个 |
Tag.WriteArray(name, 10, 20, 30) |
整写覆盖数组 / 结构点 |
Tag.ArrayLength(name) → int |
数组 / 结构点的元素个数 |
Tag.FifoPush(name, v) |
FIFO 队列点:入队 |
Tag.FifoPop(name) → double? |
FIFO 出队(空返回 null) |
Tag.FifoCount(name) → int |
FIFO 当前元素个数 |
Tag.FifoClear(name) |
FIFO 清空(复位 / 换批次) |
Tag.GetULong(name) → ulong |
读无符号 64 位点(大计数无损;GetInt 会失真) |
⚠ 点不存在是抛错,不是返回 0 / 空串 —— 按名
Tag.GetFloat("没配的点")与#PLC1.没配的点.实时值一样抛KeyNotFoundException:该处脚本执行失败、错误归入执行结果,不会静默吞掉。 想确认数据新不新鲜,判#作用域.点名.质量正常(别用「读回来是不是 0」来判断点配没配)。⚠ 数组 / 结构 / FIFO 点不能整点转文本 ——
Tag.GetString对它们直接抛可读错 (转出来只会是"System.Double[]",那是类型名、不是数据)。要数组请Tag.GetArray,自己string.Join拼。⚠ Bool 点转文本是英文
True/False—— 函数库不替任何点做本地化;要「开 / 关」请在上层做映射。
两个必须养成的习惯:
// ① 读值前先查质量,否则坏值会静默返回 0
if (#PLC1.烤箱温度.质量正常 && #PLC1.烤箱温度.实时值 > 80) { … }
// ② 动态拼的点名:照样带 #(整条引用含叶子都包进大括号,变量裸写在引号外);探存在 / 批量 / FIFO 才按名
string name = $"PLC1.设备{i}.温度";
double v = #{name + ".实时值"}; // 动态点名也走 #(叶子必写,在大括号里)
if (Tag.Exists(name)) { … }
⚠
Tag.Write写采集点在运行态是允许的(界面按钮就这么干),但脚本里写采集点会被门禁拦下
—— 见第 12 章的"写值边界"。
Math2 —— 工控数学
什么时候用:算工程量、做限幅保护、三角函数、统计一组数。全是纯函数,越界不抛错(比如 Sqrt(-1) 返回 0,Log(0) 返回 0)。
量程与保护(最常用)
| 函数 | 干什么 |
|---|---|
Math2.Scale(v, inMin, inMax, outMin, outMax) → double |
线性量程变换。Scale(raw, 0, 27648, 0, 100) 把 PLC 原始码转成 0~100% |
Math2.Clamp(v, lo, hi) → double |
限幅到 [lo,hi]。下发设备前必做,防止算出界的数 |
Math2.Round(v, n) → double |
四舍五入到 n 位小数 |
Math2.Abs(v) / Math2.Sign(v) |
绝对值 / 符号(正1、负-1、零0) |
取整
| 函数 | 干什么 |
|---|---|
Math2.Floor(v) |
向下取整(Floor(3.9)=3,Floor(-1.1)=-2) |
Math2.Ceil(v) |
向上取整(算"够不够分几批"常用) |
Math2.Truncate(v) |
截断小数向零取整(和 Floor 对负数不同) |
幂与对数
| 函数 | 干什么 |
|---|---|
Math2.Sqrt(v) |
平方根(流量按差压开方线性化常用;负数返回 0) |
Math2.Sqr(v) / Math2.Pow(b,e) |
平方 / 任意次方 |
Math2.Exp(v) / Math2.Log(v) / Math2.Log10(v) |
自然指数 / 自然对数 / 常用对数 |
三角(注意是弧度)
| 函数 | 干什么 |
|---|---|
Math2.Sin(rad) / Cos / Tan |
三角函数,参数是弧度 |
Math2.Atan(v) |
反正切,返回弧度,范围 ±90° |
Math2.Atan2(y, x) |
二参反正切,能区分四个象限(由 X/Y 分量求方向角用这个) |
Math2.Deg2Rad(deg) / Math2.Rad2Deg(rad) |
角度 ↔ 弧度换算 |
⚠ 现场习惯说角度,但
Sin/Cos/Tan吃弧度 —— 先Math2.Deg2Rad(30)再传进去。
统计一组数
| 函数 | 干什么 |
|---|---|
Math2.Avg(a,b,c) |
平均值(可直接接 Tag.GetFloats(...)) |
Math2.Sum(a,b,c) |
求和(累计流量、多路合计) |
Math2.MaxOf(a,b,c) / Math2.MinOf(a,b,c) |
一组数的最大/最小值 |
Math2.Min(a,b) / Math2.Max(a,b) |
两个数的较小/较大者 |
Math2.Mod(a,b) |
取余(b=0 返回 0)。"每 N 次做一件事"用 Mod(N)==0 |
常量
Math2.Pi(π)、Math2.E(自然常数)
Str —— 字符串处理
什么时候用:拼提示语、解析设备回文、清洗用户输入。null 当空串、越界自动裁剪、解析失败给 fallback,绝不抛错。
截取与查找
| 函数 | 干什么 |
|---|---|
Str.Sub(s, start, len) |
从 start 取 len 个字符(越界自动裁剪) |
Str.Sub(s, start) |
从 start 取到末尾 |
Str.Left(s, n) / Str.Right(s, n) |
取最左 / 最右 n 个字符 |
Str.IndexOf(s, sub) → int |
子串首次出现的下标(没有返回 -1) |
Str.Between(s, a, b) → string |
取 a、b 之间的文本 —— 解析设备返回帧特别好用 |
Str.Count(s, sub) → int |
子串出现次数(不重叠) |
Str.Contains(s, sub) → bool |
是否包含 |
Str.StartsWith(s, p) / Str.EndsWith(s, p) |
是否以某前缀/后缀开头结尾 |
增删改
| 函数 | 干什么 |
|---|---|
Str.Replace(s, old, new) |
替换全部匹配 |
Str.Trim(s) / TrimStart / TrimEnd |
去首尾 / 只去首 / 只去尾空白 |
Str.Upper(s) / Str.Lower(s) |
转大写 / 小写(忽略大小写比对前先归一) |
Str.PadLeft(s, w, '0') |
左补齐到 w 位(补零:PadLeft("7",4,'0')="0007") |
Str.PadRight(s, w) |
右补齐(等宽列对齐、定长报文填充) |
Str.Repeat(s, n) |
重复 n 遍(画分隔线) |
Str.Reverse(s) |
反转字符顺序 |
拆分与拼接
| 函数 | 干什么 |
|---|---|
Str.Split(s, ",") → string[] |
按分隔符拆(自动丢空项)。解析 CSV 行 |
Str.Join(",", a, b, c) → string |
用分隔符拼多段(Split 的逆) |
Str.Concat(a, b, c) |
直接首尾相接(无分隔符) |
Str.Format("{0}%", 75) |
格式化(用 InvariantCulture,小数点固定是 .) |
转换
| 函数 | 干什么 |
|---|---|
Str.ToInt(s, fallback=0) → int |
转整数,转不动返回 fallback 不抛错 |
Str.ToDouble(s, fallback=0) → double |
转浮点,同上 |
Str.IsEmpty(s) → bool |
是否 null / 空串 / 全空白(校验必填输入第一步) |
Str.Len(s) → int |
字符个数(null 当空串,返回 0) |
Bit —— 位运算与字节拆装
什么时候用:设备把一堆开关状态打包在一个寄存器里(状态字),你要拆出第几位;或者要把几个标志位拼成一个命令字。
取位 / 置位(状态字最常用)
| 函数 | 干什么 |
|---|---|
Bit.Get(value, bit) → bool |
看第 bit 位是不是 1(bit=0 是最低位)。拆状态字的主力 |
Bit.Set(value, bit) → int |
把第 bit 位置 1,返回新值 |
Bit.Clear(value, bit) → int |
把第 bit 位清 0,返回新值 |
Bit.Toggle(value, bit) → int |
翻转第 bit 位(做"按一下切换"的开关位) |
Bit.PopCount(value) → int |
数有几位置 1 —— 统计当前活动报警数 |
// 拆状态字:第 3 位是高温报警,第 5 位是急停
int status = Tag.GetInt("设备状态字");
bool highTemp = Bit.Get(status, 3);
bool eStop = Bit.Get(status, 5);
Log.Info($"活动报警数:{Bit.PopCount(status)}");
按位运算(掩码)
| 函数 | 干什么 |
|---|---|
Bit.And(a, b) |
按位与(取掩码位:And(状态字, 0x0F) 只留低 4 位) |
Bit.Or(a, b) |
按位或(合并多个标志位) |
Bit.Xor(a, b) |
按位异或(找哪些位变了、算 LRC 校验) |
Bit.Not(a) |
按位取反(算反掩码) |
Bit.Shl(v, n) / Bit.Shr(v, n) |
左移 / 右移(拼高低位、取位段) |
字节与字拆装
| 函数 | 干什么 |
|---|---|
Bit.LowByte(v) / Bit.HighByte(v) |
取低 / 高字节(0~255) |
Bit.LowWord(v) / Bit.HighWord(v) |
取低 / 高 16 位字(拆 32 位双字) |
Bit.MakeWord(hi, lo) → int |
高字节 + 低字节拼成 16 位字(字节序不对就把两个参数换一下) |
十六进制
| 函数 | 干什么 |
|---|---|
Bit.ToHex(v, width=0) → string |
转十六进制字符串,width>0 左补零(ToHex(255,4)="00FF") |
Bit.FromHex("0xFF", fallback=0) → int |
解析十六进制(认 0x 前缀,失败给 fallback) |
Fs —— 受限文件读写
什么时候用:写生产日志、读配置/配方文件、导出报表。
⚠ 这个库被围栏限制在工程数据目录内。越界路径会安全失败
—— 读返回兜底值、写返回false,绝不抛异常。
所以脚本读不到工程目录外面的文件,这是刻意的。
| 函数 | 干什么 |
|---|---|
Fs.AppendLine("logs/run.log", "设备已启动") |
追加一行(自动换行) —— 写事件日志最常用 |
Fs.AppendCsvRow("logs/prod.csv", Sys.Now, Tag.GetFloat("温度")) |
追加一行 CSV(自动转义逗号/引号) —— 做生产报表最顺手,Excel 能直接打开 |
Fs.AppendText(path, content) |
追加文本,不自动换行 |
Fs.WriteText(path, content) → bool |
覆盖写入(上级目录自动建) |
Fs.ReadText(path, fallback="") → string |
读整个文本(读失败给 fallback) |
Fs.ReadLines(path) → string[] |
按行读(每行一个元素) |
Fs.Exists(path) → bool |
文件是否存在 |
Fs.Size(path) → long |
字节大小(不存在返回 -1) |
Fs.Delete(path) → bool |
删除(不存在也算成功) |
Fs.MakeDir(path) → bool |
建目录(一般不用,写文件会自动建) |
Fs.ListFiles("logs", "*.csv") → string[] |
列文件名(只给文件名,不含路径) |
Fs.Combine("logs", "prod.csv") → string |
拼路径(配合 ListFiles 拼回完整相对路径) |
Fs.Root → string |
当前围栏根目录(只用于日志里说明文件存哪了) |
// 每天记一条产量到 CSV
Fs.AppendCsvRow("logs/产量.csv", Sys.Now, Tag.GetFloat("今日产量"));
// 遍历日志目录里所有 CSV
foreach (var f in Fs.ListFiles("logs", "*.csv"))
Log.Info(Fs.Combine("logs", f));
⚠
WriteText是覆盖不是追加。要往日志末尾续写用AppendLine/AppendCsvRow。
Alarm —— 报警
什么时候用:脚本自己判断出了异常,要报一条警(不是靠报警引擎的限值判定)。
| 函数 | 干什么 |
|---|---|
Alarm.Trigger("内容", level, group) |
触发一条报警。level:0信息/1警告/2报警/3故障 |
Alarm.Clear(key) |
清除一条报警(条件恢复后调) |
Alarm.IsActive(key) → bool |
该报警是否还在活动(避免重复触发、做联锁) |
Alarm.Ack(key) |
确认(应答)一条报警 |
⚠ 别在高频循环里反复
Trigger同一条报警 —— 会刷爆报警列表。
判断成立才报,恢复用Clear消掉。
if (Tag.GetFloat("油温") > 90 && !Alarm.IsActive("OIL_HOT"))
Alarm.Trigger("油温过高", level: 2, group: "液压站");
if (Tag.GetFloat("油温") < 85 && Alarm.IsActive("OIL_HOT"))
Alarm.Clear("OIL_HOT");
History —— 历史数据
什么时候用:脚本里要查"过去一段时间"的统计数据(出班报、算趋势)。
| 函数 | 干什么 |
|---|---|
History.Avg(tag, from, to) → double |
时间段平均值 |
History.Max(tag, from, to) → double |
时间段最大值(峰值) |
History.Min(tag, from, to) → double |
时间段最小值(谷值) |
History.Log(tag, value) |
主动记一条历史值(存脚本算出来的统计量) |
// 交班时算这一班的平均温度(tag 参数首选 #….点名:点位是变量,不必手写双引号)
double avg = History.Avg(#烤箱温度.点名, Sys.Now.AddHours(-8), Sys.Now);
History.Log(#烤箱温度.点名, avg); // 主动记一条:点名跟点位引用走,改点不用改这行
Fs.AppendCsvRow("logs/班报.csv", Sys.Now, avg);
⚠ 一般点的历史由记录规则自动存(见第 14 章),
History.Log只用于"脚本算出来的中间量也想留档"。
Report —— 报表 / 实时表
什么时候用:脚本要自己往报表元件(或实时表)里填数据 —— 定时采一行、按行内按钮操作。
⚠ 这里的「报表名」是报表元件的名字(RealtimeTable / Report 元件的
Name),不是画面名。
数据由脚本自管、后端不落盘;改完必须调Report.Refresh(名)才推到界面。
| 函数 | 干什么 |
|---|---|
Report.SetColumns(名, "时间", "温度") |
设列标题(个数=列数),已有行会被规整到这个列数 |
Report.AddRow(名, "值1", "值2") → int |
末尾加一行,返回新行号(0 基) |
Report.InsertRow(名, index, 值…) |
在第 index 行插入(想让最新数据置顶就传 0) |
Report.DeleteRow(名, index) |
删第 index 行(⚠ 越界会抛,先看 RowCount) |
Report.SetRows(名, rows) |
整表替换(比循环 AddRow 快得多) |
Report.SetCell(名, row, col, "值") / GetCell(名, row, col) |
改 / 读某一格 |
Report.GetRows(名) → 只读行集 |
整表行(只读快照,改它不影响真表) |
Report.ClearRows(名) / Report.Clear(名) |
清数据行(留列头) / 连列头一起清 |
Report.GetColumns(名) → string[] |
取列标题 |
Report.RowCount(名) / ColumnCount(名) / CellCount(名) → int |
行数 / 列数 / 格数 |
Report.Reports() → string[] |
现有报表 id 列表 |
Report.LastClickRow(名) / LastClickCol(名) → int |
最近被点击的行 / 列(没点过返回 -1) |
Report.RecordClick(名, row, col) |
登记一次行内按钮点击(一般由前端回调,脚本很少主动调) |
Report.Refresh(名) |
把改动推到界面(批量改完调一次即可,别每改一格调一次) |
// 周期脚本:采一行进报表,只留最近 200 行
Report.AddRow("机组表", Sys.Now.ToString("HH:mm"), Tag.GetString("1#机组.功率"));
if (Report.RowCount("机组表") > 200) Report.DeleteRow("机组表", 0);
Report.Refresh("机组表");
// 实时表里行内「启动」按钮被点之后,拿它操作的是哪一行
int i = Report.LastClickRow("机组表");
if (i >= 0 && Report.LastClickCol("机组表") == 2) Log.Info($"启动第 {i} 行");
⚠ 单元格全是字符串 —— 数值要先
ToString(),读回来要算得自己double.Parse。
Log —— 日志
什么时候用:留下"程序走到哪了、干了什么"的脚印,方便事后排查。
| 函数 | 干什么 |
|---|---|
Log.Info("消息") |
普通信息 |
Log.Warn("消息") |
警告(还没坏,但值得注意) |
Log.Error("消息") |
错误(出问题了) |
日志会进输出面板和本机日志文件。
⚠ 别在高频循环里狂打日志 —— 刷屏而且占磁盘。
Error的消息里尽量写清"哪个点 / 哪一步 / 什么值",这是排障时最先看的一级。
Sys —— 系统信息
| 函数 | 干什么 |
|---|---|
Sys.Now → DateTime |
当前本地时间(打时间戳、判班次、算时间差) |
Sys.UtcNow → DateTime |
当前 UTC 时间 |
Sys.User → string |
当前登录操作员的用户名(日志留痕、按人判权限) |
Sys.Exit() |
退出(关闭)运行程序 —— 给画面上的「退出」按钮用 |
⚠ 点的
Tag.TimeOf是 UTC,所以判断"数据多久没更新"要用Sys.UtcNow - Tag.TimeOf("点"),
别拿Sys.Now去减(会差一个时区)。🚪 退出全屏运行程序:现场运行态是全屏无边框、没有标题栏。放一个按钮,事件脚本写
Sys.Exit();即可正规退出
(宿主会优雅停机:停脚本/采集/转发、释放资源)。退出不可撤销,建议先确认再退:
csharp if (Ui.Confirm("确定退出运行程序?")) Sys.Exit();也可把这个按钮设成需要权限等级,防误触。应急时键盘 Alt+F4 也能关窗。
预览态(编辑器里试运行)点它只会停止本次预览、不会关掉编辑器 —— 交付出去的运行程序里才真退出。
Timer —— 命名定时器
什么时候用:脚本里要"每隔 N 毫秒做一次"。
| 函数 | 干什么 |
|---|---|
Timer.Every("名字", ms, () => { … }) |
每 ms 毫秒跑一次 |
Timer.Stop("名字") |
停掉这个定时器 |
关键点:名字就是身份 —— 同名再注册会先停掉旧的,所以脚本反复加载也不会越堆越多。
Timer.Every("心跳", 5000, () =>
{
Log.Info($"心跳 {Sys.Now:HH:mm:ss}");
});
⚠
ms别设太小(几毫秒),会拖垮系统。停用它要Timer.Stop("心跳"),名字要一模一样。
Comm —— 通信控制
什么时候用:查设备通不通、掉线后让脚本自动重连。
| 函数 | 干什么 |
|---|---|
Comm.IsDeviceOnline("设备名") → bool |
设备是否在线 |
Comm.Reconnect("通道名") |
重连一个通道 |
if (!Comm.IsDeviceOnline("1号PLC"))
{
Log.Warn("1号PLC 掉线,尝试重连");
Comm.Reconnect("车间Modbus");
}
⚠
Reconnect只是"断开",真正的重连靠退避策略在下一轮发生(见第 07 章)。
Ui —— 界面控制 ⚠ 需运行态就绪
什么时候用:脚本里切画面、弹提示、改元素显示。
| 函数 | 干什么 |
|---|---|
Ui.OpenScreen("画面名") |
打开/切换画面 |
Ui.CloseScreen("画面名") |
关闭画面(关弹出式子画面) |
Ui.ShowMessage("提示") |
弹提示框(不挡,发完就走) |
Ui.Confirm("确定吗?") → bool |
弹确认框,一直弹着等人点,返回是否点了确定 |
Ui.Confirm("确定吗?", 10) → bool |
同上,但 10 秒无人应答自动按「否」(显示倒计时) |
Ui.SetElementProp(画面, 元素, 属性, 值) |
改某画面某元素的属性 |
还有更省事的简写语法:#窗口.组件.属性 = 值,效果和 Ui.SetElementProp 一样。
⚠
#靠「首段 + 叶子」分流:首段是画面名且叶子是属性名(文本/可见…)→ 控件属性(本章);
首段是设备名 /全局变量且叶子是实时值/.点名(或.质量/.单位/.量程下限/.量程上限/.初始值)→ 点位(见上面Tag章)。⚠ 这一组依赖前端运行态就绪,组态阶段调用无效。
🔔 关于
Ui.Confirm(弹确认框):
- 典型用法:
if (Ui.Confirm("确定启动 1 号泵?")) { Tag.Write("1号泵.启动", 1); }。- 不写等待时长 = 一直弹着,这条脚本停在此处直到有人点;写了秒数则到点自动按「否」。
- 弹框期间其余脚本照常跑(采集、周期、条件都不受影响),不会把整机冻住。
- 一次只显示一个框,多处同时要弹会自动排队,一个一个来。
- ⚠ 只能放在按钮/元件事件里;写在周期脚本、条件脚本里会报错(定时器起拍不能卡住等人)。
- 触摸屏上会自动换成大按钮、不依赖物理键盘。
Db —— 关系数据库(SQLite 内置,零配置)
什么时候用:脚本要读写数据库(记录操作、上报数据、查工单)。
怎么连:连接在编辑器 → 设置 → 数据库连接里配,每条起一个名字;脚本里 Db.Exec("名字", …) 的 "名字" 就是它。
当前内置 SQLite —— 连接名对应工程 data/<名字>.db,建表 / 读写一条搞定,不用装任何数据库。
MySQL / PostgreSQL 也留了字段和下拉,但驱动还没接(填了运行时会明确报「驱动未接入」,不会假装能用)。
| 函数 | 干什么 |
|---|---|
Db.Exec(conn, sql, args) → int |
执行增删改,返回影响行数 |
Db.Scalar(conn, sql, args) → object? |
查单个值(总行数、某字段) |
Db.Query(conn, sql, args) → 行集 |
查多行 |
⚠⚠ 必须用参数占位
@p0 @p1…把值放进 args,绝不要把值拼进 SQL 字符串。
拼接会造成 SQL 注入。这是硬要求。
⚠ 占位符是@p0 @p1(不是@0 @1)—— 写错会报「参数个数不符」,照本文抄就对。
// ✅ 正确
Db.Exec("mes", "UPDATE orders SET status=@p0 WHERE id=@p1", "done", 1001);
// ❌ 错误 —— 有注入风险
Db.Exec("mes", $"UPDATE orders SET status='done' WHERE id={id}");
写脚本的三条经验
- 读值前先查质量 —— 用
#作用域.点名.质量正常(或按名Tag.IsGood(点名变量))。坏质量会静默返回 0,不查就会把"没读到"当成"值就是 0"。 - 下发前先
Math2.Clamp—— 算出来的值可能越界,夹到合法范围再写。 - 脚本要快 —— 所有脚本串行执行,一个慢脚本会拖住全部(见第 12 章)。
小结
- 15 个库、176 个函数,全在这里
- 读写点首选
#作用域.点名.实时值(#PLC1.锅炉温度.实时值 = 80;);点名是动态的写#{"PLC1.窑" + i + ".开.实时值"}(整条引用含叶子都在大括号里,变量裸写在引号外);按名Tag.*只作兜底(批量 / 取整个元数据对象 / 探存在 / FIFO / 数组) - 用得最多的是
Tag(读写点)和Math2(算数) - 拆状态字用
Bit,解析回文用Str,写日志用Fs - 所有库越界/失败都给兜底值,不抛异常 —— 所以"没读到"和"值是 0"要靠
#….质量正常(按名Tag.IsGood) /Fs.Exists区分 Ui需要运行态就绪,没接上会抛ScriptApiUnavailableException(Db已内置 SQLite,可直接用)
下一步:第 14 章 历史库与报警引擎