using System;
using System.Globalization;
using System.Xml.Linq;
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 .
///
/// Bench params use the simple calibration model:
/// Linear: eng = raw * Factor + Offset
/// Inverse: eng = Factor / raw + Offset
///
/// Pump params (legacy) use the P1–P6 rational transfer function via
/// . Set to enable.
///
public class CanBusParameter
{
// ── Frame addressing ─────────────────────────────────────────────────────
/// CAN message identifier this parameter belongs to.
public uint MessageId { get; set; }
/// Index of the high byte within the 8-byte CAN payload.
public ushort ByteH { get; set; }
/// Index of the low byte within the 8-byte CAN payload.
public ushort ByteL { get; set; }
// ── Metadata ─────────────────────────────────────────────────────────────
/// Human-readable parameter name used for lookup (see ).
public string Name { get; set; } = string.Empty;
///
/// Sensor/encoding type selector. Used by the receive decoder to choose
/// the correct bit-extraction formula (e.g. for temperature and RPM).
///
public int Type { get; set; } = 0;
// ── Simple calibration (bench params) ────────────────────────────────────
/// Multiplication factor: eng = raw * Factor + Offset.
public double Factor { get; set; } = 1.0;
/// Additive offset: eng = raw * Factor + Offset.
public double Offset { get; set; }
/// When true, calibration uses eng = Factor / raw + Offset.
public bool IsInverse { get; set; }
// ── Legacy P1–P6 calibration (pump params) ───────────────────────────────
/// Transfer function coefficient P1 (numerator multiplier).
public double P1 { get; set; }
/// Transfer function coefficient P2 (numerator offset).
public double P2 { get; set; }
/// Transfer function coefficient P3 (denominator multiplier).
public double P3 { get; set; }
/// Transfer function coefficient P4 (denominator offset).
public double P4 { get; set; }
/// Transfer function coefficient P5 (additive offset 1).
public double P5 { get; set; }
/// Transfer function coefficient P6 (additive offset 2).
public double P6 { get; set; }
///
/// 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.
///
public bool DisableCalibration { get; set; }
///
/// When true, is used instead of
/// . Enabled for pump params loaded via .
///
public bool UseLegacyTransform { get; set; }
// ── Runtime state ─────────────────────────────────────────────────────────
/// Current decoded engineering-unit value (updated by the CAN read thread).
public double Value { get; set; }
///
/// True when has been updated since the last UI refresh tick.
/// Reset to false by the consumer after reading.
///
public bool NeedsUpdate { get; set; }
///
/// Raised on the CAN read thread after a decoded frame causes
/// 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.
///
public event Action? ValueChanged;
///
/// Invokes . Intended to be called by the CAN decoder
/// after a value update; internal so other layers cannot raise it spuriously.
///
internal void RaiseValueChanged() => ValueChanged?.Invoke(this);
///
/// True for receive-direction params (decoded from incoming CAN frames).
/// False for transmit-direction params (packed into outgoing frames).
///
public bool IsReceive { get; set; }
///
/// Exponential moving average coefficient α ∈ (0, 1].
/// α = 1 means no smoothing (pass-through); smaller values give more smoothing.
///
public double Alpha { get; set; } = 1.0;
// ── Runtime-warning plumbing ──────────────────────────────────────────────
///
/// Static logging hook invoked when encounters
/// a zero denominator. Wired at app startup to IAppLogger.Warning.
/// Left null keeps the Models layer DI-free and testable.
/// Signature: (source, message).
///
public static Action? WarningLogger { get; set; }
/// Set once per instance after the first zero-denominator warning, to prevent hot-loop log spam.
private bool _warnedDenomZero;
// ── Convenience alias kept for cross-file compatibility ───────────────────
/// Alias for — used by legacy call sites.
public uint ID
{
get => MessageId;
set => MessageId = value;
}
// ── Simple calibration ────────────────────────────────────────────────────
///
/// Converts a raw CAN value to engineering units using Factor/Offset.
///
public double Calibrate(double raw)
{
if (IsInverse)
return raw != 0 ? Factor / raw + Offset : 0;
return raw * Factor + Offset;
}
///
/// Converts an engineering value back to raw CAN value (for transmit).
///
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) ─────────────────────────
///
/// Applies the P1–P6 rational transfer function to .
/// Used only by pump params ( = true).
/// Returns 0 and warns once if the denominator is zero.
///
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;
}
}
///
/// Returns the raw CAN value for transmission.
/// Delegates to simple or legacy calibration depending on param type.
///
public double GetTransmitValue()
{
if (UseLegacyTransform)
return GetTransformResult();
return CalibrateReverse(Value);
}
///
/// Resets calibration coefficients to the identity transform so the raw value
/// passes through unchanged.
///
public void SetIdentityCalibration()
{
P1 = 1; P2 = 0; P3 = 0; P4 = 1; P5 = 0; P6 = 0;
DisableCalibration = true;
}
// ── XML serialisation ─────────────────────────────────────────────────────
///
/// Deserialises a pump CAN parameter from an XML element inside the
/// <Params> section of a pump definition.
///
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;
}
/// Parses a decimal string that may use comma or dot as separator.
internal static double ParseDecimal(string? value, double fallback)
{
if (string.IsNullOrEmpty(value)) return fallback;
return double.Parse(value.Replace(',', '.'), CultureInfo.InvariantCulture);
}
///
/// Serialises this parameter to an XML element using the pump-param format
/// (busid, p1–p6, send, disableparams).
/// Used when persisting pump definitions to pumps.xml.
///
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;
}
/// Serialises this parameter to an XML element for persistence in bench.xml.
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 ──────────────────────────────────────
/// String constants for bench CAN parameter names.
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";
}
/// String constants for pump CAN parameter names.
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";
}
/// String constants for K-Line / KWP data dictionary keys.
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";
}
/// Relay name constants — mirror the bench XML definition.
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";
}
}