feat: developer tools page, auto-test orchestrator, BIP display, tests redesign
Bundles several feature streams that have been iterating on the working tree: - Developer Tools page (Debug-only via DEVELOPER_TOOLS symbol): hosts the identification card, manual KWP write + transaction log, ROM/EEPROM dump card with progress banner and completion message, persisted custom-commands library, persisted EEPROM passwords library. New service primitives: IKwpService.SendRawCustomAsync / ReadEepromAsync / ReadRomEepromAsync. Persistence mirrors the Clients XML pattern in two new files (custom_commands.xml, eeprom_passwords.xml). - Auto-test orchestrator (IAutoTestOrchestrator + AutoTestState): linear K-Line read -> unlock -> bench-on -> test sequence with snackbar UI and progress dialog VM, gated on dashboard alarms. - BIP-STATUS display: BipDisplayViewModel + BipDisplayView, RAM read at 0x0106 via IKwpService.ReadBipStatusAsync; status definitions in BipStatusDefinition. - Tests page redesign: TestSectionCard + PhaseTileView replacing the old TestPlanView/TestRunningView/TestDoneView/TestPreconditionsView/ TestSectionView controls and their VMs. - Pump command sliders: Fluent thick-track style with overhang thumb, click-anywhere-and-drag, mouse-wheel adjustment. - Window startup: app.manifest declares PerMonitorV2 DPI awareness, MainWindow installs a WM_GETMINMAXINFO hook in OnSourceInitialized and maximizes there (after the hook is in place) so the app fits the work area exactly on any display configuration. - Misc: PercentToPixelsConverter, seed_aliases.py one-shot pump-alias importer, tools/Import-BipStatus.ps1, kline_eeprom_spec.md and dump-functions reference docs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -22,13 +23,15 @@ namespace HC_APTBS.Services.Impl
|
||||
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 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 string CustomCommandsXml => Path.Combine(ConfigFolder, "custom_commands.xml");
|
||||
private string EepromPasswordsXml => Path.Combine(ConfigFolder, "eeprom_passwords.xml");
|
||||
|
||||
private readonly IAppLogger _log;
|
||||
private const string LogId = "ConfigurationService";
|
||||
@@ -38,7 +41,21 @@ namespace HC_APTBS.Services.Impl
|
||||
private AppSettings? _settings;
|
||||
private BenchConfiguration? _bench;
|
||||
private SortedDictionary<string, string>? _clients;
|
||||
private ObservableCollection<CustomCommand>? _customCommands;
|
||||
private ObservableCollection<EepromPassword>? _eepromPasswords;
|
||||
// Keyed by StatusID; shared by both status.xml and the pumps.xml <Palabras> fallback.
|
||||
// Two distinct pumps that happen to share the same StatusID integer get the same
|
||||
// table — acceptable given that the 9 known tables are pump-family-shared (same
|
||||
// design as the legacy runtime). Future per-pump-override seam: key on (pumpId, statusId).
|
||||
private readonly Dictionary<int, PumpStatusDefinition> _statusCache = new();
|
||||
// Lazily populated on first fallback lookup; null until first pumps.xml parse attempt.
|
||||
private Dictionary<int, PumpStatusDefinition>? _palabrasStatusCache;
|
||||
|
||||
// Alias indexes: K-Line pumpID alias -> canonical pump ID, and ModelRef alias -> canonical pump ID.
|
||||
// Built once from <Aliases> blocks across all <Pump> elements; both null until first lookup.
|
||||
// Invalidated (set to null) whenever an alias or pump is persisted.
|
||||
private Dictionary<string, string>? _klineAliasIndex;
|
||||
private Dictionary<string, string>? _modelRefAliasIndex;
|
||||
|
||||
// ── Constructor ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -89,6 +106,7 @@ namespace HC_APTBS.Services.Impl
|
||||
new XElement("MaxRpm", Settings.MaxRpm),
|
||||
new XElement("RightRelayValue", Settings.RightRelayValue),
|
||||
new XElement("DefaultIgnoreTin", Settings.DefaultIgnoreTin),
|
||||
new XElement("AutoTestSkipsOilPumpConfirm", Settings.AutoTestSkipsOilPumpConfirm),
|
||||
new XElement("LastRotationDir", Settings.LastRotationDirection),
|
||||
new XElement("DaysKeepLogs", Settings.DaysKeepLogs),
|
||||
new XElement("CompanyName", Settings.CompanyName),
|
||||
@@ -229,6 +247,17 @@ namespace HC_APTBS.Services.Impl
|
||||
// PumpID child element — GetPumpIds() finds these via Descendants("PumpID").
|
||||
xpump.Add(new XElement("PumpID", pump.Id));
|
||||
|
||||
// ── Serialise <Aliases> (equivalence detection) ──
|
||||
if (pump.KlineAliases.Count > 0 || pump.ModelRefAliases.Count > 0)
|
||||
{
|
||||
var xaliasesOut = new XElement("Aliases");
|
||||
foreach (var a in pump.KlineAliases)
|
||||
xaliasesOut.Add(new XElement("KlineId", a));
|
||||
foreach (var a in pump.ModelRefAliases)
|
||||
xaliasesOut.Add(new XElement("ModelRef", a));
|
||||
xpump.Add(xaliasesOut);
|
||||
}
|
||||
|
||||
// ── Serialise <Params> (pump CAN params use legacy P1–P6 format) ──
|
||||
if (pump.ParametersByName.Count > 0)
|
||||
{
|
||||
@@ -255,9 +284,9 @@ namespace HC_APTBS.Services.Impl
|
||||
{
|
||||
var b = pump.BipStatus.Bits[i];
|
||||
xbip.Add(new XElement("Bit",
|
||||
new XAttribute("index", i.ToString(CultureInfo.InvariantCulture)),
|
||||
new XAttribute("index", b.Index.ToString(CultureInfo.InvariantCulture)),
|
||||
new XAttribute("enabled", b.Enabled.ToString().ToLowerInvariant()),
|
||||
new XAttribute("pattern", "0x" + b.HexPattern.ToString("X4", CultureInfo.InvariantCulture)),
|
||||
new XAttribute("hex", "0x" + b.HexPattern.ToString("X4", CultureInfo.InvariantCulture)),
|
||||
new XAttribute("reaction", b.Reaction.ToString(CultureInfo.InvariantCulture)),
|
||||
new XAttribute("specialFunction", b.SpecialFunction.ToString(CultureInfo.InvariantCulture)),
|
||||
b.Description));
|
||||
@@ -282,6 +311,7 @@ namespace HC_APTBS.Services.Impl
|
||||
pumpsNode.Add(xpump);
|
||||
|
||||
xdoc.Save(PumpsXml);
|
||||
InvalidateAliasIndexes();
|
||||
_log.Info(LogId, $"SavePump({pump.Id}) — saved to pumps.xml.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -290,6 +320,149 @@ namespace HC_APTBS.Services.Impl
|
||||
}
|
||||
}
|
||||
|
||||
// ── IConfigurationService: Pump equivalence / alias lookup ────────────────
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? FindPumpIdByKlineAlias(string klinePumpId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(klinePumpId)) return null;
|
||||
EnsureAliasIndexes();
|
||||
return _klineAliasIndex!.TryGetValue(klinePumpId.Trim(), out var canonical)
|
||||
? canonical : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? FindPumpIdByModelRef(string modelRef)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(modelRef)) return null;
|
||||
EnsureAliasIndexes();
|
||||
return _modelRefAliasIndex!.TryGetValue(modelRef.Trim(), out var canonical)
|
||||
? canonical : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AddKlineAlias(string canonicalPumpId, string klineAlias)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(canonicalPumpId) || string.IsNullOrWhiteSpace(klineAlias))
|
||||
return;
|
||||
|
||||
string source = File.Exists(PumpsXml) ? PumpsXml
|
||||
: File.Exists(ConfigXml) ? ConfigXml
|
||||
: null!;
|
||||
if (source == null)
|
||||
{
|
||||
_log.Warning(LogId, $"AddKlineAlias: no pumps.xml/config.xml found.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var xdoc = XDocument.Load(source);
|
||||
var xpump = xdoc.XPathSelectElement($"/Config/Pumps/Pump[@id='{canonicalPumpId}']");
|
||||
if (xpump == null)
|
||||
{
|
||||
_log.Warning(LogId, $"AddKlineAlias: pump '{canonicalPumpId}' not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
var trimmed = klineAlias.Trim();
|
||||
var xaliases = xpump.Element("Aliases");
|
||||
if (xaliases == null)
|
||||
{
|
||||
xaliases = new XElement("Aliases");
|
||||
xpump.Add(xaliases);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var existing in xaliases.Elements("KlineId"))
|
||||
{
|
||||
if (string.Equals(existing.Value.Trim(), trimmed, StringComparison.OrdinalIgnoreCase))
|
||||
return; // already present
|
||||
}
|
||||
}
|
||||
|
||||
xaliases.Add(new XElement("KlineId", trimmed));
|
||||
xdoc.Save(source);
|
||||
InvalidateAliasIndexes();
|
||||
_log.Info(LogId, $"AddKlineAlias: '{trimmed}' -> '{canonicalPumpId}' persisted.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error(LogId, $"AddKlineAlias({canonicalPumpId}, {klineAlias}) failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void InvalidateAliasIndexes()
|
||||
{
|
||||
_klineAliasIndex = null;
|
||||
_modelRefAliasIndex = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the K-Line and ModelRef alias indexes by scanning every <c><Pump></c>
|
||||
/// element in pumps.xml/config.xml for an <c><Aliases></c> block. Cheap enough
|
||||
/// for a one-shot scan: dozens of pumps, a handful of aliases each.
|
||||
/// </summary>
|
||||
private void EnsureAliasIndexes()
|
||||
{
|
||||
if (_klineAliasIndex != null && _modelRefAliasIndex != null) return;
|
||||
|
||||
var klineMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var modelRefMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
string source = File.Exists(PumpsXml) ? PumpsXml
|
||||
: File.Exists(ConfigXml) ? ConfigXml
|
||||
: null!;
|
||||
if (source == null)
|
||||
{
|
||||
_klineAliasIndex = klineMap;
|
||||
_modelRefAliasIndex = modelRefMap;
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var xdoc = XDocument.Load(source);
|
||||
foreach (var xpump in xdoc.Descendants("Pump"))
|
||||
{
|
||||
var canonical = xpump.Attribute("id")?.Value;
|
||||
if (string.IsNullOrWhiteSpace(canonical)) continue;
|
||||
|
||||
var xaliases = xpump.Element("Aliases");
|
||||
if (xaliases == null) continue;
|
||||
|
||||
foreach (var xkid in xaliases.Elements("KlineId"))
|
||||
{
|
||||
var alias = xkid.Value?.Trim();
|
||||
if (string.IsNullOrEmpty(alias)) continue;
|
||||
if (!klineMap.ContainsKey(alias))
|
||||
klineMap[alias] = canonical;
|
||||
else
|
||||
_log.Warning(LogId,
|
||||
$"Duplicate KlineId alias '{alias}' (already mapped to '{klineMap[alias]}', ignoring duplicate under '{canonical}').");
|
||||
}
|
||||
|
||||
foreach (var xmref in xaliases.Elements("ModelRef"))
|
||||
{
|
||||
var alias = xmref.Value?.Trim();
|
||||
if (string.IsNullOrEmpty(alias)) continue;
|
||||
if (!modelRefMap.ContainsKey(alias))
|
||||
modelRefMap[alias] = canonical;
|
||||
else
|
||||
_log.Warning(LogId,
|
||||
$"Duplicate ModelRef alias '{alias}' (already mapped to '{modelRefMap[alias]}', ignoring duplicate under '{canonical}').");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error(LogId, $"EnsureAliasIndexes failed: {ex.Message}");
|
||||
}
|
||||
|
||||
_klineAliasIndex = klineMap;
|
||||
_modelRefAliasIndex = modelRefMap;
|
||||
}
|
||||
|
||||
// ── IConfigurationService: Clients ────────────────────────────────────────
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -320,6 +493,65 @@ namespace HC_APTBS.Services.Impl
|
||||
}
|
||||
}
|
||||
|
||||
// ── IConfigurationService: Developer libraries ────────────────────────────
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ObservableCollection<CustomCommand> CustomCommands
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_customCommands == null) LoadCustomCommands();
|
||||
return _customCommands!;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SaveCustomCommands()
|
||||
{
|
||||
try
|
||||
{
|
||||
var root = new XElement("CustomCommands");
|
||||
foreach (var cmd in CustomCommands)
|
||||
root.Add(new XElement("command",
|
||||
new XAttribute("name", cmd.Name ?? string.Empty),
|
||||
new XAttribute("hex", cmd.HexBytes ?? string.Empty)));
|
||||
new XDocument(root).Save(CustomCommandsXml);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error(LogId, $"SaveCustomCommands failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ObservableCollection<EepromPassword> EepromPasswords
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_eepromPasswords == null) LoadEepromPasswords();
|
||||
return _eepromPasswords!;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SaveEepromPasswords()
|
||||
{
|
||||
try
|
||||
{
|
||||
var root = new XElement("EepromPasswords");
|
||||
foreach (var pw in EepromPasswords)
|
||||
root.Add(new XElement("password",
|
||||
new XAttribute("name", pw.Name ?? string.Empty),
|
||||
new XAttribute("zone", pw.Zone.ToString("X2", CultureInfo.InvariantCulture)),
|
||||
new XAttribute("key", pw.Key.ToString("X4", CultureInfo.InvariantCulture))));
|
||||
new XDocument(root).Save(EepromPasswordsXml);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error(LogId, $"SaveEepromPasswords failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── IConfigurationService: Sensors ────────────────────────────────────────
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -365,18 +597,33 @@ namespace HC_APTBS.Services.Impl
|
||||
if (_statusCache.TryGetValue(statusId, out var cached))
|
||||
return cached;
|
||||
|
||||
// Palabras (pumps.xml) is authoritative — matches legacy runtime which read
|
||||
// /Config/Palabras/PumpStatus[@StatusID='N'] exclusively. status.xml is a
|
||||
// new-system artifact and in practice carries stale/partial definitions that
|
||||
// collide on shared StatusIDs (e.g. ID=5 labelled EMPF3 in status.xml but
|
||||
// CAN-PSGTEST in palabras). Only fall back to status.xml when palabras is
|
||||
// missing the id entirely.
|
||||
var def = LoadPumpStatusFromPumpsXml(statusId);
|
||||
|
||||
if (def == null)
|
||||
def = LoadPumpStatusFromXml(StatusXml, statusId);
|
||||
|
||||
if (def != null)
|
||||
_statusCache[statusId] = def;
|
||||
|
||||
return def;
|
||||
}
|
||||
|
||||
/// <summary>Parses a <c><PumpStatus StatusID="N"></c> element from any XML file.</summary>
|
||||
private PumpStatusDefinition? LoadPumpStatusFromXml(string xmlPath, int statusId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(StatusXml))
|
||||
{
|
||||
_log.Error(LogId, $"LoadPumpStatus: {StatusXml} not found.");
|
||||
return null;
|
||||
}
|
||||
if (!File.Exists(xmlPath)) return null;
|
||||
|
||||
var xdoc = XDocument.Load(StatusXml);
|
||||
var xdoc = XDocument.Load(xmlPath);
|
||||
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"))
|
||||
{
|
||||
@@ -387,52 +634,107 @@ namespace HC_APTBS.Services.Impl
|
||||
}
|
||||
}
|
||||
|
||||
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", CultureInfo.InvariantCulture),
|
||||
Color = xVal.Attribute("color")?.Value ?? "26C200",
|
||||
Description = xVal.Value.Trim(),
|
||||
Reaction = int.Parse(xVal.Attribute("reaction")?.Value ?? "0", CultureInfo.InvariantCulture)
|
||||
});
|
||||
}
|
||||
|
||||
def.Bits.Add(bit);
|
||||
}
|
||||
|
||||
_statusCache[statusId] = def;
|
||||
return def;
|
||||
if (xStatus == null) return null;
|
||||
return ParsePumpStatusElement(xStatus, statusId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error(LogId, $"LoadPumpStatus({statusId}) failed: {ex.Message}");
|
||||
_log.Error(LogId, $"LoadPumpStatusFromXml({xmlPath}, {statusId}) failed: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lazily loads all <c><PumpStatus></c> entries from the <c><Palabras></c>
|
||||
/// block in pumps.xml and returns the one matching <paramref name="statusId"/>.
|
||||
/// The orphan block lives outside <c></Pumps></c> and is not touched by the
|
||||
/// pump loader — this is the only path that reads it.
|
||||
/// </summary>
|
||||
private PumpStatusDefinition? LoadPumpStatusFromPumpsXml(int statusId)
|
||||
{
|
||||
// Build the cache on first call.
|
||||
if (_palabrasStatusCache == null)
|
||||
{
|
||||
_palabrasStatusCache = new Dictionary<int, PumpStatusDefinition>();
|
||||
try
|
||||
{
|
||||
if (!File.Exists(PumpsXml)) return null;
|
||||
|
||||
var xdoc = XDocument.Load(PumpsXml);
|
||||
var palabras = xdoc.Root?.Element("Palabras");
|
||||
if (palabras == null)
|
||||
{
|
||||
// Try top-level sibling — the <Palabras> block is outside </Pumps>.
|
||||
palabras = xdoc.Descendants("Palabras").FirstOrDefault();
|
||||
}
|
||||
|
||||
if (palabras == null)
|
||||
{
|
||||
_log.Error(LogId, "LoadPumpStatusFromPumpsXml: <Palabras> block not found in pumps.xml.");
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var el in palabras.Descendants("PumpStatus"))
|
||||
{
|
||||
if (!int.TryParse(el.Attribute("StatusID")?.Value, out var id)) continue;
|
||||
var parsed = ParsePumpStatusElement(el, id);
|
||||
if (parsed != null)
|
||||
_palabrasStatusCache[id] = parsed;
|
||||
else
|
||||
_log.Error(LogId, $"LoadPumpStatusFromPumpsXml: malformed PumpStatus StatusID={id} in pumps.xml.");
|
||||
}
|
||||
|
||||
_log.Info(LogId, $"Loaded {_palabrasStatusCache.Count} pump-status tables from pumps.xml <Palabras>.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error(LogId, $"LoadPumpStatusFromPumpsXml parse failed: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
_palabrasStatusCache.TryGetValue(statusId, out var def);
|
||||
if (def == null)
|
||||
_log.Error(LogId, $"LoadPumpStatus: StatusID={statusId} not found in status.xml or pumps.xml <Palabras>.");
|
||||
return def;
|
||||
}
|
||||
|
||||
/// <summary>Converts a <c><PumpStatus></c> XML element into a <see cref="PumpStatusDefinition"/>.</summary>
|
||||
private static PumpStatusDefinition ParsePumpStatusElement(XElement xStatus, int statusId)
|
||||
{
|
||||
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", CultureInfo.InvariantCulture),
|
||||
Color = xVal.Attribute("color")?.Value ?? "26C200",
|
||||
Description = xVal.Value.Trim(),
|
||||
Reaction = int.Parse(xVal.Attribute("reaction")?.Value ?? "0", CultureInfo.InvariantCulture)
|
||||
});
|
||||
}
|
||||
|
||||
def.Bits.Add(bit);
|
||||
}
|
||||
|
||||
return def;
|
||||
}
|
||||
|
||||
// ── Private loaders ───────────────────────────────────────────────────────
|
||||
|
||||
private void LoadSettings()
|
||||
@@ -466,6 +768,7 @@ namespace HC_APTBS.Services.Impl
|
||||
TryInt(r, "MaxRpm", v => _settings.MaxRpm = v);
|
||||
TryBool(r, "RightRelayValue", v => _settings.RightRelayValue = v);
|
||||
TryBool(r, "DefaultIgnoreTin", v => _settings.DefaultIgnoreTin = v);
|
||||
TryBool(r, "AutoTestSkipsOilPumpConfirm", v => _settings.AutoTestSkipsOilPumpConfirm = v);
|
||||
TryInt(r, "LastRotationDir", v => _settings.LastRotationDirection = (short)v);
|
||||
TryInt(r, "DaysKeepLogs", v => _settings.DaysKeepLogs = v);
|
||||
TryString(r, "CompanyName", v => _settings.CompanyName = v);
|
||||
@@ -599,6 +902,63 @@ namespace HC_APTBS.Services.Impl
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadCustomCommands()
|
||||
{
|
||||
_customCommands = new ObservableCollection<CustomCommand>();
|
||||
if (!File.Exists(CustomCommandsXml)) return;
|
||||
try
|
||||
{
|
||||
var xdoc = XDocument.Load(CustomCommandsXml);
|
||||
foreach (var xe in xdoc.Root!.Elements("command"))
|
||||
{
|
||||
var name = xe.Attribute("name")?.Value ?? string.Empty;
|
||||
var hex = xe.Attribute("hex")?.Value ?? string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(name) && string.IsNullOrWhiteSpace(hex))
|
||||
{
|
||||
_log.Warning(LogId, "LoadCustomCommands: skipping empty <command/> element.");
|
||||
continue;
|
||||
}
|
||||
_customCommands.Add(new CustomCommand { Name = name, HexBytes = hex });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error(LogId, $"LoadCustomCommands failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadEepromPasswords()
|
||||
{
|
||||
_eepromPasswords = new ObservableCollection<EepromPassword>();
|
||||
if (!File.Exists(EepromPasswordsXml)) return;
|
||||
try
|
||||
{
|
||||
var xdoc = XDocument.Load(EepromPasswordsXml);
|
||||
foreach (var xe in xdoc.Root!.Elements("password"))
|
||||
{
|
||||
var name = xe.Attribute("name")?.Value ?? string.Empty;
|
||||
var zoneStr = xe.Attribute("zone")?.Value ?? "0";
|
||||
var keyStr = xe.Attribute("key")?.Value ?? "0";
|
||||
|
||||
if (!byte.TryParse(zoneStr, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out byte zone))
|
||||
{
|
||||
_log.Warning(LogId, $"LoadEepromPasswords: bad zone '{zoneStr}' in <password name='{name}'/> — skipping.");
|
||||
continue;
|
||||
}
|
||||
if (!ushort.TryParse(keyStr, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ushort key))
|
||||
{
|
||||
_log.Warning(LogId, $"LoadEepromPasswords: bad key '{keyStr}' in <password name='{name}'/> — skipping.");
|
||||
continue;
|
||||
}
|
||||
_eepromPasswords.Add(new EepromPassword { Name = name, Zone = zone, Key = key });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error(LogId, $"LoadEepromPasswords failed: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadAlarms()
|
||||
{
|
||||
_settings ??= new AppSettings();
|
||||
@@ -729,6 +1089,27 @@ namespace HC_APTBS.Services.Impl
|
||||
}
|
||||
}
|
||||
|
||||
// ── Parse <Aliases> (optional — equivalence detection) ────────────────
|
||||
// Per-pump equivalence map: alternative K-Line pumpIDs and ModelReference
|
||||
// strings that should resolve to this canonical pump. See plan in
|
||||
// .claude/plans/in-the-pump-identification-velvety-meteor.md.
|
||||
var xaliases = xpump.Element("Aliases");
|
||||
if (xaliases != null)
|
||||
{
|
||||
foreach (var xkid in xaliases.Elements("KlineId"))
|
||||
{
|
||||
var alias = xkid.Value?.Trim();
|
||||
if (!string.IsNullOrEmpty(alias))
|
||||
pump.KlineAliases.Add(alias);
|
||||
}
|
||||
foreach (var xmref in xaliases.Elements("ModelRef"))
|
||||
{
|
||||
var alias = xmref.Value?.Trim();
|
||||
if (!string.IsNullOrEmpty(alias))
|
||||
pump.ModelRefAliases.Add(alias);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Parse <Tests> ─────────────────────────────────────────────────────
|
||||
var xtests = xpump.Element("Tests");
|
||||
if (xtests != null)
|
||||
@@ -752,7 +1133,8 @@ namespace HC_APTBS.Services.Impl
|
||||
{
|
||||
try
|
||||
{
|
||||
var patternStr = xbit.Attribute("pattern")?.Value ?? "0";
|
||||
// Accept both "hex" (import script) and legacy "pattern" attribute names.
|
||||
var patternStr = (xbit.Attribute("hex") ?? xbit.Attribute("pattern"))?.Value ?? "0";
|
||||
if (patternStr.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
patternStr = patternStr.Substring(2);
|
||||
|
||||
@@ -765,6 +1147,7 @@ namespace HC_APTBS.Services.Impl
|
||||
|
||||
bipDef.Bits.Add(new BipStatusDefinition
|
||||
{
|
||||
Index = int.Parse(xbit.Attribute("index")?.Value ?? bipDef.Bits.Count.ToString(), CultureInfo.InvariantCulture),
|
||||
Enabled = !string.Equals(xbit.Attribute("enabled")?.Value, "false",
|
||||
StringComparison.OrdinalIgnoreCase),
|
||||
HexPattern = ushort.Parse(patternStr, NumberStyles.HexNumber, CultureInfo.InvariantCulture),
|
||||
|
||||
Reference in New Issue
Block a user