diff --git a/ConeCalorimeter/Services/ModbusRealtimeDataService.cs b/ConeCalorimeter/Services/ModbusRealtimeDataService.cs index 1c554a4..e852605 100644 --- a/ConeCalorimeter/Services/ModbusRealtimeDataService.cs +++ b/ConeCalorimeter/Services/ModbusRealtimeDataService.cs @@ -18,7 +18,7 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService private const ushort OrificeTemperatureRegister = 30; private const ushort SampleTemperatureRegister = 36; private const ushort AbsorbanceRegister = 120; - private const ushort CFactorRegister = 312; + private const ushort CFactorRegister = 308; private const ushort HeatReleaseRateRegister = 354; private const ushort PeakHeatReleaseRateRegister = 376; private const ushort Qa180Register = 366; diff --git a/ConeCalorimeter/Services/NpoiReportExportService.cs b/ConeCalorimeter/Services/NpoiReportExportService.cs index a5d223e..dda6102 100644 --- a/ConeCalorimeter/Services/NpoiReportExportService.cs +++ b/ConeCalorimeter/Services/NpoiReportExportService.cs @@ -153,6 +153,9 @@ public sealed class NpoiReportExportService : IReportExportService { ClearSheet(sheet); + var fourDecimalCellStyle = sheet.Workbook.CreateCellStyle(); + fourDecimalCellStyle.DataFormat = sheet.Workbook.CreateDataFormat().GetFormat("0.0000"); + var headerRow = sheet.CreateRow(0); for (var i = 0; i < DataHeaders.Length; i++) { @@ -191,7 +194,7 @@ public sealed class NpoiReportExportService : IReportExportService SetNumeric(row, 22, record.CurrentMass); SetNumeric(row, 23, record.InitialMass); SetNumeric(row, 24, record.PeakHeatReleaseRate); - SetNumeric(row, 25, record.CFactor); + SetNumeric(row, 25, record.CFactor, fourDecimalCellStyle); } for (var i = 0; i < DataHeaders.Length; i++) @@ -524,11 +527,16 @@ public sealed class NpoiReportExportService : IReportExportService } } - private static void SetNumeric(IRow row, int columnIndex, double value) + private static void SetNumeric(IRow row, int columnIndex, double value, ICellStyle? cellStyle = null) { if (double.IsFinite(value)) { - row.CreateCell(columnIndex).SetCellValue(value); + var cell = row.CreateCell(columnIndex); + cell.SetCellValue(value); + if (cellStyle is not null) + { + cell.CellStyle = cellStyle; + } } } @@ -601,7 +609,9 @@ public sealed class NpoiReportExportService : IReportExportService private static string FormatValue(double? value) { - return value is null || !double.IsFinite(value.Value) ? "" : $"{value.Value:0.00}"; + return value is null || !double.IsFinite(value.Value) + ? "" + : value.Value.ToString("0.0000", CultureInfo.InvariantCulture); } private static string FormatSeconds(int? value) diff --git a/ConeCalorimeter/ViewModels/MainViewModel.cs b/ConeCalorimeter/ViewModels/MainViewModel.cs index 99f7b35..81a564a 100644 --- a/ConeCalorimeter/ViewModels/MainViewModel.cs +++ b/ConeCalorimeter/ViewModels/MainViewModel.cs @@ -25,7 +25,10 @@ public sealed class MainViewModel : ObservableObject _deviceStatusText = tcpDeviceConnectionService.StatusText; _tcpDeviceConnectionService.ConnectionStatusChanged += DeviceConnectionStatusChanged; var testPage = new TestPageViewModel(experimentDataService, tcpDeviceConnectionService); - var reportPage = new ReportPageViewModel(experimentDataService, reportExportService); + var reportPage = new ReportPageViewModel( + experimentDataService, + reportExportService, + tcpDeviceConnectionService); NavigationItems = []; var testItem = new NavigationItemViewModel("测试界面", testPage); diff --git a/ConeCalorimeter/ViewModels/ReportFieldViewModel.cs b/ConeCalorimeter/ViewModels/ReportFieldViewModel.cs index 8c4c86b..3560152 100644 --- a/ConeCalorimeter/ViewModels/ReportFieldViewModel.cs +++ b/ConeCalorimeter/ViewModels/ReportFieldViewModel.cs @@ -31,14 +31,14 @@ public sealed class ReportFieldViewModel : ObservableObject } } - public void SetAutoValue(string value) + public void SetAutoValue(string value, bool overwriteManual = false) { if (string.IsNullOrWhiteSpace(value)) { return; } - if (!string.IsNullOrWhiteSpace(Value) && !_isAutoFilled) + if (!overwriteManual && !string.IsNullOrWhiteSpace(Value) && !_isAutoFilled) { return; } diff --git a/ConeCalorimeter/ViewModels/ReportPageViewModel.cs b/ConeCalorimeter/ViewModels/ReportPageViewModel.cs index 4e34eb7..7a4793e 100644 --- a/ConeCalorimeter/ViewModels/ReportPageViewModel.cs +++ b/ConeCalorimeter/ViewModels/ReportPageViewModel.cs @@ -14,16 +14,26 @@ namespace ConeCalorimeter.ViewModels; public sealed class ReportPageViewModel : PageViewModel { + private const ushort CFactorRegister = 308; + private const double CFactorMinimum = 0; + private const double CFactorMaximum = 1000; + private const double DisplayZeroTolerance = 0.00005; + private const string FourDecimalDisplayFormat = "0.0000"; + private readonly IExperimentDataService _experimentDataService; private readonly IReportExportService _reportExportService; + private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService; + private ModbusFloatByteOrder? _cFactorByteOrder; private string _statusText = "报表信息可手动录入,导出时会合并当前采集数据。"; public ReportPageViewModel( IExperimentDataService experimentDataService, - IReportExportService reportExportService) : base("报表") + IReportExportService reportExportService, + ITcpDeviceConnectionService tcpDeviceConnectionService) : base("报表") { _experimentDataService = experimentDataService; _reportExportService = reportExportService; + _tcpDeviceConnectionService = tcpDeviceConnectionService; Sections = CreateSections(); SummaryItems = @@ -160,6 +170,7 @@ public sealed class ReportPageViewModel : PageViewModel return; } + RefreshCurrentCValueReportField(overwriteManual: true); var input = BuildInput(); var defaultName = BuildDefaultFileName(input); var dialog = new SaveFileDialog @@ -255,6 +266,48 @@ public sealed class ReportPageViewModel : PageViewModel FindField("EndTime")?.SetAutoValue(FormatSeconds(LastNonNegative(records, record => record.TestSeconds))); } + private void RefreshCurrentCValueReportField(bool overwriteManual) + { + if (TryReadCurrentCValueText(out var value)) + { + FindField("CFactor")?.SetAutoValue(value, overwriteManual); + } + } + + private bool TryReadCurrentCValueText(out string value) + { + value = string.Empty; + if (!_tcpDeviceConnectionService.TryReadFloatValues(CFactorRegister, out var result)) + { + Log.Warning( + "Report C factor D{RegisterAddress} read failed before export. ConnectionStatus={ConnectionStatus}.", + CFactorRegister, + _tcpDeviceConnectionService.StatusText); + return false; + } + + if (!ModbusFloatSelector.TrySelectRangedFloat( + result, + CFactorMinimum, + CFactorMaximum, + out var byteOrder, + out var cFactor, + out var matchCount, + _cFactorByteOrder)) + { + Log.Warning( + "Report C factor D{RegisterAddress} has no valid decoded value. Raw={RawHex}, Matches={MatchCount}.", + CFactorRegister, + result.RawHex, + matchCount); + return false; + } + + _cFactorByteOrder = byteOrder; + value = FormatValue(NormalizeDisplayValue(cFactor)); + return true; + } + private ReportFieldViewModel? FindField(string key) { return Sections.SelectMany(section => section.Fields) @@ -296,7 +349,14 @@ public sealed class ReportPageViewModel : PageViewModel private static string FormatValue(double value) { - return double.IsFinite(value) ? $"{value:0.00}" : string.Empty; + return double.IsFinite(value) + ? value.ToString(FourDecimalDisplayFormat, CultureInfo.InvariantCulture) + : string.Empty; + } + + private static double NormalizeDisplayValue(double value) + { + return Math.Abs(value) < DisplayZeroTolerance ? 0 : value; } private static bool IsValidExperimentRecord(RealtimeDataRecord record)