diff --git a/DeviceCommand/Base/EnovaDataReporter.cs b/DeviceCommand/Base/EnovaDataReporter.cs index 9fa197e..725eed8 100644 --- a/DeviceCommand/Base/EnovaDataReporter.cs +++ b/DeviceCommand/Base/EnovaDataReporter.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Text; using System.Threading; @@ -14,29 +15,72 @@ namespace DeviceCommand.Base { private readonly HttpClient _httpClient; + // ========= 静态注册表:用于 EnovaDataController 反向查找设备实例并分发数据 ========= + private static readonly List _instances = new List(); + private static readonly object _registryLock = new object(); + + /// + /// 当前已注册的所有 EnovaDataReporter 实例(线程安全快照) + /// + public static IReadOnlyList Instances + { + get + { + lock (_registryLock) + { + return _instances.ToList(); + } + } + } + + /// + /// 设备编码:用于在多设备场景下按 deviceCode 过滤分发 + /// 留空表示该实例接收所有上报数据 + /// + 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; /// - /// 构造函数注入 HttpClient(符合 Prism 依赖注入规范) + /// 收到下位机 POST 上报数据时触发 /// + public event EventHandler? ChannelDataReceived; + + public EnovaDataReporter(HttpClient httpClient) { - // 如果容器没有注入,则给个默认的单例/实例防空 _httpClient = httpClient ?? new HttpClient(); + + // 自动注册到静态实例表,便于 Controller 反向找到本实例 + lock (_registryLock) + { + _instances.Add(this); + } } - public async Task ReportChannelStateAsync(List dataList, CancellationToken ct = default) + /// + /// 从静态注册表中注销当前实例(设备销毁/释放时调用) + /// + public virtual void Unregister() + { + lock (_registryLock) + { + _instances.Remove(this); + } + } + + public async Task ReportChannelStateAsync(List dataList, CancellationToken ct = default) { if (dataList == null || dataList.Count == 0) { - return new EnovaReportResponse { Success = false, ErrorInfo = "上报数据集合为空" }; + return new ApiResponse { Success = false, ErrorInfo = "上报数据集合为空" }; } if (string.IsNullOrWhiteSpace(TargetUrl)) { - return new EnovaReportResponse { Success = false, ErrorInfo = "目标上报 URL 未配置" }; + return new ApiResponse { Success = false, ErrorInfo = "目标上报 URL 未配置" }; } try @@ -58,12 +102,12 @@ namespace DeviceCommand.Base if (response.IsSuccessStatusCode) { string responseContent = await response.Content.ReadAsStringAsync(); - var result = JsonConvert.DeserializeObject(responseContent); - return result ?? new EnovaReportResponse { Success = true }; // 防止对方返回空Body [cite: 261] + var result = JsonConvert.DeserializeObject(responseContent); + return result ?? new ApiResponse { Success = true }; // 防止对方返回空Body } else { - return new EnovaReportResponse + return new ApiResponse { Success = false, ErrorInfo = $"服务器响应错误代码: {(int)response.StatusCode} {response.ReasonPhrase}" @@ -74,8 +118,98 @@ namespace DeviceCommand.Base { // 完美承接你上位机原有的异常日志记录器逻辑 // Logger.LoggerHelper.ErrorWithNotify($"Enova3 数据上传失败: {ex.Message}"); - return new EnovaReportResponse { Success = false, ErrorInfo = $"网络异常: {ex.Message}" }; + return new ApiResponse { Success = false, ErrorInfo = $"网络异常: {ex.Message}" }; } } + + /// + /// 处理 Controller 转发过来的下位机上报数据,并触发事件 + /// + public virtual ApiResponse HandleIncomingChannelData(List 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}" }; + } + } + + /// + /// 由 Controller 调用:将下位机上报的数据广播到所有匹配的实例 + /// 当数据中含 DeviceCode 时,按 DeviceCode 精确匹配;否则广播给所有实例 + /// + public static ApiResponse Dispatch(List 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(); + 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) + }; + } } -} \ No newline at end of file +} diff --git a/DeviceCommand/Base/IEnovaDataReporter.cs b/DeviceCommand/Base/IEnovaDataReporter.cs index 15a6087..b94ce7c 100644 --- a/DeviceCommand/Base/IEnovaDataReporter.cs +++ b/DeviceCommand/Base/IEnovaDataReporter.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Model.Model; @@ -6,12 +7,28 @@ using Model.Model; namespace DeviceCommand.Base { /// - /// Enova3 上位机数据上报核心接口 + /// Enova3 通道数据接收事件参数 + /// + public class EnovaChannelDataReceivedEventArgs : EventArgs + { + public List DataList { get; } + public DateTime ReceivedTime { get; } + + public EnovaChannelDataReceivedEventArgs(List dataList) + { + DataList = dataList ?? new List(); + ReceivedTime = DateTime.Now; + } + } + + /// + /// Enova3 上位机数据上报 / 接收核心接口 + /// 既支持上位机主动推送数据到客户平台,也支持接收下位机通过 HTTP POST 上报的数据 /// public interface IEnovaDataReporter { /// - /// 客户平台接收数据的目标 HTTP URL + /// 客户平台接收数据的目标 HTTP URL(用于主动推送) /// string TargetUrl { get; set; } @@ -20,12 +37,25 @@ namespace DeviceCommand.Base /// int TimeoutMilliseconds { get; set; } + /// + /// 当 EnovaDataController 收到下位机 POST 上报的数据时触发 + /// + event EventHandler ChannelDataReceived; + /// /// 异步推送通道的实时状态数据到客户平台 /// /// 包含各通道状态的采集数据集合 /// 取消令牌 /// 平台服务器的响应状态 - Task ReportChannelStateAsync(List dataList, CancellationToken ct = default); + Task ReportChannelStateAsync(List dataList, CancellationToken ct = default); + + /// + /// 处理 EnovaDataController 转发过来的下位机上报数据 + /// 由 Controller 在收到 HTTP POST 后调用,内部会触发 事件 + /// + /// 下位机上报的通道数据集合 + /// 处理结果,将作为 HTTP 响应返回给下位机 + ApiResponse HandleIncomingChannelData(List dataList); } -} \ No newline at end of file +} diff --git a/DeviceCommand/Devices/CTS3-6-300-8IS0.cs b/DeviceCommand/Devices/CTS3-6-300-8IS0.cs index 6b8e30a..081c69c 100644 --- a/DeviceCommand/Devices/CTS3-6-300-8IS0.cs +++ b/DeviceCommand/Devices/CTS3-6-300-8IS0.cs @@ -3,15 +3,80 @@ 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 { + /// + /// CTS3-6-300-8IS0 设备:通过 EnovaDataController 接收下位机 POST 上报的通道数据 + /// public class CTS3 : EnovaDataReporter { + /// + /// 最近一次收到的通道数据快照(key = ChannelCode) + /// + public IReadOnlyDictionary LatestChannelData => _latestChannelData; + private readonly Dictionary _latestChannelData = new Dictionary(); + private readonly object _dataLock = new object(); + + /// + /// 业务侧可订阅此事件以获得更友好的回调(仅本设备数据) + /// + public event EventHandler? OnDataUpdated; + public CTS3(HttpClient httpClient) : base(httpClient) { + // 订阅基类事件:当 Controller 调用 HandleIncomingChannelData 时会触发 + ChannelDataReceived += OnChannelDataReceivedInternal; + } + + /// + /// 指定 DeviceCode 的便捷构造,便于多台 CTS3 共存时按 deviceCode 精确匹配 + /// + 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(); } } } diff --git a/DeviceCommand/Devices/Others.cs b/DeviceCommand/Devices/Others.cs new file mode 100644 index 0000000..8de9646 --- /dev/null +++ b/DeviceCommand/Devices/Others.cs @@ -0,0 +1,108 @@ +using DeviceCommand.Base; + +public class Others : ModbusTcp +{ + private readonly byte _slaveId; + + /// + /// 通道1PV + /// + private const ushort CH1_PV = 100; + + /// + /// 通道2PV + /// + private const ushort CH2_PV = 102; + + /// + /// 通道3PV + /// + private const ushort CH3_PV = 104; + + /// + /// 通道4PV + /// + 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); + } + + /// + /// 读取通道1PV + /// + public async Task ReadCH1Async(CancellationToken ct = default) + { + return await ReadRealAsync(CH1_PV, ct); + } + + /// + /// 读取通道2PV + /// + public async Task ReadCH2Async(CancellationToken ct = default) + { + return await ReadRealAsync(CH2_PV, ct); + } + + /// + /// 读取通道3PV + /// + public async Task ReadCH3Async(CancellationToken ct = default) + { + return await ReadRealAsync(CH3_PV, ct); + } + + /// + /// 读取通道4PV + /// + public async Task ReadCH4Async(CancellationToken ct = default) + { + return await ReadRealAsync(CH4_PV, ct); + } + + /// + /// 一次读取4个通道PV + /// + 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 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); + } +} \ No newline at end of file diff --git a/DeviceCommand/Devices/SDE720SH-A.cs b/DeviceCommand/Devices/SDE720SH-A.cs deleted file mode 100644 index e567702..0000000 --- a/DeviceCommand/Devices/SDE720SH-A.cs +++ /dev/null @@ -1,13 +0,0 @@ -using DeviceCommand.Base; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace DeviceCommand.Devices -{ - public class SDE720SH_A : ModbusTcp - { - } -} diff --git a/DeviceCommand/Devices/SDL710FH.cs b/DeviceCommand/Devices/SDL710FH.cs deleted file mode 100644 index e400978..0000000 --- a/DeviceCommand/Devices/SDL710FH.cs +++ /dev/null @@ -1,13 +0,0 @@ -using DeviceCommand.Base; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace DeviceCommand.Devices -{ - public class SDL710FH:ModbusTcp - { - } -} diff --git a/DeviceCommand/Devices/THC-1100-602A.cs b/DeviceCommand/Devices/THC-1100-602A.cs index 3e916ba..81b0333 100644 --- a/DeviceCommand/Devices/THC-1100-602A.cs +++ b/DeviceCommand/Devices/THC-1100-602A.cs @@ -3,12 +3,47 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace DeviceCommand.Devices { - public class THC1100:S7Device + public class THC1100 : S7Device { + // 定义 PLC 地址常量,便于后期维护 + private const string RealTimeTemperatureSetPointAddress = "DB53.DBD280"; + private const string RealTimeHumidityMeasuredValueAddress = "DB53.DBD1092"; + /// + /// 异步获取温度实时测量值 (DB53.DBD1052, 浮点型) + /// + /// 取消令牌 + /// 温度设定值 (float) + public async Task GetRealTimeTemperatureSetPointAsync(CancellationToken ct = default) + { + // S7.Net 读取浮点型时,可以直接将其强转为 float + // 内部已通过基类的 _commLock 保证线程安全 + return await ReadAsync(RealTimeTemperatureSetPointAddress, ct); + } + + /// + /// 异步获取湿度实时测量值 (DB53.DBD1092, 浮点型) + /// + /// 取消令牌 + /// 湿度测量值 (float) + public async Task GetRealTimeHumidityMeasuredValueAsync(CancellationToken ct = default) + { + return await ReadAsync(RealTimeHumidityMeasuredValueAddress, ct); + } + + /// + /// 一次性获取温湿度相关的运行数据包(选填,若需要批量展示可以使用此方法) + /// + public async Task<(float TemperatureSetPoint, float HumidityMeasuredValue)> GetThcDataPackAsync(CancellationToken ct = default) + { + var tempSet = await GetRealTimeTemperatureSetPointAsync(ct); + var humidMeasure = await GetRealTimeHumidityMeasuredValueAsync(ct); + return (tempSet, humidMeasure); + } } -} +} \ No newline at end of file diff --git a/DeviceCommand/Devices/UMC1000Rtu.cs b/DeviceCommand/Devices/UMC1000Rtu.cs new file mode 100644 index 0000000..498acd0 --- /dev/null +++ b/DeviceCommand/Devices/UMC1000Rtu.cs @@ -0,0 +1,123 @@ +using DeviceCommand.Base; +using System; +using System.IO.Ports; +using System.Threading; +using System.Threading.Tasks; + +namespace DeviceCommand.Devices +{ + /// + /// UMC1000 温湿度控制器(RS485) + /// + public class UMC1000Rtu : ModbusRtu + { + private readonly byte _slaveId; + + /// + /// 温度测量值 REAL + /// + private const ushort TemperatureAddress = 150; + + /// + /// 湿度测量值 REAL + /// + 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); + } + + /// + /// 读取温度 + /// + public async Task ReadTemperatureAsync( + CancellationToken ct = default) + { + return await ReadFloatAsync( + TemperatureAddress, + ct); + } + + /// + /// 读取湿度 + /// + public async Task ReadHumidityAsync( + CancellationToken ct = default) + { + return await ReadFloatAsync( + HumidityAddress, + ct); + } + + /// + /// 一次读取温湿度 + /// + 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 ReadFloatAsync( + ushort address, + CancellationToken ct) + { + ushort[] regs = await ReadHoldingRegistersAsync( + _slaveId, + address, + 2, + ct); + + return ToFloat(regs[0], regs[1]); + } + + /// + /// 两个寄存器转Float + /// + private static float ToFloat( + ushort highWord, + ushort lowWord) + { + byte[] bytes = + { + (byte)(highWord >> 8), + (byte)highWord, + (byte)(lowWord >> 8), + (byte)lowWord + }; + + if (BitConverter.IsLittleEndian) + Array.Reverse(bytes); + + return BitConverter.ToSingle(bytes, 0); + } + } +} \ No newline at end of file diff --git a/DeviceCommand/Devices/SDE710FH-A.cs b/DeviceCommand/Devices/UMC1300.cs similarity index 81% rename from DeviceCommand/Devices/SDE710FH-A.cs rename to DeviceCommand/Devices/UMC1300.cs index dc60f86..292d12b 100644 --- a/DeviceCommand/Devices/SDE710FH-A.cs +++ b/DeviceCommand/Devices/UMC1300.cs @@ -5,7 +5,7 @@ using System.Threading.Tasks; namespace DeviceCommand.Devices { - public class SDE710FH_A : ModbusTcp + public class UMC1300 : ModbusTcp { // 从站地址(设备 ID) private readonly byte _slaveId; @@ -27,7 +27,7 @@ namespace DeviceCommand.Devices /// 端口,默认 502 /// 发送超时(毫秒) /// 接收超时(毫秒) - public SDE710FH_A(byte slaveId, string ip, int port = 502, + public UMC1300(byte slaveId, string ip, int port = 502, int sendTimeoutMs = 3000, int receiveTimeoutMs = 3000) : base() { @@ -82,21 +82,5 @@ namespace DeviceCommand.Devices raw[3] * SCALE // 湿度 MV ); } - - // ==================== 写入 ==================== - - /// 设定温度 MV(工程值) - public async Task WriteTemperatureMVAsync(float value, CancellationToken ct = default) - { - ushort raw = (ushort)(value / SCALE); - await WriteSingleRegisterAsync(_slaveId, ADDR_TEMP_MV, raw, ct); - } - - /// 设定湿度 MV(工程值) - public async Task WriteHumidityMVAsync(float value, CancellationToken ct = default) - { - ushort raw = (ushort)(value / SCALE); - await WriteSingleRegisterAsync(_slaveId, ADDR_HUMID_MV, raw, ct); - } } } \ No newline at end of file diff --git a/IOT.sln b/IOT.sln index 18ba7a8..1f279c3 100644 --- a/IOT.sln +++ b/IOT.sln @@ -29,6 +29,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LOT", "LOT\LOT.csproj", "{0 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 GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -83,6 +85,10 @@ Global {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 GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Model/Model/EnovaChannelData.cs b/Model/Model/EnovaChannelData.cs new file mode 100644 index 0000000..0b9fc29 --- /dev/null +++ b/Model/Model/EnovaChannelData.cs @@ -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 OtherSignals { get; set; } + } + + // 严格按照文档 Page 6 要求返回的响应结构 + public class ApiResponse + { + public bool Success { get; set; } + public string ErrorInfo { get; set; } + } +} diff --git a/Model/Model/EnovaChannelReportData.cs b/Model/Model/EnovaChannelReportData.cs deleted file mode 100644 index 92962de..0000000 --- a/Model/Model/EnovaChannelReportData.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Model.Model -{ - /// - /// Enova3 通道状态上报实体(单条通道数据) - /// - public class EnovaChannelReportData - { - public string laboratory { get; set; } = string.Empty; // 实验室名称 - public string manufacture { get; set; } = string.Empty; // 供应商名称 - public string taskId { get; set; } = string.Empty; // 任务ID - public string devCode { get; set; } = string.Empty; // 台架编码(注:表格里是devCode,示例里是deviceCode,建议互补或以表格为准) - public string deviceCode { get; set; } = string.Empty; // 兼容示例中的 deviceCode - public string chnCode { get; set; } = string.Empty; // 通道编码 - public string channelCode { get; set; } = string.Empty; // 兼容示例中的 channelCode - public string chnlP { get; set; } = string.Empty; // 通道IP - public string channelIP { get; set; } = string.Empty; // 兼容示例中的 channelIP - public string pclP { get; set; } = string.Empty; // 上位机IP - public string pcIP { get; set; } = string.Empty; // 兼容示例中的 pcIP - public string pcName { get; set; } = string.Empty; // 上位机名称 - public string barCode { get; set; } = string.Empty; // 样品条码 - public string projectName { get; set; } = string.Empty; // 项目名称 - public string stepId { get; set; } = string.Empty; // 工步行 - public string cycleDepth { get; set; } = string.Empty; // 循环层 - public string cycles { get; set; } = string.Empty; // 循环次数 - public string stepTime { get; set; } = string.Empty; // 工步时间 - public string totalTime { get; set; } = string.Empty; // 运行时间 - public string powerState { get; set; } = string.Empty; // 电源状态 - public string channelState { get; set; } = string.Empty; // 通道状态 - public string voltage { get; set; } = string.Empty; // 电压 - public string current { get; set; } = string.Empty; // 电流 - public string power { get; set; } = string.Empty; // 功率 - public string givenVoltage { get; set; } = string.Empty; // 给定电压 - public string givenCurrent { get; set; } = string.Empty; // 给定电流 - public string givenPower { get; set; } = string.Empty; // 给定功率 - public string cap { get; set; } = string.Empty; // 容量 - public string chgCap { get; set; } = string.Empty; // 充电容量 - public string dchgCap { get; set; } = string.Empty; // 放电容量 - public string capDiff { get; set; } = string.Empty; // 容量差 - public string energy { get; set; } = string.Empty; // 能量 - public string chgEnergy { get; set; } = string.Empty; // 充电能量 - public string dchgEnergy { get; set; } = string.Empty; // 放电能量 - public string energyDiff { get; set; } = string.Empty; // 能量差 - public string timeStamp { get; set; } = string.Empty; // 采集时间(如:2024-07-11 13:03:03) - public string timestamp { get; set; } = string.Empty; // 兼容小写格式 - - // 强类型:其他信号集合(支持动态 KV 对) - public Dictionary otherSignals { get; set; } = new(); - } - - /// - /// 客户平台统一返回接收格式 - /// - public class EnovaReportResponse - { - public bool Success { get; set; } - public string ErrorInfo { get; set; } = string.Empty; - } -} \ No newline at end of file diff --git a/WebAPI/Controllers/EnovaDataController.cs b/WebAPI/Controllers/EnovaDataController.cs new file mode 100644 index 0000000..b2f905e --- /dev/null +++ b/WebAPI/Controllers/EnovaDataController.cs @@ -0,0 +1,44 @@ +using DeviceCommand.Base; +using Microsoft.AspNetCore.Mvc; +using Model.Model; + +[ApiController] +[Route("api/enova")] // 路由可以自己定,定好后把完整的 URL 提供给上位机配置 +public class EnovaDataController : ControllerBase +{ + /// + /// 接收下位机 POST 上报的通道数据,并联动分发到所有匹配的 EnovaDataReporter 实例(如 CTS3) + /// + [HttpPost("upload-status")] + public IActionResult ReceiveChannelStatus([FromBody] List 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}" + }); + } + } +} diff --git a/WebAPI/Program.cs b/WebAPI/Program.cs new file mode 100644 index 0000000..ed915ef --- /dev/null +++ b/WebAPI/Program.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/WebAPI/Properties/launchSettings.json b/WebAPI/Properties/launchSettings.json new file mode 100644 index 0000000..0c6d109 --- /dev/null +++ b/WebAPI/Properties/launchSettings.json @@ -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" + } + } + } +} diff --git a/WebAPI/WebAPI.csproj b/WebAPI/WebAPI.csproj new file mode 100644 index 0000000..b57eda5 --- /dev/null +++ b/WebAPI/WebAPI.csproj @@ -0,0 +1,18 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + diff --git a/WebAPI/WebAPI.http b/WebAPI/WebAPI.http new file mode 100644 index 0000000..40ba726 --- /dev/null +++ b/WebAPI/WebAPI.http @@ -0,0 +1,6 @@ +@WebAPI_HostAddress = http://localhost:5287 + +GET {{WebAPI_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/WebAPI/appsettings.Development.json b/WebAPI/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/WebAPI/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/WebAPI/appsettings.json b/WebAPI/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/WebAPI/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}