编辑
2026-04-20
C#
00

目录

📌 上节回顾
💡 核心知识讲解
工业软件为什么要分层?
四层系统的"生命周期"
第一层:上位机(HMI)——设备的"眼睛和手"
第二层:SCADA——车间的"监控摄像头"
第三层:MES——车间的"大脑"
第四层:ERP——老板的"决策系统"
四层系统的协作流程
💻 VS2026 操作步骤
📋 完整代码示例
🏭 工业实战小案例
⚠️ 避坑提醒
这几个坑,我替你踩过了
📝 本节总结
📖 系列进度提示
💬 互动引导

你有没有遇到过这个场景:

车间主任指着一个旧监控界面说,"咱们需要把产量数据实时传给财务部,让他们看得清楚"。你心想,加个接口呗,简单。结果一开始研发,才发现问题没那么简单——设备层需要采集数据,车间需要实时管理,财务部需要统计汇总,公司老板需要看经营决策……

一个小小的数据传输需求,竟然涉及4个不同的软件系统。

那今天咱们就来搞清楚:工业现场这4个系统分别是啥,它们各自负责什么,怎么才能让它们互相配合,这样你下次接需求就不会再摸不着头脑。


📌 上节回顾

上一节我们学了什么是工业数字化,掌握了C#在工业领域能解决的真实问题——让工厂从纸质记录进化到数字管理。今天咱们进一步深入,学习工业现场到底有哪些软件系统,以及C#在这些系统开发中的位置。


💡 核心知识讲解

工业软件为什么要分层?

想象一个汽车制造工厂的流水线:

有人负责现场的机械手臂、压力表、温度计——这是设备层,需要有个程序24小时监控它们。有人负责统计这条产线今天生产了多少件产品、不良率多少——这是车间层,需要实时收集和管理数据。有人负责整个工厂的排产计划、物料采购、订单跟踪——这是企业层,需要看全工厂的大数据。

如果只用一个系统搞定所有事,会怎样?庞大、臃肿、维护困难、反应迟缓。

所以工业界早就找到了最优方案:分层架构。每层各司其职,层层递进,形成一条完整的信息流链条。

四层系统的"生命周期"

image.png

数据向上流动,命令向下流动。 这是工业软件最核心的原则。


第一层:上位机(HMI)——设备的"眼睛和手"

什么是上位机? 简单说,就是和设备"直接对话"的电脑程序。设备生产的数据、设备的工作状态,都要通过上位机才能看得见、控制得了。

对比维度上位机其他三层
响应时间毫秒级秒级或分钟级
连接对象直接连接硬件设备连接数据库、网络
核心任务实时控制和显示数据管理和决策
典型场景注塑机显示屏、焊接机器人控制面板办公系统、报表系统

一个真实例子: 注塑机前面的那个显示屏,上面显示实时温度、压力、产量计数——这就是上位机。它要在20毫秒内读取传感器数据并更新显示,绝对不能卡。

用C#写上位机,通常的技术方案是 WPF(Windows Presentation Foundation)或 WinForms。为什么?因为它们和硬件驱动通信更直接,画面刷新快,工业现场很需要。


第二层:SCADA——车间的"监控摄像头"

SCADA 是英文 Supervisory Control And Data Acquisition 的缩写,翻译成中文就是"监控与数据采集"系统。

你可以把它理解为:一个智能的监控摄像头,不仅能看,还能控制。

SCADA的核心特点
连接多台设备上位机,统一监控整个车间
实时采集来自不同设备的数据,合并展示
提供历史数据曲线,帮助分析设备运行情况
当某个设备出现异常,自动告警或者触发预案
支持远程干预,比如远程停机、参数调整

一个真实场景: 你在一个电子工厂担任设备主管,手底下有12条产线,每条产线有3~5台设备。原来你得挨个走过去看屏幕,才知道生产进度。有了SCADA,你坐在办公室的电脑上,一张大屏就能看到全部产线的实时状态——温度、压力、产量、不良品数——甚至还能设定阈值自动告警。

📌 这就是SCADA存在的意义:从"点对点"的单机监控,升级到"面对面"的全景监控。


第三层:MES——车间的"大脑"

MES 全称是 Manufacturing Execution System,直译是"制造执行系统"。

