更新2026522
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ public sealed class NpoiRealtimeDataExportService : IRealtimeDataExportService
|
||||
"CO2 (%)",
|
||||
"CO (%)",
|
||||
"孔板压差 (Pa)",
|
||||
"孔板温度 (℃)",
|
||||
"孔板温度 (K)",
|
||||
"HRR",
|
||||
"热释放速率180",
|
||||
"热释放速率300",
|
||||
|
||||
@@ -23,7 +23,7 @@ public sealed class NpoiReportExportService : IReportExportService
|
||||
"CO2 (%)",
|
||||
"CO (%)",
|
||||
"孔板压差 (Pa)",
|
||||
"孔板温度 (℃)",
|
||||
"孔板温度 (K)",
|
||||
"MLR (g/s)",
|
||||
"热释放KW/m2",
|
||||
"EHC",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}" />
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user