跳到主要内容

点表与采集

点表就是把设备里的寄存器、线圈或其它数据,命名成业务能理解的点。比如把 Modbus 地址 0 命名为 temperature

采集循环就是 Zeus 按固定间隔帮你读取这些点。你不需要自己写 while trueTask.Delay、重试和界面刷新。

先看完整例子

var memory = new ModbusSlaveMemory();
memory.HoldingRegisters[0] = 185;
memory.HoldingRegisters[1] = 200;

await using var app = ZeusHost.Create(builder =>
{
builder.AddAcquisition(TimeSpan.FromMilliseconds(500));
builder.AddVirtualChannel("bus", new ModbusSlaveResponder(1, ModbusTransport.Rtu, memory));
builder.AddModbusRtu("oven", "bus", unitId: 1, points: map =>
{
map.HoldingRegister("temperature", 0, raw => raw * 0.1, new PointAlarmLimits(high: 80));
map.HoldingRegister("setpoint", 1, raw => raw * 0.1);
map.Coil("heater", 2);
});
});

app.Points.Changed += (_, e) =>
{
Console.WriteLine($"{e.Current.QualifiedName} = {e.Current.Value}");
};

await app.StartAsync();
var temperature = app.Points.Get<double>("temperature");

这段代码怎么理解

代码含义
AddAcquisition(500ms)每 500 ms 采集一轮
HoldingRegister("temperature", 0, raw => raw * 0.1, new PointAlarmLimits(high: 80))地址 0 的保持寄存器叫 temperature,按工程值判断高报
Coil("heater", 2)地址 2 的线圈叫 heater
app.Points.Changed点值或错误变化时触发
app.Points.Get<double>("temperature")读取当前最新温度
app.Points.GetHistory("temperature")读取最近成功采样历史

为什么不要自己写循环

你当然可以自己写:

while (true)
{
var values = await oven.ReadHoldingRegistersAsync(0, 2);
await Task.Delay(500);
}

但实际项目很快会遇到:

  • 多个点地址连续,应该合并读取。
  • 某轮读取失败,界面还要保留上一次成功值。
  • 多个设备需要统一间隔。
  • UI 要订阅变化,而不是到处主动拉取。

Zeus 的点表采集就是为这些场景准备的。

点名怎么查

如果整个宿主里只有一个 temperature,可以直接写:

var value = app.Points.Get<double>("temperature");

如果多台设备都有 temperature,使用完整名:

var ovenTemperature = app.Points.Get<double>("oven.temperature");
var tankTemperature = app.Points.Get<double>("tank.temperature");

读取历史

点表会为每个点保留最近 128 次成功采样。历史顺序从旧到新,采集失败只更新当前快照的 Error,不会写入历史缓冲。

var recent = app.Points.GetHistory("temperature");
foreach (var sample in recent)
{
Console.WriteLine($"{sample.UpdatedAt:HH:mm:ss} {sample.Value}");
}

如果只想在自定义场景关闭或缩短历史,可以直接注册自己的 PointTable 实例:

builder.Services.AddSingleton<PointTable>(_ => new PointTable(historyCapacity: 32));

报警限

数值点可以配置低报或高报阈值。快照的 AlarmState 会返回 DisabledUnknownNormalLowHighIsAlarmed 会在低报或高报时返回 true

map.HoldingRegister("temperature", 0, raw => raw * 0.1, new PointAlarmLimits(low: 10, high: 80));

var snapshot = app.Points.Get("temperature");
if (snapshot.IsAlarmed)
{
Console.WriteLine($"{snapshot.QualifiedName} 报警:{snapshot.AlarmState}");
}

报警限按最终写入点表的值判断。上例中原始寄存器 850 会先换算成 85.0,再触发高报。

尚无有效值怎么办

StartAsync 返回后,第一轮采集可能还在进行。稳妥写法是订阅变化:

app.Points.Changed += (_, e) =>
{
if (e.Current.Error is null)
{
Console.WriteLine($"{e.Current.QualifiedName} = {e.Current.Value}");
}
else
{
Console.WriteLine($"{e.Current.QualifiedName} 采集失败:{e.Current.Error}");
}
};

或者使用 TryGet

if (app.Points.TryGet<double>("temperature", out var value))
{
Console.WriteLine(value);
}

绑定到界面

WinForms / WPF 都可以直接绑定点:

app.Points.BindTo("temperature", temperatureLabel, value => $"{value:F1} ℃");

这样界面只关心显示格式,不需要关心采集线程和设备协议。

采集优化

Zeus 会尽量把同一设备上连续的地址合并读取。

点声明读取效果
地址 0、1、2 连续可以合并成一次读取
地址 0、100 分散通常需要分开读取
不同设备分别按设备读取

总线较慢时,优先让常用点地址连续,并适当加大采集间隔。

常见问题

现象原因处理
尚无有效值第一轮采集还没成功订阅 Changed,或稍后再 Get
点名冲突多台设备有同名点使用 设备名.点名
界面值不更新没声明点,或采集循环没启用确认 AddAcquisitionpoints 都写了
某轮失败但界面还有旧值Zeus 保留上一次成功值查看点快照里的 Error
总线被采集打满间隔太短或地址太分散加大间隔,合并连续地址

下一步:把点表写进文件看 JSON 配置