如果说SCADA是"监控",那MES就是"管理"。它不仅要看设备怎么样,还要追踪:

  • 这个订单跑到哪条产线了?
  • 已经生产了多少件,还需要多少件?
  • 这批产品的质检报告在哪?
  • 这个班组的效率怎么样?
  • 原材料还够不够用?

MES是连接上面的ERP和下面的SCADA/上位机的"桥梁"。

一个车间领班的日常工作流程,就是MES要做的事:

上班 → 看到今天的生产任务 → 分配给各条产线 → 实时查看进度 → 发现某台设备故障 → 调度维修 → 更新产能计划 → 下班前汇总日报表 → 提交给生产经理

这整个流程,就是MES在幕后自动化处理。


第四层:ERP——老板的"决策系统"

ERP 是 Enterprise Resource Planning,企业资源计划系统。

这是给老板和高管用的。他们不关心某台设备的温度是多少,只关心:

  • 这个月能赚多少钱?
  • 哪个客户最赚钱?
  • 库存积压了多少?
  • 这条产线的成本占比多少?
  • 采购的原材料还需要多久到?

ERP汇总全工厂的数据,给管理层提供决策支持。

关键认知:ERP看的是整个企业的经营全景,它的数据源就是来自MES、SCADA和各个业务系统。没有下面三层的数据,ERP就没有价值。


四层系统的协作流程

让我用一个真实的生产场景,说明这四层系统怎么协作:

场景:某电子工厂接到10000件订单,交货期7天。

阶段哪个系统负责干的事
D1早上ERP根据库存、产能、工人班次,规划这10000件应该分配到哪几条产线,什么时候完成
D1上午MES接收ERP的任务单,根据产线实时情况,生成每条产线的生产排期表
D1下午上位机+SCADA产线员工打开设备,上位机读取工艺参数,SCADA监控全产线实时运行
D2~D6MES实时收集每小时统计各产线产量、良品率、故障情况,自动调整排期以确保按时完成
D7下午ERP汇总统计本订单的成本、利润,更新库存,生成发货单,发给财务开票

你看,四层系统环环相扣。少了哪一层,整个流程都跑不起来。


💻 VS2026 操作步骤

下面咱们用VS2026创建一个简单的C#项目框架,来理解这四层系统怎么相互通信。

Step 1:新建多层架构项目结构

打开VS2026 > 文件 > 新建 > 项目,选择"创建新的解决方案",项目名称命名为 IndustrialSoftwareDemo。然后在解决方案中新建以下4个类库项目:

  • HMI.UpperMachine (上位机层)
  • SCADA.Monitoring (SCADA监控层)
  • MES.Production (MES制造执行层)
  • ERP.Business (ERP业务层)

操作方法: 右键解决方案 > 添加 > 新建项目 > 类库 (.NET),每个选一遍。

image.png

Step 2:安装工业通信库

这四层系统之间需要通过通信协议交换数据。我们使用 OPC UA(一种标准的工业通信协议)。

打开 工具 > NuGet包管理器 > 管理解决方案的NuGet包 > 搜索 OpcUaClient > 找到由 OPC Foundation 发布的 OPC.Foundation.NetStandard (版本1.4.374或更新)> 对所有4个项目安装。

操作方法:

搜索框输入"OPC" → 筛选结果看发布者是否是官方 → 点"安装"按钮 → 在弹出窗口选中所有4个项目 → 确认

提示:OPC UA是工业界的"通用语言",几乎所有设备制造商都支持。学会这个通信方式,你就能和99%的工业设备"对话"。

Step 3:定义跨层数据契约

在每一层之间,我们需要定义统一的数据格式(叫"契约")。这样即使上位机和MES是不同开发团队写的,他们也能无缝配合。

在项目根目录新建一个文件 SharedModels.cs,用Vibe Coding的方式,告诉Copilot生成数据模型。

打开VS2026的Copilot窗口(右上角的✨图标或Ctrl+Alt+I),输入以下Prompt:

