286 lines
11 KiB
C#
286 lines
11 KiB
C#
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; }
|
||
}
|
||
}
|
||
}
|