更新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
{
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)

View File

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

View File

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