创建一个用于工业软件分层的数据模型契约(C#)。 包括:DeviceStatus(包含温度、压力、生产数量、时间戳)、ProductionTask(包含任务 ID、目标数量、截止日期)、AlarmEvent(包含报警代码、严重级别、受影响设备)。 每个模型都应包含时间戳和验证方法。 请使用工业领域命名规范。

Copilot会生成完整的数据模型代码。选择"Apply",代码自动插入。


📋 完整代码示例

下面这段代码实现了:一个简化版的四层系统框架,展示数据如何从上位机层一路流向ERP层。

csharp
using System; using System.Collections.Generic; using System.Linq; // ============ 共享数据契约 ============ /// <summary> /// 设备实时状态(来自上位机) /// </summary> public class DeviceStatus { public string DeviceId { get; set; } // 设备编号,如"InjectionMachine_01" public float Temperature { get; set; } // 实时温度(℃) public float Pressure { get; set; } // 实时压力(MPa) public int ProductionCount { get; set; } // 本班次累计产量 public DateTime Timestamp { get; set; } // 数据采集时间 public bool IsAlarm { get; set; } // 是否告警 public string AlarmMessage { get; set; } // 告警信息 } /// <summary> /// 生产任务(来自MES向上位机下发) /// </summary> public class ProductionTask { public string TaskId { get; set; } // 任务ID public string ProductName { get; set; } // 产品名称 public int TargetQuantity { get; set; } // 目标产量 public int CurrentQuantity { get; set; } // 当前已生产数量 public DateTime Deadline { get; set; } // 交货期 public string AssignedDevice { get; set; } // 分配的设备 } /// <summary> /// 班次生产报表(MES汇总,提交给ERP) /// </summary> public class ShiftProductionReport { public string ShiftId { get; set; } // 班次ID public DateTime ShiftDate { get; set; } // 班次日期 public Dictionary<string, int> DeviceOutput { get; set; } // 各设备产量 public int TotalDefectives { get; set; } // 本班不良品数 public float EquipmentOee { get; set; } // 设备OEE(综合效率) public float CostPerUnit { get; set; } // 单位成本 } // ============ 第一层:上位机(HMI) ============ /// <summary> /// 上位机类,模拟从PLC读取设备数据 /// </summary> public class UpperMachineHMI { private List<DeviceStatus> _deviceStatuses = new List<DeviceStatus>(); private Random _random = new Random(); public UpperMachineHMI() { // 初始化模拟设备 _deviceStatuses.Add(new DeviceStatus { DeviceId = "InjectionMachine_01", Temperature = 0, Pressure = 0 }); _deviceStatuses.Add(new DeviceStatus { DeviceId = "InjectionMachine_02", Temperature = 0, Pressure = 0 }); } /// <summary> /// 每100毫秒读取一次设备数据(秒级响应) /// </summary> public DeviceStatus ReadDeviceData(string deviceId) { var device = _deviceStatuses.FirstOrDefault(d => d.DeviceId == deviceId); if (device == null) return null; // 模拟真实的设备数据变化 device.Temperature = 180 + (float)_random.NextDouble() * 20; // 180~200℃ device.Pressure = 90 + (float)_random.NextDouble() * 20; // 90~110 MPa device.ProductionCount += _random.Next(0, 3); // 每次增加0~2件 device.Timestamp = DateTime.Now; // 模拟告警:温度超过200℃时触发 if (device.Temperature > 200) { device.IsAlarm = true; device.AlarmMessage = "Temperature exceeds threshold"; } else { device.IsAlarm = false; device.AlarmMessage = ""; } return device; } /// <summary> /// 批量读取所有设备状态(SCADA会调用这个方法) /// </summary> public List<DeviceStatus> GetAllDeviceStatuses() { return _deviceStatuses.Select(d => ReadDeviceData(d.DeviceId)).ToList(); } } // ============ 第二层:SCADA系统 ============ /// <summary> /// SCADA监控系统,聚合所有上位机数据,提供实时告警和历史曲线 /// </summary> public class SCADAMonitoringSystem { private UpperMachineHMI _hmi; private List<DeviceStatus> _historicalData = new List<DeviceStatus>(); private Dictionary<string, int> _alarmCount = new Dictionary<string, int>(); public SCADAMonitoringSystem(UpperMachineHMI hmi) { _hmi = hmi; } /// <summary> /// 实时监控:每秒汇总一次所有设备状态 /// </summary> public void MonitorAllDevices() { var allStatuses = _hmi.GetAllDeviceStatuses(); foreach (var status in allStatuses) { _historicalData.Add(status); // 如果有告警,记录告警次数 if (status.IsAlarm) { if (!_alarmCount.ContainsKey(status.DeviceId)) _alarmCount[status.DeviceId] = 0; _alarmCount[status.DeviceId]++; // 触发告警机制 TriggerAlarm(status); } } } private void TriggerAlarm(DeviceStatus device) { Console.WriteLine($"🚨 [SCADA告警] 设备{device.DeviceId}{device.AlarmMessage},时间:{device.Timestamp:HH:mm:ss}"); } /// <summary> /// 提供历史数据曲线(给MES用于分析) /// </summary> public List<DeviceStatus> GetHistoricalData(string deviceId, int lastNRecords = 100) { return _historicalData .Where(d => d.DeviceId == deviceId) .TakeLast(lastNRecords) .ToList(); } } // ============ 第三层:MES系统 ============ /// <summary> /// MES制造执行系统,接收生产任务,下发给上位机,收集SCADA数据进行统计 /// </summary> public class MESProductionSystem { private List<ProductionTask> _tasks = new List<ProductionTask>(); private SCADAMonitoringSystem _scada; private Dictionary<string, int> _shiftOutput = new Dictionary<string, int>(); public MESProductionSystem(SCADAMonitoringSystem scada) { _scada = scada; } /// <summary> /// 接收来自ERP的生产计划任务 /// </summary> public void ReceiveProductionTask(ProductionTask task) { _tasks.Add(task); Console.WriteLine($"📋 [MES] 收到任务:{task.TaskId},目标产量{task.TargetQuantity}件,分配给{task.AssignedDevice}"); } /// <summary> /// 实时更新任务进度(从SCADA读取设备产量) /// </summary> public void UpdateTaskProgress() { foreach (var task in _tasks) { // 这里模拟从SCADA读取到的产量数据 task.CurrentQuantity += new Random().Next(0, 5); if (task.CurrentQuantity >= task.TargetQuantity) { Console.WriteLine($"✅ [MES] 任务{task.TaskId}完成!产量:{task.CurrentQuantity}/{task.TargetQuantity}"); } } } /// <summary> /// 汇总班次报表(给ERP用) /// </summary> public ShiftProductionReport GenerateShiftReport(string shiftId) { var totalOutput = _tasks.Sum(t => t.CurrentQuantity); var report = new ShiftProductionReport { ShiftId = shiftId, ShiftDate = DateTime.Now, TotalDefectives = new Random().Next(10, 50), // 模拟不良品 EquipmentOee = (float)(0.75 + new Random().NextDouble() * 0.15), // OEE: 75%~90% CostPerUnit = 45.5f // 单位成本 }; return report; } } // ============ 第四层:ERP系统 ============ /// <summary> /// ERP业务系统,汇总全工厂经营数据,提供决策支持 /// </summary> public class ERPBusinessSystem { private List<ShiftProductionReport> _reports = new List<ShiftProductionReport>(); /// <summary> /// 接收来自MES的班次报表 /// </summary> public void ReceiveShiftReport(ShiftProductionReport report) { _reports.Add(report); Console.WriteLine($"💰 [ERP] 班次{report.ShiftId}报表已入库,产量贡献度:{report.EquipmentOee:P}"); } /// <summary> /// 生成月度经营报表 /// </summary> public void GenerateMonthlyFinancialReport() { var totalProduction = _reports.Count; var avgCost = _reports.Average(r => r.CostPerUnit); var avgOee = _reports.Average(r => r.EquipmentOee); Console.WriteLine($"\n📊 [ERP月度报告]"); Console.WriteLine($" 班次数:{totalProduction}"); Console.WriteLine($" 平均单位成本:¥{avgCost:F2}"); Console.WriteLine($" 平均OEE:{avgOee:P2}"); Console.WriteLine($" 预估月利润:¥{totalProduction * (100 - avgCost):F0}"); } } // ============ 主程序:演示四层系统协作 ============ class Program { static void Main() { Console.WriteLine("=== 工业软件四层系统模拟 ===\n"); // 初始化各层系统 var hmi = new UpperMachineHMI(); var scada = new SCADAMonitoringSystem(hmi); var mes = new MESProductionSystem(scada); var erp = new ERPBusinessSystem(); // 第一步:ERP下发生产计划给MES var task = new ProductionTask { TaskId = "TASK_2026_001", ProductName = "注塑件-A123", TargetQuantity = 100, Deadline = DateTime.Now.AddHours(8), AssignedDevice = "InjectionMachine_01" }; mes.ReceiveProductionTask(task); // 第二步:MES向上位机发送工艺参数(这里简化,实际会通过OPC UA) Console.WriteLine("\n> MES向上位机发送工艺参数...\n"); // 第三步:模拟10秒的生产过程,期间SCADA不断监控 Console.WriteLine("> 生产开始,SCADA开始监控...\n"); for (int i = 0; i < 10; i++) { scada.MonitorAllDevices(); mes.UpdateTaskProgress(); System.Threading.Thread.Sleep(500); // 每0.5秒采集一次 } // 第四步:班次结束,MES生成报表给ERP Console.WriteLine("\n> 班次结束,MES生成班次报表...\n"); var report = mes.GenerateShiftReport("SHIFT_001"); erp.ReceiveShiftReport(report); // 第五步:ERP汇总分析 erp.GenerateMonthlyFinancialReport(); Console.WriteLine("\n✨ 演示结束"); } }

运行效果说明:

image.png

运行这个程序后,你会看到完整的数据流动过程:

  1. 上位机实时读取设备温度、压力、产量
  2. SCADA监控所有设备,发现温度超过200℃就触发告警
  3. MES实时更新任务进度,当产量达到目标就标记完成
  4. ERP最后收到班次报表,汇总出月度的成本和利润预测

这就是工业现场四层系统的真实协作模式。


🏭 工业实战小案例

场景需求: 一个注塑工厂有5条产线,每条产线上都有注塑机、模温机、周边设备。现在需要一个SCADA系统监控这5条产线,当任何设备温度超过阈值或压力异常时,自动发送钉钉告警到生产经理的手机,同时记录异常事件用于后续分析。

解题思路:

  1. 上位机部分:各设备通过OPC UA协议上报实时参数
  2. SCADA设计:创建一个中央监控服务,定时轮询所有设备状态
  3. 告警机制:设置多层阈值判断,触发时调用钉钉API推送消息
  4. 历史记录:异常事件落库到本地数据库或云存储

完整实现代码(包含钉钉告警集成):

csharp
using System; using System.Collections.Generic; using System.Net.Http; using System.Text.Json; using System.Threading.Tasks; using System.Linq; // 设备实时参数模型 public class DeviceRealTimeData { public string DeviceId { get; set; } // 设备编号 public string DeviceName { get; set; } // 设备名称 public float Temperature { get; set; } // 当前温度 public float TemperatureSetpoint { get; set; } // 温度设定值 public float Pressure { get; set; } // 当前压力 public bool IsRunning { get; set; } // 是否运行中 public DateTime LastReadTime { get; set; } // 最后读数时间 } // 告警事件模型 public class AlarmEvent { public string EventId { get; set; } public string DeviceId { get; set; } public string AlarmType { get; set; } // "HighTemp" / "LowTemp" / "HighPressure" 等 public string AlarmLevel { get; set; } // "Critical" / "Warning" / "Info" public string AlarmMessage { get; set; } public DateTime EventTime { get; set; } public bool IsResolved { get; set; } } // SCADA监控核心类 public class SCADAMonitoringService { // 告警阈值配置 private const float TEMP_WARNING = 190; // 温度警告阈值 private const float TEMP_CRITICAL = 210; // 温度紧急阈值 private const float PRESSURE_WARNING = 100; // 压力警告阈值 private const float PRESSURE_CRITICAL = 120; // 压力紧急阈值 private List<DeviceRealTimeData> _devices; private List<AlarmEvent> _alarmHistory = new List<AlarmEvent>(); private Dictionary<string, AlarmEvent> _activeAlarms = new Dictionary<string, AlarmEvent>(); // 当前活跃的告警 // 钉钉告警机制人员配置 private const string DINGDING_ROBOT_URL = "https://oapi.dingtalk.com/robot/send?access_token=YOUR_TOKEN"; private const string PRODUCTION_MANAGER_PHONE = "13800138000"; public SCADAMonitoringService() { InitializeDevices(); } // 初始化监控的5条产线(每条3台设备) private void InitializeDevices() { _devices = new List<DeviceRealTimeData> { // 产线1 new DeviceRealTimeData { DeviceId = "LINE_01_INJECTER", DeviceName = "注塑机-1", TemperatureSetpoint = 200 }, new DeviceRealTimeData { DeviceId = "LINE_01_MOLD_TEMP", DeviceName = "模温机-1", TemperatureSetpoint = 50 }, new DeviceRealTimeData { DeviceId = "LINE_01_DRYER", DeviceName = "干燥机-1", TemperatureSetpoint = 85 }, // 产线2 new DeviceRealTimeData { DeviceId = "LINE_02_INJECTER", DeviceName = "注塑机-2", TemperatureSetpoint = 200 }, new DeviceRealTimeData { DeviceId = "LINE_02_MOLD_TEMP", DeviceName = "模温机-2", TemperatureSetpoint = 50 }, new DeviceRealTimeData { DeviceId = "LINE_02_DRYER", DeviceName = "干燥机-2", TemperatureSetpoint = 85 }, // 产线3~5 简化 new DeviceRealTimeData { DeviceId = "LINE_03_INJECTER", DeviceName = "注塑机-3", TemperatureSetpoint = 200 }, new DeviceRealTimeData { DeviceId = "LINE_04_INJECTER", DeviceName = "注塑机-4", TemperatureSetpoint = 200 }, new DeviceRealTimeData { DeviceId = "LINE_05_INJECTER", DeviceName = "注塑机-5", TemperatureSetpoint = 200 } }; } /// <summary> /// 模拟从OPC UA读取设备实时数据 /// </summary> public void ReadDeviceDataFromOPCUA() { var random = new Random(); foreach (var device in _devices) { // 模拟设备温度波动(正常情况下在设定值±10℃内) if (device.DeviceName.Contains("注塑机")) { device.Temperature = device.TemperatureSetpoint + (float)(random.NextDouble() * 20 - 10); device.Pressure = 95 + (float)(random.NextDouble() * 15); // 95~110 MPa } else if (device.DeviceName.Contains("模温机")) { device.Temperature = device.TemperatureSetpoint + (float)(random.NextDouble() * 8 - 4); device.Pressure = 5 + (float)(random.NextDouble() * 2); } else { device.Temperature = device.TemperatureSetpoint + (float)(random.NextDouble() * 5 - 2.5f); device.Pressure = 2 + (float)(random.NextDouble() * 1); } // 随机制造一个异常(演示用) if (random.NextDouble() < 0.05) // 5%概率触发异常 { device.Temperature += 25; // 温度突然升高 } device.IsRunning = true; device.LastReadTime = DateTime.Now; } } /// <summary> /// 核心监控逻辑:检查所有设备状态,生成告警 /// </summary> public async Task MonitorAllDevicesAsync() { ReadDeviceDataFromOPCUA(); foreach (var device in _devices) { // 检查温度异常 if (device.Temperature > TEMP_CRITICAL) { await GenerateAndProcessAlarmAsync(device, "HighTemp", "Critical", $"🔴 紧急:{device.DeviceName}温度过高 {device.Temperature:F1}℃,已超过{TEMP_CRITICAL}℃!"); } else if (device.Temperature > TEMP_WARNING) { await GenerateAndProcessAlarmAsync(device, "HighTemp", "Warning", $"🟡 警告:{device.DeviceName}温度偏高 {device.Temperature:F1}℃,建议检查"); } // 检查压力异常 if (device.Pressure > PRESSURE_CRITICAL) { await GenerateAndProcessAlarmAsync(device, "HighPressure", "Critical", $"🔴 紧急:{device.DeviceName}压力过高 {device.Pressure:F1} MPa,已超过{PRESSURE_CRITICAL} MPa!"); } else if (device.Pressure > PRESSURE_WARNING) { await GenerateAndProcessAlarmAsync(device, "HighPressure", "Warning", $"🟡 警告:{device.DeviceName}压力偏高 {device.Pressure:F1} MPa,建议检查"); } // 检查告警解除 ResolveAlarmIfRecovered(device); } } /// <summary> /// 生成告警并推送到钉钉 /// </summary> private async Task GenerateAndProcessAlarmAsync(DeviceRealTimeData device, string alarmType, string level, string message) { string alarmKey = $"{device.DeviceId}_{alarmType}"; // 如果该告警已经存在且未解除,则不重复推送(避免刷屏) if (_activeAlarms.ContainsKey(alarmKey) && !_activeAlarms[alarmKey].IsResolved) { return; } var alarm = new AlarmEvent { EventId = Guid.NewGuid().ToString("N").Substring(0, 12), DeviceId = device.DeviceId, AlarmType = alarmType, AlarmLevel = level, AlarmMessage = message, EventTime = DateTime.Now, IsResolved = false }; _alarmHistory.Add(alarm); _activeAlarms[alarmKey] = alarm; // 推送钉钉告警消息 if (level == "Critical") { await SendDingDingAlarmAsync(alarm, PRODUCTION_MANAGER_PHONE); } // 本地日志输出 Console.WriteLine($"[{alarm.EventTime:HH:mm:ss}] {alarm.AlarmMessage}"); } /// <summary> /// 推送钉钉告警消息(使用机器人API) /// </summary> private async Task SendDingDingAlarmAsync(AlarmEvent alarm, string phoneNumber) { // 这里使用钉钉自定义机器人API var dingdingMessage = new { msgtype = "markdown", markdown = new { title = $"🚨 工业设备紧急告警 - {alarm.AlarmLevel}", text = $"**告警内容:** {alarm.AlarmMessage}\n" + $"**设备ID:** {alarm.DeviceId}\n" + $"**告警类型:** {alarm.AlarmType}\n" + $"**发生时间:** {alarm.EventTime:yyyy-MM-dd HH:mm:ss}\n" + $"**事件ID:** {alarm.EventId}\n\n" + $"@{phoneNumber} 请立即查看!" }, at = new { atMobiles = new[] { phoneNumber }, isAtAll = false } }; try { using (var client = new HttpClient()) { var json = JsonSerializer.Serialize(dingdingMessage); var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); var response = await client.PostAsync(DINGDING_ROBOT_URL, content); if (response.IsSuccessStatusCode) { Console.WriteLine($"✅ 钉钉告警已发送给 {phoneNumber}"); } } } catch (Exception ex) { Console.WriteLine($"❌ 钉钉告警发送失败: {ex.Message}"); } } /// <summary> /// 检查告警是否已恢复(可自动解除告警) /// </summary> private void ResolveAlarmIfRecovered(DeviceRealTimeData device) { var tempAlarmKey = $"{device.DeviceId}_HighTemp"; var pressureAlarmKey = $"{device.DeviceId}_HighPressure"; // 温度恢复正常 if (_activeAlarms.ContainsKey(tempAlarmKey) && !_activeAlarms[tempAlarmKey].IsResolved) { if (device.Temperature < TEMP_WARNING - 5) // 恢复到警告值以下5℃ { _activeAlarms[tempAlarmKey].IsResolved = true; Console.WriteLine($"✅ [告警解除] {device.DeviceName}温度已恢复正常({device.Temperature:F1}℃)"); } } // 压力恢复正常 if (_activeAlarms.ContainsKey(pressureAlarmKey) && !_activeAlarms[pressureAlarmKey].IsResolved) { if (device.Pressure < PRESSURE_WARNING - 5) { _activeAlarms[pressureAlarmKey].IsResolved = true; Console.WriteLine($"✅ [告警解除] {device.DeviceName}压力已恢复正常({device.Pressure:F1} MPa)"); } } } /// <summary> /// 生成异常事件日报表(给MES和ERP用) /// </summary> public void GenerateAlarmReport() { var todayAlarms = _alarmHistory.Where(a => a.EventTime.Date == DateTime.Now.Date).ToList(); var criticalCount = todayAlarms.Count(a => a.AlarmLevel == "Critical"); var warningCount = todayAlarms.Count(a => a.AlarmLevel == "Warning"); Console.WriteLine("\n📊 ===== 今日告警统计 ====="); Console.WriteLine($"紧急告警数:{criticalCount}次"); Console.WriteLine($"警告告警数:{warningCount}次"); Console.WriteLine($"总计:{todayAlarms.Count}次"); if (todayAlarms.Count > 0) { Console.WriteLine("\n详细记录:"); foreach (var alarm in todayAlarms.OrderByDescending(a => a.EventTime).Take(5)) { Console.WriteLine($" [{alarm.EventTime:HH:mm:ss}] {alarm.AlarmMessage} (ID:{alarm.EventId})"); } } } } // ========== 主程序演示 ========== class Program { static async Task Main() { Console.WriteLine("🏭 工业SCADA实时监控系统 - 多产线告警演示\n"); var scada = new SCADAMonitoringService(); // 模拟连续监控10次(间隔2秒) for (int cycle = 1; cycle <= 10; cycle++) { Console.WriteLine($"\n▶ 监控周期 {cycle}:"); Console.WriteLine("─────────────────────────────"); await scada.MonitorAllDevicesAsync(); System.Threading.Thread.Sleep(2000); // 间隔2秒 } // 最后生成报表 scada.GenerateAlarmReport(); Console.WriteLine("\n✨ 监控演示结束"); } }

image.png

运行后你会看到什么效果:

  1. 实时监控输出:每2秒扫描一次5条产线的所有设备温度和压力
  2. 自动告警:当温度或压力超过阈值,立即输出告警信息
  3. 钉钉推送:紧急告警会调用钉钉API,发送给生产经理手机(需配置真实的API Token)
  4. 告警解除:当数值恢复正常,自动标记告警为已解除
  5. 日报统计:最后汇总全天的告警统计和详细事件列表

这就是真实工厂现场最常见的SCADA告警系统实现方式。


⚠️ 避坑提醒

这几个坑,我替你踩过了

❌ 坑1:为了防止重复告警,把所有历史告警都放在内存

错误做法:

csharp
private List<AlarmEvent> _allAlarms; // 10万条告警全存内存

✅ 正确做法:

csharp
// 只保留最近1小时的活跃告警在内存,历史的写入数据库 private Dictionary<string, AlarmEvent> _activeAlarms; // 仅存当前活跃的 private Database _historyDb; // 历史告警写库

📌 原因:工业系统运行24小时,1天可能产生几千条告警。全存内存会导致程序越跑越慢,甚至崩溃。正确做法是只在内存维护"活跃告警"(未解除的),历史告警写进数据库或时间序列库(如InfluxDB),需要查询时再调。


❌ 坑2:直接拿Float来比较温度,遇到浮点数误差就翻车

错误做法:

csharp
if (device.Temperature == TEMP_CRITICAL) // ❌ 永远不会相等 { // 浮点数199.9999999... 永远 != 200 }

✅ 正确做法:

csharp
const float TOLERANCE = 0.1f; if (Math.Abs(device.Temperature - TEMP_CRITICAL) < TOLERANCE) { // ✅ 正确 }

📌 原因:浮点数在计算机里是近似表示的,直接用 == 比较会因为精度问题导致判断失败。工业系统对这种微小偏差很敏感,一不小心就漏掉告警。


❌ 坑3:同步调用钉钉API,一个请求慢就卡住整个监控

错误做法:

csharp
// 在主监控线程里同步发送钉钉 SendDingDingAlarm(alarm); // ❌ 阻塞3秒,期间设备数据读不到

✅ 正确做法:

csharp
// 异步发送,不阻塞主监控 _ = SendDingDingAlarmAsync(alarm); // ✅ 后台发送,主线程继续工作

📌 原因:设备监控的周期通常是100毫秒到1秒,不能有任何阻塞。如果在读设备和发告警之间加了网络请求,网络波动就会导致监控延迟,严重时会漏掉关键数据。正确做法是异步发送,核心监控逻辑永远保持高优先级。


📝 本节总结

学完本节,你掌握了工业现场最核心的系统架构:从设备硬件到企业经营决策,四层系统层层递进,分工明确。上位机负责和硬件对话,SCADA负责全景监控和告警,MES负责生产任务管理和统计,ERP负责经营分析和决策支持。这四层不是竖起来的烟囱,而是互相协作的生态——每一层的数据都向上流动,每一层的指令都向下传递。理解了这个架构,你就能明白为什么工业软件开发比普通软件难得多,也就明白了掌握这套技能为什么能值钱。


📖 系列进度提示

本文是《C# 工业数字化应用开发专家》系列第 001 节

上一节:【课程导读:什么是工业数字化,C#能做什么】

下一节:【C# 在工业领域的真实案例展示——从一个真实项目看全貌】(明天更新)


💬 互动引导

你在工作中遇到过类似的需求吗?比如需要给不同部门(生产、质检、财务)看同一份数据,但每个部门看到的层级不一样?或者需要把设备数据实时上报到云端?欢迎在评论区分享你的场景,也许下一篇案例就来自你的留言。

🔔 还没关注的同学记得点击关注,这个系列共420节课程,带你从工厂小白到工业软件开发专家。不需要多高的数学基础,也不需要多年的工作经验,只要你想学,咱们就一步步走。

本文作者:技术老小子

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!