Files
Cardiopulmonarybypasssystems/Cardiopulmonarybypasssystems/Services/ModbusTelemetryService.cs
GukSang.Jin c28eca82b6 更新
2026-04-20 16:27:25 +08:00

786 lines
28 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Globalization;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Cardiopulmonarybypasssystems.Models;
using NModbus;
namespace Cardiopulmonarybypasssystems.Services;
public sealed class ModbusTelemetryService : IModbusTelemetryService, IDisposable
{
private const string DefaultIpAddress = "192.168.1.10";
private const int DefaultPort = 502;
private const byte DefaultSlaveId = 1;
private const ushort ProximalPressureRegister = 1330;
private const ushort DistalPressureRegister = 1380;
private const double FlowRegisterScale = 0.01d;
private const double KpaToMmHg = 7.50061683d;
private const ushort PressureRegisterBlockLength = (ushort)(DistalPressureRegister - ProximalPressureRegister + 2);
private static readonly TimeSpan ConnectionAttemptTimeout = TimeSpan.FromMilliseconds(300);
private static readonly TimeSpan ConnectionRetryInterval = TimeSpan.FromSeconds(5);
private static readonly IReadOnlyDictionary<string, ushort> FlowRegisters = new Dictionary<string, ushort>(StringComparer.Ordinal)
{
["PressureDropPump"] = 1000,
["RecirculationMainPump"] = 1010,
["RecirculationReturnPump"] = 1020,
["RecirculationDrainagePump"] = 1030,
["KinkResistancePump"] = 1040,
["HemolysisDrainageSinglePump"] = 1050,
["HemolysisReturnSinglePump"] = 1060,
["HemolysisDualLumenPump"] = 1070
};
private static readonly IReadOnlyDictionary<string, string> FlowChannelNames = new Dictionary<string, string>(StringComparer.Ordinal)
{
["PressureDropPump"] = "主泵流量",
["RecirculationMainPump"] = "再循环主泵流量",
["RecirculationReturnPump"] = "动脉回输流量",
["RecirculationDrainagePump"] = "静脉引流流量",
["KinkResistancePump"] = "抗扭结主泵流量",
["HemolysisDrainageSinglePump"] = "血细胞破坏-单腔引流/回输流量",
["HemolysisReturnSinglePump"] = "双腔插管试验回路流量",
["HemolysisDualLumenPump"] = "双腔插管试验回路流量(两个管腔)"
};
private readonly object _syncRoot = new();
private readonly ModbusFactory _factory = new();
private readonly string _ipAddress;
private readonly int _port;
private readonly byte _slaveId;
private readonly List<DeviceChannel> _channels =
[
new() { Name = "主泵流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 },
new() { Name = "再循环主泵流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 },
new() { Name = "动脉回输流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 },
new() { Name = "静脉引流流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 },
new() { Name = "抗扭结主泵流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 },
new() { Name = "血细胞破坏-单腔引流/回输流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 },
new() { Name = "双腔插管试验回路流量", Unit = "L/min", Value = 0, Min = 0, Max = 7 },
new() { Name = "双腔插管试验回路流量(两个管腔)", Unit = "L/min", Value = 0, Min = 0, Max = 7 },
new() { Name = "远端压力", Unit = "mmHg", Value = 0, Min = 40, Max = 180 },
new() { Name = "近端压力", Unit = "mmHg", Value = 0, Min = 60, Max = 220 },
new() { Name = "负压辅助引流", Unit = "kPa", Value = 0, Min = -20, Max = 0 },
new() { Name = "模拟血液温度", Unit = "°C", Value = 0, Min = 34, Max = 40 },
new() { Name = "再循环率", Unit = "%", Value = 0, Min = 0, Max = 20 },
new() { Name = "游离血红蛋白", Unit = "g/L", Value = 0, Min = 0, Max = 0.08 },
new() { Name = "白细胞减少率", Unit = "%", Value = 0, Min = 0, Max = 20 }
];
private readonly List<PumpControlChannel> _pumpControls =
[
new() { Key = "NegativeAssistPump", Name = "负压泵", StartAddress = 0 },
new() { Key = "PressureDropPump", Name = "压力降/抗塌陷泵", StartAddress = 1, FlowAddress = FlowRegisters["PressureDropPump"] },
new() { Key = "RecirculationMainPump", Name = "再循环主泵", StartAddress = 2, FlowAddress = FlowRegisters["RecirculationMainPump"] },
new() { Key = "RecirculationReturnPump", Name = "回流泵", StartAddress = 3, FlowAddress = FlowRegisters["RecirculationReturnPump"] },
new() { Key = "RecirculationDrainagePump", Name = "引流泵", StartAddress = 4, FlowAddress = FlowRegisters["RecirculationDrainagePump"] },
new() { Key = "KinkResistancePump", Name = "抗扭结泵", StartAddress = 5, FlowAddress = FlowRegisters["KinkResistancePump"] },
new() { Key = "HemolysisDrainageSinglePump", Name = "血细胞破坏-单腔引流/回输泵", StartAddress = 6, FlowAddress = FlowRegisters["HemolysisDrainageSinglePump"] },
new() { Key = "HemolysisReturnSinglePump", Name = "双腔插管试验回路泵", StartAddress = 7, FlowAddress = FlowRegisters["HemolysisReturnSinglePump"] },
new() { Key = "HemolysisDualLumenPump", Name = "双腔插管试验回路泵(两个管腔)", StartAddress = 8, FlowAddress = FlowRegisters["HemolysisDualLumenPump"] }
];
private readonly List<ValveControlChannel> _valveControls =
[
new() { Key = "TestLoopValve1", Name = "测试回路阀 1", StartAddress = 10 },
new() { Key = "TestLoopValve2", Name = "测试回路阀 2", StartAddress = 11 },
new() { Key = "CirculatingWaterTemperature", Name = "循环水温", StartAddress = 12 }
];
private TcpClient? _tcpClient;
private IModbusMaster? _master;
private Task? _connectionTask;
private DateTime _nextConnectionAttemptUtc = DateTime.MinValue;
private DateTime? _lastSuccessfulReadAt;
private double? _proximalPressureRawKpa;
private double? _distalPressureRawKpa;
private double? _proximalPressureDecodedKpa;
private double? _distalPressureDecodedKpa;
private string _lastErrorMessage = "等待首次连接";
public ModbusTelemetryService()
{
_ipAddress = Environment.GetEnvironmentVariable("CPB_MODBUS_IP") ?? DefaultIpAddress;
_port = ParseIntSetting("CPB_MODBUS_PORT", DefaultPort);
_slaveId = ParseByteSetting("CPB_MODBUS_SLAVE_ID", DefaultSlaveId);
ApplyUnavailableDeviceState();
}
private int HighestConfiguredCoilAddress => Math.Max(
_pumpControls.Max(item => item.StartAddress),
_valveControls.Max(item => item.StartAddress));
public bool IsLiveConnected
{
get
{
lock (_syncRoot)
{
return _master is not null && _tcpClient?.Connected == true;
}
}
}
public string EndpointDescription => $"{_ipAddress}:{_port} / Slave {_slaveId}";
public DateTime? LastSuccessfulReadAt
{
get
{
lock (_syncRoot)
{
return _lastSuccessfulReadAt;
}
}
}
public string LastErrorMessage
{
get
{
lock (_syncRoot)
{
return _lastErrorMessage;
}
}
}
public IReadOnlyList<DeviceChannel> GetChannels()
{
EnsureConnectionScheduled();
return _channels;
}
public IReadOnlyList<PumpControlChannel> GetPumpControls()
{
EnsureConnectionScheduled();
return _pumpControls;
}
public IReadOnlyList<ValveControlChannel> GetValveControls()
{
EnsureConnectionScheduled();
return _valveControls;
}
public TelemetryUpdateSnapshot UpdateChannels()
{
EnsureConnectionScheduled();
lock (_syncRoot)
{
var liveReadSucceeded = TryReadPumpStatesAndFlows(out var failedFlowRegisterCount);
var pressureReadSucceeded = TryReadPressureChannels(liveReadSucceeded);
SyncDerivedChannels();
if (liveReadSucceeded && pressureReadSucceeded && _master is not null)
{
_lastSuccessfulReadAt = DateTime.Now;
_lastErrorMessage = failedFlowRegisterCount > 0
? $"实时数据已更新,但有 {failedFlowRegisterCount} 路流量寄存器无数据"
: "实时数据正常";
}
return BuildSnapshot(BuildAlarms());
}
}
public ushort? ReadHoldingRegister(ushort address)
{
EnsureConnectionReadyForDirectAccess();
lock (_syncRoot)
{
if (_master is null)
{
return null;
}
try
{
return _master.ReadHoldingRegisters(_slaveId, address, 1)[0];
}
catch (Exception ex)
{
HandleConnectionFailure($"读取寄存器 D{address} 失败:{ex.Message}");
return null;
}
}
}
public float? ReadHoldingFloatRegister(ushort address)
{
EnsureConnectionReadyForDirectAccess();
lock (_syncRoot)
{
if (_master is null)
{
return null;
}
try
{
var registers = _master.ReadHoldingRegisters(_slaveId, address, 2);
return address is ProximalPressureRegister or DistalPressureRegister
? (float)ConvertRegistersToPressureKpa(registers[0], registers[1])
: DecodeFloat(registers[0], registers[1], lowWordFirst: true);
}
catch (Exception ex)
{
HandleConnectionFailure($"读取浮点寄存器 D{address} 失败:{ex.Message}");
return null;
}
}
}
public bool WriteHoldingRegister(ushort address, ushort value)
{
EnsureConnectionReadyForDirectAccess();
lock (_syncRoot)
{
if (_master is null)
{
_lastErrorMessage = $"PLC 离线未执行寄存器写入D{address}";
return false;
}
try
{
_master.WriteSingleRegister(_slaveId, address, value);
return true;
}
catch (Exception ex)
{
HandleConnectionFailure($"写入寄存器 D{address} 失败:{ex.Message}");
return false;
}
}
}
public bool WriteHoldingFloatRegister(ushort address, float value)
{
EnsureConnectionReadyForDirectAccess();
lock (_syncRoot)
{
if (_master is null)
{
_lastErrorMessage = $"PLC 离线未执行浮点寄存器写入D{address}";
return false;
}
try
{
var registers = EncodeFloat(value, lowWordFirst: true);
_master.WriteMultipleRegisters(_slaveId, address, registers);
return true;
}
catch (Exception ex)
{
HandleConnectionFailure($"写入浮点寄存器 D{address} 失败:{ex.Message}");
return false;
}
}
}
public void SetPumpRunning(string pumpKey, bool isRunning)
{
lock (_syncRoot)
{
var pump = _pumpControls.FirstOrDefault(item => item.Key == pumpKey);
if (pump is null)
{
return;
}
// Legacy PLC coil path retained only for non-RS485 pump compatibility.
if (PumpActuationProfiles.IsServoRs485ManagedPump(pump.Key))
{
_lastErrorMessage = $"{pump.Name} 已切换为伺服器 RS485 主控,不再执行 PLC 泵控写入";
return;
}
if (_master is null)
{
_lastErrorMessage = $"PLC 离线,未执行泵控写入:{pump.Name}";
return;
}
try
{
_master.WriteSingleCoil(_slaveId, (ushort)pump.StartAddress, isRunning);
pump.IsRunning = isRunning;
pump.StateAvailable = true;
}
catch (Exception ex)
{
HandleConnectionFailure($"泵控写入失败:{pump.Name} / {ex.Message}");
}
}
}
public void SetValveOpen(string valveKey, bool isOpen)
{
lock (_syncRoot)
{
var valve = _valveControls.FirstOrDefault(item => item.Key == valveKey);
if (valve is null)
{
return;
}
if (_master is null)
{
_lastErrorMessage = $"PLC 离线,未执行阀控写入:{valve.Name}";
return;
}
try
{
_master.WriteSingleCoil(_slaveId, (ushort)valve.StartAddress, isOpen);
valve.IsOpen = isOpen;
valve.StateAvailable = true;
}
catch (Exception ex)
{
HandleConnectionFailure($"阀控写入失败:{valve.Name} / {ex.Message}");
}
}
}
public void Dispose()
{
lock (_syncRoot)
{
ReleaseConnection();
}
}
private void EnsureConnectionScheduled()
{
lock (_syncRoot)
{
if (_master is not null && _tcpClient?.Connected == true)
{
return;
}
if (_connectionTask is { IsCompleted: false })
{
return;
}
if (DateTime.UtcNow < _nextConnectionAttemptUtc)
{
return;
}
_nextConnectionAttemptUtc = DateTime.UtcNow.Add(ConnectionRetryInterval);
_connectionTask = Task.Run(ConnectWithTimeout);
}
}
private void EnsureConnectionReadyForDirectAccess()
{
EnsureConnectionScheduled();
Task? connectionTask;
lock (_syncRoot)
{
if (_master is not null && _tcpClient?.Connected == true)
{
return;
}
connectionTask = _connectionTask;
}
if (connectionTask is null)
{
return;
}
try
{
connectionTask.Wait(ConnectionAttemptTimeout + TimeSpan.FromMilliseconds(300));
}
catch
{
// Read/write callers handle the offline state after the wait.
}
}
private void ConnectWithTimeout()
{
TcpClient? tcpClient = null;
try
{
tcpClient = new TcpClient();
using var cancellation = new CancellationTokenSource(ConnectionAttemptTimeout);
tcpClient.ConnectAsync(_ipAddress, _port, cancellation.Token).GetAwaiter().GetResult();
tcpClient.ReceiveTimeout = (int)ConnectionAttemptTimeout.TotalMilliseconds;
tcpClient.SendTimeout = (int)ConnectionAttemptTimeout.TotalMilliseconds;
var master = _factory.CreateMaster(tcpClient);
lock (_syncRoot)
{
ReleaseConnection();
_tcpClient = tcpClient;
_master = master;
_lastErrorMessage = "PLC 已连接,等待首帧数据";
tcpClient = null;
}
}
catch (Exception ex)
{
tcpClient?.Dispose();
lock (_syncRoot)
{
HandleConnectionFailure($"连接失败:{ex.Message}");
}
}
}
private bool TryReadPumpStatesAndFlows(out int failedFlowRegisterCount)
{
failedFlowRegisterCount = 0;
if (_master is null)
{
ApplyUnavailableDeviceState();
_proximalPressureRawKpa = null;
_distalPressureRawKpa = null;
_proximalPressureDecodedKpa = null;
_distalPressureDecodedKpa = null;
_proximalPressureDecodedKpa = null;
_distalPressureDecodedKpa = null;
return false;
}
try
{
var coilStates = _master.ReadCoils(_slaveId, 0, (ushort)(HighestConfiguredCoilAddress + 1));
foreach (var pump in _pumpControls)
{
if (!PumpActuationProfiles.IsServoRs485ManagedPump(pump.Key))
{
pump.IsRunning = coilStates[pump.StartAddress];
pump.StateAvailable = true;
}
if (!pump.FlowAddress.HasValue)
{
pump.FlowAvailable = false;
}
}
foreach (var valve in _valveControls)
{
valve.IsOpen = coilStates[valve.StartAddress];
valve.StateAvailable = true;
}
foreach (var pump in _pumpControls.Where(item => item.FlowAddress.HasValue))
{
try
{
var registerValue = _master.ReadHoldingRegisters(_slaveId, (ushort)pump.FlowAddress!.Value, 1)[0];
var flowValue = ConvertRegisterToFlow(registerValue);
pump.FlowValue = flowValue;
pump.FlowAvailable = true;
SetChannelValue(FlowChannelNames[pump.Key], flowValue, true);
}
catch
{
failedFlowRegisterCount++;
pump.FlowAvailable = false;
SetChannelAvailability(FlowChannelNames[pump.Key], false);
}
}
return true;
}
catch (Exception ex)
{
HandleConnectionFailure($"读取泵控/流量失败:{ex.Message}");
return false;
}
}
private bool TryReadPressureChannels(bool liveReadSucceeded)
{
if (_master is null || !liveReadSucceeded)
{
_proximalPressureRawKpa = null;
_distalPressureRawKpa = null;
SetChannelAvailability("近端压力", false);
SetChannelAvailability("远端压力", false);
return false;
}
try
{
var pressureRegisters = _master.ReadHoldingRegisters(_slaveId, ProximalPressureRegister, PressureRegisterBlockLength);
var distalOffset = DistalPressureRegister - ProximalPressureRegister;
var proximalRawKpa = ConvertRegistersToPressureKpa(pressureRegisters[0], pressureRegisters[1]);
var distalRawKpa = ConvertRegistersToPressureKpa(pressureRegisters[distalOffset], pressureRegisters[distalOffset + 1]);
_proximalPressureRawKpa = proximalRawKpa;
_distalPressureRawKpa = distalRawKpa;
_proximalPressureDecodedKpa = proximalRawKpa;
_distalPressureDecodedKpa = distalRawKpa;
SetChannelValue("近端压力", ConvertPressureKpaToMmHg(proximalRawKpa), true);
SetChannelValue("远端压力", ConvertPressureKpaToMmHg(distalRawKpa), true);
return true;
}
catch (Exception ex)
{
HandleConnectionFailure($"读取压力失败:{ex.Message}");
return false;
}
}
private void SyncDerivedChannels()
{
var returnFlow = Channel("动脉回输流量");
var drainageFlow = Channel("静脉引流流量");
if (!returnFlow.IsAvailable || !drainageFlow.IsAvailable || returnFlow.Value <= 0.01)
{
SetChannelAvailability("再循环率", false);
return;
}
var recirculationRate = Math.Clamp((returnFlow.Value - drainageFlow.Value) / returnFlow.Value * 100d, 0d, 100d);
SetChannelValue("再循环率", recirculationRate, true);
}
private List<AlarmMessage> BuildAlarms()
{
var alarms = new List<AlarmMessage>();
var proximal = Channel("近端压力");
var distal = Channel("远端压力");
var recirculation = Channel("再循环率");
if (proximal.IsAvailable && distal.IsAvailable)
{
var deltaPressure = proximal.Value - distal.Value;
if (deltaPressure > 24)
{
alarms.Add(new AlarmMessage
{
Timestamp = DateTime.Now,
Level = "高",
Message = $"压力降 ΔP {deltaPressure:F1} mmHg 偏高,请复核 近端压力/远端压力。"
});
}
}
if (_master is null)
{
alarms.Add(new AlarmMessage
{
Timestamp = DateTime.Now,
Level = "中",
Message = $"ModbusTcp 未连接,请检查 PLC 通讯。目标 {_ipAddress}:{_port}。"
});
}
if (recirculation.IsAvailable && recirculation.Value > 8)
{
alarms.Add(new AlarmMessage
{
Timestamp = DateTime.Now,
Level = "中",
Message = $"再循环率 {recirculation.Value:F1}% 偏高,建议复核回路与泵状态。"
});
}
foreach (var pump in _pumpControls.Where(item => item.StateAvailable && item.IsRunning && item.FlowAddress.HasValue && item.FlowAvailable && item.FlowValue < 0.15d))
{
alarms.Add(new AlarmMessage
{
Timestamp = DateTime.Now,
Level = "中",
Message = $"{pump.Name} 已启动但流量未建立,请检查回路、泵头和传感器。"
});
}
foreach (var pump in _pumpControls.Where(item => item.StateAvailable && item.FlowAddress.HasValue && !item.FlowAvailable))
{
alarms.Add(new AlarmMessage
{
Timestamp = DateTime.Now,
Level = "中",
Message = $"{pump.Name} 流量寄存器 D{pump.FlowAddress!.Value} 无数据,请核对 PLC 点表和寄存器映射。"
});
}
return alarms;
}
private TelemetryUpdateSnapshot BuildSnapshot(IReadOnlyList<AlarmMessage> alarms) => new()
{
Channels = _channels.Select(channel => new TelemetryChannelSnapshot
{
Name = channel.Name,
Value = channel.Value,
IsAvailable = channel.IsAvailable
}).ToList(),
PumpControls = _pumpControls.Select(pump => new PumpControlSnapshot
{
Key = pump.Key,
IsRunning = pump.IsRunning,
FlowValue = pump.FlowValue,
StateAvailable = pump.StateAvailable,
FlowAvailable = pump.FlowAvailable
}).ToList(),
ValveControls = _valveControls.Select(valve => new ValveControlSnapshot
{
Key = valve.Key,
IsOpen = valve.IsOpen,
StateAvailable = valve.StateAvailable
}).ToList(),
Alarms = alarms
.Select(alarm => new AlarmMessage
{
Timestamp = alarm.Timestamp,
Level = alarm.Level,
Message = alarm.Message
})
.ToList(),
ProximalPressureRawKpa = _proximalPressureRawKpa,
DistalPressureRawKpa = _distalPressureRawKpa,
IsLiveConnected = _master is not null && _tcpClient?.Connected == true,
EndpointDescription = EndpointDescription,
LastSuccessfulReadAt = _lastSuccessfulReadAt,
LastErrorMessage = _lastErrorMessage
};
private void ApplyUnavailableDeviceState()
{
foreach (var pump in _pumpControls)
{
pump.StateAvailable = false;
pump.FlowAvailable = false;
}
foreach (var valve in _valveControls)
{
valve.StateAvailable = false;
}
foreach (var channelName in FlowChannelNames.Values)
{
SetChannelAvailability(channelName, false);
}
SetChannelAvailability("近端压力", false);
SetChannelAvailability("远端压力", false);
SetChannelAvailability("负压辅助引流", false);
SetChannelAvailability("模拟血液温度", false);
SetChannelAvailability("再循环率", false);
SetChannelAvailability("游离血红蛋白", false);
SetChannelAvailability("白细胞减少率", false);
}
private void HandleConnectionFailure(string errorMessage)
{
ReleaseConnection();
_nextConnectionAttemptUtc = DateTime.MinValue;
_proximalPressureRawKpa = null;
_distalPressureRawKpa = null;
_proximalPressureDecodedKpa = null;
_distalPressureDecodedKpa = null;
_lastErrorMessage = errorMessage;
ApplyUnavailableDeviceState();
}
private void SetChannelValue(string channelName, double nextValue, bool isAvailable)
{
var channel = Channel(channelName);
channel.Value = ShouldClampChannelValue(channelName)
? Math.Clamp(nextValue, channel.Min, channel.Max)
: nextValue;
channel.IsAvailable = isAvailable;
}
private void SetChannelAvailability(string channelName, bool isAvailable)
{
Channel(channelName).IsAvailable = isAvailable;
}
private void ReleaseConnection()
{
_master?.Dispose();
_tcpClient?.Dispose();
_master = null;
_tcpClient = null;
}
private static int ParseIntSetting(string key, int fallback)
{
var rawValue = Environment.GetEnvironmentVariable(key);
return int.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed)
? parsed
: fallback;
}
private static byte ParseByteSetting(string key, byte fallback)
{
var rawValue = Environment.GetEnvironmentVariable(key);
return byte.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) && parsed is >= 1 and <= 247
? parsed
: fallback;
}
private static double ConvertRegisterToFlow(ushort rawValue) => rawValue * FlowRegisterScale;
private static double ConvertRegistersToPressureKpa(ushort firstWord, ushort secondWord)
{
var lowWordFirst = DecodeFloat(firstWord, secondWord, lowWordFirst: true);
var highWordFirst = DecodeFloat(firstWord, secondWord, lowWordFirst: false);
var lowWordFirstValid = IsPlausiblePressureKpa(lowWordFirst);
var highWordFirstValid = IsPlausiblePressureKpa(highWordFirst);
if (lowWordFirstValid && !highWordFirstValid)
{
return lowWordFirst;
}
if (!lowWordFirstValid && highWordFirstValid)
{
return highWordFirst;
}
return lowWordFirst;
}
private static double ConvertPressureKpaToMmHg(double pressureKpa) => pressureKpa * KpaToMmHg;
private static float DecodeFloat(ushort firstWord, ushort secondWord, bool lowWordFirst)
{
var bits = lowWordFirst
? ((uint)secondWord << 16) | firstWord
: ((uint)firstWord << 16) | secondWord;
return BitConverter.Int32BitsToSingle(unchecked((int)bits));
}
private static ushort[] EncodeFloat(float value, bool lowWordFirst)
{
var bits = unchecked((uint)BitConverter.SingleToInt32Bits(value));
var lowWord = (ushort)(bits & 0xFFFF);
var highWord = (ushort)((bits >> 16) & 0xFFFF);
return lowWordFirst
? [lowWord, highWord]
: [highWord, lowWord];
}
private static bool IsPlausiblePressureKpa(float value) =>
!float.IsNaN(value) && !float.IsInfinity(value) && value is > -1000f and < 1000f;
private static bool ShouldClampChannelValue(string channelName) =>
channelName is not "近端压力" and not "远端压力";
private DeviceChannel Channel(string name) => _channels.First(channel => channel.Name == name);
}