一、项目背景:喷涂线为什么要做SCADA?

去年给某汽车零部件厂做的喷涂车间SCADA系统,现在还在生产线24小时跑着——之前他们的喷涂线全靠老师傅盯着:喷枪压力靠压力表读,温度靠温度计看,传送带速度凭感觉调,一旦出现“流挂”“橘皮”缺陷,要查半天是参数问题还是设备故障,返工率高得老板头疼。

客户的核心痛点很具体:

  1. 参数无记录:喷涂压力、温度、固化炉温度这些关键参数,换班时记在本子上,丢了还没法追溯;
  2. 故障难定位:喷枪堵了、传送带卡壳,要等产品出问题才发现,一停线就是半小时;
  3. 可视化差:车间只有几个孤立的仪表,没法全局看整条线的状态;
  4. 报表麻烦:每月统计产能、合格率,要人工汇总数据,错漏百出。

最终决定用Winform+WPF双框架搭SCADA:Winform做数据采集和后台配置(工控机跑着稳,技术员会操作),WPF做可视化监控(界面炫酷,老板看得懂)。现在系统能实时采集12个点位的数据,故障报警响应<3秒,合格率提升了8%——这篇就把从调试到落地的细节拆透,源码改改就能用在喷涂线上。

二、需求拆解与技术选型(为什么选双框架?)

1. 喷涂工艺SCADA核心需求清单

模块 具体要求
数据采集 采集喷枪压力(0-10MPa)、喷涂温度(0-80℃)、传送带速度(0-5m/min)、固化炉温度(0-200℃),频率1秒/次
可视化监控 WPF界面显示实时参数曲线、设备状态面板(绿色运行/红色故障/黄色预警)、生产线流程动画
报警系统 压力超上下限、温度异常、设备停机时,声光报警+短信通知管理员
参数配置 Winform界面改喷枪参数阈值、采集频率,技术员不用碰代码
数据追溯 按批次查历史参数曲线,生成日报/周报,支持Excel导出

2. 技术选型(双框架的优势)

一开始纠结只用Winform还是WPF,最后选双框架是因为:

  • Winform:兼容性拉满(车间工控机还是Win7),串口/TCP通信稳定,做采集后台和配置界面最合适;
  • WPF:数据绑定强、UI渲染炫酷,做可视化监控界面,老板和车间主任一眼就能看明白;
  • 通信协议:Modbus TCP(新设备)+ Modbus RTU(老喷枪控制器),喷涂设备主流协议;
  • 数据存储:MySQL存历史数据(按批次分表)+ SQLite存断网缓存(车间偶尔断网);
  • 可视化控件:LiveCharts(WPF实时图表)、DevExpress(Winform配置界面);
  • 跨框架通信:命名管道(Winform和WPF同机通信,延迟<10ms,比WCF轻量);
  • 报警硬件:声光报警器(RS485)+ 阿里云短信API(远程通知)。

三、核心实战:双框架协同开发

1. 第一步:Winform做数据采集(稳定是第一位)

Winform负责“脏活累活”:和设备通信、数据清洗、缓存、报警判断。这里封装Modbus通信类,处理喷涂车间的电磁干扰问题。

Modbus通信封装(抗干扰版)
using System;
using System.Threading;
using Modbus.Device;
using System.Net.Sockets;
using System.IO.Ports;

namespace SprayScada.Collect
{
    /// <summary>
    /// 喷涂设备Modbus通信助手(抗电磁干扰优化)
    /// </summary>
    public class SprayModbusHelper : IDisposable
    {
        private IModbusMaster _master;
        private string _connType; // TCP/RTU
        private string _ip;
        private int _port;
        private string _comPort;
        private int _baudRate;
        private readonly object _lockObj = new object();

        /// <summary>
        /// 初始化(喷涂车间要加CRC校验和重试)
        /// </summary>
        public bool Init(string connType, string ip, int port, string comPort, int baudRate)
        {
            lock (_lockObj)
            {
                try
                {
                    _connType = connType;
                    if (connType == "TCP")
                    {
                        TcpClient client = new TcpClient();
                        client.Connect(ip, port);
                        _master = ModbusIpMaster.CreateIp(client);
                        ((ModbusIpMaster)_master).Transport.Retries = 2; // 重试2次
                    }
                    else if (connType == "RTU")
                    {
                        SerialPort sp = new SerialPort(comPort, baudRate)
                        {
                            Parity = Parity.Even, // 老喷枪控制器用偶校验
                            StopBits = StopBits.One,
                            ReadTimeout = 1500 // 车间干扰大,超时设长点
                        };
                        sp.Open();
                        _master = ModbusSerialMaster.CreateRtu(sp);
                    }
                    _master.Transport.ReadTimeout = 2000;
                    return true;
                }
                catch (Exception ex)
                {
                    LogHelper.Error($"喷涂设备连接失败:{ex.Message}");
                    return false;
                }
            }
        }

