更新11111

This commit is contained in:
GukSang.Jin
2026-05-28 18:10:42 +08:00
parent 51d4010d29
commit 737ef1643e
3 changed files with 140 additions and 33 deletions

View File

@@ -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)

View File

@@ -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))

View File

@@ -431,8 +431,8 @@ public sealed class TestPageViewModel : PageViewModel
{ {
if (isStarting) if (isStarting)
{ {
_experimentDataService.StartTest();
ClearPlotSeries(); ClearPlotSeries();
_experimentDataService.StartTest();
} }
else else
{ {