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