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