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>
361 lines
16 KiB
C#
361 lines
16 KiB
C#
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 P1–P6 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 P1–P6 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 P1–P6 transfer function (pump params) ─────────────────────────
|
||
|
||
/// <summary>
|
||
/// Applies the P1–P6 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><Params></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>p1–p6</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";
|
||
}
|
||
}
|