using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml.Linq; namespace HC_APTBS.Models { // ── Test type constants ────────────────────────────────────────────────────── /// Well-known test type identifiers stored in . 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 ──────────────────────────────────────────────────────────── /// /// Defines a complete test procedure. A test consists of one or more /// objects that are executed in order. The test is considered successful only if all phases pass. /// 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 ──────────────────────────────────────────────────────── /// Test type identifier (see ). public string Name { get; set; } = string.Empty; /// Conditioning time in seconds: bench must hold setpoint for this long before measurement. public int ConditioningTimeSec { get; set; } /// Measurement window duration in seconds. public int MeasurementTimeSec { get; set; } /// Number of measurements taken per second during the measurement window. public double MeasurementsPerSecond { get; set; } /// Execution order index within the pump's test list. public int Order { get; set; } /// /// When true, the Tests page preconditions step gates Start on operator authentication /// () before this test may run. /// Backwards-compatible: absent attribute in older pumps.xml files defaults to false. /// public bool RequiresAuth { get; set; } = false; /// Phase definitions, executed sequentially. public List Phases { get; set; } = new(); // ── Helpers ─────────────────────────────────────────────────────────── /// Returns true if at least one phase in this test is enabled. public bool HasActivePhase() => Phases.Any(p => p.Enabled); /// Indicates whether this test collects result measurements (i.e. should appear in the report). public bool HasResults() => Phases.Any(p => p.Receives?.Count > 0); /// Returns the total estimated wall-clock time for this test in seconds. 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 ───────────────────────────────────────────────── /// Serialises this test definition to XML. 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), new XAttribute("requiresAuth", RequiresAuth)); foreach (var phase in Phases) node.Add(phase.ToXml()); return node; } /// Deserialises a test definition from XML. 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), RequiresAuth = bool.TryParse(element.Attribute("requiresAuth")?.Value, out var req) && req }; foreach (var phaseEl in element.Elements(XmlPhase)) t.Phases.Add(PhaseDefinition.FromXml(phaseEl)); return t; } } // ── PhaseDefinition ─────────────────────────────────────────────────────────── /// /// Represents a single phase within a . /// A phase specifies an RPM setpoint, temperature setpoint, parameter sends, /// and expected measurement results. /// public class PhaseDefinition { /// Display name (e.g. "1000 RPM 40°C"). public string Name { get; set; } = string.Empty; /// When true, a test failure in this phase immediately halts the entire test sequence. public bool IsCritical { get; set; } /// Execution order within the parent test. public int Order { get; set; } /// Whether this phase is included in the test run. public bool Enabled { get; set; } = true; /// Whether this phase passed all result criteria. public bool Success { get; set; } = true; /// /// Bench readiness conditions (temperature setpoint). /// Typically a single entry giving the required oil temperature. /// public List Readies { get; set; } = new(); /// /// Parameters to write to the pump before measurement /// (RPM setpoint, DFI angle, etc.). /// public List Sends { get; set; } = new(); /// /// Parameters to read and compare against target values with tolerances. /// Results are stored in . /// public List Receives { get; set; } = new(); /// Bit positions from the Alarms CAN parameter that fired during this phase. public List ErrorBits { get; set; } = new(); // ── Helpers ─────────────────────────────────────────────────────────── /// Returns the RPM setpoint TestParameter from the Sends list. public TestParameter? GetRpmSetpoint() => Sends.FirstOrDefault(tp => tp.Name == BenchParameterNames.Rpm); /// Records an alarm bit error, notifying listeners. public void RecordErrorBit(int bit) { if (!ErrorBits.Contains(bit)) ErrorBits.Add(bit); } /// Clears all accumulated result data for a fresh run. 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 ───────────────────────────────────────────────────────────── /// /// A single measurement point within a : a named CAN /// parameter with an expected value and tolerance window. /// public class TestParameter { /// CAN parameter name (see ). public string Name { get; set; } = string.Empty; /// Target setpoint or expected measurement value. public double Value { get; set; } /// Acceptable deviation from for a pass result. public double Tolerance { get; set; } /// Accumulated measurement result, populated during phase execution. public TestResult? Result { get; set; } public TestParameter() { } /// Parameter name. /// Target value. /// Acceptable deviation. public TestParameter(string name, double value, double tolerance) { Name = name; Value = value; Tolerance = tolerance; } /// Returns a shallow clone without Result or listener. 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 ──────────────────────────────────────────────────────────────── /// /// Accumulated measurement result for a single over /// one phase execution. Stores all individual samples and computes the average. /// public class TestResult { /// True if the average fell within the tolerance window. public bool Passed { get; set; } /// Average of all collected samples. public double Average { get; set; } /// Test type used to select the tolerance extension rules. public string TestType { get; set; } = string.Empty; /// All individual measurement samples collected during the phase. public List Samples { get; set; } = new(); /// Appends a new sample to the collection. public void AddSample(MeasurementSample sample) => Samples.Add(sample); /// Clears all accumulated samples. public void Clear() => Samples.Clear(); /// /// 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 / . /// /// Target value. /// Base tolerance. /// Extension factor for UP tests (0 = no extension). /// Extension factor for PFP tests (0 = no extension). 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 ───────────────────────────────────────────────────────── /// A single time-stamped measurement value collected during a phase. public class MeasurementSample { /// Engineering-unit measurement value. public double Value { get; set; } /// Timestamp when the sample was acquired (format ). 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 }; } }