feat: redesign bench calibration (factor/offset), add Ttank/P2 displays, fix sensor calibration

- Replace P1-P6 rational transfer function with factor/offset model for bench params
- Add explicit rx/tx direction flags in bench XML configuration
- Add T.Tank (BenchTemp) and P2 (AnalogSensor2) to temperature/pressure display
- Apply SensorConfiguration calibration to pressure channels, fix empty sensors.xml fallback
- Add live value labels to flowmeter charts
- Hide pump live values and PSG encoder standalone label
- Add K-Line connection state model, improve KWP service and status displays
- Restructure .claude/skills into subdirectory format

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-14 21:25:30 +02:00
parent 4964806de1
commit 4891eb6812
20 changed files with 881 additions and 185 deletions

View File

@@ -8,13 +8,12 @@ namespace HC_APTBS.Models
/// 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>Transfer function (transmit / non-receive):</b><br/>
/// <c>output = ((P1 * Value + P2) / (P3 * Value + P4)) + P5 + P6</c></para>
/// <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>Inverse transfer function (receive):</b><br/>
/// <c>output = (P2 P3·P5 P4·P6 + P4·Value) / (P1 + P3·P5 + P3·P6 P3·Value)</c></para>
///
/// <para>Set <see cref="DisableCalibration"/> to use the identity transform (P1=1, P2=0, P3=0, P4=1, P5=0, P6=0).</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
{
@@ -40,7 +39,18 @@ namespace HC_APTBS.Models
/// </summary>
public int Type { get; set; } = 0;
// ── Calibration coefficients (P1P6) ────────────────────────────────────
// ── 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; }
@@ -66,6 +76,12 @@ namespace HC_APTBS.Models
/// </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>
@@ -78,8 +94,8 @@ namespace HC_APTBS.Models
public bool NeedsUpdate { get; set; }
/// <summary>
/// When true this parameter is used in the receive direction and applies the
/// inverse transfer function in <see cref="GetTransformResult"/>.
/// 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; }
@@ -98,33 +114,62 @@ namespace HC_APTBS.Models
set => MessageId = value;
}
// ── Transfer function ────────────────────────────────────────────────────
// ── Simple calibration ────────────────────────────────────────────────────
/// <summary>
/// Applies the calibration transfer function to <see cref="Value"/>.
/// 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>
/// <returns>Calibrated engineering-unit result.</returns>
public double GetTransformResult()
{
if (IsReceive)
{
// Inverse function: maps a measured value back to the raw command value.
return (-P2 - P3 * P5 - P4 * P6 + P4 * Value)
/ (P1 + P3 * P5 + P3 * P6 - P3 * Value);
}
// Forward function: maps a setpoint to the raw CAN integer to transmit.
return ((P1 * Value + P2) / (P3 * Value + P4)) + P5 + P6;
}
/// <summary>
/// Returns the rounded integer value ready to be packed into a CAN frame byte pair.
/// Returns the raw CAN value for transmission.
/// Delegates to simple or legacy calibration depending on param type.
/// </summary>
public double GetTransmitValue() => GetTransformResult();
public double GetTransmitValue()
{
if (UseLegacyTransform)
return GetTransformResult();
return CalibrateReverse(Value);
}
/// <summary>
/// Resets calibration coefficients to the identity transform so the raw value
/// passes through <see cref="GetTransformResult"/> unchanged.
/// passes through unchanged.
/// </summary>
public void SetIdentityCalibration()
{
@@ -152,7 +197,8 @@ namespace HC_APTBS.Models
StringComparison.OrdinalIgnoreCase),
Alpha = ParseDecimal(xe.Attribute("filter")?.Value, 1.0),
DisableCalibration = string.Equals(xe.Attribute("disableparams")?.Value,
"true", StringComparison.OrdinalIgnoreCase)
"true", StringComparison.OrdinalIgnoreCase),
UseLegacyTransform = true
};
if (p.DisableCalibration)
@@ -173,7 +219,7 @@ namespace HC_APTBS.Models
}
/// <summary>Parses a decimal string that may use comma or dot as separator.</summary>
private static double ParseDecimal(string? value, double fallback)
internal static double ParseDecimal(string? value, double fallback)
{
if (string.IsNullOrEmpty(value)) return fallback;
return double.Parse(value.Replace(',', '.'), CultureInfo.InvariantCulture);
@@ -183,22 +229,19 @@ namespace HC_APTBS.Models
public XElement ToXml()
{
var elm = new XElement(Name,
new XAttribute("id", MessageId.ToString("X")),
new XAttribute("byteh", ByteH),
new XAttribute("bytel", ByteL),
new XAttribute("filter", Alpha),
new XAttribute("disableparams", DisableCalibration));
new XAttribute("id", MessageId.ToString("X")),
new XAttribute("byteh", ByteH),
new XAttribute("bytel", ByteL),
new XAttribute("direction", IsReceive ? "rx" : "tx"));
if (!DisableCalibration)
{
elm.Add(
new XAttribute("p1", P1),
new XAttribute("p2", P2),
new XAttribute("p3", P3),
new XAttribute("p4", P4),
new XAttribute("p5", P5),
new XAttribute("p6", P6));
}
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;
}