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