Files
FATrace/FATrace.App/TScalTcp.cs
2025-10-29 11:42:58 +08:00

286 lines
11 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.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Globalization; // 数值解析区域性
using System.Net; // IPAddress 校验
using System.Threading; // CancellationTokenSource
using TouchSocket.Core; // TouchSocket 核心扩展Span.ToString 等)
using TouchSocket.Sockets; // TcpClient、TerminatorPackageAdapter 等
namespace FATrace.App
{
public class TScalTcp
{
// ============== 字段与状态 ==============
private readonly string _ip; // 目标仪表的 IP
private readonly int _port; // 目标仪表的端口
private TcpClient? _client; // TouchSocket 的 TcpClient注意命名空间是 TouchSocket.Sockets
private CancellationTokenSource? _cts; // 取消标记,用于 Stop 时快速中断
private volatile bool _isConnected; // 连接状态
// ============== 事件 ==============
/// <summary>
/// 当解析到稳定ST的称重数据时触发。主界面可订阅该事件以获取稳定重量值。
/// </summary>
public event EventHandler<StableWeightEventArgs>? StableWeightReceived;
/// <summary>
/// 当通信发生错误(连接失败、接收异常、断线等)时触发。主界面可订阅用于日志与提示。
/// </summary>
public event EventHandler<string>? CommunicationError;
/// <summary>
/// 当前是否已连接。
/// </summary>
public bool IsConnected => _isConnected;
// ============== 构造 ==============
/// <summary>
/// 构造函数。需要提供 IP 和端口。
/// </summary>
/// <param name="ip">称重仪表的 TCP Server IP例如 192.168.1.100</param>
/// <param name="port">称重仪表的 TCP Server 端口1-65535</param>
public TScalTcp(string ip, int port)
{
if (string.IsNullOrWhiteSpace(ip))
throw new ArgumentException("IP 不能为空", nameof(ip));
if (!IPAddress.TryParse(ip, out _))
throw new ArgumentException("IP 格式不正确", nameof(ip));
if (port <= 0 || port > 65535)
throw new ArgumentOutOfRangeException(nameof(port), "端口必须位于 1-65535 之间");
_ip = ip;
_port = port;
}
// ============== 对外方法 ==============
/// <summary>
/// 启动并连接到称重仪表TCP Server
/// 使用 TouchSocket 的 TerminatorPackageAdapter("\r\n") 将数据按 CRLF 分包为“行”,便于文本解析。
/// </summary>
public async Task StartAsync()
{
// 保证幂等:启动前先停止旧连接
await StopAsync().ConfigureAwait(false);
_cts = new CancellationTokenSource();
var client = new TcpClient();
// 配置:远端地址 + 行分包适配器CRLF
var config = new TouchSocketConfig()
.SetRemoteIPHost($"{_ip}:{_port}")
.SetTcpDataHandlingAdapter(() => new TerminatorPackageAdapter("\r\n"));
// 订阅接收与断线事件
client.Received = (c, e) =>
{
try
{
// 设备发送 ASCII 文本,这里按 ASCII 解码即可UTF-8 对 ASCII 兼容,但此处更明确)
// 使用 ByteBlock.ToArray() 直接解码,兼容不同版本的 ByteBlock API
string line = Encoding.ASCII.GetString(e.ByteBlock.ToArray()).Trim();
if (string.IsNullOrEmpty(line))
return Task.CompletedTask;
var parsed = TryParseLine(line);
if (parsed != null && parsed.IsStable)
{
// 仅稳定(ST)时上报
try
{
StableWeightReceived?.Invoke(this, parsed);
}
catch (Exception ex)
{
// 订阅方异常不影响接收线程
RaiseCommunicationError($"处理稳定值事件异常:{ex.Message}");
}
}
}
catch (Exception ex)
{
// 解码或解析异常,通知但不中断接收
RaiseCommunicationError($"接收处理异常:{ex.Message}");
}
return Task.CompletedTask;
}; // 每收到一行CRLF 结尾)即触发
client.Closed = (c, e) =>
{
_isConnected = false;
RaiseCommunicationError("连接已断开");
return Task.CompletedTask;
};
try
{
await client.SetupAsync(config).ConfigureAwait(false); // 载入配置
await client.ConnectAsync().ConfigureAwait(false); // 建立连接(失败会抛异常)
_client = client;
_isConnected = true;
}
catch (Exception ex)
{
// 连接失败:通知错误并清理
RaiseCommunicationError($"连接失败:{ex.Message}");
try { await client.CloseAsync().ConfigureAwait(false); } catch { /* 忽略关闭异常 */ }
_client = null;
_isConnected = false;
throw; // 继续抛给调用方,便于上层处理
}
}
/// <summary>
/// 停止并断开与称重仪表的连接。
/// </summary>
public async Task StopAsync()
{
var client = _client;
_client = null; // 先置空,避免并发再次使用
try
{
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
if (client != null)
{
try
{
// 反订阅,避免关闭后回调
client.Received = null; // v3 采用委托属性方式
client.Closed = null;
}
catch { /* 忽略反订阅异常 */ }
await client.CloseAsync().ConfigureAwait(false);
}
}
catch (Exception ex)
{
RaiseCommunicationError($"关闭连接异常:{ex.Message}");
}
finally
{
_isConnected = false;
}
}
// ============== 内部回调 ==============
// (已通过 lambda 直接订阅 Received 事件)
// ============== 解析逻辑 ==============
/// <summary>
/// 解析一行称重数据(示例:"US,GS 3.79g g" 或 "ST,NT 12.34 g")。
/// 仅当状态为 ST稳定时返回事件参数其他情况返回 null。
/// </summary>
private StableWeightEventArgs? TryParseLine(string line)
{
ReadOnlySpan<char> s = line.AsSpan().Trim();
if (s.Length < 2)
return null;
// 判断状态前缀ST/US
bool isStable;
char c0 = char.ToUpperInvariant(s[0]);
char c1 = char.ToUpperInvariant(s[1]);
if (c0 == 'S' && c1 == 'T') isStable = true;
else if (c0 == 'U' && c1 == 'S') isStable = false;
else return null;
// 查找逗号分隔
int comma = s.IndexOf(',');
if (comma < 0)
return null;
// 逗号后类型GS/NT 等)
int idx = comma + 1;
while (idx < s.Length && char.IsWhiteSpace(s[idx])) idx++;
int typeStart = idx;
while (idx < s.Length && char.IsLetter(s[idx])) idx++;
ReadOnlySpan<char> typeSpan = (idx > typeStart) ? s.Slice(typeStart, idx - typeStart) : ReadOnlySpan<char>.Empty;
// 跳过空白,查找数字
while (idx < s.Length && char.IsWhiteSpace(s[idx])) idx++;
int numStart = -1;
for (int i = idx; i < s.Length; i++)
{
char ch = s[i];
if ((ch >= '0' && ch <= '9') || ch == '+' || ch == '-') { numStart = i; break; }
}
if (numStart < 0)
return null;
int p = numStart; bool dotSeen = false;
while (p < s.Length)
{
char ch = s[p];
if (ch >= '0' && ch <= '9') p++;
else if (ch == '.' && !dotSeen) { dotSeen = true; p++; }
else break;
}
if (p <= numStart)
return null;
var numSpan = s.Slice(numStart, p - numStart);
if (!decimal.TryParse(numSpan, NumberStyles.Number, CultureInfo.InvariantCulture, out var value))
return null;
// 读取紧随数字后的单位字母(如 g/kg
int unitStart = p;
while (p < s.Length && char.IsLetter(s[p])) p++;
var unitSpan = (p > unitStart) ? s.Slice(unitStart, p - unitStart) : ReadOnlySpan<char>.Empty;
if (!isStable)
return null; // 仅稳定值上报
return new StableWeightEventArgs
{
Raw = line,
Status = "ST",
WeightType = typeSpan.IsEmpty ? string.Empty : typeSpan.ToString(),
Value = value,
Unit = unitSpan.IsEmpty ? string.Empty : unitSpan.ToString(),
IsStable = true,
Timestamp = DateTimeOffset.Now
};
}
// ============== 工具 ==============
private void RaiseCommunicationError(string message)
{
try { CommunicationError?.Invoke(this, message); }
catch { /* 忽略事件回调异常 */ }
}
// ============== 事件参数类型 ==============
/// <summary>
/// 稳定称重数据事件参数。
/// </summary>
public sealed class StableWeightEventArgs : EventArgs
{
/// <summary>原始行文本(已去除 CRLF。</summary>
public string Raw { get; set; } = string.Empty;
/// <summary>状态(仅 ST 才会触发该事件)。</summary>
public string Status { get; set; } = string.Empty;
/// <summary>重量类型(如 GS=毛重NT=净重)。</summary>
public string WeightType { get; set; } = string.Empty;
/// <summary>数值。</summary>
public decimal Value { get; set; }
/// <summary>单位(如 g、kg。</summary>
public string Unit { get; set; } = string.Empty;
/// <summary>是否稳定(固定为 true。</summary>
public bool IsStable { get; set; }
/// <summary>接收时间戳。</summary>
public DateTimeOffset Timestamp { get; set; }
}
}
}