C# CNC数据采集系统开发:协议选型、数据清洗与避坑实践
简介基于C#的CNC数据采集系统源码与数据集是一套面向个人学习者的完整实践资源适合正在接触工业自动化、希望掌握Windows平台下CNC机床数据采集与处理的开发者。资源围绕FANUC数控系统展开涵盖实时监控、数据记录、故障诊断和历史数据分析等核心模块能够帮助读者理解从机床通信、数据解析到界面展示的完整链路。包体共包含29个文件整体大小约652KB以C#源码cs、界面资源resx、项目配置config以及FANUC官方动态库dll为主另有数据集定义文件xsd和备份源码zbak便于对照学习和调试还原。资源还附带README说明与项目结构文件目录清晰适合按模块渐进阅读。目前已有167人学习下载对于想要深入了解CNC通信协议、.NET框架应用以及工业数据可视化的人来说是一份高性价比的入门参考。通过研读源码和数据集学习者可以掌握真实环境下的数据采集思路并为后续开发高效可靠的智能制造系统打下扎实基础。1. 基于C#的CNC数据采集系统解决的是车间里最朴素却最头疼的问题基于C#的CNC数据采集系统解决的是车间里最朴素却最头疼的问题把机床屏幕上的加工数据变成电脑里能计算、能分析的结构化数据。一台加工中心每秒钟都在产生程序号、主轴负载、进给倍率、报警代码但这些数据大多数时候只停在机床屏幕上等着操作工抄进Excel。C#在Windows生态下做上位机积累深串口、TCP、以太网、OPC都有现成库开发效率高部署也比Python稳。这套方案会从通信协议层讲到代码结构再告诉你采集到的原始数据和能用来分析的训练数据之间还差多少步。适合正在做机床联网的工程师、做数字化交付的乙方以及想给自家产线补数据底座的设备部负责人。2. 采集通道怎么选FOCAS、MTConnect还是裸TCP直读2.1 三种常见通道的设计差异做CNC数据采集第一件事不是写代码是确认机床控制器到底给你开了哪几扇门。常见通道就三类。第一类是FOCAS发那科CNC提供的以太网通信库可以读到系统变量、坐标、主轴负载、报警历史和程序运行状态几乎用发那科控制器的机床都能走这条通道。第二类是MTConnectAMT推的开放标准协议基于HTTP和XML主流机床在近几年系统中都内置了agent数据按设备和数据项的方式暴露。第三类是裸TCP或者串口直读很多国产数控系统、老式控制器或者PLC网关干脆把地址表开放给你自己构造报文去读这类接入在现有车间里占了很大比例。三个通道对应三种开发模式。FOCAS是动态库调用C#通过DllImport或者封装好的NuGet包去调用数据深度高宏变量、PMC信号都能读但格式多为厂商私有换一个品牌就要重写适配。MTConnect是标准HTTP请求C#直接用HttpClient拉数据数据结构是XML有统一的数据项标识扩展性好但采样频率通常只能到100毫秒到1秒的粒度做实时性强的控制不够用。裸TCP直读最灵活数据格式是定长字节流收发都由自己控制延迟最低但协议文档往往藏在机床说明书里解析费劲不同系统格式还不一致经常要花大量时间去对齐。选型上我一般这样判断单台机床做深要做刀具寿命预测这类偏状态分析的优先FOCAS或宏变量读取车间层做采集汇总需要把多品牌统一收口的优先MTConnect如果现场已经装了PLC网关或者国产系统地址表清楚直接走裸TCP省掉一层转换实时性更好。没有哪个方案一劳永逸得根据当前产线实际情况定。2.2 裸TCP读机床状态的最小示例假设现场是一台支持TCP Modbus协议的数控系统手册给出保持寄存器地址40001是主轴负载百分比40002是进给倍率40003是主轴转速都是16位无符号整数。C#里要实现的就是建立TCP连接按Modbus TCP协议构造读取保持寄存器的报文解析返回字节。下面是一个可运行的最小示例using System.Net.Sockets; using System.Buffers.Binary; public class CncTcpClient { private TcpClient _tcp; private NetworkStream _stream; private ushort _transactionId 0; public bool Connect(string ip, int port 502) { _tcp new TcpClient(); _tcp.Connect(ip, port); _stream _tcp.GetStream(); return _stream.CanRead; } public ushort[] ReadHoldingRegisters(byte unitId, ushort startAddress, ushort count) { _transactionId; Spanbyte request stackalloc byte[12]; BinaryPrimitives.WriteUInt16BigEndian(request, _transactionId); BinaryPrimitives.WriteUInt16BigEndian(request[2..], 0); BinaryPrimitives.WriteUInt16BigEndian(request[4..], 6); request[6] unitId; request[7] 0x03; BinaryPrimitives.WriteUInt16BigEndian(request[8..], startAddress); BinaryPrimitives.WriteUInt16BigEndian(request[10..], count); lock (this) { _stream.Write(request); Spanbyte response stackalloc byte[5 count * 2]; _stream.Read(response); if (response[1] ! 0x03) throw new Exception(功能码异常); ushort[] values new ushort[count]; for (int i 0; i count; i) { values[i] BinaryPrimitives.ReadUInt16BigEndian(response[3 i * 2..]); } return values; } } }这段代码的核心是Modbus TCP报文协议。前6个字节是MBAP头事务ID用来匹配请求和响应协议ID固定为0后续字节长度指从单元标识开始的长度。第7字节是功能码0x03表示读保持寄存器读主轴负载、倍率、转速这类模拟量都用它。第8到第11字节是起始地址和读取数量对应40001到40003这种保持寄存器地址。需要特别注意Modbus TCP的数据是大端序而Windows是小端序解析高低字节时不能直接强转所以用BinaryPrimitives按大端读取。这段代码没有做粘包处理和超时重试实际产线里肯定不够但通信框架是对的。把它跑起来之后每100毫秒调用一次ReadHoldingRegisters读到的主轴负载就会变成一组连续曲线。接下来要做的是把数值存下来并且带上准确的时间戳这就引出了数据怎么组织的问题。提示Modbus TCP的单元标识不是IP地址多台机床即使IP不同如果串了PLC网关转发单元标识必须按网关配置逐台区分否则会读到重复数据。2.3 通道选型决策表采集通道实时性数据深度开发复杂度适合场景FOCAS/FANUC库高可到10ms高宏变量、PMC、系统变量中依赖SDK封装发那科机床深度采集MTConnect中100ms到1s中标准数据项多低HTTP加XML解析多品牌机床联网汇总裸TCP/Modbus高可到10ms低只能读开放地址表中协议文档复杂国产数控系统、PLC网关表里有一点要特别提醒FOCAS实时性虽然高但它不是标准协议C#调用时如果SDK没提供.NET库还得自己写P/Invoke或者用C/CLI封装工程量不能按普通HTTP接口来估。MTConnect开发最舒服但采样频率受agent自身限制做主轴负载突变检测就可能漏掉尖峰。裸TCP最灵活可一旦系统换了固件版本、地址表变了代码就得跟着改。选型时要根据当前车间设备的开放情况来定而不是追求所谓的最优协议。3. 源码拆解一个采集进程从初始化到落库的完整路径3.1 项目分层与关键文件拿到一套C#的CNC采集系统源码先不要急着看每个文件先看分层。常见结构是四层通信层负责建立连接、收发字节流屏蔽底层协议差异协议解析层把字节转换成状态对象比如主轴负载、倍率、坐标值业务逻辑层处理采集策略、断线重连、缓存补传存储层负责把数据写到数据库、消息队列或文件里。四层之间用接口隔离通信层从Modbus替换成FOCAS还是MTConnect上层代码不用大改这是源码里最重要的设计。在一套真实可用的采集端源码里关键文件通常有这些。CncChannel.cs是通道抽象定义Open、Close、Read接口ModbusChannel.cs和FanucChannel.cs是两个通道实现CncDataPoint.cs定义单个数据点结构DataSampler.cs是采集调度核心维护每个点位最后采集的时间LocalCache.cs负责断网时先写本地文件DataUploader.cs负责把缓存上传到中央库。新手看源码容易犯的错误是直奔界面的代码结果被UI代码淹没。我建议先看DataSampler.cs和CncChannel.cs把数据流理清再去看每一层的具体实现。这里有一个C#特有的注意点。采集系统通常跑在Windows服务或者控制台进程里不要在UI线程里做Socket读操作不然机床一多界面就会卡死。源码一般会用BackgroundService、Thread或Task.Run跑采集循环界面只展示从队列里取到的数据。如果源码采用的是事件驱动比如用ManualResetEventSlim控制采集和存储节奏这种设计在数据密集型场景尤其管用因为存储线程和采集线程可以彻底解耦。3.2 采集主循环轮询频率与超时控制采集主循环是整个系统的核心。常见实现会维护一张点位表记录每个采集项的通道类型、地址、数据类型、采集周期循环扫描这张表到期的点位就执行采集再交给后续管线。这比固定间隔采集所有点位好因为主轴转速这类快速变量需要高频采集而程序名这类静态信息低频就够了可以减少通信压力。下面是简化版的主循环public class DataSampler { private readonly ListDataPointConfig _configs; private readonly Dictionarystring, DateTime _lastReadAt new(); private CancellationTokenSource _cts new(); public void Start() { _cts new CancellationTokenSource(); Task.Run(() SamplingLoop(_cts.Token)); } private async Task SamplingLoop(CancellationToken token) { while (!token.IsCancellationRequested) { DateTime now DateTime.UtcNow; foreach (var cfg in _configs) { if (now - _lastReadAt.GetValueOrDefault(cfg.Name) cfg.Interval) continue; try { var value await ReadValueAsync(cfg); _lastReadAt[cfg.Name] DateTime.UtcNow; OnDataReceived?.Invoke(cfg.Name, value, DateTime.UtcNow); } catch (Exception ex) { _logger.LogWarning(点位 {Name} 采集失败: {Msg}, cfg.Name, ex.Message); } } await Task.Delay(10, token); } } }这个循环的好处是每个点位有自己的采集周期。假设点位表里有100个点位真正每100毫秒采集的可能只有主轴负载和倍率其他是1秒或5秒一次通信载荷被控制在合理范围。异常处理要放在单点位粒度上不能因为某个点位超时就中断整个循环否则所有点位都会停滞。catch里只记录警告重连逻辑由通道层负责这样故障时系统仍能维持大部分点位正常。超时控制是这个环节最容易翻车的点。Socket.ReadTimeout如果设得太大比如30秒一个掉线设备会占住采集线程很久导致所有点位排队延迟。我一般把ReadTimeout设在2到3秒超时后置为断线状态并触发重连。还有一个坑是Async读和CancellationToken的耦合取消采集不等于立即关闭连接需要在通道层把WaitHandle和Socket关闭一起处理否则采集线程一直阻塞在Read操作上。3.3 本地缓存断线补偿与数据完整性数据完整性的关键不是依赖网络而是依赖本地缓存。车间网络交换机偶尔重启、中央数据库临时维护、上位机进程重启这些都是家常便饭。如果采集端只把数据丢给TCP连接断线的几分钟就会产生空洞报表里看就是一段缺失。可行性较高的做法是每条记录先写本地缓存再异步上传中央库上传成功后才删除本地记录。C#里最简单的实现是用SQLite做本地队列public class LocalCache { private readonly SQLiteConnection _conn; private readonly SemaphoreSlim _writeLock new(1, 1); public LocalCache(string dbPath) { _conn new SQLiteConnection(Data Source dbPath); _conn.Open(); } public async Task AppendAsync(CncDataRecord record) { await _writeLock.WaitAsync(); try { using var cmd _conn.CreateCommand(); cmd.CommandText INSERT INTO cache(ts, device, point, value) VALUES(ts, device, point, value); cmd.Parameters.AddWithValue(ts, record.Timestamp); cmd.Parameters.AddWithValue(device, record.DeviceId); cmd.Parameters.AddWithValue(point, record.PointName); cmd.Parameters.AddWithValue(value, record.Value); cmd.ExecuteNonQuery(); } finally { _writeLock.Release(); } } }用SQLite当写缓冲的原因是事务性。SQLite单文件、不需要额外服务嵌入式部署很省心。SemaphoreSlim保证并发写入不冲突因为采集进程里可能有多个通道同时写数据不加锁的话SQLite会频繁报database is locked。断线重连后上传线程按时间戳顺序读取未上传记录成功后按主键删除本质就是个待重试队列。本地缓存文件也不是越大越好。SQLite文件一直不清理会越来越大到几百兆后写入性能明显下滑。我一般在启动时检查缓存文件大小超过阈值就清理已上传记录并执行一次压缩。如果现场环境导致缓存文件损坏SQLite的journal机制能恢复一部分数据但最坏情况还是要手工导出所以缓存文件本身也要定时备份。有个细节要提醒不要把删除逻辑放在上传成功回调里因为回调可能不准确最稳妥的是上传线程读完后用独立事务按主键删除。4. 把采集数据变成数据集清洗、对齐与样本标注4.1 数据集字段设计与时序对齐采集系统跑起来后数据库里存的是原始数据点但机器学习模型需要的是结构化样本。把原始数据变成数据集第一关是字段设计。一个用于设备状态分析的CNC数据集通常要包含设备ID、时间戳、程序号、刀具号、主轴负载、主轴转速、进给倍率、三轴坐标和报警代码。这些字段来自不同通道比如刀具号来自宏变量报警代码来自PLC信号采集时间戳来自服务器本地。多源数据要融合成一条样本对齐是关键否则模型会学到错位的时空特征。对齐的常见做法是以时间戳为基准做最近邻匹配。比如主轴负载100毫秒采一次坐标值200毫秒采一次在组装样本时把坐标值向前查找找到最接近负载时间戳的采集值。这里最容易忽略的是时区差异。如果采集端用DateTime.Now中央服务器却用UTC存储时间戳一混数据对齐直接出错。我建议所有设备统一用DateTime.UtcNow生成采集时间戳展示层再按本地时区换算。这是数据集的隐藏坑看起来字段都对一画图就对不上。数据集建设的第二步是降采样。机床采集数据一天可以产生几百万条直接拿来做训练集文件大且冗余度高。我会按工况切片比如一个加工周期内保留主轴负载的均值、峰值和标准差把刀具号和程序号作为标签字段。这样一条记录代表一个加工步骤或工件循环数据量从百万降到几千模型训练反而更稳因为噪声和冗余采样都被剔掉了。4.2 异常样本的清洗规则原始数据里垃圾数据比例远超想象。常见的异常有几类传感器尖峰主轴负载偶尔跳出远超正常范围的值通常是干扰信号停机前数据段机床关电或急停状态不完整通信异常导致的默认值很多系统在通信中断时会把负载返回0或者上一次的值。这三类不清理模型会学到错误映射。清洗时先看设备状态字段。如果系统能读到运行状态先把状态不为运行的数据段标记掉然后用滑动窗口处理尖峰。下面是一段清洗逻辑示意import pandas as pd def clean_cnc_data(df): # 按设备分组处理 cleaned [] for device, group in df.groupby(device_id): group group.sort_values(timestamp) # 去掉设备非运行状态的数据 group group[group[run_state] RUN] # 滑动窗口去尖峰 rolling_med group[spindle_load].rolling(10, centerTrue).median() diff (group[spindle_load] - rolling_med).abs() group group[diff 3 * group[spindle_load].std()] cleaned.append(group) return pd.concat(cleaned)这里的清洗按设备分组进行因为不同机床的负载正常范围差别很大加工铸铁的卧加和加工铝件的车削中心基准完全不同放在一起做统计阈值会大规模误杀。滑窗中值是对周期性信号比较温和的尖峰处理方式但如果负载本来就是渐变信号这个方法可能把真实工艺波动也滤掉需要根据实际波形调窗口宽度和阈值倍数。清洗之后有一个容易被漏掉的步骤状态字段的补全。很多时候设备状态本身也是采集来的中间会有缺失。这种情况我会用程序号变化来判断加工周期边界或者用主轴启停信号做状态推断这些在采集数据里通常比较可靠。如果状态字段空洞太多宁可直接删掉这段样本也不要填充捏造值否则会污染故障样本的标注。4.3 训练样本的标注与导出格式采集数据清洗完要变成可训练的样本集还需要标注。以刀具寿命预测为例原始库里只有刀具号、主轴负载和加工时间没有“这把刀用了多少次”的信息。标注方法通常是把刀具号和换刀记录对齐通过换刀事件切分刀具生命周期每把刀在其周期内的数据段打上对应剩余寿命百分比。机加工数据集里常用标签是剩余有用寿命定义为当前时刻到换刀时刻的加工周期数。标注质量取决于车间有没有换刀记录。老机床没有自动换刀检测就得靠人工录入或者加装传感设备去推断这条路不轻松。我的建议是初期只标注程序号级别的工况标签先把数据按程序号加刀具号分组在无标签数据上做聚类观察能否区分不同加工阶段。这样在缺少精确寿命标签的情况下也可以做异常检测或设备状态识别。导出格式方面工业数据集最常见的是CSV和TDMS这类时序格式。C#采集端存的数据可以先导出成带表头的CSV列包括timestamp、device_id、program_no、tool_no、spindle_load、spindle_speed、feed_rate、x_pos、y_pos、z_pos、alarm_code。导出时做一次类型压缩时间戳用字符串但保留毫秒精度数值用double类型。多设备合并成一个数据集文件时我会先按设备分组导出再在训练时做分组归一化避免不同主轴量程互相干扰。数据集规范后做工况识别或异常预警就有底子。5. C# CNC采集系统避坑连接掉线、数据漂移和时钟错位的解决5.1 现象FOCAS连接运行两小时后就定时掉线现象是程序刚启动采集正常运行两小时后FOCAS读取超时之后重连也连不上只能重启服务重启后又恢复。这个问题网上被反复提起多半是FOCAS会话句柄泄漏。很多调用FOCAS库的C#封装建立连接后没有正确管理会话句柄每次重连都创建新Session却不释放旧句柄系统资源耗尽后连接就被掐断。解决方法是检查FOCAS相关调用确认是否有对应释放接口在采集通道被替换或服务停止时调用。用C#做封装时把连接包装成IDisposable把关闭逻辑放在Dispose里。另外FOCAS库本身对连接数有限制多线程同时读一个Session也可能在连接层报错需要注意线程安全。这个坑最麻烦的是它表现得很像网络问题实际与网络无关排查时要先看句柄数不要一上来就调TCP参数。5.2 现象主轴负载值偶发为0或者长时间不变这个现象常出现在快速变化的信号上。主轴负载是伺服驱动器计算的数值协议解析时如果传输过程出错错误值可能被当成0。偶发为0有两个常见原因一是TCP粘包导致错位两次响应连在一起按固定长度切割后部分字节来自前一个包二是FOCAS读取了未启用的宏变量或数据项返回空值。针对粘包可行做法是用长度字段加状态机解析先按预期长度收满整个响应帧再校验功能码和数据长度最后才解析数值。宏变量未启用则需要对照机床电气手册确认该变量确实映射到实际寄存器。处理“长时间不变”可以在采样层记录每个点位最近若干条数值的方差方差接近0且数值非0时触发报警因为机床真在加工时主轴负载不可能肉眼可见地纹丝不动。宁可把这类数据标记为异常也别让它污染正常样本。5.3 现象采集时间戳和机床实际时间相差越来越大时间戳是数据集基石一旦漂移所有对齐都白做。这个问题常见原因是采集服务用Windows系统时间做基准而服务器本身没配NTP加上虚机平台时间漂移速度不一几小时后可能差出几秒。更麻烦的是FOCAS返回的是机床内部时间采集端又是本地时间两边不对齐数据关联就会产生偏差。解决分两层。底层把采集服务的宿主机接入NTP时间源配置自动同步虚机环境开启主机与虚机时间同步。上层在设计数据模型时以采集端时间戳为基准机床内部时间只当独立原始字段保留不做依赖。多通道融合样本时也以采集端时间为准做索引。另一个隐藏点是时区转换C#里DateTime.Now在不同时区环境会出问题统一用UtcNow能避免这类玄学错误。5.4 现象多台机床同时采集时丢包严重机床数量从几台增到几十台时采集端会出现轮询超时、数据丢失。原因通常是并发模型不对。最常见的是每台机床一个TCP连接但所有连接都在同一个线程池里网络抖动导致部分读操作排队超过超时时间连接被判为断开反复重连后丢包率更高。另一个原因是单机采集频率过高比如每台机床每50毫秒采一个点10台就是每秒200个请求连接数和CPU都扛不住。解决思路是分层限流。先把所有点位采集周期统一到100毫秒以上除非有强实时需求。再把采集线程从IO线程中剥离每个通道一个专用读循环数据写入有界队列由独立存储线程消费失败时直接丢弃本次采样不阻塞后续。最后对机床分组采集比如10台一组组内串行、组间并行。这三步做完丢包问题基本能压住。如果仍然丢包就要检查中央库写入性能改批量事务提交一次提交几十条记录比逐条提交高效得多。6. 从能跑到跑稳回放验证与延迟优化的几个手段采集系统刚写完时最危险的错误是“感觉它跑对了”。现场数据没有现成答案可以对照等上了产线再发现逻辑错了代价就不是改代码那么简单。我习惯在做真机联调之前先把数据回放一遍。做法是把一段已知的机床数据导出成JSON或二进制流写一个回放器按原节奏推给接收端应用层完全感知不到数据来源。这样协议解析逻辑可以反复修改测试不用反复打扰车间设备。延迟优化的核心是顺着瓶颈找资源。Socket缓冲区、轮询线程数、数据库写入批次三个参数依次调整。Socket.ReceiveBufferSize调到64KB以上能在网络抖动时降低丢包采集线程和机床数不再一对一改成一个通道一个线程数据库写入从1条改成50条一批写入耗时能降一个数量级。三个参数调完配合NTP统一时钟和本地SQLite缓存系统基本可以稳定跑在产线上。我自己有一条教训曾经急着上线把采集端部署后就关掉了远程调试结果现场连续跑三天才发现一台机床的宏变量地址被改过那段期间数据全是错的而本地缓存里因为时间戳不可信也无法重算。从那以后我给自己定了规矩采集端必须保留诊断日志日志里必须记录原始报文的十六进制内容。数据出错时原始报文是唯一能让问题快速复现的线索。对一个CNC数据采集项目来说把代码跑通只是及格跑出可信的数据才是交付。希望帮到你。本文还有配套的精品资源点击获取