initial commit
This commit is contained in:
43
Models/Alarm.cs
Normal file
43
Models/Alarm.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace HC_APTBS.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines a single alarm condition monitored by the bench controller.
|
||||
/// Alarms are bit-mapped: the bench asserts a bitmask in the Alarms CAN parameter
|
||||
/// (<see cref="BenchParameterNames.Alarms"/>) and each bit corresponds to a named condition.
|
||||
/// </summary>
|
||||
public class Alarm
|
||||
{
|
||||
/// <summary>Bit position in the alarm bitmask (0-based).</summary>
|
||||
public int Bit { get; set; }
|
||||
|
||||
/// <summary>Human-readable description of this alarm condition.</summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// When true, this alarm immediately halts any running test and requires
|
||||
/// operator acknowledgement before the bench can be restarted.
|
||||
/// </summary>
|
||||
public bool IsCritical { get; set; }
|
||||
|
||||
/// <summary>True while this alarm condition is currently active.</summary>
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
/// <summary>Serialises this alarm to XML for persistence in alarms.xml.</summary>
|
||||
public XElement ToXml()
|
||||
=> new XElement("Alarm",
|
||||
new XAttribute("bit", Bit),
|
||||
new XAttribute("desc", Description),
|
||||
new XAttribute("critical", IsCritical));
|
||||
|
||||
/// <summary>Deserialises an alarm from an XML element.</summary>
|
||||
public static Alarm FromXml(XElement element)
|
||||
=> new Alarm
|
||||
{
|
||||
Bit = int.Parse(element.Attribute("bit")?.Value ?? "0"),
|
||||
Description = element.Attribute("desc")?.Value ?? string.Empty,
|
||||
IsCritical = bool.Parse(element.Attribute("critical")?.Value ?? "false")
|
||||
};
|
||||
}
|
||||
}
|
||||
173
Models/BenchConfiguration.cs
Normal file
173
Models/BenchConfiguration.cs
Normal file
@@ -0,0 +1,173 @@
|
||||
using System.Collections.Generic;
|
||||
using Peak.Can.Basic;
|
||||
using TPCANHandle = System.UInt16;
|
||||
|
||||
namespace HC_APTBS.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Complete bench parameter configuration: CAN parameter map plus relay definitions.
|
||||
/// Loaded from bench.xml at startup and edited via the BenchParamConfig view.
|
||||
/// </summary>
|
||||
public class BenchConfiguration
|
||||
{
|
||||
/// <summary>PCAN channel handle used for bench communication.</summary>
|
||||
public TPCANHandle Channel { get; set; } = PCANBasic.PCAN_USBBUS1;
|
||||
|
||||
/// <summary>All bench parameters, keyed by name for quick lookup.</summary>
|
||||
public Dictionary<string, CanBusParameter> ParametersByName { get; set; } = new();
|
||||
|
||||
/// <summary>Bench parameters grouped by CAN message ID for frame decoding.</summary>
|
||||
public Dictionary<uint, List<CanBusParameter>> ParametersById { get; set; } = new();
|
||||
|
||||
/// <summary>Relay / solenoid output definitions, keyed by name.</summary>
|
||||
public Dictionary<string, Relay> Relays { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Application-wide configuration settings persisted in config.xml.
|
||||
/// </summary>
|
||||
public class AppSettings
|
||||
{
|
||||
// ── Temperature control ───────────────────────────────────────────────
|
||||
|
||||
/// <summary>Maximum allowable oil temperature (°C) before the bench raises an alarm.</summary>
|
||||
public int TempMax { get; set; } = 45;
|
||||
|
||||
/// <summary>Minimum oil temperature (°C) required before tests begin.</summary>
|
||||
public int TempMin { get; set; } = 35;
|
||||
|
||||
// ── Refresh intervals ─────────────────────────────────────────────────
|
||||
|
||||
/// <summary>UI status refresh period (ms) during normal bench operation.</summary>
|
||||
public int RefreshBenchInterfaceMs { get; set; } = 20;
|
||||
|
||||
/// <summary>UI refresh period (ms) while reading pump EEPROM data over K-Line.</summary>
|
||||
public int RefreshWhileReadingMs { get; set; } = 1500;
|
||||
|
||||
/// <summary>CAN bus polling interval (ms) on the read thread.</summary>
|
||||
public int RefreshCanBusReadMs { get; set; } = 2;
|
||||
|
||||
/// <summary>Interval (ms) between pump-status CAN request messages.</summary>
|
||||
public int RefreshPumpRequestMs { get; set; } = 250;
|
||||
|
||||
/// <summary>Interval (ms) between pump-parameter CAN request messages.</summary>
|
||||
public int RefreshPumpParamsMs { get; set; } = 4;
|
||||
|
||||
/// <summary>Blink period (ms) for LED indicator controls.</summary>
|
||||
public int BlinkIntervalMs { get; set; } = 1000;
|
||||
|
||||
/// <summary>Flasher relay toggle interval (ms).</summary>
|
||||
public int FlasherIntervalMs { get; set; } = 800;
|
||||
|
||||
// ── PID temperature controller ────────────────────────────────────────
|
||||
|
||||
/// <summary>Proportional gain.</summary>
|
||||
public double PidP { get; set; } = 0.1;
|
||||
|
||||
/// <summary>Integral gain.</summary>
|
||||
public double PidI { get; set; } = 0.1;
|
||||
|
||||
/// <summary>Derivative gain.</summary>
|
||||
public double PidD { get; set; } = 0.04;
|
||||
|
||||
/// <summary>PID loop period (ms).</summary>
|
||||
public int PidLoopMs { get; set; } = 250;
|
||||
|
||||
// ── Safety limits ─────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Maximum allowable bench speed (RPM) before the bench triggers an emergency stop.</summary>
|
||||
public int SecurityRpmLimit { get; set; } = 2500;
|
||||
|
||||
/// <summary>Maximum allowable bench pressure (bar).</summary>
|
||||
public int MaxPressureBar { get; set; } = 26;
|
||||
|
||||
// ── Test tolerance extensions ─────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Fractional tolerance extension for UP tests.
|
||||
/// A value of 0.08 extends each tolerance bound by 8% of its magnitude.
|
||||
/// </summary>
|
||||
public double ToleranceUpExtension { get; set; } = 0.08;
|
||||
|
||||
/// <summary>
|
||||
/// Fractional tolerance extension for PFP tests.
|
||||
/// A value of 0.1 extends each tolerance bound by 10% of its magnitude.
|
||||
/// </summary>
|
||||
public double TolerancePfpExtension { get; set; } = 0.1;
|
||||
|
||||
// ── Encoder ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Encoder pulses per revolution (default 4096 for PSG encoder).</summary>
|
||||
public int EncoderResolution { get; set; } = 4096;
|
||||
|
||||
// ── Motor control ─────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Analogue output voltage (V) that corresponds to maximum RPM.</summary>
|
||||
public double VoltageForMaxRpm { get; set; } = 10;
|
||||
|
||||
/// <summary>Maximum motor speed the bench can command (RPM).</summary>
|
||||
public int MaxRpm { get; set; } = 2500;
|
||||
|
||||
// ── Direction ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Default relay state for the "right" rotation direction.</summary>
|
||||
public bool RightRelayValue { get; set; } = true;
|
||||
|
||||
/// <summary>Last rotation direction selected by the operator.</summary>
|
||||
public short LastRotationDirection { get; set; } = RotationDirection.Right;
|
||||
|
||||
/// <summary>When true, the T-in temperature sensor check is bypassed.</summary>
|
||||
public bool DefaultIgnoreTin { get; set; } = true;
|
||||
|
||||
// ── Log rotation ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Number of daily log files to retain before the oldest is deleted.</summary>
|
||||
public int DaysKeepLogs { get; set; } = 7;
|
||||
|
||||
// ── Report ────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Company name printed in the report header.</summary>
|
||||
public string CompanyName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Company address/info printed in the report header.</summary>
|
||||
public string CompanyInfo { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Absolute path to the company logo image for the report.</summary>
|
||||
public string ReportLogoPath { get; set; } = string.Empty;
|
||||
|
||||
// ── K-Line port ───────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Serial port or FTDI device identifier for the K-Line interface.</summary>
|
||||
public string KLinePort { get; set; } = string.Empty;
|
||||
|
||||
// ── UI ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>UI language code, e.g. "ESP" or "ENG".</summary>
|
||||
public string Language { get; set; } = "ESP";
|
||||
|
||||
// ── Relations ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>RPM-to-voltage lookup table for motor speed control.</summary>
|
||||
public List<RpmVoltageRelation> Relations { get; set; } = new();
|
||||
|
||||
// ── Sensor calibration ────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Calibration data for each analogue sensor channel (keyed by channel number).</summary>
|
||||
public Dictionary<int, SensorConfiguration> Sensors { get; set; } = new();
|
||||
|
||||
// ── Alarms ────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Active alarm definitions loaded from alarms.xml.</summary>
|
||||
public List<Alarm> Alarms { get; set; } = new();
|
||||
|
||||
// ── Clients ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Client/operator database, keyed by name (sorted).</summary>
|
||||
public SortedDictionary<string, string> Clients { get; set; } = new();
|
||||
|
||||
// ── Pump IDs ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>List of known pump identifiers available in the database.</summary>
|
||||
public List<string> PumpIds { get; set; } = new();
|
||||
}
|
||||
}
|
||||
283
Models/CanBusParameter.cs
Normal file
283
Models/CanBusParameter.cs
Normal file
@@ -0,0 +1,283 @@
|
||||
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>Transfer function (transmit / non-receive):</b><br/>
|
||||
/// <c>output = ((P1 * Value + P2) / (P3 * Value + P4)) + P5 + P6</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>
|
||||
/// </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;
|
||||
|
||||
// ── Calibration coefficients (P1–P6) ─────────────────────────────────────
|
||||
|
||||
/// <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; }
|
||||
|
||||
// ── 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>
|
||||
/// When true this parameter is used in the receive direction and applies the
|
||||
/// inverse transfer function in <see cref="GetTransformResult"/>.
|
||||
/// </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;
|
||||
}
|
||||
|
||||
// ── Transfer function ─────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Applies the calibration transfer function to <see cref="Value"/>.
|
||||
/// </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.
|
||||
/// </summary>
|
||||
public double GetTransmitValue() => GetTransformResult();
|
||||
|
||||
/// <summary>
|
||||
/// Resets calibration coefficients to the identity transform so the raw value
|
||||
/// passes through <see cref="GetTransformResult"/> 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)
|
||||
};
|
||||
|
||||
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>
|
||||
private 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 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("filter", Alpha),
|
||||
new XAttribute("disableparams", DisableCalibration));
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
159
Models/PumpDefinition.cs
Normal file
159
Models/PumpDefinition.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
using System.Collections.Generic;
|
||||
using Peak.Can.Basic;
|
||||
using TPCANHandle = System.UInt16;
|
||||
|
||||
namespace HC_APTBS.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes a diesel injection pump model supported by the test bench.
|
||||
/// This is the configuration-time definition loaded from the pump database.
|
||||
/// Runtime state (current RPM, temperature, test progress) lives in the ViewModel.
|
||||
/// </summary>
|
||||
public class PumpDefinition
|
||||
{
|
||||
// ── Identity ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Pump identifier code (e.g. "VP44-0460424", loaded from K-Line EEPROM).</summary>
|
||||
public string Id { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Pump model name shown in the UI.</summary>
|
||||
public string Model { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Serial number stamped on the pump body.</summary>
|
||||
public string SerialNumber { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Injector nozzle specification string (parsed from ECU text).</summary>
|
||||
public string Injector { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>High-pressure tube specification.</summary>
|
||||
public string Tube { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Solenoid valve specification.</summary>
|
||||
public string Valve { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Electrical supply tension.</summary>
|
||||
public string Tension { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Free-text info line from the pump database.</summary>
|
||||
public string Info { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Raw ECU text returned by ReadEcuInfo over K-Line.</summary>
|
||||
public string EcuText { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Lock-angle (shaft timing reference) in degrees.</summary>
|
||||
public string Chaveta { get; set; } = string.Empty;
|
||||
|
||||
// ── Physical parameters ───────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Injection timing lock angle (degrees) used during the Lock Angle test phase.
|
||||
/// </summary>
|
||||
public double LockAngle { get; set; }
|
||||
|
||||
/// <summary>Measured lock angle result, populated after the Lock Angle phase completes.</summary>
|
||||
public double LockAngleResult { get; set; }
|
||||
|
||||
/// <summary>True if this pump model has a pilot/pre-injection solenoid.</summary>
|
||||
public bool HasPreInjection { get; set; }
|
||||
|
||||
/// <summary>True for 4-cylinder pump configurations; false for 6-cylinder.</summary>
|
||||
public bool Is4Cylinder { get; set; } = true;
|
||||
|
||||
/// <summary>Unlock protocol variant (0 = none, 1 = type 1, 2 = type 2).</summary>
|
||||
public int UnlockType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Pump shaft rotation direction.
|
||||
/// See <see cref="RotationDirection.LeftName"/> and <see cref="RotationDirection.RightName"/>.
|
||||
/// </summary>
|
||||
public string Rotation { get; set; } = RotationDirection.RightName;
|
||||
|
||||
// ── CAN configuration ─────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>PCAN channel handle used to communicate with this pump.</summary>
|
||||
public TPCANHandle CanChannel { get; set; } = PCANBasic.PCAN_USBBUS1;
|
||||
|
||||
/// <summary>CAN baudrate for this pump (most VP44 units use 500 kbps).</summary>
|
||||
public TPCANBaudrate CanBaudrate { get; set; } = TPCANBaudrate.PCAN_BAUD_500K;
|
||||
|
||||
/// <summary>
|
||||
/// Parameters specific to this pump, keyed by name for quick lookup.
|
||||
/// These are separate from the bench parameters.
|
||||
/// </summary>
|
||||
public Dictionary<string, CanBusParameter> ParametersByName { get; set; } = new();
|
||||
|
||||
/// <summary>Pump CAN parameters grouped by message ID for frame decoding.</summary>
|
||||
public Dictionary<uint, List<CanBusParameter>> ParametersById { get; set; } = new();
|
||||
|
||||
// ── K-Line data ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// K-Line / KWP2000 data read from the pump ECU EEPROM.
|
||||
/// Keys match <see cref="KlineKeys"/> constants.
|
||||
/// </summary>
|
||||
public Dictionary<string, string> KlineInfo { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Pump hardware version used to select the correct KWP protocol variant.
|
||||
/// 0 = V1 original, 1 = V2, 2 = V3/V4.
|
||||
/// </summary>
|
||||
public int KwpVersion { get; set; }
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Ordered list of test procedures applicable to this pump model.</summary>
|
||||
public List<TestDefinition> Tests { get; set; } = new();
|
||||
|
||||
// ── Runtime live values ────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Current fuel quantity feedback value (me) from the pump ECU.</summary>
|
||||
public double ValueMe { get; set; }
|
||||
|
||||
/// <summary>Current FBKW (Füllungsbeiwert / fill factor) value from the pump ECU.</summary>
|
||||
public double ValueFbkw { get; set; }
|
||||
|
||||
/// <summary>Current pre-injection quantity value from the pump ECU.</summary>
|
||||
public double ValuePreIn { get; set; }
|
||||
|
||||
// ── Test helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Merges phases from a WL-type test into the first F-type test in the list,
|
||||
/// then renames it to WL. This matches the legacy pump database convention
|
||||
/// where WL extends the base F test with warm-up phases.
|
||||
/// </summary>
|
||||
internal void CombineTestWL(TestDefinition wlTest)
|
||||
{
|
||||
foreach (var existing in Tests)
|
||||
{
|
||||
if (existing.Name == TestType.F)
|
||||
{
|
||||
existing.Phases.AddRange(wlTest.Phases);
|
||||
existing.Name = TestType.Wl;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Parses the injector, tube, valve, and tension fields from the raw ECU text string.
|
||||
/// The ECU text format is: "(injector) tube valve tension"
|
||||
/// </summary>
|
||||
public void ParseEcuText()
|
||||
{
|
||||
if (string.IsNullOrEmpty(EcuText)) return;
|
||||
|
||||
int closeParenPos = EcuText.IndexOf(')') + 1;
|
||||
Injector = EcuText[..closeParenPos].Trim();
|
||||
|
||||
var rest = EcuText[closeParenPos..];
|
||||
var parts = rest.Split(new[] { " " }, System.StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (parts.Length > 0) Tube = parts[0].Trim();
|
||||
if (parts.Length > 1) Valve = parts[1].Trim();
|
||||
if (parts.Length > 2) Tension = parts[2].Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
53
Models/PumpStatusDefinition.cs
Normal file
53
Models/PumpStatusDefinition.cs
Normal file
@@ -0,0 +1,53 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace HC_APTBS.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes how a multi-bit status word returned by the pump ECU should be
|
||||
/// displayed in the UI. Each <see cref="PumpStatusDefinition"/> maps to one
|
||||
/// CAN status parameter and contains a set of bit-field <see cref="StatusBit"/>
|
||||
/// definitions.
|
||||
/// </summary>
|
||||
public class PumpStatusDefinition
|
||||
{
|
||||
/// <summary>Numeric identifier of this status word.</summary>
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>Display label for the status word group.</summary>
|
||||
public string Name { get; set; } = "-";
|
||||
|
||||
/// <summary>Bit-field definitions within this status word.</summary>
|
||||
public List<StatusBit> Bits { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines the meaning of a single bit (or bit-group) within a pump status word.
|
||||
/// </summary>
|
||||
public class StatusBit
|
||||
{
|
||||
/// <summary>Bit position (0-based) within the status word.</summary>
|
||||
public int Bit { get; set; }
|
||||
|
||||
/// <summary>When false, this bit position is ignored in the display.</summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>Possible states and their display colours.</summary>
|
||||
public List<StatusBitValue> Values { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single state value for a <see cref="StatusBit"/>: a numeric state code
|
||||
/// mapped to a display colour and human-readable description.
|
||||
/// </summary>
|
||||
public class StatusBitValue
|
||||
{
|
||||
/// <summary>Numeric state (0 or 1 for single-bit fields).</summary>
|
||||
public int State { get; set; }
|
||||
|
||||
/// <summary>HTML hex colour used to paint the indicator (e.g. "26C200" for green).</summary>
|
||||
public string Color { get; set; } = "26C200";
|
||||
|
||||
/// <summary>Human-readable description of this state.</summary>
|
||||
public string Description { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
88
Models/Relay.cs
Normal file
88
Models/Relay.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace HC_APTBS.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a single relay or solenoid output on the test bench.
|
||||
/// Each relay is mapped to a specific bit position within a CAN message.
|
||||
/// The bench controller reads a bitmask from CAN message ID <see cref="MessageId"/>
|
||||
/// and asserts the corresponding output.
|
||||
/// </summary>
|
||||
public class Relay
|
||||
{
|
||||
/// <summary>
|
||||
/// True when the relay is energised (output ON); false when de-energised.
|
||||
/// </summary>
|
||||
public const bool On = true;
|
||||
/// <summary>False indicates the relay is de-energised (output OFF).</summary>
|
||||
public const bool Off = false;
|
||||
|
||||
/// <summary>Human-readable relay name (see <see cref="RelayNames"/>).</summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>CAN message identifier that carries this relay's bitmask.</summary>
|
||||
public uint MessageId { get; set; }
|
||||
|
||||
/// <summary>Bit position within the 64-bit relay bitmask sent in the CAN payload.</summary>
|
||||
public int Bit { get; set; }
|
||||
|
||||
/// <summary>Current output state: true = energised, false = de-energised.</summary>
|
||||
public bool State { get; set; }
|
||||
|
||||
/// <summary>Parameterless constructor for deserialisation.</summary>
|
||||
public Relay() { }
|
||||
|
||||
/// <param name="name">Relay name constant from <see cref="RelayNames"/>.</param>
|
||||
/// <param name="messageId">CAN message ID carrying the relay bitmask.</param>
|
||||
/// <param name="bit">Bit position in the 64-bit bitmask.</param>
|
||||
public Relay(string name, uint messageId, int bit)
|
||||
{
|
||||
Name = name;
|
||||
MessageId = messageId;
|
||||
Bit = bit;
|
||||
}
|
||||
|
||||
/// <summary>Serialises this relay definition to XML for persistence.</summary>
|
||||
public XElement ToXml()
|
||||
=> new XElement("Rele",
|
||||
new XAttribute("name", Name),
|
||||
new XAttribute("id", MessageId.ToString("X")),
|
||||
new XAttribute("bit", Bit));
|
||||
}
|
||||
|
||||
/// <summary>Pump rotation direction constants.</summary>
|
||||
public static class RotationDirection
|
||||
{
|
||||
/// <summary>CAN value for counter-clockwise (left) rotation.</summary>
|
||||
public const short Left = 2;
|
||||
/// <summary>CAN value for clockwise (right) rotation.</summary>
|
||||
public const short Right = 1;
|
||||
|
||||
public const string LeftName = "left";
|
||||
public const string RightName = "right";
|
||||
}
|
||||
|
||||
/// <summary>Encoder operating mode constants.</summary>
|
||||
public static class EncoderMode
|
||||
{
|
||||
public const double ModeOn = 1;
|
||||
public const double ModeOff = 0;
|
||||
|
||||
/// <summary>4-cylinder pulse-per-revolution mode.</summary>
|
||||
public const double Pumps4 = 1;
|
||||
/// <summary>6-cylinder pulse-per-revolution mode.</summary>
|
||||
public const double Pumps6 = 0;
|
||||
|
||||
public const double PositionRelative = 0;
|
||||
public const double PositionAbsolute = 1;
|
||||
}
|
||||
|
||||
/// <summary>CAN baudrate selector values sent to the bench firmware.</summary>
|
||||
public static class BaudrateSelection
|
||||
{
|
||||
/// <summary>Select 500 kbps.</summary>
|
||||
public const double Val500K = 1;
|
||||
/// <summary>Select 250 kbps.</summary>
|
||||
public const double Val250K = 0;
|
||||
}
|
||||
}
|
||||
96
Models/RpmVoltageRelation.cs
Normal file
96
Models/RpmVoltageRelation.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
|
||||
namespace HC_APTBS.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Maps a motor RPM setpoint to the corresponding analogue control voltage
|
||||
/// required to drive the bench motor to that speed.
|
||||
///
|
||||
/// <para>
|
||||
/// The lookup table is ordered by ascending RPM. <see cref="VoltageForRpm"/>
|
||||
/// performs a linear scan and returns the voltage of the last entry whose
|
||||
/// RPM is ≤ the requested value (step interpolation).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class RpmVoltageRelation
|
||||
{
|
||||
/// <summary>Analogue voltage to apply to the motor controller (V).</summary>
|
||||
public double Voltage { get; set; }
|
||||
|
||||
/// <summary>Target motor speed (RPM).</summary>
|
||||
public int Rpm { get; set; }
|
||||
|
||||
/// <param name="voltage">Motor control voltage (V).</param>
|
||||
/// <param name="rpm">Corresponding motor speed (RPM).</param>
|
||||
public RpmVoltageRelation(double voltage, int rpm)
|
||||
{
|
||||
Voltage = voltage;
|
||||
Rpm = rpm;
|
||||
}
|
||||
|
||||
// ── Lookup ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Returns the control voltage for the given <paramref name="targetRpm"/>
|
||||
/// by scanning the ordered <paramref name="table"/> for the closest lower entry.
|
||||
/// Returns 0 if the table is empty or no lower entry is found.
|
||||
/// </summary>
|
||||
/// <param name="targetRpm">Requested motor speed in RPM.</param>
|
||||
/// <param name="table">Ordered (ascending RPM) lookup table.</param>
|
||||
public static double VoltageForRpm(int targetRpm, IReadOnlyList<RpmVoltageRelation> table)
|
||||
{
|
||||
double previousVoltage = -1;
|
||||
foreach (var entry in table)
|
||||
{
|
||||
if (entry.Rpm == targetRpm)
|
||||
return entry.Voltage;
|
||||
|
||||
if (targetRpm < entry.Rpm)
|
||||
return previousVoltage < 0 ? 0 : previousVoltage;
|
||||
|
||||
previousVoltage = entry.Voltage;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ── Serialisation ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Serialises a list of relations to the compact pipe-separated storage format
|
||||
/// used in config.xml: <c>RPM|Voltage;RPM|Voltage;…</c>
|
||||
/// </summary>
|
||||
public static string Serialise(IReadOnlyList<RpmVoltageRelation> relations)
|
||||
{
|
||||
if (relations == null || relations.Count == 0) return string.Empty;
|
||||
|
||||
var parts = new List<string>(relations.Count);
|
||||
foreach (var r in relations)
|
||||
parts.Add($"{r.Rpm}|{r.Voltage.ToString(CultureInfo.InvariantCulture)}");
|
||||
|
||||
return string.Join(";", parts);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserialises a list of relations from the compact storage format.
|
||||
/// </summary>
|
||||
public static List<RpmVoltageRelation> Deserialise(string serialised)
|
||||
{
|
||||
var result = new List<RpmVoltageRelation>();
|
||||
if (string.IsNullOrWhiteSpace(serialised)) return result;
|
||||
|
||||
foreach (var part in serialised.Split(';'))
|
||||
{
|
||||
var tokens = part.Split('|');
|
||||
if (tokens.Length != 2) continue;
|
||||
if (!int.TryParse(tokens[0], out int rpm)) continue;
|
||||
if (!double.TryParse(tokens[1], NumberStyles.Float,
|
||||
CultureInfo.InvariantCulture, out double voltage)) continue;
|
||||
|
||||
result.Add(new RpmVoltageRelation(voltage, rpm));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
106
Models/SensorConfiguration.cs
Normal file
106
Models/SensorConfiguration.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace HC_APTBS.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Calibration parameters for an analogue sensor channel.
|
||||
/// Maps a raw ADC counts value (from the bench CAN bus) to an
|
||||
/// engineering-unit value using a voltage-range to value-range linear mapping,
|
||||
/// then applies a gain and offset correction.
|
||||
///
|
||||
/// <para>
|
||||
/// The bench ADC uses a 10-bit (1024-step) range over 0–5 V.
|
||||
/// Conversion: V = (rawCanBusValue × 5000 / 1024) / 1000
|
||||
/// Engineering value = V × ((MaxVal − MinVal) / (MaxVolt − MinVolt)) × Gain + Offset
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class SensorConfiguration
|
||||
{
|
||||
/// <summary>1-based sensor channel number.</summary>
|
||||
public short Number { get; set; } = 1;
|
||||
|
||||
/// <summary>Display name for the sensor (e.g. "Pressure").</summary>
|
||||
public string SensorName { get; set; } = "Sensor";
|
||||
|
||||
/// <summary>Minimum input voltage at the sensor connector (V).</summary>
|
||||
public double MinVolt { get; set; } = 0;
|
||||
|
||||
/// <summary>Maximum input voltage at the sensor connector (V).</summary>
|
||||
public double MaxVolt { get; set; } = 5;
|
||||
|
||||
/// <summary>Engineering-unit value corresponding to <see cref="MinVolt"/>.</summary>
|
||||
public double MinVal { get; set; } = 0;
|
||||
|
||||
/// <summary>Engineering-unit value corresponding to <see cref="MaxVolt"/>.</summary>
|
||||
public double MaxVal { get; set; } = 15;
|
||||
|
||||
/// <summary>Multiplicative gain correction applied after the range mapping.</summary>
|
||||
public double Gain { get; set; } = 1;
|
||||
|
||||
/// <summary>Additive offset correction applied after the gain.</summary>
|
||||
public double Offset { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Converts a raw CAN bus ADC count to a calibrated engineering-unit value.
|
||||
/// </summary>
|
||||
/// <param name="rawCanBusValue">10-bit ADC count from the CAN frame.</param>
|
||||
/// <returns>Calibrated value in engineering units.</returns>
|
||||
public double GetValueFromRaw(double rawCanBusValue)
|
||||
{
|
||||
// Convert ADC counts → volts (10-bit ADC, 5 V reference)
|
||||
double volts = rawCanBusValue * 5000.0 / 1024.0 / 1000.0;
|
||||
|
||||
// Map voltage range → engineering-unit range
|
||||
double value = volts * ((MaxVal - MinVal) / (MaxVolt - MinVolt));
|
||||
|
||||
return value * Gain + Offset;
|
||||
}
|
||||
|
||||
/// <summary>Serialises this sensor configuration to XML.</summary>
|
||||
public XElement ToXml()
|
||||
=> new XElement("sensor",
|
||||
new XAttribute("num", Number),
|
||||
new XAttribute("name", SensorName),
|
||||
new XElement("Gain", Gain),
|
||||
new XElement("Offset", Offset),
|
||||
new XElement("MinVolt", MinVolt),
|
||||
new XElement("MaxVolt", MaxVolt),
|
||||
new XElement("MinVal", MinVal),
|
||||
new XElement("MaxVal", MaxVal));
|
||||
|
||||
/// <summary>Deserialises a sensor configuration from XML.</summary>
|
||||
public static SensorConfiguration FromXml(XElement element)
|
||||
{
|
||||
var sc = new SensorConfiguration();
|
||||
TryParse(element.Attribute("num")?.Value, v => sc.Number = short.Parse(v));
|
||||
TryParse(element.Attribute("name")?.Value, v => sc.SensorName = v);
|
||||
TryParse(element.Element("Gain")?.Value, v => sc.Gain = double.Parse(v));
|
||||
TryParse(element.Element("Offset")?.Value, v => sc.Offset = double.Parse(v));
|
||||
TryParse(element.Element("MinVolt")?.Value, v => sc.MinVolt = double.Parse(v));
|
||||
TryParse(element.Element("MaxVolt")?.Value, v => sc.MaxVolt = double.Parse(v));
|
||||
TryParse(element.Element("MinVal")?.Value, v => sc.MinVal = double.Parse(v));
|
||||
TryParse(element.Element("MaxVal")?.Value, v => sc.MaxVal = double.Parse(v));
|
||||
return sc;
|
||||
}
|
||||
|
||||
/// <summary>Creates the default pressure sensor calibration for channel 1.</summary>
|
||||
public static SensorConfiguration DefaultPressureSensor()
|
||||
=> new SensorConfiguration
|
||||
{
|
||||
Number = 1,
|
||||
SensorName = "Pressure",
|
||||
Offset = -4.1,
|
||||
MinVolt = 0.5,
|
||||
MaxVolt = 3.2,
|
||||
MinVal = 0,
|
||||
MaxVal = 20
|
||||
};
|
||||
|
||||
private static void TryParse(string? value, Action<string> assign)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value))
|
||||
try { assign(value!); } catch { /* Ignore malformed XML values */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
425
Models/TestDefinition.cs
Normal file
425
Models/TestDefinition.cs
Normal file
@@ -0,0 +1,425 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace HC_APTBS.Models
|
||||
{
|
||||
// ── Test type constants ──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Well-known test type identifiers stored in <see cref="TestDefinition.Name"/>.</summary>
|
||||
public static class TestType
|
||||
{
|
||||
public const string Dfi = "DFI";
|
||||
public const string F = "F";
|
||||
public const string Wl = "WL";
|
||||
public const string Up = "UP";
|
||||
public const string Pfp = "PFP";
|
||||
public const string Svme = "SVME";
|
||||
public const string Other = "OTHER";
|
||||
}
|
||||
|
||||
// ── TestDefinition ────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Defines a complete test procedure. A test consists of one or more <see cref="PhaseDefinition"/>
|
||||
/// objects that are executed in order. The test is considered successful only if all phases pass.
|
||||
/// </summary>
|
||||
public class TestDefinition
|
||||
{
|
||||
// ── XML element name constants ─────────────────────────────────────────
|
||||
|
||||
internal const string XmlTest = "Test";
|
||||
internal const string XmlPhase = "Fase";
|
||||
internal const string XmlSends = "Sends";
|
||||
internal const string XmlReady = "Ready";
|
||||
internal const string XmlReceives = "Receives";
|
||||
internal const string XmlResult = "Result";
|
||||
internal const string XmlMeasure = "Measure";
|
||||
|
||||
public const string TimestampFormat = "yyyy/MM/dd/ HH:mm:ss";
|
||||
|
||||
// ── Properties ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Test type identifier (see <see cref="TestType"/>).</summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Conditioning time in seconds: bench must hold setpoint for this long before measurement.</summary>
|
||||
public int ConditioningTimeSec { get; set; }
|
||||
|
||||
/// <summary>Measurement window duration in seconds.</summary>
|
||||
public int MeasurementTimeSec { get; set; }
|
||||
|
||||
/// <summary>Number of measurements taken per second during the measurement window.</summary>
|
||||
public double MeasurementsPerSecond { get; set; }
|
||||
|
||||
/// <summary>Execution order index within the pump's test list.</summary>
|
||||
public int Order { get; set; }
|
||||
|
||||
/// <summary>Phase definitions, executed sequentially.</summary>
|
||||
public List<PhaseDefinition> Phases { get; set; } = new();
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Returns true if at least one phase in this test is enabled.</summary>
|
||||
public bool HasActivePhase() => Phases.Any(p => p.Enabled);
|
||||
|
||||
/// <summary>Indicates whether this test collects result measurements (i.e. should appear in the report).</summary>
|
||||
public bool HasResults() => Phases.Any(p => p.Receives?.Count > 0);
|
||||
|
||||
/// <summary>Returns the total estimated wall-clock time for this test in seconds.</summary>
|
||||
public int EstimatedTotalSeconds()
|
||||
{
|
||||
int total = 0;
|
||||
foreach (var p in Phases)
|
||||
{
|
||||
if (p.Enabled)
|
||||
total += ConditioningTimeSec + MeasurementTimeSec + 5; // 5 s bench ramp-up estimate
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// ── XML serialisation ─────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Serialises this test definition to XML.</summary>
|
||||
public XElement ToXml()
|
||||
{
|
||||
var node = new XElement(XmlTest,
|
||||
new XAttribute("name", Name),
|
||||
new XAttribute("Tacond", ConditioningTimeSec),
|
||||
new XAttribute("Tmeasur", MeasurementTimeSec),
|
||||
new XAttribute("MeasurePerSecond", MeasurementsPerSecond),
|
||||
new XAttribute("orden", Order));
|
||||
|
||||
foreach (var phase in Phases)
|
||||
node.Add(phase.ToXml());
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/// <summary>Deserialises a test definition from XML.</summary>
|
||||
public static TestDefinition FromXml(XElement element)
|
||||
{
|
||||
var t = new TestDefinition
|
||||
{
|
||||
Name = element.Attribute("name")!.Value,
|
||||
ConditioningTimeSec = int.Parse(element.Attribute("Tacond")!.Value),
|
||||
MeasurementTimeSec = int.Parse(element.Attribute("Tmeasur")!.Value),
|
||||
MeasurementsPerSecond = double.Parse(
|
||||
element.Attribute("MeasurePerSecond")!.Value, CultureInfo.InvariantCulture),
|
||||
Order = int.Parse(element.Attribute("orden")!.Value)
|
||||
};
|
||||
|
||||
foreach (var phaseEl in element.Elements(XmlPhase))
|
||||
t.Phases.Add(PhaseDefinition.FromXml(phaseEl));
|
||||
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
// ── PhaseDefinition ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single phase within a <see cref="TestDefinition"/>.
|
||||
/// A phase specifies an RPM setpoint, temperature setpoint, parameter sends,
|
||||
/// and expected measurement results.
|
||||
/// </summary>
|
||||
public class PhaseDefinition
|
||||
{
|
||||
/// <summary>Display name (e.g. "1000 RPM 40°C").</summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>When true, a test failure in this phase immediately halts the entire test sequence.</summary>
|
||||
public bool IsCritical { get; set; }
|
||||
|
||||
/// <summary>Execution order within the parent test.</summary>
|
||||
public int Order { get; set; }
|
||||
|
||||
/// <summary>Whether this phase is included in the test run.</summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
/// <summary>Whether this phase passed all result criteria.</summary>
|
||||
public bool Success { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Bench readiness conditions (temperature setpoint).
|
||||
/// Typically a single entry giving the required oil temperature.
|
||||
/// </summary>
|
||||
public List<TestParameter> Readies { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Parameters to write to the pump before measurement
|
||||
/// (RPM setpoint, DFI angle, etc.).
|
||||
/// </summary>
|
||||
public List<TestParameter> Sends { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Parameters to read and compare against target values with tolerances.
|
||||
/// Results are stored in <see cref="TestParameter.Result"/>.
|
||||
/// </summary>
|
||||
public List<TestParameter> Receives { get; set; } = new();
|
||||
|
||||
/// <summary>Bit positions from the Alarms CAN parameter that fired during this phase.</summary>
|
||||
public List<int> ErrorBits { get; set; } = new();
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Returns the RPM setpoint TestParameter from the Sends list.</summary>
|
||||
public TestParameter? GetRpmSetpoint()
|
||||
=> Sends.FirstOrDefault(tp => tp.Name == BenchParameterNames.Rpm);
|
||||
|
||||
/// <summary>Records an alarm bit error, notifying listeners.</summary>
|
||||
public void RecordErrorBit(int bit)
|
||||
{
|
||||
if (!ErrorBits.Contains(bit))
|
||||
ErrorBits.Add(bit);
|
||||
}
|
||||
|
||||
/// <summary>Clears all accumulated result data for a fresh run.</summary>
|
||||
public void ClearResults()
|
||||
{
|
||||
foreach (var tp in Receives)
|
||||
tp.Result = null;
|
||||
}
|
||||
|
||||
internal bool ReceiveResults() => Receives?.Count > 0;
|
||||
|
||||
// ── XML ───────────────────────────────────────────────────────────────
|
||||
|
||||
internal XElement ToXml()
|
||||
{
|
||||
var node = new XElement(TestDefinition.XmlPhase,
|
||||
new XAttribute("name", Name),
|
||||
new XAttribute("critica", IsCritical),
|
||||
new XAttribute("orden", Order));
|
||||
|
||||
if (Readies.Count > 0)
|
||||
{
|
||||
var readyNode = new XElement(TestDefinition.XmlReady);
|
||||
foreach (var tp in Readies) readyNode.Add(tp.ToXml());
|
||||
node.Add(readyNode);
|
||||
}
|
||||
|
||||
if (Sends.Count > 0)
|
||||
{
|
||||
var sendsNode = new XElement(TestDefinition.XmlSends);
|
||||
foreach (var tp in Sends) sendsNode.Add(tp.ToXml());
|
||||
node.Add(sendsNode);
|
||||
}
|
||||
|
||||
if (Receives.Count > 0)
|
||||
{
|
||||
var recvNode = new XElement(TestDefinition.XmlReceives);
|
||||
foreach (var tp in Receives) if (tp != null) recvNode.Add(tp.ToXml());
|
||||
node.Add(recvNode);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
internal static PhaseDefinition FromXml(XElement element)
|
||||
{
|
||||
var p = new PhaseDefinition
|
||||
{
|
||||
Name = element.Attribute("name")!.Value,
|
||||
IsCritical = bool.Parse(element.Attribute("critica")!.Value),
|
||||
Order = int.Parse(element.Attribute("orden")!.Value)
|
||||
};
|
||||
|
||||
foreach (var group in element.Elements())
|
||||
{
|
||||
if (group.Name.LocalName == TestDefinition.XmlReady)
|
||||
foreach (var xtp in group.Elements()) p.Readies.Add(TestParameter.FromXml(xtp));
|
||||
else if (group.Name.LocalName == TestDefinition.XmlSends)
|
||||
foreach (var xtp in group.Elements()) p.Sends.Add(TestParameter.FromXml(xtp));
|
||||
else if (group.Name.LocalName == TestDefinition.XmlReceives)
|
||||
foreach (var xtp in group.Elements()) p.Receives.Add(TestParameter.FromXml(xtp));
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
// ── TestParameter ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// A single measurement point within a <see cref="PhaseDefinition"/>: a named CAN
|
||||
/// parameter with an expected value and tolerance window.
|
||||
/// </summary>
|
||||
public class TestParameter
|
||||
{
|
||||
/// <summary>CAN parameter name (see <see cref="BenchParameterNames"/>).</summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Target setpoint or expected measurement value.</summary>
|
||||
public double Value { get; set; }
|
||||
|
||||
/// <summary>Acceptable deviation from <see cref="Value"/> for a pass result.</summary>
|
||||
public double Tolerance { get; set; }
|
||||
|
||||
/// <summary>Accumulated measurement result, populated during phase execution.</summary>
|
||||
public TestResult? Result { get; set; }
|
||||
|
||||
public TestParameter() { }
|
||||
|
||||
/// <param name="name">Parameter name.</param>
|
||||
/// <param name="value">Target value.</param>
|
||||
/// <param name="tolerance">Acceptable deviation.</param>
|
||||
public TestParameter(string name, double value, double tolerance)
|
||||
{
|
||||
Name = name;
|
||||
Value = value;
|
||||
Tolerance = tolerance;
|
||||
}
|
||||
|
||||
/// <summary>Returns a shallow clone without Result or listener.</summary>
|
||||
public TestParameter Clone() => new(Name, Value, Tolerance);
|
||||
|
||||
// ── XML ───────────────────────────────────────────────────────────────
|
||||
|
||||
internal XElement ToXml()
|
||||
{
|
||||
var node = new XElement(Name,
|
||||
new XAttribute("value", Value),
|
||||
new XAttribute("tolerance", Tolerance));
|
||||
|
||||
if (Result != null)
|
||||
node.Add(Result.ToXml());
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
internal static TestParameter FromXml(XElement element)
|
||||
{
|
||||
var tp = new TestParameter(
|
||||
element.Name.LocalName,
|
||||
double.Parse(element.Attribute("value")!.Value, CultureInfo.InvariantCulture),
|
||||
double.Parse(element.Attribute("tolerance")!.Value, CultureInfo.InvariantCulture));
|
||||
|
||||
var resultEl = element.Element(TestDefinition.XmlResult);
|
||||
if (resultEl != null)
|
||||
tp.Result = TestResult.FromXml(resultEl);
|
||||
|
||||
return tp;
|
||||
}
|
||||
}
|
||||
|
||||
// ── TestResult ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Accumulated measurement result for a single <see cref="TestParameter"/> over
|
||||
/// one phase execution. Stores all individual samples and computes the average.
|
||||
/// </summary>
|
||||
public class TestResult
|
||||
{
|
||||
/// <summary>True if the average fell within the tolerance window.</summary>
|
||||
public bool Passed { get; set; }
|
||||
|
||||
/// <summary>Average of all collected samples.</summary>
|
||||
public double Average { get; set; }
|
||||
|
||||
/// <summary>Test type used to select the tolerance extension rules.</summary>
|
||||
public string TestType { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>All individual measurement samples collected during the phase.</summary>
|
||||
public List<MeasurementSample> Samples { get; set; } = new();
|
||||
|
||||
/// <summary>Appends a new sample to the collection.</summary>
|
||||
public void AddSample(MeasurementSample sample) => Samples.Add(sample);
|
||||
|
||||
/// <summary>Clears all accumulated samples.</summary>
|
||||
public void Clear() => Samples.Clear();
|
||||
|
||||
/// <summary>
|
||||
/// Computes the average of all samples and evaluates pass/fail against the
|
||||
/// tolerance window. UP and PFP test types apply a configurable extension factor
|
||||
/// supplied by <paramref name="upExtension"/> / <paramref name="pfpExtension"/>.
|
||||
/// </summary>
|
||||
/// <param name="expected">Target value.</param>
|
||||
/// <param name="tolerance">Base tolerance.</param>
|
||||
/// <param name="upExtension">Extension factor for UP tests (0 = no extension).</param>
|
||||
/// <param name="pfpExtension">Extension factor for PFP tests (0 = no extension).</param>
|
||||
public void Evaluate(
|
||||
double expected,
|
||||
double tolerance,
|
||||
double upExtension = 0,
|
||||
double pfpExtension = 0)
|
||||
{
|
||||
if (Samples.Count == 0) { Passed = false; return; }
|
||||
|
||||
double sum = 0;
|
||||
foreach (var s in Samples) sum += s.Value;
|
||||
Average = Math.Round(sum / Samples.Count, 2);
|
||||
|
||||
if (TestType == global::HC_APTBS.Models.TestType.Up)
|
||||
{
|
||||
double hi = (expected + tolerance) * (1 + upExtension);
|
||||
double lo = (expected - tolerance) * (1 - upExtension);
|
||||
Passed = Average >= lo && Average <= hi;
|
||||
}
|
||||
else if (TestType == global::HC_APTBS.Models.TestType.Pfp)
|
||||
{
|
||||
double hi = (expected + tolerance) * (1 + pfpExtension);
|
||||
double lo = (expected - tolerance) * (1 - pfpExtension);
|
||||
Passed = Average >= lo && Average <= hi;
|
||||
}
|
||||
else
|
||||
{
|
||||
Passed = Math.Abs(Average - expected) <= tolerance;
|
||||
}
|
||||
}
|
||||
|
||||
// ── XML ───────────────────────────────────────────────────────────────
|
||||
|
||||
internal XElement ToXml()
|
||||
{
|
||||
var node = new XElement(TestDefinition.XmlResult,
|
||||
new XAttribute("average", Average),
|
||||
new XAttribute("result", Passed),
|
||||
new XAttribute("testtype", TestType));
|
||||
|
||||
foreach (var s in Samples)
|
||||
node.Add(s.ToXml());
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
internal static TestResult FromXml(XElement element)
|
||||
{
|
||||
var tr = new TestResult
|
||||
{
|
||||
Passed = bool.Parse(element.Attribute("result")!.Value),
|
||||
Average = double.Parse(element.Attribute("average")!.Value, CultureInfo.InvariantCulture),
|
||||
TestType = element.Attribute("testtype")?.Value ?? string.Empty
|
||||
};
|
||||
foreach (var measureEl in element.Elements())
|
||||
tr.Samples.Add(MeasurementSample.FromXml(measureEl));
|
||||
return tr;
|
||||
}
|
||||
}
|
||||
|
||||
// ── MeasurementSample ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>A single time-stamped measurement value collected during a phase.</summary>
|
||||
public class MeasurementSample
|
||||
{
|
||||
/// <summary>Engineering-unit measurement value.</summary>
|
||||
public double Value { get; set; }
|
||||
|
||||
/// <summary>Timestamp when the sample was acquired (format <see cref="TestDefinition.TimestampFormat"/>).</summary>
|
||||
public string Timestamp { get; set; } = string.Empty;
|
||||
|
||||
internal XElement ToXml()
|
||||
=> new XElement(TestDefinition.XmlMeasure,
|
||||
new XAttribute("value", Value),
|
||||
new XAttribute("time", Timestamp));
|
||||
|
||||
internal static MeasurementSample FromXml(XElement element)
|
||||
=> new MeasurementSample
|
||||
{
|
||||
Value = double.Parse(element.Attribute("value")!.Value, CultureInfo.InvariantCulture),
|
||||
Timestamp = element.Attribute("time")?.Value ?? string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user