        /// <summary>
        /// 读取喷枪压力(寄存器0x0001,系数0.01MPa)
        /// </summary>
        public float ReadSprayPressure(byte slaveId)
        {
            for (int i = 0; i < 3; i++) // 3次重试,抗干扰
            {
                try
                {
                    ushort[] regs = _master.ReadHoldingRegisters(slaveId, 0x0001, 1);
                    if (regs != null && regs.Length > 0)
                    {
                        float pressure = regs[0] * 0.01f;
                        // 过滤异常值(压力不可能超过10MPa)
                        if (pressure >= 0 && pressure <= 10) return pressure;
                    }
                }
                catch { Thread.Sleep(300); }
            }
            return -1; // 采集失败标记
        }

        public void Dispose()
        {
            if (_master != null) _master.Dispose();
        }
    }
}
采集线程与跨框架数据推送

Winform启动采集线程,把数据通过命名管道推给WPF(同机通信最快的方式):

using System.IO.Pipes;
using System.Threading;
using System.Text.Json;

namespace SprayScada.Collect
{
    public class DataCollectManager
    {
        private SprayModbusHelper _modbusHelper;
        private Thread _collectThread;
        private bool _isRunning;
        private NamedPipeServerStream _pipeServer; // 命名管道服务端

        public void StartCollect()
        {
            _modbusHelper = new SprayModbusHelper();
            if (!_modbusHelper.Init("TCP", "192.168.1.10", 502, "", 0)) return;

            // 启动命名管道服务端
            _pipeServer = new NamedPipeServerStream("SprayScadaPipe", PipeDirection.Out);
            _pipeServer.WaitForConnection();

            _isRunning = true;
            _collectThread = new Thread(CollectLoop) { IsBackground = true };
            _collectThread.Start();
        }

        private void CollectLoop()
        {
            while (_isRunning)
            {
                try
                {
                    // 采集关键参数
                    SprayData data = new SprayData
                    {
                        Pressure = _modbusHelper.ReadSprayPressure(1),
                        Temperature = _modbusHelper.ReadSprayTemperature(1),
                        Speed = _modbusHelper.ReadConveyorSpeed(1),
                        OvenTemp = _modbusHelper.ReadOvenTemperature(1),
                        CollectTime = DateTime.Now
                    };

                    // 推给WPF
                    string json = JsonSerializer.Serialize(data);
                    byte[] buffer = System.Text.Encoding.UTF8.GetBytes(json);
                    _pipeServer.Write(buffer, 0, buffer.Length);

                    // 报警判断(压力低于0.5MPa或高于8MPa报警)
                    if (data.Pressure < 0.5 || data.Pressure > 8)
                    {
                        AlarmManager.TriggerAlarm("喷枪压力异常", data.Pressure.ToString("F2") + "MPa");
                    }

                    Thread.Sleep(1000); // 1秒采集一次
                }
                catch (Exception ex)
                {
                    LogHelper.Error($"采集异常:{ex.Message}");
                }
            }
        }
    }

    // 喷涂数据实体
    public class SprayData
    {
        public float Pressure { get; set; }
        public float Temperature { get; set; }
        public float Speed { get; set; }
        public float OvenTemp { get; set; }
        public DateTime CollectTime { get; set; }
    }
}

2. 第二步:WPF做可视化监控(老板看得懂的界面)

WPF负责把Winform推过来的数据做成可视化,重点是实时曲线设备状态面板,用LiveCharts做图表,绑定数据后自动刷新。

WPF接收数据并绑定图表
using System.IO.Pipes;
using System.Threading;
using System.Text.Json;
using LiveCharts;
using LiveCharts.Wpf;

namespace SprayScada.Monitor
{
    public partial class MainWindow : Window
    {
        private NamedPipeClientStream _pipeClient;
        private Thread _receiveThread;
        private bool _isRunning;

        // 图表数据
        public SeriesCollection PressureSeries { get; set; }
        public List<string> TimeLabels { get; set; }

