更新2026522

This commit is contained in:
GukSang.Jin
2026-05-22 18:27:18 +08:00
parent 22261c48a9
commit 3aabe94d05
6 changed files with 149 additions and 16 deletions

View File

@@ -1,4 +1,5 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Windows.Threading;
using ConeCalorimeter.Models;
@@ -17,7 +18,9 @@ public sealed class ExperimentDataService : IExperimentDataService
private double _accumulatedTotalHeatRelease;
private double _accumulatedTotalSmoke;
private int? _lastAccumulationSeconds;
private int? _lastRecordedSeconds;
private bool _isTestRunning;
private bool _isPollingSnapshot;
public ExperimentDataService(IRealtimeDataService realtimeDataService)
{
@@ -29,8 +32,7 @@ public sealed class ExperimentDataService : IExperimentDataService
{
Interval = TimeSpan.FromSeconds(1)
};
_timer.Tick += (_, _) => RecordSnapshot(
_realtimeDataService.GetCurrentSnapshot(DateTime.Now - _startedAt));
_timer.Tick += async (_, _) => await RecordCurrentSnapshotAsync();
_timer.Start();
}
@@ -60,7 +62,36 @@ public sealed class ExperimentDataService : IExperimentDataService
public void ClearRecords()
{
Records.Clear();
ResetComputedState();
if (!_isTestRunning)
{
ResetComputedState();
}
}
private async Task RecordCurrentSnapshotAsync()
{
if (_isPollingSnapshot)
{
return;
}
_isPollingSnapshot = true;
try
{
var elapsed = DateTime.Now - _startedAt;
var snapshot = await Task.Run(() => _realtimeDataService.GetCurrentSnapshot(elapsed));
RecordSnapshot(snapshot);
}
catch (Exception ex)
{
Debug.WriteLine($"Realtime snapshot polling failed: {ex.Message}");
}
finally
{
_isPollingSnapshot = false;
}
}
private void RecordSnapshot(RealtimeSnapshot snapshot)
@@ -70,9 +101,10 @@ public sealed class ExperimentDataService : IExperimentDataService
CurrentSnapshot = accumulatedSnapshot;
if (_isTestRunning)
if (_isTestRunning && ShouldRecordSnapshot(accumulatedSnapshot))
{
Records.Add(RealtimeDataRecord.FromSnapshot(accumulatedSnapshot));
_lastRecordedSeconds = accumulatedSnapshot.TestSeconds;
while (Records.Count > MaximumRows)
{
@@ -83,6 +115,12 @@ public sealed class ExperimentDataService : IExperimentDataService
SnapshotUpdated?.Invoke(this, accumulatedSnapshot);
}
private bool ShouldRecordSnapshot(RealtimeSnapshot snapshot)
{
return snapshot.TestSeconds >= 0
&& (!_lastRecordedSeconds.HasValue || snapshot.TestSeconds > _lastRecordedSeconds.Value);
}
private RealtimeSnapshot ApplyAccumulatedTotals(RealtimeSnapshot snapshot)
{
if (!_isTestRunning)
@@ -199,5 +237,6 @@ public sealed class ExperimentDataService : IExperimentDataService
_accumulatedTotalHeatRelease = 0;
_accumulatedTotalSmoke = 0;
_lastAccumulationSeconds = null;
_lastRecordedSeconds = null;
}
}

View File

@@ -14,7 +14,7 @@ public sealed class NpoiRealtimeDataExportService : IRealtimeDataExportService
"CO2 (%)",
"CO (%)",
"孔板压差 (Pa)",
"孔板温度 ()",
"孔板温度 (K)",
"HRR",
"热释放速率180",
"热释放速率300",

View File

@@ -23,7 +23,7 @@ public sealed class NpoiReportExportService : IReportExportService
"CO2 (%)",
"CO (%)",
"孔板压差 (Pa)",
"孔板温度 ()",
"孔板温度 (K)",
"MLR (g/s)",
"热释放KW/m2",
"EHC",

View File

