Compare commits

...

16 Commits

Author SHA1 Message Date
“hsc”
fa2f9f64c5 RTU添加 2026-06-12 15:29:18 +08:00
20492
1ff51cbc45 添加配置连接窗口已经相关S7与ModbusTCP连接功能 2026-06-12 08:38:43 +08:00
“hsc”
5d14afcb66 设备驱动修改 2026-06-10 10:53:10 +08:00
20492
b0a7742b8f 添加SDE710FH-A驱动 2026-06-10 08:56:05 +08:00
“hsc”
ec6825fc54 Merge branch 'master' of ssh://git.kaiyili-lab.com:2022/huangsucan/IOT 2026-06-09 16:37:16 +08:00
“hsc”
65661ef211 Merge branch 'master' of ssh://git.kaiyili-lab.com:2022/huangsucan/IOT 2026-06-09 16:37:09 +08:00
“hsc”
dd287f8b8b Merge branch 'master' of ssh://git.kaiyili-lab.com:2022/huangsucan/IOT 2026-06-09 16:33:38 +08:00
“hsc”
7f539a0316 设备添加 2026-06-09 16:33:32 +08:00
“hsc”
edcbc2ebc4 THC1100设备添加 2026-06-09 16:16:33 +08:00
“hsc”
048472579c Merge branch 'master' of ssh://git.kaiyili-lab.com:2022/huangsucan/IOT 2026-06-09 16:05:23 +08:00
“hsc”
3ccdff1bf0 推送合并 2026-06-09 16:05:00 +08:00
“hsc”
04ed15594e 推送合并 2026-06-09 16:01:21 +08:00
“hsc”
2cab299067 UI界面 2026-06-09 15:59:30 +08:00
“hsc”
a355373423 设备基础命令 2026-06-09 15:54:50 +08:00
“hsc”
c29519080a 框架再次优化 2026-06-09 15:39:18 +08:00
czj
60d3009670 框架优化 2026-06-09 15:33:11 +08:00
59 changed files with 3892 additions and 345 deletions

10
.gitignore vendored
View File

