灯塔与蜂鸣器:工业界的"仪表盘"
这是「从零搭建工业控制系统」系列第12篇。阀门控制聊完了,这篇说灯塔。工厂车间里抬头就能看到的那根三色灯柱,比任何弹窗都管用。
为什么不用弹窗?
做软件开发的人有个本能反应:出问题了弹个MessageBox。但在工厂环境,操作员可能戴着防尘手套,站在2米外的操作台前,根本看不清屏幕上的弹窗。
灯塔不一样。红灯亮了+蜂鸣器响了,隔着车间都能看到听到。不需要操作员盯着屏幕。
项目里用了一根五段式灯塔:蓝、绿、橙、红四个LED加一个蜂鸣器,分别对应五个数字输出点(DO点位号视硬件配置而定)。
灯塔规则定义
先明确每个灯代表什么:
| 输出点 | 颜色 | 含义 | 触发条件 |
|---|---|---|---|
| DO_蓝 | 蓝灯 | 通信连接 | 上位机与下位机连接时亮 |
| DO_绿 | 绿灯 | 运行中 | Sequence运行时亮(RunState==1) |
| DO_橙 | 橙灯 | 完成 | Sequence运行完成时亮(RunState: 1→0) |
| DO_红 | 红灯 | 报警 | AlarmState发生报警时亮(AlarmState!=0) |
| DO_蜂 | 蜂鸣器 | 声音报警 | 橙灯或红灯亮时响5秒 |
这些规则直接写在 TowerLightService 的注释里,作为需求文档:

/// <summary>
/// 灯塔控制服务
/// 灯塔规则:
/// - 蓝灯: 上位机与下位机连接时亮灯,断开时不亮
/// - 绿灯: Sequence运行时亮灯(RunState==1),其他时候不亮
/// - 橙灯: Sequence运行完成时亮灯(RunState: 1→0),保持到下次运行开始
/// - 红灯: AlarmState发生报警时亮灯(AlarmState!=0),报警解除后熄灭
/// - 蜂鸣器: 橙灯亮或红灯亮时响5秒
/// </summary>
服务架构
TowerLightService 用500毫秒的定时器轮询系统状态,自动控制灯塔:
public partial class TowerLightService : ObservableObject, IDisposable
{
private const int MONITOR_INTERVAL_MS = 500;
private const int BUZZER_DURATION_MS = 5000;
private readonly DeviceCmdService _deviceCmdService;
private readonly ChamberSubsystemStatus _status;
private readonly ChamberCoilStatusHelper _coilStatusHelper;
private readonly Timer _monitorTimer;
private Timer? _buzzerTimer;
// 状态追踪
private int _previousAlarmState;
private int _previousRunState;
private bool _sequenceCompleted; // 橙灯保持标志
// 线圈缓存
private Models.ModbusCoil? _buzzerCoil;
private Models.ModbusCoil? _blueLedCoil;
private Models.ModbusCoil? _greenLedCoil;
private Models.ModbusCoil? _orangeLedCoil;
private Models.ModbusCoil? _redLedCoil;
}
每个灯和蜂鸣器都对应一个 ModbusCoil 对象,初始化时通过枚举获取:
private void InitializeCoils()
{
_buzzerCoil = _deviceCmdService.GetCoilByEnum(DeviceType.DigitalIO, DigitalDOCodes.Buzzer);
_blueLedCoil = _deviceCmdService.GetCoilByEnum(DeviceType.DigitalIO, DigitalDOCodes.BlueLed);
_greenLedCoil = _deviceCmdService.GetCoilByEnum(DeviceType.DigitalIO, DigitalDOCodes.GreenLed);
_orangeLedCoil = _deviceCmdService.GetCoilByEnum(DeviceType.DigitalIO, DigitalDOCodes.OrangeLed);
_redLedCoil = _deviceCmdService.GetCoilByEnum(DeviceType.DigitalIO, DigitalDOCodes.RedLed);
}
用枚举而不是硬编码地址,换硬件时只改枚举映射,不用改业务代码。

状态更新逻辑
定时器回调里更新所有灯的状态:
private async Task UpdateTowerLightStateAsync()
{
if (!IsDeviceConnected)
{
_logger.Debug("DigitalIO设备未连接,跳过灯塔状态更新");
return;
}
// 1. 蓝灯:连接状态
var isConnected = _deviceCmdService.DigitalIO?.IsConnected ?? false;
if (BlueLedOn != isConnected)
{
await SetBlueLedAsync(isConnected);
}
// 2. 获取当前报警状态
// 合并硬件报警(DI)与软件追踪报警
var hwAlarmState = _coilStatusHelper.CalculateTriggerStatus();
var swAlarmState = _status.TriggerStatus;
var currentAlarmState = (hwAlarmState == TriggerStatus.Alarm || swAlarmState == TriggerStatus.Alarm)
? (int)TriggerStatus.Alarm
: (int)TriggerStatus.NoAlarm;
// 3. 红灯:报警状态
// 4. 绿灯/橙灯:运行状态
// 5. 蜂鸣器:橙灯或红灯亮时触发
}
红灯的逻辑有个细节:硬件报警和软件追踪报警要合并。任何一个触发,红灯都亮。这俩来源不同——硬件报警来自DI输入,软件追踪报警来自 TraceMonitorService 写入的状态。

