From a355373423026aa435798c556d107c906cce5cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Chsc=E2=80=9D?= <“huangsucan@kaiyili-lab.com”> Date: Tue, 9 Jun 2026 15:51:00 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=BE=E5=A4=87=E5=9F=BA=E7=A1=80=E5=91=BD?= =?UTF-8?q?=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DeviceCommand/Base/EnovaDataReporter.cs | 81 ++++++++++ DeviceCommand/Base/IBaseInterface.cs | 16 ++ DeviceCommand/Base/IEnovaDataReporter.cs | 31 ++++ DeviceCommand/Base/IModbusDevice.cs | 19 +++ DeviceCommand/Base/IS7Device.cs | 22 +++ DeviceCommand/Base/ISerialPort.cs | 13 ++ DeviceCommand/Base/ITcp.cs | 15 ++ DeviceCommand/Base/ModbusRtu.cs | 164 +++++++++++++++++++++ DeviceCommand/Base/ModbusTcp.cs | 158 ++++++++++++++++++++ DeviceCommand/Base/S7Device.cs | 169 +++++++++++++++++++++ DeviceCommand/Base/Serial_Port.cs | 156 ++++++++++++++++++++ DeviceCommand/Base/TCP.cs | 179 +++++++++++++++++++++++ DeviceCommand/DeviceCommand.csproj | 24 +++ LAEPS.sln => IOT.sln | 12 +- LOT/LOT.csproj | 1 + Model/Model.csproj | 6 +- Model/Model/EnovaChannelReportData.cs | 61 ++++++++ 17 files changed, 1119 insertions(+), 8 deletions(-) create mode 100644 DeviceCommand/Base/EnovaDataReporter.cs create mode 100644 DeviceCommand/Base/IBaseInterface.cs create mode 100644 DeviceCommand/Base/IEnovaDataReporter.cs create mode 100644 DeviceCommand/Base/IModbusDevice.cs create mode 100644 DeviceCommand/Base/IS7Device.cs create mode 100644 DeviceCommand/Base/ISerialPort.cs create mode 100644 DeviceCommand/Base/ITcp.cs create mode 100644 DeviceCommand/Base/ModbusRtu.cs create mode 100644 DeviceCommand/Base/ModbusTcp.cs create mode 100644 DeviceCommand/Base/S7Device.cs create mode 100644 DeviceCommand/Base/Serial_Port.cs create mode 100644 DeviceCommand/Base/TCP.cs create mode 100644 DeviceCommand/DeviceCommand.csproj rename LAEPS.sln => IOT.sln (94%) create mode 100644 Model/Model/EnovaChannelReportData.cs diff --git a/DeviceCommand/Base/EnovaDataReporter.cs b/DeviceCommand/Base/EnovaDataReporter.cs new file mode 100644 index 0000000..9fa197e --- /dev/null +++ b/DeviceCommand/Base/EnovaDataReporter.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Model.Model; +using Newtonsoft.Json; + +namespace DeviceCommand.Base +{ + + public class EnovaDataReporter : IEnovaDataReporter + { + private readonly HttpClient _httpClient; + + // 显式实现/自动属性,方便外部随时更新配置 + public string TargetUrl { get; set; } = "http://127.0.0.1:8080/api/channel/state"; + public int TimeoutMilliseconds { get; set; } = 5000; + + /// + /// 构造函数注入 HttpClient(符合 Prism 依赖注入规范) + /// + public EnovaDataReporter(HttpClient httpClient) + { + // 如果容器没有注入,则给个默认的单例/实例防空 + _httpClient = httpClient ?? new HttpClient(); + } + + public async Task ReportChannelStateAsync(List dataList, CancellationToken ct = default) + { + if (dataList == null || dataList.Count == 0) + { + return new EnovaReportResponse { Success = false, ErrorInfo = "上报数据集合为空" }; + } + + if (string.IsNullOrWhiteSpace(TargetUrl)) + { + return new EnovaReportResponse { Success = false, ErrorInfo = "目标上报 URL 未配置" }; + } + + try + { + // 1. 序列化为标准 JSON 字符串 + string jsonPayload = JsonConvert.SerializeObject(dataList); + var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json"); + + // 2. 绑定联动超时控制 + using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); + if (TimeoutMilliseconds > 0) + { + cts.CancelAfter(TimeoutMilliseconds); + } + + HttpResponseMessage response = await _httpClient.PostAsync(TargetUrl, content, cts.Token); + + // 4. 解析返回值 + if (response.IsSuccessStatusCode) + { + string responseContent = await response.Content.ReadAsStringAsync(); + var result = JsonConvert.DeserializeObject(responseContent); + return result ?? new EnovaReportResponse { Success = true }; // 防止对方返回空Body [cite: 261] + } + else + { + return new EnovaReportResponse + { + Success = false, + ErrorInfo = $"服务器响应错误代码: {(int)response.StatusCode} {response.ReasonPhrase}" + }; + } + } + catch (Exception ex) + { + // 完美承接你上位机原有的异常日志记录器逻辑 + // Logger.LoggerHelper.ErrorWithNotify($"Enova3 数据上传失败: {ex.Message}"); + return new EnovaReportResponse { Success = false, ErrorInfo = $"网络异常: {ex.Message}" }; + } + } + } +} \ No newline at end of file diff --git a/DeviceCommand/Base/IBaseInterface.cs b/DeviceCommand/Base/IBaseInterface.cs new file mode 100644 index 0000000..ebffb50 --- /dev/null +++ b/DeviceCommand/Base/IBaseInterface.cs @@ -0,0 +1,16 @@ +using NModbus; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace DeviceCommand.Base +{ + public interface IBaseInterface + { + public bool IsConnected { get; } + public Task ConnectAsync(CancellationToken ct = default); + public void Close(); + } +} diff --git a/DeviceCommand/Base/IEnovaDataReporter.cs b/DeviceCommand/Base/IEnovaDataReporter.cs new file mode 100644 index 0000000..15a6087 --- /dev/null +++ b/DeviceCommand/Base/IEnovaDataReporter.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Model.Model; + +namespace DeviceCommand.Base +{ + /// + /// Enova3 上位机数据上报核心接口 + /// + public interface IEnovaDataReporter + { + /// + /// 客户平台接收数据的目标 HTTP URL + /// + string TargetUrl { get; set; } + + /// + /// HTTP 请求超时时间(毫秒) + /// + int TimeoutMilliseconds { get; set; } + + /// + /// 异步推送通道的实时状态数据到客户平台 + /// + /// 包含各通道状态的采集数据集合 + /// 取消令牌 + /// 平台服务器的响应状态 + Task ReportChannelStateAsync(List dataList, CancellationToken ct = default); + } +} \ No newline at end of file diff --git a/DeviceCommand/Base/IModbusDevice.cs b/DeviceCommand/Base/IModbusDevice.cs new file mode 100644 index 0000000..360e843 --- /dev/null +++ b/DeviceCommand/Base/IModbusDevice.cs @@ -0,0 +1,19 @@ +using NModbus; + + +namespace DeviceCommand.Base +{ + public interface IModbusDevice : IBaseInterface,IDisposable + { + IModbusMaster Modbus { get; } + + Task WriteSingleRegisterAsync(byte slaveAddress, ushort registerAddress, ushort value, CancellationToken ct = default); + Task WriteMultipleRegistersAsync(byte slaveAddress, ushort startAddress, ushort[] values, CancellationToken ct = default); + Task ReadHoldingRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default); + + Task WriteSingleCoilAsync(byte slaveAddress, ushort coilAddress, bool value, CancellationToken ct = default); + Task ReadCoilsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default); + + Task ReadInputRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default); + } +} \ No newline at end of file diff --git a/DeviceCommand/Base/IS7Device.cs b/DeviceCommand/Base/IS7Device.cs new file mode 100644 index 0000000..b58d401 --- /dev/null +++ b/DeviceCommand/Base/IS7Device.cs @@ -0,0 +1,22 @@ +using S7.Net; +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace DeviceCommand.Base +{ + public interface IS7Device : IBaseInterface, IDisposable + { + // 暴露出 S7.Net 原生的 Plc 对象,方便外部进行复杂扩展 + Plc PlcContext { get; } + + // 核心读写接口(支持直接传物理地址,如 "DB1.DBD0" 或 "M0.0") + Task WriteAsync(string address, object value, CancellationToken ct = default); + Task ReadAsync(string address, CancellationToken ct = default); + Task ReadAsync(string address, CancellationToken ct = default); + + // 批量读取/写入原始字节(常用于大块数据交互) + Task ReadBytesAsync(DataType dataType, int db, int startByteAdr, int count, CancellationToken ct = default); + Task WriteBytesAsync(DataType dataType, int db, int startByteAdr, byte[] value, CancellationToken ct = default); + } +} \ No newline at end of file diff --git a/DeviceCommand/Base/ISerialPort.cs b/DeviceCommand/Base/ISerialPort.cs new file mode 100644 index 0000000..e54a802 --- /dev/null +++ b/DeviceCommand/Base/ISerialPort.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace DeviceCommand.Base +{ + public interface ISerialPort : IBaseInterface,IDisposable + { + Task SendAsync(string data, CancellationToken ct = default); + Task ReadAsync(string delimiter = "\n", CancellationToken ct = default); + Task WriteReadAsync(string command, string delimiter = "\n", CancellationToken ct = default); + } +} \ No newline at end of file diff --git a/DeviceCommand/Base/ITcp.cs b/DeviceCommand/Base/ITcp.cs new file mode 100644 index 0000000..e889668 --- /dev/null +++ b/DeviceCommand/Base/ITcp.cs @@ -0,0 +1,15 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace DeviceCommand.Base +{ + public interface ITcp : IBaseInterface,IDisposable + { + Task SendAsync(byte[] buffer, CancellationToken ct = default); + Task SendAsync(string str, CancellationToken ct = default); + Task ReadAsync(int length, CancellationToken ct = default); + Task ReadAsync(string delimiter = "\n", CancellationToken ct = default); + Task WriteReadAsync(string command, string delimiter = "\n", CancellationToken ct = default); + } +} \ No newline at end of file diff --git a/DeviceCommand/Base/ModbusRtu.cs b/DeviceCommand/Base/ModbusRtu.cs new file mode 100644 index 0000000..c9ceecb --- /dev/null +++ b/DeviceCommand/Base/ModbusRtu.cs @@ -0,0 +1,164 @@ +using NModbus; +using NModbus.Serial; +using System; +using System.IO.Ports; +using System.Threading; +using System.Threading.Tasks; + +namespace DeviceCommand.Base +{ + public class ModbusRtu : IModbusDevice + { + public string PortName { get; private set; } = "COM1"; + public int BaudRate { get; private set; } = 9600; + public int DataBits { get; private set; } = 8; + public StopBits StopBits { get; private set; } = StopBits.One; + public Parity Parity { get; private set; } = Parity.None; + public int ReadTimeout { get; private set; } = 3000; + public int WriteTimeout { get; private set; } = 3000; + + private SerialPort _serialPort; + public IModbusMaster Modbus { get; private set; } + public bool IsConnected => _serialPort?.IsOpen ?? false; + + protected readonly SemaphoreSlim _commLock = new(1, 1); + + public ModbusRtu() + { + _serialPort = new SerialPort(); + } + + public void ConfigureDevice(string portName, int baudRate, int dataBits = 8, StopBits stopBits = StopBits.One, Parity parity = Parity.None, int readTimeout = 3000, int writeTimeout = 3000) + { + PortName = portName; + BaudRate = baudRate; + DataBits = dataBits; + StopBits = stopBits; + Parity = parity; + ReadTimeout = readTimeout; + WriteTimeout = writeTimeout; + } + + public virtual async Task ConnectAsync(CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + if (_serialPort.IsOpen) + _serialPort.Close(); + + _serialPort.PortName = PortName; + _serialPort.BaudRate = BaudRate; + _serialPort.DataBits = DataBits; + _serialPort.StopBits = StopBits; + _serialPort.Parity = Parity; + _serialPort.ReadTimeout = ReadTimeout; + _serialPort.WriteTimeout = WriteTimeout; + _serialPort.Open(); + + Modbus = new ModbusFactory().CreateRtuMaster(_serialPort); + return true; + } + finally + { + _commLock.Release(); + } + } + + public virtual void Close() + { + if (_serialPort.IsOpen) + _serialPort.Close(); + } + + public async Task WriteSingleRegisterAsync(byte slaveAddress, ushort registerAddress, ushort value, CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + await Modbus.WriteSingleRegisterAsync(slaveAddress, registerAddress, value) + .WaitAsync(TimeSpan.FromMilliseconds(WriteTimeout), ct); + } + finally + { + _commLock.Release(); + } + } + + public async Task WriteMultipleRegistersAsync(byte slaveAddress, ushort startAddress, ushort[] values, CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + await Modbus.WriteMultipleRegistersAsync(slaveAddress, startAddress, values) + .WaitAsync(TimeSpan.FromMilliseconds(WriteTimeout), ct); + } + finally + { + _commLock.Release(); + } + } + + public async Task ReadHoldingRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + return await Modbus.ReadHoldingRegistersAsync(slaveAddress, startAddress, numberOfPoints) + .WaitAsync(TimeSpan.FromMilliseconds(ReadTimeout), ct); + } + finally + { + _commLock.Release(); + } + } + + public async Task WriteSingleCoilAsync(byte slaveAddress, ushort coilAddress, bool value, CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + await Modbus.WriteSingleCoilAsync(slaveAddress, coilAddress, value) + .WaitAsync(TimeSpan.FromMilliseconds(WriteTimeout), ct); + } + finally + { + _commLock.Release(); + } + } + + public async Task ReadCoilsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + return await Modbus.ReadCoilsAsync(slaveAddress, startAddress, numberOfPoints) + .WaitAsync(TimeSpan.FromMilliseconds(ReadTimeout), ct); + } + finally + { + _commLock.Release(); + } + } + + public async Task ReadInputRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + return await Modbus.ReadInputRegistersAsync(slaveAddress, startAddress, numberOfPoints) + .WaitAsync(TimeSpan.FromMilliseconds(ReadTimeout), ct); + } + finally + { + _commLock.Release(); + } + } + + public void Dispose() + { + _serialPort?.Dispose(); + _commLock?.Dispose(); + } + } +} \ No newline at end of file diff --git a/DeviceCommand/Base/ModbusTcp.cs b/DeviceCommand/Base/ModbusTcp.cs new file mode 100644 index 0000000..8db68f3 --- /dev/null +++ b/DeviceCommand/Base/ModbusTcp.cs @@ -0,0 +1,158 @@ +using NModbus; +using System; +using System.Net; +using System.Net.Sockets; +using System.Threading; +using System.Threading.Tasks; + +namespace DeviceCommand.Base +{ + public class ModbusTcp : IModbusDevice + { + public string IPAddress { get; private set; } = "127.0.0.1"; + public int Port { get; private set; } = 502; + public int SendTimeout { get; private set; } = 3000; + public int ReceiveTimeout { get; private set; } = 3000; + + private TcpClient _tcpClient; + public IModbusMaster Modbus { get; private set; } + public bool IsConnected => _tcpClient?.Connected ?? false; + + protected readonly SemaphoreSlim _commLock = new(1, 1); + + public ModbusTcp() + { + _tcpClient = new TcpClient(); + } + + public void ConfigureDevice(string ipAddress, int port, int sendTimeout = 3000, int receiveTimeout = 3000) + { + IPAddress = ipAddress; + Port = port; + SendTimeout = sendTimeout; + ReceiveTimeout = receiveTimeout; + } + + public virtual async Task ConnectAsync(CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + if (_tcpClient.Connected) + { + var remoteEndPoint = (IPEndPoint)_tcpClient.Client.RemoteEndPoint!; + if (remoteEndPoint.Address.MapToIPv4().ToString() == IPAddress && remoteEndPoint.Port == Port) + return true; + } + + _tcpClient.Close(); + _tcpClient.Dispose(); + _tcpClient = new TcpClient(); + + await _tcpClient.ConnectAsync(IPAddress, Port, ct); + Modbus = new ModbusFactory().CreateMaster(_tcpClient); + return true; + } + finally + { + _commLock.Release(); + } + } + + public virtual void Close() + { + if (_tcpClient.Connected) _tcpClient.Close(); + } + + public async Task WriteSingleRegisterAsync(byte slaveAddress, ushort registerAddress, ushort value, CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + // 修复:FromMinutes 改为 FromMilliseconds + await Modbus.WriteSingleRegisterAsync(slaveAddress, registerAddress, value) + .WaitAsync(TimeSpan.FromMilliseconds(SendTimeout), ct); + } + finally + { + _commLock.Release(); + } + } + + public async Task WriteMultipleRegistersAsync(byte slaveAddress, ushort startAddress, ushort[] values, CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + await Modbus.WriteMultipleRegistersAsync(slaveAddress, startAddress, values) + .WaitAsync(TimeSpan.FromMilliseconds(SendTimeout), ct); + } + finally + { + _commLock.Release(); + } + } + + public async Task ReadHoldingRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + return await Modbus.ReadHoldingRegistersAsync(slaveAddress, startAddress, numberOfPoints) + .WaitAsync(TimeSpan.FromMilliseconds(ReceiveTimeout), ct); + } + finally + { + _commLock.Release(); + } + } + + public async Task WriteSingleCoilAsync(byte slaveAddress, ushort coilAddress, bool value, CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + await Modbus.WriteSingleCoilAsync(slaveAddress, coilAddress, value) + .WaitAsync(TimeSpan.FromMilliseconds(SendTimeout), ct); + } + finally + { + _commLock.Release(); + } + } + + public async Task ReadCoilsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + return await Modbus.ReadCoilsAsync(slaveAddress, startAddress, numberOfPoints) + .WaitAsync(TimeSpan.FromMilliseconds(ReceiveTimeout), ct); + } + finally + { + _commLock.Release(); + } + } + + public async Task ReadInputRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + return await Modbus.ReadInputRegistersAsync(slaveAddress, startAddress, numberOfPoints) + .WaitAsync(TimeSpan.FromMilliseconds(ReceiveTimeout), ct); + } + finally + { + _commLock.Release(); + } + } + + public void Dispose() + { + _tcpClient?.Dispose(); + _commLock?.Dispose(); + } + } +} \ No newline at end of file diff --git a/DeviceCommand/Base/S7Device.cs b/DeviceCommand/Base/S7Device.cs new file mode 100644 index 0000000..6a9d672 --- /dev/null +++ b/DeviceCommand/Base/S7Device.cs @@ -0,0 +1,169 @@ +using S7.Net; +using System; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; + +namespace DeviceCommand.Base +{ + public class S7Device : IS7Device + { + // 保持和你一致的连接参数命名属性 + public string IPAddress { get; private set; } = "127.0.0.1"; + public CpuType CpuType { get; private set; } = CpuType.S71200; + public short Rack { get; private set; } = 0; + public short Slot { get; private set; } = 1; + public int SendTimeout { get; private set; } = 3000; + public int ReceiveTimeout { get; private set; } = 3000; + + private Plc _plc; + public Plc PlcContext => _plc; + + // S7.Net 的 Plc.IsConnected 属性内部会通过 Socket 状态进行判断 + public bool IsConnected => _plc?.IsConnected ?? false; + + // 统一线程锁 + protected readonly SemaphoreSlim _commLock = new(1, 1); + + public S7Device() + { + // 初始化默认配置 + _plc = new Plc(CpuType, IPAddress, Rack, Slot); + } + + /// + /// 设备参数配置(符合你的命名风格) + /// + public void ConfigureDevice(string ipAddress, CpuType cpuType, short rack = 0, short slot = 1, int sendTimeout = 3000, int receiveTimeout = 3000) + { + IPAddress = ipAddress; + CpuType = cpuType; + Rack = rack; + Slot = slot; + SendTimeout = sendTimeout; + ReceiveTimeout = receiveTimeout; + } + + public virtual async Task ConnectAsync(CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + // 如果已经连接,检查当前的 IP 和 CPU 类型是否一致,一致则直接复用 + if (_plc != null && _plc.IsConnected) + { + if (_plc.IP == IPAddress && _plc.CPU == CpuType && _plc.Rack == Rack && _plc.Slot == Slot) + return true; + } + + // 修复:释放并彻底清空旧连接实例 + if (_plc != null) + { + _plc.Close(); + } + + // 重新实例化 Plc 对象并配置超时 + _plc = new Plc(CpuType, IPAddress, Rack, Slot) + { + ReadTimeout = ReceiveTimeout, + WriteTimeout = SendTimeout + }; + + // 部分版本 S7.Net 的 OpenAsync 本身不接受 CancellationToken,我们通过 WaitAsync 实现超时 + await _plc.OpenAsync().WaitAsync(TimeSpan.FromMilliseconds(SendTimeout), ct); + return _plc.IsConnected; + } + catch + { + return false; + } + finally + { + _commLock.Release(); + } + } + + public virtual void Close() + { + if (_plc != null && _plc.IsConnected) + _plc.Close(); + } + + public async Task WriteAsync(string address, object value, CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + if (!IsConnected) throw new InvalidOperationException("PLC未连接。"); + + await _plc.WriteAsync(address, value) + .WaitAsync(TimeSpan.FromMilliseconds(SendTimeout), ct); + } + finally + { + _commLock.Release(); + } + } + + public async Task ReadAsync(string address, CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + if (!IsConnected) throw new InvalidOperationException("PLC未连接。"); + + return await _plc.ReadAsync(address) + .WaitAsync(TimeSpan.FromMilliseconds(ReceiveTimeout), ct); + } + finally + { + _commLock.Release(); + } + } + + public async Task ReadAsync(string address, CancellationToken ct = default) + { + var result = await ReadAsync(address, ct); + return (T)result; + } + + public async Task ReadBytesAsync(DataType dataType, int db, int startByteAdr, int count, CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + if (!IsConnected) throw new InvalidOperationException("PLC未连接。"); + + return await _plc.ReadBytesAsync(dataType, db, startByteAdr, count) + .WaitAsync(TimeSpan.FromMilliseconds(ReceiveTimeout), ct); + } + finally + { + _commLock.Release(); + } + } + + public async Task WriteBytesAsync(DataType dataType, int db, int startByteAdr, byte[] value, CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + if (!IsConnected) throw new InvalidOperationException("PLC未连接。"); + + await _plc.WriteBytesAsync(dataType, db, startByteAdr, value) + .WaitAsync(TimeSpan.FromMilliseconds(SendTimeout), ct); + } + finally + { + _commLock.Release(); + } + } + + public void Dispose() + { + _plc?.Close(); + _commLock?.Dispose(); + } + } +} \ No newline at end of file diff --git a/DeviceCommand/Base/Serial_Port.cs b/DeviceCommand/Base/Serial_Port.cs new file mode 100644 index 0000000..9c0ed0f --- /dev/null +++ b/DeviceCommand/Base/Serial_Port.cs @@ -0,0 +1,156 @@ +using System; +using System.IO.Ports; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace DeviceCommand.Base +{ + public class Serial_Port : ISerialPort + { + public string PortName { get; set; } = "COM1"; + public int BaudRate { get; set; } = 9600; + public int DataBits { get; set; } = 8; + public StopBits StopBits { get; set; } = StopBits.One; + public Parity Parity { get; set; } = Parity.None; + public int ReadTimeout { get; set; } = 3000; + public int WriteTimeout { get; set; } = 3000; + + private SerialPort _serialPort; + public bool IsConnected => _serialPort?.IsOpen ?? false; + protected readonly SemaphoreSlim commLock = new(1, 1); + + public Serial_Port() + { + _serialPort = new SerialPort(); + } + + public void ConfigureDevice(string portName, int baudRate, int dataBits = 8, StopBits stopBits = StopBits.One, Parity parity = Parity.None, int readTimeout = 3000, int writeTimeout = 3000) + { + PortName = portName; + BaudRate = baudRate; + DataBits = dataBits; + StopBits = stopBits; + Parity = parity; + ReadTimeout = readTimeout; + WriteTimeout = writeTimeout; + } + + public virtual async Task ConnectAsync(CancellationToken ct = default) + { + await commLock.WaitAsync(ct); + try + { + if (_serialPort.IsOpen) _serialPort.Close(); + + _serialPort.PortName = PortName; + _serialPort.BaudRate = BaudRate; + _serialPort.DataBits = DataBits; + _serialPort.StopBits = StopBits; + _serialPort.Parity = Parity; + _serialPort.ReadTimeout = ReadTimeout; + _serialPort.WriteTimeout = WriteTimeout; + + _serialPort.Open(); + return true; + } + finally + { + commLock.Release(); + } + } + + public virtual void Close() + { + if (_serialPort.IsOpen) _serialPort.Close(); + } + + // 内部无锁发送方法,供原子组合操作调用 + private async Task LoglessSendAsync(string data, CancellationToken ct) + { + if (!_serialPort.IsOpen) throw new InvalidOperationException("串口未打开。"); + + byte[] bytes = Encoding.UTF8.GetBytes(data); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); + if (WriteTimeout > 0) cts.CancelAfter(WriteTimeout); + + await _serialPort.BaseStream.WriteAsync(bytes, 0, bytes.Length, cts.Token); + } + + public async Task SendAsync(string data, CancellationToken ct = default) + { + await commLock.WaitAsync(ct); + try + { + await LoglessSendAsync(data, ct); + } + finally + { + commLock.Release(); + } + } + + // 内部无锁读取方法,利用 BaseStream 挂起线程,高性能不吃 CPU + private async Task LoglessReadAsync(string delimiter, CancellationToken ct) + { + if (!_serialPort.IsOpen) throw new InvalidOperationException("串口未打开。"); + + delimiter ??= "\n"; + var sb = new StringBuilder(); + byte[] buffer = new byte[1024]; + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); + if (ReadTimeout > 0) cts.CancelAfter(ReadTimeout); + + while (!cts.Token.IsCancellationRequested) + { + // 核心优化:利用流异步挂起,替代原先的 BytesToRead 循环延时 + int bytesRead = await _serialPort.BaseStream.ReadAsync(buffer, 0, buffer.Length, cts.Token); + if (bytesRead == 0) continue; + + sb.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead)); + + int index = sb.ToString().IndexOf(delimiter, StringComparison.Ordinal); + if (index >= 0) + { + return sb.ToString(0, index).Trim(); + } + } + throw new TimeoutException("读取数据超时"); + } + + public async Task ReadAsync(string delimiter = "\n", CancellationToken ct = default) + { + await commLock.WaitAsync(ct); + try + { + return await LoglessReadAsync(delimiter, ct); + } + finally + { + commLock.Release(); + } + } + + // 核心优化:保证多线程环境下发送和等待回包是一个原子过程 + public async Task WriteReadAsync(string command, string delimiter = "\n", CancellationToken ct = default) + { + await commLock.WaitAsync(ct); + try + { + await LoglessSendAsync(command, ct); + return await LoglessReadAsync(delimiter, ct); + } + finally + { + commLock.Release(); + } + } + + public void Dispose() + { + _serialPort?.Dispose(); + commLock?.Dispose(); + } + } +} \ No newline at end of file diff --git a/DeviceCommand/Base/TCP.cs b/DeviceCommand/Base/TCP.cs new file mode 100644 index 0000000..28f5621 --- /dev/null +++ b/DeviceCommand/Base/TCP.cs @@ -0,0 +1,179 @@ +using System; +using System.IO; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace DeviceCommand.Base +{ + public class Tcp : ITcp + { + public string IPAddress { get; set; } = "127.0.0.1"; + public int Port { get; set; } = 502; + public int SendTimeout { get; set; } = 3000; + public int ReceiveTimeout { get; set; } = 3000; + + private TcpClient _tcpClient; + public bool IsConnected => _tcpClient?.Connected ?? false; + protected readonly SemaphoreSlim _commLock = new(1, 1); + + public Tcp() + { + _tcpClient = new TcpClient(); + } + + public void ConfigureDevice(string ipAddress, int port, int sendTimeout = 3000, int receiveTimeout = 3000) + { + IPAddress = ipAddress; + Port = port; + SendTimeout = sendTimeout; + ReceiveTimeout = receiveTimeout; + } + + public virtual async Task ConnectAsync(CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + if (_tcpClient.Connected) return true; + + // 修复:释放并彻底清空旧的连接实例,否则复用引发异常 + _tcpClient.Close(); + _tcpClient.Dispose(); + + _tcpClient = new TcpClient(); + await _tcpClient.ConnectAsync(IPAddress, Port, ct); + return true; + } + finally + { + _commLock.Release(); + } + } + + public virtual void Close() + { + if (_tcpClient.Connected) _tcpClient.Close(); + } + + private async Task LoglessSendAsync(byte[] buffer, CancellationToken ct) + { + if (!IsConnected) throw new InvalidOperationException("TCP未连接。"); + + NetworkStream stream = _tcpClient.GetStream(); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); + if (SendTimeout > 0) cts.CancelAfter(SendTimeout); + + await stream.WriteAsync(buffer, 0, buffer.Length, cts.Token); + } + + public async Task SendAsync(byte[] buffer, CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + await LoglessSendAsync(buffer, ct); + } + finally + { + _commLock.Release(); + } + } + + public async Task SendAsync(string str, CancellationToken ct = default) + { + await SendAsync(Encoding.UTF8.GetBytes(str), ct); + } + + public async Task ReadAsync(int length, CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + if (!IsConnected) throw new InvalidOperationException("TCP未连接。"); + + NetworkStream stream = _tcpClient.GetStream(); + byte[] buffer = new byte[length]; + int offset = 0; + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); + if (ReceiveTimeout > 0) cts.CancelAfter(ReceiveTimeout); + + while (offset < length) + { + int read = await stream.ReadAsync(buffer, offset, length - offset, cts.Token); + if (read == 0) break; + offset += read; + } + + return offset == 0 ? Array.Empty() : buffer[..offset]; + } + finally + { + _commLock.Release(); + } + } + + private async Task LoglessReadAsync(string delimiter, CancellationToken ct) + { + if (!IsConnected) throw new InvalidOperationException("TCP未连接。"); + + delimiter ??= "\n"; + var sb = new StringBuilder(); + byte[] buffer = new byte[1024]; + NetworkStream stream = _tcpClient.GetStream(); + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); + if (ReceiveTimeout > 0) cts.CancelAfter(ReceiveTimeout); + + while (!cts.Token.IsCancellationRequested) + { + int bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cts.Token); + if (bytesRead == 0) throw new IOException("远程主机已关闭连接"); + + sb.Append(Encoding.UTF8.GetString(buffer, 0, bytesRead)); + + int index = sb.ToString().IndexOf(delimiter, StringComparison.Ordinal); + if (index >= 0) + return sb.ToString(0, index).Trim(); + } + + throw new TimeoutException("读取数据超时"); + } + + public async Task ReadAsync(string delimiter = "\n", CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + return await LoglessReadAsync(delimiter, ct); + } + finally + { + _commLock.Release(); + } + } + + // 核心优化:确保发送与读取在同一组锁生命周期内 + public async Task WriteReadAsync(string command, string delimiter = "\n", CancellationToken ct = default) + { + await _commLock.WaitAsync(ct); + try + { + await LoglessSendAsync(Encoding.UTF8.GetBytes(command), ct); + return await LoglessReadAsync(delimiter, ct); + } + finally + { + _commLock.Release(); + } + } + + public void Dispose() + { + _tcpClient?.Dispose(); + _commLock?.Dispose(); + } + } +} \ No newline at end of file diff --git a/DeviceCommand/DeviceCommand.csproj b/DeviceCommand/DeviceCommand.csproj new file mode 100644 index 0000000..90ec535 --- /dev/null +++ b/DeviceCommand/DeviceCommand.csproj @@ -0,0 +1,24 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/LAEPS.sln b/IOT.sln similarity index 94% rename from LAEPS.sln rename to IOT.sln index f2f01c9..18ba7a8 100644 --- a/LAEPS.sln +++ b/IOT.sln @@ -13,8 +13,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Service", "Service\Service. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LAEPS", "LAEPS\LAEPS.csproj", "{5EC9A233-D154-4B77-6911-063269A883E9}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Module", "Module", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoginModule", "LoginModule\LoginModule.csproj", "{F79AC87E-7A5A-486F-BE6C-51E81CA569E4}" @@ -29,6 +27,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MainModule", "MainModule\Ma EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LOT", "LOT\LOT.csproj", "{01E01684-DDE8-4B00-9BFC-2C5CDB2A261F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeviceCommand", "DeviceCommand\DeviceCommand.csproj", "{2F035F70-5F1D-4C22-B4F0-1AEA0ED127A6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -55,10 +55,6 @@ Global {C769E6C6-55E9-40C3-A611-9EFAB101BE6A}.Debug|Any CPU.Build.0 = Debug|Any CPU {C769E6C6-55E9-40C3-A611-9EFAB101BE6A}.Release|Any CPU.ActiveCfg = Release|Any CPU {C769E6C6-55E9-40C3-A611-9EFAB101BE6A}.Release|Any CPU.Build.0 = Release|Any CPU - {5EC9A233-D154-4B77-6911-063269A883E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5EC9A233-D154-4B77-6911-063269A883E9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5EC9A233-D154-4B77-6911-063269A883E9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5EC9A233-D154-4B77-6911-063269A883E9}.Release|Any CPU.Build.0 = Release|Any CPU {F79AC87E-7A5A-486F-BE6C-51E81CA569E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F79AC87E-7A5A-486F-BE6C-51E81CA569E4}.Debug|Any CPU.Build.0 = Debug|Any CPU {F79AC87E-7A5A-486F-BE6C-51E81CA569E4}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -83,6 +79,10 @@ Global {01E01684-DDE8-4B00-9BFC-2C5CDB2A261F}.Debug|Any CPU.Build.0 = Debug|Any CPU {01E01684-DDE8-4B00-9BFC-2C5CDB2A261F}.Release|Any CPU.ActiveCfg = Release|Any CPU {01E01684-DDE8-4B00-9BFC-2C5CDB2A261F}.Release|Any CPU.Build.0 = Release|Any CPU + {2F035F70-5F1D-4C22-B4F0-1AEA0ED127A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F035F70-5F1D-4C22-B4F0-1AEA0ED127A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F035F70-5F1D-4C22-B4F0-1AEA0ED127A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F035F70-5F1D-4C22-B4F0-1AEA0ED127A6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/LOT/LOT.csproj b/LOT/LOT.csproj index c85fb8b..cd8ca8e 100644 --- a/LOT/LOT.csproj +++ b/LOT/LOT.csproj @@ -40,6 +40,7 @@ + diff --git a/Model/Model.csproj b/Model/Model.csproj index b17c3e0..caf2bb7 100644 --- a/Model/Model.csproj +++ b/Model/Model.csproj @@ -7,11 +7,13 @@ - + + + - + diff --git a/Model/Model/EnovaChannelReportData.cs b/Model/Model/EnovaChannelReportData.cs new file mode 100644 index 0000000..92962de --- /dev/null +++ b/Model/Model/EnovaChannelReportData.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; + +namespace Model.Model +{ + /// + /// Enova3 通道状态上报实体(单条通道数据) + /// + public class EnovaChannelReportData + { + public string laboratory { get; set; } = string.Empty; // 实验室名称 + public string manufacture { get; set; } = string.Empty; // 供应商名称 + public string taskId { get; set; } = string.Empty; // 任务ID + public string devCode { get; set; } = string.Empty; // 台架编码(注:表格里是devCode,示例里是deviceCode,建议互补或以表格为准) + public string deviceCode { get; set; } = string.Empty; // 兼容示例中的 deviceCode + public string chnCode { get; set; } = string.Empty; // 通道编码 + public string channelCode { get; set; } = string.Empty; // 兼容示例中的 channelCode + public string chnlP { get; set; } = string.Empty; // 通道IP + public string channelIP { get; set; } = string.Empty; // 兼容示例中的 channelIP + public string pclP { get; set; } = string.Empty; // 上位机IP + public string pcIP { get; set; } = string.Empty; // 兼容示例中的 pcIP + public string pcName { get; set; } = string.Empty; // 上位机名称 + public string barCode { get; set; } = string.Empty; // 样品条码 + public string projectName { get; set; } = string.Empty; // 项目名称 + public string stepId { get; set; } = string.Empty; // 工步行 + public string cycleDepth { get; set; } = string.Empty; // 循环层 + public string cycles { get; set; } = string.Empty; // 循环次数 + public string stepTime { get; set; } = string.Empty; // 工步时间 + public string totalTime { get; set; } = string.Empty; // 运行时间 + public string powerState { get; set; } = string.Empty; // 电源状态 + public string channelState { get; set; } = string.Empty; // 通道状态 + public string voltage { get; set; } = string.Empty; // 电压 + public string current { get; set; } = string.Empty; // 电流 + public string power { get; set; } = string.Empty; // 功率 + public string givenVoltage { get; set; } = string.Empty; // 给定电压 + public string givenCurrent { get; set; } = string.Empty; // 给定电流 + public string givenPower { get; set; } = string.Empty; // 给定功率 + public string cap { get; set; } = string.Empty; // 容量 + public string chgCap { get; set; } = string.Empty; // 充电容量 + public string dchgCap { get; set; } = string.Empty; // 放电容量 + public string capDiff { get; set; } = string.Empty; // 容量差 + public string energy { get; set; } = string.Empty; // 能量 + public string chgEnergy { get; set; } = string.Empty; // 充电能量 + public string dchgEnergy { get; set; } = string.Empty; // 放电能量 + public string energyDiff { get; set; } = string.Empty; // 能量差 + public string timeStamp { get; set; } = string.Empty; // 采集时间(如:2024-07-11 13:03:03) + public string timestamp { get; set; } = string.Empty; // 兼容小写格式 + + // 强类型:其他信号集合(支持动态 KV 对) + public Dictionary otherSignals { get; set; } = new(); + } + + /// + /// 客户平台统一返回接收格式 + /// + public class EnovaReportResponse + { + public bool Success { get; set; } + public string ErrorInfo { get; set; } = string.Empty; + } +} \ No newline at end of file