Compare commits

...

5 Commits

Author SHA1 Message Date
“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
41 changed files with 1333 additions and 333 deletions

2
.gitignore vendored
View File

@@ -343,7 +343,7 @@ ASALocalRun/
# NVidia Nsight GPU debugger configuration file
*.nvuser
![Dd]ebug/[Mm]odules/
# MFractors (Xamarin productivity tool) working folder
.mfractor/

View File

@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Model.Model;
using Newtonsoft.Json;
namespace DeviceCommand.Base
{
public class EnovaDataReporter : IEnovaDataReporter
{
private readonly HttpClient _httpClient;
// 显式实现/自动属性,方便外部随时更新配置
public string TargetUrl { get; set; } = "http://127.0.0.1:8080/api/channel/state";
public int TimeoutMilliseconds { get; set; } = 5000;
/// <summary>
/// 构造函数注入 HttpClient符合 Prism 依赖注入规范)
/// </summary>
public EnovaDataReporter(HttpClient httpClient)
{
// 如果容器没有注入,则给个默认的单例/实例防空
_httpClient = httpClient ?? new HttpClient();
}
public async Task<EnovaReportResponse> ReportChannelStateAsync(List<EnovaChannelReportData> dataList, CancellationToken ct = default)
{
if (dataList == null || dataList.Count == 0)
{
return new EnovaReportResponse { Success = false, ErrorInfo = "上报数据集合为空" };
}
if (string.IsNullOrWhiteSpace(TargetUrl))
{
return new EnovaReportResponse { Success = false, ErrorInfo = "目标上报 URL 未配置" };
}
try
{
// 1. 序列化为标准 JSON 字符串
string jsonPayload = JsonConvert.SerializeObject(dataList);
var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
// 2. 绑定联动超时控制
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
if (TimeoutMilliseconds > 0)
{
cts.CancelAfter(TimeoutMilliseconds);
}
HttpResponseMessage response = await _httpClient.PostAsync(TargetUrl, content, cts.Token);
// 4. 解析返回值
if (response.IsSuccessStatusCode)
{
string responseContent = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<EnovaReportResponse>(responseContent);
return result ?? new EnovaReportResponse { Success = true }; // 防止对方返回空Body [cite: 261]
}
else
{
return new EnovaReportResponse
{
Success = false,
ErrorInfo = $"服务器响应错误代码: {(int)response.StatusCode} {response.ReasonPhrase}"
};
}
}
catch (Exception ex)
{
// 完美承接你上位机原有的异常日志记录器逻辑
// Logger.LoggerHelper.ErrorWithNotify($"Enova3 数据上传失败: {ex.Message}");
return new EnovaReportResponse { Success = false, ErrorInfo = $"网络异常: {ex.Message}" };
}
}
}
}

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,31 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Model.Model;
namespace DeviceCommand.Base
{
/// <summary>
/// Enova3 上位机数据上报核心接口
/// </summary>
public interface IEnovaDataReporter
{
/// <summary>
/// 客户平台接收数据的目标 HTTP URL
/// </summary>
string TargetUrl { get; set; }
/// <summary>
/// HTTP 请求超时时间(毫秒)
/// </summary>
int TimeoutMilliseconds { get; set; }
/// <summary>
/// 异步推送通道的实时状态数据到客户平台
/// </summary>
/// <param name="dataList">包含各通道状态的采集数据集合</param>
/// <param name="ct">取消令牌</param>
/// <returns>平台服务器的响应状态</returns>
Task<EnovaReportResponse> ReportChannelStateAsync(List<EnovaChannelReportData> dataList, CancellationToken ct = default);
}
}

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,158 @@
using NModbus;
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace DeviceCommand.Base
{
public class ModbusTcp : IModbusDevice
{
public string IPAddress { get; private set; } = "127.0.0.1";
public int Port { get; private set; } = 502;
public int SendTimeout { get; private set; } = 3000;
public int ReceiveTimeout { get; private set; } = 3000;
private TcpClient _tcpClient;
public IModbusMaster Modbus { get; private set; }
public bool IsConnected => _tcpClient?.Connected ?? false;
protected readonly SemaphoreSlim _commLock = new(1, 1);
public ModbusTcp()
{
_tcpClient = new TcpClient();
}
public void ConfigureDevice(string ipAddress, int port, int sendTimeout = 3000, int receiveTimeout = 3000)
{
IPAddress = ipAddress;
Port = port;
SendTimeout = sendTimeout;
ReceiveTimeout = receiveTimeout;
}
public virtual async Task<bool> ConnectAsync(CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
if (_tcpClient.Connected)
{
var remoteEndPoint = (IPEndPoint)_tcpClient.Client.RemoteEndPoint!;
if (remoteEndPoint.Address.MapToIPv4().ToString() == IPAddress && remoteEndPoint.Port == Port)
return true;
}
_tcpClient.Close();
_tcpClient.Dispose();
_tcpClient = new TcpClient();
await _tcpClient.ConnectAsync(IPAddress, Port, ct);
Modbus = new ModbusFactory().CreateMaster(_tcpClient);
return true;
}
finally
{
_commLock.Release();
}
}
public virtual void Close()
{
if (_tcpClient.Connected) _tcpClient.Close();
}
public async Task WriteSingleRegisterAsync(byte slaveAddress, ushort registerAddress, ushort value, CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
// 修复FromMinutes 改为 FromMilliseconds
await Modbus.WriteSingleRegisterAsync(slaveAddress, registerAddress, value)
.WaitAsync(TimeSpan.FromMilliseconds(SendTimeout), ct);
}
finally
{
_commLock.Release();
}
}
public async Task WriteMultipleRegistersAsync(byte slaveAddress, ushort startAddress, ushort[] values, CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
await Modbus.WriteMultipleRegistersAsync(slaveAddress, startAddress, values)
.WaitAsync(TimeSpan.FromMilliseconds(SendTimeout), ct);
}
finally
{
_commLock.Release();
}
}
public async Task<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,169 @@
using S7.Net;
using System;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
namespace DeviceCommand.Base
{
public class S7Device : IS7Device
{
// 保持和你一致的连接参数命名属性
public string IPAddress { get; private set; } = "127.0.0.1";
public CpuType CpuType { get; private set; } = CpuType.S71200;
public short Rack { get; private set; } = 0;
public short Slot { get; private set; } = 1;
public int SendTimeout { get; private set; } = 3000;
public int ReceiveTimeout { get; private set; } = 3000;
private Plc _plc;
public Plc PlcContext => _plc;
// S7.Net 的 Plc.IsConnected 属性内部会通过 Socket 状态进行判断
public bool IsConnected => _plc?.IsConnected ?? false;
// 统一线程锁
protected readonly SemaphoreSlim _commLock = new(1, 1);
public S7Device()
{
// 初始化默认配置
_plc = new Plc(CpuType, IPAddress, Rack, Slot);
}
/// <summary>
/// 设备参数配置(符合你的命名风格)
/// </summary>
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
{
// 如果已经连接,检查当前的 IP 和 CPU 类型是否一致,一致则直接复用
if (_plc != null && _plc.IsConnected)
{
if (_plc.IP == IPAddress && _plc.CPU == CpuType && _plc.Rack == Rack && _plc.Slot == Slot)
return true;
}
// 修复:释放并彻底清空旧连接实例
if (_plc != null)
{
_plc.Close();
}
// 重新实例化 Plc 对象并配置超时
_plc = new Plc(CpuType, IPAddress, Rack, Slot)
{
ReadTimeout = ReceiveTimeout,
WriteTimeout = SendTimeout
};
// 部分版本 S7.Net 的 OpenAsync 本身不接受 CancellationToken我们通过 WaitAsync 实现超时
await _plc.OpenAsync().WaitAsync(TimeSpan.FromMilliseconds(SendTimeout), ct);
return _plc.IsConnected;
}
catch
{
return false;
}
finally
{
_commLock.Release();
}
}
public virtual void Close()
{
if (_plc != null && _plc.IsConnected)
_plc.Close();
}
public async Task WriteAsync(string address, object value, CancellationToken ct = default)
{
await _commLock.WaitAsync(ct);
try
{
if (!IsConnected) throw new InvalidOperationException("PLC未连接。");
await _plc.WriteAsync(address, value)
.WaitAsync(TimeSpan.FromMilliseconds(SendTimeout), ct);
}
finally
{
_commLock.Release();
}
}
public async Task<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);
return (T)result;
}
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,24 @@
<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>
<ItemGroup>
<Folder Include="Devices\" />
</ItemGroup>
</Project>

