initial commit

This commit is contained in:
2026-04-11 12:45:18 +02:00
commit 6e1b929e2f
1246 changed files with 177580 additions and 0 deletions

43
Models/Alarm.cs Normal file
View 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")
};
}
}

View 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
View 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 (P1P6) ─────────────────────────────────────
/// <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>&lt;Params&gt;</c> section of a pump definition.
/// </summary>
public static CanBusParameter FromXml(XElement xe)
{
var p = new CanBusParameter
{
Name = xe.Name.LocalName,
MessageId = uint.Parse(xe.Attribute("busid")?.Value ?? "0",
NumberStyles.HexNumber),
ByteH = ushort.Parse(xe.Attribute("byteh")?.Value ?? "0"),
ByteL = ushort.Parse(xe.Attribute("bytel")?.Value ?? "0"),
Type = int.Parse(xe.Attribute("type")?.Value ?? "0"),
IsReceive = !string.Equals(xe.Attribute("send")?.Value, "true",
StringComparison.OrdinalIgnoreCase),
Alpha = ParseDecimal(xe.Attribute("filter")?.Value, 1.0),
DisableCalibration = string.Equals(xe.Attribute("disableparams")?.Value,
"true", StringComparison.OrdinalIgnoreCase)
};
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
View 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();
}
}
}

View 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
View 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;
}
}

View 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;
}
}
}

View 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 05 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
View 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
};
}
}