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