更新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.Collections.ObjectModel;
using System.Diagnostics;
using System.Windows.Threading; using System.Windows.Threading;
using ConeCalorimeter.Models; using ConeCalorimeter.Models;
@@ -17,7 +18,9 @@ public sealed class ExperimentDataService : IExperimentDataService
private double _accumulatedTotalHeatRelease; private double _accumulatedTotalHeatRelease;
private double _accumulatedTotalSmoke; private double _accumulatedTotalSmoke;
private int? _lastAccumulationSeconds; private int? _lastAccumulationSeconds;
private int? _lastRecordedSeconds;
private bool _isTestRunning; private bool _isTestRunning;
private bool _isPollingSnapshot;
public ExperimentDataService(IRealtimeDataService realtimeDataService) public ExperimentDataService(IRealtimeDataService realtimeDataService)
{ {
@@ -29,8 +32,7 @@ public sealed class ExperimentDataService : IExperimentDataService
{ {
Interval = TimeSpan.FromSeconds(1) Interval = TimeSpan.FromSeconds(1)
}; };
_timer.Tick += (_, _) => RecordSnapshot( _timer.Tick += async (_, _) => await RecordCurrentSnapshotAsync();
_realtimeDataService.GetCurrentSnapshot(DateTime.Now - _startedAt));
_timer.Start(); _timer.Start();
} }
@@ -60,8 +62,37 @@ public sealed class ExperimentDataService : IExperimentDataService
public void ClearRecords() public void ClearRecords()
{ {
Records.Clear(); Records.Clear();
if (!_isTestRunning)
{
ResetComputedState(); 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) private void RecordSnapshot(RealtimeSnapshot snapshot)
{ {
@@ -70,9 +101,10 @@ public sealed class ExperimentDataService : IExperimentDataService
CurrentSnapshot = accumulatedSnapshot; CurrentSnapshot = accumulatedSnapshot;
if (_isTestRunning) if (_isTestRunning && ShouldRecordSnapshot(accumulatedSnapshot))
{ {
Records.Add(RealtimeDataRecord.FromSnapshot(accumulatedSnapshot)); Records.Add(RealtimeDataRecord.FromSnapshot(accumulatedSnapshot));
_lastRecordedSeconds = accumulatedSnapshot.TestSeconds;
while (Records.Count > MaximumRows) while (Records.Count > MaximumRows)
{ {
@@ -83,6 +115,12 @@ public sealed class ExperimentDataService : IExperimentDataService
SnapshotUpdated?.Invoke(this, accumulatedSnapshot); SnapshotUpdated?.Invoke(this, accumulatedSnapshot);
} }
private bool ShouldRecordSnapshot(RealtimeSnapshot snapshot)
{
return snapshot.TestSeconds >= 0
&& (!_lastRecordedSeconds.HasValue || snapshot.TestSeconds > _lastRecordedSeconds.Value);
}
private RealtimeSnapshot ApplyAccumulatedTotals(RealtimeSnapshot snapshot) private RealtimeSnapshot ApplyAccumulatedTotals(RealtimeSnapshot snapshot)
{ {
if (!_isTestRunning) if (!_isTestRunning)
@@ -199,5 +237,6 @@ public sealed class ExperimentDataService : IExperimentDataService
_accumulatedTotalHeatRelease = 0; _accumulatedTotalHeatRelease = 0;
_accumulatedTotalSmoke = 0; _accumulatedTotalSmoke = 0;
_lastAccumulationSeconds = null; _lastAccumulationSeconds = null;
_lastRecordedSeconds = null;
} }
} }

View File

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

View File

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

View File

@@ -17,6 +17,10 @@ public sealed class TestPageViewModel : PageViewModel
private const string ResetAction = "复位"; private const string ResetAction = "复位";
private const ushort ResetCoil = 88; private const ushort ResetCoil = 88;
private const ushort ResetCompleteCoil = 89; 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 ResetPulseDuration = TimeSpan.FromMilliseconds(300);
private static readonly TimeSpan ResetCompletionTimeout = TimeSpan.FromSeconds(15); private static readonly TimeSpan ResetCompletionTimeout = TimeSpan.FromSeconds(15);
@@ -29,6 +33,7 @@ public sealed class TestPageViewModel : PageViewModel
private DeviceActionViewModel _resetAction = null!; private DeviceActionViewModel _resetAction = null!;
private bool _flameDetected; private bool _flameDetected;
private bool _resetInProgress; private bool _resetInProgress;
private int? _lastPlottedSeconds;
private DateTime _resetStartedAt; private DateTime _resetStartedAt;
private string _lastAction = "待机"; private string _lastAction = "待机";
@@ -45,7 +50,7 @@ public sealed class TestPageViewModel : PageViewModel
new MetricDisplayViewModel("孔板压差", "Pa"), new MetricDisplayViewModel("孔板压差", "Pa"),
new MetricDisplayViewModel("孔板温度", "K"), new MetricDisplayViewModel("孔板温度", "K"),
new MetricDisplayViewModel("辐射锥温度", "°C"), new MetricDisplayViewModel("辐射锥温度", "°C"),
new MetricDisplayViewModel("辐射照度", "kW") new MetricDisplayViewModel("辐射照度", "kW/㎡")
]; ];
GasMetrics = GasMetrics =
@@ -236,18 +241,34 @@ public sealed class TestPageViewModel : PageViewModel
FlameDetected = snapshot.FlameDetected; FlameDetected = snapshot.FlameDetected;
OnPropertyChanged(nameof(FlameStatus)); OnPropertyChanged(nameof(FlameStatus));
UpdatePlot(snapshot);
}
private void UpdatePlot(RealtimeSnapshot snapshot)
{
if (!ShouldPlotSnapshot(snapshot))
{
return;
}
var x = snapshot.TestSeconds; var x = snapshot.TestSeconds;
AppendPoint(_heatReleaseSeries, x, snapshot.HeatReleaseRate); AppendPoint(_heatReleaseSeries, x, snapshot.HeatReleaseRate);
AppendPoint(_totalHeatSeries, x, snapshot.TotalHeatRelease); AppendPoint(_totalHeatSeries, x, snapshot.TotalHeatRelease);
AppendPoint(_totalSmokeSeries, x, snapshot.TotalSmoke); AppendPoint(_totalSmokeSeries, x, snapshot.TotalSmoke);
_lastPlottedSeconds = snapshot.TestSeconds;
var bottomAxis = HeatReleasePlot.Axes.First(axis => axis.Position == AxisPosition.Bottom); UpdateTimeAxis(x);
if (x > bottomAxis.Maximum - 20) UpdateValueAxes();
{ HeatReleasePlot.InvalidatePlot(true);
bottomAxis.Maximum = x + 40;
} }
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) private static void AppendPoint(LineSeries series, double x, double y)
@@ -259,12 +280,43 @@ public sealed class TestPageViewModel : PageViewModel
series.Points.Add(new DataPoint(x, y)); series.Points.Add(new DataPoint(x, y));
if (series.Points.Count > 600) if (series.Points.Count > MaximumPlotPoints)
{ {
series.Points.RemoveAt(0); 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) public bool StartMomentaryDeviceAction(string? action)
{ {
return TryWriteMomentaryDeviceAction(action, true); return TryWriteMomentaryDeviceAction(action, true);
@@ -396,7 +448,10 @@ public sealed class TestPageViewModel : PageViewModel
_totalSmokeSeries.Points.Clear(); _totalSmokeSeries.Points.Clear();
var bottomAxis = HeatReleasePlot.Axes.First(axis => axis.Position == AxisPosition.Bottom); var bottomAxis = HeatReleasePlot.Axes.First(axis => axis.Position == AxisPosition.Bottom);
bottomAxis.Minimum = 0;
bottomAxis.Maximum = 300; bottomAxis.Maximum = 300;
UpdateValueAxes();
_lastPlottedSeconds = null;
HeatReleasePlot.InvalidatePlot(true); HeatReleasePlot.InvalidatePlot(true);
} }

View File

@@ -112,7 +112,8 @@
</Grid> </Grid>
</Border> </Border>
<DataGrid Grid.Row="1" <DataGrid x:Name="RealtimeDataGrid"
Grid.Row="1"
ItemsSource="{Binding Rows}" ItemsSource="{Binding Rows}"
AutoGenerateColumns="False" AutoGenerateColumns="False"
CanUserAddRows="False" CanUserAddRows="False"
@@ -138,7 +139,7 @@
<DataGridTextColumn Header="CO2 (%)" Binding="{Binding CarbonDioxideText}" Width="88" ElementStyle="{StaticResource RealtimeTextElementStyle}" /> <DataGridTextColumn Header="CO2 (%)" Binding="{Binding CarbonDioxideText}" Width="88" ElementStyle="{StaticResource RealtimeTextElementStyle}" />
<DataGridTextColumn Header="CO (%)" Binding="{Binding CarbonMonoxideText}" Width="82" 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="孔板压差 (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="HRR" Binding="{Binding Hrr50Text}" Width="78" ElementStyle="{StaticResource RealtimeTextElementStyle}" />
<DataGridTextColumn Header="热释放速率180" Binding="{Binding Qa180Text}" Width="118" 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}" /> <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.Controls;
using System.Windows.Threading;
using ConeCalorimeter.ViewModels;
namespace ConeCalorimeter.Views; namespace ConeCalorimeter.Views;
public partial class RealtimeDataView : UserControl public partial class RealtimeDataView : UserControl
{ {
private INotifyCollectionChanged? _rowsCollection;
public RealtimeDataView() public RealtimeDataView()
{ {
InitializeComponent(); 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);
} }
} }