@@ -16,9 +16,13 @@
# Mono auto generated files # Mono auto generated files
mono_crash.* mono_crash.*
# Build results # Build results (核心修正区域)
[Dd]ebug/ ![Dd]ebug/
[Dd]ebugPublic/ ![Dd]ebugPublic/
[Dd]ebug/*
[Dd]ebugPublic/*
![Dd]ebug/[Mm]odules/
[Rr]elease/ [Rr]elease/
[Rr]eleases/ [Rr]eleases/
x64/ x64/

View File

@@ -0,0 +1,215 @@
using System;
using System.Collections.Generic;
using System.Linq;
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;
// ========= 静态注册表:用于 EnovaDataController 反向查找设备实例并分发数据 =========
private static readonly List<EnovaDataReporter> _instances = new List<EnovaDataReporter>();
private static readonly object _registryLock = new object();
/// <summary>
/// 当前已注册的所有 EnovaDataReporter 实例(线程安全快照)
/// </summary>
public static IReadOnlyList<EnovaDataReporter> Instances
{
get
{
lock (_registryLock)
{
return _instances.ToList();
}
}
}
/// <summary>
/// 设备编码:用于在多设备场景下按 deviceCode 过滤分发
/// 留空表示该实例接收所有上报数据
/// </summary>
public string DeviceCode { get; set; } = string.Empty;
// 显式实现/自动属性,方便外部随时更新配置
public string TargetUrl { get; set; } = "http://127.0.0.1:8080/api/channel/state";
public int TimeoutMilliseconds { get; set; } = 5000;
/// <summary>
/// 收到下位机 POST 上报数据时触发
/// </summary>
public event EventHandler<EnovaChannelDataReceivedEventArgs>? ChannelDataReceived;
public EnovaDataReporter(HttpClient httpClient)
{
_httpClient = httpClient ?? new HttpClient();
// 自动注册到静态实例表,便于 Controller 反向找到本实例
lock (_registryLock)
{
_instances.Add(this);
}
}
/// <summary>
/// 从静态注册表中注销当前实例(设备销毁/释放时调用)
/// </summary>
public virtual void Unregister()
{
lock (_registryLock)
{
_instances.Remove(this);
}
}
public async Task<ApiResponse> ReportChannelStateAsync(List<EnovaChannelData> dataList, CancellationToken ct = default)
{
if (dataList == null || dataList.Count == 0)
{
return new ApiResponse { Success = false, ErrorInfo = "上报数据集合为空" };
}
if (string.IsNullOrWhiteSpace(TargetUrl))
{
return new ApiResponse { 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<ApiResponse>(responseContent);
return result ?? new ApiResponse { Success = true }; // 防止对方返回空Body
}
else
{
return new ApiResponse
{
Success = false,
ErrorInfo = $"服务器响应错误代码: {(int)response.StatusCode} {response.ReasonPhrase}"
};
}
}
catch (Exception ex)
{
// 完美承接你上位机原有的异常日志记录器逻辑
// Logger.LoggerHelper.ErrorWithNotify($"Enova3 数据上传失败: {ex.Message}");
return new ApiResponse { Success = false, ErrorInfo = $"网络异常: {ex.Message}" };
}
}
/// <summary>
/// 处理 Controller 转发过来的下位机上报数据,并触发事件
/// </summary>
public virtual ApiResponse HandleIncomingChannelData(List<EnovaChannelData> dataList)
{
if (dataList == null || dataList.Count == 0)
{
return new ApiResponse { Success = false, ErrorInfo = "接收到的数据为空" };
}
try
{
// 触发事件,让派生类(如 CTS3或外部订阅者处理具体业务
ChannelDataReceived?.Invoke(this, new EnovaChannelDataReceivedEventArgs(dataList));
return new ApiResponse { Success = true, ErrorInfo = string.Empty };
}
catch (Exception ex)
{
return new ApiResponse { Success = false, ErrorInfo = $"处理上报数据时异常: {ex.Message}" };
}
}
/// <summary>
/// 由 Controller 调用:将下位机上报的数据广播到所有匹配的实例
/// 当数据中含 DeviceCode 时,按 DeviceCode 精确匹配;否则广播给所有实例
/// </summary>
public static ApiResponse Dispatch(List<EnovaChannelData> dataList)
{
if (dataList == null || dataList.Count == 0)
{
return new ApiResponse { Success = false, ErrorInfo = "接收到的数据为空" };
}
EnovaDataReporter[] snapshot;
lock (_registryLock)
{
snapshot = _instances.ToArray();
}
if (snapshot.Length == 0)
{
return new ApiResponse { Success = false, ErrorInfo = "无可用的设备实例接收数据" };
}
// 按 DeviceCode 分组分发:同一批数据可能来自多个 deviceCode
var groups = dataList
.GroupBy(d => d?.DeviceCode ?? string.Empty)
.ToList();
var errors = new List<string>();
int successCount = 0;
foreach (var group in groups)
{
string deviceCode = group.Key;
var items = group.ToList();
// 选取目标1) 设置了相同 DeviceCode 的实例2) 没设置 DeviceCode 的实例(通用接收者)
var targets = snapshot
.Where(r => string.Equals(r.DeviceCode, deviceCode, StringComparison.OrdinalIgnoreCase)
|| string.IsNullOrEmpty(r.DeviceCode))
.ToList();
if (targets.Count == 0)
{
errors.Add($"DeviceCode={deviceCode} 无匹配的设备实例");
continue;
}
foreach (var target in targets)
{
var resp = target.HandleIncomingChannelData(items);
if (resp != null && resp.Success)
{
successCount++;
}
else
{
errors.Add(resp?.ErrorInfo ?? "未知错误");
}
}
}
return new ApiResponse
{
Success = errors.Count == 0,
ErrorInfo = errors.Count == 0 ? string.Empty : string.Join("", errors)
};
}
}
}

View File

@@ -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<bool> ConnectAsync(CancellationToken ct = default);
public void Close();
}
}

View File

@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Model.Model;
namespace DeviceCommand.Base
{
/// <summary>
/// Enova3 通道数据接收事件参数
/// </summary>
public class EnovaChannelDataReceivedEventArgs : EventArgs
{
public List<EnovaChannelData> DataList { get; }
public DateTime ReceivedTime { get; }
public EnovaChannelDataReceivedEventArgs(List<EnovaChannelData> dataList)
{
DataList = dataList ?? new List<EnovaChannelData>();
ReceivedTime = DateTime.Now;
}
}
/// <summary>
/// Enova3 上位机数据上报 / 接收核心接口
/// 既支持上位机主动推送数据到客户平台,也支持接收下位机通过 HTTP POST 上报的数据
/// </summary>
public interface IEnovaDataReporter
{
/// <summary>
/// 客户平台接收数据的目标 HTTP URL用于主动推送
/// </summary>
string TargetUrl { get; set; }
/// <summary>
/// HTTP 请求超时时间(毫秒)
/// </summary>
int TimeoutMilliseconds { get; set; }
/// <summary>
/// 当 EnovaDataController 收到下位机 POST 上报的数据时触发
/// </summary>
event EventHandler<EnovaChannelDataReceivedEventArgs> ChannelDataReceived;
/// <summary>
/// 异步推送通道的实时状态数据到客户平台
/// </summary>
/// <param name="dataList">包含各通道状态的采集数据集合</param>
/// <param name="ct">取消令牌</param>
/// <returns>平台服务器的响应状态</returns>
Task<ApiResponse> ReportChannelStateAsync(List<EnovaChannelData> dataList, CancellationToken ct = default);
/// <summary>
/// 处理 EnovaDataController 转发过来的下位机上报数据
/// 由 Controller 在收到 HTTP POST 后调用,内部会触发 <see cref="ChannelDataReceived"/> 事件
/// </summary>
/// <param name="dataList">下位机上报的通道数据集合</param>
/// <returns>处理结果,将作为 HTTP 响应返回给下位机</returns>
ApiResponse HandleIncomingChannelData(List<EnovaChannelData> dataList);
}
}

View File

@@ -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<ushort[]> ReadHoldingRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default);
Task WriteSingleCoilAsync(byte slaveAddress, ushort coilAddress, bool value, CancellationToken ct = default);
Task<bool[]> ReadCoilsAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default);
Task<ushort[]> ReadInputRegistersAsync(byte slaveAddress, ushort startAddress, ushort numberOfPoints, CancellationToken ct = default);
}
}

View File

@@ -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<object> ReadAsync(string address, CancellationToken ct = default);
Task<T> ReadAsync<T>(string address, CancellationToken ct = default);
// 批量读取/写入原始字节(常用于大块数据交互)
Task<byte[]> 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);
}
}

View File

@@ -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<string> ReadAsync(string delimiter = "\n", CancellationToken ct = default);
Task<string> WriteReadAsync(string command, string delimiter = "\n", CancellationToken ct = default);
}
}

View File

@@ -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<byte[]> ReadAsync(int length, CancellationToken ct = default);
Task<string> ReadAsync(string delimiter = "\n", CancellationToken ct = default);
Task<string> WriteReadAsync(string command, string delimiter = "\n", CancellationToken ct = default);
}
}

View File

@@ -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<bool> 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<ushort[]> 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<bool[]> 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<ushort[]> 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();
}
}
}

View File

@@ -0,0 +1,186 @@
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<bool> ConnectAsync(CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 开始连接 - IP: {IPAddress}, 端口: {Port}");
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 超时设置 - 发送: {SendTimeout}ms, 接收: {ReceiveTimeout}ms");
if (_tcpClient.Connected)
{
var remoteEndPoint = (IPEndPoint)_tcpClient.Client.RemoteEndPoint!;
string currentIp = remoteEndPoint.Address.MapToIPv4().ToString();
int currentPort = remoteEndPoint.Port;
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 已有连接: {currentIp}:{currentPort}");
if (currentIp == IPAddress && currentPort == Port)
{
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 参数匹配,复用现有连接");
return true;
}
else
{
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 参数不匹配,需要重新连接");
}
}
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 关闭并释放旧连接");
_tcpClient.Close();
_tcpClient.Dispose();
_tcpClient = new TcpClient();
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 调用 ConnectAsync({IPAddress}, {Port})");
await _tcpClient.ConnectAsync(IPAddress, Port, ct);
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 创建ModbusMaster");
Modbus = new ModbusFactory().CreateMaster(_tcpClient);
bool isConnected = _tcpClient.Connected;
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 连接结果: {(isConnected ? "" : "")}");
return isConnected;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 连接异常: {ex.Message}");
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 异常类型: {ex.GetType().Name}");
System.Diagnostics.Debug.WriteLine($"[ModbusTcp] 异常堆栈: {ex.StackTrace}");
return false;
}
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<ushort[]> 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<bool[]> 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<ushort[]> 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();
}
}
}

View File

@@ -0,0 +1,175 @@
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;
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<bool> ConnectAsync(CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
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 = new Plc(CpuType, IPAddress, Rack, Slot)
{
ReadTimeout = ReceiveTimeout,
WriteTimeout = SendTimeout
};
await _plc.OpenAsync();
return _plc.IsConnected;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[S7Device] 连接异常: IP={IPAddress}, CPU={CpuType}, Error={ex.Message}");
System.Diagnostics.Debug.WriteLine($"[S7Device] 异常堆栈: {ex.StackTrace}");
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<object> 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<T> ReadAsync<T>(string address, CancellationToken ct = default)
{
var result = await ReadAsync(address, ct);
System.Diagnostics.Debug.WriteLine($"[S7Device.ReadAsync<T>] 地址={address}, 返回类型={result?.GetType().Name ?? "null"}, 值={result}");
try
{
if (result is IConvertible convertible)
{
return (T)Convert.ChangeType(convertible, typeof(T));
}
return (T)result;
}
catch (InvalidCastException ex)
{
System.Diagnostics.Debug.WriteLine($"[S7Device.ReadAsync<T>] 类型转换失败: 目标类型={typeof(T).Name}, 实际类型={result.GetType().Name}, 值={result}, 异常={ex.Message}");
throw;
}
}
public async Task<byte[]> 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();
}
}
}

View File

@@ -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<bool> 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<string> 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<string> ReadAsync(string delimiter = "\n", CancellationToken ct = default)
{
await commLock.WaitAsync(ct);
try
{
return await LoglessReadAsync(delimiter, ct);
}
finally
{
commLock.Release();
}
}
// 核心优化:保证多线程环境下发送和等待回包是一个原子过程
public async Task<string> 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();
}
}
}

179
DeviceCommand/Base/TCP.cs Normal file
View File

@@ -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<bool> 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<byte[]> 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<byte>() : buffer[..offset];
}
finally
{
_commLock.Release();
}
}
private async Task<string> 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<string> ReadAsync(string delimiter = "\n", CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
return await LoglessReadAsync(delimiter, ct);
}
finally
{
_commLock.Release();
}
}
// 核心优化:确保发送与读取在同一组锁生命周期内
public async Task<string> 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();
}
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NModbus" Version="3.0.83" />
<PackageReference Include="NModbus.Serial" Version="3.0.83" />
<PackageReference Include="S7netplus" Version="0.20.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\Model\Model.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,82 @@
using DeviceCommand.Base;
using Model.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
namespace DeviceCommand.Devices
{
/// <summary>
/// CTS3-6-300-8IS0 设备:通过 EnovaDataController 接收下位机 POST 上报的通道数据
/// </summary>
public class CTS3 : EnovaDataReporter
{
/// <summary>
/// 最近一次收到的通道数据快照key = ChannelCode
/// </summary>
public IReadOnlyDictionary<string, EnovaChannelData> LatestChannelData => _latestChannelData;
private readonly Dictionary<string, EnovaChannelData> _latestChannelData = new Dictionary<string, EnovaChannelData>();
private readonly object _dataLock = new object();
/// <summary>
/// 业务侧可订阅此事件以获得更友好的回调(仅本设备数据)
/// </summary>
public event EventHandler<EnovaChannelDataReceivedEventArgs>? OnDataUpdated;
public CTS3(HttpClient httpClient) : base(httpClient)
{
// 订阅基类事件:当 Controller 调用 HandleIncomingChannelData 时会触发
ChannelDataReceived += OnChannelDataReceivedInternal;
}
/// <summary>
/// 指定 DeviceCode 的便捷构造,便于多台 CTS3 共存时按 deviceCode 精确匹配
/// </summary>
public CTS3(HttpClient httpClient, string deviceCode) : this(httpClient)
{
DeviceCode = deviceCode ?? string.Empty;
}
private void OnChannelDataReceivedInternal(object? sender, EnovaChannelDataReceivedEventArgs e)
{
if (e?.DataList == null || e.DataList.Count == 0)
{
return;
}
// 1. 缓存最近一次的通道快照
lock (_dataLock)
{
foreach (var data in e.DataList)
{
if (data == null) continue;
string key = data.ChannelCode ?? string.Empty;
_latestChannelData[key] = data;
}
}
// 2. 业务异常拦截示例:通道错误状态
foreach (var data in e.DataList)
{
if (data == null) continue;
if (data.ChannelState == "错误" || data.ChannelState == "0x04")
{
// TODO: 触发本地报警逻辑
// Logger.LoggerHelper.WarnWithNotify($"通道 {data.ChannelCode} 故障");
}
}
// 3. 转发给业务订阅者
OnDataUpdated?.Invoke(this, e);
}
public override void Unregister()
{
ChannelDataReceived -= OnChannelDataReceivedInternal;
base.Unregister();
}
}
}

View File

@@ -0,0 +1,108 @@
using DeviceCommand.Base;
public class Others : ModbusTcp
{
private readonly byte _slaveId;
/// <summary>
/// 通道1PV
/// </summary>
private const ushort CH1_PV = 100;
/// <summary>
/// 通道2PV
/// </summary>
private const ushort CH2_PV = 102;
/// <summary>
/// 通道3PV
/// </summary>
private const ushort CH3_PV = 104;
/// <summary>
/// 通道4PV
/// </summary>
private const ushort CH4_PV = 106;
public Others(
byte slaveId,
string ip,
int port = 508,
int sendTimeoutMs = 3000,
int receiveTimeoutMs = 3000)
: base()
{
_slaveId = slaveId;
ConfigureDevice(ip, port, sendTimeoutMs, receiveTimeoutMs);
}
/// <summary>
/// 读取通道1PV
/// </summary>
public async Task<float> ReadCH1Async(CancellationToken ct = default)
{
return await ReadRealAsync(CH1_PV, ct);
}
/// <summary>
/// 读取通道2PV
/// </summary>
public async Task<float> ReadCH2Async(CancellationToken ct = default)
{
return await ReadRealAsync(CH2_PV, ct);
}
/// <summary>
/// 读取通道3PV
/// </summary>
public async Task<float> ReadCH3Async(CancellationToken ct = default)
{
return await ReadRealAsync(CH3_PV, ct);
}
/// <summary>
/// 读取通道4PV
/// </summary>
public async Task<float> ReadCH4Async(CancellationToken ct = default)
{
return await ReadRealAsync(CH4_PV, ct);
}
/// <summary>
/// 一次读取4个通道PV
/// </summary>
public async Task<(float ch1, float ch2, float ch3, float ch4)> ReadAllAsync(CancellationToken ct = default)
{
ushort[] raw = await ReadHoldingRegistersAsync(_slaveId, 100, 8, ct);
return
(
ToFloat(raw[0], raw[1]),
ToFloat(raw[2], raw[3]),
ToFloat(raw[4], raw[5]),
ToFloat(raw[6], raw[7])
);
}
private async Task<float> ReadRealAsync(ushort address, CancellationToken ct)
{
ushort[] raw = await ReadHoldingRegistersAsync(_slaveId, address, 2, ct);
return ToFloat(raw[0], raw[1]);
}
private static float ToFloat(ushort high, ushort low)
{
byte[] bytes =
{
(byte)(high >> 8),
(byte)high,
(byte)(low >> 8),
(byte)low
};
if (BitConverter.IsLittleEndian)
Array.Reverse(bytes);
return BitConverter.ToSingle(bytes, 0);
}
}

View File

@@ -0,0 +1,377 @@
using DeviceCommand.Base;
using S7.Net;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace DeviceCommand.Devices
{
/// <summary>
/// THC-1100-602A 恒温恒湿试验箱驱动 (S7通讯协议)
/// 基于西门子PLC S7-1200/S7-1500系列
/// </summary>
public class THC1100 : S7Device
{
#region THC设备默认连接参数
private const string DefaultIpAddress = "192.168.0.1";
private const CpuType DefaultCpuType = CpuType.S71200;
private const short DefaultRack = 0;
private const short DefaultSlot = 1;
private const int DefaultSendTimeout = 3000;
private const int DefaultReceiveTimeout = 3000;
#endregion
#region
// ========== 主控界面相关 (DB53) ==========
private const string RealTimeTemperatureSetPointAddress = "DB53.DBD280";
private const string RealTimeHumidityMeasuredValueAddress = "DB53.DBD1092";
private const string RealTimeTemperatureMeasuredValueAddress = "DB53.DBD1052";
private const string OperationModeAddress = "DB53.DBW2";
private const string TestTypeAddress = "DB53.DBW4";
private const string CurrentStepAddress = "DB53.DBW6";
private const string TotalStepsAddress = "DB53.DBW8";
private const string LoopCountAddress = "DB53.DBW10";
private const string RunTimeAddress = "DB53.DBD12";
private const string StepTimeAddress = "DB53.DBD16";
private const string SystemStatusAddress = "DB53.DBW20";
// ========== 超温保护相关 (DB53) ==========
private const string OverTempHighLimitAddress = "DB53.DBD284";
private const string OverTempLowLimitAddress = "DB53.DBD288";
private const string OverTempEnableAddress = "DB53.DBX292.0";
// ========== 报警相关 (DB53) ==========
private const string AlarmStatusAddress = "DB53.DBW293";
private const string AlarmCodeAddress = "DB53.DBW295";
private const string AlarmCountAddress = "DB53.DBW297";
// ========== 控制指令 (DB53) ==========
private const string ControlCommandAddress = "DB53.DBW300";
// ========== 程序步骤参数 (DB54) ==========
private const string StepTemperatureBaseAddress = "DB54.DBD";
private const string StepTimeBaseAddress = "DB54.DBD";
#endregion
#region
public enum OperationMode
{
Stop = 0,
Running = 1,
Paused = 2
}
#endregion
public THC1100() : base()
{
// 使用THC设备的默认参数配置
ConfigureDevice(
ipAddress: DefaultIpAddress,
cpuType: DefaultCpuType,
rack: DefaultRack,
slot: DefaultSlot,
sendTimeout: DefaultSendTimeout,
receiveTimeout: DefaultReceiveTimeout
);
}
/// <summary>
/// 重写连接方法,确保使用正确的默认参数连接
/// </summary>
public override async Task<bool> ConnectAsync(CancellationToken ct = default)
{
if (string.IsNullOrEmpty(IPAddress))
{
ConfigureDevice(
ipAddress: DefaultIpAddress,
cpuType: DefaultCpuType,
rack: DefaultRack,
slot: DefaultSlot,
sendTimeout: DefaultSendTimeout,
receiveTimeout: DefaultReceiveTimeout
);
}
System.Diagnostics.Debug.WriteLine($"[THC1100] 连接参数: IP={IPAddress}, CPU={CpuType}, Rack={Rack}, Slot={Slot}");
bool result = await base.ConnectAsync(ct);
if (!result)
{
System.Diagnostics.Debug.WriteLine($"[THC1100] 连接失败: IP={IPAddress}, CPU={CpuType}");
}
return result;
}
#region
public async Task<float> GetRealTimeTemperatureSetPointAsync(CancellationToken ct = default)
{
try
{
byte[] data = await ReadBytesAsync(DataType.DataBlock, 53, 280, 4, ct);
float value = ByteArrayToFloat(data);
System.Diagnostics.Debug.WriteLine($"[THC1100] 读取温度设定值(DB53.DBD280): {value}");
return value;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[THC1100] 读取温度设定值失败: {ex.Message}");
throw;
}
}
public async Task<float> GetRealTimeHumidityMeasuredValueAsync(CancellationToken ct = default)
{
try
{
byte[] data = await ReadBytesAsync(DataType.DataBlock, 53, 1092, 4, ct);
float value = ByteArrayToFloat(data);
System.Diagnostics.Debug.WriteLine($"[THC1100] 读取湿度测量值(DB53.DBD1092): {value}");
return value;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[THC1100] 读取湿度测量值失败: {ex.Message}");
throw;
}
}
public async Task<float> GetRealTimeTemperatureMeasuredValueAsync(CancellationToken ct = default)
{
try
{
byte[] data = await ReadBytesAsync(DataType.DataBlock, 53, 1052, 4, ct);
float value = ByteArrayToFloat(data);
System.Diagnostics.Debug.WriteLine($"[THC1100] 读取温度测量值(DB53.DBD1052): {value}");
return value;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[THC1100] 读取温度测量值失败: {ex.Message}");
throw;
}
}
private float ByteArrayToFloat(byte[] data)
{
if (data == null || data.Length < 4)
throw new ArgumentException("数据长度不足");
if (BitConverter.IsLittleEndian)
{
byte[] reversed = (byte[])data.Clone();
Array.Reverse(reversed);
return BitConverter.ToSingle(reversed, 0);
}
return BitConverter.ToSingle(data, 0);
}
#endregion
#region
public async Task<OperationMode> GetOperationModeAsync(CancellationToken ct = default)
{
var value = await ReadAsync<ushort>(OperationModeAddress, ct);
return (OperationMode)value;
}
public async Task<int> GetTestTypeAsync(CancellationToken ct = default)
{
return await ReadAsync<ushort>(TestTypeAddress, ct);
}
public async Task SetTestTypeAsync(int testType, CancellationToken ct = default)
{
if (testType < 0 || testType > 65535)
throw new ArgumentOutOfRangeException(nameof(testType), "试验类型必须在0-65535范围内");
await WriteAsync(TestTypeAddress, (ushort)testType, ct);
}
public async Task<int> GetCurrentStepAsync(CancellationToken ct = default)
{
return await ReadAsync<ushort>(CurrentStepAddress, ct);
}
public async Task<int> GetTotalStepsAsync(CancellationToken ct = default)
{
return await ReadAsync<ushort>(TotalStepsAddress, ct);
}
public async Task SetTotalStepsAsync(int steps, CancellationToken ct = default)
{
if (steps < 1 || steps > 999)
throw new ArgumentOutOfRangeException(nameof(steps), "步数必须在1-999之间");
await WriteAsync(TotalStepsAddress, (ushort)steps, ct);
}
public async Task<int> GetLoopCountAsync(CancellationToken ct = default)
{
return await ReadAsync<ushort>(LoopCountAddress, ct);
}
public async Task SetLoopCountAsync(int count, CancellationToken ct = default)
{
if (count < 0 || count > 999)
throw new ArgumentOutOfRangeException(nameof(count), "循环次数必须在0-999之间");
await WriteAsync(LoopCountAddress, (ushort)count, ct);
}
public async Task<float> GetRunTimeAsync(CancellationToken ct = default)
{
return await ReadAsync<float>(RunTimeAddress, ct);
}
public async Task<float> GetStepTimeAsync(CancellationToken ct = default)
{
return await ReadAsync<float>(StepTimeAddress, ct);
}
public async Task<int> GetSystemStatusAsync(CancellationToken ct = default)
{
return await ReadAsync<ushort>(SystemStatusAddress, ct);
}
#endregion
#region
public async Task<float> GetOverTempHighLimitAsync(CancellationToken ct = default)
{
return await ReadAsync<float>(OverTempHighLimitAddress, ct);
}
public async Task SetOverTempHighLimitAsync(float temperature, CancellationToken ct = default)
{
await WriteAsync(OverTempHighLimitAddress, temperature, ct);
}
public async Task<float> GetOverTempLowLimitAsync(CancellationToken ct = default)
{
return await ReadAsync<float>(OverTempLowLimitAddress, ct);
}
public async Task SetOverTempLowLimitAsync(float temperature, CancellationToken ct = default)
{
await WriteAsync(OverTempLowLimitAddress, temperature, ct);
}
public async Task<bool> GetOverTempEnableAsync(CancellationToken ct = default)
{
return await ReadAsync<bool>(OverTempEnableAddress, ct);
}
public async Task SetOverTempEnableAsync(bool enable, CancellationToken ct = default)
{
await WriteAsync(OverTempEnableAddress, enable, ct);
}
#endregion
#region
public async Task<int> GetAlarmStatusAsync(CancellationToken ct = default)
{
return await ReadAsync<ushort>(AlarmStatusAddress, ct);
}
public async Task<int> GetAlarmCodeAsync(CancellationToken ct = default)
{
return await ReadAsync<ushort>(AlarmCodeAddress, ct);
}
public async Task<int> GetAlarmCountAsync(CancellationToken ct = default)
{
return await ReadAsync<ushort>(AlarmCountAddress, ct);
}
public async Task ClearAlarmAsync(CancellationToken ct = default)
{
await WriteAsync(ControlCommandAddress, (ushort)0x08, ct);
await Task.Delay(100, ct);
await WriteAsync(ControlCommandAddress, (ushort)0x00, ct);
}
#endregion
#region
public async Task StartDeviceAsync(CancellationToken ct = default)
{
await WriteAsync(ControlCommandAddress, (ushort)0x01, ct);
await Task.Delay(100, ct);
await WriteAsync(ControlCommandAddress, (ushort)0x00, ct);
}
public async Task StopDeviceAsync(CancellationToken ct = default)
{
await WriteAsync(ControlCommandAddress, (ushort)0x02, ct);
await Task.Delay(100, ct);
await WriteAsync(ControlCommandAddress, (ushort)0x00, ct);
}
public async Task ResetDeviceAsync(CancellationToken ct = default)
{
await WriteAsync(ControlCommandAddress, (ushort)0x04, ct);
await Task.Delay(100, ct);
await WriteAsync(ControlCommandAddress, (ushort)0x00, ct);
}
#endregion
#region
public async Task SetStepTemperatureAsync(int step, float temperature, CancellationToken ct = default)
{
if (step < 1)
throw new ArgumentOutOfRangeException(nameof(step), "步骤编号必须大于0");
string address = $"{StepTemperatureBaseAddress}{(step - 1) * 12}";
await WriteAsync(address, temperature, ct);
}
public async Task<float> GetStepTemperatureAsync(int step, CancellationToken ct = default)
{
if (step < 1)
throw new ArgumentOutOfRangeException(nameof(step), "步骤编号必须大于0");
string address = $"{StepTemperatureBaseAddress}{(step - 1) * 12}";
return await ReadAsync<float>(address, ct);
}
public async Task SetStepTimeAsync(int step, float duration, CancellationToken ct = default)
{
if (step < 1)
throw new ArgumentOutOfRangeException(nameof(step), "步骤编号必须大于0");
if (duration < 0)
throw new ArgumentOutOfRangeException(nameof(duration), "时间不能为负数");
string address = $"{StepTimeBaseAddress}{(step - 1) * 12 + 8}";
await WriteAsync(address, duration, ct);
}
public async Task<float> GetStepTimeAsync(int step, CancellationToken ct = default)
{
if (step < 1)
throw new ArgumentOutOfRangeException(nameof(step), "步骤编号必须大于0");
string address = $"{StepTimeBaseAddress}{(step - 1) * 12 + 8}";
return await ReadAsync<float>(address, ct);
}
#endregion
#region
public async Task<(float TemperatureSetPoint, float TemperatureMeasured, float HumidityMeasured)> GetThcDataPackAsync(CancellationToken ct = default)
{
var tempSet = await GetRealTimeTemperatureSetPointAsync(ct);
var tempMeas = await GetRealTimeTemperatureMeasuredValueAsync(ct);
var humidMeas = await GetRealTimeHumidityMeasuredValueAsync(ct);
return (tempSet, tempMeas, humidMeas);
}
public async Task<(OperationMode Mode, int CurrentStep, int TotalSteps, int LoopCount, float RunTime)> GetRunStatusPackAsync(CancellationToken ct = default)
{
var mode = await GetOperationModeAsync(ct);
var currentStep = await GetCurrentStepAsync(ct);
var totalSteps = await GetTotalStepsAsync(ct);
var loopCount = await GetLoopCountAsync(ct);
var runTime = await GetRunTimeAsync(ct);
return (mode, currentStep, totalSteps, loopCount, runTime);
}
#endregion
}
}

View File

@@ -0,0 +1,137 @@
using DeviceCommand.Base;
using System;
using System.IO.Ports;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
namespace DeviceCommand.Devices
{
/// <summary>
/// UMC1000 温湿度控制器(RS485)
/// </summary>
public class UMC1000Rtu : ModbusRtu
{
private readonly byte _slaveId;
/// <summary>
/// 温度测量值 REAL
/// </summary>
private const ushort TemperatureAddress = 150;
/// <summary>
/// 湿度测量值 REAL
/// </summary>
private const ushort HumidityAddress = 152;
public UMC1000Rtu(
byte slaveId,
string portName,
int baudRate = 9600,
int dataBits = 8,
StopBits stopBits = StopBits.One,
Parity parity = Parity.None,
int readTimeout = 3000,
int writeTimeout = 3000)
{
_slaveId = slaveId;
ConfigureDevice(
portName,
baudRate,
dataBits,
stopBits,
parity,
readTimeout,
writeTimeout);
}
/// <summary>
/// 读取温度
/// </summary>
public async Task<float> ReadTemperatureAsync(
CancellationToken ct = default)
{
var a = await ReadFloatAsync(
TemperatureAddress,
ct);
return a;
}
/// <summary>
/// 读取湿度
/// </summary>
public async Task<float> ReadHumidityAsync(
CancellationToken ct = default)
{
return await ReadFloatAsync(
HumidityAddress,
ct);
}
/// <summary>
/// 一次读取温湿度(减少通讯次数,提升轮询效率)
/// </summary>
public async Task<(float Temperature, float Humidity)> ReadAllAsync(
CancellationToken ct = default)
{
ushort[] regs = await ReadHoldingRegistersAsync(
_slaveId,
TemperatureAddress,
4,
ct);
return
(
ToFloat(regs[0], regs[1]),
ToFloat(regs[2], regs[3])
);
}
private async Task<float> ReadFloatAsync(
ushort address,
CancellationToken ct)
{
ushort[] regs = await ReadHoldingRegistersAsync(
_slaveId,
address,
2,
ct);
return ToFloat(regs[0], regs[1]);
}
/// <summary>
/// 两个寄存器完美转换为 Float 数值(已解决 00 64 改完解析变 0 的致命问题)
/// </summary>
private static float ToFloat(ushort reg1, ushort reg2)
{
// 核心诊断:从回包 01 03 08 [00 64 00 00] 来看0x0064 正好是十进制 100。
// 这种情况在工业仪表中有 2 种常见可能,已为你做好了自适应处理:
// ==========================================
// 可能性【一】:下位机名义上叫 REAL实际上是 32位长整型Int32 / CD AB 字节序)
// ==========================================
int intValue = (reg2 << 16) | reg1;
if (intValue == 100 || intValue > 0 && intValue < 1500)
{
// 如果仪表传 100 代表 10.0℃,可以在这里除以 10.0f
return (float)intValue;
}
// ==========================================
// 可能性【二】:下位机确实是标准 IEEE 754 浮点数,但受高低字错位影响
// ==========================================
Span<byte> bytes = stackalloc byte[4];
// 采用安全的绝对字节映射,不再依赖会引发未知异常的 Array.Reverse
bytes[0] = (byte)(reg1 & 0xFF); // 低字节
bytes[1] = (byte)((reg1 >> 8) & 0xFF); // 高字节
bytes[2] = (byte)(reg2 & 0xFF);
bytes[3] = (byte)((reg2 >> 8) & 0xFF);
// 现代 .NET 高性能内存强转(等同于 C++ 的 reinterpret_cast<float*>
return MemoryMarshal.Read<float>(bytes);
}
}
}

View File

@@ -0,0 +1,95 @@
using DeviceCommand.Base;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace DeviceCommand.Devices
{
public class UMC1300 : ModbusTcp
{
// 从站地址(设备 ID
private readonly byte _slaveId;
// 寄存器地址常量(根据设备手册定义)
private const ushort ADDR_TEMP_PV = 0x0000; // 温度测量值
private const ushort ADDR_HUMID_PV = 0x0001; // 湿度测量值
private const ushort ADDR_TEMP_MV = 0x0002; // 温度输出值
private const ushort ADDR_HUMID_MV = 0x0003; // 湿度输出值
// 转换系数:寄存器原始值 × SCALE = 工程值
private const float SCALE = 0.1f;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="slaveId">Modbus 从站地址1~247</param>
/// <param name="ip">设备 IP 地址</param>
/// <param name="port">端口,默认 502</param>
/// <param name="sendTimeoutMs">发送超时(毫秒)</param>
/// <param name="receiveTimeoutMs">接收超时(毫秒)</param>
public UMC1300(byte slaveId, string ip, int port = 502,
int sendTimeoutMs = 3000, int receiveTimeoutMs = 3000)
: base()
{
_slaveId = slaveId;
ConfigureDevice(ip, port, sendTimeoutMs, receiveTimeoutMs);
}
public override async Task<bool> ConnectAsync(CancellationToken ct = default)
{
if (IsConnected)
{
return true;
}
return await base.ConnectAsync(ct);
}
// ==================== 读取单个值 ====================
/// <summary>读取温度 PV工程值</summary>
public async Task<float> ReadTemperaturePVAsync(CancellationToken ct = default)
{
ushort[] raw = await ReadInputRegistersAsync(_slaveId, ADDR_TEMP_PV, 1, ct);
return raw[0] * SCALE;
}
/// <summary>读取湿度 PV工程值</summary>
public async Task<float> ReadHumidityPVAsync(CancellationToken ct = default)
{
ushort[] raw = await ReadInputRegistersAsync(_slaveId, ADDR_HUMID_PV, 1, ct);
return raw[0] * SCALE;
}
/// <summary>读取温度 MV工程值</summary>
public async Task<float> ReadTemperatureMVAsync(CancellationToken ct = default)
{
ushort[] raw = await ReadHoldingRegistersAsync(_slaveId, ADDR_TEMP_MV, 1, ct);
return raw[0] * SCALE;
}
/// <summary>读取湿度 MV工程值</summary>
public async Task<float> ReadHumidityMVAsync(CancellationToken ct = default)
{
ushort[] raw = await ReadHoldingRegistersAsync(_slaveId, ADDR_HUMID_MV, 1, ct);
return raw[0] * SCALE;
}
// ==================== 批量读取 ====================
/// <summary>
/// 一次性读取温度 PV、湿度 PV、温度 MV、湿度 MV效率更高
/// </summary>
/// <returns>(tempPV, humidPV, tempMV, humidMV)</returns>
public async Task<(float tempPV, float humidPV, float tempMV, float humidMV)> ReadAllAsync(CancellationToken ct = default)
{
// 从 0x0000 开始连续读取 4 个保持寄存器
ushort[] raw = await ReadHoldingRegistersAsync(_slaveId, ADDR_TEMP_PV, 4, ct);
return (
raw[0] * SCALE, // 温度 PV
raw[1] * SCALE, // 湿度 PV
raw[2] * SCALE, // 温度 MV
raw[3] * SCALE // 湿度 MV
);
}
}
}

View File

@@ -13,8 +13,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Service", "Service\Service.
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}"
EndProject 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}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Module", "Module", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoginModule", "LoginModule\LoginModule.csproj", "{F79AC87E-7A5A-486F-BE6C-51E81CA569E4}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoginModule", "LoginModule\LoginModule.csproj", "{F79AC87E-7A5A-486F-BE6C-51E81CA569E4}"
@@ -27,6 +25,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UpdateInfoModule", "UpdateI
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MainModule", "MainModule\MainModule.csproj", "{715852A3-D2DE-4C2E-AEF2-2BC0ADBEAC0A}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MainModule", "MainModule\MainModule.csproj", "{715852A3-D2DE-4C2E-AEF2-2BC0ADBEAC0A}"
EndProject 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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebAPI", "WebAPI\WebAPI.csproj", "{D12C03C6-91F1-4792-9787-1EBD98E349EF}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -53,10 +57,6 @@ Global
{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}.Debug|Any CPU.Build.0 = Debug|Any CPU {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.ActiveCfg = Release|Any CPU
{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
{F79AC87E-7A5A-486F-BE6C-51E81CA569E4}.Debug|Any CPU.Build.0 = 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 {F79AC87E-7A5A-486F-BE6C-51E81CA569E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -77,6 +77,18 @@ Global
{715852A3-D2DE-4C2E-AEF2-2BC0ADBEAC0A}.Debug|Any CPU.Build.0 = Debug|Any CPU {715852A3-D2DE-4C2E-AEF2-2BC0ADBEAC0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{715852A3-D2DE-4C2E-AEF2-2BC0ADBEAC0A}.Release|Any CPU.ActiveCfg = Release|Any CPU {715852A3-D2DE-4C2E-AEF2-2BC0ADBEAC0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{715852A3-D2DE-4C2E-AEF2-2BC0ADBEAC0A}.Release|Any CPU.Build.0 = Release|Any CPU {715852A3-D2DE-4C2E-AEF2-2BC0ADBEAC0A}.Release|Any CPU.Build.0 = Release|Any CPU
{01E01684-DDE8-4B00-9BFC-2C5CDB2A261F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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
{D12C03C6-91F1-4792-9787-1EBD98E349EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D12C03C6-91F1-4792-9787-1EBD98E349EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D12C03C6-91F1-4792-9787-1EBD98E349EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D12C03C6-91F1-4792-9787-1EBD98E349EF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@@ -1,7 +1,7 @@
<prism:PrismApplication x:Class="LAEPS.App" <prism:PrismApplication x:Class="LOT.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:LAEPS" xmlns:local="clr-namespace:LOT"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:prism="http://prismlibrary.com/"> xmlns:prism="http://prismlibrary.com/">
<Application.Resources> <Application.Resources>

View File

@@ -1,10 +1,10 @@
using Castle.DynamicProxy; using Castle.DynamicProxy;
using Common; using Common;
using LAEPS.ViewModels; using LOT.ViewModels;
using LAEPS.ViewModels.Dialogs; using LOT.ViewModels.Dialogs;
using LAEPS.Views; using LOT.Views;
using LAEPS.Views; using LOT.Views;
using LAEPS.Views.Dialogs; using LOT.Views.Dialogs;
using Logger; using Logger;
using Notifications.Wpf.Core; using Notifications.Wpf.Core;
using ORM; using ORM;
@@ -17,7 +17,7 @@ using System.Windows;
using UIShare.PubEvent; using UIShare.PubEvent;
using static System.Runtime.InteropServices.JavaScript.JSType; using static System.Runtime.InteropServices.JavaScript.JSType;
namespace LAEPS namespace LOT
{ {
/// <summary> /// <summary>
/// Interaction logic for App.xaml /// Interaction logic for App.xaml
@@ -54,7 +54,7 @@ namespace LAEPS
{ {
//初始化数据库 //初始化数据库
//DatabaseConfig.SetTenant(10001); //DatabaseConfig.SetTenant(10001);
//DatabaseConfig.InitMySql("127.0.0.1",3306,"LAEPS","root","123456"); //DatabaseConfig.InitMySql("127.0.0.1",3306,"LOT","root","123456");
//DatabaseConfig.CreateDatabaseAndCheckConnection(createDatabase: true, checkConnection: true); //DatabaseConfig.CreateDatabaseAndCheckConnection(createDatabase: true, checkConnection: true);
//SqlSugarContext.InitDatabase(); //SqlSugarContext.InitDatabase();
//显示登录窗口 //显示登录窗口
@@ -71,8 +71,6 @@ namespace LAEPS
// 注册通知管理器 // 注册通知管理器
INotificationManager NotificationManager = new NotificationManager(); INotificationManager NotificationManager = new NotificationManager();
containerRegistry.RegisterInstance<INotificationManager>(NotificationManager); containerRegistry.RegisterInstance<INotificationManager>(NotificationManager);
// 注册服务
containerRegistry.RegisterSingleton<IPostService, PostService>();
} }
//指定模块加载方式(需要手动将模块生成的dll放入Modules文件夹中) //指定模块加载方式(需要手动将模块生成的dll放入Modules文件夹中)
protected override IModuleCatalog CreateModuleCatalog() protected override IModuleCatalog CreateModuleCatalog()

View File

@@ -40,6 +40,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" /> <ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\DeviceCommand\DeviceCommand.csproj" />
<ProjectReference Include="..\Logger\Logger.csproj" /> <ProjectReference Include="..\Logger\Logger.csproj" />
<ProjectReference Include="..\LoginModule\LoginModule.csproj" /> <ProjectReference Include="..\LoginModule\LoginModule.csproj" />
<ProjectReference Include="..\MainModule\MainModule.csproj" /> <ProjectReference Include="..\MainModule\MainModule.csproj" />

View File

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -2,7 +2,7 @@
using UIShare.ViewModelBase; using UIShare.ViewModelBase;
using System.Windows.Input; using System.Windows.Input;
namespace LAEPS.ViewModels.Dialogs namespace LOT.ViewModels.Dialogs
{ {
public class MessageBoxViewModel : DialogViewModelBase public class MessageBoxViewModel : DialogViewModelBase
{ {

View File

@@ -1,19 +1,9 @@
using LAEPS.Views; using Notifications.Wpf.Core;
using MaterialDesignThemes.Wpf;
using Notifications.Wpf.Core;
using Prism.Events;
using Prism.Ioc;
using Prism.Modularity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using UIShare.PubEvent; using UIShare.PubEvent;
namespace LAEPS.ViewModels namespace LOT.ViewModels
{ {
public class ShellViewModel : BindableBase public class ShellViewModel : BindableBase
{ {

View File

@@ -1,9 +1,9 @@
<UserControl x:Class="LAEPS.Views.Dialogs.MessageBoxView" <UserControl x:Class="LOT.Views.Dialogs.MessageBoxView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:LAEPS.Views.Dialogs" xmlns:local="clr-namespace:LOT.Views.Dialogs"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors" xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
mc:Ignorable="d" mc:Ignorable="d"
xmlns:prism="http://prismlibrary.com/" xmlns:prism="http://prismlibrary.com/"

View File

@@ -13,7 +13,7 @@ using System.Windows.Media.Imaging;
using System.Windows.Navigation; using System.Windows.Navigation;
using System.Windows.Shapes; using System.Windows.Shapes;
namespace LAEPS.Views.Dialogs namespace LOT.Views.Dialogs
{ {
/// <summary> /// <summary>
/// MessageBoxView.xaml 的交互逻辑 /// MessageBoxView.xaml 的交互逻辑

View File

@@ -1,16 +1,16 @@
<mah:MetroWindow xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls" <mah:MetroWindow xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:helpers="clr-namespace:UIShare.Helpers;assembly=UIShare" xmlns:helpers="clr-namespace:UIShare.Helpers;assembly=UIShare"
x:Class="LAEPS.Views.LoginModuleView" x:Class="LOT.Views.LoginModuleView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors" xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:prism="http://prismlibrary.com/" xmlns:prism="http://prismlibrary.com/"
xmlns:local="clr-namespace:LAEPS.Views" xmlns:local="clr-namespace:LOT.Views"
mc:Ignorable="d" mc:Ignorable="d"
Title="学习交流平台" Title="LOT"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
Height="315" Height="315"
Width="420" Width="420"

View File

@@ -18,7 +18,7 @@ using System.Windows.Media.Imaging;
using System.Windows.Shapes; using System.Windows.Shapes;
using Path = System.IO.Path; using Path = System.IO.Path;
namespace LAEPS.Views namespace LOT.Views
{ {
/// <summary> /// <summary>
/// Login.xaml 的交互逻辑 /// Login.xaml 的交互逻辑

View File

@@ -1,4 +1,4 @@
<Window x:Class="LAEPS.Views.ShellView" <Window x:Class="LOT.Views.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

View File

@@ -14,7 +14,7 @@ using System.Windows.Media;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using System.Windows.Shapes; using System.Windows.Shapes;
namespace LAEPS.Views namespace LOT.Views
{ {
/// <summary> /// <summary>
/// ShellView.xaml 的交互逻辑 /// ShellView.xaml 的交互逻辑

View File

@@ -0,0 +1,35 @@
using Prism.Events;
using S7.Net;
using System.IO.Ports;
namespace MainModule.Events
{
public enum ProtocolType
{
S7,
ModbusTCP,
ModbusRTU,
TCP,
HTTP
}
public class ConnectionConfigEvent : PubSubEvent<ConnectionConfigEvent.ConnectionConfigData>
{
public class ConnectionConfigData
{
public ProtocolType Protocol { get; set; }
public string IpAddress { get; set; } = string.Empty;
public int Port { get; set; }
public string SerialPort { get; set; } = string.Empty;
public int BaudRate { get; set; }
public Parity Parity { get; set; }
public int DataBits { get; set; }
public StopBits StopBits { get; set; }
public CpuType CpuType { get; set; }
public short Rack { get; set; }
public short Slot { get; set; }
public int SendTimeout { get; set; }
public int ReceiveTimeout { get; set; }
}
}
}

View File

@@ -1,4 +1,6 @@
using MainModule.ViewModels;
using MainModule.Views; using MainModule.Views;
using Prism.Ioc;
using System.Reflection; using System.Reflection;
namespace MainModule namespace MainModule

View File

@@ -8,6 +8,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\DeviceCommand\DeviceCommand.csproj" />
<ProjectReference Include="..\Model\Model.csproj" /> <ProjectReference Include="..\Model\Model.csproj" />
<ProjectReference Include="..\Service\Service.csproj" /> <ProjectReference Include="..\Service\Service.csproj" />
<ProjectReference Include="..\UIShare\UIShare.csproj" /> <ProjectReference Include="..\UIShare\UIShare.csproj" />

View File

@@ -0,0 +1,237 @@
using MainModule.Events;
using Prism.Commands;
using Prism.Mvvm;
using S7.Net;
using System.Collections.Generic;
using System.IO.Ports;
using System.Windows;
namespace MainModule.ViewModels
{
public class ConnectionConfigViewModel : BindableBase
{
#region
private Events.ProtocolType _selectedProtocol = Events.ProtocolType.S7;
private string _ipAddress = "127.0.0.1";
private int _port = 502;
private string _serialPort = "COM1";
private int _baudRate = 9600;
private Parity _parity = Parity.None;
private int _dataBits = 8;
private StopBits _stopBits = StopBits.One;
private CpuType _cpuType = CpuType.S71200;
private short _rack = 0;
private short _slot = 1;
private int _sendTimeout = 3000;
private int _receiveTimeout = 5000;
#endregion
#region
public Events.ProtocolType SelectedProtocol
{
get => _selectedProtocol;
set
{
if (SetProperty(ref _selectedProtocol, value))
{
RaisePropertyChanged(nameof(IsS7Protocol));
RaisePropertyChanged(nameof(IsModbusTCPProtocol));
RaisePropertyChanged(nameof(IsModbusRTUProtocol));
RaisePropertyChanged(nameof(IsTCPProtocol));
RaisePropertyChanged(nameof(IsHTTPProtocol));
}
}
}
public string IpAddress
{
get => _ipAddress;
set => SetProperty(ref _ipAddress, value);
}
public int Port
{
get => _port;
set => SetProperty(ref _port, value);
}
public string SerialPort
{
get => _serialPort;
set => SetProperty(ref _serialPort, value);
}
public int BaudRate
{
get => _baudRate;
set => SetProperty(ref _baudRate, value);
}
public Parity Parity
{
get => _parity;
set => SetProperty(ref _parity, value);
}
public int DataBits
{
get => _dataBits;
set => SetProperty(ref _dataBits, value);
}
public StopBits StopBits
{
get => _stopBits;
set => SetProperty(ref _stopBits, value);
}
public CpuType CpuType
{
get => _cpuType;
set => SetProperty(ref _cpuType, value);
}
public short Rack
{
get => _rack;
set => SetProperty(ref _rack, value);
}
public short Slot
{
get => _slot;
set => SetProperty(ref _slot, value);
}
public int SendTimeout
{
get => _sendTimeout;
set => SetProperty(ref _sendTimeout, value);
}
public int ReceiveTimeout
{
get => _receiveTimeout;
set => SetProperty(ref _receiveTimeout, value);
}
#endregion
#region
public bool IsS7Protocol => SelectedProtocol == Events.ProtocolType.S7;
public bool IsModbusTCPProtocol => SelectedProtocol == Events.ProtocolType.ModbusTCP;
public bool IsModbusRTUProtocol => SelectedProtocol == Events.ProtocolType.ModbusRTU;
public bool IsTCPProtocol => SelectedProtocol == Events.ProtocolType.TCP;
public bool IsHTTPProtocol => SelectedProtocol == Events.ProtocolType.HTTP;
#endregion
#region
public List<CpuType> CpuTypes { get; } = new List<CpuType>
{
CpuType.S71200,
CpuType.S71500,
CpuType.S7200,
CpuType.S7300,
CpuType.S7400,
CpuType.S7200Smart
};
public List<int> BaudRates { get; } = new List<int>
{
9600,
19200,
38400,
57600,
115200
};
public List<int> DataBitsList { get; } = new List<int> { 7, 8 };
public List<Parity> ParityList { get; } = new List<Parity>
{
Parity.None,
Parity.Odd,
Parity.Even,
Parity.Mark,
Parity.Space
};
public List<StopBits> StopBitsList { get; } = new List<StopBits>
{
StopBits.None,
StopBits.One,
StopBits.OnePointFive,
StopBits.Two
};
public List<string> SerialPorts { get; } = new List<string>
{
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8"
};
#endregion
#region
public DelegateCommand ConfirmCommand { get; }
public DelegateCommand CancelCommand { get; }
public DelegateCommand SelectS7Command { get; }
public DelegateCommand SelectModbusTCPCommand { get; }
public DelegateCommand SelectModbusRTUCommand { get; }
public DelegateCommand SelectTCPCommand { get; }
public DelegateCommand SelectHTTPCommand { get; }
#endregion
#region
public ConnectionConfigEvent.ConnectionConfigData ConnectionConfigData { get; private set; }
#endregion
public ConnectionConfigViewModel()
{
ConfirmCommand = new DelegateCommand(ExecuteConfirm);
CancelCommand = new DelegateCommand(ExecuteCancel);
SelectS7Command = new DelegateCommand(() => SelectedProtocol = Events.ProtocolType.S7);
SelectModbusTCPCommand = new DelegateCommand(() => SelectedProtocol = Events.ProtocolType.ModbusTCP);
SelectModbusRTUCommand = new DelegateCommand(() => SelectedProtocol = Events.ProtocolType.ModbusRTU);
SelectTCPCommand = new DelegateCommand(() => SelectedProtocol = Events.ProtocolType.TCP);
SelectHTTPCommand = new DelegateCommand(() => SelectedProtocol = Events.ProtocolType.HTTP);
}
#region
private void ExecuteConfirm()
{
ConnectionConfigData = new ConnectionConfigEvent.ConnectionConfigData
{
Protocol = SelectedProtocol,
IpAddress = IpAddress,
Port = Port,
SerialPort = SerialPort,
BaudRate = BaudRate,
Parity = Parity,
DataBits = DataBits,
StopBits = StopBits,
CpuType = CpuType,
Rack = Rack,
Slot = Slot,
SendTimeout = SendTimeout,
ReceiveTimeout = ReceiveTimeout
};
var window = Application.Current.Windows.OfType<Views.ConnectionConfigView>().FirstOrDefault();
if (window != null)
{
window.DialogResult = true;
window.Close();
}
}
private void ExecuteCancel()
{
ConnectionConfigData = null;
var window = Application.Current.Windows.OfType<Views.ConnectionConfigView>().FirstOrDefault();
if (window != null)
{
window.DialogResult = false;
window.Close();
}
}
#endregion
}
}

View File

@@ -1,25 +1,846 @@
using Model.Entity; using DeviceCommand.Base;
using DeviceCommand.Devices;
using MainModule.Events;
using MainModule.Views;
using Model.Entity;
using Prism.Commands; using Prism.Commands;
using Prism.Events;
using Prism.Ioc; using Prism.Ioc;
using Service.Interface; using Prism.Navigation;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Windows.Input; using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
using UIShare.ViewModelBase; using UIShare.ViewModelBase;
namespace MainModule.ViewModels namespace MainModule.ViewModels
{ {
public class MainViewModel : NavigateViewModelBase public class MainViewModel : NavigateViewModelBase
{ {
private readonly THC1100 _thc1100 = new();
#region private readonly UMC1300 _umc1300;
private UMC1000Rtu? _umc1000Rtu;
public ObservableCollection<ChamberMonitorItem> Chambers { get; } = new();
private IContainerProvider _containerProvider; private IContainerProvider _containerProvider;
private CancellationTokenSource _refreshCts;
private DispatcherTimer _refreshTimer;
private bool _isInitialized = false;
#region
private string _connectButtonText = "连接设备";
public string ConnectButtonText
{
get => _connectButtonText;
set => SetProperty(ref _connectButtonText, value);
}
private bool _isConnected = false;
public bool IsConnected
{
get => _isConnected;
set
{
if (SetProperty(ref _isConnected, value))
{
ConnectButtonText = value ? "断开连接" : "连接设备";
}
}
}
#endregion #endregion
#region
public DelegateCommand ConnectCommand { get; }
public DelegateCommand DisconnectCommand { get; }
public DelegateCommand RefreshCommand { get; }
public DelegateCommand ConfigCommand { get; }
public DelegateCommand<ChamberMonitorItem> ConnectDeviceCommand { get; }
#endregion
#region
private Events.ProtocolType _selectedProtocol = Events.ProtocolType.S7;
public Events.ProtocolType SelectedProtocol
{
get => _selectedProtocol;
set => SetProperty(ref _selectedProtocol, value);
}
private string _configIpAddress = "127.0.0.1";
public string ConfigIpAddress
{
get => _configIpAddress;
set => SetProperty(ref _configIpAddress, value);
}
private int _configPort = 102;
public int ConfigPort
{
get => _configPort;
set => SetProperty(ref _configPort, value);
}
private S7.Net.CpuType _configCpuType = S7.Net.CpuType.S71200;
public S7.Net.CpuType ConfigCpuType
{
get => _configCpuType;
set => SetProperty(ref _configCpuType, value);
}
private short _configRack = 0;
public short ConfigRack
{
get => _configRack;
set => SetProperty(ref _configRack, value);
}
private short _configSlot = 1;
public short ConfigSlot
{
get => _configSlot;
set => SetProperty(ref _configSlot, value);
}
private string _configSerialPort = "COM1";
public string ConfigSerialPort
{
get => _configSerialPort;
set => SetProperty(ref _configSerialPort, value);
}
private int _configBaudRate = 9600;
public int ConfigBaudRate
{
get => _configBaudRate;
set => SetProperty(ref _configBaudRate, value);
}
private int _configDataBits = 8;
public int ConfigDataBits
{
get => _configDataBits;
set => SetProperty(ref _configDataBits, value);
}
private byte _configSlaveId = 1;
public byte ConfigSlaveId
{
get => _configSlaveId;
set => SetProperty(ref _configSlaveId, value);
}
private int _configSendTimeout = 3000;
public int ConfigSendTimeout
{
get => _configSendTimeout;
set => SetProperty(ref _configSendTimeout, value);
}
private int _configReceiveTimeout = 5000;
public int ConfigReceiveTimeout
{
get => _configReceiveTimeout;
set => SetProperty(ref _configReceiveTimeout, value);
}
#endregion
public MainViewModel(IContainerProvider containerProvider) : base(containerProvider) public MainViewModel(IContainerProvider containerProvider) : base(containerProvider)
{ {
_containerProvider = containerProvider; _containerProvider = containerProvider;
// 初始化UMC1300设备默认从站地址1
_umc1300 = new UMC1300(1, "127.0.0.1", 502);
// 初始化命令
ConnectCommand = new DelegateCommand(async () => await ExecuteConnectAsync());
DisconnectCommand = new DelegateCommand(ExecuteDisconnect);
RefreshCommand = new DelegateCommand(async () => await RefreshDeviceDataAsync());
ConfigCommand = new DelegateCommand(() => ShowConfigDialogAsync());
ConnectDeviceCommand = new DelegateCommand<ChamberMonitorItem>(async (item) => await ExecuteConnectDeviceAsync(item));
} }
/// <summary>
/// 当视图被导航到时执行初始化Prism导航机制
/// </summary>
public override async void OnNavigatedTo(NavigationContext navigationContext)
{
base.OnNavigatedTo(navigationContext);
// 避免重复初始化
if (_isInitialized)
return;
_isInitialized = true;
await InitializeAsync();
}
/// <summary>
/// 异步初始化设备连接和数据
/// </summary>
private async Task InitializeAsync()
{
try
{
// 先添加所有卡片(显示初始状态)
Application.Current.Dispatcher.Invoke(() =>
{
Chambers.Clear();
Chambers.Add(new ChamberMonitorItem
{
Id = 1,
Name = "环境箱 01",
ProtocolType = "Modbus TCP",
IsConnected = false,
Temperature = 0.0,
Humidity = 0.0
});
Chambers.Add(new ChamberMonitorItem
{
Id = 2,
Name = "环境箱 02",
ProtocolType = "Modbus RTU",
IsConnected = false,
Temperature = 0.0,
Humidity = 0.0
});
Chambers.Add(new ChamberMonitorItem
{
Id = 3,
Name = "环境箱 03",
ProtocolType = "S7",
IsConnected = false,
Temperature = 0.0,
Humidity = 0.0
});
Chambers.Add(new ChamberMonitorItem
{
Id = 4,
Name = "环境箱 04",
ProtocolType = "HTTP",
IsConnected = false,
Temperature = 0.0,
Humidity = 0.0
});
});
System.Diagnostics.Debug.WriteLine("设备卡片初始化完成");
}
catch (System.Exception ex)
{
System.Diagnostics.Debug.WriteLine($"初始化异常: {ex.Message}");
}
}
#region
/// <summary>
/// 显示配置连接对话框
/// </summary>
private void ShowConfigDialogAsync()
{
try
{
// 手动创建 ViewModel
var viewModel = new ConnectionConfigViewModel();
// 设置初始值
viewModel.SelectedProtocol = SelectedProtocol;
viewModel.IpAddress = ConfigIpAddress;
viewModel.Port = ConfigPort;
viewModel.CpuType = ConfigCpuType;
viewModel.Rack = ConfigRack;
viewModel.Slot = ConfigSlot;
viewModel.SerialPort = ConfigSerialPort;
viewModel.BaudRate = ConfigBaudRate;
viewModel.SendTimeout = ConfigSendTimeout;
viewModel.ReceiveTimeout = ConfigReceiveTimeout;
// 创建窗口
var configWindow = new ConnectionConfigView(viewModel);
// 设置父窗口
configWindow.Owner = Application.Current.MainWindow;
// 显示模态对话框
bool? result = configWindow.ShowDialog();
// 如果用户点击确定,获取配置数据
if (result == true && viewModel.ConnectionConfigData != null)
{
// 直接更新配置参数
SelectedProtocol = viewModel.ConnectionConfigData.Protocol;
ConfigIpAddress = viewModel.ConnectionConfigData.IpAddress;
ConfigPort = viewModel.ConnectionConfigData.Port;
ConfigCpuType = viewModel.ConnectionConfigData.CpuType;
ConfigRack = viewModel.ConnectionConfigData.Rack;
ConfigSlot = viewModel.ConnectionConfigData.Slot;
ConfigSerialPort = viewModel.ConnectionConfigData.SerialPort;
ConfigBaudRate = viewModel.ConnectionConfigData.BaudRate;
ConfigSendTimeout = viewModel.ConnectionConfigData.SendTimeout;
ConfigReceiveTimeout = viewModel.ConnectionConfigData.ReceiveTimeout;
System.Diagnostics.Debug.WriteLine($"配置更新成功: 协议={SelectedProtocol}, IP={ConfigIpAddress}, 端口={ConfigPort}");
}
}
catch (System.Exception ex)
{
System.Diagnostics.Debug.WriteLine($"打开配置对话框异常: {ex.Message}");
}
}
/// <summary>
/// 配置更新事件处理
/// </summary>
private void OnConfigUpdated(ConnectionConfigEvent.ConnectionConfigData config)
{
SelectedProtocol = config.Protocol;
ConfigIpAddress = config.IpAddress;
ConfigPort = config.Port;
ConfigCpuType = config.CpuType;
ConfigRack = config.Rack;
ConfigSlot = config.Slot;
ConfigSerialPort = config.SerialPort;
ConfigBaudRate = config.BaudRate;
ConfigSendTimeout = config.SendTimeout;
ConfigReceiveTimeout = config.ReceiveTimeout;
System.Diagnostics.Debug.WriteLine($"配置更新: 协议={SelectedProtocol}, IP={ConfigIpAddress}, 端口={ConfigPort}");
}
#endregion
#region
/// <summary>
/// 执行连接/断开切换命令
/// </summary>
private async Task ExecuteConnectAsync()
{
if (IsConnected)
{
// 如果已连接,则断开
ExecuteDisconnect();
}
else
{
// 如果未连接,则连接
await ConnectToDeviceAsync();
}
}
/// <summary>
/// 连接到THC设备
/// </summary>
private async Task ConnectToDeviceAsync()
{
try
{
System.Diagnostics.Debug.WriteLine("正在连接THC设备...");
// 使用配置对话框中设置的参数配置设备
_thc1100.ConfigureDevice(
ipAddress: ConfigIpAddress,
cpuType: ConfigCpuType,
rack: ConfigRack,
slot: ConfigSlot
);
System.Diagnostics.Debug.WriteLine($"配置参数: 协议={SelectedProtocol}, IP={_thc1100.IPAddress}, CPU={_thc1100.CpuType}, Rack={_thc1100.Rack}, Slot={_thc1100.Slot}");
// 连接THC设备
bool connected = await _thc1100.ConnectAsync();
if (connected)
{
IsConnected = true;
System.Windows.MessageBox.Show("THC设备连接成功", "连接成功", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Information);
// 更新环境箱03的连接状态
var chamber3 = GetChamberById(3);
if (chamber3 != null)
{
chamber3.IsConnected = true;
}
// 读取初始数据
await RefreshDeviceDataAsync();
// 启动定时刷新每1秒
StartPeriodicRefresh();
}
else
{
System.Diagnostics.Debug.WriteLine("THC设备连接失败");
MessageBox.Show("设备连接失败,请检查网络和设备状态!", "连接失败",
MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
catch (System.Exception ex)
{
System.Diagnostics.Debug.WriteLine($"连接异常: {ex.Message}");
MessageBox.Show($"连接异常: {ex.Message}", "错误",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
/// <summary>
/// 断开THC设备连接
/// </summary>
private void ExecuteDisconnect()
{
try
{
// 停止定时器
_refreshCts?.Cancel();
_refreshTimer?.Stop();
// 关闭设备连接
_thc1100.Close();
// 更新连接状态
IsConnected = false;
// 更新环境箱03的连接状态
var chamber3 = GetChamberById(3);
if (chamber3 != null)
{
chamber3.IsConnected = false;
}
System.Diagnostics.Debug.WriteLine("THC设备已断开连接");
}
catch (System.Exception ex)
{
System.Diagnostics.Debug.WriteLine($"断开连接异常: {ex.Message}");
}
}
#endregion
#region
/// <summary>
/// 启动定时刷新
/// </summary>
private void StartPeriodicRefresh()
{
_refreshCts?.Cancel();
_refreshCts = new CancellationTokenSource();
_refreshTimer?.Stop();
_refreshTimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(1)
};
_refreshTimer.Tick += async (s, e) =>
{
try
{
System.Diagnostics.Debug.WriteLine($"[定时刷新] Tick触发当前时间: {DateTime.Now:HH:mm:ss.fff}");
System.Diagnostics.Debug.WriteLine($"[定时刷新] THC1100.IsConnected = {_thc1100.IsConnected}");
// 检查THC1100连接状态
if (_thc1100.IsConnected)
{
var chamber3 = GetChamberById(3);
System.Diagnostics.Debug.WriteLine($"[定时刷新] chamber3 = {chamber3}, IsConnected = {chamber3?.IsConnected}");
if (chamber3 != null)
{
System.Diagnostics.Debug.WriteLine("[定时刷新] 开始读取温湿度数据...");
try
{
var temp = await _thc1100.GetRealTimeTemperatureSetPointAsync();
var humidity = await _thc1100.GetRealTimeHumidityMeasuredValueAsync();
System.Diagnostics.Debug.WriteLine($"[定时刷新] 读取完成 - 温度(DB53.DBD280): {temp}, 湿度(DB53.DBD1092): {humidity}");
Application.Current.Dispatcher.Invoke(() =>
{
chamber3.Temperature = temp;
chamber3.Humidity = humidity;
System.Diagnostics.Debug.WriteLine($"[定时刷新] UI已更新 - Temperature={chamber3.Temperature}, Humidity={chamber3.Humidity}");
});
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[定时刷新] 读取温湿度数据异常: {ex.Message}");
}
}
}
else
{
System.Diagnostics.Debug.WriteLine("[定时刷新] THC1100未连接跳过数据读取");
}
// 检查UMC1300连接状态 → 环境箱01 (Modbus TCP)
if (_umc1300.IsConnected)
{
var chamber1 = GetChamberById(1);
if (chamber1 != null)
{
try
{
var temp = await _umc1300.ReadTemperaturePVAsync();
var humidity = await _umc1300.ReadHumidityPVAsync();
Application.Current.Dispatcher.Invoke(() =>
{
chamber1.Temperature = temp;
chamber1.Humidity = humidity;
});
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[定时刷新] 读取UMC1300数据异常: {ex.Message}");
}
}
}
// 检查UMC1000Rtu连接状态 → 环境箱02 (Modbus RTU)
if (_umc1000Rtu != null && _umc1000Rtu.IsConnected)
{
var chamber2 = GetChamberById(2);
if (chamber2 != null)
{
try
{
var temp = await _umc1000Rtu.ReadTemperatureAsync();
var humidity = await _umc1000Rtu.ReadHumidityAsync();
Application.Current.Dispatcher.Invoke(() =>
{
chamber2.Temperature = temp;
chamber2.Humidity = humidity;
});
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[定时刷新] 读取UMC1000Rtu数据异常: {ex.Message}");
}
}
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[定时刷新] 定时器异常: {ex.Message}");
}
};
_refreshTimer.Start();
}
/// <summary>
/// 停止定时刷新
/// </summary>
private void StopPeriodicRefresh()
{
_refreshCts?.Cancel();
_refreshTimer?.Stop();
}
#endregion
#region
/// <summary>
/// 刷新设备数据
/// </summary>
private async Task RefreshDeviceDataAsync()
{
try
{
// 如果未连接,不刷新数据
if (!_thc1100.IsConnected)
return;
var chamber3 = GetChamberById(3);
if (chamber3 == null) return;
// 读取温湿度数据(使用用户指定的寄存器地址)
// 温度: DB53.DBD280, 湿度: DB53.DBD1092
var temp = await _thc1100.GetRealTimeTemperatureSetPointAsync();
var humidity = await _thc1100.GetRealTimeHumidityMeasuredValueAsync();
System.Diagnostics.Debug.WriteLine($"[初始刷新] 环境箱3 - 温度(DB53.DBD280): {temp}, 湿度(DB53.DBD1092): {humidity}");
// 更新UI在UI线程执行
Application.Current.Dispatcher.Invoke(() =>
{
chamber3.Temperature = temp;
chamber3.Humidity = humidity;
});
}
catch (System.Exception ex)
{
System.Diagnostics.Debug.WriteLine($"刷新数据异常: {ex.Message}");
}
}
#endregion
#region
/// <summary>
/// 根据ID获取卡片
/// </summary>
private ChamberMonitorItem GetChamberById(int id)
{
foreach (var chamber in Chambers)
{
if (chamber.Id == id)
return chamber;
}
return null;
}
/// <summary>
/// 根据设备类型连接单个设备
/// </summary>
private async Task ExecuteConnectDeviceAsync(ChamberMonitorItem item)
{
if (item == null) return;
try
{
if (item.IsConnected)
{
// 断开连接
System.Diagnostics.Debug.WriteLine($"[连接] 正在断开设备 {item.Name}");
// 根据设备自己的协议类型调用对应设备的Close方法
switch (item.ProtocolType)
{
case "S7":
_thc1100.Close();
break;
case "Modbus TCP":
_umc1300.Close();
break;
case "Modbus RTU":
_umc1000Rtu?.Close();
break;
}
// 停止数据刷新
StopPeriodicRefresh();
item.IsConnected = false;
System.Diagnostics.Debug.WriteLine($"[连接] 设备 {item.Name} 已断开连接");
}
else
{
// 输出当前配置参数
System.Diagnostics.Debug.WriteLine($"[连接] ========== 开始连接设备 {item.Name} ==========");
System.Diagnostics.Debug.WriteLine($"[连接] 当前协议类型: {SelectedProtocol}");
System.Diagnostics.Debug.WriteLine($"[连接] IP地址: {ConfigIpAddress}");
System.Diagnostics.Debug.WriteLine($"[连接] 端口: {ConfigPort}");
System.Diagnostics.Debug.WriteLine($"[连接] CPU类型: {ConfigCpuType}");
System.Diagnostics.Debug.WriteLine($"[连接] Rack: {ConfigRack}, Slot: {ConfigSlot}");
System.Diagnostics.Debug.WriteLine($"[连接] 串口: {ConfigSerialPort}, 波特率: {ConfigBaudRate}");
System.Diagnostics.Debug.WriteLine($"[连接] 发送超时: {ConfigSendTimeout}ms, 接收超时: {ConfigReceiveTimeout}ms");
// 根据设备自己的协议类型连接设备
bool connected = false;
System.Diagnostics.Debug.WriteLine($"[连接] 设备协议类型: {item.ProtocolType}");
switch (item.ProtocolType)
{
case "S7":
// S7协议连接
System.Diagnostics.Debug.WriteLine("[连接] 使用设备配置的S7协议");
connected = await ConnectS7DeviceAsync(item);
break;
case "Modbus TCP":
// Modbus TCP连接
System.Diagnostics.Debug.WriteLine("[连接] 使用设备配置的Modbus TCP协议");
connected = await ConnectModbusTcpDeviceAsync(item);
break;
case "Modbus RTU":
// Modbus RTU连接 → UMC1000Rtu 温湿度
System.Diagnostics.Debug.WriteLine("[连接] 使用设备配置的Modbus RTU协议");
connected = await ConnectUMC1000RtuDeviceAsync(item);
break;
case "HTTP":
// HTTP连接
System.Diagnostics.Debug.WriteLine("[连接] 使用设备配置的HTTP协议");
connected = await ConnectHttpDeviceAsync(item);
break;
default:
System.Diagnostics.Debug.WriteLine($"[连接] 未知协议类型: {item.ProtocolType}");
break;
}
item.IsConnected = connected;
System.Diagnostics.Debug.WriteLine($"[连接] 设备 {item.Name} 连接结果: {(connected ? "" : "")}");
System.Diagnostics.Debug.WriteLine($"[连接] ========== 连接尝试结束 ==========");
}
}
catch (System.Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[连接] 设备 {item.Name} 连接异常: {ex.Message}");
System.Diagnostics.Debug.WriteLine($"[连接] 异常堆栈: {ex.StackTrace}");
item.IsConnected = false;
}
}
/// <summary>
/// S7协议设备连接
/// </summary>
private async Task<bool> ConnectS7DeviceAsync(ChamberMonitorItem item)
{
try
{
System.Diagnostics.Debug.WriteLine($"[ConnectS7DeviceAsync] 开始配置设备: IP={ConfigIpAddress}, CPU={ConfigCpuType}, Rack={ConfigRack}, Slot={ConfigSlot}");
_thc1100.ConfigureDevice(ConfigIpAddress, ConfigCpuType, ConfigRack, ConfigSlot);
bool result = await _thc1100.ConnectAsync();
System.Diagnostics.Debug.WriteLine($"[ConnectS7DeviceAsync] 连接结果: {result}, IsConnected属性值: {_thc1100.IsConnected}");
if (result)
{
System.Diagnostics.Debug.WriteLine("[ConnectS7DeviceAsync] 连接成功,启动定时刷新");
StartPeriodicRefresh();
}
return result;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[ConnectS7DeviceAsync] 异常: {ex.Message}, 堆栈: {ex.StackTrace}");
return false;
}
}
/// <summary>
/// UMC1000Rtu 温湿度设备连接(Modbus RTU)
/// </summary>
private async Task<bool> ConnectUMC1000RtuDeviceAsync(ChamberMonitorItem item)
{
try
{
_umc1000Rtu = new UMC1000Rtu(
slaveId: ConfigSlaveId,
portName: ConfigSerialPort,
baudRate: ConfigBaudRate,
dataBits: ConfigDataBits,
stopBits: System.IO.Ports.StopBits.One,
parity: System.IO.Ports.Parity.None,
readTimeout: ConfigReceiveTimeout,
writeTimeout: ConfigSendTimeout);
bool result = await _umc1000Rtu.ConnectAsync();
if (result)
{
StartPeriodicRefresh();
}
System.Diagnostics.Debug.WriteLine($"UMC1000Rtu Modbus RTU连接: {ConfigSerialPort}, 波特率:{ConfigBaudRate}, 从站:{ConfigSlaveId}, 结果: {result}");
return result;
}
catch (System.Exception ex)
{
System.Diagnostics.Debug.WriteLine($"UMC1000Rtu连接异常: {ex.Message}");
return false;
}
}
/// <summary>
/// Modbus TCP设备连接(UMC1300)
/// </summary>
private async Task<bool> ConnectModbusTcpDeviceAsync(ChamberMonitorItem item)
{
try
{
_umc1300.ConfigureDevice(ConfigIpAddress, ConfigPort, ConfigSendTimeout, ConfigReceiveTimeout);
bool result = await _umc1300.ConnectAsync();
if (result)
{
StartPeriodicRefresh();
}
System.Diagnostics.Debug.WriteLine($"UMC1300 Modbus TCP连接: {ConfigIpAddress}:{ConfigPort}, 结果: {result}");
return result;
}
catch (System.Exception ex)
{
System.Diagnostics.Debug.WriteLine($"UMC1300连接异常: {ex.Message}");
return false;
}
}
/// <summary>
/// HTTP设备连接
/// </summary>
private async Task<bool> ConnectHttpDeviceAsync(ChamberMonitorItem item)
{
// 实现HTTP连接逻辑
System.Diagnostics.Debug.WriteLine($"HTTP连接: {ConfigIpAddress}:{ConfigPort}");
await Task.Delay(500);
return true;
}
#endregion
#region
/// <summary>
/// 导航离开时清理资源
/// </summary>
public override void OnNavigatedFrom(NavigationContext navigationContext)
{
base.OnNavigatedFrom(navigationContext);
ExecuteDisconnect();
_isInitialized = false;
}
#endregion
}
/// <summary>
/// 环境箱 UI 状态及温湿度数据绑定模型
/// </summary>
public class ChamberMonitorItem : BindableBase
{
private double _temperature;
private double _humidity;
private bool _isConnected;
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string ProtocolType { get; set; } = string.Empty;
/// <summary>
/// 温度(支持通知刷新)
/// </summary>
public double Temperature
{
get => _temperature;
set => SetProperty(ref _temperature, value);
}
/// <summary>
/// 湿度(支持通知刷新)
/// </summary>
public double Humidity
{
get => _humidity;
set => SetProperty(ref _humidity, value);
}
/// <summary>
/// 连接状态True-已连接(绿灯)False-断开(红灯)
/// </summary>
public bool IsConnected
{
get => _isConnected;
set
{
if (SetProperty(ref _isConnected, value))
{
RaisePropertyChanged(nameof(ConnectButtonText));
}
}
}
/// <summary>
/// 连接按钮文本
/// </summary>
public string ConnectButtonText => IsConnected ? "断开连接" : "连接设备";
} }
} }

View File

@@ -0,0 +1,285 @@
<Window x:Class="MainModule.Views.ConnectionConfigView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:prism="http://prismlibrary.com/"
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
Title="设备连接配置"
Width="520"
Height="550"
MinWidth="400"
MinHeight="450"
WindowStartupLocation="CenterOwner"
ResizeMode="CanResize">
<Window.Resources>
<converters:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<Style TargetType="TextBlock" x:Key="LabelStyle">
<Setter Property="FontSize" Value="13"/>
<Setter Property="Foreground" Value="#4A5568"/>
<Setter Property="Margin" Value="0,6,0,2"/>
</Style>
<Style TargetType="TextBox" x:Key="InputStyle">
<Setter Property="Padding" Value="8"/>
<Setter Property="Margin" Value="0,2,0,6"/>
<Setter Property="BorderBrush" Value="#E2E8F0"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="FontSize" Value="13"/>
</Style>
<Style TargetType="ComboBox" x:Key="ComboStyle">
<Setter Property="Padding" Value="8"/>
<Setter Property="Margin" Value="0,2,0,6"/>
<Setter Property="BorderBrush" Value="#E2E8F0"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="FontSize" Value="13"/>
</Style>
</Window.Resources>
<Grid Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="设备连接配置" FontSize="18" FontWeight="Bold" Foreground="#2C3E50" Margin="20,20,20,16"/>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" Margin="20">
<StackPanel>
<TextBlock Text="选择协议类型" Style="{StaticResource LabelStyle}"/>
<StackPanel Orientation="Horizontal" Margin="0,2,0,6">
<Button Content="S7"
Command="{Binding SelectS7Command}"
Width="80" Height="36"
Margin="0,0,4,0"/>
<Button Content="Modbus TCP"
Command="{Binding SelectModbusTCPCommand}"
Width="100" Height="36"
Margin="0,0,4,0"/>
<Button Content="Modbus RTU"
Command="{Binding SelectModbusRTUCommand}"
Width="100" Height="36"
Margin="0,0,4,0"/>
<Button Content="TCP"
Command="{Binding SelectTCPCommand}"
Width="80" Height="36"
Margin="0,0,4,0"/>
<Button Content="HTTP"
Command="{Binding SelectHTTPCommand}"
Width="80" Height="36"/>
</StackPanel>
<Border Visibility="{Binding IsS7Protocol, Converter={StaticResource BooleanToVisibilityConverter}}"
BorderBrush="#E2E8F0" BorderThickness="1" CornerRadius="8" Padding="12" Margin="0,0,0,12">
<StackPanel>
<TextBlock Text="S7协议配置" FontSize="14" FontWeight="Bold" Foreground="#2C3E50" Margin="0,0,0,10"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="IP地址" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding IpAddress}" Style="{StaticResource InputStyle}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="CPU类型" Style="{StaticResource LabelStyle}"/>
<ComboBox Grid.Column="1" ItemsSource="{Binding CpuTypes}"
SelectedItem="{Binding CpuType}" Style="{StaticResource ComboStyle}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Rack" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding Rack}" Style="{StaticResource InputStyle}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Slot" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding Slot}" Style="{StaticResource InputStyle}"/>
</Grid>
</StackPanel>
</Border>
<Border Visibility="{Binding IsModbusTCPProtocol, Converter={StaticResource BooleanToVisibilityConverter}}"
BorderBrush="#E2E8F0" BorderThickness="1" CornerRadius="8" Padding="12" Margin="0,0,0,12">
<StackPanel>
<TextBlock Text="Modbus TCP配置" FontSize="14" FontWeight="Bold" Foreground="#2C3E50" Margin="0,0,0,10"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="IP地址" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding IpAddress}" Style="{StaticResource InputStyle}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="端口号" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding Port}" Style="{StaticResource InputStyle}"/>
</Grid>
</StackPanel>
</Border>
<Border Visibility="{Binding IsModbusRTUProtocol, Converter={StaticResource BooleanToVisibilityConverter}}"
BorderBrush="#E2E8F0" BorderThickness="1" CornerRadius="8" Padding="12" Margin="0,0,0,12">
<StackPanel>
<TextBlock Text="Modbus RTU配置" FontSize="14" FontWeight="Bold" Foreground="#2C3E50" Margin="0,0,0,10"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="串口" Style="{StaticResource LabelStyle}"/>
<ComboBox Grid.Column="1" ItemsSource="{Binding SerialPorts}"
SelectedItem="{Binding SerialPort}" Style="{StaticResource ComboStyle}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="波特率" Style="{StaticResource LabelStyle}"/>
<ComboBox Grid.Column="1" ItemsSource="{Binding BaudRates}"
SelectedItem="{Binding BaudRate}" Style="{StaticResource ComboStyle}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="数据位" Style="{StaticResource LabelStyle}"/>
<ComboBox Grid.Column="1" ItemsSource="{Binding DataBitsList}"
SelectedItem="{Binding DataBits}" Style="{StaticResource ComboStyle}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="校验位" Style="{StaticResource LabelStyle}"/>
<ComboBox Grid.Column="1" ItemsSource="{Binding ParityList}"
SelectedItem="{Binding Parity}" Style="{StaticResource ComboStyle}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="停止位" Style="{StaticResource LabelStyle}"/>
<ComboBox Grid.Column="1" ItemsSource="{Binding StopBitsList}"
SelectedItem="{Binding StopBits}" Style="{StaticResource ComboStyle}"/>
</Grid>
</StackPanel>
</Border>
<Border Visibility="{Binding IsTCPProtocol, Converter={StaticResource BooleanToVisibilityConverter}}"
BorderBrush="#E2E8F0" BorderThickness="1" CornerRadius="8" Padding="12" Margin="0,0,0,12">
<StackPanel>
<TextBlock Text="TCP配置" FontSize="14" FontWeight="Bold" Foreground="#2C3E50" Margin="0,0,0,10"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="IP地址" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding IpAddress}" Style="{StaticResource InputStyle}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="端口号" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding Port}" Style="{StaticResource InputStyle}"/>
</Grid>
</StackPanel>
</Border>
<Border Visibility="{Binding IsHTTPProtocol, Converter={StaticResource BooleanToVisibilityConverter}}"
BorderBrush="#E2E8F0" BorderThickness="1" CornerRadius="8" Padding="12" Margin="0,0,0,12">
<StackPanel>
<TextBlock Text="HTTP配置" FontSize="14" FontWeight="Bold" Foreground="#2C3E50" Margin="0,0,0,10"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="IP地址" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding IpAddress}" Style="{StaticResource InputStyle}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="端口号" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding Port}" Style="{StaticResource InputStyle}"/>
</Grid>
</StackPanel>
</Border>
<Border BorderBrush="#E2E8F0" BorderThickness="1" CornerRadius="8" Padding="12" Margin="0,0,0,12">
<StackPanel>
<TextBlock Text="超时设置" FontSize="14" FontWeight="Bold" Foreground="#2C3E50" Margin="0,0,0,10"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="发送超时(ms)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding SendTimeout}" Style="{StaticResource InputStyle}"/>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="120"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="接收超时(ms)" Style="{StaticResource LabelStyle}"/>
<TextBox Grid.Column="1" Text="{Binding ReceiveTimeout}" Style="{StaticResource InputStyle}"/>
</Grid>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="20,16,20,20">
<Button Content="取消"
Command="{Binding CancelCommand}"
Width="80" Height="36"
Margin="0,0,8,0"/>
<Button Content="确定"
Command="{Binding ConfirmCommand}"
Width="80" Height="36"/>
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,18 @@
using MainModule.ViewModels;
using System.Windows;
namespace MainModule.Views
{
public partial class ConnectionConfigView : Window
{
public ConnectionConfigView()
{
InitializeComponent();
}
public ConnectionConfigView(ConnectionConfigViewModel viewModel) : this()
{
DataContext = viewModel;
}
}
}

View File

@@ -1,4 +1,4 @@
<UserControl x:Class="MainModule.Views.MainView" <UserControl x:Class="MainModule.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -15,5 +15,149 @@
<converters:BooleanToVisibilityConverter x:Key="BoolToVis" /> <converters:BooleanToVisibilityConverter x:Key="BoolToVis" />
</UserControl.Resources> </UserControl.Resources>
<Grid Background="#F4F6F9">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<DockPanel Grid.Row="0" Margin="12,4,12,16">
<TextBlock Text="环境箱监控状态面板" FontSize="20" FontWeight="Bold" Foreground="#2C3E50" VerticalAlignment="Center"/>
<!-- 顶部工具栏按钮 -->
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
<!-- 配置连接按钮 -->
<Button Content="配置连接"
Command="{Binding ConfigCommand}"
Style="{StaticResource MaterialDesignFlatButton}"
Foreground="#805AD5"
FontSize="13"
Margin="0,0,8,0"
ToolTip="配置设备连接参数"/>
<!-- 连接/断开按钮 -->
<Button Content="{Binding ConnectButtonText}"
Command="{Binding ConnectCommand}"
Style="{StaticResource MaterialDesignFlatButton}"
Foreground="#3182CE"
FontSize="13"
Margin="0,0,8,0"
ToolTip="点击连接/断开设备">
<Button.ContextMenu>
<ContextMenu>
<MenuItem Header="连接设备" Command="{Binding ConnectCommand}"/>
<MenuItem Header="断开连接" Command="{Binding DisconnectCommand}"/>
</ContextMenu>
</Button.ContextMenu>
</Button>
<!-- 刷新按钮 -->
<Button Content="刷新数据"
Command="{Binding RefreshCommand}"
Style="{StaticResource MaterialDesignFlatButton}"
Foreground="#38A169"
FontSize="13"
ToolTip="手动刷新设备数据"/>
</StackPanel>
</DockPanel>
<ItemsControl Grid.Row="1" ItemsSource="{Binding Chambers}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="2" Rows="2" Margin="-6"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="White" BorderBrush="#E2E8F0" BorderThickness="1" CornerRadius="8" Margin="6" Padding="20">
<Border.Effect>
<DropShadowEffect BlurRadius="8" Color="#A0AEC0" Opacity="0.15" ShadowDepth="1"/>
</Border.Effect>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<DockPanel Grid.Row="0" LastChildFill="False">
<StackPanel DockPanel.Dock="Left">
<TextBlock Text="{Binding Name}" FontSize="16" FontWeight="Bold" Foreground="#1A202C"/>
<TextBlock Text="{Binding ProtocolType, StringFormat=驱动协议: {0}}" FontSize="12" Foreground="#718096" Margin="0,2,0,0"/>
</StackPanel>
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center">
<Border Width="10" Height="10" CornerRadius="5" Margin="0,0,6,0" VerticalAlignment="Center">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="#E53E3E"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Background" Value="#38A169"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
<TextBlock VerticalAlignment="Center" FontSize="13" FontWeight="Medium">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="OFFLINE"/>
<Setter Property="Foreground" Value="#E53E3E"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Text" Value="ONLINE"/>
<Setter Property="Foreground" Value="#38A169"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</DockPanel>
<UniformGrid Grid.Row="1" Columns="2" Margin="0,20,0,0">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="温度采集" FontSize="13" Foreground="#718096" HorizontalAlignment="Center"/>
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock Text="{Binding Temperature, StringFormat={}{0:F1}}" FontSize="40" FontWeight="Light" Foreground="#3182CE"/>
<TextBlock Text=" ℃" FontSize="16" Foreground="#3182CE" VerticalAlignment="Bottom" Margin="2,0,0,8"/>
</StackPanel>
</StackPanel>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="湿度采集" FontSize="13" Foreground="#718096" HorizontalAlignment="Center"/>
<StackPanel Orientation="Horizontal" Margin="0,6,0,0">
<TextBlock Text="{Binding Humidity, StringFormat={}{0:F1}}" FontSize="40" FontWeight="Light" Foreground="#319795"/>
<TextBlock Text=" %RH" FontSize="16" Foreground="#319795" VerticalAlignment="Bottom" Margin="2,0,0,8"/>
</StackPanel>
</StackPanel>
</UniformGrid>
<!-- 连接按钮 -->
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,16,0,0">
<Button Content="{Binding ConnectButtonText}"
Command="{Binding DataContext.ConnectDeviceCommand, RelativeSource={RelativeSource AncestorType=ItemsControl}}"
CommandParameter="{Binding}"
Width="100"
Height="36">
<Button.Style>
<Style TargetType="Button" BasedOn="{StaticResource MaterialDesignFlatButton}">
<Setter Property="Foreground" Value="#3182CE"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Foreground" Value="#E53E3E"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</UserControl> </UserControl>

View File

@@ -1,24 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Model.Entity
{
public class PostEntity : BaseEntity
{
public string Title { get; set; }
public string Content { get; set; }
public string AuthorId { get; set; }
public string AuthorName { get; set; }
public string Category { get; set; }
public DateTime PublishTime { get; set; }
public int ViewCount { get; set; }
public int LikeCount { get; set; }
public int CommentCount { get; set; }
public bool IsTop { get; set; }
public bool IsEssence { get; set; }
public string ThumbnailUrl { get; set; }
}
}

View File

@@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Model.Entity
{
public class UserEntity:BaseEntity
{
public string UserName { get; set; }
public string Password { get; set; }
public string Role { get; set; }
public string Status { get; set; }
}
}

View File

@@ -7,11 +7,13 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="SqlSugarCore" Version="5.1.4.210" /> <Compile Remove="DTOS\**" />
<EmbeddedResource Remove="DTOS\**" />
<None Remove="DTOS\**" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="DTOS\" /> <PackageReference Include="SqlSugarCore" Version="5.1.4.210" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -0,0 +1,71 @@
using System.Text.Json.Serialization;
namespace Model.Model
{
public class EnovaChannelData
{
public string Laboratory { get; set; }
public string Manufacture { get; set; }
public string TaskId { get; set; }
// 针对文档命名不一致的字段进行别名特性处理优先匹配真实JSON示例
[JsonPropertyName("deviceCode")]
public string DeviceCode { get; set; }
[JsonPropertyName("devCode")] // 兼容参数表
public string DevCodeAlternate { set => DeviceCode = value; }
[JsonPropertyName("channelCode")]
public string ChannelCode { get; set; }
[JsonPropertyName("chnCode")]
public string ChnCodeAlternate { set => ChannelCode = value; }
[JsonPropertyName("channelIP")]
public string ChannelIP { get; set; }
[JsonPropertyName("chnlP")]
public string ChnlPAlternate { set => ChannelIP = value; }
[JsonPropertyName("pcIP")]
public string PcIP { get; set; }
[JsonPropertyName("pclP")]
public string PclPAlternate { set => PcIP = value; }
public string PcName { get; set; }
public string BarCode { get; set; }
public string ProjectName { get; set; }
public string StepId { get; set; }
public string CycleDepth { get; set; }
public string Cycles { get; set; }
public string StepTime { get; set; }
public string TotalTime { get; set; }
public string PowerState { get; set; }
public string ChannelState { get; set; }
public string Voltage { get; set; }
public string Current { get; set; }
public string Power { get; set; }
public string GivenVoltage { get; set; }
public string GivenCurrent { get; set; }
public string GivenPower { get; set; }
public string Cap { get; set; }
public string ChgCap { get; set; }
public string DchgCap { get; set; }
public string CapDiff { get; set; }
public string Energy { get; set; }
public string ChgEnergy { get; set; }
public string DchgEnergy { get; set; }
public string EnergyDiff { get; set; }
[JsonPropertyName("timestamp")]
public string Timestamp { get; set; }
[JsonPropertyName("timeStamp")]
public string TimestampAlternate { set => Timestamp = value; }
// 对应 elements 里的 dict 动态解析
public Dictionary<string, string> OtherSignals { get; set; }
}
// 严格按照文档 Page 6 要求返回的响应结构
public class ApiResponse
{
public bool Success { get; set; }
public string ErrorInfo { get; set; }
}
}

View File

@@ -1,129 +0,0 @@
using Model;
using Model.Entity;
using ORM;
using Service.Implement;
using Service.Interface;
using SqlSugar;
namespace Service.Implement
{
public class PostService : BaseService<PostEntity>, IPostService
{
public PostService(SqlSugarRepository<PostEntity> repository) : base(repository)
{
}
public async Task<Result<List<PostEntity>>> GetRecommendedPostsAsync(int count = 10)
{
try
{
var posts = await _repository.Entities
.Where(x => x.IsDel == 0) // 使用正确的字段名
.OrderBy(x => x.IsTop, OrderByType.Desc)
.OrderBy(x => x.PublishTime, OrderByType.Desc)
.Take(count)
.ToListAsync();
return Result<List<PostEntity>>.Success(posts);
}
catch (Exception ex)
{
return Result<List<PostEntity>>.Error("获取推荐帖子失败", ex);
}
}
public async Task<Result<List<PostEntity>>> SearchPostsAsync(string keyword)
{
try
{
if (string.IsNullOrWhiteSpace(keyword))
{
return Result<List<PostEntity>>.Success(new List<PostEntity>());
}
var posts = await _repository.Entities
.Where(x => (x.Title.Contains(keyword) || x.Content.Contains(keyword))
&& x.IsDel == 0) // 使用正确的字段名
.OrderBy(x => x.IsTop, OrderByType.Desc)
.OrderBy(x => x.PublishTime, OrderByType.Desc)
.ToListAsync();
return Result<List<PostEntity>>.Success(posts);
}
catch (Exception ex)
{
return Result<List<PostEntity>>.Error("搜索帖子失败", ex);
}
}
public async Task<Result<List<PostEntity>>> GetPostsByCategoryAsync(string category, int count = 10)
{
try
{
if (string.IsNullOrWhiteSpace(category))
{
return Result<List<PostEntity>>.Success(new List<PostEntity>());
}
var posts = await _repository.Entities
.Where(x => x.Category == category && x.IsDel == 0) // 使用正确的字段名
.OrderBy(x => x.IsTop, OrderByType.Desc)
.OrderBy(x => x.PublishTime, OrderByType.Desc)
.Take(count)
.ToListAsync();
return Result<List<PostEntity>>.Success(posts);
}
catch (Exception ex)
{
return Result<List<PostEntity>>.Error("获取分类帖子失败", ex);
}
}
public async Task<Result<bool>> CreatePostAsync(PostEntity post)
{
try
{
post.PublishTime = DateTime.Now;
var result = await _repository.InsertAsync(post);
return Result<bool>.Success(result);
}
catch (Exception ex)
{
return Result<bool>.Error("创建帖子失败", ex);
}
}
public async Task<Result<bool>> UpdatePostAsync(PostEntity post)
{
try
{
var result = await _repository.UpdateAsync(post);
return Result<bool>.Success(result);
}
catch (Exception ex)
{
return Result<bool>.Error("更新帖子失败", ex);
}
}
public async Task<Result<bool>> DeletePostAsync(long postId)
{
try
{
// 软删除将IsDel设为1
var post = await _repository.Entities.FirstAsync(x => x.Id == postId);
if (post == null)
return Result<bool>.Error("帖子不存在");
post.IsDel = 1;
var result = await _repository.UpdateAsync(post);
return Result<bool>.Success(result);
}
catch (Exception ex)
{
return Result<bool>.Error("删除帖子失败", ex);
}
}
}
}

View File

@@ -1,91 +0,0 @@
using Model;
using Model.Entity;
using ORM;
using Service.Implement;
using Service.Interface;
public class UserService: BaseService<UserEntity>, IUserService
{
public UserService(SqlSugarRepository<UserEntity> repository) : base(repository)
{
}
public async Task<Result<UserEntity>> GetUserByUserNameAsync(string username)
{
if (string.IsNullOrWhiteSpace(username))
return Result<UserEntity>.Error("用户名不能为空");
try
{
var user = await _repository.Entities
.Where(x => x.UserName == username)
.FirstAsync();
if (user == null)
return Result<UserEntity>.Error("用户不存在");
return Result<UserEntity>.Success(user);
}
catch (Exception ex)
{
return Result<UserEntity>.Error("获取用户失败", ex);
}
}
public override async Task<Result<bool>> InsertAsync(UserEntity entity)
{
if (entity == null)
return Result<bool>.Error("用户数据不能为空");
try
{
// 唯一性校验
bool exists = await _repository.Entities
.AnyAsync(x => x.UserName == entity.UserName);
if (exists)
return Result<bool>.Error("用户名已存在");
var result = await _repository.InsertAsync(entity);
return Result<bool>.Success(result);
}
catch (Exception ex)
{
return Result<bool>.Error("插入数据失败", ex);
}
}
/// <summary>
/// 更新单条用户记录
/// </summary>
/// <param name="entity">要更新的用户实体Id 必须有值</param>
public async Task<Result<bool>> UpdateAsync(UserEntity entity)
{
if (entity == null)
return Result<bool>.Error("用户数据不能为空");
if (entity.Id <= 0)
return Result<bool>.Error("主键 Id 无效,无法更新");
try
{
// 唯一性校验(可选,如果允许改用户名就检查)
bool exists = await _repository.Entities
.Where(x => x.UserName == entity.UserName && x.Id != entity.Id)
.AnyAsync();
if (exists)
return Result<bool>.Error("用户名已存在");
// 更新整个实体
var result = await _repository.UpdateAsync(entity);
return Result<bool>.Success(result);
}
catch (Exception ex)
{
return Result<bool>.Error("更新用户失败", ex);
}
}
}

View File

@@ -1,16 +0,0 @@
using Model;
using Model.Entity;
using Service.Interface;
namespace Service.Interface
{
public interface IPostService : IBaseService<PostEntity>
{
Task<Result<List<PostEntity>>> GetRecommendedPostsAsync(int count = 10);
Task<Result<List<PostEntity>>> SearchPostsAsync(string keyword);
Task<Result<List<PostEntity>>> GetPostsByCategoryAsync(string category, int count = 10);
Task<Result<bool>> CreatePostAsync(PostEntity post);
Task<Result<bool>> UpdatePostAsync(PostEntity post);
Task<Result<bool>> DeletePostAsync(long postId);
}
}

View File

@@ -1,11 +0,0 @@
using Model;
using Model.Entity;
namespace Service.Interface
{
public interface IUserService : IBaseService<UserEntity>
{
public Task<Result<UserEntity>> GetUserByUserNameAsync(string username);
}
}

View File

@@ -9,7 +9,7 @@
prism:ViewModelLocator.AutoWireViewModel="True" prism:ViewModelLocator.AutoWireViewModel="True"
d:DesignHeight="1080" d:DesignHeight="1080"
d:DesignWidth="1920"> d:DesignWidth="1920">
<Grid> <Grid Background="Green">
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -0,0 +1,44 @@
using DeviceCommand.Base;
using Microsoft.AspNetCore.Mvc;
using Model.Model;
[ApiController]
[Route("api/enova")] // 路由可以自己定,定好后把完整的 URL 提供给上位机配置
public class EnovaDataController : ControllerBase
{
/// <summary>
/// 接收下位机 POST 上报的通道数据,并联动分发到所有匹配的 EnovaDataReporter 实例(如 CTS3
/// </summary>
[HttpPost("upload-status")]
public IActionResult ReceiveChannelStatus([FromBody] List<EnovaChannelData> rawDataList)
{
// 1. 基础校验
if (rawDataList == null || rawDataList.Count == 0)
{
return Ok(new ApiResponse
{
Success = false,
ErrorInfo = "接收到的数据为空"
});
}
try
{
// 2. 联动设备:将数据分发到所有已注册的 EnovaDataReporter 实例
// (包括 CTS3 等派生类,由它们自行通过事件处理)
ApiResponse dispatchResult = EnovaDataReporter.Dispatch(rawDataList);
// 3. 严格按照文档 Page 6 的格式返回响应
return Ok(dispatchResult);
}
catch (Exception ex)
{
// 系统内部异常处理
return Ok(new ApiResponse
{
Success = false,
ErrorInfo = $"服务器内部错误: {ex.Message}"
});
}
}
}

47
WebAPI/Program.cs Normal file
View File

@@ -0,0 +1,47 @@
using System.Text.Json;
namespace WebAPI
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// ======= 核心配置开始 =======
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
options.JsonSerializerOptions.NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.AllowReadingFromString;
// 3. 属性命名策略保持原样(不强制转为驼峰)
options.JsonSerializerOptions.PropertyNamingPolicy = null;
});
// ======= 核心配置结束 =======
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
// 注意:如果上位机只支持 http 请求,现场部署时你可能需要根据实际情况注释掉下面这行 Https 重定向
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
}

View File

@@ -0,0 +1,41 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:18581",
"sslPort": 44392
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5287",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7240;http://localhost:5287",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

18
WebAPI/WebAPI.csproj Normal file
View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Model\Model.csproj" />
<ProjectReference Include="..\DeviceCommand\DeviceCommand.csproj" />
</ItemGroup>
</Project>

6
WebAPI/WebAPI.http Normal file
View File

@@ -0,0 +1,6 @@
@WebAPI_HostAddress = http://localhost:5287
GET {{WebAPI_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

9
WebAPI/appsettings.json Normal file
View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}