Files
HC_APTBS/Models/CanBusParameter.cs
LucianoDev 827b811b39 feat: developer tools page, auto-test orchestrator, BIP display, tests redesign
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>
2026-05-07 13:59:50 +02:00

422 lines
19 KiB
C#
Raw Permalink 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>
/// Represents a single logical parameter exchanged over CAN.
/// Each parameter occupies up to two bytes within a CAN frame identified by <see cref="MessageId"/>.
///
/// <para><b>Bench params</b> use the simple calibration model:<br/>
/// Linear: <c>eng = raw * Factor + Offset</c><br/>
/// Inverse: <c>eng = Factor / raw + Offset</c></para>
///
/// <para><b>Pump params</b> (legacy) use the P1P6 rational transfer function via
/// <see cref="GetTransformResult"/>. Set <see cref="UseLegacyTransform"/> to enable.</para>
/// </summary>
public class CanBusParameter
{
// ── Frame addressing ─────────────────────────────────────────────────────
/// <summary>CAN message identifier this parameter belongs to.</summary>
public uint MessageId { get; set; }
/// <summary>Index of the high byte within the 8-byte CAN payload.</summary>
public ushort ByteH { get; set; }
/// <summary>Index of the low byte within the 8-byte CAN payload.</summary>
public ushort ByteL { get; set; }
// ── Metadata ─────────────────────────────────────────────────────────────
/// <summary>Human-readable parameter name used for lookup (see <see cref="BenchParameterNames"/>).</summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Sensor/encoding type selector. Used by the receive decoder to choose
/// the correct bit-extraction formula (e.g. for temperature and RPM).
/// </summary>
public int Type { get; set; } = 0;
// ── Simple calibration (bench params) ────────────────────────────────────
/// <summary>Multiplication factor: <c>eng = raw * Factor + Offset</c>.</summary>
public double Factor { get; set; } = 1.0;
/// <summary>Additive offset: <c>eng = raw * Factor + Offset</c>.</summary>
public double Offset { get; set; }
/// <summary>When true, calibration uses <c>eng = Factor / raw + Offset</c>.</summary>
public bool IsInverse { get; set; }
// ── Legacy P1P6 calibration (pump params) ───────────────────────────────
/// <summary>Transfer function coefficient P1 (numerator multiplier).</summary>
public double P1 { get; set; }
/// <summary>Transfer function coefficient P2 (numerator offset).</summary>
public double P2 { get; set; }
/// <summary>Transfer function coefficient P3 (denominator multiplier).</summary>
public double P3 { get; set; }
/// <summary>Transfer function coefficient P4 (denominator offset).</summary>
public double P4 { get; set; }
/// <summary>Transfer function coefficient P5 (additive offset 1).</summary>
public double P5 { get; set; }
/// <summary>Transfer function coefficient P6 (additive offset 2).</summary>
public double P6 { get; set; }
/// <summary>
/// When true, the calibration coefficients are set to the identity transform
/// (P1=1, P4=1, all others 0) so the raw value passes through unchanged.
/// </summary>
public bool DisableCalibration { get; set; }
/// <summary>
/// When true, <see cref="GetTransformResult"/> is used instead of
/// <see cref="Calibrate"/>. Enabled for pump params loaded via <see cref="FromXml"/>.
/// </summary>
public bool UseLegacyTransform { get; set; }
// ── Runtime state ─────────────────────────────────────────────────────────
/// <summary>Current decoded engineering-unit value (updated by the CAN read thread).</summary>
public double Value { get; set; }
/// <summary>
/// True when <see cref="Value"/> has been updated since the last UI refresh tick.
/// Reset to false by the consumer after reading.
/// </summary>
public bool NeedsUpdate { get; set; }
/// <summary>
/// Raised on the CAN read thread after a decoded frame causes <see cref="Value"/>
/// to change. The decoder compares post-filter values and only fires on a real
/// delta, so handlers that only care about state transitions do not need their own
/// change-detection. Handlers run on the CAN read thread — they must not block and
/// must marshal to the UI thread themselves if they touch WPF state.
/// </summary>
public event Action<CanBusParameter>? ValueChanged;
/// <summary>
/// Invokes <see cref="ValueChanged"/>. Intended to be called by the CAN decoder
/// after a value update; internal so other layers cannot raise it spuriously.
/// </summary>
internal void RaiseValueChanged() => ValueChanged?.Invoke(this);
/// <summary>
/// True for receive-direction params (decoded from incoming CAN frames).
/// False for transmit-direction params (packed into outgoing frames).
/// </summary>
public bool IsReceive { get; set; }
/// <summary>
/// Exponential moving average coefficient α ∈ (0, 1].
/// α = 1 means no smoothing (pass-through); smaller values give more smoothing.
/// </summary>
public double Alpha { get; set; } = 1.0;
// ── Runtime-warning plumbing ──────────────────────────────────────────────
/// <summary>
/// Static logging hook invoked when <see cref="GetTransformResult"/> encounters
/// a zero denominator. 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-denominator warning, to prevent hot-loop log spam.</summary>
private bool _warnedDenomZero;
// ── Convenience alias kept for cross-file compatibility ───────────────────
/// <summary>Alias for <see cref="MessageId"/> — used by legacy call sites.</summary>
public uint ID
{
get => MessageId;
set => MessageId = value;
}
// ── Simple calibration ────────────────────────────────────────────────────
/// <summary>
/// Converts a raw CAN value to engineering units using Factor/Offset.
/// </summary>
public double Calibrate(double raw)
{
if (IsInverse)
return raw != 0 ? Factor / raw + Offset : 0;
return raw * Factor + Offset;
}
/// <summary>
/// Converts an engineering value back to raw CAN value (for transmit).
/// </summary>
public double CalibrateReverse(double eng)
{
if (IsInverse)
{
double denom = eng - Offset;
return denom != 0 ? Factor / denom : 0;
}
return Factor != 0 ? (eng - Offset) / Factor : 0;
}
// ── Legacy P1P6 transfer function (pump params) ─────────────────────────
/// <summary>
/// Applies the P1P6 rational transfer function to <see cref="Value"/>.
/// Used only by pump params (<see cref="UseLegacyTransform"/> = true).
/// Returns 0 and warns once if the denominator is zero.
/// </summary>
public double GetTransformResult()
{
if (IsReceive)
{
double denom = P1 + P3 * P5 + P3 * P6 - P3 * Value;
if (denom == 0.0)
{
if (!_warnedDenomZero)
{
WarningLogger?.Invoke(nameof(CanBusParameter),
$"Zero denominator for '{Name}' (receive). Returning 0.");
_warnedDenomZero = true;
}
return 0.0;
}
return (-P2 - P3 * P5 - P4 * P6 + P4 * Value) / denom;
}
{
double denom = P3 * Value + P4;
if (denom == 0.0)
{
if (!_warnedDenomZero)
{
WarningLogger?.Invoke(nameof(CanBusParameter),
$"Zero denominator for '{Name}' (transmit). Returning 0.");
_warnedDenomZero = true;
}
return 0.0;
}
return ((P1 * Value + P2) / denom) + P5 + P6;
}
}
/// <summary>
/// Returns the raw CAN value for transmission.
/// Delegates to simple or legacy calibration depending on param type.
/// </summary>
public double GetTransmitValue()
{
if (UseLegacyTransform)
return GetTransformResult();
return CalibrateReverse(Value);
}
/// <summary>
/// Resets calibration coefficients to the identity transform so the raw value
/// passes through unchanged.
/// </summary>
public void SetIdentityCalibration()
{
P1 = 1; P2 = 0; P3 = 0; P4 = 1; P5 = 0; P6 = 0;
DisableCalibration = true;
}
// ── XML serialisation ─────────────────────────────────────────────────────
/// <summary>
/// Deserialises a pump CAN parameter from an XML element inside the
/// <c>&lt;Params&gt;</c> section of a pump definition.
/// </summary>
public static CanBusParameter FromXml(XElement xe)
{
string name = xe.Name.LocalName;
ushort byteh = ushort.Parse(xe.Attribute("byteh")?.Value ?? "0");
ushort bytel = ushort.Parse(xe.Attribute("bytel")?.Value ?? "0");
if (byteh > 7 || bytel > 7)
{
throw new ArgumentOutOfRangeException(nameof(xe),
$"Param '{name}': byteh={byteh} bytel={bytel} out of 0-7.");
}
var p = new CanBusParameter
{
Name = name,
MessageId = uint.Parse(xe.Attribute("busid")?.Value ?? "0",
NumberStyles.HexNumber),
ByteH = byteh,
ByteL = bytel,
Type = int.Parse(xe.Attribute("type")?.Value ?? "0"),
IsReceive = !string.Equals(xe.Attribute("send")?.Value, "true",
StringComparison.OrdinalIgnoreCase),
Alpha = ParseDecimal(xe.Attribute("filter")?.Value, 1.0),
DisableCalibration = string.Equals(xe.Attribute("disableparams")?.Value,
"true", StringComparison.OrdinalIgnoreCase),
UseLegacyTransform = true
};
if (p.DisableCalibration)
{
p.SetIdentityCalibration();
}
else
{
p.P1 = ParseDecimal(xe.Attribute("p1")?.Value, 1.0);
p.P2 = ParseDecimal(xe.Attribute("p2")?.Value, 0.0);
p.P3 = ParseDecimal(xe.Attribute("p3")?.Value, 0.0);
p.P4 = ParseDecimal(xe.Attribute("p4")?.Value, 1.0);
p.P5 = ParseDecimal(xe.Attribute("p5")?.Value, 0.0);
p.P6 = ParseDecimal(xe.Attribute("p6")?.Value, 0.0);
}
return p;
}
/// <summary>Parses a decimal string that may use comma or dot as separator.</summary>
internal static double ParseDecimal(string? value, double fallback)
{
if (string.IsNullOrEmpty(value)) return fallback;
return double.Parse(value.Replace(',', '.'), CultureInfo.InvariantCulture);
}
/// <summary>
/// Serialises this parameter to an XML element using the pump-param format
/// (<c>busid</c>, <c>p1p6</c>, <c>send</c>, <c>disableparams</c>).
/// Used when persisting pump definitions to <c>pumps.xml</c>.
/// </summary>
public XElement ToPumpXml()
{
var elm = new XElement(Name,
new XAttribute("busid", MessageId.ToString("X")),
new XAttribute("byteh", ByteH),
new XAttribute("bytel", ByteL),
new XAttribute("type", Type));
if (!IsReceive)
elm.Add(new XAttribute("send", "true"));
if (Alpha != 1.0)
elm.Add(new XAttribute("filter", Alpha.ToString(CultureInfo.InvariantCulture)));
if (DisableCalibration)
{
elm.Add(new XAttribute("disableparams", "true"));
}
else
{
elm.Add(new XAttribute("p1", P1.ToString(CultureInfo.InvariantCulture)));
elm.Add(new XAttribute("p2", P2.ToString(CultureInfo.InvariantCulture)));
elm.Add(new XAttribute("p3", P3.ToString(CultureInfo.InvariantCulture)));
elm.Add(new XAttribute("p4", P4.ToString(CultureInfo.InvariantCulture)));
elm.Add(new XAttribute("p5", P5.ToString(CultureInfo.InvariantCulture)));
elm.Add(new XAttribute("p6", P6.ToString(CultureInfo.InvariantCulture)));
}
return elm;
}
/// <summary>Serialises this parameter to an XML element for persistence in bench.xml.</summary>
public XElement ToXml()
{
var elm = new XElement(Name,
new XAttribute("id", MessageId.ToString("X")),
new XAttribute("byteh", ByteH),
new XAttribute("bytel", ByteL),
new XAttribute("direction", IsReceive ? "rx" : "tx"));
if (Alpha != 1.0)
elm.Add(new XAttribute("filter", Alpha));
if (Factor != 1.0)
elm.Add(new XAttribute("factor", Factor));
if (Offset != 0.0)
elm.Add(new XAttribute("offset", Offset));
if (IsInverse)
elm.Add(new XAttribute("type", "inverse"));
return elm;
}
}
// ── Well-known parameter name constants ──────────────────────────────────────
/// <summary>String constants for bench CAN parameter names.</summary>
public static class BenchParameterNames
{
public const string Rpm = "RPM";
public const string Counter = "Counter";
public const string BaudRate = "BaudRate";
public const string BenchRpm = "BenchRPM";
public const string BenchCounter = "BenchCounter";
public const string Temp = "BenchTemp";
public const string TempIn = "T-in";
public const string TempOut = "T-out";
public const string Temp4 = "T4";
public const string QDelivery = "QDelivery";
public const string QOver = "QOver";
public const string ElectronicMsg = "ElectronicMsg";
public const string EncoderResolution = "EncoderResolution";
public const string PsgEncoderValue = "PSGEncoderValue";
public const string PsgEncoderWorking = "PSGEncoderWorking";
public const string InjEncoderValue = "InyectorEncoderValue";
public const string InjEncoderWorking = "InyectorEncoderWorking";
public const string ManualEncoderValue = "ManualEncoderValue";
public const string Alarms = "Alarms";
public const string Pressure = "Pressure";
public const string AnalogSensor2 = "AnalogicSensor2";
}
/// <summary>String constants for pump CAN parameter names.</summary>
public static class PumpParameterNames
{
public const string MemoryRequest = "MemoryRequest";
public const string Me = "me";
public const string Fbkw = "FBKW";
public const string PreIn = "mepi";
public const string Rpm = "RPM";
public const string Temp = "Temp";
public const string Tein = "Tein";
public const string TestUnlock = "TestUnlock";
public const string TestImmo = "TestImmo";
public const string Status = "Status";
public const string Empf3 = "Empf3";
}
/// <summary>String constants for K-Line / KWP data dictionary keys.</summary>
public static class KlineKeys
{
public const string PumpId = "pumpID";
public const string SerialNumber = "SerialNumber";
public const string ModelReference = "ModelReference";
public const string PumpControl = "PumpControl";
public const string ModelIndex = "ModelIndex";
public const string DataRecord = "DataRecord";
public const string SwVersion1 = "SWV1";
public const string SwVersion2 = "SWV2";
public const string Dfi = "dfi";
public const string Errors = "ErrorCodes";
public const string Result = "result";
public const string NoErrors = "No fault codes";
public const string ConnectError = "CON ERROR";
}
/// <summary>Relay name constants — mirror the bench XML definition.</summary>
public static class RelayNames
{
public const string Electronic = "Electronic";
public const string OilPump = "OilPump";
public const string DepositCooler = "DepositCooler";
public const string DepositHeater = "DepositHeater";
public const string DirectionRight = "Reserve";
public const string Counter = "Counter";
public const string DirectionLeft = "Direction";
public const string TinCooler = "TinCooler";
public const string Pulse4Signal = "Pulse4Signal";
public const string Flasher = "Flasher";
}
}