243 lines
6.8 KiB
C#
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;
|
|
}
|
|
}
|