简介:基于C#接口实现上位机与FANUC机器人通信的完整示例包,面向工业自动化开发者、PLC工程师及.NET程序员,解决串口或以太网通信协议对接、指令收发与参数配置等实际问题。包体共73个文件,大小约207.57MB,包含可编译的C#源码工程、项目配置与第三方通信库、调试运行程序、说明文档以及厂家提供的通信接口手册,目录结构完整,支持在Visual Studio中直接打开、还原依赖并运行验证。已有5554人学习下载,具备一定实操验证基础。资料内除了接口定义与控制器实现示例,还提供连接测试工具和参考压缩包,覆盖从接口抽象、类实现到上位机界面调用的完整链路;接口中定义了发送指令、接收反馈、设置参数等方法,具体控制类封装底层通信逻辑,界面层通过按钮触发调用并显示机器人反馈信息,并配有操作说明图片、运行截图与排错思路,便于对照练习和二次开发。适合需要快速搭建工业通信原型、理解C#接口特性的中高级开发者,也可作为课程设计或项目起步参考。 做工业上位机开发的兄弟应该都有过这种经历:现场调试FANUC机器人通信,代码看着逻辑没问题,TCP连接也建立了,但发过去的指令就跟石沉大海一样,机器人一点反应都没有。我在这个项目里用C#配合interface设计了一套与FANUC机器人的通信层,从通信协议约定到TCP收发实现全部走了一遍,跑了两条产线,目前状态稳定。今天把整个设计思路、核心代码和踩过的坑整理出来,给后面要接FANUC或者其他品牌机器人的朋友一个参考。
1. 先别急着写代码:为什么通信层要用interface来收口
1.1 上位机项目的通病:业务逻辑被通信代码绑架
大多数上位机项目,写着写着就变成这样:窗体按钮的事件里直接new TcpClient(),连接、发送、解析全部揉在一个方法里。今天只接一台FANUC还好,明天要加一台ABB,后天客户说"我们也有一台老款的KUKA你顺便接一下",情况就失控了。
更难受的是调试。通信代码和业务代码耦合在一起,机器人没动的时候,你根本分不清是通信层发错了,还是机器人逻辑没对上。我在这个项目里踩过几次这种坑之后,决定把通信层彻底抽出来,用C#的interface给上位机和机器人之间画一条清晰的边界线。
1.2 interface背后那个"依赖倒置"的思路
interface在这里不是花架子,它解决的是上位机开发里最本质的问题:上位机的业务逻辑不应该依赖某个具体机器人的通信协议。
- 业务层只关心"发一个取位置指令,拿到坐标结果",不关心这个指令是TCP发出去的还是串口发出去的;
- 通信层只关心"把字节流发出去,把响应收回来",不关心业务层拿这个数据去干嘛;
- 两者通过interface约定一个"通信契约",互不越界。
接口定义清楚之后,实现类可以随时替换。测试的时候我可以挂一个虚拟机器人Simulator,开发的时候连真机,客户现场出问题了我还能挂一个抓包模式。这些动作不需要改动业务层的任何代码,这就是interface带来的直接好处。
public interface IRobotCommunicator : IDisposable { bool IsConnected { get; } bool Connect(string ipAddress, int port, int timeoutMs = 3000); void Disconnect(); Task<RobotResponse> SendCommandAsync(RobotCommand command); }这个接口很精简,就四个成员。但要注意,connect和send必须分离,不能把连接埋进构造函数里,否则换模拟器的时候没法绕开网络连接,Mock也就失去了意义。
2. FANUC机器人端要做的准备工作
2.1 网络配置和Socket功能选项
FANUC机器人和上位机通信,最常用的是TCP/IP Socket方式。前提是机器人控制柜上开通了Socket Messaging功能选项,这个在购机时可以选配,也可以通过FANUC售后开通。没有这个选项,机器人端的TP程序里是找不到网络通信相关指令的。
网络配置上,FANUC控制柜一般会在示教器上通过MENU -> Setup -> Host Comm或者TCP/IP菜单设置控制柜的IP地址,需要一个固定的IP,和上位机放在同一个网段。别小看这一步,我遇到过一个现场的机器人IP和摄像头网关冲突,搞得通信时断时续,排查了半天。
建议先把机器人端的网络参数确认清楚,记下这几个值:
| 配置项 | 典型值 | 备注 |
|---|---|---|
| 机器人控制器IP | 192.168.1.50 | 需固定,不能在DHCP池里 |
| 子网掩码 | 255.255.255.0 | 与上位机一致 |
| 通信端口 | 8000 | 自定义,避开常用端口 |
| 上位机IP | 192.168.1.100 | 建议也固定 |
2.2 机器人端Socket程序逻辑(服务端模式)
FANUC机器人通常作为Socket Server等待上位机连接。示教器上的TP程序逻辑大致是这样的:
! 伪代码示意,具体指令以机器人安装的选项版本为准 ! 1. 创建服务端Socket,监听8000端口 TCPCREATE('SERVER', 8000, STATUS, SOCKET) ! 2. 等待上位机连接 TCPACCEPT(SOCKET, STATUS, NEWSOCKET) ! 3. 接收上位机发来的数据 TCPRECV(NEWSOCKET, BUFFER, LENGTH, STATUS) ! 4. 解析指令并执行动作 IF BUFFER = 'GET=POS' THEN TP_POS = GetCurrentPos() TCPSEND(NEWSOCKET, '0POS=' + TP_POS, STATUS) ENDIF这一步的难点在于:FANUC的Socket Messaging指令在不同软件版本上名称和参数有差异,建议先在TP程序里写一个最简单"接收指令、原样返回"的回环逻辑,把链路打通之后再扩展业务指令。先用Tera Term或者Windows自带的Telnet客户端手动连接机器人端口,能收发数据,再写正式逻辑,这是最快的方式。
2.3 通信协议约定:谁先说话
很多上位机通信出问题,一半以上是协议没约定清楚。FANUC机器人作为Server,上位机作为Client,这个清晰,但指令格式、响应格式、字符编码、结束符,这四件事必须在动代码之前定死。
我在这个项目里定义的协议很简单,全部基于ASCII文本行:
请求帧: 命令名=参数\r\n 示例: GET=POS\r\n SET=DO_1:ON\r\n 响应帧: 状态码+命令名=数据\r\n 示例: 0POS=124.50,-23.10,655.20\r\n 1ERR=UNKNOW_CMD\r\n- 状态码0表示成功,1表示业务错误,2表示参数格式错误;
- 每帧以
\r\n结束,机器人端解析的时候按行切割; - 全链路使用ASCII编码,发送中文注释在机器人端很容易乱码,能免则免。
协议简单的好处是机器人端TP程序解析容易,上位机这边也不容易出错。很多工程师喜欢用XML或者JSON,但机器人端没有现成的解析库,用KAREL硬写JSON解析纯属给自己挖坑。
3. C#接口设计与核心实现代码
3.1 命令和响应的数据模型
接口有了,接下来定义命令和响应的载体。这段代码看起来简单,但它是整个通信层的"通用语言"。
public class RobotCommand { public string Command { get; set; } // 命令名,如 GET/SET public string Payload { get; set; } // 参数,如 POR/DO_1:ON public override string ToString() => $"{Command}={Payload}\r\n"; } public class RobotResponse { public bool Success { get; set; } public string Message { get; set; } public string RawData { get; set; } }ToString()把命令对象转换成协议规定的文本帧,这样后面实现类里就不会到处拼字符串。
3.2 FanucTcpCommunicator实现类的关键细节
下面是核心的TCP通信实现类,我在里面做了几件比较重要的事,逐一说明。
public class FanucTcpCommunicator : IRobotCommunicator { private TcpClient _tcpClient; private NetworkStream _stream; private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1); private readonly int _defaultTimeout = 2000; public bool IsConnected => _tcpClient != null && _tcpClient.Connected; public bool Connect(string ipAddress, int port, int timeoutMs = 3000) { Disconnect(); _tcpClient = new TcpClient(); var connectTask = _tcpClient.ConnectAsync(ipAddress, port); if (!connectTask.Wait(timeoutMs)) { _tcpClient.Close(); throw new TimeoutException($"连接FANUC机器人超时:{ipAddress}:{port}"); } _stream = _tcpClient.GetStream(); _stream.ReadTimeout = _defaultTimeout; _stream.WriteTimeout = _defaultTimeout; return true; } public void Disconnect() { _stream?.Close(); _stream = null; _tcpClient?.Close(); _tcpClient = null; } public async Task<RobotResponse> SendCommandAsync(RobotCommand command) { await _sendLock.WaitAsync(); try { if (!IsConnected || _stream == null) return new RobotResponse { Success = false, Message = "未连接" }; byte[] sendBuffer = Encoding.ASCII.GetBytes(command.ToString()); await _stream.WriteAsync(sendBuffer, 0, sendBuffer.Length); await _stream.FlushAsync(); byte[] receiveBuffer = new byte[1024]; using (var cts = new CancellationTokenSource(_defaultTimeout)) { int bytesRead = await _stream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length, cts.Token); if (bytesRead == 0) return new RobotResponse { Success = false, Message = "连接已被机器人端关闭" }; string raw = Encoding.ASCII.GetString(receiveBuffer, 0, bytesRead); return ParseResponse(raw); } } catch (OperationCanceledException) { return new RobotResponse { Success = false, Message = "读取响应超时" }; } catch (IOException ex) { return new RobotResponse { Success = false, Message = $"通信异常:{ex.Message}" }; } finally { _sendLock.Release(); } } private RobotResponse ParseResponse(string raw) { string[] lines = raw.TrimEnd('\r', '\n').Split('\n'); string firstLine = lines[0].Trim(); if (string.IsNullOrEmpty(firstLine)) return new RobotResponse { Success = false, Message = "空响应" }; if (firstLine.StartsWith("0")) return new RobotResponse { Success = true, Message = "OK", RawData = firstLine }; return new RobotResponse { Success = false, Message = firstLine, RawData = raw }; } }这里有几个关键点需要注意:
- SemaphoreSlim线程锁:上位机界面上的自动扫描线程和手动操作线程可能同时调用发送方法,TcpClient的NetworkStream不是线程安全的,不锁的话会出现"流被占用"的异常。锁的范围必须覆盖发送和接收,单锁发送会把两个指令交叉写入流,机器人端解析必崩;
- 异步方法内部用CancellationTokenSource做超时:
ReadTimeout在某些底层网络异常时并不能完全兜底,CancellationToken是更可靠的方式; - byte[]长度1024:FANUC机器人指令返回的位置数据一般不会超过这个长度,但如果你的项目会传大块数据,需要提高或改动态读取。
3.3 业务层怎么用interface
实现类写完之后,业务层只依赖接口,不依赖具体类。
public class RobotService { private readonly IRobotCommunicator _communicator; public RobotService(IRobotCommunicator communicator) { _communicator = communicator; } public async Task<double[]> GetCurrentPositionAsync() { var response = await _communicator.SendCommandAsync( new RobotCommand { Command = "GET", Payload = "POS" }); if (!response.Success) throw new Exception($"获取位置失败:{response.Message}"); string[] parts = response.RawData.Split('=')[1].Split(','); return Array.ConvertAll(parts, double.Parse); } }这样业务层完全不需要知道对面是FANUC还是ABB。后续哪怕换一个机器人,只要实现同一个IRobotCommunicator接口,RobotService一行代码都不用动。
4. 实战中容易翻车的几个通信细节
4.1 超时设置不能全靠默认值
有经验的工程师都知道,工业现场的网络环境远比办公室复杂。交换机拥塞、机器人控制柜CPU繁忙、网线接触不良,任何一个因素都可能导致响应延迟。
读超时如果设太短,机器人那边TP程序多执行两条逻辑,上位机就报超时了;设太长,操作工按了急停,上位机还要傻等好几秒。我最终把读超时放在2000ms,写超时1000ms,收发分离。connect超时单独3000ms。三者不能共用一个值,否则调试的时候很难定位是哪一步慢。
4.2 断线重连的时机和幂等性
机器人重启、网线被现场物料刮断,都是常见事。断线之后怎么处理,最好在设计阶段就想好:
- 发送指令时发现
IsConnected == false,直接触发自动重连,不要返回错误让操作员手动处理; - 重连要加间隔,比如失败后延迟2秒再试,防止机器人还没启动完就疯狂重连;
- 重连成功后主动发送一次
PING=PING探活指令,确认机器人端收发链路恢复。
public async Task EnsureConnectedAsync(string ip, int port) { if (IsConnected) return; for (int i = 0; i < 5; i++) { try { Connect(ip, port, 3000); var ping = await SendCommandAsync(new RobotCommand { Command = "PING", Payload = "PING" }); if (ping.Success) return; } catch { await Task.Delay(2000); } } throw new Exception("机器人通信重连失败"); }4.3 数据解析不要只读一次
Windows的TCP接收有个特性:一次Read不一定能收到完整的一帧数据。机器人端的TCPSEND可能把一帧数据拆成两个TCP包发出来,或者反过来,两个响应帧粘在一个包里。如果只调一次Read就解析,极大概率会碰到半包或者粘包。
更稳的做法是循环读取,直到拿到的数据以\r\n结尾:
private string ReadLine(NetworkStream stream, CancellationToken ct) { var buffer = new byte[1]; var lineBuilder = new StringBuilder(); while (true) { ct.ThrowIfCancellationRequested(); int read = stream.Read(buffer, 0, 1); if (read == 0) break; char ch = (char)buffer[0]; if (ch == '\n') break; if (ch != '\r') lineBuilder.Append(ch); } return lineBuilder.ToString(); }这个方案是按字节读,性能不算最优,但对于机器人通信这种低频交互场景完全够用,而且逻辑简单不容易出错。如果数据量大,可以改成ReadAsync到缓冲区后缓存起来再按行切,道理一样。
5. 亲测排障记录:三个真实问题的完整排查链路
5.1 问题一:机器人"收到指令但不动"
现象:上位机发送SET=DO_1:ON,返回正常,但机器人侧的DO_1没有动作。
初步猜测:指令格式错了?机器人端解析有问题?还是DO编号对不上?
排查过程:
- 先用Tera Term手动连接机器人端口,逐条发送指令,发现
SET=DO_1:ON确实返回成功; - 但回到示教器看,DO_1依然是OFF,说明机器人端程序逻辑有问题;
- 单步运行TP程序,发现TCPRECV之后多了一条IF判断,判断的字符串是
DO_1=ON而不是DO_1:ON,上位机和机器人端协议里,参数分隔符一个用的冒号,一个用的等号,两边根本没对上。
根因:协议约定只在纸上写了,没有在机器人端做同义对照。这个锅不在C#代码,在沟通环节。
解决:统一把参数分隔符定为冒号,上位机和机器人端程序都改掉,用一条自动化测试脚本把常用指令全部过一遍。
5.2 问题二:返回数据的第一个字节神秘消失
现象:上位机收到的响应始终少一个字符,比如0POS=...变成了POS=...,状态码0丢了。
初步猜测:解码问题?TCP分包问题?
排查过程:
- 抓包对比,发现上位机发出的请求和机器人返回的TCP包都完整,但Wireshark里能看到返回包的前面有几个特殊的字节序列
FF FA之类; - 查了一下发现FANUC Socket Messaging默认开启了TELNET协议模式,TELNET的IAC命令在传输过程中会被处理掉,而第一个可显示的字符被当成了TELNET协商内容;
- 那个"消失"的
0,实际上是被TELNET层解释为子选项协商的一部分。
根因:机器人端Socket打开了TELNET模式,应该使用Raw模式(原始TCP)通信。
解决:在机器人端Socket Messaging配置里关闭TELNET,改为RAW模式,重连后响应数据完整。
这个坑极具迷惑性,因为连接正常、请求正常、大部分响应正常,只有首字节消失或者偶尔多出几个奇怪字符,不抓包基本发现不了。
5.3 问题三:长时间空闲后连接自动断开
现象:产线中午停线一小时,下午恢复生产,上位机发指令全部超时,重连之后恢复正常。
初步猜测:机器人端TCP超时?交换机端口老化?
排查过程:
- 查看机器人端TP程序日志,发现控制柜显示连接早已断开;
- 检查上位机程序日志,发现上一次成功通信是停线前,之后一直没有发送任何数据;
- 检查交换机配置,发现启用了闲置连接回收策略,空闲超过1800秒会发RST包断开连接;
- 机器人端作为Server,在没有数据收发时不会主动维持连接,交换机RST一到,两边都不知道,处于假连接状态。
根因:网络设备空闲回收机制 + 没有心跳。
解决:上位机加一个独立的心跳线程,每10秒发送一次PING=PING,机器人端收到后回复,链路保持活跃。同时在上位机做好断线检测,万一心跳失败立刻触发自动重连。
这里要注意心跳指令必须是轻量的,不能让机器人执行实际动作,否则每10秒机器人就动一下,生产线就乱了。心跳的响应状态码和处理逻辑要单独定义。
6. interface抽象带来的额外收益:模拟器与多品牌适配
最后说一下interface设计给我省下的两个大麻烦。
第一个是模拟器。开发阶段没有机器人可以联调,我写了一个SimulatorCommunicator : IRobotCommunicator,随机生成位置数据返回。业务逻辑的开发、界面调试、异常处理,全都可以在没有硬件的情况下完成。到了现场,只需要把依赖注入的实现类从Simulator换成FanucTcpCommunicator,其他代码一行不用改。
public class SimulatorCommunicator : IRobotCommunicator { public bool IsConnected => true; public bool Connect(string ipAddress, int port, int timeoutMs = 3000) => true; public void Disconnect() { } public Task<RobotResponse> SendCommandAsync(RobotCommand command) { var random = new Random(); string pos = $"0POS={random.Next(100, 500)},{random.Next(-100, 100)},{random.Next(300, 800)}"; return Task.FromResult(new RobotResponse { Success = true, RawData = pos }); } }第二个是品牌适配。后来客户那边真的加了一台其他品牌的机器人,通信协议不一样,但同样是TCP加文本指令。我只需要多写一个实现类,在工厂方法里按配置启用不同的实现,完全不需要动RobotService和界面层。这就是开头说的"面向接口编程"最实在的回报:它能让你在一个项目里轻松应对"未来还会变"的部分。
项目收尾的时候我复盘了一下,interface这套做法本身不复杂,复杂的是"提前看清楚哪部分会变"。通信协议会变,机器人品牌会变,网络环境会变,但"发指令、收响应"这个交互模式不会变。把不会变的东西抽象成接口,把会变的东西塞进实现类,上位机项目就能越做越顺手。
本文还有配套的精品资源,点击获取