Move Start/Stop/Report buttons from the middle panel to the top of TestPanelView (automated tests section), matching the old application layout. Remove inline Operator/Client text fields — operator identity now comes from a UserCheckDialog (username/password) shown before the existing ReportDialog. Add credential storage to ConfigurationService. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
736 lines
34 KiB
C#
736 lines
34 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)
|
|
{
|
|
_log.Info(LogId, $"SavePump({pump.Id}) — not yet implemented.");
|
|
}
|
|
|
|
// ── 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}");
|
|
}
|
|
}
|
|
|
|
// ── 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, "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))
|
|
{
|
|
_settings.Sensors[1] = SensorConfiguration.DefaultPressureSensor();
|
|
return;
|
|
}
|
|
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}");
|
|
}
|
|
}
|
|
|
|
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 ───────────────────────────────────────────────────────
|
|
|
|
private static CanBusParameter ParseParamElement(XElement xe)
|
|
{
|
|
var p = 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 = double.Parse(xe.Attribute("filter")?.Value ?? "1",
|
|
System.Globalization.CultureInfo.InvariantCulture),
|
|
DisableCalibration = bool.Parse(xe.Attribute("disableparams")?.Value ?? "true"),
|
|
// Bench params default to receive unless explicitly marked send="true".
|
|
IsReceive = !string.Equals(xe.Attribute("send")?.Value, "true",
|
|
StringComparison.OrdinalIgnoreCase)
|
|
};
|
|
|
|
if (!p.DisableCalibration)
|
|
{
|
|
p.P1 = double.Parse(xe.Attribute("p1")?.Value ?? "1", System.Globalization.CultureInfo.InvariantCulture);
|
|
p.P2 = double.Parse(xe.Attribute("p2")?.Value ?? "0", System.Globalization.CultureInfo.InvariantCulture);
|
|
p.P3 = double.Parse(xe.Attribute("p3")?.Value ?? "0", System.Globalization.CultureInfo.InvariantCulture);
|
|
p.P4 = double.Parse(xe.Attribute("p4")?.Value ?? "1", System.Globalization.CultureInfo.InvariantCulture);
|
|
p.P5 = double.Parse(xe.Attribute("p5")?.Value ?? "0", System.Globalization.CultureInfo.InvariantCulture);
|
|
p.P6 = double.Parse(xe.Attribute("p6")?.Value ?? "0", System.Globalization.CultureInfo.InvariantCulture);
|
|
}
|
|
else
|
|
{
|
|
p.SetIdentityCalibration();
|
|
}
|
|
|
|
return p;
|
|
}
|
|
|
|
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.</summary>
|
|
private static string DefaultBenchXml() => @"<?xml version=""1.0"" encoding=""utf-8""?>
|
|
<Bench>
|
|
<RPM id=""10"" byteh=""1"" bytel=""0"" filter=""1"" disableparams=""true"" send=""true"" />
|
|
<Counter id=""11"" byteh=""1"" bytel=""0"" filter=""1"" disableparams=""true"" send=""true"" />
|
|
<BaudRate id=""55"" byteh=""0"" bytel=""0"" filter=""1"" disableparams=""true"" send=""true"" />
|
|
<BenchRPM id=""13"" byteh=""1"" bytel=""0"" filter=""1"" disableparams=""true"" />
|
|
<BenchCounter id=""13"" byteh=""3"" bytel=""2"" filter=""1"" disableparams=""true"" />
|
|
<BenchTemp id=""14"" byteh=""1"" bytel=""0"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""10"" p5=""-20"" p6=""0"" />
|
|
<T-in id=""14"" byteh=""3"" bytel=""2"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""10"" p5=""-20"" p6=""0"" />
|
|
<T-out id=""14"" byteh=""5"" bytel=""4"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""10"" p5=""-20"" p6=""0"" />
|
|
<T4 id=""14"" byteh=""7"" bytel=""6"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""10"" p5=""-20"" p6=""0"" />
|
|
<QDelivery id=""8"" byteh=""5"" bytel=""3"" filter=""0.01"" disableparams=""false"" p1=""0"" p2=""2.03"" p3=""1E-06"" p4=""0"" p5=""0"" p6=""0"" />
|
|
<QOver id=""8"" byteh=""2"" bytel=""0"" filter=""0.11"" disableparams=""false"" p1=""0"" p2=""0.51"" p3=""1E-06"" p4=""0"" p5=""0"" p6=""0"" />
|
|
<PSGEncoderValue id=""50"" byteh=""4"" bytel=""5"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""1"" p5=""0"" p6=""0"" />
|
|
<PSGEncoderWorking id=""50"" byteh=""7"" bytel=""7"" filter=""1"" disableparams=""true"" />
|
|
<InyectorEncoderValue id=""50"" byteh=""2"" bytel=""3"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""1"" p5=""0"" p6=""0"" />
|
|
<InyectorEncoderWorking id=""50"" byteh=""6"" bytel=""6"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""1"" p5=""0"" p6=""0"" />
|
|
<ManualEncoderValue id=""50"" byteh=""0"" bytel=""1"" filter=""1"" disableparams=""false"" p1=""1"" p2=""0"" p3=""0"" p4=""1"" p5=""0"" p6=""0"" />
|
|
<EncoderResolution id=""51"" byteh=""6"" bytel=""7"" filter=""1"" disableparams=""true"" send=""true"" />
|
|
<ElectronicMsg id=""51"" byteh=""0"" bytel=""0"" filter=""1"" disableparams=""true"" send=""true"" />
|
|
<Alarms id=""8"" byteh=""7"" bytel=""6"" filter=""1"" disableparams=""true"" />
|
|
<Pressure id=""13"" byteh=""4"" bytel=""5"" filter=""1"" disableparams=""true"" />
|
|
<AnalogicSensor2 id=""13"" byteh=""6"" bytel=""7"" filter=""1"" disableparams=""true"" />
|
|
<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; }
|
|
}
|
|
}
|
|
}
|