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:
2026-05-07 13:59:50 +02:00
parent da0581967b
commit 827b811b39
102 changed files with 7522 additions and 1798 deletions

123
seed_aliases.py Normal file
View File

@@ -0,0 +1,123 @@
"""One-shot seeder: inject <Aliases> blocks into pumps.xml from the legacy
KlineIDs comma-string at old_source/Herlic2.0/App.config.
Reads canonical -> [aliases] map (hard-coded below from the legacy default),
finds each <Pump id="..."> in pumps.xml, and inserts an <Aliases><KlineId>...
block as the first child of that <Pump> element.
Idempotent: if a <Pump> already has an <Aliases> child, the script merges new
aliases into it without creating duplicates.
"""
import sys
from pathlib import Path
import xml.etree.ElementTree as ET
# Source: old_source/Herlic2.0/App.config -> KlineIDs setting (default value).
# Format: "klineID:canonicalPumpID,..."
LEGACY_KLINEIDS = (
"0470004007:0470004002,0470004008:0470004006,0470004012:0470004004,"
"1093412060:0470504028,1093412050:0470504012,1093424041:0470504034,"
"0986444003:0470504004,1093412071:0470504033,1093423001:0470504027,"
"0986444012:0470504011,1093412070:0470504033,0986444005:0470504009,"
"1093424051:0470504046,1093424025:1093424026,0986444002:0470504003,"
"1093424027:1093424026,1093423002:0470504027,1093424024:1093424026,"
"0986444006:0470506002,0986444011:0470504010,0986444044:0470506037,"
"1093421004:0470504026,1093412072:0470504033,1093411003:0470504030,"
"1093424040:0470504034,0986444019:0470504020,0986444004:0470504005,"
"1093411022:0470504037,0986444042:0470506017,0986444014:0470504015,"
"1093421006:0470504026,1093421005:0470504026,1093424080:0470504046,"
"0986444026:0470506030,1093411004:0470504030,0986444010:0470506009,"
"0986444008:0470506006,"
)
PUMPS_XML = Path.home() / ".HC_APTBS" / "config" / "pumps.xml"
def parse_legacy(s: str) -> dict[str, list[str]]:
"""Group legacy entries by canonical pump ID."""
aliases_by_canonical: dict[str, list[str]] = {}
for entry in s.split(","):
entry = entry.strip()
if not entry or ":" not in entry:
continue
kline_id, canonical = entry.split(":", 1)
kline_id = kline_id.strip()
canonical = canonical.strip()
if not kline_id or not canonical:
continue
bucket = aliases_by_canonical.setdefault(canonical, [])
if kline_id not in bucket:
bucket.append(kline_id)
return aliases_by_canonical
def main() -> int:
aliases_by_canonical = parse_legacy(LEGACY_KLINEIDS)
print(f"Parsed {sum(len(v) for v in aliases_by_canonical.values())} aliases "
f"across {len(aliases_by_canonical)} canonical pumps.")
if not PUMPS_XML.exists():
print(f"ERROR: {PUMPS_XML} does not exist.", file=sys.stderr)
return 1
tree = ET.parse(PUMPS_XML)
root = tree.getroot()
pumps_by_id: dict[str, ET.Element] = {}
for pump in root.iter("Pump"):
pid = pump.attrib.get("id")
if pid:
pumps_by_id[pid] = pump
missing = sorted(set(aliases_by_canonical) - set(pumps_by_id))
if missing:
print(f"WARNING: {len(missing)} canonical pumps from legacy data not in "
f"pumps.xml — skipping: {missing}", file=sys.stderr)
injected_pumps = 0
injected_aliases = 0
skipped_existing = 0
for canonical, kline_ids in sorted(aliases_by_canonical.items()):
pump = pumps_by_id.get(canonical)
if pump is None:
continue
existing = pump.find("Aliases")
if existing is None:
existing = ET.Element("Aliases")
pump.insert(0, existing)
existing.text = "\n "
existing.tail = "\n "
already = {(c.tag, (c.text or "").strip())
for c in existing if c.tag in ("KlineId", "ModelRef")}
added_here = 0
for k in kline_ids:
key = ("KlineId", k)
if key in already:
skipped_existing += 1
continue
kid = ET.SubElement(existing, "KlineId")
kid.text = k
kid.tail = "\n "
already.add(key)
added_here += 1
injected_aliases += 1
if added_here:
injected_pumps += 1
# Fix the tail of the last child so the closing tag is on its own line.
last = list(existing)[-1]
last.tail = "\n "
tree.write(PUMPS_XML, encoding="utf-8", xml_declaration=True)
print(f"Injected {injected_aliases} aliases into {injected_pumps} pumps.")
if skipped_existing:
print(f" ({skipped_existing} aliases were already present, skipped.)")
return 0
if __name__ == "__main__":
sys.exit(main())