Bundles several feature streams that have been iterating on the working tree: - Developer Tools page (Debug-only via DEVELOPER_TOOLS symbol): hosts the identification card, manual KWP write + transaction log, ROM/EEPROM dump card with progress banner and completion message, persisted custom-commands library, persisted EEPROM passwords library. New service primitives: IKwpService.SendRawCustomAsync / ReadEepromAsync / ReadRomEepromAsync. Persistence mirrors the Clients XML pattern in two new files (custom_commands.xml, eeprom_passwords.xml). - Auto-test orchestrator (IAutoTestOrchestrator + AutoTestState): linear K-Line read -> unlock -> bench-on -> test sequence with snackbar UI and progress dialog VM, gated on dashboard alarms. - BIP-STATUS display: BipDisplayViewModel + BipDisplayView, RAM read at 0x0106 via IKwpService.ReadBipStatusAsync; status definitions in BipStatusDefinition. - Tests page redesign: TestSectionCard + PhaseTileView replacing the old TestPlanView/TestRunningView/TestDoneView/TestPreconditionsView/ TestSectionView controls and their VMs. - Pump command sliders: Fluent thick-track style with overhang thumb, click-anywhere-and-drag, mouse-wheel adjustment. - Window startup: app.manifest declares PerMonitorV2 DPI awareness, MainWindow installs a WM_GETMINMAXINFO hook in OnSourceInitialized and maximizes there (after the hook is in place) so the app fits the work area exactly on any display configuration. - Misc: PercentToPixelsConverter, seed_aliases.py one-shot pump-alias importer, tools/Import-BipStatus.ps1, kline_eeprom_spec.md and dump-functions reference docs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
291 lines
12 KiB
C#
291 lines
12 KiB
C#
using System;
|
|
using System.Collections.ObjectModel;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using LiveChartsCore;
|
|
using LiveChartsCore.Defaults;
|
|
using LiveChartsCore.SkiaSharpView;
|
|
using LiveChartsCore.SkiaSharpView.Painting;
|
|
using SkiaSharp;
|
|
|
|
namespace HC_APTBS.ViewModels
|
|
{
|
|
/// <summary>
|
|
/// Reusable ViewModel for a single real-time scrolling line chart.
|
|
/// Backed by LiveChartsCore with a fixed-width sample window.
|
|
///
|
|
/// <para>Two modes:</para>
|
|
/// <list type="bullet">
|
|
/// <item><b>Index-axis (default):</b> <see cref="LineSeries{TModel}"/> of doubles; X = array index;
|
|
/// axis is static. Samples shuffle through fixed X slots on each <see cref="AddValue"/>.</item>
|
|
/// <item><b>Smooth-scroll (opt-in):</b> <see cref="LineSeries{TModel}"/> of <see cref="ObservablePoint"/>
|
|
/// with X on a monotonic sample-index timeline. A host-driven per-frame hook calls
|
|
/// <see cref="UpdateViewport"/> to slide the X-axis window, producing true continuous
|
|
/// leftward motion independent of data cadence.</item>
|
|
/// </list>
|
|
/// </summary>
|
|
public sealed partial class SingleFlowChartViewModel : ObservableObject
|
|
{
|
|
private const int DefaultMaxSamples = 200;
|
|
private const int SmoothTrimMargin = 4;
|
|
|
|
private readonly bool _smoothScroll;
|
|
private readonly int _maxSamples;
|
|
|
|
// Index-axis mode storage.
|
|
private readonly ObservableCollection<double>? _values;
|
|
|
|
// Smooth-scroll mode storage + timeline state.
|
|
// The series holds N committed points at integer X positions plus one trailing
|
|
// "endpoint" whose X/Y interpolate each render frame from the previous committed
|
|
// point toward the pending sample. On each new sample the endpoint snaps to its
|
|
// target (becoming committed) and a new endpoint is appended at the same spot.
|
|
// Net effect: the line tip slides continuously instead of hopping +1 in X per sample.
|
|
private readonly ObservableCollection<ObservablePoint>? _points;
|
|
private long _nextCommitX;
|
|
private double _prevCommittedValue;
|
|
private double _pendingValue;
|
|
private DateTime _pendingArrivalUtc;
|
|
private int _sampleCount;
|
|
private ObservablePoint? _endpoint;
|
|
|
|
/// <summary>Chart title label.</summary>
|
|
[ObservableProperty] private string _title = string.Empty;
|
|
|
|
/// <summary>Most recent sample value, displayed as a numeric label alongside the chart.</summary>
|
|
[ObservableProperty] private double _currentValue;
|
|
|
|
/// <summary>Series array bound to the CartesianChart.</summary>
|
|
public ISeries[] Series { get; }
|
|
|
|
/// <summary>X axes for the chart (auto-scrolling, no labels).</summary>
|
|
public Axis[] XAxes { get; }
|
|
|
|
/// <summary>Y axes for the chart.</summary>
|
|
public Axis[] YAxes { get; }
|
|
|
|
/// <summary>
|
|
/// Tolerance band sections overlaid on the chart.
|
|
/// Updated when <see cref="SetTolerance"/> is called.
|
|
/// </summary>
|
|
[ObservableProperty] private RectangularSection[] _sections = Array.Empty<RectangularSection>();
|
|
|
|
/// <summary>
|
|
/// Creates a new chart ViewModel.
|
|
/// </summary>
|
|
/// <param name="title">Display title for the chart.</param>
|
|
/// <param name="lineColor">SKColor for the line series.</param>
|
|
/// <param name="maxSamples">Maximum number of samples before the oldest is dropped.</param>
|
|
/// <param name="smoothScroll">
|
|
/// When true, store samples on a monotonic X timeline and expect the host to call
|
|
/// <see cref="UpdateViewport"/> each render frame to slide the visible window.
|
|
/// When false (default), use the legacy index-axis behavior.
|
|
/// </param>
|
|
public SingleFlowChartViewModel(string title, SKColor lineColor, int maxSamples = DefaultMaxSamples, bool smoothScroll = false)
|
|
{
|
|
_title = title;
|
|
_maxSamples = maxSamples;
|
|
_smoothScroll = smoothScroll;
|
|
|
|
if (smoothScroll)
|
|
{
|
|
_points = new ObservableCollection<ObservablePoint>();
|
|
Series = new ISeries[]
|
|
{
|
|
new LineSeries<ObservablePoint>
|
|
{
|
|
Values = _points,
|
|
Fill = null,
|
|
GeometrySize = 0,
|
|
Stroke = new SolidColorPaint(lineColor, 2),
|
|
LineSmoothness = 0,
|
|
AnimationsSpeed = TimeSpan.Zero
|
|
}
|
|
};
|
|
|
|
XAxes = new Axis[]
|
|
{
|
|
new Axis
|
|
{
|
|
IsVisible = false,
|
|
AnimationsSpeed = TimeSpan.Zero,
|
|
MinLimit = 0,
|
|
MaxLimit = maxSamples
|
|
}
|
|
};
|
|
}
|
|
else
|
|
{
|
|
_values = new ObservableCollection<double>();
|
|
Series = new ISeries[]
|
|
{
|
|
new LineSeries<double>
|
|
{
|
|
Values = _values,
|
|
Fill = null,
|
|
GeometrySize = 0,
|
|
Stroke = new SolidColorPaint(lineColor, 2),
|
|
LineSmoothness = 0,
|
|
AnimationsSpeed = TimeSpan.Zero
|
|
}
|
|
};
|
|
|
|
XAxes = new Axis[]
|
|
{
|
|
new Axis
|
|
{
|
|
IsVisible = false,
|
|
AnimationsSpeed = TimeSpan.Zero
|
|
}
|
|
};
|
|
}
|
|
|
|
YAxes = new Axis[]
|
|
{
|
|
new Axis
|
|
{
|
|
AnimationsSpeed = TimeSpan.Zero,
|
|
MinLimit = 0,
|
|
SeparatorsPaint = new SolidColorPaint(new SKColor(224, 224, 224), 0.75f)
|
|
}
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Appends a value to the chart. Drops the oldest value when the window is full.
|
|
/// Must be called on the UI thread.
|
|
/// </summary>
|
|
public void AddValue(double value)
|
|
{
|
|
CurrentValue = value;
|
|
|
|
if (_smoothScroll)
|
|
{
|
|
_sampleCount++;
|
|
|
|
if (_sampleCount == 1)
|
|
{
|
|
// First sample — commit at X=0 and anchor the viewport so the point sits
|
|
// at the right edge. No endpoint yet; we need two samples before we can
|
|
// interpolate between previous and pending.
|
|
_points!.Add(new ObservablePoint(0, value));
|
|
_nextCommitX = 1;
|
|
_prevCommittedValue = value;
|
|
_pendingValue = value;
|
|
_pendingArrivalUtc = DateTime.UtcNow;
|
|
XAxes[0].MaxLimit = 0;
|
|
XAxes[0].MinLimit = -_maxSamples;
|
|
}
|
|
else if (_sampleCount == 2)
|
|
{
|
|
// Second sample — create the sliding endpoint at the previous committed
|
|
// point. UpdateViewport will interpolate it toward the pending target.
|
|
_pendingValue = value;
|
|
_pendingArrivalUtc = DateTime.UtcNow;
|
|
_endpoint = new ObservablePoint(_nextCommitX - 1, _prevCommittedValue);
|
|
_points!.Add(_endpoint);
|
|
}
|
|
else
|
|
{
|
|
// Third+ sample — finalize the existing endpoint at its target (making
|
|
// it a committed point), advance the commit index, then append a fresh
|
|
// endpoint at the just-finalized position. Because the endpoint is
|
|
// already at X = _nextCommitX after the last render frame with
|
|
// fraction≈1, the viewport continues from the same position without
|
|
// a visible hop.
|
|
_endpoint!.X = _nextCommitX;
|
|
_endpoint.Y = _pendingValue;
|
|
_prevCommittedValue = _pendingValue;
|
|
_nextCommitX++;
|
|
|
|
_pendingValue = value;
|
|
_pendingArrivalUtc = DateTime.UtcNow;
|
|
_endpoint = new ObservablePoint(_nextCommitX - 1, _prevCommittedValue);
|
|
_points!.Add(_endpoint);
|
|
|
|
// Keep a small margin past the visible window so the leftmost point
|
|
// doesn't pop as the viewport advances past the trim boundary.
|
|
while (_points.Count > _maxSamples + SmoothTrimMargin)
|
|
_points.RemoveAt(0);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_values!.Add(value);
|
|
if (_values.Count > _maxSamples)
|
|
_values.RemoveAt(0);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Smooth-scroll mode only: slides the X-axis viewport so the rightmost visible edge
|
|
/// drifts continuously past the most recent sample, producing frame-rate smooth motion
|
|
/// independent of data cadence. Host calls this once per render frame (e.g. from
|
|
/// <c>CompositionTarget.Rendering</c>).
|
|
/// </summary>
|
|
/// <param name="nowUtc">Current time; pass <see cref="DateTime.UtcNow"/>.</param>
|
|
/// <param name="nominalPeriodMs">
|
|
/// Expected inter-sample period in milliseconds (e.g. <c>1000/Hz</c>). Used to interpolate
|
|
/// fractional progress between samples.
|
|
/// </param>
|
|
public void UpdateViewport(DateTime nowUtc, double nominalPeriodMs)
|
|
{
|
|
if (!_smoothScroll || _endpoint == null || nominalPeriodMs <= 0)
|
|
return;
|
|
|
|
double fraction = (nowUtc - _pendingArrivalUtc).TotalMilliseconds / nominalPeriodMs;
|
|
if (fraction < 0) fraction = 0;
|
|
else if (fraction > 1) fraction = 1;
|
|
|
|
double slideX = (_nextCommitX - 1) + fraction;
|
|
double slideY = _prevCommittedValue + (_pendingValue - _prevCommittedValue) * fraction;
|
|
|
|
_endpoint.X = slideX;
|
|
_endpoint.Y = slideY;
|
|
|
|
XAxes[0].MaxLimit = slideX;
|
|
XAxes[0].MinLimit = slideX - _maxSamples;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the tolerance band (displayed as a shaded region on the chart).
|
|
/// </summary>
|
|
/// <param name="target">Center value of the tolerance band.</param>
|
|
/// <param name="tolerance">Half-width of the tolerance band.</param>
|
|
public void SetTolerance(double target, double tolerance)
|
|
{
|
|
Sections = new[]
|
|
{
|
|
new RectangularSection
|
|
{
|
|
Yi = target - tolerance,
|
|
Yj = target + tolerance,
|
|
Fill = new SolidColorPaint(SKColors.LimeGreen.WithAlpha(40))
|
|
}
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears all sample data and tolerance bands. Resets the smooth-scroll timeline.
|
|
/// </summary>
|
|
public void Clear()
|
|
{
|
|
if (_smoothScroll)
|
|
{
|
|
_points!.Clear();
|
|
_nextCommitX = 0;
|
|
_prevCommittedValue = 0;
|
|
_pendingValue = 0;
|
|
_sampleCount = 0;
|
|
_endpoint = null;
|
|
XAxes[0].MinLimit = 0;
|
|
XAxes[0].MaxLimit = _maxSamples;
|
|
}
|
|
else
|
|
{
|
|
_values!.Clear();
|
|
}
|
|
Sections = Array.Empty<RectangularSection>();
|
|
}
|
|
}
|
|
}
|