跳到主要内容

宿主

宿主就是 Zeus 程序的总开关。它负责创建通道、打开通道、启动采集循环、停止时释放资源。

最小写法​

await using var app = ZeusHost.Create(builder =>
{
builder.AddVirtualChannel("meter");
});

await app.StartAsync();
var meter = app.Channels.Get("meter");
await meter.WriteAsync(new byte[] { 0x01 });
await app.StopAsync();

读法是:

  1. ZeusHost.Create:声明“这个程序有哪些通道、设备和服务”。
  2. StartAsync:真正打开通道并启动后台服务。
  3. Channels.Get:拿到运行中的通道。
  4. StopAsync:关闭通道并释放资源。

Create 阶段只声明,不打开​

这段代码只是在登记通道:

await using var app = ZeusHost.Create(builder =>
{
builder.AddSerialPort("meter", "COM3", 9600);
});

此时串口还没有打开。真正打开是在:

await app.StartAsync();

所以如果你在 StartAsync 前调用 WriteAsync,会看到“当前为 Created,无法写入”。

注册顺序怎么写​

一般按这个顺序:

await using var app = ZeusHost.Create(builder =>
{
builder.AddAcquisition(TimeSpan.FromMilliseconds(500));
builder.AddSerialPort("bus", "COM3", 9600);
builder.AddModbusRtu("oven", "bus", unitId: 1, points: map =>
{
map.HoldingRegister("temperature", 0, 0.1)
.HoldingRegister("setpoint", 1, 0.1).Writable("setpoint");
});
});
顺序为什么
先配置采集采集间隔是宿主级设置
再注册通道设备需要绑定已有通道
再注册设备设备会引用通道名
最后启动宿主所有东西都声明完再打开

运行时可以查什么​

var channel = app.Channels.Get("bus");
var device = app.Devices.Get<ModbusDevice>("oven");
var temperature = app.Points.Get<double>("temperature");
目录用途
app.Channels查通道,例如串口、TCP、UDP、虚拟通道
app.Devices查设备,例如 Modbus 设备
app.Points查点表,例如温度、压力、开关量

找不到名称时,异常会列出当前已经注册的名称,方便你对照拼写。

停止后再启动​

StopAsync 只关闭通道并暂停采集,底层宿主仍保持。再次 StartAsync 会重新打开通道:

await app.StopAsync();
await app.StartAsync();

需要彻底释放日志、监视器和 Generic Host 时,调用 DisposeAsync(await using 会自动做这件事)。释放后再启动必须重新 ZeusHost.Create。

运行中增删通道和设备​

await app.AddVirtualChannelAsync("bus");
app.AddModbusRtu("oven", "bus", points: map => map.HoldingRegister("pv", 0));

await app.RemoveDeviceAsync("oven");
await app.RemoveChannelAsync("bus");

宿主已启动时,新通道会立即打开。移除通道默认会先卸载仍绑定它的设备;若要自己先卸设备,传入 removeBoundDevices: false。

如果传入的 CancellationToken 已取消,运行期新增或移除会直接按取消返回,不会先把通道或设备半注册、半移除。宿主运行中新增 required 通道但打开失败时,也会从目录回滚,避免留下一个调用方以为失败、目录里却存在的故障通道。

故障自动重连​

通道进入 Faulted 后,默认等待 1 秒再 OpenAsync,随后按 2 倍退避,上限 30 秒。主动 CloseAsync 不会重连。

builder.AddReconnect(options =>
{
options.Enabled = true;
options.InitialDelay = TimeSpan.FromSeconds(1);
options.MaxDelay = TimeSpan.FromSeconds(30);
});

现场若要自己控制重连,设 options.Enabled = false,再订阅 StateChanged 后调用 OpenAsync。

调用方取消不是故障:OpenAsync、WriteAsync、StartAsync 或运行期新增通道时,如果是传入的取消令牌触发,Zeus 会保留 OperationCanceledException,不会把通道标成 Faulted,也不会触发自动重连。只有真实打开、接收或写入失败进入 Faulted 时,自动重连才会接管。

在宿主里注册自己的服务​

Zeus 使用 .NET 依赖注入。你可以把自己的业务服务放进去:

await using var app = ZeusHost.Create(builder =>
{
builder.Services.AddSingleton<MyBusinessService>();
builder.AddVirtualChannel("meter");
});

这适合把采集后的业务处理、报警判断、数据库写入等逻辑拆出窗体类。

自定义设备如果也要从容器取日志,使用带 IServiceProvider 的工厂:

builder.AddDevice("meter", "bus", (services, name, channel) =>
new TemperatureMeter(name, channel, services.GetRequiredService<ILogger<TemperatureMeter>>()));

配置日志​

builder.Logging 是标准 ILoggingBuilder。默认沿用 Generic Host 的控制台记录器;需要 JSON、文件或 Serilog 时在这里接:

await using var app = ZeusHost.Create(builder =>
{
builder.Logging.AddJsonConsole();
builder.Logging.SetMinimumLevel(LogLevel.Information);
builder.AddVirtualChannel("meter");
});

通道开关、重连、采集失败和点写回失败会写入 ILogger,并带上 ZeusLogEvents 中的稳定事件编号。现场可按 Id 过滤,而不依赖中文消息文本。

排障时把全部通道的 TX/RX 报文打进日志:

builder.AddCommunicationLogging(); // 默认 Debug

默认级别是 Debug,避免十六进制载荷冲掉业务日志。分类名默认 Zeus.Communication,可在选项里改。

常见问题​

现象原因处理
通道名称已存在注册了两个同名通道给通道改名,例如 ovenBus、plcBus
当前为 Created,无法写入还没启动宿主先 await app.StartAsync()
某个通道启动失败串口被占用、TCP/UDP 地址错误或参数错误其它通道会继续启动;默认会自动重连
停止后再启动没反应调用了 DisposeAsync 而不是 StopAsync释放后必须重新 Create
关闭程序后端口仍被占用没释放宿主使用 await using,或在关闭窗口时调用释放逻辑

下一步:没有硬件先看 虚拟通道,接真实设备看 串口 或 TCP/UDP 客户端。