C#调用FOCAS协议实现发那科CNC设备数据采集与监控
简介面向工业自动化领域的软件开发者这份C#源码用于解决发那科数控系统FOCAS接口的数据采集问题适合需要读取机床状态、主轴信息、刀具参数及运行数据的上位机开发场景。压缩包共51个文件大小约3.45MB其中cs源文件与dll动态库构成主要代码框架同时带有配置文件、可执行程序和Visual Studio解决方案可直接打开工程查看或二次编译使用。目前已有90人学习下载。通过阅读源码可以掌握网络套接字连接、FOCAS命令封装、二进制数据解析和异常处理等关键实现对理解工业通信编程、快速搭建CNC数据采集原型具有实际参考价值。资源还提供了完整的工程目录结构便于对照dll依赖与配置文件理解FOCAS协议在.NET环境下的调用方式减少查阅手册和调试时间。整体代码结构清晰适合具备一定C#基础的工程师作为对接参考。 手里有十几台发那科系统的加工中心车间要上设备监控和工时统计第一件事就是把每台机床的实时加工参数给搞上来。一开始大家习惯用串口或者OPC串口线缆麻烦且只能靠近距离OPC的配置和授权又是一堆事。后来发现发那科自带一个叫FOCAS的以太网通信协议用C#上位机直接调用动态库就能把宏变量、坐标、报警、主轴负载这些参数稳定读出来而且不用动机床的梯形图。这篇文章就把我折腾这个项目的完整过程写下来从库的引用、连接握手、三类高频参数的采集代码到踩过的几个典型坑一次性说清楚。FOCAS这词拆开是Fanuc Open CNC API Specification说白了就是发那科官方开的以太网通信门。C#上位机只要想办法把它的fwlib32.dll调用起来就可以和机床的以太网口直接对话读参数、发指令、拿报警都行。适合正在做MES对接、设备数据采集、DNC监控或者想自己写个小工具查机床状态的兄弟参考。1. 为什么绕开串口和OPC直接盯上FOCAS车间的CNC设备要联网采集方案其实不止一个。但是你把每种方案都摆到台面上比一遍就会发现FOCAS在发那科系统上有不可替代的优势尤其是你手头机床种类杂、要采的数据项又多的时候。1.1 串口方案线少但数据项太受限RS232串口确实是老设备最常见的通信方式。拉一根三芯线到机床后面连上232转USB的盒子上位机写串口解析程序也能拿宏变量、坐标这些数据。但它的限制非常明显距离短超过15米就得上转485或者加延长器车间里布线环境本来就乱多拉一根线就多一个故障点。串口是独占式的你采数据的时候人家操作工如果也想用串口传程序DNC在线加工会直接冲突。数据项要靠宏程序往串口里吐很多老机床根本没做这个宏程序你得上机床改梯形图或者加宏代码。我最早就是用串口结果每台机床要单独写一套处理逻辑有的发那科0i MD输出格式跟16i完全对不上最后统一推倒重来。如果你的目标是采集少量数据、做简单的DNC串口还能凑合一旦涉及大量参数的周期采集它扛不住。1.2 OPC方案配置复杂授权费也是钱OPC是Windows平台设备通信的老路子通过厂商提供的OPC Server把CNC数据映射成变量上位机用OPC客户端去订阅。好处是标准化坏处也一字排开发那科的OPC Server并不是免费捆绑的有些版本需要单独买授权。Server装在工控机上还需要额外的网关设备或者专用板卡配置步骤很繁琐数据点位表要靠填变量名逐条加。OPC经典版(DCOM)跨机器部署容易遇到权限问题车间里那些Windows机器补丁又乱连不上是家常便饭。我身边有同事用OPC做了一版调试到怀疑人生。不是OPC不好而是对这种单一种类机床的采集场景上OPC属于杀鸡用了牛刀成本高收益还不一定比直接调协议好。1.3 FOCAS方案链路最短、权限清晰、现成APIFOCAS的核心价值在于它把通信链路压缩到最短。上位机走TCP/IP直接连机床网口中间不需要专用Server、不需要授权网关、不需要额外硬件发那科把封装好的动态库给你C#通过平台调用就能对接。而且FOCAS提供的API覆盖面很全常用的有数据类别典型API函数说明连接管理cnc_allclibhndl3 / cnc_allclibhndl建立/断开连接句柄坐标读取cnc_rdaxisdata绝对/机械/相对坐标宏变量读取cnc_rdmacro读#500~#999等宏变量刀具/程序cnc_rdprgnum当前程序号、刀具号运行状态cnc_rdstat自动/手动/报警状态报警信息cnc_rdalm当前/历史报警编号参数读写cnc_rdparam系统参数读取修改主轴/进给cnc_rdspdl / cnc_rdfeed主轴转速、进给倍率这些API几乎覆盖了车间监控的上层需求。更关键的是发那科把机床侧的数据交互规则直接封装好了我们不需要关心底层TCP报文怎么拼、校验和怎么算拿来即用。1.4 什么样的项目适合选FOCAS总结一下如果你遇到下面这几种情况FOCAS基本是首选机床是发那科系统0i、16i、18i、30i、31i等且开通了FOCAS以太网功能。上位机需要周期采集坐标、宏变量、报警频率在1秒到几秒一次。不想上太重的OPC架构希望用最小成本快速打通数据链路。数据最终要进MES或者数据库需要稳定的二次开发接口。反过来说如果机床是西门子、三菱或者马扎克FOCAS就不适用了那得走各家自己的协议或者OPC UA别指着一把钥匙开所有锁。2. 库引用与TCP握手先把C#和发那科系统牵上线C#调用FOCAS的方式很简单就是平台调用P/Invoke一个原生动态库。但这部分有个大前提很多新手直接卡在第一步动态库版本和机床选项没对齐连库都加载不成功后面全是浮云。2.1 拿到正确的fwlib动态库FOCAS库文件一般叫fwlib32.dll还有一个64位的fwlib64.dll。安装FOCAS Library开发包之后在安装目录下能找到这两个文件。注意几点你的上位机进程是32位还是64位必须和dll位数一致。这条我踩过后面专门讲。开发包还会带一份FOCAS1库的说明文档和C/C头文件推荐把头文件保留好C#结构体定义全靠对照它来写。有些机床或者集成商给的SDK版本比较老函数名可能带后缀或者略有差异。最好以实际拿到的手册为准。把fwlib32.dll放到编译输出目录或者放到C:\Windows\System32都行。我习惯放在程序目录下发布的时候带着走装到任何工控机上都不会污染系统目录。2.2 机床侧的准备IP、端口、选项功能FOCAS走的是TCP/IP端口默认是8193。机床侧要做三件事发那科系统一般按SYSTEM键进入以太网设定界面给CNC设置一个固定IP掩码、网关按车间局域网规划填好。确认以太网功能和FOCAS/以太网选项是开启状态。发那科很多功能是选配的没开通选项功能IP配了也连不上。这条最容易忽视很多人排查半天最后发现机床根本没买这个功能。有些系统还需要设置端口号默认8193一般不用动但确认一下没坏处。我在实际对接中遇到一个很实际的问题网线直接连电脑和机床的网口IP配在同一网段才能通。比如电脑是192.168.1.100机床就设192.168.1.50。子网掩码都用255.255.255.0第一轮不要开复杂路由先保证物理通再谈协议通。提示调试连接前先在Windows命令行里执行ping 机床IP能通再往下走。如果ping都不通别急着怀疑代码先把网线、IP、交换机端口查一遍。2.3 C#里的P/Invoke声明与连接代码FOCAS库的C接口导出的函数几乎都是short返回值0表示成功宏定义是EW_OK。我们用C#声明时原样对应即可。下面是连接机床的完整代码骨架using System; using System.Runtime.InteropServices; public static class FocasNative { [DllImport(fwlib32.dll, EntryPoint cnc_allclibhndl3)] public static extern short cnc_allclibhndl3( string ip, ushort port, int timeout, ref ushort handle); [DllImport(fwlib32.dll, EntryPoint cnc_allclibhndl)] public static extern short cnc_allclibhndl(ushort handle); }调用方式ushort handle 0; string ip 192.168.1.50; ushort port 8193; int timeout 10; // 秒 short ret FocasNative.cnc_allclibhndl3(ip, port, timeout, ref handle); if (ret 0) { Console.WriteLine($连接成功句柄{handle}); // 之后所有读取操作都靠这个handle } else { Console.WriteLine($连接失败错误码{ret}); return; }连接成功之后会得到一个句柄handle后续所有API几乎都要传入这个handle。断开时调用cnc_allclibhndl释放句柄。程序退出前必须要做否则机床侧的通信资源会被一直占用。2.4 断连与资源释放有的上位机程序跑几天后出现机床网络卡死或者无法重连的情况多半是句柄没有释放干净。我一般在封装类里写一个Dispose方法public void Close() { if (_handle ! 0) { FocasNative.cnc_allclibhndl(_handle); _handle 0; } }C#没有析构函数保证原生句柄一定会被回收最好在界面关闭事件、服务停止事件里显式调用。如果程序是Windows服务或者后台任务更要注意当网络异常时先释放句柄再重连不要一股脑反复new连接。3. 三个高频采集场景的代码骨架宏变量、绝对坐标、报警连接打通之后剩下的就是按需取数。这里我挑三个车间监控最常用的场景把完整的结构体定义和调用代码给出来。3.1 读宏变量跟踪加工参数的关键入口发那科的宏变量是车间数据采集的核心。很多机床会把刀具寿命、加工数量、工件计数、当前程序的关键参数放在宏变量里用#500到#999中间某段。上位机可以通过cnc_rdmacro把这些值读出来不用上机床看面板。先定义C#结构体。FOCAS的IODBMC结构对应C定义如下我按顺序封送[StructLayout(LayoutKind.Sequential)] public struct IODBMC { public short datano; // 数据号 public short type; // 数据类型0短整型1长整型2浮点 [MarshalAs(UnmanagedType.ByValArray, SizeConst 10)] public int[] cdata; // 短整型数据 [MarshalAs(UnmanagedType.ByValArray, SizeConst 10)] public int[] ldata; // 长整型数据 [MarshalAs(UnmanagedType.ByValArray, SizeConst 10)] public double[] rdata; // 浮点数据 }调用函数声明[DllImport(fwlib32.dll, EntryPoint cnc_rdmacro)] public static extern short cnc_rdmacro( ushort handle, ushort number, short length, out IODBMC macro);读取宏变量示例short ret FocasNative.cnc_rdmacro(handle, 500, -1, out IODBMC macro); if (ret 0) { double value macro.type switch { 0 macro.cdata[0], 1 macro.ldata[0], 2 macro.rdata[0], _ 0 }; Console.WriteLine($宏变量#500的值{value}, 类型{macro.type}); }number参数是宏变量号直接传500就代表#500length传-1表示读取1个。如果把宏变量当数组用可以传具体的长度。这里有个细节type字段决定你去取cdata还是ldata还是rdata千万别不管类型直接读取rdata否则在整数型宏变量上会拿不到正确数据。3.2 读绝对坐标和机械坐标设备定位与打卡依据很多MES系统需要记录工件的加工坐标或者设备当前位置。FOCAS提供cnc_rdaxisdatatype传0是绝对坐标1是机械坐标2是相对坐标3是剩余移动量。一次调用能拿回XYZ等多轴数据。结构体定义[StructLayout(LayoutKind.Sequential)] public struct IODBAXISDATA { public double x; public double y; public double z; public double a; public double b; public double c; public double u; public double v; public double w; public short alarm; public short valid; }函数声明和调用[DllImport(fwlib32.dll, EntryPoint cnc_rdaxisdata)] public static extern short cnc_rdaxisdata( ushort handle, short type, short length, out IODBAXISDATA axisdata); // 读取绝对坐标 short ret FocasNative.cnc_rdaxisdata(handle, 0, 0, out IODBAXISDATA axis); if (ret 0) { Console.WriteLine($X{axis.x:F3}, Y{axis.y:F3}, Z{axis.z:F3}); }axisdata结构体里如果机床没有A/B/C轴这些字段会是0。valid字段可以告诉你哪些轴有效。我在项目里采集五轴机床时就用到了A轴和C轴注意一下数据单位是毫米还是英寸取决于机床参数设定一般默认毫米。3.3 读报警设备状态监控的晴雨表报警数据对于设备监控同样重要。FOCAS的cnc_rdalm能读当前报警和历史报警。结构体定义如下[StructLayout(LayoutKind.Sequential)] public struct IODBALM { public byte alm_no; // 报警数量 public byte type; // 报警类型 public short dummy; // 对齐保留 [MarshalAs(UnmanagedType.ByValArray, SizeConst 40)] public uint[] alm; // 报警编号 public int axis; // 轴号 public int use_msg; // 是否使用消息 }函数声明[DllImport(fwlib32.dll, EntryPoint cnc_rdalm)] public static extern short cnc_rdalm( ushort handle, short type, out IODBALM alm);type传-1表示读所有报警传0读当前报警传1读历史报警。调用示例short ret FocasNative.cnc_rdalm(handle, 0, out IODBALM alm); if (ret 0) { Console.WriteLine($当前报警数量{alm.alm_no}); for (int i 0; i alm.alm_no i 40; i) { Console.WriteLine($报警号{alm.alm[i]}); } }报警号拿到之后要和报警文本对应起来。不同机型报警号含义可能不同我建议维护一张报警号到描述的映射表常见的像1000多号是系统报警2000多号往往是用户宏报警具体含义要结合机床厂家的报警表。4. 采集实战中的四类坑从连接失败到数据漂移这项目做下来最大的收获不是代码本身而是那些查了半天的坑。其中有些坑特别隐蔽网上资料还少我把它们按排查链路写出来你直接对着查。4.1 32位/64位库不匹配最隐蔽的加载异常一开始我在本机用64位Windows写了个测试程序DllImport加载fwlib32.dll结果直接抛BadImageFormatException提示尝试读取或写入受保护的内存之类。后来才意识到fwlib32.dll要求进程是32位你得在Visual Studio里把项目的平台目标改成x86或者引用fwlib64.dll并保持x64。这个坑非常隐蔽因为报错信息往往只是AccessViolationException或者DllNotFoundException不会直接说你位数不对。排查方式很简单确认你的程序集目标平台和dll的位数一致。注意发布到车间工控机时如果那台机器是64位Win10别急着改成AnyCPU建议直接x86编译并带上fwlib32.dll兼容性最稳。除非你明确要用的某个第三方组件仅支持64位。4.2 机床配了IP但ping不通问题经常出在交换机端口有台机床面板上IP配了半天设好了也不报错但电脑就是ping不通。我先测了电脑到交换机的通断没问题网线换过最后才发现是车间交换机端口没插稳水晶头弹片老化导致接触不良。另一台机床更离谱IP和电脑不在同一网段但面板上看起来都是192.168.1.x仔细一看子网掩码被改成了255.255.0.0。这种细微差别会导致TCP连接失败。整套排查链路建议按这个顺序来在电脑上ipconfig确认本机IP和机床IP必须同网段。ping 机床IP不通就检查物理链路——网线、交换机端口、水晶头、面板网口。ping通后用telnet 机床IP 8193测试端口通断不通查机床侧FOCAS选项功能。端口通了再跑C#连接代码这时候错误码还有问题就大概率是代码或库版本了。4.3 连续高频采集导致机床网络闪断项目初期为了秒级刷新我按500ms一次轮询所有数据项跑了一个多小时那台机床的以太网通信突然断了面板上显示FOCAS通信超时怎么重连都失败最后重启机床才恢复。后来查FOCAS手册发现它的通信节点数是有限的频繁连接和断开或者单次通信占用的握手时间太长会把机床的通信资源耗尽。解决办法有两层降低采集频率坐标和状态类数据1~3秒采一次足够宏变量和报警可以更长别做几百毫秒的极限刷新。长连接复用不要把一次采集做成连接→读→断开的短连接模式创建一个连接句柄后长期复用只在掉线时重连。车间设备监控不是证券行情秒级就是天花板了再高频除了增加故障率没有任何实际收益。4.4 结构体字段错位导致读出的报警号全不对用C#封送时最容易的一块就是结构体布局。FOCAS的C头文件里很多结构体有对齐问题C#默认LayoutKind.Sequential在大部分场景没问题但如果字段里有byte、short混着int、double可能对不上C里的实际内存布局。我遇到的具体情况是IODBALM这个结构体C里前面有byte alm_no、byte type然后有个short保留字段接着是long alm[40]C#里如果只声明了byte、byte、short、int[]默认对齐和C是一致的但如果你少了dummy字段后面所有数据都会错位读出来的报警号全是乱的。经验是严格对照头文件把C里的每个字段都对应进来哪怕是文档里没用的保留字段也要占位。不确定时先用Marshal.OffsetOf逐个字段看偏移量和C的sizeof/offsetof对比。结构体定义改一次就重新跑一轮完整读测试不要只测一个函数。5. 采集频率、异常恢复与Log设计让程序真正能在车间跑起来代码能读到数据只是一个demo能连续跑三个月不崩才是生产工具。最后这部分是纯经验之谈我在多个车间长期运行后总结出的稳定性设计。5.1 多台设备的轮询调度假设车间有10台发那科机床我不会每台开一个独立循环、10个线程同时死读那样压力太大。更好的做法是用一个后台采集线程按固定时间片轮流连接所有机床。大致思路如下维护一个机床IP列表每台配一个采集周期比如5秒。后台线程循环遍历列表到了采集时间的机床就发送读取指令。单台机床一次性把该周期要的数据项全部读走不要分多次减少握手开销。所有数据放进一个线程安全的队列由另一个线程负责批量写入数据库或者推送MES。这种设计的好处是控制住了并发量也方便以后往里加新机床只需要在配置表里加一行。5.2 掉线自愈机制车间网络再稳也有拉闸、换网线、机床重启的时候。程序必须能自我恢复不能一掉线就躺平等人工去重启采集服务。我维护了一个简单的状态机每次读取返回非零错误码标记该机床通信异常。异常后先释放句柄等待3~5秒重新建立连接。连续重试3次失败将机床状态置为离线写入日志同时继续每30秒尝试一次。一旦重连成功恢复正常的采集周期并在日志里记录恢复时间。如果机床停机时间长程序中还要注意时间戳处理。车间数据要求的是真实发生时间不是采集程序的上位机时间尽量在写入数据库时同时保存上位机时间和机床侧时间。5.3 日志设计排障的第一现场很多人觉得日志是给开发看的车间环境不重要。但实际上一台设备异常操作工会说程序卡了维修工会说机床没坏这时候唯一能还原现场的就是日志。我建议至少记录三类信息连接事件每次连接成功/失败、重连次数、脱机时长。数据异常FOCAS返回的错误码、异常的数值范围比如坐标突然跳变、宏变量为NaN。系统级事件服务启停、采集线程重启、数据库写入失败。日志文件按天滚动保留30天。C#里用现成的NLog或者Serilog都可以注意写日志本身不能阻塞采集流程异步日志是默认选择。这套日志陪我排查过好几次诡异问题每次都是日志先还原了现场再定位到具体原因。5.4 实际运行中的几个小技巧最后分享几个我反复用的小技巧。第一机床的IP不要写死在代码里用配置文件json或者ini维护。车间改IP是常有的事改配置比改代码重新发布效率高得多。第二采集到的原始数据不要直接丢数据库在内存里做个最近N条的环形缓冲。万一上位机进程崩溃数据还能在日志里留个尾巴。第三如果MES或者数据库短时间不可用采集线程不要停先把数据积压在队列里恢复后追写。否则丢几分钟数据对产量统计影响非常大。第四用C# WinForms做上位机界面时采集线程不要操作UI控件用事件或者异步委托把数据推到界面层。有人图省事直接在采集线程里更新文本框界面卡死是必然的。这个项目从最初的一台机床验证到整个车间的批量接入前前后后花了不到两周真正写核心代码的时间其实就两三天剩下全是在和网络环境、机床选项、结构体布局这些细节作斗争。如果你正在做类似的事希望这篇文章能帮你少走几条弯路。先把连接和宏变量跑通再逐步加报警和坐标你会发现自己写的这个采集工具比很多商业软件还顺手。本文还有配套的精品资源点击获取