View File

@@ -13,8 +13,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Service", "Service\Service.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LAEPS", "LAEPS\LAEPS.csproj", "{5EC9A233-D154-4B77-6911-063269A883E9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Module", "Module", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoginModule", "LoginModule\LoginModule.csproj", "{F79AC87E-7A5A-486F-BE6C-51E81CA569E4}"
@@ -27,6 +25,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UpdateInfoModule", "UpdateI
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MainModule", "MainModule\MainModule.csproj", "{715852A3-D2DE-4C2E-AEF2-2BC0ADBEAC0A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LOT", "LOT\LOT.csproj", "{01E01684-DDE8-4B00-9BFC-2C5CDB2A261F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DeviceCommand", "DeviceCommand\DeviceCommand.csproj", "{2F035F70-5F1D-4C22-B4F0-1AEA0ED127A6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -53,10 +55,6 @@ Global
{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C769E6C6-55E9-40C3-A611-9EFAB101BE6A}.Release|Any CPU.Build.0 = Release|Any CPU
{5EC9A233-D154-4B77-6911-063269A883E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5EC9A233-D154-4B77-6911-063269A883E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5EC9A233-D154-4B77-6911-063269A883E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5EC9A233-D154-4B77-6911-063269A883E9}.Release|Any CPU.Build.0 = Release|Any CPU
{F79AC87E-7A5A-486F-BE6C-51E81CA569E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F79AC87E-7A5A-486F-BE6C-51E81CA569E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F79AC87E-7A5A-486F-BE6C-51E81CA569E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -77,6 +75,14 @@ Global
{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.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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
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: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:prism="http://prismlibrary.com/">
<Application.Resources>

View File

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

View File

@@ -40,6 +40,7 @@
<ItemGroup>
<ProjectReference Include="..\Common\Common.csproj" />
<ProjectReference Include="..\DeviceCommand\DeviceCommand.csproj" />
<ProjectReference Include="..\Logger\Logger.csproj" />
<ProjectReference Include="..\LoginModule\LoginModule.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 System.Windows.Input;
namespace LAEPS.ViewModels.Dialogs
namespace LOT.ViewModels.Dialogs
{
public class MessageBoxViewModel : DialogViewModelBase
{

View File

@@ -1,4 +1,4 @@
using LAEPS.Views;
using LOT.Views;
using MaterialDesignThemes.Wpf;
using Notifications.Wpf.Core;
using Prism.Events;
@@ -13,7 +13,7 @@ using System.Windows;
using System.Windows.Input;
using UIShare.PubEvent;
namespace LAEPS.ViewModels
namespace LOT.ViewModels
{
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: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:local="clr-namespace:LAEPS.Views.Dialogs"
xmlns:local="clr-namespace:LOT.Views.Dialogs"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
mc:Ignorable="d"
xmlns:prism="http://prismlibrary.com/"

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Path = System.IO.Path;
namespace LAEPS.Views
namespace LOT.Views
{
/// <summary>
/// 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:x="http://schemas.microsoft.com/winfx/2006/xaml"
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.Shapes;
namespace LAEPS.Views
namespace LOT.Views
{
/// <summary>
/// ShellView.xaml 的交互逻辑

View File

@@ -1,25 +1,112 @@
using Model.Entity;
using Prism.Commands;
using Prism.Ioc;
using Service.Interface;
using System.Collections.ObjectModel;
using System.Windows.Input;
using UIShare.ViewModelBase;
namespace MainModule.ViewModels
{
public class MainViewModel : NavigateViewModelBase
{
#region
public ObservableCollection<ChamberMonitorItem> Chambers { get; } = new();
private IContainerProvider _containerProvider;
#endregion
public MainViewModel(IContainerProvider containerProvider ) : base(containerProvider)
public MainViewModel(IContainerProvider containerProvider) : base(containerProvider)
{
_containerProvider=containerProvider;
_containerProvider = containerProvider;
// 触发配置与绑定初始化
ConfigureAndBindChambers();
}
/// <summary>
/// 核心配置与数据绑定逻辑
/// </summary>
private void ConfigureAndBindChambers()
{
// 实例 1Modbus TCP 环境箱配置
Chambers.Add(new ChamberMonitorItem
{
Id = 1,
Name = "环境箱 01",
ProtocolType = "Modbus TCP",
IsConnected = false,
Temperature = 0.0,
Humidity = 0.0
});
// 实例 2Modbus TCP B+ 环境箱配置
Chambers.Add(new ChamberMonitorItem
{
Id = 2,
Name = "环境箱 02",
ProtocolType = "Modbus TCP B+",
IsConnected = false,
Temperature = 0.0,
Humidity = 0.0
});
// 实例 3西门子 S7 环境箱配置
Chambers.Add(new ChamberMonitorItem
{
Id = 3,
Name = "环境箱 03",
ProtocolType = "S7",
IsConnected = false,
Temperature = 0.0,
Humidity = 0.0
});
// 实例 4HTTP 环境箱配置
Chambers.Add(new ChamberMonitorItem
{
Id = 4,
Name = "环境箱 04",
ProtocolType = "HTTP",
IsConnected = false,
Temperature = 0.0,
Humidity = 0.0
});
}
}
}
/// <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 => SetProperty(ref _isConnected, value);
}
}
}

View File

@@ -14,6 +14,91 @@
<UserControl.Resources>
<converters:BooleanToVisibilityConverter x:Key="BoolToVis" />
</UserControl.Resources>
</UserControl>
<Grid Background="#F4F6F9">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="环境箱监控状态面板" FontSize="20" FontWeight="Bold" Foreground="#2C3E50" Margin="12,4,0,16"/>
<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="*"/>
</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>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</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>
<ItemGroup>
<PackageReference Include="SqlSugarCore" Version="5.1.4.210" />
<Compile Remove="DTOS\**" />
<EmbeddedResource Remove="DTOS\**" />
<None Remove="DTOS\**" />
</ItemGroup>
<ItemGroup>
<Folder Include="DTOS\" />
<PackageReference Include="SqlSugarCore" Version="5.1.4.210" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
namespace Model.Model
{
/// <summary>
/// Enova3 通道状态上报实体(单条通道数据)
/// </summary>
public class EnovaChannelReportData
{
public string laboratory { get; set; } = string.Empty; // 实验室名称
public string manufacture { get; set; } = string.Empty; // 供应商名称
public string taskId { get; set; } = string.Empty; // 任务ID
public string devCode { get; set; } = string.Empty; // 台架编码表格里是devCode示例里是deviceCode建议互补或以表格为准
public string deviceCode { get; set; } = string.Empty; // 兼容示例中的 deviceCode
public string chnCode { get; set; } = string.Empty; // 通道编码
public string channelCode { get; set; } = string.Empty; // 兼容示例中的 channelCode
public string chnlP { get; set; } = string.Empty; // 通道IP
public string channelIP { get; set; } = string.Empty; // 兼容示例中的 channelIP
public string pclP { get; set; } = string.Empty; // 上位机IP
public string pcIP { get; set; } = string.Empty; // 兼容示例中的 pcIP
public string pcName { get; set; } = string.Empty; // 上位机名称
public string barCode { get; set; } = string.Empty; // 样品条码
public string projectName { get; set; } = string.Empty; // 项目名称
public string stepId { get; set; } = string.Empty; // 工步行
public string cycleDepth { get; set; } = string.Empty; // 循环层
public string cycles { get; set; } = string.Empty; // 循环次数
public string stepTime { get; set; } = string.Empty; // 工步时间
public string totalTime { get; set; } = string.Empty; // 运行时间
public string powerState { get; set; } = string.Empty; // 电源状态
public string channelState { get; set; } = string.Empty; // 通道状态
public string voltage { get; set; } = string.Empty; // 电压
public string current { get; set; } = string.Empty; // 电流
public string power { get; set; } = string.Empty; // 功率
public string givenVoltage { get; set; } = string.Empty; // 给定电压
public string givenCurrent { get; set; } = string.Empty; // 给定电流
public string givenPower { get; set; } = string.Empty; // 给定功率
public string cap { get; set; } = string.Empty; // 容量
public string chgCap { get; set; } = string.Empty; // 充电容量
public string dchgCap { get; set; } = string.Empty; // 放电容量
public string capDiff { get; set; } = string.Empty; // 容量差
public string energy { get; set; } = string.Empty; // 能量
public string chgEnergy { get; set; } = string.Empty; // 充电能量
public string dchgEnergy { get; set; } = string.Empty; // 放电能量
public string energyDiff { get; set; } = string.Empty; // 能量差
public string timeStamp { get; set; } = string.Empty; // 采集时间2024-07-11 13:03:03
public string timestamp { get; set; } = string.Empty; // 兼容小写格式
// 强类型:其他信号集合(支持动态 KV 对)
public Dictionary<string, string> otherSignals { get; set; } = new();
}
/// <summary>
/// 客户平台统一返回接收格式
/// </summary>
public class EnovaReportResponse
{
public bool Success { get; set; }
public string ErrorInfo { get; set; } = string.Empty;
}
}

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"
d:DesignHeight="1080"
d:DesignWidth="1920">
<Grid>
<Grid Background="Green">
</Grid>
</UserControl>