diff --git a/Resources/Strings.en.xaml b/Resources/Strings.en.xaml
index 0b08b10..893e835 100644
--- a/Resources/Strings.en.xaml
+++ b/Resources/Strings.en.xaml
@@ -240,6 +240,52 @@
Set PSG zero reference
Set INJ zero reference
+
+ RPM Command
+ Actual
+ Target
+ Apply
+ Actuators & Relays
+ Direction
+ Right
+ Left
+ Oil Pump
+ Counter
+ Set
+ Temperature
+ Setpoint °C
+ Tolerance ±°C
+ Heater
+ Dep. Cool.
+ T-In Cool.
+ Misc Relays
+ Electronic
+ Flasher
+ Pulse 4
+ Live Data
+ Live Charts
+ Advance Monitor
+ Lock Offset Δ°
+ Zero PSG
+ Zero INJ
+ Lock
+ Bench RPM
+ Pressure P1
+ Pressure P2
+ Q Delivery
+ Q Over
+ T-In
+ T-Out
+ T4
+ T-Tank
+ Bench Temp
+ bar
+ cc/s
+ Q Delivery
+ Q Over
+ Pressure P1
+ Pressure P2
+
▶ START TEST
■ STOP
diff --git a/Resources/Strings.es.xaml b/Resources/Strings.es.xaml
index d38262b..2456e50 100644
--- a/Resources/Strings.es.xaml
+++ b/Resources/Strings.es.xaml
@@ -240,6 +240,52 @@
Fijar referencia cero PSG
Fijar referencia cero INJ
+
+ Mando RPM
+ Actual
+ Objetivo
+ Aplicar
+ Actuadores y Relés
+ Dirección
+ Derecha
+ Izquierda
+ Bomba de Aceite
+ Contador
+ Fijar
+ Temperatura
+ Consigna °C
+ Tolerancia ±°C
+ Calefactor
+ Refr. Dep.
+ Refr. T-In
+ Relés Misc.
+ Electrónico
+ Flasher
+ Pulso 4
+ Datos en Vivo
+ Gráficos
+ Monitor de Avance
+ Offset Bloqueo Δ°
+ Cero PSG
+ Cero INJ
+ Bloqueo
+ RPM Banco
+ Presión P1
+ Presión P2
+ Q Entrega
+ Q Derrame
+ T-Ent.
+ T-Sal.
+ T4
+ T-Tanque
+ T. Banco
+ bar
+ cc/iny.
+ Q Entrega
+ Q Derrame
+ Presión P1
+ Presión P2
+
▶ INICIAR TEST
■ PARAR
diff --git a/Resources/Styles.xaml b/Resources/Styles.xaml
index c806b27..1eb08af 100644
--- a/Resources/Styles.xaml
+++ b/Resources/Styles.xaml
@@ -93,6 +93,11 @@
IsHitTestVisible="False"/>
+
+
+
+
+
diff --git a/ViewModels/AngleDisplayViewModel.cs b/ViewModels/AngleDisplayViewModel.cs
index 8d10eaa..5f0e4ec 100644
--- a/ViewModels/AngleDisplayViewModel.cs
+++ b/ViewModels/AngleDisplayViewModel.cs
@@ -31,10 +31,15 @@ namespace HC_APTBS.ViewModels
private double _zeroPsgEncoder;
private double _zeroInjDegrees;
private double _injEncoderDegrees;
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(PrimaryGaugeAngle))]
+ [NotifyPropertyChangedFor(nameof(SecondaryGaugeAngle))]
private double _currentManualDegrees;
private double _lockAngleValue;
- private bool _isLockSet;
- private bool _isManualVisible;
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(TargetAngleForGauge))]
+ private bool _isLockSet;
+ private bool _isManualVisible;
// ── Observable properties (bound by View) ─────────────────────────────────
@@ -68,6 +73,27 @@ namespace HC_APTBS.ViewModels
/// Foreground brush for the lock angle display (Green/Red/White).
[ObservableProperty] private Brush _lockAngleForeground = Brushes.White;
+ // ── Numeric doubles for radial advance-monitor control ────────────────────
+
+ /// PSG relative angle as a double for gauge binding.
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(PrimaryGaugeAngle))]
+ private double _psgRelativeDegrees;
+
+ /// INJ encoder absolute degrees (0–360) as a double.
+ [ObservableProperty] private double _injEncoderDegreesValue;
+
+ /// Lock angle target (0–360 degrees) as a double for gauge binding.
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(TargetAngleForGauge))]
+ private double _targetLockAngle;
+
+ /// True when bench RPM ≥ 31, hysteresis-cleared below 29.
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(PrimaryGaugeAngle))]
+ [NotifyPropertyChangedFor(nameof(SecondaryGaugeAngle))]
+ private bool _isRunningMode;
+
// ── Constructor ───────────────────────────────────────────────────────────
///
@@ -78,6 +104,17 @@ namespace HC_APTBS.ViewModels
_encoderResolution = configService.Settings.EncoderResolution;
}
+ // ── Computed for radial gauge ─────────────────────────────────────────────
+
+ /// Primary thumb angle: manual-wheel angle in hand-mode, PSG relative angle when running.
+ public double PrimaryGaugeAngle => IsRunningMode ? PsgRelativeDegrees : CurrentManualDegrees;
+
+ /// Target thumb angle for the radial gauge, or null when no lock has been set yet.
+ public double? TargetAngleForGauge => IsLockSet ? TargetLockAngle : (double?)null;
+
+ /// Ghost secondary thumb angle: manual-wheel position shown when running, null in hand-mode (primary already shows it).
+ public double? SecondaryGaugeAngle => IsRunningMode ? CurrentManualDegrees : (double?)null;
+
// ── Public update (called from MainViewModel.OnRefreshTick) ───────────────
///
@@ -98,6 +135,11 @@ namespace HC_APTBS.ViewModels
_currentRpm = rpm;
_isDirectionRight = isDirectionRight;
+ if (!IsRunningMode && rpm >= 31.0)
+ IsRunningMode = true;
+ else if (IsRunningMode && rpm < 29.0)
+ IsRunningMode = false;
+
RecalculatePsg();
RecalculateInj();
RecalculateManual();
@@ -150,7 +192,8 @@ namespace HC_APTBS.ViewModels
if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double delta))
{
_lockAngleValue = CalculateLockAngle(delta);
- _isLockSet = true;
+ TargetLockAngle = _lockAngleValue;
+ IsLockSet = true;
LockAngleDisplay = FormatAngle(_lockAngleValue);
}
@@ -183,8 +226,9 @@ namespace HC_APTBS.ViewModels
if (relDeg > 180) relDeg = 360 - relDeg;
relDeg *= sign;
- PsgEncoderAngle = FormatAngle(rawDeg);
- PsgRelativeAngle = FormatAngle(relDeg);
+ PsgRelativeDegrees = relDeg;
+ PsgEncoderAngle = FormatAngle(rawDeg);
+ PsgRelativeAngle = FormatAngle(relDeg);
}
private void RecalculateInj()
@@ -202,7 +246,8 @@ namespace HC_APTBS.ViewModels
double sign = DirectionSign;
// INJ encoder → degrees (full 0–360, NO 180 normalization).
- _injEncoderDegrees = _injRaw * (360.0 / _encoderResolution);
+ _injEncoderDegrees = _injRaw * (360.0 / _encoderResolution);
+ InjEncoderDegreesValue = _injEncoderDegrees;
// Relative angle from zero reference.
double relDeg = (_injEncoderDegrees - _zeroInjDegrees) * sign;
@@ -216,13 +261,14 @@ namespace HC_APTBS.ViewModels
CultureInfo.InvariantCulture, out double delta))
{
_lockAngleValue = CalculateLockAngle(delta);
+ TargetLockAngle = _lockAngleValue;
LockAngleDisplay = FormatAngle(_lockAngleValue);
}
}
private void RecalculateManual()
{
- _currentManualDegrees = _manualRaw * (360.0 / _encoderResolution);
+ CurrentManualDegrees = _manualRaw * (360.0 / _encoderResolution);
if (_currentRpm < 30)
{
diff --git a/ViewModels/BenchControlViewModel.cs b/ViewModels/BenchControlViewModel.cs
index 9205aa6..0731c4c 100644
--- a/ViewModels/BenchControlViewModel.cs
+++ b/ViewModels/BenchControlViewModel.cs
@@ -22,7 +22,12 @@ namespace HC_APTBS.ViewModels
// ── Direction ─────────────────────────────────────────────────────────────
/// True when the bench rotates clockwise (right). False for counter-clockwise (left).
- [ObservableProperty] private bool _isDirectionRight = true;
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(IsDirectionLeft))]
+ private bool _isDirectionRight = true;
+
+ /// True when the bench rotates counter-clockwise (left). Computed inverse of .
+ public bool IsDirectionLeft => !IsDirectionRight;
// ── Oil pump ──────────────────────────────────────────────────────────────
@@ -85,6 +90,14 @@ namespace HC_APTBS.ViewModels
_bench.SetRelay(RelayNames.DirectionLeft, !value);
}
+ /// Sets the bench rotation direction to clockwise (right).
+ [RelayCommand]
+ private void SetDirectionRight() => IsDirectionRight = true;
+
+ /// Sets the bench rotation direction to counter-clockwise (left).
+ [RelayCommand]
+ private void SetDirectionLeft() => IsDirectionRight = false;
+
// ── Oil pump toggle ───────────────────────────────────────────────────────
partial void OnIsOilPumpOnChanged(bool value)
@@ -167,6 +180,13 @@ namespace HC_APTBS.ViewModels
IsBenchRunning = false;
}
+ ///
+ /// Applies the RPM value from and starts the bench.
+ /// Bound to the inline Apply button in BenchRpmCommandCard.
+ ///
+ [RelayCommand]
+ private void ApplyRpm() => StartBench();
+
///
/// Quick-select button handler: sets the RPM input and starts the bench.
///
diff --git a/Views/Controls/RadialAngleGauge.cs b/Views/Controls/RadialAngleGauge.cs
new file mode 100644
index 0000000..857c10e
--- /dev/null
+++ b/Views/Controls/RadialAngleGauge.cs
@@ -0,0 +1,228 @@
+using System;
+using System.Globalization;
+using System.Windows;
+using System.Windows.Media;
+
+namespace HC_APTBS.Views.Controls
+{
+ ///
+ /// Geometry-rendered circular angle gauge for the bench advance-monitoring display.
+ /// Shows a 360° dial with 5° tick steps, major labels every 45°, and up to three
+ /// directional thumb markers: a primary thumb (manual wheel or PSG), an optional
+ /// target thumb (lock-angle target, colour-coded for tolerance), and an optional
+ /// ghost thumb (secondary / de-emphasised context indicator).
+ ///
+ public sealed class RadialAngleGauge : FrameworkElement
+ {
+ // ── Dependency properties ─────────────────────────────────────────────────
+
+ public static readonly DependencyProperty PrimaryAngleProperty =
+ DependencyProperty.Register(nameof(PrimaryAngle), typeof(double), typeof(RadialAngleGauge),
+ new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsRender));
+
+ public static readonly DependencyProperty TargetAngleProperty =
+ DependencyProperty.Register(nameof(TargetAngle), typeof(double?), typeof(RadialAngleGauge),
+ new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));
+
+ public static readonly DependencyProperty SecondaryAngleProperty =
+ DependencyProperty.Register(nameof(SecondaryAngle), typeof(double?), typeof(RadialAngleGauge),
+ new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));
+
+ public static readonly DependencyProperty PrimaryBrushProperty =
+ DependencyProperty.Register(nameof(PrimaryBrush), typeof(Brush), typeof(RadialAngleGauge),
+ new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));
+
+ public static readonly DependencyProperty TargetBrushProperty =
+ DependencyProperty.Register(nameof(TargetBrush), typeof(Brush), typeof(RadialAngleGauge),
+ new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsRender));
+
+ public static readonly DependencyProperty IsRunningModeProperty =
+ DependencyProperty.Register(nameof(IsRunningMode), typeof(bool), typeof(RadialAngleGauge),
+ new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.AffectsRender));
+
+ // ── Property accessors ────────────────────────────────────────────────────
+
+ /// Primary thumb angle in degrees (0 = top, clockwise positive).
+ public double PrimaryAngle
+ {
+ get => (double)GetValue(PrimaryAngleProperty);
+ set => SetValue(PrimaryAngleProperty, value);
+ }
+
+ /// Target thumb angle in degrees, or null to hide it.
+ public double? TargetAngle
+ {
+ get => (double?)GetValue(TargetAngleProperty);
+ set => SetValue(TargetAngleProperty, value);
+ }
+
+ /// Ghost secondary thumb angle in degrees, or null to hide it.
+ public double? SecondaryAngle
+ {
+ get => (double?)GetValue(SecondaryAngleProperty);
+ set => SetValue(SecondaryAngleProperty, value);
+ }
+
+ /// Fill brush for the primary thumb. Falls back to the accent brush when null.
+ public Brush? PrimaryBrush
+ {
+ get => (Brush?)GetValue(PrimaryBrushProperty);
+ set => SetValue(PrimaryBrushProperty, value);
+ }
+
+ /// Fill brush for the target thumb (green = within tolerance, red = off-target).
+ public Brush? TargetBrush
+ {
+ get => (Brush?)GetValue(TargetBrushProperty);
+ set => SetValue(TargetBrushProperty, value);
+ }
+
+ /// When true the mode label switches to PSG context; adjusts visual emphasis.
+ public bool IsRunningMode
+ {
+ get => (bool)GetValue(IsRunningModeProperty);
+ set => SetValue(IsRunningModeProperty, value);
+ }
+
+ // ── Layout ────────────────────────────────────────────────────────────────
+
+ protected override Size MeasureOverride(Size availableSize)
+ {
+ double side = double.IsInfinity(availableSize.Width) ? 240.0 : availableSize.Width;
+ if (!double.IsInfinity(availableSize.Height))
+ side = Math.Min(side, availableSize.Height);
+ side = Math.Max(side, 120.0);
+ return new Size(side, side);
+ }
+
+ // ── Render ────────────────────────────────────────────────────────────────
+
+ protected override void OnRender(DrawingContext dc)
+ {
+ double w = ActualWidth;
+ double h = ActualHeight;
+ double side = Math.Min(w, h);
+ double cx = w / 2.0;
+ double cy = h / 2.0;
+
+ // Radii as fractions of half-side
+ double halfS = side / 2.0;
+ double rOuter = halfS * 0.88;
+ double rInner = halfS * 0.82;
+ double rTickOut = halfS * 0.86;
+ double rMinorIn = halfS * 0.80;
+ double rMajorIn = halfS * 0.74;
+ double rLabel = halfS * 0.68;
+ double rThumbTip = halfS * 0.88;
+ double rThumbBase= halfS * 0.72;
+ double thumbHalf = halfS * 0.06; // half-width of triangle base
+
+ // Brushes — use WPF-UI resources when available, safe hard-coded fallbacks otherwise
+ Brush ringBrush = GetThemeBrush("ControlStrokeColorDefaultBrush", new SolidColorBrush(Color.FromArgb(64, 200, 200, 200)));
+ Brush tickBrush = GetThemeBrush("TextFillColorSecondaryBrush", new SolidColorBrush(Color.FromArgb(120, 180, 180, 180)));
+ Brush majorBrush = GetThemeBrush("TextFillColorPrimaryBrush", Brushes.WhiteSmoke);
+ Brush accentBrush = GetThemeBrush("AccentFillColorPrimaryBrush", Brushes.DodgerBlue);
+ Brush primaryFill = PrimaryBrush ?? accentBrush;
+ Brush targetFill = TargetBrush ?? Brushes.White;
+ Brush ghostFill = new SolidColorBrush(Color.FromArgb(80, 200, 200, 200));
+
+ var ringPen = new Pen(ringBrush, 1.0);
+ var minorPen = new Pen(tickBrush, 1.0);
+ var majorPen = new Pen(majorBrush, 2.0);
+
+ // Background ring (outer circle)
+ dc.DrawEllipse(null, ringPen, new Point(cx, cy), rOuter, rOuter);
+ dc.DrawEllipse(null, new Pen(ringBrush, 0.5), new Point(cx, cy), rInner, rInner);
+
+ // Tick marks and labels
+ var labelTypeface = new Typeface("Segoe UI");
+ double dpi = VisualTreeHelper.GetDpi(this).PixelsPerDip;
+
+ for (int deg = 0; deg < 360; deg += 5)
+ {
+ bool isMajor = (deg % 45 == 0);
+ double rad = deg * Math.PI / 180.0;
+ double sinA = Math.Sin(rad);
+ double cosA = Math.Cos(rad);
+
+ double oR = rTickOut;
+ double iR = isMajor ? rMajorIn : rMinorIn;
+ var p1 = new Point(cx + oR * sinA, cy - oR * cosA);
+ var p2 = new Point(cx + iR * sinA, cy - iR * cosA);
+ dc.DrawLine(isMajor ? majorPen : minorPen, p1, p2);
+
+ if (isMajor)
+ {
+ string label = deg.ToString(CultureInfo.InvariantCulture);
+ var ft = new FormattedText(label, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
+ labelTypeface, 10, majorBrush, dpi);
+ var labelPt = new Point(
+ cx + rLabel * sinA - ft.Width / 2,
+ cy - rLabel * cosA - ft.Height / 2);
+ dc.DrawText(ft, labelPt);
+ }
+ }
+
+ // Ghost / secondary thumb
+ if (SecondaryAngle.HasValue)
+ DrawThumb(dc, cx, cy, SecondaryAngle.Value, rThumbTip, rThumbBase, thumbHalf, ghostFill, null);
+
+ // Target thumb (outline + thin fill)
+ if (TargetAngle.HasValue)
+ {
+ var targetOutline = new Pen(targetFill, 1.5);
+ DrawThumb(dc, cx, cy, TargetAngle.Value, rThumbTip + halfS * 0.03, rThumbBase - halfS * 0.03, thumbHalf * 1.3, null, targetOutline);
+ }
+
+ // Primary thumb (filled)
+ DrawThumb(dc, cx, cy, PrimaryAngle, rThumbTip, rThumbBase, thumbHalf, primaryFill, null);
+
+ // Centre readout
+ string centre = $"{PrimaryAngle:F1}°";
+ var cft = new FormattedText(centre, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
+ new Typeface("Consolas"), 14, primaryFill, dpi);
+ dc.DrawText(cft, new Point(cx - cft.Width / 2, cy - cft.Height / 2));
+
+ // Mode label (tiny)
+ string modeLabel = IsRunningMode ? "PSG" : "MAN";
+ var mft = new FormattedText(modeLabel, CultureInfo.CurrentCulture, FlowDirection.LeftToRight,
+ new Typeface("Segoe UI"), 9, tickBrush, dpi);
+ dc.DrawText(mft, new Point(cx - mft.Width / 2, cy + 12));
+ }
+
+ // ── Helpers ───────────────────────────────────────────────────────────────
+
+ /// Draws an inward-pointing triangle thumb at the given dial angle.
+ private static void DrawThumb(
+ DrawingContext dc, double cx, double cy,
+ double angleDeg, double tipR, double baseR, double halfWidth,
+ Brush? fill, Pen? outline)
+ {
+ double rad = angleDeg * Math.PI / 180.0;
+ double sinA = Math.Sin(rad);
+ double cosA = Math.Cos(rad);
+ double perpSin = Math.Sin(rad + Math.PI / 2.0);
+ double perpCos = Math.Cos(rad + Math.PI / 2.0);
+
+ var tip = new Point(cx + tipR * sinA, cy - tipR * cosA);
+ var left = new Point(cx + baseR * sinA + halfWidth * perpSin,
+ cy - baseR * cosA - halfWidth * perpCos);
+ var right = new Point(cx + baseR * sinA - halfWidth * perpSin,
+ cy - baseR * cosA + halfWidth * perpCos);
+
+ var geo = new StreamGeometry();
+ using (var ctx = geo.Open())
+ {
+ ctx.BeginFigure(tip, true, true);
+ ctx.LineTo(left, true, false);
+ ctx.LineTo(right, true, false);
+ }
+ geo.Freeze();
+ dc.DrawGeometry(fill, outline, geo);
+ }
+
+ /// Resolves a WPF-UI theme brush by key with a safe fallback.
+ private Brush GetThemeBrush(string key, Brush fallback)
+ => TryFindResource(key) as Brush ?? fallback;
+ }
+}
diff --git a/Views/Pages/BenchPage.xaml b/Views/Pages/BenchPage.xaml
index 46a19f8..d99feee 100644
--- a/Views/Pages/BenchPage.xaml
+++ b/Views/Pages/BenchPage.xaml
@@ -5,62 +5,59 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:uc="clr-namespace:HC_APTBS.Views.UserControls"
mc:Ignorable="d"
- d:DesignHeight="900" d:DesignWidth="1280">
+ d:DesignHeight="900" d:DesignWidth="1400">
-
-
-
-
-
-
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/Views/UserControls/AdvanceMonitorCard.xaml b/Views/UserControls/AdvanceMonitorCard.xaml
new file mode 100644
index 0000000..ff574c2
--- /dev/null
+++ b/Views/UserControls/AdvanceMonitorCard.xaml
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Views/UserControls/AdvanceMonitorCard.xaml.cs b/Views/UserControls/AdvanceMonitorCard.xaml.cs
new file mode 100644
index 0000000..8693bdc
--- /dev/null
+++ b/Views/UserControls/AdvanceMonitorCard.xaml.cs
@@ -0,0 +1,14 @@
+using System.Windows.Controls;
+
+namespace HC_APTBS.Views.UserControls
+{
+ ///
+ /// Fluent card hosting the radial advance angle gauge, PSG/INJ readouts,
+ /// lock-offset input, and set-zero buttons.
+ /// DataContext = .
+ ///
+ public partial class AdvanceMonitorCard : UserControl
+ {
+ public AdvanceMonitorCard() => InitializeComponent();
+ }
+}
diff --git a/Views/UserControls/BenchActuatorsCard.xaml b/Views/UserControls/BenchActuatorsCard.xaml
new file mode 100644
index 0000000..eec3136
--- /dev/null
+++ b/Views/UserControls/BenchActuatorsCard.xaml
@@ -0,0 +1,219 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Views/UserControls/BenchActuatorsCard.xaml.cs b/Views/UserControls/BenchActuatorsCard.xaml.cs
new file mode 100644
index 0000000..d8e62a7
--- /dev/null
+++ b/Views/UserControls/BenchActuatorsCard.xaml.cs
@@ -0,0 +1,14 @@
+using System.Windows.Controls;
+
+namespace HC_APTBS.Views.UserControls
+{
+ ///
+ /// Fluent card grouping all bench actuator controls: oil pump, counter, direction,
+ /// temperature PID, and auxiliary relays.
+ /// DataContext = .
+ ///
+ public partial class BenchActuatorsCard : UserControl
+ {
+ public BenchActuatorsCard() => InitializeComponent();
+ }
+}
diff --git a/Views/UserControls/BenchChartsCard.xaml b/Views/UserControls/BenchChartsCard.xaml
new file mode 100644
index 0000000..48189a9
--- /dev/null
+++ b/Views/UserControls/BenchChartsCard.xaml
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Views/UserControls/BenchChartsCard.xaml.cs b/Views/UserControls/BenchChartsCard.xaml.cs
new file mode 100644
index 0000000..c4a7aa0
--- /dev/null
+++ b/Views/UserControls/BenchChartsCard.xaml.cs
@@ -0,0 +1,14 @@
+using System.Windows.Controls;
+
+namespace HC_APTBS.Views.UserControls
+{
+ ///
+ /// Fluent card with a 2×2 grid of compact real-time bench charts
+ /// (Q-Delivery, Q-Over, P1, P2).
+ /// DataContext = .
+ ///
+ public partial class BenchChartsCard : UserControl
+ {
+ public BenchChartsCard() => InitializeComponent();
+ }
+}
diff --git a/Views/UserControls/BenchLiveDataCard.xaml b/Views/UserControls/BenchLiveDataCard.xaml
new file mode 100644
index 0000000..d97e4be
--- /dev/null
+++ b/Views/UserControls/BenchLiveDataCard.xaml
@@ -0,0 +1,235 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Views/UserControls/BenchLiveDataCard.xaml.cs b/Views/UserControls/BenchLiveDataCard.xaml.cs
new file mode 100644
index 0000000..85826f6
--- /dev/null
+++ b/Views/UserControls/BenchLiveDataCard.xaml.cs
@@ -0,0 +1,14 @@
+using System.Windows.Controls;
+
+namespace HC_APTBS.Views.UserControls
+{
+ ///
+ /// Fluent card with two rows of KPI tiles showing bench live readings
+ /// (RPM, pressures, flow, temperatures).
+ /// DataContext = .
+ ///
+ public partial class BenchLiveDataCard : UserControl
+ {
+ public BenchLiveDataCard() => InitializeComponent();
+ }
+}
diff --git a/Views/UserControls/BenchRpmCommandCard.xaml b/Views/UserControls/BenchRpmCommandCard.xaml
new file mode 100644
index 0000000..45833ae
--- /dev/null
+++ b/Views/UserControls/BenchRpmCommandCard.xaml
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Views/UserControls/BenchRpmCommandCard.xaml.cs b/Views/UserControls/BenchRpmCommandCard.xaml.cs
new file mode 100644
index 0000000..0fd491c
--- /dev/null
+++ b/Views/UserControls/BenchRpmCommandCard.xaml.cs
@@ -0,0 +1,14 @@
+using System.Windows.Controls;
+
+namespace HC_APTBS.Views.UserControls
+{
+ ///
+ /// Fluent card providing RPM numeric input, preset buttons, Start/Stop and
+ /// voltage readout for manual bench motor control.
+ /// DataContext = .
+ ///
+ public partial class BenchRpmCommandCard : UserControl
+ {
+ public BenchRpmCommandCard() => InitializeComponent();
+ }
+}
diff --git a/Views/UserControls/FlowmeterChartView.xaml b/Views/UserControls/FlowmeterChartView.xaml
index db41982..9cdc113 100644
--- a/Views/UserControls/FlowmeterChartView.xaml
+++ b/Views/UserControls/FlowmeterChartView.xaml
@@ -18,7 +18,8 @@
VerticalAlignment="Center"/>
-
/// UserControl hosting a single real-time flowmeter chart.
/// DataContext is expected to be a .
+ /// Set to true to reduce chart height to 90 px
+ /// (used in the 2×2 bench chart grid).
///
public partial class FlowmeterChartView : UserControl
{
+ /// When true the chart height shrinks from 120 to 90 px.
+ public static readonly DependencyProperty IsCompactProperty =
+ DependencyProperty.Register(nameof(IsCompact), typeof(bool), typeof(FlowmeterChartView),
+ new FrameworkPropertyMetadata(false,
+ (d, e) => ((FlowmeterChartView)d).ChartHeight = (bool)e.NewValue ? 90.0 : 120.0));
+
+ public bool IsCompact
+ {
+ get => (bool)GetValue(IsCompactProperty);
+ set => SetValue(IsCompactProperty, value);
+ }
+
+ /// Derived chart height (120 or 90); bound in XAML.
+ public static readonly DependencyProperty ChartHeightProperty =
+ DependencyProperty.Register(nameof(ChartHeight), typeof(double), typeof(FlowmeterChartView),
+ new FrameworkPropertyMetadata(120.0));
+
+ public double ChartHeight
+ {
+ get => (double)GetValue(ChartHeightProperty);
+ set => SetValue(ChartHeightProperty, value);
+ }
+
public FlowmeterChartView()
{
InitializeComponent();