2026-01-30 09:24:53 +08:00

537 lines
27 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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();
}
}
}