        public MainWindow()
        {
            InitializeComponent();

            // 初始化图表
            PressureSeries = new SeriesCollection
            {
                new LineSeries
                {
                    Title = "喷枪压力(MPa)",
                    Values = new ChartValues<float>(),
                    PointGeometrySize = 5,
                    LineSmoothness = 0.2
                }
            };
            TimeLabels = new List<string>();
            DataContext = this;

            // 连接命名管道
            _pipeClient = new NamedPipeClientStream(".", "SprayScadaPipe", PipeDirection.In);
            _pipeClient.Connect();
            _isRunning = true;
            _receiveThread = new Thread(ReceiveLoop) { IsBackground = true };
            _receiveThread.Start();
        }

        private void ReceiveLoop()
        {
            while (_isRunning)
            {
                try
                {
                    byte[] buffer = new byte[1024];
                    int bytesRead = _pipeClient.Read(buffer, 0, buffer.Length);
                    if (bytesRead > 0)
                    {
                        string json = System.Text.Encoding.UTF8.GetString(buffer, 0, bytesRead);
                        SprayData data = JsonSerializer.Deserialize<SprayData>(json);

                        // 跨线程更新UI
                        Dispatcher.Invoke(() =>
                        {
                            // 更新实时参数面板
                            txtPressure.Text = $"{data.Pressure:F2} MPa";
                            txtTemp.Text = $"{data.Temperature:F1} ℃";
                            txtSpeed.Text = $"{data.Speed:F1} m/min";
                            txtOven.Text = $"{data.OvenTemp:F1} ℃";

                            // 更新设备状态(压力异常标红)
                            if (data.Pressure < 0.5 || data.Pressure > 8)
                            {
                                borderPressure.Background = Brushes.Red;
                            }
                            else
                            {
                                borderPressure.Background = Brushes.Green;
                            }

                            // 更新图表(只显示最近20个点)
                            PressureSeries[0].Values.Add(data.Pressure);
                            TimeLabels.Add(data.CollectTime.ToString("HH:mm:ss"));
                            if (PressureSeries[0].Values.Count > 20)
                            {
                                PressureSeries[0].Values.RemoveAt(0);
                                TimeLabels.RemoveAt(0);
                            }
                            chartPressure.AxisX[0].Labels = TimeLabels;
                        });
                    }
                }
                catch (Exception ex)
                {
                    Dispatcher.Invoke(() => { txtLog.Text += $"接收数据异常:{ex.Message}\r\n"; });
                }
            }
        }
    }
}
WPF生产线流程动画

用WPF的Canvas做喷涂线流程动画,模拟传送带移动、喷枪喷涂:

<Canvas Width="800" Height="200" Background="#F5F5F5">
    <!-- 传送带 -->
    <Rectangle Width="700" Height="30" Canvas.Left="50" Canvas.Top="100" Fill="#D3D3D3"/>
    <!-- 产品工件 -->
    <Ellipse x:Name="partEllipse" Width="20" Height="20" Canvas.Left="50" Canvas.Top="90" Fill="#FF6347">
        <Ellipse.Triggers>
            <EventTrigger RoutedEvent="Loaded">
                <BeginStoryboard>
                    <Storyboard RepeatBehavior="Forever">
                        <DoubleAnimation 
                            Storyboard.TargetName="partEllipse"
                            Storyboard.TargetProperty="(Canvas.Left)"
                            From="50" To="700" Duration="0:0:10"/>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </Ellipse.Triggers>
    </Ellipse>
    <!-- 喷枪 -->
    <Rectangle Width="10" Height="50" Canvas.Left="300" Canvas.Top="50" Fill="#696969"/>
    <!-- 喷涂效果(粒子动画) -->
    <Rectangle Width="20" Height="30" Canvas.Left="300" Canvas.Top="100" Fill="#FFD700" Opacity="0.5">
        <Rectangle.Triggers>
            <EventTrigger RoutedEvent="Loaded">
                <BeginStoryboard>
                    <Storyboard RepeatBehavior="Forever">
                        <DoubleAnimation 
                            Storyboard.TargetProperty="Opacity"
                            From="0.5" To="0.2" Duration="0:0:0.5" AutoReverse="True"/>
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </Rectangle.Triggers>
    </Rectangle>
</Canvas>

3. 第三步:报警与报表模块(商用级必备)

声光报警+短信通知
using System.IO.Ports;
using Aliyun.Acs.Dysmsapi.Model.V20170525; // 阿里云短信SDK

