using System; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Hosting; namespace RIZO.Admin.WebApi.PLC.Service { /// /// PF6螺丝枪基础通讯服务(核心功能:TCP连接、心跳、全报文发收测试) /// 适配PF6开放协议V2.5版本,支持所有已定义报文的批量发送与调试 /// public class PF6ScrewGunService : BackgroundService { // TCP通讯核心对象 private TcpClient _tcpClient; private NetworkStream _networkStream; // 螺丝枪配置(与协议文档一致) private readonly string _screwGunIp = "192.168.1.145"; // 控制器IP(可配置) private readonly int _screwGunPort = 4545; // 协议默认端口(文档明确) private readonly TimeSpan _heartbeatInterval = TimeSpan.FromSeconds(10); // 心跳间隔(文档要求10秒) private readonly TimeSpan _reconnectInterval = TimeSpan.FromSeconds(8); // 重连间隔(延长避免频繁重试) private readonly TimeSpan _testCommandInterval = TimeSpan.FromSeconds(15); // 测试指令间隔(降低频率) private readonly TimeSpan _batchTestInterval = TimeSpan.FromSeconds(30); // 全报文批量测试间隔 // 协议基础常量(严格遵循PF6开放协议V2.5文档) private const string MessageEndFlag = "00"; // 报文结束标识(文档第1页明确) private const int MessageTotalLength = 20; // 发送报文固定长度(文档"字符总长20字节") private const string HeartbeatMid = "9999"; // 心跳指令MID(文档测试页面确认) private const string TestTightenMid = "0610"; // 测试拧紧指令MID(文档功能按钮确认) private const string HandshakeMid = "0001"; // 握手指令MID(文档初始连接流程) private const string PsetQueryMid = "0005"; // 参数组查询MID(文档PSET查询功能) private const string TightenResultMid = "0620"; // 拧紧结果查询MID(文档明确) private const string ProtocolVersion = "3030"; // 协议版本号(文档报文示例) private const string MessagePrefix = "303020"; // 报文固定前缀(文档2017-12-1交互示例) private const string FillChar = "20"; // 填充字符(文档"20填满字符总长") private const int LinkTimeout = 1000; // 链路超时(文档配置页面1000ms) private const int AppCommandTimeout = 1000; // 应用指令超时(文档配置页面1000ms) private const int MaxRetries = 3; // 最大重试次数(文档配置页面3次) private const int ReceiveBufferSize = 4096; // 接收缓冲区扩大(文档响应报文可能超过1024字节) /// /// 服务核心执行逻辑 /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] PF6螺丝枪通讯服务开始启动..."); Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 服务支持报文:握手(0001)、心跳(9999)、测试拧紧(0610)、参数组查询(0005)、拧紧结果查询(0620)"); _tcpClient = new TcpClient(); // 服务主循环:断线自动重连(响应停止指令) while (!stoppingToken.IsCancellationRequested) { try { // 1. 建立TCP连接(带重试) bool connectSuccess = await ConnectWithRetryAsync(stoppingToken); if (!connectSuccess) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 连接重试失败,等待下一轮重连"); await Task.Delay(_reconnectInterval, stoppingToken); continue; } // 2. 发送握手报文并确认(协议必须步骤) bool handshakeSuccess = await PerformHandshakeAsync(stoppingToken); if (!handshakeSuccess) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 握手失败,关闭连接准备重连"); DisconnectFromScrewGun(); await Task.Delay(_reconnectInterval, stoppingToken); continue; } // 3. 并行执行:心跳维持 + 全报文批量测试 + 常规测试指令 var taskArray = new[] { KeepHeartbeatContinuousAsync(stoppingToken), SendBatchAllMessagesTestAsync(stoppingToken), SendTestCommandAndReceiveResultAsync(stoppingToken) }; await Task.WhenAny(taskArray); Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 核心任务退出,准备重连"); } catch (OperationCanceledException) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] PF6螺丝枪通讯服务收到停止指令,正在退出..."); break; } catch (Exception ex) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] PF6螺丝枪通讯服务运行异常,准备重连:{ex.Message}"); } finally { DisconnectFromScrewGun(); Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 当前连接已断开,等待重连..."); if (!stoppingToken.IsCancellationRequested) { await Task.Delay(_reconnectInterval, stoppingToken); } } } Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] PF6螺丝枪通讯服务已停止"); } /// /// 带重试机制的TCP连接(遵循协议重试次数配置) /// private async Task ConnectWithRetryAsync(CancellationToken stoppingToken) { int retryCount = 0; while (retryCount < MaxRetries && !stoppingToken.IsCancellationRequested) { try { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 第{retryCount + 1}次尝试连接螺丝枪:{_screwGunIp}:{_screwGunPort}"); // 重置TcpClient状态 if (_tcpClient.Connected) { _tcpClient.Close(); } _tcpClient?.Dispose(); _tcpClient = new TcpClient(); _tcpClient.ReceiveTimeout = LinkTimeout; _tcpClient.SendTimeout = LinkTimeout; // 建立连接(设置连接超时,避免无限等待) var connectTask = _tcpClient.ConnectAsync(_screwGunIp, _screwGunPort); var timeoutTask = Task.Delay(LinkTimeout * 2, stoppingToken); var completedTask = await Task.WhenAny(connectTask, timeoutTask); if (completedTask == timeoutTask) { throw new TimeoutException("连接超时(超过2000ms)"); } // 初始化网络流(配置超时参数与协议一致) _networkStream = _tcpClient.GetStream(); _networkStream.ReadTimeout = AppCommandTimeout; _networkStream.WriteTimeout = AppCommandTimeout; Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 螺丝枪TCP连接成功!(链路超时:{LinkTimeout}ms,指令超时:{AppCommandTimeout}ms)"); return true; } catch (Exception ex) { retryCount++; Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 第{retryCount}次连接失败:{ex.Message}"); if (retryCount < MaxRetries) { await Task.Delay(1000 * retryCount, stoppingToken); // 重试间隔递增(1s→2s→3s) } } } return false; } /// /// 协议握手流程(发送MID=0001,等待控制器确认) /// private async Task PerformHandshakeAsync(CancellationToken stoppingToken) { try { // 构建握手报文(严格遵循文档格式) var handshakeMessage = BuildStandardMessage(HandshakeMid); Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 准备发送握手报文(MID=0001),报文内容:{handshakeMessage}"); // 发送握手报文(带重试) bool sendSuccess = await SendMessageWithRetryAsync(handshakeMessage, stoppingToken, "握手"); if (!sendSuccess) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 握手报文发送失败(已重试{MaxRetries}次)"); return false; } // 接收握手响应(文档响应报文可能较长,需完整接收) Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 等待握手响应(超时:{AppCommandTimeout}ms)"); var handshakeResponse = await ReceiveMessageAsync(stoppingToken, "握手"); // 校验响应有效性 if (string.IsNullOrEmpty(handshakeResponse)) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 握手响应为空,无效"); return false; } if (!handshakeResponse.EndsWith(MessageEndFlag)) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 握手响应未包含结束标识({MessageEndFlag}),响应内容:{handshakeResponse}"); return false; } Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 握手成功!响应报文:{handshakeResponse}(长度:{handshakeResponse.Length})"); return true; } catch (Exception ex) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 握手过程异常:{ex.Message}"); return false; } } /// /// 持续发送心跳(协议要求10秒间隔,无响应需重连) /// private async Task KeepHeartbeatContinuousAsync(CancellationToken stoppingToken) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 心跳任务启动(间隔:{_heartbeatInterval.TotalSeconds}秒),仅确保发送成功,不强制要求响应"); while (!stoppingToken.IsCancellationRequested && _tcpClient.Connected) { try { var heartbeatMessage = BuildStandardMessage(HeartbeatMid); bool sendSuccess = await SendMessageWithRetryAsync(heartbeatMessage, stoppingToken, "心跳"); if (sendSuccess) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 心跳报文发送成功(MID=9999),报文内容:{heartbeatMessage}"); } else { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 心跳报文发送失败(已重试{MaxRetries}次),终止心跳任务"); break; } await Task.Delay(_heartbeatInterval, stoppingToken); } catch (Exception ex) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 心跳交互异常,终止心跳任务:{ex.Message}"); break; } } } /// /// 常规测试指令发送与响应接收(MID=0610) /// private async Task SendTestCommandAndReceiveResultAsync(CancellationToken stoppingToken) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 常规测试任务启动(间隔:{_testCommandInterval.TotalSeconds}秒,MID=0610)"); while (!stoppingToken.IsCancellationRequested && _tcpClient.Connected) { try { // 构建测试拧紧指令(额外数据"0101"为测试参数) var testMessage = BuildStandardMessage(TestTightenMid, "0101"); Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 准备发送常规测试指令(MID=0610),报文内容:{testMessage}"); // 发送指令(带重试) bool sendSuccess = await SendMessageWithRetryAsync(testMessage, stoppingToken, "常规测试拧紧"); if (!sendSuccess) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 常规测试指令发送失败(已重试{MaxRetries}次),延迟5秒重试"); await Task.Delay(5000, stoppingToken); continue; } // 接收响应(文档响应可能包含多个字段,需完整读取) var testResponse = await ReceiveMessageAsync(stoppingToken, "常规测试拧紧"); Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 常规测试指令响应成功!响应内容:{testResponse}(长度:{testResponse.Length})"); await Task.Delay(_testCommandInterval, stoppingToken); } catch (Exception ex) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 常规测试指令交互异常,延迟后重试:{ex.Message}"); await Task.Delay(3000, stoppingToken); } } } /// /// 批量发送所有已定义报文(用于完整调试,包含所有功能) /// private async Task SendBatchAllMessagesTestAsync(CancellationToken stoppingToken) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 全报文批量测试任务启动(间隔:{_batchTestInterval.TotalSeconds}秒)"); while (!stoppingToken.IsCancellationRequested && _tcpClient.Connected) { try { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] ============== 开始全报文批量测试 =============="); // 1. 参数组查询(MID=0005,查询全部参数组,额外数据为空) await SendSingleMessageWithResponseAsync(PsetQueryMid, "", "参数组查询(MID=0005)", stoppingToken); await Task.Delay(2000, stoppingToken); // 间隔2秒,避免报文拥堵 // 2. 参数组查询(MID=0005,查询指定参数组ID=01) await SendSingleMessageWithResponseAsync(PsetQueryMid, "01", "参数组查询(MID=0005,指定ID=01)", stoppingToken); await Task.Delay(2000, stoppingToken); // 3. 拧紧结果查询(MID=0620,查询全部结果) await SendSingleMessageWithResponseAsync(TightenResultMid, "", "拧紧结果查询(MID=0620)", stoppingToken); await Task.Delay(2000, stoppingToken); // 4. 拧紧结果查询(MID=0620,查询指定SN=0001) await SendSingleMessageWithResponseAsync(TightenResultMid, "0001", "拧紧结果查询(MID=0620,指定SN=0001)", stoppingToken); await Task.Delay(2000, stoppingToken); // 5. 测试拧紧(MID=0610,额外参数=0202) await SendSingleMessageWithResponseAsync(TestTightenMid, "0202", "测试拧紧(MID=0610,参数=0202)", stoppingToken); Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] ============== 全报文批量测试结束 ==============\n"); await Task.Delay(_batchTestInterval, stoppingToken); } catch (Exception ex) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 全报文批量测试异常:{ex.Message}\n"); await Task.Delay(10000, stoppingToken); } } } /// /// 发送单个报文并接收响应(封装通用逻辑,简化批量测试) /// private async Task SendSingleMessageWithResponseAsync(string mid, string extraData, string messageDesc, CancellationToken stoppingToken) { try { // 构建报文 var message = BuildStandardMessage(mid, extraData); Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 准备发送 {messageDesc},报文内容:{message}"); // 发送报文 bool sendSuccess = await SendMessageWithRetryAsync(message, stoppingToken, messageDesc); if (!sendSuccess) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {messageDesc} 发送失败(已重试{MaxRetries}次)"); return; } // 接收响应 var response = await ReceiveMessageAsync(stoppingToken, messageDesc); Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {messageDesc} 响应成功!响应内容:{response}(长度:{response.Length})"); } catch (Exception ex) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {messageDesc} 交互异常:{ex.Message}"); } } /// /// 构建标准PF6协议报文(严格遵循文档格式要求,增加参数校验) /// private string BuildStandardMessage(string mid, string extraData = "") { // 前置严格校验,避免无效报文 if (string.IsNullOrEmpty(mid)) { throw new ArgumentNullException(nameof(mid), "指令MID不能为空(PF6协议要求4位字符)"); } if (mid.Length != 4) { throw new ArgumentException($"MID必须为4位字符(当前MID:{mid},长度:{mid.Length})", nameof(mid)); } // 额外数据避免过长,防止填充后超出长度限制 if (!string.IsNullOrEmpty(extraData) && extraData.Length > 8) { throw new ArgumentException("额外数据长度不能超过8位(避免超出报文总长度限制)", nameof(extraData)); } var messageBuilder = new StringBuilder(); // 拼接格式:固定前缀 + MID + 版本号 + 额外数据 + 填充符 + 结束标识 messageBuilder.Append(MessagePrefix) .Append(mid) .Append(ProtocolVersion) .Append(extraData); // 补全填充字符(确保总长达到20-2=18字节,预留结束标识位置) while (messageBuilder.Length < MessageTotalLength - MessageEndFlag.Length) { messageBuilder.Append(FillChar); } // 追加结束标识 messageBuilder.Append(MessageEndFlag); // 长度校验(严格保证20字节,协议强制要求) if (messageBuilder.Length != MessageTotalLength) { throw new InvalidOperationException($"构建的报文长度不符合PF6协议要求,预期{MessageTotalLength}位,实际{messageBuilder.Length}位,报文内容:{messageBuilder.ToString()}"); } Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 报文构建成功(MID={mid}),长度:{messageBuilder.Length}位,内容:{messageBuilder.ToString()}"); return messageBuilder.ToString(); } /// /// 发送报文(带重试机制,遵循协议重试次数,增加报文描述日志) /// private async Task SendMessageWithRetryAsync(string message, CancellationToken stoppingToken, string messageDesc) { int retryCount = 0; while (retryCount < MaxRetries && !stoppingToken.IsCancellationRequested && _tcpClient.Connected) { try { var sendData = Encoding.ASCII.GetBytes(message); await _networkStream.WriteAsync(sendData, 0, sendData.Length, stoppingToken); await _networkStream.FlushAsync(stoppingToken); Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {messageDesc} 报文发送成功(字节数:{sendData.Length})"); return true; } catch (Exception ex) { retryCount++; Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {messageDesc} 报文发送重试{retryCount}次:{ex.Message}"); if (retryCount < MaxRetries) { await Task.Delay(500, stoppingToken); // 每次重试间隔500ms } } } return false; } /// /// 接收报文(支持动态长度,处理协议响应格式,增加报文描述日志) /// private async Task ReceiveMessageAsync(CancellationToken stoppingToken, string messageDesc) { if (!_tcpClient.Connected || _networkStream == null) { throw new InvalidOperationException("未与螺丝枪建立有效TCP连接,无法接收报文"); } var receiveBuffer = new byte[ReceiveBufferSize]; var totalBytesRead = 0; var responseBuilder = new StringBuilder(); try { // 循环读取直到收到结束标识或超时 while (!stoppingToken.IsCancellationRequested) { var bytesRead = await _networkStream.ReadAsync(receiveBuffer, 0, receiveBuffer.Length, stoppingToken); if (bytesRead == 0) { throw new SocketException((int)SocketError.ConnectionReset); } totalBytesRead += bytesRead; var segment = Encoding.ASCII.GetString(receiveBuffer, 0, bytesRead).TrimEnd('\0', ' '); responseBuilder.Append(segment); Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 接收{messageDesc}响应片段,读取字节数:{bytesRead},累计字节数:{totalBytesRead},片段内容:{segment}"); // 检查是否包含结束标识(协议结束标志"00") if (responseBuilder.ToString().EndsWith(MessageEndFlag)) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 接收{messageDesc}响应完成(已检测到结束标识{MessageEndFlag})"); break; } // 防止缓冲区溢出 if (totalBytesRead >= ReceiveBufferSize) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 接收{messageDesc}响应超过缓冲区上限({ReceiveBufferSize}字节),停止读取"); break; } } var finalResponse = responseBuilder.ToString().TrimEnd('\0', ' '); Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {messageDesc} 响应处理完成,最终内容长度:{finalResponse.Length}"); return finalResponse; } catch (TimeoutException) { // 心跳响应可能超时,返回已接收数据 var partialData = responseBuilder.ToString().TrimEnd('\0', ' '); Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 接收{messageDesc}响应超时,已接收部分数据:{partialData}(长度:{partialData.Length})"); return partialData; } } /// /// 构建PF6协议参数组查询报文(MID=0005,封装专用方法,提高可读性) /// private string BuildPsetQueryMessage(string psetId = "") { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 构建参数组查询报文,参数组ID:{(string.IsNullOrEmpty(psetId) ? "全部" : psetId)}"); return BuildStandardMessage(PsetQueryMid, psetId); } /// /// 构建PF6协议拧紧结果查询报文(MID=0620,封装专用方法,提高可读性) /// private string BuildTightenResultQueryMessage(string snCode = "") { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 构建拧紧结果查询报文,产品SN:{(string.IsNullOrEmpty(snCode) ? "全部" : snCode)}"); return BuildStandardMessage(TightenResultMid, snCode); // 绑定正确MID=0620,支持自定义SN码 } /// /// 断开连接并释放资源(增加详细日志,避免资源泄漏) /// private void DisconnectFromScrewGun() { try { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 准备断开与螺丝枪的连接,释放相关资源"); _networkStream?.Dispose(); _networkStream = null; if (_tcpClient.Connected) { _tcpClient.Close(); Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] TCP连接已主动关闭"); } _tcpClient?.Dispose(); Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 资源释放完成"); } catch (Exception ex) { Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] 断开连接时发生异常:{ex.Message}"); } } /// /// 释放资源(重写BackgroundService的Dispose方法) /// public override void Dispose() { DisconnectFromScrewGun(); base.Dispose(); } } }