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; // 连接状态
// ============== 事件 ==============
///
/// 当解析到稳定(ST)的称重数据时触发。主界面可订阅该事件以获取稳定重量值。
///
public event EventHandler? StableWeightReceived;
///
/// 当通信发生错误(连接失败、接收异常、断线等)时触发。主界面可订阅用于日志与提示。
///
public event EventHandler? CommunicationError;
///
/// 当前是否已连接。
///
public bool IsConnected => _isConnected;
// ============== 构造 ==============
///
/// 构造函数。需要提供 IP 和端口。
///
/// 称重仪表的 TCP Server IP(例如 192.168.1.100)
/// 称重仪表的 TCP Server 端口(1-65535)
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;
}
// ============== 对外方法 ==============
///
/// 启动并连接到称重仪表(TCP Server)。
/// 使用 TouchSocket 的 TerminatorPackageAdapter("\r\n") 将数据按 CRLF 分包为“行”,便于文本解析。
///
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; // 继续抛给调用方,便于上层处理
}
}
///
/// 停止并断开与称重仪表的连接。
///
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 事件)
// ============== 解析逻辑 ==============
///
/// 解析一行称重数据(示例:"US,GS 3.79g g" 或 "ST,NT 12.34 g")。
/// 仅当状态为 ST(稳定)时返回事件参数,其他情况返回 null。
///
private StableWeightEventArgs? TryParseLine(string line)
{
ReadOnlySpan 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 typeSpan = (idx > typeStart) ? s.Slice(typeStart, idx - typeStart) : ReadOnlySpan.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.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 { /* 忽略事件回调异常 */ }
}
// ============== 事件参数类型 ==============
///
/// 稳定称重数据事件参数。
///
public sealed class StableWeightEventArgs : EventArgs
{
/// 原始行文本(已去除 CRLF)。
public string Raw { get; set; } = string.Empty;
/// 状态(仅 ST 才会触发该事件)。
public string Status { get; set; } = string.Empty;
/// 重量类型(如 GS=毛重,NT=净重)。
public string WeightType { get; set; } = string.Empty;
/// 数值。
public decimal Value { get; set; }
/// 单位(如 g、kg)。
public string Unit { get; set; } = string.Empty;
/// 是否稳定(固定为 true)。
public bool IsStable { get; set; }
/// 接收时间戳。
public DateTimeOffset Timestamp { get; set; }
}
}
}