Files
ConeCalorimeter/ConeCalorimeter/Services/ExperimentDataService.cs
GukSang.Jin 5212c4f5ef 更新2027
2026-06-17 16:11:14 +08:00

516 lines
16 KiB
C#

using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Windows.Threading;
using ConeCalorimeter.Models;
using Serilog;
namespace ConeCalorimeter.Services;
public sealed class ExperimentDataService : IExperimentDataService
{
private const double TotalHeatReleaseDivisor = 1000;
private static readonly TimeSpan TimerInterval = TimeSpan.FromMilliseconds(250);
private static readonly TimeSpan DevicePollInterval = TimeSpan.FromSeconds(1);
private readonly IRealtimeDataService _realtimeDataService;
private readonly IHeatReleaseCorrectionService _heatReleaseCorrectionService;
private readonly BulkObservableCollection<RealtimeDataRecord> _records;
private readonly DispatcherTimer _timer;
private readonly Stopwatch _testClock = new();
private DateTime _lastDevicePollStartedAtUtc = DateTime.MinValue;
private double? _initialMass;
private double? _previousMass;
private int? _previousMassSeconds;
private double _accumulatedTotalHeatRelease;
private double _accumulatedTotalSmoke;
private int? _lastAccumulationSeconds;
private int? _lastRecordedSeconds;
private int? _deviceTestTimerBaselineSeconds;
private bool _hasObservedDeviceTestTimerReset;
private bool _isTestRunning;
private bool _isRecordingActive;
private bool _isPollingSnapshot;
private RealtimeSnapshot? _completedSnapshot;
public ExperimentDataService(
IRealtimeDataService realtimeDataService,
IHeatReleaseCorrectionService heatReleaseCorrectionService)
{
_realtimeDataService = realtimeDataService;
_heatReleaseCorrectionService = heatReleaseCorrectionService;
_records = [];
Records = _records;
CurrentSnapshot = BuildIdleSnapshot(_realtimeDataService.GetCurrentSnapshot(TimeSpan.Zero));
HeatReleaseCorrectionStatus = "尚未完成测试,未生成正式 HRR 校正结果。";
_timer = new DispatcherTimer
{
Interval = TimerInterval
};
_timer.Tick += async (_, _) => await TickAsync();
_timer.Start();
}
public ObservableCollection<RealtimeDataRecord> Records { get; }
public RealtimeSnapshot CurrentSnapshot { get; private set; }
public bool IsHeatReleaseCorrectionApplied { get; private set; }
public string HeatReleaseCorrectionStatus { get; private set; }
public event EventHandler<RealtimeSnapshot>? SnapshotUpdated;
public event EventHandler? HeatReleaseCorrectionStatusChanged;
public void StartTest()
{
Records.Clear();
ResetComputedState();
_completedSnapshot = null;
SetHeatReleaseCorrectionStatus(false, "测试进行中,当前显示和记录为原始 HRR/THR。");
_isTestRunning = true;
_isRecordingActive = false;
_testClock.Reset();
PrepareDeviceTestTimerGate();
PublishSnapshot(BuildWaitingSnapshot(CurrentSnapshot));
QueueImmediateDevicePoll();
Log.Information(
"Experiment test started. Waiting for D1015 timing marker. DeviceTimerBaseline={DeviceTimerBaseline}.",
_deviceTestTimerBaselineSeconds);
}
public void StopTest()
{
_isTestRunning = false;
_isRecordingActive = false;
_testClock.Stop();
ApplyFinalHeatReleaseCorrection();
_completedSnapshot = BuildCompletedSnapshot(CurrentSnapshot);
PublishSnapshot(BuildIdleSnapshot(CurrentSnapshot));
Log.Information(
"Experiment test stopped. Records={RecordCount}, LastRecordedSeconds={LastRecordedSeconds}.",
Records.Count,
_lastRecordedSeconds);
}
public void ClearRecords()
{
Records.Clear();
if (!_isTestRunning)
{
_testClock.Reset();
_isRecordingActive = false;
_completedSnapshot = null;
SetHeatReleaseCorrectionStatus(false, "记录已清空,未生成正式 HRR 校正结果。");
ResetComputedState();
PublishSnapshot(BuildIdleSnapshot(CurrentSnapshot));
}
}
private async Task TickAsync()
{
await RefreshDeviceSnapshotAsync();
}
private void QueueImmediateDevicePoll()
{
_lastDevicePollStartedAtUtc = DateTime.MinValue;
_ = RefreshDeviceSnapshotAsync();
}
private async Task RefreshDeviceSnapshotAsync()
{
if (!TryBeginDevicePoll())
{
return;
}
try
{
var elapsed = _isTestRunning && _isRecordingActive ? _testClock.Elapsed : TimeSpan.Zero;
var snapshot = await Task.Run(() => _realtimeDataService.GetCurrentSnapshot(elapsed));
if (_isTestRunning)
{
RecordDeviceSnapshot(snapshot);
}
else
{
PublishSnapshot(BuildIdleSnapshot(snapshot));
}
}
catch (Exception ex)
{
Debug.WriteLine($"Realtime snapshot polling failed: {ex.Message}");
Log.Warning(ex, "Realtime snapshot polling failed.");
}
finally
{
_isPollingSnapshot = false;
}
}
private bool TryBeginDevicePoll()
{
if (_isPollingSnapshot)
{
return false;
}
var now = DateTime.UtcNow;
if (now - _lastDevicePollStartedAtUtc < DevicePollInterval)
{
return false;
}
_lastDevicePollStartedAtUtc = now;
_isPollingSnapshot = true;
return true;
}
private void RecordDeviceSnapshot(RealtimeSnapshot snapshot)
{
if (!_isTestRunning)
{
return;
}
if (!_isRecordingActive)
{
if (!ShouldStartTimedRecording(snapshot))
{
PublishSnapshot(BuildWaitingSnapshot(snapshot));
return;
}
BeginTimedRecording(snapshot);
snapshot = snapshot with
{
TestSeconds = 0
};
}
else
{
snapshot = snapshot with
{
TestSeconds = GetElapsedTestSeconds()
};
}
var currentSecond = Math.Max(0, snapshot.TestSeconds);
if (_lastRecordedSeconds.HasValue && currentSecond <= _lastRecordedSeconds.Value)
{
PublishSnapshot(BuildDisplaySnapshot(snapshot));
return;
}
var timedSnapshot = snapshot with
{
TestSeconds = currentSecond
};
var massSnapshot = ApplyMassLoss(timedSnapshot);
var accumulatedSnapshot = ApplyAccumulatedTotals(massSnapshot);
PublishSnapshot(accumulatedSnapshot);
Records.Add(RealtimeDataRecord.FromSnapshot(accumulatedSnapshot));
_lastRecordedSeconds = currentSecond;
}
private RealtimeSnapshot BuildDisplaySnapshot(RealtimeSnapshot snapshot)
{
if (!_isTestRunning)
{
return BuildIdleSnapshot(snapshot);
}
if (!_isRecordingActive)
{
return BuildWaitingSnapshot(snapshot);
}
var timedSnapshot = snapshot with
{
TestSeconds = GetElapsedTestSeconds()
};
if (!_initialMass.HasValue && double.IsFinite(timedSnapshot.CurrentMass))
{
_initialMass = timedSnapshot.CurrentMass;
}
var massLoss = _initialMass.HasValue && double.IsFinite(timedSnapshot.CurrentMass)
? _initialMass.Value - timedSnapshot.CurrentMass
: double.NaN;
return timedSnapshot with
{
InitialMass = _initialMass ?? double.NaN,
MassLoss = massLoss,
MassLossRate = CurrentSnapshot.MassLossRate,
TotalHeatRelease = _accumulatedTotalHeatRelease,
TotalSmoke = _accumulatedTotalSmoke
};
}
private RealtimeSnapshot BuildIdleSnapshot(RealtimeSnapshot snapshot)
{
var completedSnapshot = _completedSnapshot;
return snapshot with
{
HeatReleaseRate = completedSnapshot?.HeatReleaseRate ?? snapshot.HeatReleaseRate,
PeakHeatReleaseRate = completedSnapshot?.PeakHeatReleaseRate ?? snapshot.PeakHeatReleaseRate,
TestSeconds = completedSnapshot?.TestSeconds ?? -1,
InitialMass = completedSnapshot?.InitialMass ?? double.NaN,
MassLoss = completedSnapshot?.MassLoss ?? double.NaN,
MassLossRate = completedSnapshot?.MassLossRate ?? double.NaN,
TotalHeatRelease = completedSnapshot?.TotalHeatRelease ?? double.NaN,
TotalSmoke = completedSnapshot?.TotalSmoke ?? double.NaN
};
}
private RealtimeSnapshot BuildWaitingSnapshot(RealtimeSnapshot snapshot)
{
var displayInitialMass = double.IsFinite(snapshot.CurrentMass)
? snapshot.CurrentMass
: double.NaN;
return snapshot with
{
TestSeconds = -1,
InitialMass = displayInitialMass,
MassLoss = double.IsFinite(displayInitialMass) ? 0 : double.NaN,
MassLossRate = double.NaN,
TotalHeatRelease = 0,
TotalSmoke = 0
};
}
private RealtimeSnapshot? BuildCompletedSnapshot(RealtimeSnapshot snapshot)
{
if (Records.Count == 0)
{
return snapshot.TestSeconds >= 0 ? snapshot : null;
}
var lastRecord = Records[Records.Count - 1];
return snapshot with
{
HeatReleaseRate = lastRecord.HeatReleaseRate,
PeakHeatReleaseRate = lastRecord.PeakHeatReleaseRate,
TotalHeatRelease = lastRecord.TotalHeatRelease,
InitialMass = lastRecord.InitialMass,
MassLoss = lastRecord.MassLoss,
MassLossRate = lastRecord.MassLossRate,
IgnitionSeconds = lastRecord.IgnitionSeconds,
TestSeconds = lastRecord.TestSeconds,
TotalSmoke = lastRecord.TotalSmoke
};
}
private void ApplyFinalHeatReleaseCorrection()
{
if (Records.Count == 0)
{
SetHeatReleaseCorrectionStatus(false, "没有有效记录,无法生成正式 HRR 校正结果。");
return;
}
var result = _heatReleaseCorrectionService.Correct(Records.ToList());
if (result.IsApplied)
{
_records.ReplaceAll(result.Records);
}
SetHeatReleaseCorrectionStatus(result.IsApplied, result.Status);
}
private void SetHeatReleaseCorrectionStatus(bool isApplied, string status)
{
IsHeatReleaseCorrectionApplied = isApplied;
HeatReleaseCorrectionStatus = status;
HeatReleaseCorrectionStatusChanged?.Invoke(this, EventArgs.Empty);
}
private void PrepareDeviceTestTimerGate()
{
var currentDeviceSeconds = CurrentSnapshot.DeviceTestSeconds;
_deviceTestTimerBaselineSeconds = currentDeviceSeconds >= 0 ? currentDeviceSeconds : null;
_hasObservedDeviceTestTimerReset = !_deviceTestTimerBaselineSeconds.HasValue
|| _deviceTestTimerBaselineSeconds.Value <= 0;
}
private bool ShouldStartTimedRecording(RealtimeSnapshot snapshot)
{
var deviceSeconds = snapshot.DeviceTestSeconds;
if (deviceSeconds < 0)
{
return false;
}
if (!_deviceTestTimerBaselineSeconds.HasValue || _hasObservedDeviceTestTimerReset)
{
return deviceSeconds > 0;
}
var baselineSeconds = _deviceTestTimerBaselineSeconds.Value;
if (deviceSeconds <= 0)
{
_hasObservedDeviceTestTimerReset = true;
return false;
}
return deviceSeconds != baselineSeconds;
}
private void BeginTimedRecording(RealtimeSnapshot snapshot)
{
_isRecordingActive = true;
_testClock.Restart();
if (double.IsFinite(snapshot.CurrentMass))
{
_initialMass = snapshot.CurrentMass;
}
Log.Information(
"Experiment timed recording started by D1015. DeviceTestSeconds={DeviceTestSeconds}, InitialMass={InitialMass}.",
snapshot.DeviceTestSeconds,
_initialMass);
}
private int GetElapsedTestSeconds()
{
return Math.Max(0, (int)Math.Floor(_testClock.Elapsed.TotalSeconds));
}
private void PublishSnapshot(RealtimeSnapshot snapshot)
{
CurrentSnapshot = snapshot;
SnapshotUpdated?.Invoke(this, snapshot);
}
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 / TotalHeatReleaseDivisor;
}
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;
}
}