Files
CapMachine/CapMachine.Wpf/Services/PPCService.cs
2026-05-08 17:19:16 +08:00

806 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 CapMachine.Core;
using CapMachine.Wpf.Models.PPCalc;
using CapMachine.Wpf.Models.Tag;
using CapMachine.Wpf.PPCalculation;
using ImTools;
using MaterialDesignThemes.Wpf;
using NLog;
using NPOI.XWPF.UserModel;
using Prism.Events;
using Prism.Mvvm;
using Prism.Services.Dialogs;
using SixLabors.ImageSharp.ColorSpaces;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
namespace CapMachine.Wpf.Services
{
/// <summary>
/// 物性计算的服务
/// </summary>
public class PPCService : BindableBase
{
/// <summary>
/// 计算扫描 Task
/// </summary>
private static Task CalcTask { get; set; }
public ConfigService ConfigService { get; }
private IEventAggregator _EventAggregator { get; set; }
public DataRecordService DataRecordService { get; }
public SysRunService SysRunServer { get; }
public ILogService Logger { get; }
public MachineRtDataService MachineRtDataService { get; }
public IDialogService DialogService { get; }
private readonly SuperheatSubcoolCalculator _superheatSubcoolCalculator;
private readonly ThermodynamicSixResultsCalculator _thermodynamicSixResultsCalculator;
/// <summary>
/// 标签中心
/// </summary>
public TagManager TagManager { get; set; }
/// <summary>
/// 实例化
/// </summary>
public PPCService(ConfigService configService, IEventAggregator eventAggregator,
DataRecordService dataRecordService, SysRunService sysRunService, ILogService logService,
MachineRtDataService machineRtDataService, IDialogService dialogService
)
{
ConfigService = configService;
//事件服务
_EventAggregator = eventAggregator;
DataRecordService = dataRecordService;
SysRunServer = sysRunService;
Logger = logService;
MachineRtDataService = machineRtDataService;
DialogService = dialogService;
TagManager = MachineRtDataService.TagManger;
SpeedTag = TagManager.DicTags.GetValueOrDefault("转速[rpm]");
ExPressTag = TagManager.DicTags.GetValueOrDefault("排气压力[BarA]");
ExTempTag = TagManager.DicTags.GetValueOrDefault("排气温度[℃]");
HVPwTag = TagManager.DicTags.GetValueOrDefault("HV[W]");
InhPressTag = TagManager.DicTags.GetValueOrDefault("吸气压力[BarA]");
InhTempTag = TagManager.DicTags.GetValueOrDefault("吸气温度[℃]");
TxvFrTempTag = TagManager.DicTags.GetValueOrDefault("膨胀阀前温度[℃]");
TxvFrPressTag = TagManager.DicTags.GetValueOrDefault("膨胀阀前压力[BarA]");
GasPreValvePressTag = TagManager.DicTags.GetValueOrDefault("气路阀前压力[BarA]");
GasPreValveTempTag = TagManager.DicTags.GetValueOrDefault("气路阀前温度[℃]");
DrynessTag = TagManager.DicTags.GetValueOrDefault("干度[-]");
if (DrynessTag == null)
{
DrynessTag = TagManager.DicTags.GetValueOrDefault("干度");
}
VRVTag = TagManager.DicTags.GetValueOrDefault("冷媒流量[kg/h]");
if (VRVTag == null)
{
VRVTag = TagManager.DicTags.GetValueOrDefault("冷媒流量[kg/h]");
}
LiqRefFlowTag = TagManager.DicTags.GetValueOrDefault("液冷媒流量[kg/h]");
if (LiqRefFlowTag == null)
{
LiqRefFlowTag = TagManager.DicTags.GetValueOrDefault("液体流量[kg/h]");
}
LubeFlowTag = TagManager.DicTags.GetValueOrDefault("润滑油流量[kg/h]");
if (LubeFlowTag == null)
{
LubeFlowTag = TagManager.DicTags.GetValueOrDefault("润滑油流量[kg/h]");
}
Superheat = TagManager.DicTags.GetValueOrDefault("过热度[K]");
Subcool = TagManager.DicTags.GetValueOrDefault("过冷度[K]");
HeatingCapacityTag = TagManager.DicTags.GetValueOrDefault("制热量Qh[KW]")
?? TagManager.DicTags.GetValueOrDefault("制热量Qh[W]");
COPHeatTag = TagManager.DicTags.GetValueOrDefault("压缩机性能系数(制热)[K]")
?? TagManager.DicTags.GetValueOrDefault("压缩机性能系数(制热COP)");
IsentrpEffTag = TagManager.DicTags.GetValueOrDefault("等熵效率ns[%]");
CoolCapacityTag = TagManager.DicTags.GetValueOrDefault("制冷量Qc[KW]")
?? TagManager.DicTags.GetValueOrDefault("制冷量Qc[W]");
COPCoolTag = TagManager.DicTags.GetValueOrDefault("压缩机性能系数(制冷)[K]")
?? TagManager.DicTags.GetValueOrDefault("压缩机性能系数(制冷COP)");
VoltricEffTag = TagManager.DicTags.GetValueOrDefault("容积效率nv[%]");
_superheatSubcoolCalculator = new SuperheatSubcoolCalculator(_refpropLock);
_thermodynamicSixResultsCalculator = new ThermodynamicSixResultsCalculator(_refpropLock);
SuperHeatCoolConfig.FluidsPath = ConfigHelper.GetValue("FluidsPath");
SuperHeatCoolConfig.Cryogen = ConfigHelper.GetValue("Cryogen");
ReloadTherdyH3TempOffset();
// 订阅 ConfigService.CurExpInfo 属性变化,实验切换时自动刷新排量缓存
ConfigService.PropertyChanged += (sender, e) =>
{
if (e.PropertyName == nameof(ConfigService.CurExpInfo))
{
RefreshDisplacementCache();
}
};
// 首次初始化排量缓存
RefreshDisplacementCache();
RtScanDeviceStart();
}
private const string TherdyH3TempOffsetConfigKey = "Therdy_H3TempOffset_C";
public void ReloadTherdyH3TempOffset()
{
double offsetC = -10.0;
try
{
string raw = ConfigHelper.GetValue(TherdyH3TempOffsetConfigKey);
if (!string.IsNullOrWhiteSpace(raw) && double.TryParse(raw, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed))
{
offsetC = parsed;
}
}
catch
{
}
TherdyH3TempOffset_C = offsetC;
_thermodynamicSixResultsCalculator.SetH3TempOffset_C(offsetC);
}
/// <summary>
/// 当前的配置
/// </summary>
public SuperHeatCoolConfigModel SuperHeatCoolConfig { get; set; } = new SuperHeatCoolConfigModel();
/// <summary>
/// 保存配置信息
/// </summary>
public void SaveSuperHeatCoolConfig()
{
ConfigHelper.SetValue("FluidsPath", SuperHeatCoolConfig.FluidsPath);
ConfigHelper.SetValue("Cryogen", SuperHeatCoolConfig.Cryogen);
}
private double _therdyH3TempOffset_C = -10.0;
public double TherdyH3TempOffset_C
{
get { return _therdyH3TempOffset_C; }
private set
{
_therdyH3TempOffset_C = value;
RaisePropertyChanged();
}
}
/// <summary>
/// 吸气压力
/// </summary>
public ITag InhPressTag { get; set; }
/// <summary>
/// 转速标签
/// </summary>
public ITag? SpeedTag { get; set; }
/// <summary>
/// 排气压力
/// </summary>
public ITag? ExPressTag { get; set; }
/// <summary>
/// 排气温度
/// </summary>
public ITag? ExTempTag { get; set; }
/// <summary>
/// 压缩机功率HV 电源)
/// </summary>
public ITag? HVPwTag { get; set; }
/// <summary>
/// 吸气温度
/// </summary>
public ITag InhTempTag { get; set; }
/// <summary>
/// 液体阀前温度
/// </summary>
public ITag TxvFrTempTag { get; set; }
/// <summary>
/// 液体阀前压力
/// </summary>
public ITag TxvFrPressTag { get; set; }
/// <summary>
/// 过热度
/// </summary>
public ITag Superheat { get; set; }
/// <summary>
/// 过冷度
/// </summary>
public ITag Subcool { get; set; }
/// <summary>
/// 干度(无量纲 [-]
/// </summary>
public ITag DrynessTag { get; set; }
private double _DrynessTag2Value;
/// <summary>
/// 干度2无量纲 [-]
/// </summary>
public double DrynessTag2Value
{
get { return _DrynessTag2Value; }
set { _DrynessTag2Value = value; RaisePropertyChanged(); }
}
/// <summary>
/// 气路阀前压力BarA
/// </summary>
public ITag GasPreValvePressTag { get; set; }
/// <summary>
/// 气路阀前温度(℃)
/// </summary>
public ITag GasPreValveTempTag { get; set; }
/// <summary>
/// 冷媒流量kg/h
/// </summary>
public ITag VRVTag { get; set; }
/// <summary>
/// 液体流量kg/h
/// 液冷媒流量kg/h=液体流量kg/h
/// </summary>
public ITag LiqRefFlowTag { get; set; }
/// <summary>
/// 润滑油流量kg/h
/// </summary>
public ITag LubeFlowTag { get; set; }
public ITag? HeatingCapacityTag { get; set; }
public ITag? COPHeatTag { get; set; }
public ITag? IsentrpEffTag { get; set; }
public ITag? CoolCapacityTag { get; set; }
public ITag? COPCoolTag { get; set; }
public ITag? VoltricEffTag { get; set; }
/// <summary>
/// 启用计算
/// </summary>
private bool RtCalcEnable { get; set; } = true;
/// <summary>
/// 触发日志
/// </summary>
private bool DebugLog { get; set; } = false;
/// <summary>
/// 缓存的压缩机排量值cc在实验切换时自动刷新
/// </summary>
private double _cachedDisplacement_cc = double.NaN;
/// <summary>
/// 缓存数据来源标识用于调试ExpInfo=实验信息, Config=配置文件, Default=默认值
/// </summary>
private string _cachedSource = string.Empty;
private int _CurDisplacementCc;
/// <summary>
/// 当前的排量信息(供 UI 展示)
/// </summary>
public int CurDisplacementCc
{
get { return _CurDisplacementCc; }
set { _CurDisplacementCc = value; RaisePropertyChanged(); }
}
private double _HeatingCapacityQh_kW;
/// <summary>
/// 制热量 Qh [kW]
/// </summary>
public double HeatingCapacityQh_kW
{
get { return _HeatingCapacityQh_kW; }
set { _HeatingCapacityQh_kW = value; RaisePropertyChanged(); }
}
private double _COPHeating;
/// <summary>
/// 压缩机性能系数 COP制热[-]
/// </summary>
public double COPHeating
{
get { return _COPHeating; }
set { _COPHeating = value; RaisePropertyChanged(); }
}
private double _IsentropicEfficiencyPct;
/// <summary>
/// 等熵效率 ηs [%]
/// </summary>
public double IsentropicEfficiencyPct
{
get { return _IsentropicEfficiencyPct; }
set { _IsentropicEfficiencyPct = value; RaisePropertyChanged(); }
}
private double _CoolingCapacityQc_kW;
/// <summary>
/// 制冷量 Qc [kW]
/// </summary>
public double CoolingCapacityQc_kW
{
get { return _CoolingCapacityQc_kW; }
set { _CoolingCapacityQc_kW = value; RaisePropertyChanged(); }
}
private double _COPCooling;
/// <summary>
/// 压缩机性能系数 COP制冷[-]
/// </summary>
public double COPCooling
{
get { return _COPCooling; }
set { _COPCooling = value; RaisePropertyChanged(); }
}
private double _VolumetricEfficiencyPct;
/// <summary>
/// 容积效率 ηv [%]
/// </summary>
public double VolumetricEfficiencyPct
{
get { return _VolumetricEfficiencyPct; }
set { _VolumetricEfficiencyPct = value; RaisePropertyChanged(); }
}
/// <summary>
/// PLC扫描线程
/// </summary>
private void RtScanDeviceStart()
{
CalcTask = Task.Run(async () =>
{
while (RtCalcEnable)
{
await Task.Delay(300);
try
{
double[] x = new double[20];
double wm = 0.0;
// 幂等初始化:仅首次或工质/路径变化时执行 SETPATH/SETUP提高每秒循环效率
if (!EnsureRefpropInitialized(out var initErr))
{
// 初始化失败,跳过本周期
Logger?.Error($"REFPROP 初始化失败: {initErr}");
continue;
}
// WMOL 仅在需要时调用;若调用,需设置 x[0]=1.0(纯工质)
x[0] = 1.0;
IRefProp64.WMOLdll(x, ref wm);
UpdateSuperheatAndSubcool_BySatp();
if (TryUpdateThermodynamicSixResults(out var thermoErr))
{
if (!string.IsNullOrWhiteSpace(thermoErr))
{
//Logger?.Warn($"六个物性结果计算警告: {thermoErr}");
}
}
else
{
if (!string.IsNullOrWhiteSpace(thermoErr))
{
//Logger?.Error($"六个物性结果计算失败: {thermoErr}");
}
}
}
catch (Exception ex)
{
Logger.Error(String.Format("ErrSource : {0} ErrMsg : {1}", ex.StackTrace.ToString(), ex.Message.ToString()));
}
}
});
}
/// <summary>
/// 按流程图更新制热量、COP(制热)、等熵效率、制冷量、COP(制冷)、容积效率。
/// </summary>
/// <param name="error">
/// 错误/警告信息输出。
/// - 当方法返回 <see langword="false"/> 时,<paramref name="error"/> 为失败原因,调用方应视为本周期计算无效。
/// - 当方法返回 <see langword="true"/> 但 <paramref name="error"/> 非空时,表示仅部分结果无法计算(例如缺少排量导致容积效率为 NaN
/// </param>
/// <returns>
/// 是否成功完成本周期的结果更新。
/// - <see langword="true"/>:至少已成功更新 Qh/Qc/COP/ηs 等主要结果;容积效率可能因缺失排量而为 NaN。
/// - <see langword="false"/>:关键输入或 REFPROP 计算失败,本周期结果不更新。
/// </returns>
private bool TryUpdateThermodynamicSixResults(out string error)
{
error = string.Empty;
if (InhPressTag == null || InhTempTag == null)
{
error = "缺少吸气压力/吸气温度标签";
return false;
}
if (TxvFrPressTag == null || TxvFrTempTag == null)
{
error = "缺少膨胀阀前压力/膨胀阀前温度标签";
return false;
}
if (ExPressTag == null || ExTempTag == null)
{
error = "缺少排气压力/排气温度标签";
return false;
}
if (HVPwTag == null)
{
error = "缺少 HV[W] 功率标签";
return false;
}
if (VRVTag == null)
{
error = "缺少总流量(冷媒流量)标签";
return false;
}
double suctionPress_BarA = InhPressTag.EngPvValue;
double suctionTemp_C = InhTempTag.EngPvValue;
double dischargePress_BarA = ExPressTag.EngPvValue;
double dischargeTemp_C = ExTempTag.EngPvValue;
double txvFrPress_BarA = TxvFrPressTag.EngPvValue;
double txvFrTemp_C = TxvFrTempTag.EngPvValue;
double totalFlow_kg_h = VRVTag.EngPvValue;
double w_W = HVPwTag.EngPvValue;
double speed_rpm = SpeedTag?.EngPvValue ?? double.NaN;
if (!TryGetCompressorDisplacement_cc(out var disp_cc, out var dispErr))
{
error = dispErr;
return false;
}
//这里把输入数据写死计算出结果。
//#if DEBUG
// // 附件截图输入单位换算MPa -> BarAkW -> W
// suctionPress_BarA = 0.3 * 10.0;
// suctionTemp_C = 10.8;
// dischargePress_BarA = 1.502 * 10.0;
// dischargeTemp_C = 88.4;
// txvFrPress_BarA = 1.494 * 10.0;
// txvFrTemp_C = 45.0;
// totalFlow_kg_h = 100.1;
// w_W = 1.289 * 1000.0;
// speed_rpm = 3004;
// disp_cc = 35;
//#endif
if (!double.IsNaN(w_W) && !double.IsInfinity(w_W) && w_W == 0)
{
HeatingCapacityQh_kW = 0;
CoolingCapacityQc_kW = 0;
COPHeating = 0;
COPCooling = 0;
IsentropicEfficiencyPct = 0;
VolumetricEfficiencyPct = 0;
if (HeatingCapacityTag != null)
{
HeatingCapacityTag.EngPvValue = 0;
}
if (COPHeatTag != null)
{
COPHeatTag.EngPvValue = 0;
}
if (IsentrpEffTag != null)
{
IsentrpEffTag.EngPvValue = 0;
}
if (CoolCapacityTag != null)
{
CoolCapacityTag.EngPvValue = 0;
}
if (COPCoolTag != null)
{
COPCoolTag.EngPvValue = 0;
}
if (VoltricEffTag != null)
{
VoltricEffTag.EngPvValue = 0;
}
return true;
}
var calcInput = new ThermodynamicSixResultsCalculator.Input(
suctionPress_BarA: suctionPress_BarA,
suctionTemp_C: suctionTemp_C,
dischargePress_BarA: dischargePress_BarA,
dischargeTemp_C: dischargeTemp_C,
txvFrPress_BarA: txvFrPress_BarA,
txvFrTemp_C: txvFrTemp_C,
hvPower_W: w_W,
totalFlow_kg_h: totalFlow_kg_h,
speed_rpm: speed_rpm,
displacement_cc: disp_cc);
if (!_thermodynamicSixResultsCalculator.TryCalculate(calcInput, out var r, out var calcErr))
{
error = calcErr;
return false;
}
HeatingCapacityQh_kW = r.HeatingCapacityQh_kW;
CoolingCapacityQc_kW = r.CoolingCapacityQc_kW;
COPHeating = r.COPHeating;
COPCooling = r.COPCooling;
IsentropicEfficiencyPct = r.IsentropicEfficiencyPct;
if (HeatingCapacityTag != null)
{
HeatingCapacityTag.EngPvValue = HeatingCapacityQh_kW * 1000.0;
}
if (COPHeatTag != null)
{
COPHeatTag.EngPvValue = COPHeating;
}
if (IsentrpEffTag != null)
{
IsentrpEffTag.EngPvValue = IsentropicEfficiencyPct;
}
if (CoolCapacityTag != null)
{
CoolCapacityTag.EngPvValue = CoolingCapacityQc_kW * 1000.0;
}
if (COPCoolTag != null)
{
COPCoolTag.EngPvValue = COPCooling;
}
if (double.IsNaN(r.VolumetricEfficiencyPct) || double.IsInfinity(r.VolumetricEfficiencyPct))
{
VolumetricEfficiencyPct = double.NaN;
error = calcErr;
return true;
}
VolumetricEfficiencyPct = r.VolumetricEfficiencyPct;
if (VoltricEffTag != null)
{
VoltricEffTag.EngPvValue = VolumetricEfficiencyPct;
}
error = calcErr;
return true;
}
private void UpdateSuperheatAndSubcool_BySatp()
{
_superheatSubcoolCalculator.Calculate(
inhPressBarA: InhPressTag.EngPvValue,
inhTempC: InhTempTag.EngPvValue,
txvFrPressBarA: TxvFrPressTag.EngPvValue,
txvFrTempC: TxvFrTempTag.EngPvValue,
out var superheatK,
out var subcoolK);
Superheat.EngPvValue = superheatK;
Subcool.EngPvValue = subcoolK;
}
// 若类中尚未定义,请添加全局互斥锁,串行化所有 REFPROP 调用
private static readonly object _refpropLock = new object();
// REFPROP 初始化状态(全局、幂等)
private static volatile bool _rpInitialized = false;
/// <summary>
/// 幂等初始化:设置流体路径/工质/参考态;确保全局只初始化一次。
/// 注意:所有 REFPROP 原生调用都需在 _refpropLock 下串行化,包括初始化调用。
/// </summary>
private bool EnsureRefpropInitialized(out string error)
{
error = string.Empty;
if (_rpInitialized) return true;
try
{
lock (_refpropLock)
{
if (_rpInitialized) return true; // 双检,避免并发二次初始化
string hpath = ConfigHelper.GetValue("FluidsPath");
if (string.IsNullOrWhiteSpace(hpath)) hpath = @".\PPCalculation\REFPROP\FLUIDS";
string configuredCryogen = ConfigHelper.GetValue("Cryogen");
if (string.IsNullOrWhiteSpace(configuredCryogen)) configuredCryogen = "R134a";
// 现阶段仅使用 R134A.FLD如需扩展可根据 configuredCryogen 选择不同文件
string hfldCore = configuredCryogen.Equals("R134a", StringComparison.OrdinalIgnoreCase)
? "R134A.FLD"
: "R134A.FLD";
long size = hpath.Length;
string hpathPadded = hpath + new string(' ', Math.Max(0, 255 - (int)size));
IRefProp64.SETPATHdll(hpathPadded, ref size);
long numComps = 1;
string hfld = hfldCore;
size = hfld.Length;
string hfldPadded = hfld + new string(' ', Math.Max(0, 10000 - (int)size));
string hfmix = "hmx.bnc" + new string(' ', 255);
string hrf = "DEF";
string herr = new string(' ', 255);
long ierr = 0;
long hfldLen = hfldPadded.Length, hfmixLen = hfmix.Length, hrfLen = hrf.Length, herrLen = herr.Length;
IRefProp64.SETUPdll(ref numComps, ref hfldPadded, ref hfmix, ref hrf,
ref ierr, ref herr, ref hfldLen, ref hfmixLen, ref hrfLen, ref herrLen);
if (ierr != 0)
{
error = $"REFPROP 初始化失败: {herr.Trim()} (ierr={ierr})";
_rpInitialized = false;
return false;
}
_rpInitialized = true;
return true;
}
}
catch (Exception ex)
{
error = $"REFPROP 初始化异常: {ex.Message}";
Logger.Error(error);
_rpInitialized = false;
return false;
}
}
/// <summary>
/// 获取压缩机排量(使用缓存机制,避免每次计算周期实时读取)
/// </summary>
/// <param name="displacement_cc">排量输出,单位 cccm³/rev。</param>
/// <param name="error">失败原因(仅在缓存异常时返回)。</param>
/// <returns>始终返回 true因为默认回退 35cc 保证缓存始终有效)。</returns>
private bool TryGetCompressorDisplacement_cc(out double displacement_cc, out string error)
{
displacement_cc = _cachedDisplacement_cc;
error = string.Empty;
return true;
}
/// <summary>
/// 刷新压缩机排量缓存,按优先级读取:实验信息 > 配置文件 > 默认值
/// 实验切换时自动调用,保证缓存与当前实验信息一致
/// </summary>
private void RefreshDisplacementCache()
{
const double defaultDisplacementCc = 35d;
double displacementCc = defaultDisplacementCc;
string source = "Default";
// 优先1从当前实验信息读取
if (ConfigService?.CurExpInfo != null &&
TryParseCompressorDisplacementTextToCc(ConfigService.CurExpInfo.CapDisplacement, out var expCc) &&
expCc > 0)
{
displacementCc = expCc;
source = "ExpInfo";
}
// 优先2从 App.config 配置读取
else
{
string configValue = ConfigHelper.GetValue("CompressorDisplacementCc");
if (!string.IsNullOrWhiteSpace(configValue) &&
TryParseCompressorDisplacementTextToCc(configValue, out var cfgCc) &&
cfgCc > 0)
{
displacementCc = cfgCc;
source = "Config";
}
// 优先3使用默认值已在初始化时设置
}
// 更新缓存字段
_cachedDisplacement_cc = displacementCc;
_cachedSource = source;
// 同步更新 UI 展示属性
CurDisplacementCc = (int)displacementCc;
// 记录日志(便于调试)
Logger?.Info($"压缩机排量缓存已刷新: {displacementCc}cc (来源: {source})");
}
/// <summary>
/// 强制刷新压缩机排量缓存(供外部主动调用,用于调试或特殊情况如 App.config 配置修改后)
/// </summary>
public void ForceRefreshDisplacementCache()
{
RefreshDisplacementCache();
}
/// <summary>
/// 解析压缩机排量文本,支持多种格式(如 34.5cc、34,5 cm3 等)
/// </summary>
/// <param name="text">排量文本</param>
/// <param name="displacementCc">解析出的排量值cc</param>
/// <returns>是否解析成功</returns>
private static bool TryParseCompressorDisplacementTextToCc(string? text, out double displacementCc)
{
displacementCc = double.NaN;
if (string.IsNullOrWhiteSpace(text))
{
return false;
}
var normalized = text.Trim().ToLowerInvariant();
normalized = normalized.Replace(',', '.');
var match = Regex.Match(normalized, @"[-+]?\d+(\.\d+)?");
if (!match.Success)
{
return false;
}
if (!double.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out var v))
{
return false;
}
displacementCc = v;
return true;
}
}
}