宿主
宿主就是 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();
读法是:
ZeusHost.Create:声明“这个程序有哪些通道、设备和服务”。StartAsync:真正打开通道并启动后台服务。Channels.Get:拿到运行中的通道。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 客户端。