设备驱动修改

This commit is contained in:
“hsc”
2026-06-10 10:45:51 +08:00
parent b0a7742b8f
commit 5d14afcb66
19 changed files with 764 additions and 122 deletions

View File

@@ -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<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>
/// 构造函数注入 HttpClient符合 Prism 依赖注入规范)
/// 收到下位机 POST 上报数据时触发
/// </summary>
public event EventHandler<EnovaChannelDataReceivedEventArgs>? ChannelDataReceived;
public EnovaDataReporter(HttpClient httpClient)
{
// 如果容器没有注入,则给个默认的单例/实例防空
_httpClient = httpClient ?? new HttpClient();
// 自动注册到静态实例表,便于 Controller 反向找到本实例
lock (_registryLock)
{
_instances.Add(this);
}
}
public async Task<EnovaReportResponse> ReportChannelStateAsync(List<EnovaChannelReportData> dataList, CancellationToken ct = default)
/// <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 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<EnovaReportResponse>(responseContent);
return result ?? new EnovaReportResponse { Success = true }; // 防止对方返回空Body [cite: 261]
var result = JsonConvert.DeserializeObject<ApiResponse>(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}" };
}
}
/// <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

@@ -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
{
/// <summary>
/// Enova3 上位机数据上报核心接口
/// 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
/// 客户平台接收数据的目标 HTTP URL(用于主动推送)
/// </summary>
string TargetUrl { get; set; }
@@ -20,12 +37,25 @@ namespace DeviceCommand.Base
/// </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<EnovaReportResponse> ReportChannelStateAsync(List<EnovaChannelReportData> dataList, CancellationToken ct = default);
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

@@ -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
{
/// <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

@@ -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
{
}
}

View File

@@ -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
{
}
}

View File

@@ -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";
/// <summary>
/// 异步获取温度实时测量值 (DB53.DBD1052, 浮点型)
/// </summary>
/// <param name="ct">取消令牌</param>
/// <returns>温度设定值 (float)</returns>
public async Task<float> GetRealTimeTemperatureSetPointAsync(CancellationToken ct = default)
{
// S7.Net 读取浮点型时,可以直接将其强转为 float
// 内部已通过基类的 _commLock 保证线程安全
return await ReadAsync<float>(RealTimeTemperatureSetPointAddress, ct);
}
/// <summary>
/// 异步获取湿度实时测量值 (DB53.DBD1092, 浮点型)
/// </summary>
/// <param name="ct">取消令牌</param>
/// <returns>湿度测量值 (float)</returns>
public async Task<float> GetRealTimeHumidityMeasuredValueAsync(CancellationToken ct = default)
{
return await ReadAsync<float>(RealTimeHumidityMeasuredValueAddress, ct);
}
/// <summary>
/// 一次性获取温湿度相关的运行数据包(选填,若需要批量展示可以使用此方法)
/// </summary>
public async Task<(float TemperatureSetPoint, float HumidityMeasuredValue)> GetThcDataPackAsync(CancellationToken ct = default)
{
var tempSet = await GetRealTimeTemperatureSetPointAsync(ct);
var humidMeasure = await GetRealTimeHumidityMeasuredValueAsync(ct);
return (tempSet, humidMeasure);
}
}
}

View File

@@ -0,0 +1,123 @@
using DeviceCommand.Base;
using System;
using System.IO.Ports;
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)
{
return await ReadFloatAsync(
TemperatureAddress,
ct);
}
/// <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
/// </summary>
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);
}
}
}

View File

@@ -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
/// <param name="port">端口,默认 502</param>
/// <param name="sendTimeoutMs">发送超时(毫秒)</param>
/// <param name="receiveTimeoutMs">接收超时(毫秒)</param>
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
);
}
// ==================== 写入 ====================
/// <summary>设定温度 MV工程值</summary>
public async Task WriteTemperatureMVAsync(float value, CancellationToken ct = default)
{
ushort raw = (ushort)(value / SCALE);
await WriteSingleRegisterAsync(_slaveId, ADDR_TEMP_MV, raw, ct);
}
/// <summary>设定湿度 MV工程值</summary>
public async Task WriteHumidityMVAsync(float value, CancellationToken ct = default)
{
ushort raw = (ushort)(value / SCALE);
await WriteSingleRegisterAsync(_slaveId, ADDR_HUMID_MV, raw, ct);
}
}
}

View File

@@ -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

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,61 +0,0 @@
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

@@ -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": "*"
}