
简介基于C#的CNC数据采集系统源码与数据集面向个人学习者和工业自动化入门者解决FANUC数控机床在Windows环境下的实时数据采集与监控难题。压缩包共29个文件以C#源文件为主涵盖连接管理、操作界面、数据集定义等模块并附带Fwlib32.dll等FANUC接口库、配置文件(.config)、资源文件(.resx)及数据集架构(.xsd)整体仅652KB便于快速下载与本地调试。该资源已有167人学习下载适合对照代码逐行理解。学习时可重点关注WinForms界面与机床通信的搭建过程、实时状态读取与数据记录逻辑以及基于数据集进行历史分析的设计思路备份文件(.zbak)和README说明为二次开发与排错提供了对照参考有助于快速掌握从数据采集到可视化呈现的完整链路为进入智能制造领域积累实战经验。1. 从一台 CNC 到一套数据这份 C# 数据采集资源到底解决什么前几年做车间设备联网主管丢给我一句话把每台机床的坐标、主轴负载和报警先记到数据库里报表按天汇总。机床侧没有现成的 SCADA只有几台发那科和三菱系统现场环境还混着两台只能走串口的老设备。当时用 C# 写数据采集服务是最稳的解法而这份基于 C# 的 CNC 数据采集系统源码与数据集恰好覆盖了从通信协议、采数循环、落库到数据集整理的完整链路。它解决的从来不是“读一个坐标”这种单点问题而是把“机床屏幕上的实时状态”变成“数据库里可查询、可回溯、可训练的历史样本”这条路怎么走通。适合正在做 C# 上位机、工厂信息化、设备数据采集的人也适合想用真实工业通讯练手 C# 编程的开发者。下面按协议选型、代码实现、落库与数据集、常见坑的顺序展开最后给一个验证与进阶的方向。2. 采集路线怎么选FOCAS、OPC UA 与串口的边界2.1 主流对接方式与选型CNC 数据采集的第一步不是写代码而是选对通讯方式。不同品牌、不同年代的系统对外提供的数据接口完全不一样。我见过不少项目在选型上翻车买了几万块的采集盒子最后发现老机床根本没有对应的数字接口。常见的对接方式可以归成五类下面这张表基本覆盖了工厂里能遇到的机型协议/方式适用机型链路形态能读到什么主要缺点FOCAS2发那科 0i / 16i / 18i / 30i 等以太网直连网口或交换机坐标、主轴转速/负载、报警、程序号、宏变量、刀具号品牌绑定只能读发那科MC 协议三菱 M70 / M80 / M700 等以太网坐标、主轴、报警、PMC 信号文档偏硬例程少OPC UA西门子 840D 等中高端控制器以太网标准化节点跨品牌整合配置复杂节点梳理费时间串口宏程序无网口的老机床、通用系统RS232/RS485坐标、程序状态等字符串流速度慢、易丢行Modbus TCP机床改造后加采集模块以太网外部传感器、电气信号读不到系统内部变量选型上我一般按这个原则单一种类且以发那科为主优先 FOCAS2官方 DLL 封装完整C# 里 P/Invoke 直接调用稳定性和数据完整度都比其他方式好车间机型杂、要对接 MES 做整合优先 OPC UA实在没有网口的旧设备串口宏程序兜底。这套源码的采集层也是按这个思路组织的不同协议之间通过接口隔离最理想的局面是一套 C# 服务通吃换协议只换适配器。2.2 把协议层和数据层拆开接口与数据模型我第一次做采集的时候把 FOCAS 的 DLL 调用直接写在窗体按钮里结果后来要加一台三菱机床整段代码推倒重来。正确的做法是先把采集动作抽象成接口上层只管拿数据底层才关心协议细节。这是这套源码里最值得抄的部分之一。public interface ICncDataSource { bool Connect(CncConnectionConfig config); CncSnapshot ReadCurrentState(); void Disconnect(); }这里Connect负责建立与机床的会话传入的CncConnectionConfig至少包含 IP 地址、端口号、连接超时这几项ReadCurrentState每次调用返回一个完整的快照对象Disconnect负责释放会话资源。上层采集服务只依赖这个接口不关心背后是 FOCAS、MC 协议还是 OPC UA。快照对象是整个采集系统的核心数据结构字段设计直接决定后面落库和做数据集是否顺手。我常用的模型长这样public sealed class CncSnapshot { public DateTime Timestamp { get; set; } public string MachineNo { get; set; } public double XAbs { get; set; } public double YAbs { get; set; } public double ZAbs { get; set; } public double XMach { get; set; } public double YMach { get; set; } public double ZMach { get; set; } public short SpindleSpeed { get; set; } public short SpindleLoad { get; set; } public short FeedRate { get; set; } public string ProgramNo { get; set; } public short AlarmNo { get; set; } }注意这里用的是“一次读一批”的快照而不是“一条一个变量”。原因是采集服务要面对多台机床和高频轮询每次调用通讯接口都有握手成本按快照批量读取能把开销降一个数量级同时保证同一时刻的坐标和负载是逻辑一致的。Timestamp字段建议优先取机床系统时间取不到再退回 PC 时间后面避坑章会专门说这个问题。2.3 同一个框架给其他工业设备复用接口抽象还有一个隐藏收益这套框架不只适用于 CN C。工厂里常见的拧紧工具比如 Atlas Copco 的 Power Focus 6000读取扭矩值的做法和读 CNC 如出一辙——厂商提供通讯协议或 .NET SDK你只需要把ICncDataSource换成ITighteningToolDataSource快照对象换成扭矩、角度、拧紧结果采集循环和数据链路完全不动。这类型设备通常走以太网返回 JSON 或专用协议报文解析层独立出来就好。也就是说这份源码里的架构价值大于协议本身的价值学会了这套分层后续接任何带网口的工业设备都是同一个套路。3. 把采集代码跑起来FOCAS2 连接、读数与重连3.1 引入 fwlib32 并建立连接FOCAS2 是发那科的以太网数据通讯库核心动态库是fwlib32.dll。C# 里通过DllImport调用第一步是把常用函数声明出来。注意字符串参数要指定字符集否则在中文系统上有编码风险。[DllImport(fwlib32.dll, CharSet CharSet.Ansi)] public static extern short cnc_allclibhndl3( string ip, ushort port, int timeout, out ushort handle);建立连接的代码很短ushort handle 0; short ret cnc_allclibhndl3(192.168.1.8, 8193, 8000, out handle); if (ret 0) { Console.WriteLine($Connection OK, handle{handle}); } else { Console.WriteLine($Connection failed, ret{ret}); }这段代码的逻辑是向指定 IP 的机床发起以太网连接请求成功时返回句柄后续所有读取操作都靠这个句柄。参数说明端口默认是 8193这是发那科 FOCAS 以太网的固定监听端口超时单位是毫秒8000 毫秒比较稳妥设太短会在机床网络模块繁忙时误报失败ret为 0 表示成功其他值对应不同类型的错误码。连接失败时先查机床侧的以太网模块是否启用再查 PC 到机床的物理链路和防火墙放行情况这两个原因占了我遇到问题的八成。3.2 读坐标、主轴负载与报警坐标读取是采集系统的核心动作。FOCAS 用结构体返回四组坐标值C# 侧结构体声明必须和 C 语言的内存布局一致否则读出来的是乱码。[StructLayout(LayoutKind.Sequential)] public struct ODBPos { public short dummy; [MarshalAs(UnmanagedType.ByValArray, SizeConst 6)] public double[] abs; [MarshalAs(UnmanagedType.ByValArray, SizeConst 6)] public double[] mach; [MarshalAs(UnmanagedType.ByValArray, SizeConst 6)] public double[] rel; [MarshalAs(UnmanagedType.ByValArray, SizeConst 6)] public double[] dist; }abs是绝对坐标mach是机械坐标rel是相对坐标dist是剩余移动量。数组长度按 6 声明是为了兼容 4 轴、5 轴机型如果按 3 个轴写死数组长度在五轴机床上会越界。读取调用ODBPos pos new ODBPos(); short ret cnc_rdposition(handle, out pos); if (ret 0) { double xAbs pos.abs[0]; double yAbs pos.abs[1]; double zAbs pos.abs[2]; }主轴数据的结构体更轻量[StructLayout(LayoutKind.Sequential)] public struct ODBSpindle { public short dummy; public short alarm; public short speed; public short load; public short run; public short mtch; }读取后speed需要除以 100 才是实际 RPM因为发那科返回的是转每分钟的百分之一load是 0 到 100 的整数百分比直接可用。报警读取用cnc_rdalm返回报警数量和报警号适合做实时报警看板。这里有一个值得记住的细节cnc_rdspindle的返回值由机床侧定时刷新通常每秒才更新一次把轮询间隔压到 10ms 没有意义反而徒增网络负载。3.3 轮询循环与断线重连策略连接和单次读取通了之后下一步是把它放进循环。采集服务不能简单用while(true)加Thread.Sleep要考虑取消、异常和断线恢复。我一般这样组织while (!ct.IsCancellationRequested) { short ret ReadOnce(out CncSnapshot snapshot); if (ret ! 0) { await TryReconnectAsync(); continue; } _buffer.Add(snapshot); await Task.Delay(_pollIntervalMs, ct); }ReadOnce内部依次调用读坐标和读主轴的函数拼装成快照失败时先重连再继续避免在断线状态下空转刷错误日志。轮询间隔_pollIntervalMs我一般设 500 毫秒坐标和负载这类信号受机床侧刷新限制再快也拿不到新值。断线重连要特别注意频率曾经有同事把重连写成死循环 100ms 一次直接把机床的网络模块搞到无响应。带退避的重连策略是必需品private async Taskbool TryReconnectAsync() { for (int i 1; i 5; i) { cnc_freelibhndl(_handle); short ret cnc_allclibhndl3(_ip, _port, 4000, out _handle); if (ret 0) return true; await Task.Delay(TimeSpan.FromSeconds(i * 2)); } return false; }逻辑是先释放旧句柄再尝试重新连接失败后等待时间按 2 秒、4 秒、6 秒递增最多试 5 次。参数说明cnc_freelibhndl释放句柄这步不能省FOCAS 的会话资源有限不释放会让机床侧挂着无效连接积累到上限后就再也连不进去了。重连上限和间隔根据车间规模调整几十台机床并发时建议上限降到 3 次。4. 数据落库与数据集组织从批量写入到 PHM2012 式样本4.1 高频采集下的批量写入采集循环跑起来以后数据量很快就不是单台机床的问题了。一台机床 500ms 一条一天约 17 万条十台机床就是 170 万条。如果每来一条就执行一次INSERT数据库的压力会非常难看尤其是在 Windows 服务里和界面抢资源的时候。这套源码的存储层用的是内存队列加批量落库做法很实用。private readonly BlockingCollectionCncSnapshot _buffer new(2000); // 采集线程写入 _buffer.Add(snapshot); // 存储线程批量取出 var batch new ListCncSnapshot(); while (_buffer.TryTake(out var item, 100)) { batch.Add(item); } if (batch.Count 0) { BulkSave(batch); }BlockingCollection的容量设为 2000 是给采集线程一个背压上限如果存储线程来不及写库队列满了以后Add会阻塞反过来限制采集速度防止内存无限增长。TryTake的超时参数 100 毫秒保证存储线程在队列暂时为空时不会空转。批量写入的 SQL 用拼接即可数据量不算离谱var sb new StringBuilder(INSERT INTO cnc_snapshot (ts, machine_no, x_abs, y_abs, z_abs, spindle_speed, spindle_load) VALUES ); for (int i 0; i batch.Count; i) { sb.Append($({batch[i].Timestamp:yyyy-MM-dd HH:mm:ss.fff}, ) .Append(${batch[i].MachineNo}, ) .Append(${batch[i].XAbs}, {batch[i].YAbs}, {batch[i].ZAbs}, ) .Append(${batch[i].SpindleSpeed}, {batch[i].SpindleLoad}),); }代码的逻辑是把一批快照拼成一条多行INSERT一次提交。参数说明时间格式里的fff是毫秒一定要保留否则同一秒内多条数据的时间戳会重复字符串字段在拼接时注意转义内部系统的数值型字段不存在注入问题但如果是刀具号、程序号这类文本字段最好做一层替换。再往上走几十台机床并发时拼 SQL 就不够看了生产环境我一般换SqlBulkCopy或者直接上时序数据库。4.2 存储选型与时间戳约定存储选型没有绝对答案看场景和运维能力存储适合场景注意点SQLite / Access单台 PC 个人学习、离线采集并发写入弱数据量大后查询变慢MySQL / PostgreSQL车间级多机床、对接 MES需要专职运维批量写入配合建索引InfluxDB 等时序库大规模采集、按时间聚合分析学习成本略高但聚合查询很方便个人学习和单机调试阶段直接用 SQLite 或 Access 最省事正经上线到车间我会优先 MySQL因为后面要出的日报、OEE、趋势图都是按时间范围查关系型数据库配合索引足够稳。时间戳约定这件事值得在落库之前就想清楚。每台机床的系统时间和 PC 时间往往不一致有的差几分钟有的差几个小时。如果直接用DateTime.Now打时间戳后面做回放核对时会出现诡异的时间断层或重叠。我的做法是优先调用 FOCAS 的cnc_rdtime读取机床系统时间读到就用机床时间读不到再用 PC 时间同时把时间源标记在表里。多台机床同时采集时统一换算成 UTC 存储展示层再转本地时间。4.3 按 PHM2012 的组织方式整理成数据集这套资源里的“数据集”不是随手导出的 CSV而是按可训练、可分析的标准组织的历史样本。很多做采集的人把数据存进数据库就以为结束了实际上数据集的形态决定了下游能做什么。可以参考公开的 PHM2012 数据集的组织思路。PHM2012 是铣刀磨损预测领域的经典数据它的核心组织形式是每一行是一条按时间排序的观测记录包含加工过程中的传感器信号同时在合适粒度上附带磨损量标签。我们采集的主轴负载、进给、坐标完全可以按同样的方式组织。我建议的字段结构如下列名类型示例说明timestampdatetime2024-05-11 09:00:00.123统一 UTCmachine_nostringCNC-01机台标识program_nostringO0001程序号tool_nostringT03刀具号x_posdouble123.456绝对坐标spindle_loadshort65主轴负载百分比spindle_speedshort8000主轴转速 RPMfeed_rateshort1200进给速度label_weardouble0.15磨损量标签按需填写文件命名建议CNC-01_20240511.csv一台机一天一个文件。关键点在于原始信号列和标签列要分开特征衍生值不要和原始信号混在同一层。这样后续做磨损预测时可以直接拿原始负载信号做特征提取而不是被已经算好的均值、方差束缚。5. 常见问题与排查五个频发的翻车现场5.1 坐标读出来全是 0现象连接正常返回 0但abs、mach里的坐标全部是 0。网络上表面看起来通了数据却不能用。原因最常见的两个一是机床没有回零绝对坐标系尚未建立二是接口函数用错了读坐标用了读伺服状态的函数或者坐标类型选错比如拿相对坐标当绝对坐标用。解决先到机床面板上手动移动一个轴对比程序输出的值是否跟手轮移动一致然后确认机床已经完成回零操作最后逐个输出abs、mach、rel三组值看清楚哪一组在动、动的方式是否符合预期。一套采集系统最先调通的一定是坐标数据这块不确认好后面的负载和报警数据都不可信。5.2 采集一启动 UI 就假死现象点击“开始采集”后窗体立刻无响应鼠标转圈过几秒甚至提示程序未响应。原因通讯调用是同步阻塞的把它直接放在 UI 线程里执行一次读数是几十毫秒到几百毫秒加上重连的退避等待UI 线程就被占死了。解决把采集循环放到独立的BackgroundService或者Task里UI 只负责显示。我做这类系统时一般把采集做成 Windows 服务界面只是一个查看端通过内部队列或者 SignalR 推送数据。这样即使界面卡死采集进程不受影响数据不丢。5.3 时间戳乱跳间隔忽大忽小现象表里相邻两条数据的时间差不稳定设置的是 500ms结果经常出现 10 秒甚至几分钟的间隔。原因多数情况是批量写入时数据库连接不稳定触发重连重连期间采集线程在等存储线程轮询循环被拉长另一种是 PC 时间被 NTP 自动校时导致时间戳整体跳变。解决把采集和存储彻底分离采集线程只往队列里放数据存储线程卡住时背压阻塞但不要让采集线程去等数据库时间戳尽量用机床时间或者单调时钟源避免DateTime.Now在跨校时场景下出问题。排查时可以按时间排序查前后两条的时间差找出大间隔集中在哪个时间段再对应当时日志看是网络断链还是数据库阻塞。5.4 重启服务后连不上句柄耗尽现象服务第一次启动连接正常重启后要么连不上要么连接成功但读几次就报错机床侧的网络模块像是死了。原因程序退出时没有调用cnc_freelibhndl释放句柄机床侧认为连接还活着把会话资源占住积累多次重启后FOCAS 允许的并发连接数被耗尽。另外多个采集任务复用同一个句柄也是常见诱因。解决Disconnect里必须调用cnc_freelibhndl每台机床创建自己的句柄严禁全局共享重连逻辑里先释放旧句柄再建新连接。调试阶段可以等 5 到 10 分钟让机床侧超时回收再试重连。5.5 串口中文乱码与丢行现象老机床走串口宏程序输出读回来的中文报警信息是乱码或者整行数据时有时无。原因串口编码不匹配老机床宏程序输出通常是 GB2312而SerialPort默认按 ASCII 解析另外读缓冲太小数据来得快时来不及取走新数据覆盖旧数据造成丢行。解决创建串口时显式设置编码和缓冲区serialPort.Encoding Encoding.GetEncoding(GB2312); serialPort.BufferSize 4096;逻辑是让串口按中文字符集解析字节流并把缓冲区调大以匹配机床的输出速度。参数说明BufferSize设 4096 是经验值太小的缓冲区在 9600 波特率下都容易溢出。数据解析时按行读取每一行做长度和首字符校验解析失败时保留原始行到日志文件方便回头排查是机床输出格式变体还是解析逻辑缺陷不要悄悄丢弃。6. 验证与进阶回放核对数据把负载信号变成磨损特征上线第一周不要急着看报表先做一次回放核对。把数据库里的历史数据按时间排序检查相邻记录的时间间隔是否稳定这是检验采集链路可靠性的最快办法。下面的 SQL 在 MySQL 里可以直接跑其他数据库换对应的窗口函数即可SELECT ts, TIMESTAMPDIFF(MILLISECOND, LAG(ts) OVER (ORDER BY ts), ts) AS gap_ms FROM cnc_snapshot WHERE machine_no CNC-01 ORDER BY ts;如果设定 500ms 采一次gap_ms大部分应该落在 500 左右偶尔出现 1000 属于正常抖动。连续出现大间隔说明那段时间网络断链或存储阻塞。丢包率用实际行数除以按时间跨度推算的理论行数即可跑一遍核对数据有多少缺口一目了然。数据可靠之后可以试着把主轴负载信号升级成磨损特征。最简单的做法是滑动窗口对负载序列取均值、峰峰值和峭度按刀具和工序分组保存这样原始信号就被压缩成了可训练的样本列。窗口大小我一般取一次走刀的采样点数拿不准时先固定 500 点跑一轮观察特征曲线是否平滑不平滑就加大窗口。负载信号配合同步采集的进给和坐标已经足够支撑刀具磨损趋势分析。这个思路和 PHM2012 数据集的用法相同只是特征工程这一步要在落地时自己补。有段时间我图省事采集服务上线后没做回放核对月底盘点时生产主管拿着报表问CNC-03 周二夜班的数据怎么缺了两小时查到最后是机床网络模块半夜掉线重连失败后服务还在跑但数据一直是空快照。从那以后每套系统上线我都强制走一遍回放核对把缺口数量和位置打出来看一眼再收工。希望帮到你。本文还有配套的精品资源点击获取