13 · 脚本函数库速查

相关:第 12 章 脚本运行时与脚本形态

这一章是什么

脚本里能直接调用的全部函数,一共 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}");

写脚本的三条经验

  1. 读值前先查质量 —— 用 #作用域.点名.质量正常(或按名 Tag.IsGood(点名变量))。坏质量会静默返回 0,不查就会把"没读到"当成"值就是 0"。
  2. 下发前先 Math2.Clamp —— 算出来的值可能越界,夹到合法范围再写。
  3. 脚本要快 —— 所有脚本串行执行,一个慢脚本会拖住全部(见第 12 章)。

小结

  • 15 个库、176 个函数,全在这里
  • 读写点首选 #作用域.点名.实时值(#PLC1.锅炉温度.实时值 = 80;);点名是动态的写 #{"PLC1.窑" + i + ".开.实时值"}(整条引用含叶子都在大括号里,变量裸写在引号外);按名 Tag.* 只作兜底(批量 / 取整个元数据对象 / 探存在 / FIFO / 数组)
  • 用得最多的是 Tag(读写点)和 Math2(算数)
  • 拆状态字用 Bit,解析回文用 Str,写日志用 Fs
  • 所有库越界/失败都给兜底值,不抛异常 —— 所以"没读到"和"值是 0"要靠 #….质量正常(按名 Tag.IsGood) / Fs.Exists 区分
  • Ui 需要运行态就绪,没接上会抛 ScriptApiUnavailableException(Db 已内置 SQLite,可直接用)

下一步:第 14 章 历史库与报警引擎