Files
HC_APTBS/Models/CanBusParameter.cs
LucianoDev c617854c09 feat: implement SavePump/SaveAlarms, fix config round-trip bugs, redesign PDF reports
Config system fixes:
- Implement SavePump() — full XML serialization with insert/update by pump ID
- Add CanBusParameter.ToPumpXml() for legacy P1-P6 pump param format
- Fix LastRotationDirection never loaded in LoadSettings()
- Add SaveAlarms() to ConfigurationService and IConfigurationService
- Remove dead fields AppSettings.Clients and AppSettings.PumpIds

PDF report redesign:
- Professional layout with charts, verdict badges, and tolerance bands
- Add ReportChartRenderer (SVG) and ReportTheme styling constants
- Embed default_logo.png as fallback report logo

Documentation:
- Add gap analysis docs (config validation, ford unlock, missing features)
- Update CLAUDE.md architecture, known gaps, and debt tracking

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 15:21:22 +02:00

361 lines
16 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>
/// 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>
/// 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;
// ── 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).
/// </summary>
public double GetTransformResult()
{
if (IsReceive)
{
return (-P2 - P3 * P5 - P4 * P6 + P4 * Value)
/ (P1 + P3 * P5 + P3 * P6 - P3 * Value);
}
return ((P1 * Value + P2) / (P3 * Value + P4)) + 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)
{
var p = new CanBusParameter
{
Name = xe.Name.LocalName,
MessageId = uint.Parse(xe.Attribute("busid")?.Value ?? "0",
NumberStyles.HexNumber),
ByteH = ushort.Parse(xe.Attribute("byteh")?.Value ?? "0"),
ByteL = ushort.Parse(xe.Attribute("bytel")?.Value ?? "0"),
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";
}
}