Files
ConeCalorimeter/ConeCalorimeter/Services/ExperimentDataService.cs
2026-05-22 18:27:18 +08:00

243 lines
6.8 KiB
C#

using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Windows.Threading;
using ConeCalorimeter.Models;
namespace ConeCalorimeter.Services;
public sealed class ExperimentDataService : IExperimentDataService
{
private const int MaximumRows = 1200;
private readonly IRealtimeDataService _realtimeDataService;
private readonly DispatcherTimer _timer;
private readonly DateTime _startedAt = DateTime.Now;
private double? _initialMass;
private double? _previousMass;
private int? _previousMassSeconds;
private double _accumulatedTotalHeatRelease;
private double _accumulatedTotalSmoke;
private int? _lastAccumulationSeconds;
private int? _lastRecordedSeconds;
private bool _isTestRunning;
private bool _isPollingSnapshot;
public ExperimentDataService(IRealtimeDataService realtimeDataService)
{
_realtimeDataService = realtimeDataService;
Records = [];
CurrentSnapshot = _realtimeDataService.GetCurrentSnapshot(TimeSpan.Zero);
_timer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(1)
};
_timer.Tick += async (_, _) => await RecordCurrentSnapshotAsync();
_timer.Start();
}
public ObservableCollection<RealtimeDataRecord> Records { get; }
public RealtimeSnapshot CurrentSnapshot { get; private set; }
public event EventHandler<RealtimeSnapshot>? SnapshotUpdated;
public void StartTest()
{
Records.Clear();
ResetComputedState();
_isTestRunning = true;
if (double.IsFinite(CurrentSnapshot.CurrentMass))
{
_initialMass = CurrentSnapshot.CurrentMass;
}
}
public void StopTest()
{
_isTestRunning = false;
}
public void ClearRecords()
{
Records.Clear();
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)
{
var massSnapshot = ApplyMassLoss(snapshot);
var accumulatedSnapshot = ApplyAccumulatedTotals(massSnapshot);
CurrentSnapshot = accumulatedSnapshot;
if (_isTestRunning && ShouldRecordSnapshot(accumulatedSnapshot))
{
Records.Add(RealtimeDataRecord.FromSnapshot(accumulatedSnapshot));
_lastRecordedSeconds = accumulatedSnapshot.TestSeconds;
while (Records.Count > MaximumRows)
{
Records.RemoveAt(0);
}
}
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)
{
return snapshot with
{
TotalHeatRelease = double.NaN,
TotalSmoke = double.NaN
};
}
if (snapshot.TestSeconds < 0)
{
return snapshot with
{
TotalHeatRelease = _accumulatedTotalHeatRelease,
TotalSmoke = _accumulatedTotalSmoke
};
}
if (!_lastAccumulationSeconds.HasValue)
{
_lastAccumulationSeconds = snapshot.TestSeconds;
return snapshot with
{
TotalHeatRelease = _accumulatedTotalHeatRelease,
TotalSmoke = _accumulatedTotalSmoke
};
}
var deltaSeconds = snapshot.TestSeconds - _lastAccumulationSeconds.Value;
if (deltaSeconds > 0)
{
if (double.IsFinite(snapshot.HeatReleaseRate))
{
_accumulatedTotalHeatRelease += snapshot.HeatReleaseRate * deltaSeconds / 1000;
}
if (double.IsFinite(snapshot.SmokeProduction))
{
_accumulatedTotalSmoke += snapshot.SmokeProduction * deltaSeconds;
}
_lastAccumulationSeconds = snapshot.TestSeconds;
}
return snapshot with
{
TotalHeatRelease = _accumulatedTotalHeatRelease,
TotalSmoke = _accumulatedTotalSmoke
};
}
private RealtimeSnapshot ApplyMassLoss(RealtimeSnapshot snapshot)
{
if (!_isTestRunning)
{
return snapshot with
{
InitialMass = _initialMass ?? double.NaN,
MassLoss = double.NaN,
MassLossRate = double.NaN
};
}
if (!_initialMass.HasValue && double.IsFinite(snapshot.CurrentMass))
{
_initialMass = snapshot.CurrentMass;
}
var massLoss = _initialMass.HasValue && double.IsFinite(snapshot.CurrentMass)
? _initialMass.Value - snapshot.CurrentMass
: double.NaN;
var massLossRate = CalculateMassLossRate(snapshot);
return snapshot with
{
InitialMass = _initialMass ?? double.NaN,
MassLoss = massLoss,
MassLossRate = massLossRate
};
}
private double CalculateMassLossRate(RealtimeSnapshot snapshot)
{
if (snapshot.TestSeconds < 0 || !double.IsFinite(snapshot.CurrentMass))
{
return double.NaN;
}
if (!_previousMass.HasValue || !_previousMassSeconds.HasValue)
{
_previousMass = snapshot.CurrentMass;
_previousMassSeconds = snapshot.TestSeconds;
return double.NaN;
}
var deltaSeconds = snapshot.TestSeconds - _previousMassSeconds.Value;
var deltaMass = _previousMass.Value - snapshot.CurrentMass;
_previousMass = snapshot.CurrentMass;
_previousMassSeconds = snapshot.TestSeconds;
return deltaSeconds > 0 && deltaMass >= 0
? deltaMass / deltaSeconds
: double.NaN;
}
private void ResetComputedState()
{
_initialMass = null;
_previousMass = null;
_previousMassSeconds = null;
_accumulatedTotalHeatRelease = 0;
_accumulatedTotalSmoke = 0;
_lastAccumulationSeconds = null;
_lastRecordedSeconds = null;
}
}