@@ -17,6 +17,10 @@ public sealed class TestPageViewModel : PageViewModel
private const string ResetAction = "复位";
private const ushort ResetCoil = 88;
private const ushort ResetCompleteCoil = 89;
private const double MinimumHeatReleaseAxisMaximum = 150;
private const double MinimumTotalAxisMaximum = 150;
private const double PlotAxisPaddingFactor = 1.1;
private const int MaximumPlotPoints = 600;
private static readonly TimeSpan ResetPulseDuration = TimeSpan.FromMilliseconds(300);
private static readonly TimeSpan ResetCompletionTimeout = TimeSpan.FromSeconds(15);
@@ -29,6 +33,7 @@ public sealed class TestPageViewModel : PageViewModel
private DeviceActionViewModel _resetAction = null!;
private bool _flameDetected;
private bool _resetInProgress;
private int? _lastPlottedSeconds;
private DateTime _resetStartedAt;
private string _lastAction = "待机";
@@ -45,7 +50,7 @@ public sealed class TestPageViewModel : PageViewModel
new MetricDisplayViewModel("孔板压差", "Pa"),
new MetricDisplayViewModel("孔板温度", "K"),
new MetricDisplayViewModel("辐射锥温度", "°C"),
new MetricDisplayViewModel("辐射照度", "kW")
new MetricDisplayViewModel("辐射照度", "kW/㎡")
];
GasMetrics =
@@ -236,20 +241,36 @@ public sealed class TestPageViewModel : PageViewModel
FlameDetected = snapshot.FlameDetected;
OnPropertyChanged(nameof(FlameStatus));
UpdatePlot(snapshot);
}
private void UpdatePlot(RealtimeSnapshot snapshot)
{
if (!ShouldPlotSnapshot(snapshot))
{
return;
}
var x = snapshot.TestSeconds;
AppendPoint(_heatReleaseSeries, x, snapshot.HeatReleaseRate);
AppendPoint(_totalHeatSeries, x, snapshot.TotalHeatRelease);
AppendPoint(_totalSmokeSeries, x, snapshot.TotalSmoke);
_lastPlottedSeconds = snapshot.TestSeconds;
var bottomAxis = HeatReleasePlot.Axes.First(axis => axis.Position == AxisPosition.Bottom);
if (x > bottomAxis.Maximum - 20)
{
bottomAxis.Maximum = x + 40;
}
UpdateTimeAxis(x);
UpdateValueAxes();
HeatReleasePlot.InvalidatePlot(true);
}
private bool ShouldPlotSnapshot(RealtimeSnapshot snapshot)
{
return snapshot.TestSeconds >= 0
&& (!_lastPlottedSeconds.HasValue || snapshot.TestSeconds > _lastPlottedSeconds.Value)
&& double.IsFinite(snapshot.HeatReleaseRate)
&& double.IsFinite(snapshot.TotalHeatRelease)
&& double.IsFinite(snapshot.TotalSmoke);
}
private static void AppendPoint(LineSeries series, double x, double y)
{
if (!double.IsFinite(x) || x < 0 || !double.IsFinite(y))
@@ -259,12 +280,43 @@ public sealed class TestPageViewModel : PageViewModel
series.Points.Add(new DataPoint(x, y));
if (series.Points.Count > 600)
if (series.Points.Count > MaximumPlotPoints)
{
series.Points.RemoveAt(0);
}
}
private void UpdateTimeAxis(double latestSeconds)
{
var bottomAxis = HeatReleasePlot.Axes.First(axis => axis.Position == AxisPosition.Bottom);
bottomAxis.Minimum = Math.Max(0, latestSeconds - MaximumPlotPoints);
bottomAxis.Maximum = Math.Max(300, latestSeconds + 40);
}
private void UpdateValueAxes()
{
var heatReleaseAxis = HeatReleasePlot.Axes.First(axis => axis.Position == AxisPosition.Left);
heatReleaseAxis.Maximum = CalculateAxisMaximum(MinimumHeatReleaseAxisMaximum, _heatReleaseSeries);
var totalAxis = HeatReleasePlot.Axes.First(axis => axis.Key == "TotalAxis");
totalAxis.Maximum = CalculateAxisMaximum(
MinimumTotalAxisMaximum,
_totalHeatSeries,
_totalSmokeSeries);
}
private static double CalculateAxisMaximum(double minimumMaximum, params LineSeries[] seriesCollection)
{
var maximum = seriesCollection
.SelectMany(series => series.Points)
.Where(point => double.IsFinite(point.Y))
.Select(point => point.Y)
.DefaultIfEmpty(0)
.Max();
return Math.Max(minimumMaximum, Math.Ceiling(maximum * PlotAxisPaddingFactor));
}
public bool StartMomentaryDeviceAction(string? action)
{
return TryWriteMomentaryDeviceAction(action, true);
@@ -396,7 +448,10 @@ public sealed class TestPageViewModel : PageViewModel
_totalSmokeSeries.Points.Clear();
var bottomAxis = HeatReleasePlot.Axes.First(axis => axis.Position == AxisPosition.Bottom);
bottomAxis.Minimum = 0;
bottomAxis.Maximum = 300;
UpdateValueAxes();
_lastPlottedSeconds = null;
HeatReleasePlot.InvalidatePlot(true);
}

