Files
HC_APTBS/Models/SensorConfiguration.cs
LucianoDev 0280a2fad1 feat: page-based navigation shell + Tests page wizard
Replace the monolithic MainWindow with a SelectedPage-driven shell
(Dashboard / Pump / Bench / Tests / Results / Settings). The Tests
page gets the Plan -> Preconditions -> Running -> Done wizard from
ui-structure.md \u00a74, backed by a 7-item precondition gate and
shared sub-views (PhaseCardView / TestSectionView / GraphicIndicatorView)
extracted from the now-deleted monolithic TestPanelView.

New VMs / views:
- Tests wizard: TestPreconditions, PhaseCard, GraphicIndicator,
  TestSection, TestPlan, TestRunning, TestDone
- Dashboard panels: DashboardConnection, DashboardReadings,
  DashboardAlarms, InterlockBanner, ResultHistory
- Pump / bench panels: PumpIdentificationPanel, PumpLiveData,
  UnlockPanel, BenchDriveControl, BenchReadings, RelayBank,
  TemperatureControl, DtcList, AuthGate
- Dialogs: generic ConfirmDialog, UserManageDialog, UserPromptDialog

Supporting changes:
- IsOilPumpOn exposed on MainViewModel for precondition evaluation
- RequiresAuth added to TestDefinition (XML round-trip)
- BipStatusDefinition + CompletedTestRun models
- ~35 new Test.* localization keys (en + es)
- Settings moved from modal dialog to full page
- Pause / Retry / Skip stubs in TestRunningView; full spec in
  docs/gap-test-running-controls.md for follow-up implementation
- docs/ui-structure.md captures the wizard design

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-18 13:11:34 +02:00

140 lines
6.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Globalization;
using System.Xml.Linq;
namespace HC_APTBS.Models
{
/// <summary>
/// Calibration parameters for an analogue sensor channel.
/// Maps a raw ADC counts value (from the bench CAN bus) to an
/// engineering-unit value using a voltage-range to value-range linear mapping,
/// then applies a gain and offset correction.
///
/// <para>
/// The bench ADC uses a 10-bit (1024-step) range over 05 V.
/// Conversion: V = (rawCanBusValue × 5000 / 1024) / 1000
/// Engineering value = V × ((MaxVal MinVal) / (MaxVolt MinVolt)) × Gain + Offset
/// </para>
/// </summary>
public class SensorConfiguration
{
/// <summary>1-based sensor channel number.</summary>
public short Number { get; set; } = 1;
/// <summary>Display name for the sensor (e.g. "Pressure").</summary>
public string SensorName { get; set; } = "Sensor";
/// <summary>Minimum input voltage at the sensor connector (V).</summary>
public double MinVolt { get; set; } = 0;
/// <summary>Maximum input voltage at the sensor connector (V).</summary>
public double MaxVolt { get; set; } = 5;
/// <summary>Engineering-unit value corresponding to <see cref="MinVolt"/>.</summary>
public double MinVal { get; set; } = 0;
/// <summary>Engineering-unit value corresponding to <see cref="MaxVolt"/>.</summary>
public double MaxVal { get; set; } = 15;
/// <summary>Multiplicative gain correction applied after the range mapping.</summary>
public double Gain { get; set; } = 1;
/// <summary>Additive offset correction applied after the gain.</summary>
public double Offset { get; set; } = 0;
/// <summary>
/// Static logging hook used by runtime guards and <see cref="TryParse"/>.
/// Wired at app startup to <c>IAppLogger.Warning</c>. Left null keeps the
/// Models layer DI-free and testable. Signature: (source, message).
/// </summary>
public static Action<string, string>? WarningLogger { get; set; }
/// <summary>Set once per instance after the first zero-range warning, to prevent hot-loop log spam.</summary>
private bool _warnedRangeZero;
/// <summary>
/// Converts a raw CAN bus ADC count to a calibrated engineering-unit value.
/// Returns 0 and warns once if <c>MaxVolt == MinVolt</c>.
/// </summary>
/// <param name="rawCanBusValue">10-bit ADC count from the CAN frame.</param>
/// <returns>Calibrated value in engineering units.</returns>
public double GetValueFromRaw(double rawCanBusValue)
{
double range = MaxVolt - MinVolt;
if (range == 0.0)
{
if (!_warnedRangeZero)
{
WarningLogger?.Invoke(nameof(SensorConfiguration),
$"MaxVolt == MinVolt for sensor '{SensorName}' (channel {Number}). Returning 0.");
_warnedRangeZero = true;
}
return 0.0;
}
// Convert ADC counts → volts (10-bit ADC, 5 V reference)
double volts = rawCanBusValue * 5000.0 / 1024.0 / 1000.0;
// Map voltage range → engineering-unit range
double value = volts * ((MaxVal - MinVal) / range);
return value * Gain + Offset;
}
/// <summary>Serialises this sensor configuration to XML.</summary>
public XElement ToXml()
=> new XElement("sensor",
new XAttribute("num", Number),
new XAttribute("name", SensorName),
new XElement("Gain", Gain),
new XElement("Offset", Offset),
new XElement("MinVolt", MinVolt),
new XElement("MaxVolt", MaxVolt),
new XElement("MinVal", MinVal),
new XElement("MaxVal", MaxVal));
/// <summary>Deserialises a sensor configuration from XML.</summary>
public static SensorConfiguration FromXml(XElement element)
{
var sc = new SensorConfiguration();
TryParse(element.Attribute("num")?.Value, "num", v => sc.Number = short.Parse(v, CultureInfo.InvariantCulture));
TryParse(element.Attribute("name")?.Value, "name", v => sc.SensorName = v);
TryParse(element.Element("Gain")?.Value, "Gain", v => sc.Gain = ParseInvariant(v));
TryParse(element.Element("Offset")?.Value, "Offset", v => sc.Offset = ParseInvariant(v));
TryParse(element.Element("MinVolt")?.Value, "MinVolt", v => sc.MinVolt = ParseInvariant(v));
TryParse(element.Element("MaxVolt")?.Value, "MaxVolt", v => sc.MaxVolt = ParseInvariant(v));
TryParse(element.Element("MinVal")?.Value, "MinVal", v => sc.MinVal = ParseInvariant(v));
TryParse(element.Element("MaxVal")?.Value, "MaxVal", v => sc.MaxVal = ParseInvariant(v));
return sc;
}
/// <summary>Parses a decimal under <see cref="CultureInfo.InvariantCulture"/>, tolerating comma separators.</summary>
private static double ParseInvariant(string v)
=> double.Parse(v.Replace(',', '.'), CultureInfo.InvariantCulture);
/// <summary>Creates the default pressure sensor calibration for channel 1.</summary>
public static SensorConfiguration DefaultPressureSensor()
=> new SensorConfiguration
{
Number = 1,
SensorName = "Pressure",
Offset = -4.1,
MinVolt = 0.5,
MaxVolt = 3.2,
MinVal = 0,
MaxVal = 20
};
private static void TryParse(string? value, string fieldName, Action<string> assign)
{
if (string.IsNullOrEmpty(value)) return;
try { assign(value!); }
catch (Exception ex)
{
WarningLogger?.Invoke(nameof(SensorConfiguration),
$"Skipped malformed '{fieldName}' value '{value}': {ex.Message}");
}
}
}
}