你有没有遇到过这个场景:
车间主任指着一个旧监控界面说,"咱们需要把产量数据实时传给财务部,让他们看得清楚"。你心想,加个接口呗,简单。结果一开始研发,才发现问题没那么简单——设备层需要采集数据,车间需要实时管理,财务部需要统计汇总,公司老板需要看经营决策……
一个小小的数据传输需求,竟然涉及4个不同的软件系统。
那今天咱们就来搞清楚:工业现场这4个系统分别是啥,它们各自负责什么,怎么才能让它们互相配合,这样你下次接需求就不会再摸不着头脑。
上一节我们学了什么是工业数字化,掌握了C#在工业领域能解决的真实问题——让工厂从纸质记录进化到数字管理。今天咱们进一步深入,学习工业现场到底有哪些软件系统,以及C#在这些系统开发中的位置。
想象一个汽车制造工厂的流水线:
有人负责现场的机械手臂、压力表、温度计——这是设备层,需要有个程序24小时监控它们。有人负责统计这条产线今天生产了多少件产品、不良率多少——这是车间层,需要实时收集和管理数据。有人负责整个工厂的排产计划、物料采购、订单跟踪——这是企业层,需要看全工厂的大数据。
如果只用一个系统搞定所有事,会怎样?庞大、臃肿、维护困难、反应迟缓。
所以工业界早就找到了最优方案:分层架构。每层各司其职,层层递进,形成一条完整的信息流链条。

数据向上流动,命令向下流动。 这是工业软件最核心的原则。
什么是上位机? 简单说,就是和设备"直接对话"的电脑程序。设备生产的数据、设备的工作状态,都要通过上位机才能看得见、控制得了。
| 对比维度 | 上位机 | 其他三层 |
|---|---|---|
| 响应时间 | 毫秒级 | 秒级或分钟级 |
| 连接对象 | 直接连接硬件设备 | 连接数据库、网络 |
| 核心任务 | 实时控制和显示 | 数据管理和决策 |
| 典型场景 | 注塑机显示屏、焊接机器人控制面板 | 办公系统、报表系统 |
一个真实例子: 注塑机前面的那个显示屏,上面显示实时温度、压力、产量计数——这就是上位机。它要在20毫秒内读取传感器数据并更新显示,绝对不能卡。
用C#写上位机,通常的技术方案是 WPF(Windows Presentation Foundation)或 WinForms。为什么?因为它们和硬件驱动通信更直接,画面刷新快,工业现场很需要。
SCADA 是英文 Supervisory Control And Data Acquisition 的缩写,翻译成中文就是"监控与数据采集"系统。
你可以把它理解为:一个智能的监控摄像头,不仅能看,还能控制。
| SCADA的核心特点 |
|---|
| 连接多台设备上位机,统一监控整个车间 |
| 实时采集来自不同设备的数据,合并展示 |
| 提供历史数据曲线,帮助分析设备运行情况 |
| 当某个设备出现异常,自动告警或者触发预案 |
| 支持远程干预,比如远程停机、参数调整 |
一个真实场景: 你在一个电子工厂担任设备主管,手底下有12条产线,每条产线有3~5台设备。原来你得挨个走过去看屏幕,才知道生产进度。有了SCADA,你坐在办公室的电脑上,一张大屏就能看到全部产线的实时状态——温度、压力、产量、不良品数——甚至还能设定阈值自动告警。
📌 这就是SCADA存在的意义:从"点对点"的单机监控,升级到"面对面"的全景监控。
MES 全称是 Manufacturing Execution System,直译是"制造执行系统"。
如果说SCADA是"监控",那MES就是"管理"。它不仅要看设备怎么样,还要追踪:
MES是连接上面的ERP和下面的SCADA/上位机的"桥梁"。
一个车间领班的日常工作流程,就是MES要做的事:
上班 → 看到今天的生产任务 → 分配给各条产线 → 实时查看进度 → 发现某台设备故障 → 调度维修 → 更新产能计划 → 下班前汇总日报表 → 提交给生产经理
这整个流程,就是MES在幕后自动化处理。
ERP 是 Enterprise Resource Planning,企业资源计划系统。
这是给老板和高管用的。他们不关心某台设备的温度是多少,只关心:
ERP汇总全工厂的数据,给管理层提供决策支持。
关键认知:ERP看的是整个企业的经营全景,它的数据源就是来自MES、SCADA和各个业务系统。没有下面三层的数据,ERP就没有价值。
让我用一个真实的生产场景,说明这四层系统怎么协作:
场景:某电子工厂接到10000件订单,交货期7天。
| 阶段 | 哪个系统负责 | 干的事 |
|---|---|---|
| D1早上 | ERP | 根据库存、产能、工人班次,规划这10000件应该分配到哪几条产线,什么时候完成 |
| D1上午 | MES | 接收ERP的任务单,根据产线实时情况,生成每条产线的生产排期表 |
| D1下午 | 上位机+SCADA | 产线员工打开设备,上位机读取工艺参数,SCADA监控全产线实时运行 |
| D2~D6 | MES实时收集 | 每小时统计各产线产量、良品率、故障情况,自动调整排期以确保按时完成 |
| D7下午 | ERP汇总 | 统计本订单的成本、利润,更新库存,生成发货单,发给财务开票 |
你看,四层系统环环相扣。少了哪一层,整个流程都跑不起来。
下面咱们用VS2026创建一个简单的C#项目框架,来理解这四层系统怎么相互通信。
Step 1:新建多层架构项目结构
打开VS2026 > 文件 > 新建 > 项目,选择"创建新的解决方案",项目名称命名为 IndustrialSoftwareDemo。然后在解决方案中新建以下4个类库项目:
HMI.UpperMachine (上位机层)SCADA.Monitoring (SCADA监控层)MES.Production (MES制造执行层)ERP.Business (ERP业务层)操作方法: 右键解决方案 > 添加 > 新建项目 > 类库 (.NET),每个选一遍。

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层。
csharpusing 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✨ 演示结束");
}
}
运行效果说明:

运行这个程序后,你会看到完整的数据流动过程:
这就是工业现场四层系统的真实协作模式。
场景需求: 一个注塑工厂有5条产线,每条产线上都有注塑机、模温机、周边设备。现在需要一个SCADA系统监控这5条产线,当任何设备温度超过阈值或压力异常时,自动发送钉钉告警到生产经理的手机,同时记录异常事件用于后续分析。
解题思路:
完整实现代码(包含钉钉告警集成):
csharpusing 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✨ 监控演示结束");
}
}

运行后你会看到什么效果:
这就是真实工厂现场最常见的SCADA告警系统实现方式。
❌ 坑1:为了防止重复告警,把所有历史告警都放在内存
错误做法:
csharpprivate List<AlarmEvent> _allAlarms; // 10万条告警全存内存
✅ 正确做法:
csharp// 只保留最近1小时的活跃告警在内存,历史的写入数据库
private Dictionary<string, AlarmEvent> _activeAlarms; // 仅存当前活跃的
private Database _historyDb; // 历史告警写库
📌 原因:工业系统运行24小时,1天可能产生几千条告警。全存内存会导致程序越跑越慢,甚至崩溃。正确做法是只在内存维护"活跃告警"(未解除的),历史告警写进数据库或时间序列库(如InfluxDB),需要查询时再调。
❌ 坑2:直接拿Float来比较温度,遇到浮点数误差就翻车
错误做法:
csharpif (device.Temperature == TEMP_CRITICAL) // ❌ 永远不会相等
{
// 浮点数199.9999999... 永远 != 200
}
✅ 正确做法:
csharpconst 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 许可协议。转载请注明出处!