537 lines
27 KiB
C#
Raw Normal View History

2026-01-30 09:24:53 +08:00
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
{
/// <summary>
/// PF6螺丝枪基础通讯服务核心功能TCP连接、心跳、全报文发收测试
/// 适配PF6开放协议V2.5版本,支持所有已定义报文的批量发送与调试
/// </summary>
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字节
/// <summary>
/// 服务核心执行逻辑
/// </summary>
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螺丝枪通讯服务已停止");
}
/// <summary>
/// 带重试机制的TCP连接遵循协议重试次数配置
/// </summary>
private async Task<bool> 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;
}
/// <summary>
/// 协议握手流程发送MID=0001等待控制器确认
/// </summary>
private async Task<bool> 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;
}
}
/// <summary>
/// 持续发送心跳协议要求10秒间隔无响应需重连
/// </summary>
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;
}
}
}
/// <summary>
/// 常规测试指令发送与响应接收MID=0610
/// </summary>
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);
}
}
}
/// <summary>
/// 批量发送所有已定义报文(用于完整调试,包含所有功能)
/// </summary>
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);
}
}
}
/// <summary>
/// 发送单个报文并接收响应(封装通用逻辑,简化批量测试)
/// </summary>
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}");
}
}
/// <summary>
/// 构建标准PF6协议报文严格遵循文档格式要求增加参数校验
/// </summary>
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();
}
/// <summary>
/// 发送报文(带重试机制,遵循协议重试次数,增加报文描述日志)
/// </summary>
private async Task<bool> 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;
}
/// <summary>
/// 接收报文(支持动态长度,处理协议响应格式,增加报文描述日志)
/// </summary>
private async Task<string> 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;
}
}
/// <summary>
/// 构建PF6协议参数组查询报文MID=0005封装专用方法提高可读性
/// </summary>
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);
}
/// <summary>
/// 构建PF6协议拧紧结果查询报文MID=0620封装专用方法提高可读性
/// </summary>
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码
}
/// <summary>
/// 断开连接并释放资源(增加详细日志,避免资源泄漏)
/// </summary>
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}");
}
}
/// <summary>
/// 释放资源重写BackgroundService的Dispose方法
/// </summary>
public override void Dispose()
{
DisconnectFromScrewGun();
base.Dispose();
}
}
}