更新11111
This commit is contained in:
@@ -8,12 +8,14 @@ namespace ConeCalorimeter.Services;
|
|||||||
|
|
||||||
public sealed class ExperimentDataService : IExperimentDataService
|
public sealed class ExperimentDataService : IExperimentDataService
|
||||||
{
|
{
|
||||||
private const int MaximumRows = 1200;
|
|
||||||
private const double TotalHeatReleaseDivisor = 1000;
|
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 IRealtimeDataService _realtimeDataService;
|
||||||
private readonly DispatcherTimer _timer;
|
private readonly DispatcherTimer _timer;
|
||||||
private readonly DateTime _startedAt = DateTime.Now;
|
private readonly Stopwatch _testClock = new();
|
||||||
|
private DateTime _lastDevicePollStartedAtUtc = DateTime.MinValue;
|
||||||
private double? _initialMass;
|
private double? _initialMass;
|
||||||
private double? _previousMass;
|
private double? _previousMass;
|
||||||
private int? _previousMassSeconds;
|
private int? _previousMassSeconds;
|
||||||
@@ -28,13 +30,13 @@ public sealed class ExperimentDataService : IExperimentDataService
|
|||||||
{
|
{
|
||||||
_realtimeDataService = realtimeDataService;
|
_realtimeDataService = realtimeDataService;
|
||||||
Records = [];
|
Records = [];
|
||||||
CurrentSnapshot = _realtimeDataService.GetCurrentSnapshot(TimeSpan.Zero);
|
CurrentSnapshot = BuildIdleSnapshot(_realtimeDataService.GetCurrentSnapshot(TimeSpan.Zero));
|
||||||
|
|
||||||
_timer = new DispatcherTimer
|
_timer = new DispatcherTimer
|
||||||
{
|
{
|
||||||
Interval = TimeSpan.FromSeconds(1)
|
Interval = TimerInterval
|
||||||
};
|
};
|
||||||
_timer.Tick += async (_, _) => await RecordCurrentSnapshotAsync();
|
_timer.Tick += async (_, _) => await TickAsync();
|
||||||
_timer.Start();
|
_timer.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,21 +51,33 @@ public sealed class ExperimentDataService : IExperimentDataService
|
|||||||
Records.Clear();
|
Records.Clear();
|
||||||
ResetComputedState();
|
ResetComputedState();
|
||||||
_isTestRunning = true;
|
_isTestRunning = true;
|
||||||
|
_testClock.Restart();
|
||||||
|
|
||||||
if (double.IsFinite(CurrentSnapshot.CurrentMass))
|
if (double.IsFinite(CurrentSnapshot.CurrentMass))
|
||||||
{
|
{
|
||||||
_initialMass = CurrentSnapshot.CurrentMass;
|
_initialMass = CurrentSnapshot.CurrentMass;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RecordElapsedSnapshots();
|
||||||
|
QueueImmediateDevicePoll();
|
||||||
|
|
||||||
Log.Information(
|
Log.Information(
|
||||||
"Experiment test started. InitialMass={InitialMass}, SnapshotSeconds={TestSeconds}.",
|
"Experiment test started. InitialMass={InitialMass}, SystemSeconds={TestSeconds}.",
|
||||||
_initialMass,
|
_initialMass,
|
||||||
CurrentSnapshot.TestSeconds);
|
CurrentSnapshot.TestSeconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void StopTest()
|
public void StopTest()
|
||||||
{
|
{
|
||||||
|
if (_isTestRunning)
|
||||||
|
{
|
||||||
|
RecordElapsedSnapshots();
|
||||||
|
}
|
||||||
|
|
||||||
_isTestRunning = false;
|
_isTestRunning = false;
|
||||||
|
_testClock.Stop();
|
||||||
|
PublishSnapshot(BuildIdleSnapshot(CurrentSnapshot));
|
||||||
|
|
||||||
Log.Information(
|
Log.Information(
|
||||||
"Experiment test stopped. Records={RecordCount}, LastRecordedSeconds={LastRecordedSeconds}.",
|
"Experiment test stopped. Records={RecordCount}, LastRecordedSeconds={LastRecordedSeconds}.",
|
||||||
Records.Count,
|
Records.Count,
|
||||||
@@ -76,24 +90,40 @@ public sealed class ExperimentDataService : IExperimentDataService
|
|||||||
|
|
||||||
if (!_isTestRunning)
|
if (!_isTestRunning)
|
||||||
{
|
{
|
||||||
|
_testClock.Reset();
|
||||||
ResetComputedState();
|
ResetComputedState();
|
||||||
|
PublishSnapshot(BuildIdleSnapshot(CurrentSnapshot));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RecordCurrentSnapshotAsync()
|
private async Task TickAsync()
|
||||||
{
|
{
|
||||||
if (_isPollingSnapshot)
|
if (_isTestRunning)
|
||||||
|
{
|
||||||
|
RecordElapsedSnapshots();
|
||||||
|
}
|
||||||
|
|
||||||
|
await RefreshDeviceSnapshotAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void QueueImmediateDevicePoll()
|
||||||
|
{
|
||||||
|
_lastDevicePollStartedAtUtc = DateTime.MinValue;
|
||||||
|
_ = RefreshDeviceSnapshotAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RefreshDeviceSnapshotAsync()
|
||||||
|
{
|
||||||
|
if (!TryBeginDevicePoll())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_isPollingSnapshot = true;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var elapsed = DateTime.Now - _startedAt;
|
var elapsed = _isTestRunning ? _testClock.Elapsed : TimeSpan.Zero;
|
||||||
var snapshot = await Task.Run(() => _realtimeDataService.GetCurrentSnapshot(elapsed));
|
var snapshot = await Task.Run(() => _realtimeDataService.GetCurrentSnapshot(elapsed));
|
||||||
RecordSnapshot(snapshot);
|
PublishSnapshot(BuildDisplaySnapshot(snapshot));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -106,31 +136,102 @@ public sealed class ExperimentDataService : IExperimentDataService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RecordSnapshot(RealtimeSnapshot snapshot)
|
private bool TryBeginDevicePoll()
|
||||||
{
|
{
|
||||||
var massSnapshot = ApplyMassLoss(snapshot);
|
if (_isPollingSnapshot)
|
||||||
var accumulatedSnapshot = ApplyAccumulatedTotals(massSnapshot);
|
|
||||||
|
|
||||||
CurrentSnapshot = accumulatedSnapshot;
|
|
||||||
|
|
||||||
if (_isTestRunning && ShouldRecordSnapshot(accumulatedSnapshot))
|
|
||||||
{
|
{
|
||||||
Records.Add(RealtimeDataRecord.FromSnapshot(accumulatedSnapshot));
|
return false;
|
||||||
_lastRecordedSeconds = accumulatedSnapshot.TestSeconds;
|
|
||||||
|
|
||||||
while (Records.Count > MaximumRows)
|
|
||||||
{
|
|
||||||
Records.RemoveAt(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SnapshotUpdated?.Invoke(this, accumulatedSnapshot);
|
var now = DateTime.UtcNow;
|
||||||
|
if (now - _lastDevicePollStartedAtUtc < DevicePollInterval)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastDevicePollStartedAtUtc = now;
|
||||||
|
_isPollingSnapshot = true;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool ShouldRecordSnapshot(RealtimeSnapshot snapshot)
|
private void RecordElapsedSnapshots()
|
||||||
{
|
{
|
||||||
return snapshot.TestSeconds >= 0
|
if (!_isTestRunning)
|
||||||
&& (!_lastRecordedSeconds.HasValue || snapshot.TestSeconds > _lastRecordedSeconds.Value);
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentSecond = GetElapsedTestSeconds();
|
||||||
|
var nextSecond = _lastRecordedSeconds.HasValue ? _lastRecordedSeconds.Value + 1 : 0;
|
||||||
|
|
||||||
|
for (var second = nextSecond; second <= currentSecond; second++)
|
||||||
|
{
|
||||||
|
var timedSnapshot = CurrentSnapshot with
|
||||||
|
{
|
||||||
|
TestSeconds = second
|
||||||
|
};
|
||||||
|
var massSnapshot = ApplyMassLoss(timedSnapshot);
|
||||||
|
var accumulatedSnapshot = ApplyAccumulatedTotals(massSnapshot);
|
||||||
|
|
||||||
|
PublishSnapshot(accumulatedSnapshot);
|
||||||
|
Records.Add(RealtimeDataRecord.FromSnapshot(accumulatedSnapshot));
|
||||||
|
_lastRecordedSeconds = second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private RealtimeSnapshot BuildDisplaySnapshot(RealtimeSnapshot snapshot)
|
||||||
|
{
|
||||||
|
if (!_isTestRunning)
|
||||||
|
{
|
||||||
|
return BuildIdleSnapshot(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)
|
||||||
|
{
|
||||||
|
return snapshot with
|
||||||
|
{
|
||||||
|
TestSeconds = -1,
|
||||||
|
InitialMass = _initialMass ?? double.NaN,
|
||||||
|
MassLoss = double.NaN,
|
||||||
|
MassLossRate = double.NaN,
|
||||||
|
TotalHeatRelease = double.NaN,
|
||||||
|
TotalSmoke = double.NaN
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
private RealtimeSnapshot ApplyAccumulatedTotals(RealtimeSnapshot snapshot)
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService
|
|||||||
private const ushort SmokeProductionRegister = 392;
|
private const ushort SmokeProductionRegister = 392;
|
||||||
private const ushort IrradianceRegister = 410;
|
private const ushort IrradianceRegister = 410;
|
||||||
private const ushort IgnitionSecondsRegister = 1014;
|
private const ushort IgnitionSecondsRegister = 1014;
|
||||||
private const ushort TestSecondsRegister = 1015;
|
|
||||||
private const ushort M3FlameMonitorBit = 3;
|
private const ushort M3FlameMonitorBit = 3;
|
||||||
private static readonly ModbusFloatByteOrder[] RealtimeFloatByteOrders =
|
private static readonly ModbusFloatByteOrder[] RealtimeFloatByteOrders =
|
||||||
[
|
[
|
||||||
@@ -85,10 +84,17 @@ public sealed class ModbusRealtimeDataService : IRealtimeDataService
|
|||||||
MassLoss: double.NaN,
|
MassLoss: double.NaN,
|
||||||
MassLossRate: double.NaN,
|
MassLossRate: double.NaN,
|
||||||
IgnitionSeconds: ReadInt16OrEmpty(IgnitionSecondsRegister),
|
IgnitionSeconds: ReadInt16OrEmpty(IgnitionSecondsRegister),
|
||||||
TestSeconds: ReadInt16OrEmpty(TestSecondsRegister),
|
TestSeconds: ToElapsedSeconds(elapsed),
|
||||||
TotalSmoke: double.NaN);
|
TotalSmoke: double.NaN);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int ToElapsedSeconds(TimeSpan elapsed)
|
||||||
|
{
|
||||||
|
return elapsed >= TimeSpan.Zero
|
||||||
|
? (int)Math.Floor(elapsed.TotalSeconds)
|
||||||
|
: -1;
|
||||||
|
}
|
||||||
|
|
||||||
private double ReadRangedFloatOrEmpty(string label, ushort registerAddress, double minimum, double maximum)
|
private double ReadRangedFloatOrEmpty(string label, ushort registerAddress, double minimum, double maximum)
|
||||||
{
|
{
|
||||||
if (!_tcpDeviceConnectionService.TryReadFloatValues(registerAddress, out var result))
|
if (!_tcpDeviceConnectionService.TryReadFloatValues(registerAddress, out var result))
|
||||||
|
|||||||
@@ -431,8 +431,8 @@ public sealed class TestPageViewModel : PageViewModel
|
|||||||
{
|
{
|
||||||
if (isStarting)
|
if (isStarting)
|
||||||
{
|
{
|
||||||
_experimentDataService.StartTest();
|
|
||||||
ClearPlotSeries();
|
ClearPlotSeries();
|
||||||
|
_experimentDataService.StartTest();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user