橙灯的保持逻辑
橙灯表示"完成",有个特殊逻辑:Sequence从运行状态(RunState1)变回空闲(RunState0)时亮,一直保持到下次运行开始。
// 检测RunState从1→0的跳变
if (_previousRunState == (int)RunStatus.Running
&& currentRunState == (int)RunStatus.Idle)
{
_sequenceCompleted = true;
await SetOrangeLedAsync(true);
}
// 下次运行开始时熄灭橙灯
if (currentRunState == (int)RunStatus.Running)
{
_sequenceCompleted = false;
await SetOrangeLedAsync(false);
}
用 _sequenceCompleted 标志位追踪状态,避免重复设置。

蜂鸣器定时关闭
蜂鸣器不能一直响,响5秒自动关。用一个单独的定时器:
public async Task TriggerBuzzerAsync(int durationMs = 5000)
{
await SetBuzzerAsync(true);
_buzzerTimer?.Dispose();
_buzzerTimer = new Timer(async _ =>
{
await SetBuzzerAsync(false);
}, null, durationMs, Timeout.Infinite);
}
蜂鸣器持续时间从配置文件读取:
private void LoadConfiguration()
{
var iniConfig = ConfigReader.Instance.ReadConfig();
IsEnabled = iniConfig.TowerLightServiceEnable;
BuzzerDurationSeconds = iniConfig.BuzzerDurationSeconds > 0
? iniConfig.BuzzerDurationSeconds : 5;
}
Config.ini 里的配置:
[TowerLight]
TowerLightServiceEnable = True
BuzzerDurationSeconds = 5
UI状态绑定
所有灯的状态都是 [ObservableProperty],直接绑定到UI:
[ObservableProperty] private bool blueLedOn;
[ObservableProperty] private bool greenLedOn;
[ObservableProperty] private bool orangeLedOn;
[ObservableProperty] private bool redLedOn;
[ObservableProperty] private bool buzzerOn;
界面上用圆形图标表示灯塔状态,操作员一眼就能看到当前系统处于什么状态。
手动控制
除了自动模式,还支持手动控制,方便测试:
public async Task ManualControlAsync(bool blue, bool green, bool orange, bool red, bool buzzer)
{
await SetBlueLedAsync(blue);
await SetGreenLedAsync(green);
await SetOrangeLedAsync(orange);
await SetRedLedAsync(red);
await SetBuzzerAsync(buzzer);
}
停止时全部关闭
服务停止时关掉所有灯和蜂鸣器,避免灯一直亮着误导操作员:
public void StopMonitoring()
{
_monitorTimer.Change(Timeout.Infinite, Timeout.Infinite);
_buzzerTimer?.Change(Timeout.Infinite, Timeout.Infinite);
_ = TurnOffAllAsync();
IsRunning = false;
}
灯塔踩坑清单
| 坑 | 现象 | 解决 |
|---|---|---|
| 初始化误触发 | 启动时红灯闪一下 | 用安全默认值初始化_previousAlarmState |
| 蜂鸣器一直响 | 没有关闭定时器 | 单独Timer定时关闭 |
| 橙灯不保持 | 完成后马上灭 | _sequenceCompleted标志位 |
| 设备断开还在写 | 写入失败刷日志 | IsDeviceConnected检查 |
| 硬件软件报警没合并 | 软件报警红灯不亮 | hwAlarmState和swAlarmState取或 |
本篇小结
| 知识点 | 关键做法 |
|---|---|
| 五路输出 | 蜂鸣器 + 四色LED(蓝绿橙红) |
| 轮询监控 | 500ms Timer定时更新 |
| 状态合并 | 硬件报警 + 软件追踪报警取或 |
| 橙灯保持 | 标志位追踪,下次运行才灭 |
| 蜂鸣器定时 | 单独Timer 5秒后自动关闭 |
| 配置化 | Config.ini控制启用和蜂鸣时长 |
灯塔的价值在于:不需要操作员看屏幕,一眼就知道系统状态。
下期预告
第13篇:安全监控服务
灯塔是"显示"安全状态,下篇讲"执行"安全动作——压力超标自动关阀门、断电自动启动保护。
:灯塔与蜂鸣器——工业界的仪表盘&spm=1001.2101.3001.5002&articleId=163966935&d=1&t=3&u=0e6cb51c65bc4cfaae54c36f3da02019)

被折叠的 条评论
为什么被折叠?



