更新
This commit is contained in:
@@ -21,6 +21,7 @@ namespace ConeCalorimeter
|
||||
experimentDataService,
|
||||
_tcpDeviceConnectionService,
|
||||
new NpoiReportExportService(),
|
||||
new NpoiRealtimeDataExportService(),
|
||||
new HelpDialogService());
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,14 @@ public sealed record RealtimeDataRecord(
|
||||
double CarbonDioxide,
|
||||
double CarbonMonoxide,
|
||||
double HeatReleaseRate,
|
||||
double PeakHeatReleaseRate,
|
||||
double CFactor,
|
||||
double Qa180,
|
||||
double Qa300,
|
||||
double TotalHeatRelease,
|
||||
double SmokeProduction,
|
||||
double CurrentMass,
|
||||
double InitialMass,
|
||||
double MassLoss,
|
||||
int IgnitionSeconds,
|
||||
int TestSeconds,
|
||||
@@ -38,11 +41,14 @@ public sealed record RealtimeDataRecord(
|
||||
snapshot.CarbonDioxide,
|
||||
snapshot.CarbonMonoxide,
|
||||
snapshot.HeatReleaseRate,
|
||||
snapshot.PeakHeatReleaseRate,
|
||||
snapshot.CFactor,
|
||||
snapshot.Qa180,
|
||||
snapshot.Qa300,
|
||||
snapshot.TotalHeatRelease,
|
||||
snapshot.SmokeProduction,
|
||||
snapshot.CurrentMass,
|
||||
snapshot.InitialMass,
|
||||
snapshot.MassLoss,
|
||||
snapshot.IgnitionSeconds,
|
||||
snapshot.TestSeconds,
|
||||
|
||||
@@ -12,11 +12,14 @@ public sealed record RealtimeSnapshot(
|
||||
double CarbonDioxide,
|
||||
double CarbonMonoxide,
|
||||
double HeatReleaseRate,
|
||||
double PeakHeatReleaseRate,
|
||||
double CFactor,
|
||||
double Qa180,
|
||||
double Qa300,
|
||||
double TotalHeatRelease,
|
||||
double SmokeProduction,
|
||||
double CurrentMass,
|
||||
double InitialMass,
|
||||
double MassLoss,
|
||||
int IgnitionSeconds,
|
||||
int TestSeconds,
|
||||
|
||||
@@ -55,7 +55,6 @@ public sealed class ExperimentDataService : IExperimentDataService
|
||||
public void StopTest()
|
||||
{
|
||||
_isTestRunning = false;
|
||||
_initialMass = null;
|
||||
}
|
||||
|
||||
public void ClearRecords()
|
||||
@@ -139,6 +138,7 @@ public sealed class ExperimentDataService : IExperimentDataService
|
||||
{
|
||||
return snapshot with
|
||||
{
|
||||
InitialMass = _initialMass ?? double.NaN,
|
||||
MassLoss = double.NaN
|
||||
};
|
||||
}
|
||||
@@ -154,6 +154,7 @@ public sealed class ExperimentDataService : IExperimentDataService
|
||||
|
||||
return snapshot with
|
||||
{
|
||||
InitialMass = _initialMass ?? double.NaN,
|
||||
MassLoss = massLoss
|
||||
};
|
||||
}
|
||||
|
||||
8
ConeCalorimeter/Services/IRealtimeDataExportService.cs
Normal file
8
ConeCalorimeter/Services/IRealtimeDataExportService.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using ConeCalorimeter.Models;
|
||||
|
||||
namespace ConeCalorimeter.Services;
|
||||
|
||||
public interface IRealtimeDataExportService
|
||||
{
|
||||
void Export(string outputPath, IReadOnlyList<RealtimeDataRecord> records);
|
||||
}
|
||||
@@ -13,7 +13,9 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService
|
||||
private const ushort ConeTemperatureRegister = 26;
|
||||
private const ushort OrificeTemperatureRegister = 30;
|
||||
private const ushort SampleTemperatureRegister = 36;
|
||||
private const ushort CFactorRegister = 312;
|
||||
private const ushort HeatReleaseRateRegister = 354;
|
||||
private const ushort PeakHeatReleaseRateRegister = 376;
|
||||
private const ushort Qa180Register = 366;
|
||||
private const ushort Qa300Register = 370;
|
||||
private const ushort SmokeProductionRegister = 392;
|
||||
@@ -42,11 +44,14 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService
|
||||
CarbonDioxide: ReadFloatOrEmpty(CarbonDioxideRegister),
|
||||
CarbonMonoxide: ReadFloatOrEmpty(CarbonMonoxideRegister),
|
||||
HeatReleaseRate: ReadFloatOrEmpty(HeatReleaseRateRegister),
|
||||
PeakHeatReleaseRate: ReadFloatOrEmpty(PeakHeatReleaseRateRegister),
|
||||
CFactor: ReadFloatOrEmpty(CFactorRegister),
|
||||
Qa180: ReadFloatOrEmpty(Qa180Register),
|
||||
Qa300: ReadFloatOrEmpty(Qa300Register),
|
||||
TotalHeatRelease: double.NaN,
|
||||
SmokeProduction: ReadFloatOrEmpty(SmokeProductionRegister),
|
||||
CurrentMass: ReadFloatOrEmpty(CurrentMassRegister),
|
||||
InitialMass: double.NaN,
|
||||
MassLoss: double.NaN,
|
||||
IgnitionSeconds: ReadInt16OrEmpty(IgnitionSecondsRegister),
|
||||
TestSeconds: ReadInt16OrEmpty(TestSecondsRegister),
|
||||
|
||||
87
ConeCalorimeter/Services/NpoiRealtimeDataExportService.cs
Normal file
87
ConeCalorimeter/Services/NpoiRealtimeDataExportService.cs
Normal file
@@ -0,0 +1,87 @@
|
||||
using System.IO;
|
||||
using ConeCalorimeter.Models;
|
||||
using NPOI.HSSF.UserModel;
|
||||
using NPOI.SS.UserModel;
|
||||
|
||||
namespace ConeCalorimeter.Services;
|
||||
|
||||
public sealed class NpoiRealtimeDataExportService : IRealtimeDataExportService
|
||||
{
|
||||
private static readonly string[] Headers =
|
||||
[
|
||||
"Time(s)",
|
||||
"O2 (%)",
|
||||
"CO2 (%)",
|
||||
"CO (%)",
|
||||
"孔板压差 (Pa)",
|
||||
"孔板温度 (℃)",
|
||||
"HRR",
|
||||
"THR (MJ/m2)",
|
||||
"SPR",
|
||||
"TSR (m2)",
|
||||
"MLR (g/s)",
|
||||
"热释放KW/m2",
|
||||
"EHC",
|
||||
"损失质量",
|
||||
"试样温度(℃)"
|
||||
];
|
||||
|
||||
public void Export(string outputPath, IReadOnlyList<RealtimeDataRecord> records)
|
||||
{
|
||||
var workbook = new HSSFWorkbook();
|
||||
var sheet = workbook.CreateSheet("实时数据");
|
||||
|
||||
WriteHeader(sheet);
|
||||
WriteRecords(sheet, records);
|
||||
|
||||
using var outputStream = File.Create(outputPath);
|
||||
workbook.Write(outputStream);
|
||||
}
|
||||
|
||||
private static void WriteHeader(ISheet sheet)
|
||||
{
|
||||
var row = sheet.CreateRow(0);
|
||||
for (var i = 0; i < Headers.Length; i++)
|
||||
{
|
||||
row.CreateCell(i).SetCellValue(Headers[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteRecords(ISheet sheet, IReadOnlyList<RealtimeDataRecord> records)
|
||||
{
|
||||
for (var i = 0; i < records.Count; i++)
|
||||
{
|
||||
var record = records[i];
|
||||
var row = sheet.CreateRow(i + 1);
|
||||
|
||||
SetNumeric(row, 0, record.TestSeconds >= 0 ? record.TestSeconds : double.NaN);
|
||||
SetNumeric(row, 1, record.Oxygen);
|
||||
SetNumeric(row, 2, record.CarbonDioxide);
|
||||
SetNumeric(row, 3, record.CarbonMonoxide);
|
||||
SetNumeric(row, 4, record.OrificePressure);
|
||||
SetNumeric(row, 5, record.OrificeTemperature);
|
||||
SetNumeric(row, 6, record.HeatReleaseRate);
|
||||
SetNumeric(row, 7, record.TotalHeatRelease);
|
||||
SetNumeric(row, 8, record.SmokeProduction);
|
||||
SetNumeric(row, 9, record.TotalSmoke);
|
||||
SetNumeric(row, 10, record.MassLossRate);
|
||||
SetNumeric(row, 11, record.HeatReleaseRateKw);
|
||||
SetNumeric(row, 12, record.EffectiveHeatOfCombustion);
|
||||
SetNumeric(row, 13, record.MassLoss);
|
||||
SetNumeric(row, 14, record.SampleTemperature);
|
||||
}
|
||||
|
||||
for (var i = 0; i < Headers.Length; i++)
|
||||
{
|
||||
sheet.AutoSizeColumn(i);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetNumeric(IRow row, int columnIndex, double value)
|
||||
{
|
||||
if (double.IsFinite(value))
|
||||
{
|
||||
row.CreateCell(columnIndex).SetCellValue(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,7 +59,7 @@ public sealed class NpoiReportExportService : IReportExportService
|
||||
SetValueBesideLabel(sheet, "材料", input.Material);
|
||||
SetValueBesideLabel(sheet, "样品", input.SampleName);
|
||||
SetValueBesideLabel(sheet, "厚度", input.Thickness);
|
||||
SetValueBesideLabel(sheet, "初始质量", FirstNonEmpty(input.InitialMass, FormatWithUnit(records.FirstOrDefault()?.CurrentMass, "g")));
|
||||
SetValueBesideLabel(sheet, "初始质量", FirstNonEmpty(input.InitialMass, summary.InitialMass));
|
||||
SetValueBesideLabel(sheet, "辐射面积", input.IrradiatedArea);
|
||||
SetValueBesideLabel(sheet, "热辐射值", input.Irradiance);
|
||||
SetValueBesideLabel(sheet, "辐射距离", input.IrradianceDistance);
|
||||
@@ -87,7 +87,7 @@ public sealed class NpoiReportExportService : IReportExportService
|
||||
SetValueBesideLabel(sheet, "结束标准", input.EndCriteria);
|
||||
SetValueBesideLabel(sheet, "结束时间", FirstNonEmpty(input.EndTime, summary.EndTime));
|
||||
SetValueBesideLabel(sheet, "E等价热值", input.EquivalentHeatValue);
|
||||
SetValueBesideLabel(sheet, "C-系数", input.CFactor);
|
||||
SetValueBesideLabel(sheet, "C-系数", FirstNonEmpty(input.CFactor, summary.CFactor));
|
||||
SetValueBesideLabel(sheet, "光程", input.LightPath);
|
||||
SetValueBesideLabel(sheet, "O2延迟时间", input.O2DelayTime);
|
||||
SetValueBesideLabel(sheet, "CO2延迟时间", input.CO2DelayTime);
|
||||
@@ -255,18 +255,20 @@ public sealed class NpoiReportExportService : IReportExportService
|
||||
{
|
||||
if (records.Count == 0)
|
||||
{
|
||||
return new ReportSummary("", "", "", "", "", "", "");
|
||||
return new ReportSummary("", "", "", "", "", "", "", "", "");
|
||||
}
|
||||
|
||||
var last = records[^1];
|
||||
var ignition = records.FirstOrDefault(record => record.FlameDetected)?.TestSeconds;
|
||||
|
||||
return new ReportSummary(
|
||||
PeakHeatReleaseRate: FormatWithUnit(records.Max(record => record.HeatReleaseRate), "kW/㎡"),
|
||||
PeakSmokeProduction: FormatWithUnit(records.Max(record => record.SmokeProduction), "m²/s"),
|
||||
TotalHeatRelease: FormatWithUnit(last.TotalHeatRelease, "MJ/㎡"),
|
||||
TotalSmoke: FormatWithUnit(last.TotalSmoke, "m²"),
|
||||
MassLoss: FormatWithUnit(last.MassLoss, "g"),
|
||||
PeakHeatReleaseRate: FormatWithUnit(LastFinite(records, record => record.PeakHeatReleaseRate), "kW/㎡"),
|
||||
PeakSmokeProduction: FormatWithUnit(MaxFinite(records, record => record.SmokeProduction), "m²/s"),
|
||||
TotalHeatRelease: FormatWithUnit(LastFinite(records, record => record.TotalHeatRelease), "MJ/㎡"),
|
||||
TotalSmoke: FormatWithUnit(LastFinite(records, record => record.TotalSmoke), "m²"),
|
||||
MassLoss: FormatWithUnit(LastFinite(records, record => record.MassLoss), "g"),
|
||||
InitialMass: FormatWithUnit(LastFinite(records, record => record.InitialMass), "g"),
|
||||
CFactor: FormatValue(LastFinite(records, record => record.CFactor)),
|
||||
IgnitionTime: ignition is null ? "" : $"{ignition.Value} s",
|
||||
EndTime: $"{last.TestSeconds} s");
|
||||
}
|
||||
@@ -276,6 +278,40 @@ public sealed class NpoiReportExportService : IReportExportService
|
||||
return value is null || !double.IsFinite(value.Value) ? "" : $"{value.Value:0.00} {unit}";
|
||||
}
|
||||
|
||||
private static string FormatValue(double? value)
|
||||
{
|
||||
return value is null || !double.IsFinite(value.Value) ? "" : $"{value.Value:0.00}";
|
||||
}
|
||||
|
||||
private static double? LastFinite(IReadOnlyList<RealtimeDataRecord> records, Func<RealtimeDataRecord, double> selector)
|
||||
{
|
||||
for (var i = records.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var value = selector(records[i]);
|
||||
if (double.IsFinite(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static double? MaxFinite(IReadOnlyList<RealtimeDataRecord> records, Func<RealtimeDataRecord, double> selector)
|
||||
{
|
||||
double? max = null;
|
||||
foreach (var record in records)
|
||||
{
|
||||
var value = selector(record);
|
||||
if (double.IsFinite(value) && (!max.HasValue || value > max.Value))
|
||||
{
|
||||
max = value;
|
||||
}
|
||||
}
|
||||
|
||||
return max;
|
||||
}
|
||||
|
||||
private static string FirstNonEmpty(params string[] values)
|
||||
{
|
||||
return values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)) ?? string.Empty;
|
||||
@@ -287,6 +323,8 @@ public sealed class NpoiReportExportService : IReportExportService
|
||||
string TotalHeatRelease,
|
||||
string TotalSmoke,
|
||||
string MassLoss,
|
||||
string InitialMass,
|
||||
string CFactor,
|
||||
string IgnitionTime,
|
||||
string EndTime);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ public sealed class MainViewModel : ObservableObject
|
||||
IExperimentDataService experimentDataService,
|
||||
ITcpDeviceConnectionService tcpDeviceConnectionService,
|
||||
IReportExportService reportExportService,
|
||||
IRealtimeDataExportService realtimeDataExportService,
|
||||
IHelpDialogService helpDialogService)
|
||||
{
|
||||
_helpDialogService = helpDialogService;
|
||||
@@ -39,7 +40,7 @@ public sealed class MainViewModel : ObservableObject
|
||||
new SmokeDensitySettingsViewModel(() => SelectPage(testItem), tcpDeviceConnectionService));
|
||||
var realtimeDataItem = new NavigationItemViewModel(
|
||||
"实时数据",
|
||||
new RealtimeDataViewModel(experimentDataService, () => SelectPage(testItem)));
|
||||
new RealtimeDataViewModel(experimentDataService, realtimeDataExportService, () => SelectPage(testItem)));
|
||||
var helpItem = new NavigationItemViewModel(
|
||||
"帮助",
|
||||
ShowMainHelp);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Windows;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ConeCalorimeter.Services;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace ConeCalorimeter.ViewModels;
|
||||
|
||||
@@ -10,11 +12,16 @@ public sealed class RealtimeDataViewModel : PageViewModel
|
||||
{
|
||||
private readonly Action _closeAction;
|
||||
private readonly IExperimentDataService _experimentDataService;
|
||||
private readonly IRealtimeDataExportService _realtimeDataExportService;
|
||||
private string _statusText = string.Empty;
|
||||
|
||||
public RealtimeDataViewModel(IExperimentDataService experimentDataService, Action closeAction) : base("实时数据")
|
||||
public RealtimeDataViewModel(
|
||||
IExperimentDataService experimentDataService,
|
||||
IRealtimeDataExportService realtimeDataExportService,
|
||||
Action closeAction) : base("实时数据")
|
||||
{
|
||||
_experimentDataService = experimentDataService;
|
||||
_realtimeDataExportService = realtimeDataExportService;
|
||||
_closeAction = closeAction;
|
||||
|
||||
Rows = [];
|
||||
@@ -77,7 +84,39 @@ public sealed class RealtimeDataViewModel : PageViewModel
|
||||
|
||||
private void ExportRows()
|
||||
{
|
||||
StatusText = Rows.Count == 0 ? "没有可导出的数据" : $"已准备导出 {Rows.Count} 条记录";
|
||||
var records = _experimentDataService.Records.ToList();
|
||||
if (records.Count == 0)
|
||||
{
|
||||
StatusText = "没有可导出的数据";
|
||||
return;
|
||||
}
|
||||
|
||||
var dialog = new SaveFileDialog
|
||||
{
|
||||
Title = "导出实时数据",
|
||||
Filter = "Excel 97-2003 工作簿 (*.xls)|*.xls",
|
||||
FileName = $"实时数据_{DateTime.Now:yyyyMMdd_HHmmss}.xls",
|
||||
DefaultExt = ".xls",
|
||||
AddExtension = true,
|
||||
OverwritePrompt = true
|
||||
};
|
||||
|
||||
if (dialog.ShowDialog() != true)
|
||||
{
|
||||
StatusText = "已取消导出";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_realtimeDataExportService.Export(dialog.FileName, records);
|
||||
StatusText = $"已导出:{dialog.FileName}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"导出失败:{ex.Message}";
|
||||
MessageBox.Show(StatusText, "导出实时数据", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ namespace ConeCalorimeter.ViewModels;
|
||||
public sealed class ReportFieldViewModel : ObservableObject
|
||||
{
|
||||
private string _value;
|
||||
private bool _isAutoFilled;
|
||||
private bool _isSettingAutoValue;
|
||||
|
||||
public ReportFieldViewModel(string key, string label, string value = "")
|
||||
{
|
||||
@@ -20,6 +22,30 @@ public sealed class ReportFieldViewModel : ObservableObject
|
||||
public string Value
|
||||
{
|
||||
get => _value;
|
||||
set => SetProperty(ref _value, value);
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _value, value) && !_isSettingAutoValue)
|
||||
{
|
||||
_isAutoFilled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAutoValue(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(Value) && !_isAutoFilled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isSettingAutoValue = true;
|
||||
Value = value;
|
||||
_isSettingAutoValue = false;
|
||||
_isAutoFilled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,10 +141,12 @@ public sealed class ReportPageViewModel : PageViewModel
|
||||
|
||||
SummaryItems[1].Value = records.First().Timestamp.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.CurrentCulture);
|
||||
SummaryItems[2].Value = records.Last().Timestamp.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.CurrentCulture);
|
||||
SummaryItems[3].Value = $"{records.Max(row => row.HeatReleaseRate):0.00} kW/㎡";
|
||||
SummaryItems[4].Value = $"{records.Last().TotalHeatRelease:0.00} MJ/㎡";
|
||||
SummaryItems[5].Value = $"{records.Last().TotalSmoke:0.00} m²";
|
||||
SummaryItems[6].Value = $"{records.Last().MassLoss:0.00} g";
|
||||
var last = records.Last();
|
||||
SummaryItems[3].Value = FormatWithUnit(last.PeakHeatReleaseRate, "kW/㎡");
|
||||
SummaryItems[4].Value = FormatWithUnit(last.TotalHeatRelease, "MJ/㎡");
|
||||
SummaryItems[5].Value = FormatWithUnit(last.TotalSmoke, "m²");
|
||||
SummaryItems[6].Value = FormatWithUnit(last.MassLoss, "g");
|
||||
FillCollectedReportFields(records);
|
||||
}
|
||||
|
||||
private void ExportReport()
|
||||
@@ -224,4 +226,40 @@ public sealed class ReportPageViewModel : PageViewModel
|
||||
{
|
||||
return values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)) ?? string.Empty;
|
||||
}
|
||||
|
||||
private static string FormatWithUnit(double value, string unit)
|
||||
{
|
||||
return double.IsFinite(value) ? $"{value:0.00} {unit}" : "-";
|
||||
}
|
||||
|
||||
private void FillCollectedReportFields(IReadOnlyList<RealtimeDataRecord> records)
|
||||
{
|
||||
FindField("InitialMass")?.SetAutoValue(FormatWithUnit(LastFinite(records, record => record.InitialMass), "g"));
|
||||
FindField("CFactor")?.SetAutoValue(FormatValue(LastFinite(records, record => record.CFactor)));
|
||||
}
|
||||
|
||||
private ReportFieldViewModel? FindField(string key)
|
||||
{
|
||||
return Sections.SelectMany(section => section.Fields)
|
||||
.FirstOrDefault(field => field.Key == key);
|
||||
}
|
||||
|
||||
private static double LastFinite(IReadOnlyList<RealtimeDataRecord> records, Func<RealtimeDataRecord, double> selector)
|
||||
{
|
||||
for (var i = records.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var value = selector(records[i]);
|
||||
if (double.IsFinite(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
private static string FormatValue(double value)
|
||||
{
|
||||
return double.IsFinite(value) ? $"{value:0.00}" : string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user