1. 为什么我放弃了网关方案,改用网线直连
车间里那台发那科 0i-MF 系统已经跑了六年,最近产线要做设备联网,老板要求把加工数据实时传到 MES 里。我第一反应是找设备科要网关方案——毕竟市面上做协议转换的盒子一抓一大把,什么 Profinet 转 Ethernet/IP、Modbus 转 OPC UA,听起来省事。但实际一算账,问题就来了。
网关方案的核心逻辑是:CNC 的以太网口先接到网关,网关再把数据翻译成上层系统能懂的协议,最后通过车间交换机上传。这套链路里,网关本身就是一个故障点。车间环境里电磁干扰大、粉尘多,网关盒子夏天烫得能煎鸡蛋,冬天又因为温差结露短路。更麻烦的是,网关的协议映射表是固定的,发那科这边加一个宏变量,网关那边就得重新配置,每次改动都要停机半天。
后来我翻发那科官方文档,发现 FOCAS 库本身就支持以太网直连。FOCAS 是发那科提供的底层通信接口,全称是 FANUC Open CNC API Specification,它允许外部程序直接读写 CNC 的内部寄存器、宏变量、报警信息、刀具补偿等数据。换句话说,只要你的电脑和 CNC 的以太网口在同一个网段,用一根网线直连,再调用 FOCAS 的动态链接库,就能把数据抓出来。中间不需要任何网关、不需要协议转换、不需要额外的硬件盒子。
这个发现让我重新算了笔账:一根六类屏蔽网线几十块钱,一台工控机或者旧笔记本就能跑采集程序,成本几乎为零。而且直连的延迟极低,实测从 CNC 发出信号到采集端收到数据,端到端延迟稳定在 8 到 12 毫秒之间,比网关方案动辄 50 毫秒以上的延迟好太多。对于需要做实时刀具补偿或者自适应控制的场景,这个差距是致命的。
当然,直连方案也不是没有门槛。FOCAS 库的调用需要你对 CNC 的数据结构有一定了解,比如宏变量的地址映射、刀具补偿的存储格式、报警代码的解析规则。这些在发那科的手册里都有,但手册写得像天书,全是十六进制地址和偏移量。我踩过的坑包括:把宏变量地址算错导致读出来全是零、网线没屏蔽导致数据包丢失、CNC 的以太网参数没配对导致连接超时。这些问题后面会一个个拆开讲。
这篇文章适合三类人看:第一类是车间设备工程师,想自己做数据采集但不想被网关厂商绑死;第二类是自动化集成商,需要给客户做 CNC 联网方案但预算有限;第三类是搞 SCADA 或者 MES 的软件工程师,需要对接发那科设备但不懂底层通信。不管你是哪一类,只要跟着下面的步骤走,一根网线、一台电脑、一个下午的时间,就能把 CNC 的数据抓出来。
2. 直连方案的核心思路与硬件选型
2.1 为什么以太网直连比网关更稳
网关方案的本质是“翻译”——它把 CNC 的私有协议翻译成标准协议。但翻译这件事,在工业现场往往意味着额外的延迟和故障点。我见过太多网关因为散热不良死机、因为固件 bug 丢包、因为协议版本不匹配导致数据错位。而以太网直连是“原生对话”——你的程序直接和 CNC 的通信栈打交道,中间没有任何中间商。
从网络拓扑上看,直连方案极其简单:CNC 的以太网口通过一根网线连接到采集电脑的网口,两边配置同一个网段的 IP 地址,然后采集程序调用 FOCAS 库建立 TCP 连接。这个连接是长连接,一旦建立就持续保持,数据可以按需轮询或者订阅。发那科 0i 系列和 30i 系列都支持这种模式,老一点的 16i/18i 也支持,只是 FOCAS 库的版本要对应。
这里有个关键点:发那科 CNC 的以太网口通常有两个用途——一个是连接上位机做数据采集,另一个是连接伺服放大器或者 I/O 模块。这两个用途的端口是分开的,采集用的端口一般是 CD38A 或者 CD38B 接口,具体看系统型号。你需要在 CNC 的参数页面里确认以太网口的功能定义,别把采集口和伺服口搞混了。我见过有人把网线插到伺服通信口上,结果 CNC 直接报警,折腾了半天才发现插错了。
2.2 网线与网卡的选择要点
网线这块,我强烈建议用六类屏蔽网线(Cat6 STP),长度不要超过 30 米。车间里变频器、伺服驱动器、中频炉这些设备产生的电磁干扰非常强,非屏蔽网线跑 FOCAS 协议的时候,丢包率能到 5% 以上。丢包的直接后果就是采集程序报“连接中断”或者“数据校验失败”,而且这种故障是偶发的,很难排查。屏蔽网线配合金属屏蔽水晶头,两端做好接地,丢包率可以降到万分之一以下。
网卡方面,采集电脑最好用 Intel 的千兆网卡,比如 I210 或者 I219 系列。这些网卡在工业现场验证过,驱动稳定,支持巨帧(Jumbo Frame),对 FOCAS 的短包高频通信很友好。如果你用 Realtek 的网卡,可能会遇到中断合并导致的延迟抖动,采集周期从 10 毫秒跳到 50 毫秒,做实时控制就废了。另外,采集电脑的网卡要设置成静态 IP,不要用 DHCP,因为 CNC 那边也是静态 IP,DHCP 协商过程可能引入不必要的延迟。
IP 地址的规划很简单:CNC 设成 192.168.1.10,采集电脑设成 192.168.1.100,子网掩码都是 255.255.255.0。网关不用填,因为直连不需要跨网段。这里有个细节:发那科 CNC 的 IP 地址设置页面里,有一个“路由器地址”或者“网关地址”的选项,直连场景下留空或者填 0.0.0.0 就行。如果你填了网关地址,CNC 可能会尝试把数据包发到网关,反而导致通信失败。
2.3 FOCAS 库的版本匹配与获取
FOCAS 库是发那科官方提供的,但获取渠道比较特殊。它不公开下载,需要你联系发那科代理商或者从发那科的技术支持网站申请。库文件通常是一个压缩包,里面包含 Fwlib32.dll(32 位)或者 Fwlib64.dll(64 位)、头文件 Fwlib32.h、以及一个 PDF 格式的 API 手册。版本号很重要:FOCAS1 对应老系统(0i-A/B),FOCAS2 对应新系统(0i-C/D/MF),FOCAS3 是最新的,支持 30i/31i/32i 系列。
我手头这台 0i-MF 用的是 FOCAS2,库版本是 4.7。如果你不确定自己的系统支持哪个版本,可以在 CNC 的“系统参数”页面里找“FOCAS 版本”这一项,或者直接问发那科的技术支持。版本不匹配的典型症状是:调用 cnc_allclibhndl3 函数时返回错误码 -16(功能不支持),或者连接建立后读数据全是零。这时候别急着怀疑网线,先检查库版本。
库文件的部署也有讲究。Fwlib32.dll 必须放在采集程序的同一目录下,或者放到系统目录 C:\Windows\System32 里。如果你用 Python 调用,需要把 DLL 路径加到 os.environ['PATH'] 里,或者用 ctypes 的 WinDLL 指定绝对路径。我试过用 Python 的 ctypes 直接加载,结果因为 DLL 依赖的 VC++ 运行库没装,报“找不到模块”。后来装了 Visual C++ Redistributable 2015 才解决。所以部署之前,先把 VC++ 运行库装好,省得后面折腾。
3. FOCAS 核心函数与数据采集实操
3.1 建立连接与句柄管理
FOCAS 的通信模型是基于句柄的。你可以把它理解成“先挂号,再看病”——调用 cnc_allclibhndl3 函数向 CNC 申请一个句柄,后续所有读写操作都通过这个句柄进行。函数原型是这样的:
short cnc_allclibhndl3(char *ipaddr, unsigned short port, long timeout, unsigned short *handle);参数说明:ipaddr 是 CNC 的 IP 地址字符串,port 固定填 8193(FOCAS 的默认端口),timeout 是超时时间,单位是秒,一般填 10 就行。handle 是输出参数,函数调用成功后会把句柄值写进去。返回值是 0 表示成功,非零表示失败。
这里有个坑:timeout 参数的单位是秒,但实际测试下来,如果网线没插好或者 IP 配错了,函数会阻塞整整 10 秒才返回。如果你在循环里反复调用,程序会卡死。我的做法是先用 ping 命令确认网络通不通,再调用连接函数。另外,句柄用完之后必须调用 cnc_freelibhndl 释放,否则句柄泄漏,跑几个小时之后 CNC 会拒绝新连接。
连接建立之后,你可以调用 cnc_statinfo 函数读取 CNC 的当前状态,包括运行模式、自动运行状态、报警状态、主轴转速、进给速度等。这个函数返回一个结构体,字段很多,但常用的就几个:run 表示运行状态(0 表示停止,1 表示运行),motion 表示运动状态,alarm 表示报警状态。我一般用这个函数做心跳检测,每秒钟调一次,如果连续三次返回错误,就触发重连逻辑。
3.2 宏变量读取与地址映射
宏变量是 CNC 里最灵活的数据存储区,编号从 #1 到 #999,还有 #100 到 #199 的公共变量和 #500 到 #999 的保持变量。FOCAS 提供了 cnc_rdmacro 函数来读取宏变量,原型如下:
short cnc_rdmacro(unsigned short handle, short number, short length, IODBMR *macro);number 是宏变量编号,length 是数据长度(一般填 10),macro 是输出结构体。这个结构体里有一个 data 字段,是 long 类型,存储宏变量的值。注意:宏变量的值在 CNC 内部是定点数,小数点位置由宏变量的属性决定。比如 #100 存储的是 12345,但实际表示的是 123.45,因为它的属性是“小数点后两位”。这个属性可以通过 cnc_rdmacro 返回的结构体里的 dec 字段获取,dec 表示小数位数。
我踩过的坑:一开始没管 dec 字段,直接把 data 当成整数用,结果读出来的刀具补偿值差了 100 倍。后来才发现,宏变量的值需要除以 10 的 dec 次方才是真实值。比如 dec=2,data=12345,真实值就是 123.45。这个细节在发那科的手册里写得很隐蔽,不注意根本发现不了。
批量读取宏变量可以用 cnc_rdmacrorange 函数,一次读多个连续的宏变量,效率比逐个读高很多。比如你要读 #500 到 #599 这 100 个变量,用 cnc_rdmacrorange 一次调用就能全部读出来,耗时不到 5 毫秒。如果逐个调用 cnc_rdmacro,100 次调用加起来要 50 毫秒以上。对于需要高频采集的场景,这个差距很关键。
3.3 刀具补偿与报警信息采集
刀具补偿数据是车间最关心的信息之一。FOCAS 提供了 cnc_rdtofs 函数来读取刀具补偿值,原型如下:
short cnc_rdtofs(unsigned short handle, short number, short type, short length, IODBTO *tofs);number 是刀具号,type 是补偿类型(0 表示形状补偿,1 表示磨损补偿),length 是数据长度。返回的结构体里,data 字段是补偿值,dec 字段是小数位数。和宏变量一样,真实值需要除以 10 的 dec 次方。
报警信息的采集用 cnc_rdalminfo 函数,它可以读取当前报警列表,包括报警号、报警类型、报警信息。这个函数返回一个结构体数组,每个元素包含一个报警的详细信息。我一般用这个函数做实时监控,一旦有报警就立即推送到 MES 系统,同时触发声光报警器。注意:报警信息的字符串是 ASCII 编码的,但有些特殊字符(比如中文报警)可能是 Shift-JIS 编码,需要做编码转换。我试过用 Python 的 decode('shift-jis') 来解码,效果不错。
还有一个常用的函数是 cnc_rddynamic2,它可以一次性读取多个动态数据,包括主轴转速、进给速度、当前刀具号、当前程序号等。这个函数的效率很高,适合做高频采集。我实测下来,调用一次 cnc_rddynamic2 耗时约 2 毫秒,每秒可以采集 500 次以上,完全满足实时监控的需求。
3.4 采集程序的完整实现框架
下面是一个用 Python 实现的采集程序框架,基于 ctypes 调用 FOCAS 库:
import ctypes import time from ctypes import wintypes # 加载 FOCAS 库 focas = ctypes.WinDLL('Fwlib32.dll') # 定义函数原型 focas.cnc_allclibhndl3.argtypes = [ctypes.c_char_p, ctypes.c_ushort, ctypes.c_long, ctypes.POINTER(ctypes.c_ushort)] focas.cnc_allclibhndl3.restype = ctypes.c_short focas.cnc_statinfo.argtypes = [ctypes.c_ushort, ctypes.POINTER(ODBST)] focas.cnc_statinfo.restype = ctypes.c_short # 定义结构体 class ODBST(ctypes.Structure): _fields_ = [ ('dummy', ctypes.c_short), ('tmmode', ctypes.c_short), ('aut', ctypes.c_short), ('run', ctypes.c_short), ('motion', ctypes.c_short), ('mstb', ctypes.c_short), ('alarm', ctypes.c_short), ('edit', ctypes.c_short), ] # 建立连接 handle = ctypes.c_ushort() ip = b'192.168.1.10' ret = focas.cnc_allclibhndl3(ip, 8193, 10, ctypes.byref(handle)) if ret != 0: print(f'连接失败,错误码:{ret}') exit(1) print(f'连接成功,句柄:{handle.value}') # 采集循环 try: while True: stat = ODBST() ret = focas.cnc_statinfo(handle, ctypes.byref(stat)) if ret == 0: print(f'运行状态:{stat.run},报警状态:{stat.alarm}') else: print(f'读取失败,错误码:{ret}') time.sleep(0.1) except KeyboardInterrupt: pass finally: focas.cnc_freelibhndl(handle) print('连接已释放')这个框架可以直接跑,但有几个细节要注意:第一,结构体的字段顺序必须和 FOCAS 手册里定义的一致,否则读出来的数据是错位的。第二,ctypes 的 WinDLL 加载方式要求 DLL 在系统路径或者当前目录下,如果报“找不到模块”,检查 DLL 路径。第三,采集循环里要加异常处理,网络抖动或者 CNC 重启会导致连接断开,程序要能自动重连。
4. 常见故障排查与避坑经验
4.1 连接失败的错误码速查
FOCAS 的错误码是排查问题的第一手线索。下面这张表是我在实际项目中整理出来的常见错误码和对应原因:
| 错误码 | 含义 | 排查方向 |
|---|---|---|
| -1 | 连接超时 | 检查网线、IP 地址、CNC 以太网口是否启用 |
| -2 | 句柄无效 | 句柄未初始化或已释放,检查代码逻辑 |
| -3 | 数据长度错误 | 结构体大小和 FOCAS 版本不匹配 |
| -8 | 超时 | CNC 忙或者通信负载过高,降低采集频率 |
| -16 | 功能不支持 | FOCAS 库版本和 CNC 系统不匹配 |
| -17 | 参数错误 | 函数参数填错,检查宏变量编号或刀具号 |
我遇到最多的是 -1 和 -8。-1 通常是网线没插好或者 IP 配错了,用 ping 命令一测就知道。-8 比较麻烦,一般是 CNC 在跑高速加工的时候,通信负载太高,FOCAS 请求排队超时。解决办法是降低采集频率,或者把采集程序放到 CNC 空闲的时候跑。我试过在粗加工阶段每秒采集 100 次,结果频繁报 -8,后来降到每秒 10 次就稳定了。
4.2 数据读出来全是零怎么办
这个问题我遇到过三次,每次原因都不一样。第一次是宏变量地址算错了——发那科的宏变量编号是从 #1 开始的,但 FOCAS 函数里的 number 参数是从 0 开始的,所以读 #100 要传 99。第二次是 CNC 的宏变量保护功能打开了,外部设备只能读不能写,但读出来的值全是零。需要在 CNC 参数里把“宏变量保护”关掉。第三次是 FOCAS 库版本不对,换了一个匹配的版本就好了。
排查这个问题的顺序是:先用 cnc_statinfo 确认连接正常,再用 cnc_rdmacro 读一个已知有值的宏变量(比如 #100 里手动设一个 123),如果读出来是零,检查 number 参数是否减了 1。如果还是零,检查 CNC 的宏变量保护参数。如果还是零,换 FOCAS 库版本。
4.3 网络抖动与丢包的处理技巧
车间里的网络环境比办公室恶劣得多。我见过变频器启动的瞬间,采集程序直接报连接中断。后来用 Wireshark 抓包分析,发现是变频器产生的电磁干扰导致网线里出现了大量 CRC 错误帧。解决办法有三个:第一,换屏蔽网线,两端接地;第二,在采集电脑和 CNC 之间加一个工业级交换机,做电气隔离;第三,在采集程序里加心跳检测和自动重连。
心跳检测的逻辑很简单:每秒钟调用一次 cnc_statinfo,如果连续三次返回错误,就释放句柄、重新连接。重连的时候要加一个退避策略,比如第一次等 1 秒,第二次等 2 秒,第三次等 4 秒,避免频繁重连把 CNC 的通信栈打爆。我实测下来,加了心跳检测之后,即使网络抖动导致连接断开,程序也能在 3 秒内自动恢复,数据丢失不超过 30 个采集周期。
4.4 采集频率与 CNC 负载的平衡
FOCAS 的采集频率不是越高越好。CNC 的通信栈资源有限,如果采集频率太高,CNC 的加工性能会受影响。我做过测试:在 0i-MF 上,采集频率从每秒 10 次提高到每秒 100 次,CNC 的插补周期从 4 毫秒抖动到 8 毫秒,加工表面质量明显下降。后来把采集频率降到每秒 20 次,加工质量恢复正常。
所以采集频率要根据实际需求来定。如果只是做状态监控,每秒 1 次就够了。如果要做实时刀具补偿,每秒 10 到 20 次也够了。如果要做振动分析或者自适应控制,可能需要每秒 100 次以上,但这时候建议用发那科的高速数据采集接口(比如 FOCAS 的 cnc_rdhighspeed 函数),而不是普通的轮询方式。
5. 数据上云与 SCADA 对接的扩展思路
5.1 从 FOCAS 到 OPC UA 的数据桥接
采集到数据之后,下一步就是把它送到 SCADA 或者 MES 系统。最直接的方式是在采集程序里集成 OPC UA 服务端,把 FOCAS 读到的数据映射成 OPC UA 的节点。Python 里可以用 asyncua 库来实现,代码量不大,但要注意线程安全——FOCAS 的调用是阻塞的,OPC UA 的服务端是异步的,两者混用的时候要用队列做缓冲。
我一般的做法是:采集线程负责调用 FOCAS 读数据,读到之后放到一个线程安全的队列里;OPC UA 服务端从队列里取数据,更新节点值。这样采集和发布解耦,即使 OPC UA 客户端连接断开,采集也不会受影响。队列的长度设成 1000 就行,满了就丢弃最老的数据,避免内存泄漏。
5.2 数据库存储与历史数据查询
如果要做历史数据查询,可以把采集到的数据存到时序数据库里,比如 InfluxDB 或者 TDengine。InfluxDB 的写入性能很好,单机每秒可以写入 10 万条数据,完全满足车间级的数据量。存储的时候要注意:每条数据要带时间戳、设备编号、数据点名称、数值、质量码。质量码用来标识数据是否有效,比如 FOCAS 返回错误的时候,质量码设成 Bad,SCADA 那边就会显示灰色。
查询的时候,可以用 InfluxDB 的 Flux 语言做聚合,比如查某台设备过去一小时的报警次数、平均主轴转速、最大进给速度等。这些数据可以用来做 OEE 分析、刀具寿命预测、能耗优化。我做过一个项目,把 FOCAS 采集的数据和 MES 的工单数据关联起来,实现了按工单统计加工时长和刀具消耗,车间主任看了之后直接拍板推广。
5.3 多台 CNC 的集中采集架构
车间里通常不止一台 CNC,可能是 10 台、20 台甚至更多。如果每台 CNC 配一台采集电脑,成本太高,也不方便管理。我的做法是用一台工控机跑多个采集线程,每个线程负责一台 CNC 的数据采集。工控机用双网卡:一个网卡接车间交换机,和 MES 通信;另一个网卡接一个独立的交换机,所有 CNC 都连到这个交换机上。
这个架构的关键是 IP 地址规划。CNC 的 IP 地址按产线编号,比如产线 1 的 CNC 用 192.168.1.x,产线 2 的用 192.168.2.x。采集工控机的第二个网卡配多个 IP 地址,或者用 VLAN 做隔离。我试过用 VLAN,配置稍微复杂一点,但隔离效果好,不同产线的广播包不会互相干扰。如果不想折腾 VLAN,直接用多个网卡也行,一个网卡对应一个网段。
采集线程的管理用线程池就行,每个线程独立连接一台 CNC,互不干扰。如果某台 CNC 断线了,只有对应的线程会报错,其他线程不受影响。线程池的大小根据工控机的 CPU 核心数来定,一般每个核心跑 5 到 10 个线程比较合适。我用的工控机是 4 核的,跑了 30 个采集线程,CPU 占用率在 40% 左右,还有余量。
5.4 数据安全与访问控制
CNC 的数据涉及生产工艺参数,有些参数是核心机密,不能随便让人看。所以在采集端就要做好访问控制。我的做法是:采集程序只读取需要的数据,不读取敏感参数(比如宏变量里的加密配方);OPC UA 服务端启用用户名密码认证,不同用户有不同的节点访问权限;数据库的查询接口加 Token 验证,Token 有有效期,过期自动失效。
另外,采集程序本身也要做防护。FOCAS 库的调用是本地调用,不涉及网络传输,所以不用担心数据在传输过程中被截获。但采集程序所在的工控机要加防火墙,只开放必要的端口(比如 OPC UA 的 4840 端口),其他端口全部关闭。工控机也要定期打补丁,防止病毒入侵。我见过一个案例,车间的工控机中了勒索病毒,采集程序被加密,导致整个产线的数据中断了两天。后来加了防火墙和定期备份才解决。
6. 实操心得与最后几句
这套直连方案我在三个车间、五种机型上验证过,最老的系统是 0i-MB,最新的 0i-MF Plus,FOCAS 库从 4.1 到 5.2 都用过。整体感受是:直连方案的门槛在“第一次调通”,一旦调通之后,稳定性比网关方案高一个数量级。网关方案平均每两个月要重启一次,直连方案跑了半年没断过。
几个关键心得:第一,网线一定要用屏蔽的,别省这个钱;第二,FOCAS 库版本要和 CNC 系统匹配,不确定就问发那科技术支持;第三,采集频率别贪高,够用就行;第四,心跳检测和自动重连是必须的,车间网络没有想象中那么稳;第五,宏变量的 dec 字段一定要处理,不然数据全是错的。
最后分享一个小技巧:如果你手头没有 FOCAS 库,可以先在 CNC 的以太网设置页面里打开“FOCAS 通信”功能,然后用 telnet 命令连到 CNC 的 8193 端口,如果能看到欢迎信息,说明通信链路是通的。这个测试不需要 FOCAS 库,也不需要写代码,五分钟就能确认网络配置对不对。确认通了之后,再去申请 FOCAS 库,能省不少时间。