View File

@@ -112,7 +112,8 @@
</Grid>
</Border>
<DataGrid Grid.Row="1"
<DataGrid x:Name="RealtimeDataGrid"
Grid.Row="1"
ItemsSource="{Binding Rows}"
AutoGenerateColumns="False"
CanUserAddRows="False"
@@ -138,7 +139,7 @@
<DataGridTextColumn Header="CO2 (%)" Binding="{Binding CarbonDioxideText}" Width="88" ElementStyle="{StaticResource RealtimeTextElementStyle}" />
<DataGridTextColumn Header="CO (%)" Binding="{Binding CarbonMonoxideText}" Width="82" ElementStyle="{StaticResource RealtimeTextElementStyle}" />
<DataGridTextColumn Header="孔板压差 (Pa)" Binding="{Binding OrificePressureText}" Width="116" ElementStyle="{StaticResource RealtimeTextElementStyle}" />
<DataGridTextColumn Header="孔板温度 ()" Binding="{Binding OrificeTemperatureText}" Width="116" ElementStyle="{StaticResource RealtimeTextElementStyle}" />
<DataGridTextColumn Header="孔板温度 (K)" Binding="{Binding OrificeTemperatureText}" Width="116" ElementStyle="{StaticResource RealtimeTextElementStyle}" />
<DataGridTextColumn Header="HRR" Binding="{Binding Hrr50Text}" Width="78" ElementStyle="{StaticResource RealtimeTextElementStyle}" />
<DataGridTextColumn Header="热释放速率180" Binding="{Binding Qa180Text}" Width="118" ElementStyle="{StaticResource RealtimeTextElementStyle}" />
<DataGridTextColumn Header="热释放速率300" Binding="{Binding Qa300Text}" Width="118" ElementStyle="{StaticResource RealtimeTextElementStyle}" />

View File

@@ -1,11 +1,49 @@
using System.Collections.Specialized;
using System.Windows.Controls;
using System.Windows.Threading;
using ConeCalorimeter.ViewModels;
namespace ConeCalorimeter.Views;
public partial class RealtimeDataView : UserControl
{
private INotifyCollectionChanged? _rowsCollection;
public RealtimeDataView()
{
InitializeComponent();
DataContextChanged += (_, _) => SubscribeRowsCollection();
Unloaded += (_, _) => UnsubscribeRowsCollection();
}
private void SubscribeRowsCollection()
{
UnsubscribeRowsCollection();
if (DataContext is RealtimeDataViewModel viewModel)
{
_rowsCollection = viewModel.Rows;
_rowsCollection.CollectionChanged += RowsCollectionChanged;
}
}
private void UnsubscribeRowsCollection()
{
if (_rowsCollection is not null)
{
_rowsCollection.CollectionChanged -= RowsCollectionChanged;
_rowsCollection = null;
}
}
private void RowsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action != NotifyCollectionChangedAction.Add || e.NewItems is null || e.NewItems.Count == 0)
{
return;
}
var latest = e.NewItems[e.NewItems.Count - 1];
Dispatcher.BeginInvoke(new Action(() => RealtimeDataGrid.ScrollIntoView(latest)), DispatcherPriority.Background);
}
}