Files
HC_APTBS/Services/Impl/ConfigurationService.cs
LucianoDev c617854c09 feat: implement SavePump/SaveAlarms, fix config round-trip bugs, redesign PDF reports
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>
2026-04-15 15:21:22 +02:00

835 lines
36 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 P1P6 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; }
}
}
}