Config system fixes: - Implement SavePump() — full XML serialization with insert/update by pump ID - Add CanBusParameter.ToPumpXml() for legacy P1-P6 pump param format - Fix LastRotationDirection never loaded in LoadSettings() - Add SaveAlarms() to ConfigurationService and IConfigurationService - Remove dead fields AppSettings.Clients and AppSettings.PumpIds PDF report redesign: - Professional layout with charts, verdict badges, and tolerance bands - Add ReportChartRenderer (SVG) and ReportTheme styling constants - Embed default_logo.png as fallback report logo Documentation: - Add gap analysis docs (config validation, ford unlock, missing features) - Update CLAUDE.md architecture, known gaps, and debt tracking Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
835 lines
36 KiB
C#
835 lines
36 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Globalization;
|
||
using System.IO;
|
||
using System.Xml.Linq;
|
||
using HC_APTBS.Models;
|
||
using Peak.Can.Basic;
|
||
|
||
namespace HC_APTBS.Services.Impl
|
||
{
|
||
/// <summary>
|
||
/// XML-backed implementation of <see cref="IConfigurationService"/>.
|
||
/// All configuration files live under <c>%UserProfile%\.HC_APTBS\config\</c>.
|
||
/// </summary>
|
||
public sealed class ConfigurationService : IConfigurationService
|
||
{
|
||
// ── Paths ─────────────────────────────────────────────────────────────────
|
||
|
||
private static readonly string ConfigFolder =
|
||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||
".HC_APTBS", "config");
|
||
|
||
private string ConfigXml => Path.Combine(ConfigFolder, "config.xml");
|
||
private string PumpsXml => Path.Combine(ConfigFolder, "pumps.xml");
|
||
private string BenchXml => Path.Combine(ConfigFolder, "bench.xml");
|
||
private string SensorsXml => Path.Combine(ConfigFolder, "sensors.xml");
|
||
private string ClientsXml => Path.Combine(ConfigFolder, "clients.xml");
|
||
private string AlarmsXml => Path.Combine(ConfigFolder, "alarms.xml");
|
||
private string StatusXml => Path.Combine(ConfigFolder, "status.xml");
|
||
|
||
private readonly IAppLogger _log;
|
||
private const string LogId = "ConfigurationService";
|
||
|
||
// ── Cached instances ──────────────────────────────────────────────────────
|
||
|
||
private AppSettings? _settings;
|
||
private BenchConfiguration? _bench;
|
||
private SortedDictionary<string, string>? _clients;
|
||
private readonly Dictionary<int, PumpStatusDefinition> _statusCache = new();
|
||
|
||
// ── Constructor ───────────────────────────────────────────────────────────
|
||
|
||
/// <param name="logger">Application logger.</param>
|
||
public ConfigurationService(IAppLogger logger)
|
||
{
|
||
_log = logger;
|
||
Directory.CreateDirectory(ConfigFolder);
|
||
}
|
||
|
||
// ── IConfigurationService: Settings ───────────────────────────────────────
|
||
|
||
/// <inheritdoc/>
|
||
public AppSettings Settings
|
||
{
|
||
get
|
||
{
|
||
if (_settings == null) LoadSettings();
|
||
return _settings!;
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public void SaveSettings()
|
||
{
|
||
try
|
||
{
|
||
var root = new XElement("Config",
|
||
new XElement("TMax", Settings.TempMax),
|
||
new XElement("TMin", Settings.TempMin),
|
||
new XElement("RefreshBenchInterface", Settings.RefreshBenchInterfaceMs),
|
||
new XElement("RefreshWhileReading", Settings.RefreshWhileReadingMs),
|
||
new XElement("RefreshCanBusRead", Settings.RefreshCanBusReadMs),
|
||
new XElement("RefreshPumpRequest", Settings.RefreshPumpRequestMs),
|
||
new XElement("RefreshPumpParams", Settings.RefreshPumpParamsMs),
|
||
new XElement("BlinkInterval", Settings.BlinkIntervalMs),
|
||
new XElement("FlasherInterval", Settings.FlasherIntervalMs),
|
||
new XElement("PidP", Settings.PidP),
|
||
new XElement("PidI", Settings.PidI),
|
||
new XElement("PidD", Settings.PidD),
|
||
new XElement("PidMs", Settings.PidLoopMs),
|
||
new XElement("SecurityRpm", Settings.SecurityRpmLimit),
|
||
new XElement("MaxPressure", Settings.MaxPressureBar),
|
||
new XElement("ToleranceUp", Settings.ToleranceUpExtension),
|
||
new XElement("TolerancePfp", Settings.TolerancePfpExtension),
|
||
new XElement("EncoderResolution", Settings.EncoderResolution),
|
||
new XElement("VoltageForMaxRpm", Settings.VoltageForMaxRpm),
|
||
new XElement("MaxRpm", Settings.MaxRpm),
|
||
new XElement("RightRelayValue", Settings.RightRelayValue),
|
||
new XElement("DefaultIgnoreTin", Settings.DefaultIgnoreTin),
|
||
new XElement("LastRotationDir", Settings.LastRotationDirection),
|
||
new XElement("DaysKeepLogs", Settings.DaysKeepLogs),
|
||
new XElement("CompanyName", Settings.CompanyName),
|
||
new XElement("CompanyInfo", Settings.CompanyInfo),
|
||
new XElement("ReportLogoPath", Settings.ReportLogoPath),
|
||
new XElement("KLinePort", Settings.KLinePort),
|
||
new XElement("Language", Settings.Language),
|
||
new XElement("Relations", RpmVoltageRelation.Serialise(Settings.Relations)),
|
||
new XElement("Users", Settings.Users)
|
||
);
|
||
new XDocument(root).Save(ConfigXml);
|
||
SaveSensors();
|
||
SaveClients();
|
||
_log.Info(LogId, "Settings saved.");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_log.Error(LogId, $"SaveSettings failed: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
// ── IConfigurationService: Bench ──────────────────────────────────────────
|
||
|
||
/// <inheritdoc/>
|
||
public BenchConfiguration Bench
|
||
{
|
||
get
|
||
{
|
||
if (_bench == null) LoadBench();
|
||
return _bench!;
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public void SaveBench()
|
||
{
|
||
try
|
||
{
|
||
var root = new XElement("Bench");
|
||
foreach (var param in Bench.ParametersByName.Values)
|
||
root.Add(param.ToXml());
|
||
|
||
var relesNode = new XElement("Reles");
|
||
foreach (var relay in Bench.Relays.Values)
|
||
relesNode.Add(relay.ToXml());
|
||
root.Add(relesNode);
|
||
|
||
new XDocument(root).Save(BenchXml);
|
||
_log.Info(LogId, "Bench configuration saved.");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_log.Error(LogId, $"SaveBench failed: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
// ── IConfigurationService: Pumps ──────────────────────────────────────────
|
||
|
||
/// <inheritdoc/>
|
||
public IReadOnlyList<string> GetPumpIds()
|
||
{
|
||
var ids = new List<string>();
|
||
string source = File.Exists(PumpsXml) ? PumpsXml
|
||
: File.Exists(ConfigXml) ? ConfigXml
|
||
: null!;
|
||
if (source == null) return ids;
|
||
try
|
||
{
|
||
var xdoc = XDocument.Load(source);
|
||
foreach (var xid in xdoc.Descendants("PumpID"))
|
||
ids.Add(xid.Value);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_log.Error(LogId, $"GetPumpIds failed: {ex.Message}");
|
||
}
|
||
return ids;
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public PumpDefinition? LoadPump(string pumpId)
|
||
{
|
||
string source = File.Exists(PumpsXml) ? PumpsXml
|
||
: File.Exists(ConfigXml) ? ConfigXml
|
||
: null!;
|
||
if (source == null) return null;
|
||
try
|
||
{
|
||
var xdoc = XDocument.Load(source);
|
||
var xpump = xdoc.XPathSelectElement($"/Config/Pumps/Pump[@id='{pumpId}']");
|
||
if (xpump == null) return null;
|
||
return ParsePumpElement(xpump);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_log.Error(LogId, $"LoadPump({pumpId}) failed: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public void SavePump(PumpDefinition pump)
|
||
{
|
||
try
|
||
{
|
||
XDocument xdoc;
|
||
if (File.Exists(PumpsXml))
|
||
{
|
||
xdoc = XDocument.Load(PumpsXml);
|
||
}
|
||
else
|
||
{
|
||
xdoc = new XDocument(new XElement("Config", new XElement("Pumps")));
|
||
}
|
||
|
||
var root = xdoc.Root!;
|
||
var pumpsNode = root.Element("Pumps");
|
||
if (pumpsNode == null)
|
||
{
|
||
pumpsNode = new XElement("Pumps");
|
||
root.Add(pumpsNode);
|
||
}
|
||
|
||
// Build the <Pump> element mirroring ParsePumpElement's expected format.
|
||
var xpump = new XElement("Pump",
|
||
new XAttribute("id", pump.Id),
|
||
new XAttribute("model", pump.Model),
|
||
new XAttribute("text", pump.EcuText),
|
||
new XAttribute("chaveta", pump.Chaveta),
|
||
new XAttribute("rotation", pump.Rotation),
|
||
new XAttribute("info", pump.Info),
|
||
new XAttribute("preinjection", pump.HasPreInjection.ToString().ToLowerInvariant()),
|
||
new XAttribute("cilinders4", pump.Is4Cylinder.ToString().ToLowerInvariant()),
|
||
new XAttribute("unlock", pump.UnlockType),
|
||
new XAttribute("baudrate", pump.CanBaudrate == Peak.Can.Basic.TPCANBaudrate.PCAN_BAUD_250K ? "250" : "500"),
|
||
new XAttribute("lockangle", pump.LockAngle.ToString(CultureInfo.InvariantCulture)));
|
||
|
||
// PumpID child element — GetPumpIds() finds these via Descendants("PumpID").
|
||
xpump.Add(new XElement("PumpID", pump.Id));
|
||
|
||
// ── Serialise <Params> (pump CAN params use legacy P1–P6 format) ──
|
||
if (pump.ParametersByName.Count > 0)
|
||
{
|
||
var xparams = new XElement("Params");
|
||
foreach (var param in pump.ParametersByName.Values)
|
||
xparams.Add(param.ToPumpXml());
|
||
xpump.Add(xparams);
|
||
}
|
||
|
||
// ── Serialise <Tests> ──
|
||
if (pump.Tests.Count > 0)
|
||
{
|
||
var xtests = new XElement("Tests");
|
||
foreach (var test in pump.Tests)
|
||
xtests.Add(test.ToXml());
|
||
xpump.Add(xtests);
|
||
}
|
||
|
||
// ── Find existing pump by ID and replace, or append ──
|
||
XElement? existing = null;
|
||
foreach (var child in pumpsNode.Elements("Pump"))
|
||
{
|
||
if (child.Attribute("id")?.Value == pump.Id)
|
||
{
|
||
existing = child;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (existing != null)
|
||
existing.ReplaceWith(xpump);
|
||
else
|
||
pumpsNode.Add(xpump);
|
||
|
||
xdoc.Save(PumpsXml);
|
||
_log.Info(LogId, $"SavePump({pump.Id}) — saved to pumps.xml.");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_log.Error(LogId, $"SavePump({pump.Id}) failed: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
// ── IConfigurationService: Clients ────────────────────────────────────────
|
||
|
||
/// <inheritdoc/>
|
||
public SortedDictionary<string, string> Clients
|
||
{
|
||
get
|
||
{
|
||
if (_clients == null) LoadClients();
|
||
return _clients!;
|
||
}
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public void SaveClients()
|
||
{
|
||
try
|
||
{
|
||
var root = new XElement("Clients");
|
||
foreach (var kv in Clients)
|
||
root.Add(new XElement("client",
|
||
new XAttribute("name", kv.Key),
|
||
new XAttribute("contact", kv.Value)));
|
||
new XDocument(root).Save(ClientsXml);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_log.Error(LogId, $"SaveClients failed: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
// ── IConfigurationService: Sensors ────────────────────────────────────────
|
||
|
||
/// <inheritdoc/>
|
||
public void SaveSensors()
|
||
{
|
||
try
|
||
{
|
||
var root = new XElement("Sensors");
|
||
foreach (var s in Settings.Sensors.Values)
|
||
root.Add(s.ToXml());
|
||
new XDocument(root).Save(SensorsXml);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_log.Error(LogId, $"SaveSensors failed: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
// ── IConfigurationService: Alarms ─────────────────────────────────────────
|
||
|
||
/// <inheritdoc/>
|
||
public void SaveAlarms()
|
||
{
|
||
try
|
||
{
|
||
var root = new XElement("Alarms");
|
||
foreach (var alarm in Settings.Alarms)
|
||
root.Add(alarm.ToXml());
|
||
new XDocument(root).Save(AlarmsXml);
|
||
_log.Info(LogId, "Alarms saved.");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_log.Error(LogId, $"SaveAlarms failed: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
// ── Pump status definitions ───────────────────────────────────────────────
|
||
|
||
/// <inheritdoc/>
|
||
public PumpStatusDefinition? LoadPumpStatus(int statusId)
|
||
{
|
||
if (_statusCache.TryGetValue(statusId, out var cached))
|
||
return cached;
|
||
|
||
try
|
||
{
|
||
if (!File.Exists(StatusXml))
|
||
{
|
||
_log.Error(LogId, $"LoadPumpStatus: {StatusXml} not found.");
|
||
return null;
|
||
}
|
||
|
||
var xdoc = XDocument.Load(StatusXml);
|
||
if (xdoc.Root == null) return null;
|
||
|
||
// Search for <PumpStatus StatusID="N"> in the document.
|
||
XElement? xStatus = null;
|
||
foreach (var el in xdoc.Root.Descendants("PumpStatus"))
|
||
{
|
||
if (el.Attribute("StatusID")?.Value == statusId.ToString())
|
||
{
|
||
xStatus = el;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (xStatus == null)
|
||
{
|
||
_log.Error(LogId, $"LoadPumpStatus: StatusID={statusId} not found in status.xml.");
|
||
return null;
|
||
}
|
||
|
||
var def = new PumpStatusDefinition
|
||
{
|
||
Id = statusId,
|
||
Name = xStatus.Attribute("name")?.Value ?? "-"
|
||
};
|
||
|
||
foreach (var xState in xStatus.Elements("State"))
|
||
{
|
||
var bit = new StatusBit
|
||
{
|
||
Bit = int.Parse(xState.Attribute("bit")?.Value ?? "0"),
|
||
Enabled = string.Equals(
|
||
xState.Attribute("enabled")?.Value, "true",
|
||
StringComparison.OrdinalIgnoreCase)
|
||
};
|
||
|
||
foreach (var xVal in xState.Elements("StateValue"))
|
||
{
|
||
bit.Values.Add(new StatusBitValue
|
||
{
|
||
State = int.Parse(xVal.Attribute("value")?.Value ?? "0"),
|
||
Color = xVal.Attribute("color")?.Value ?? "26C200",
|
||
Description = xVal.Value.Trim()
|
||
});
|
||
}
|
||
|
||
def.Bits.Add(bit);
|
||
}
|
||
|
||
_statusCache[statusId] = def;
|
||
return def;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_log.Error(LogId, $"LoadPumpStatus({statusId}) failed: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
// ── Private loaders ───────────────────────────────────────────────────────
|
||
|
||
private void LoadSettings()
|
||
{
|
||
_settings = new AppSettings();
|
||
if (!File.Exists(ConfigXml)) return;
|
||
|
||
try
|
||
{
|
||
var xdoc = XDocument.Load(ConfigXml);
|
||
var r = xdoc.Root!;
|
||
TryInt(r, "TMax", v => _settings.TempMax = v);
|
||
TryInt(r, "TMin", v => _settings.TempMin = v);
|
||
TryInt(r, "RefreshBenchInterface", v => _settings.RefreshBenchInterfaceMs = v);
|
||
TryInt(r, "RefreshWhileReading", v => _settings.RefreshWhileReadingMs = v);
|
||
TryInt(r, "RefreshCanBusRead", v => _settings.RefreshCanBusReadMs = v);
|
||
TryInt(r, "RefreshPumpRequest", v => _settings.RefreshPumpRequestMs = v);
|
||
TryInt(r, "RefreshPumpParams", v => _settings.RefreshPumpParamsMs = v);
|
||
TryInt(r, "BlinkInterval", v => _settings.BlinkIntervalMs = v);
|
||
TryInt(r, "FlasherInterval", v => _settings.FlasherIntervalMs = v);
|
||
TryDouble(r, "PidP", v => _settings.PidP = v);
|
||
TryDouble(r, "PidI", v => _settings.PidI = v);
|
||
TryDouble(r, "PidD", v => _settings.PidD = v);
|
||
TryInt(r, "PidMs", v => _settings.PidLoopMs = v);
|
||
TryInt(r, "SecurityRpm", v => _settings.SecurityRpmLimit = v);
|
||
TryInt(r, "MaxPressure", v => _settings.MaxPressureBar = v);
|
||
TryDouble(r, "ToleranceUp", v => _settings.ToleranceUpExtension = v);
|
||
TryDouble(r, "TolerancePfp", v => _settings.TolerancePfpExtension = v);
|
||
TryInt(r, "EncoderResolution", v => _settings.EncoderResolution = v);
|
||
TryDouble(r, "VoltageForMaxRpm", v => _settings.VoltageForMaxRpm = v);
|
||
TryInt(r, "MaxRpm", v => _settings.MaxRpm = v);
|
||
TryBool(r, "RightRelayValue", v => _settings.RightRelayValue = v);
|
||
TryBool(r, "DefaultIgnoreTin", v => _settings.DefaultIgnoreTin = v);
|
||
TryInt(r, "LastRotationDir", v => _settings.LastRotationDirection = (short)v);
|
||
TryInt(r, "DaysKeepLogs", v => _settings.DaysKeepLogs = v);
|
||
TryString(r, "CompanyName", v => _settings.CompanyName = v);
|
||
TryString(r, "CompanyInfo", v => _settings.CompanyInfo = v);
|
||
TryString(r, "ReportLogoPath", v => _settings.ReportLogoPath = v);
|
||
TryString(r, "KLinePort", v => _settings.KLinePort = v);
|
||
TryString(r, "Language", v => _settings.Language = v);
|
||
TryString(r, "Relations", v => _settings.Relations = RpmVoltageRelation.Deserialise(v));
|
||
TryString(r, "Users", v => _settings.Users = v);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_log.Error(LogId, $"LoadSettings failed: {ex.Message}");
|
||
}
|
||
|
||
LoadSensors();
|
||
LoadClients();
|
||
LoadAlarms();
|
||
}
|
||
|
||
private void LoadBench()
|
||
{
|
||
_bench = new BenchConfiguration();
|
||
|
||
string xml = File.Exists(BenchXml)
|
||
? File.ReadAllText(BenchXml)
|
||
: DefaultBenchXml();
|
||
|
||
try
|
||
{
|
||
var xdoc = XDocument.Parse(xml);
|
||
|
||
foreach (var xe in xdoc.Root!.Elements())
|
||
{
|
||
if (xe.Name.LocalName == "Reles")
|
||
{
|
||
foreach (var xr in xe.Elements("Rele"))
|
||
ParseRelayElement(xr);
|
||
continue;
|
||
}
|
||
|
||
var param = ParseParamElement(xe);
|
||
_bench.ParametersByName[param.Name] = param;
|
||
|
||
if (!_bench.ParametersById.TryGetValue(param.MessageId, out var list))
|
||
{
|
||
list = new List<CanBusParameter>();
|
||
_bench.ParametersById[param.MessageId] = list;
|
||
}
|
||
list.Add(param);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_log.Error(LogId, $"LoadBench failed: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private void LoadSensors()
|
||
{
|
||
_settings ??= new AppSettings();
|
||
if (File.Exists(SensorsXml))
|
||
{
|
||
try
|
||
{
|
||
var xdoc = XDocument.Load(SensorsXml);
|
||
foreach (var xs in xdoc.Root!.Elements("sensor"))
|
||
{
|
||
var sc = SensorConfiguration.FromXml(xs);
|
||
_settings.Sensors[sc.Number] = sc;
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_log.Error(LogId, $"LoadSensors failed: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
// Ensure default calibrations exist for the two analogue channels.
|
||
if (!_settings.Sensors.ContainsKey(1))
|
||
_settings.Sensors[1] = SensorConfiguration.DefaultPressureSensor();
|
||
if (!_settings.Sensors.ContainsKey(2))
|
||
_settings.Sensors[2] = new SensorConfiguration { Number = 2, SensorName = "AnalogSensor2" };
|
||
}
|
||
|
||
private void LoadClients()
|
||
{
|
||
_clients = new SortedDictionary<string, string>();
|
||
if (!File.Exists(ClientsXml)) return;
|
||
try
|
||
{
|
||
var xdoc = XDocument.Load(ClientsXml);
|
||
foreach (var xc in xdoc.Root!.Elements("client"))
|
||
_clients[xc.Attribute("name")?.Value ?? ""] = xc.Attribute("contact")?.Value ?? "";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_log.Error(LogId, $"LoadClients failed: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private void LoadAlarms()
|
||
{
|
||
_settings ??= new AppSettings();
|
||
if (!File.Exists(AlarmsXml)) return;
|
||
try
|
||
{
|
||
var xdoc = XDocument.Load(AlarmsXml);
|
||
foreach (var xa in xdoc.Root!.Elements("Alarm"))
|
||
_settings.Alarms.Add(Alarm.FromXml(xa));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_log.Error(LogId, $"LoadAlarms failed: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
// ── Parsing helpers ───────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Parses a bench CAN parameter from an XML element.
|
||
/// Uses the clean factor/offset calibration model with explicit direction flags.
|
||
/// </summary>
|
||
private static CanBusParameter ParseParamElement(XElement xe)
|
||
{
|
||
string direction = xe.Attribute("direction")?.Value ?? "rx";
|
||
|
||
return new CanBusParameter
|
||
{
|
||
Name = xe.Name.LocalName,
|
||
MessageId = Convert.ToUInt32(xe.Attribute("id")?.Value ?? "0", 16),
|
||
ByteH = ushort.Parse(xe.Attribute("byteh")?.Value ?? "0"),
|
||
ByteL = ushort.Parse(xe.Attribute("bytel")?.Value ?? "0"),
|
||
Alpha = CanBusParameter.ParseDecimal(xe.Attribute("filter")?.Value, 1.0),
|
||
IsReceive = string.Equals(direction, "rx", StringComparison.OrdinalIgnoreCase),
|
||
Factor = CanBusParameter.ParseDecimal(xe.Attribute("factor")?.Value, 1.0),
|
||
Offset = CanBusParameter.ParseDecimal(xe.Attribute("offset")?.Value, 0.0),
|
||
IsInverse = string.Equals(xe.Attribute("type")?.Value, "inverse",
|
||
StringComparison.OrdinalIgnoreCase),
|
||
UseLegacyTransform = false,
|
||
};
|
||
}
|
||
|
||
private void ParseRelayElement(XElement xr)
|
||
{
|
||
var relay = new Relay(
|
||
xr.Attribute("name")?.Value ?? "",
|
||
Convert.ToUInt32(xr.Attribute("id")?.Value ?? "0", 16),
|
||
int.Parse(xr.Attribute("bit")?.Value ?? "0"));
|
||
_bench!.Relays[relay.Name] = relay;
|
||
}
|
||
|
||
private static PumpDefinition? ParsePumpElement(XElement xpump)
|
||
{
|
||
var pump = new PumpDefinition
|
||
{
|
||
Id = xpump.Attribute("id")?.Value ?? string.Empty,
|
||
Model = xpump.Attribute("model")?.Value ?? string.Empty,
|
||
EcuText = xpump.Attribute("text")?.Value ?? string.Empty,
|
||
Chaveta = xpump.Attribute("chaveta")?.Value ?? string.Empty,
|
||
Rotation = xpump.Attribute("rotation")?.Value ?? RotationDirection.RightName,
|
||
Info = xpump.Attribute("info")?.Value ?? string.Empty,
|
||
HasPreInjection = string.Equals(xpump.Attribute("preinjection")?.Value,
|
||
"true", StringComparison.OrdinalIgnoreCase),
|
||
Is4Cylinder = string.Equals(xpump.Attribute("cilinders4")?.Value,
|
||
"true", StringComparison.OrdinalIgnoreCase),
|
||
UnlockType = int.Parse(xpump.Attribute("unlock")?.Value ?? "0"),
|
||
CanBaudrate = xpump.Attribute("baudrate")?.Value == "250"
|
||
? TPCANBaudrate.PCAN_BAUD_250K
|
||
: TPCANBaudrate.PCAN_BAUD_500K,
|
||
};
|
||
|
||
// Parse lockangle — may use comma as decimal separator.
|
||
var lockStr = xpump.Attribute("lockangle")?.Value;
|
||
if (!string.IsNullOrEmpty(lockStr) &&
|
||
double.TryParse(lockStr.Replace(',', '.'),
|
||
NumberStyles.Float, CultureInfo.InvariantCulture, out var la))
|
||
{
|
||
pump.LockAngle = la;
|
||
}
|
||
|
||
// ── Parse <Params> ────────────────────────────────────────────────────
|
||
var xparams = xpump.Element("Params");
|
||
if (xparams != null)
|
||
{
|
||
foreach (var xe in xparams.Elements())
|
||
{
|
||
var param = CanBusParameter.FromXml(xe);
|
||
|
||
pump.ParametersByName[param.Name] = param;
|
||
|
||
if (!pump.ParametersById.ContainsKey(param.MessageId))
|
||
pump.ParametersById[param.MessageId] = new List<CanBusParameter>();
|
||
pump.ParametersById[param.MessageId].Add(param);
|
||
}
|
||
|
||
// Safety net: pump RPM is always a receive parameter.
|
||
if (pump.ParametersByName.TryGetValue(PumpParameterNames.Rpm, out var pumpRpm)
|
||
&& !pumpRpm.IsReceive)
|
||
{
|
||
pumpRpm.IsReceive = true;
|
||
}
|
||
}
|
||
|
||
// ── Parse <Tests> ─────────────────────────────────────────────────────
|
||
var xtests = xpump.Element("Tests");
|
||
if (xtests != null)
|
||
{
|
||
foreach (var xtest in xtests.Elements("Test"))
|
||
{
|
||
var test = TestDefinition.FromXml(xtest);
|
||
if (test.Name == TestType.Wl)
|
||
pump.CombineTestWL(test);
|
||
else
|
||
pump.Tests.Add(test);
|
||
}
|
||
}
|
||
|
||
return pump;
|
||
}
|
||
|
||
// ── Default bench XML ─────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// Returns the factory-default bench parameter XML used when bench.xml is absent.
|
||
/// Uses direction/factor/offset calibration model. Defaults: direction="rx", factor=1, offset=0, type="linear".
|
||
/// </summary>
|
||
private static string DefaultBenchXml() => @"<?xml version=""1.0"" encoding=""utf-8""?>
|
||
<Bench>
|
||
<!-- TX: values sent from software to bench controller -->
|
||
<RPM id=""10"" byteh=""1"" bytel=""0"" direction=""tx"" />
|
||
<Counter id=""11"" byteh=""1"" bytel=""0"" direction=""tx"" />
|
||
<BaudRate id=""55"" byteh=""0"" bytel=""0"" direction=""tx"" />
|
||
<EncoderResolution id=""51"" byteh=""6"" bytel=""7"" direction=""tx"" />
|
||
<ElectronicMsg id=""51"" byteh=""0"" bytel=""0"" direction=""tx"" />
|
||
|
||
<!-- RX: values received from bench controller (direction=""rx"" is the default) -->
|
||
<BenchRPM id=""13"" byteh=""1"" bytel=""0"" />
|
||
<BenchCounter id=""13"" byteh=""3"" bytel=""2"" />
|
||
<BenchTemp id=""14"" byteh=""1"" bytel=""0"" factor=""0.1"" offset=""-20"" />
|
||
<T-in id=""14"" byteh=""3"" bytel=""2"" factor=""0.1"" offset=""-20"" />
|
||
<T-out id=""14"" byteh=""5"" bytel=""4"" factor=""0.1"" offset=""-20"" />
|
||
<T4 id=""14"" byteh=""7"" bytel=""6"" factor=""0.1"" offset=""-20"" />
|
||
<QDelivery id=""8"" byteh=""5"" bytel=""3"" factor=""2030000"" type=""inverse"" filter=""0.01"" />
|
||
<QOver id=""8"" byteh=""2"" bytel=""0"" factor=""510000"" type=""inverse"" filter=""0.11"" />
|
||
<PSGEncoderValue id=""50"" byteh=""4"" bytel=""5"" />
|
||
<PSGEncoderWorking id=""50"" byteh=""7"" bytel=""7"" />
|
||
<InyectorEncoderValue id=""50"" byteh=""2"" bytel=""3"" />
|
||
<InyectorEncoderWorking id=""50"" byteh=""6"" bytel=""6"" />
|
||
<ManualEncoderValue id=""50"" byteh=""0"" bytel=""1"" />
|
||
<Alarms id=""8"" byteh=""7"" bytel=""6"" />
|
||
<Pressure id=""13"" byteh=""4"" bytel=""5"" />
|
||
<AnalogicSensor2 id=""13"" byteh=""6"" bytel=""7"" />
|
||
|
||
<Reles>
|
||
<Rele name=""Electronic"" id=""15"" bit=""0"" />
|
||
<Rele name=""OilPump"" id=""15"" bit=""4"" />
|
||
<Rele name=""DepositCooler"" id=""15"" bit=""8"" />
|
||
<Rele name=""DepositHeater"" id=""15"" bit=""12"" />
|
||
<Rele name=""Reserve"" id=""15"" bit=""16"" />
|
||
<Rele name=""Counter"" id=""15"" bit=""20"" />
|
||
<Rele name=""Direction"" id=""15"" bit=""24"" />
|
||
<Rele name=""TinCooler"" id=""15"" bit=""28"" />
|
||
<Rele name=""Pulse4Signal"" id=""15"" bit=""32"" />
|
||
<Rele name=""Flasher"" id=""15"" bit=""44"" />
|
||
</Reles>
|
||
</Bench>";
|
||
|
||
// ── XML parse helpers ─────────────────────────────────────────────────────
|
||
|
||
private static void TryInt(XElement root, string name, Action<int> assign)
|
||
{
|
||
try { if (int.TryParse(root.Element(name)?.Value, out int v)) assign(v); }
|
||
catch { /* ignore malformed XML */ }
|
||
}
|
||
private static void TryDouble(XElement root, string name, Action<double> assign)
|
||
{
|
||
try
|
||
{
|
||
var val = root.Element(name)?.Value;
|
||
if (val != null && double.TryParse(val,
|
||
System.Globalization.NumberStyles.Float,
|
||
System.Globalization.CultureInfo.InvariantCulture, out double v)) assign(v);
|
||
}
|
||
catch { }
|
||
}
|
||
private static void TryBool(XElement root, string name, Action<bool> assign)
|
||
{
|
||
try { if (bool.TryParse(root.Element(name)?.Value, out bool v)) assign(v); }
|
||
catch { }
|
||
}
|
||
private static void TryString(XElement root, string name, Action<string> assign)
|
||
{
|
||
try { var v = root.Element(name)?.Value; if (v != null) assign(v); }
|
||
catch { }
|
||
}
|
||
|
||
// ── Users ─────────────────────────────────────────────────────────────────
|
||
|
||
/// <inheritdoc/>
|
||
public bool ValidateUser(string username, string password)
|
||
{
|
||
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
|
||
return false;
|
||
|
||
string check = username + ":" + password;
|
||
foreach (string entry in Settings.Users.Split(','))
|
||
{
|
||
if (entry == check) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public IReadOnlyDictionary<string, string> GetUsers()
|
||
{
|
||
var dict = new Dictionary<string, string>();
|
||
foreach (string kv in Settings.Users.Split(','))
|
||
{
|
||
string[] parts = kv.Split(':');
|
||
if (parts.Length == 2 && parts[0].Length > 0)
|
||
dict[parts[0]] = parts[1];
|
||
}
|
||
return dict;
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public void UpdateUsers(Dictionary<string, string> users)
|
||
{
|
||
var entries = new List<string>(users.Count);
|
||
foreach (var kv in users)
|
||
entries.Add(kv.Key + ":" + kv.Value);
|
||
|
||
Settings.Users = string.Join(",", entries);
|
||
SaveSettings();
|
||
}
|
||
}
|
||
|
||
// ── XPath extension shim ──────────────────────────────────────────────────────
|
||
|
||
internal static class XDocumentExtensions
|
||
{
|
||
/// <summary>Minimal XPath-style element selector used to find pump elements by attribute.</summary>
|
||
internal static XElement? XPathSelectElement(this XDocument doc, string xpath)
|
||
{
|
||
// Parse "/Config/Pumps/Pump[@id='xxx']"
|
||
// Sufficient for the pump-lookup use case; not a general XPath engine.
|
||
try
|
||
{
|
||
var parts = xpath.TrimStart('/').Split('/');
|
||
XElement? current = doc.Root;
|
||
// Skip the first part when it names the root element (e.g. "/Config/..." with root <Config>)
|
||
int startIndex = (parts.Length > 0 && current?.Name.LocalName == parts[0]) ? 1 : 0;
|
||
for (int pi = startIndex; pi < parts.Length; pi++)
|
||
{
|
||
if (current == null) return null;
|
||
var part = parts[pi];
|
||
int attrStart = part.IndexOf('[');
|
||
if (attrStart < 0)
|
||
{
|
||
current = current.Element(part);
|
||
}
|
||
else
|
||
{
|
||
string elemName = part[..attrStart];
|
||
string attrExpr = part[(attrStart + 1)..^1]; // strip [ and ]
|
||
if (attrExpr.StartsWith("@"))
|
||
{
|
||
var eqIdx = attrExpr.IndexOf('=');
|
||
string attrName = attrExpr[1..eqIdx];
|
||
string attrValue = attrExpr[(eqIdx + 1)..].Trim('\'', '"');
|
||
var parent = current;
|
||
current = null;
|
||
foreach (var child in parent.Elements(elemName))
|
||
{
|
||
if (child.Attribute(attrName)?.Value == attrValue)
|
||
{ current = child; break; }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return current;
|
||
}
|
||
catch { return null; }
|
||
}
|
||
}
|
||
}
|