namespace SprayScada.Alarm
{
    public class AlarmManager
    {
        private static SerialPort _alarmPort; // 声光报警器串口

        static AlarmManager()
        {
            // 初始化声光报警器
            _alarmPort = new SerialPort("COM4", 9600);
            _alarmPort.Open();
        }

        /// <summary>
        /// 触发报警(声光+短信)
        /// </summary>
        public static void TriggerAlarm(string title, string content)
        {
            // 声光报警:发送0x01命令
            _alarmPort.Write(new byte[] { 0x01 }, 0, 1);

            // 短信通知管理员
            SendSms("13800138000", $"【喷涂SCADA】{title}{content}");

            // 记录报警日志
            LogHelper.Alarm($"[{DateTime.Now}] {title} - {content}");
        }

        private static void SendSms(string phone, string content)
        {
            // 阿里云短信API调用(省略配置代码,官网有示例)
            SendSmsRequest request = new SendSmsRequest();
            request.PhoneNumbers = phone;
            request.SignName = "喷涂SCADA";
            request.TemplateCode = "SMS_123456789";
            request.TemplateParam = $"{{\"content\":\"{content}\"}}";
            // 发送请求...
        }
    }
}
Winform报表导出(DevExpress)
using DevExpress.XtraReports.UI;
using System.Data;

namespace SprayScada.Report
{
    public class ReportManager
    {
        /// <summary>
        /// 生成日报表并导出Excel
        /// </summary>
        public static void GenerateDailyReport(DateTime date)
        {
            // 查询当日数据
            DataTable dt = MySqlHelper.Query($"SELECT * FROM spray_data WHERE DATE(collect_time)='{date:yyyy-MM-dd}'");

            // 绑定DevExpress报表
            XtraReport report = new XtraReport1();
            report.DataSource = dt;
            report.ExportToXlsx($"喷涂日报_{date:yyyyMMdd}.xlsx");
            LogHelper.Info($"日报表导出成功:喷涂日报_{date:yyyyMMdd}.xlsx");
        }
    }
}

四、车间踩坑实录(这些坑让系统稳定了半年)

坑1:喷枪压力采集跳变(电磁干扰是元凶)

  • 现象:采集的压力值从5MPa突然跳到9MPa,然后又跳回来;
  • 原因:喷涂车间有静电和电磁干扰,传感器信号线没接地;
  • 解决
    1. 给传感器信号线套屏蔽层,屏蔽层一端接地;
    2. 在采集代码里加“连续读两次,差值<0.2MPa才采用”的过滤逻辑;
    3. 把Modbus RTU换成Modbus TCP,抗干扰更强。

坑2:Winform和WPF通信卡顿

  • 现象:WPF界面的数据延迟越来越大,最后卡住;
  • 原因:命名管道没做流量控制,采集数据堆积;
  • 解决:在Winform端每次推送前检查管道缓冲区,满了就清空;WPF端用异步读取,避免阻塞。

坑3:WPF图表刷新闪烁

  • 现象:实时曲线每更新一次就闪一下,看着眼晕;
  • 原因:每次更新都重新设置AxisX的Labels,导致控件重绘;
  • 解决:用ObservableCollection绑定Labels,只增删元素不重置整个集合。

坑4:固化炉温度采集不准

  • 现象:采集的温度比实际低10℃;
  • 原因:温度传感器探头位置不对,离加热管太远;
  • 解决:让车间师傅把探头移到工件附近,代码里加校准系数(采集值+10)。

五、总结:双框架SCADA的优势与拓展

这套双框架SCADA的好处很明显:Winform把“稳定”做到位,WPF把“体验”做到位,既满足工控机的兼容性,又满足管理层的可视化需求。现在生产线的返工率从12%降到了4%,老板再也不用天天蹲车间了。

如果要拓展,还能加这些功能:

  1. AI质检:接工业相机,用OpenCV识别喷涂缺陷,联动SCADA调整参数;
  2. 远程监控:加个WebAPI,用手机APP看实时数据;
  3. 能耗统计:采集空压机、加热炉的能耗,分析节能空间;
  4. 配方管理:不同工件的喷涂参数存成配方,一键切换。

最后提一句:做工业SCADA,别光盯着UI炫酷,现场的抗干扰、数据的准确性、系统的稳定性才是核心——毕竟车间里的设备不会因为代码“好看”就不闹脾气。源码里的核心模块都能直接搬,有问题评论区交流就行。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