diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0cfad6e..51881c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,5 +48,12 @@ jobs: - name: Validate JSON schemas run: | - python -c "from pathlib import Path; import json; from jsonschema import Draft202012Validator; root=Path('schemas'); -[Draft202012Validator.check_schema(json.loads(p.read_text(encoding='utf-8'))) for p in root.glob('*.json')]; print('schemas ok')" + python -c " + from pathlib import Path + import json + from jsonschema import Draft202012Validator + root = Path('schemas') + for p in root.glob('*.json'): + Draft202012Validator.check_schema(json.loads(p.read_text(encoding='utf-8'))) + print('schemas ok') + " diff --git a/.gitignore b/.gitignore index f240954..6cfef9b 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,7 @@ captures/community/* *.log !captures/synthetic/*.log !captures/synthetic/*.asc +!tests/fixtures/*.log # Regenerable opendbc fetch cache research/_opendbc_fetch/ diff --git a/docs/generated/buses.md b/docs/generated/buses.md index e2a7c48..f9781b0 100644 --- a/docs/generated/buses.md +++ b/docs/generated/buses.md @@ -3,18 +3,24 @@ | Platform | Bus ID | OEM name | Bitrate | Confidence | |----------|--------|----------|---------|------------| | `honda.accord.gen10.us` | `vehicle_can` | — | UNKNOWN | DOCUMENTED | +| `honda.accord.gen11.us` | `pt_canfd` | Powertrain CAN-FD (opendbc Bus.pt) | UNKNOWN | UNKNOWN | | `honda.civic.gen10.us` | `vehicle_can` | — | UNKNOWN | DOCUMENTED | | `honda.civic.gen11.us` | `vehicle_can` | — | UNKNOWN | DOCUMENTED | +| `honda.civic.gen7.us` | `obd_kline` | OBD-II K-Line (ISO 9141-2 / ISO 14230-4) | 10400 | DOCUMENTED | +| `honda.civic.gen7.us` | `f_can` | F-CAN (if present) | UNKNOWN | UNKNOWN | +| `honda.civic.gen7.us` | `b_can` | B-CAN / body multiplex | UNKNOWN | UNKNOWN | | `honda.civic.gen8.us` | `f_can` | F-CAN | 500000 | DOCUMENTED | | `honda.civic.gen8.us` | `b_can` | B-CAN | 33333 | DOCUMENTED | | `honda.civic.gen9.us` | `f_can` | F-CAN / OBD-II CAN | 500000 | DOCUMENTED | | `honda.civic.gen9.us` | `b_can` | B-CAN | UNKNOWN | UNKNOWN | | `honda.crv.gen5.us` | `vehicle_can` | — | UNKNOWN | DOCUMENTED | +| `honda.crv.gen6.us` | `pt_canfd` | Powertrain CAN-FD (opendbc Bus.pt) | UNKNOWN | UNKNOWN | | `honda.element.gen1.us` | `unknown_can` | — | UNKNOWN | UNKNOWN | | `honda.fit.gen3.us` | `vehicle_can` | F-CAN | UNKNOWN | DOCUMENTED | | `honda.hrv.gen2.us` | `unknown_can` | — | UNKNOWN | UNKNOWN | | `honda.insight.gen3.us` | `vehicle_can` | F-CAN | UNKNOWN | DOCUMENTED | | `honda.odyssey.gen5.us` | `vehicle_can` | F-CAN | UNKNOWN | DOCUMENTED | | `honda.pilot.gen3.us` | `unknown_can` | — | UNKNOWN | UNKNOWN | +| `honda.pilot.gen4.us` | `pt_canfd` | Powertrain CAN-FD (opendbc Bus.pt) | UNKNOWN | UNKNOWN | | `honda.prelude.gen5.us` | `unknown_can` | — | UNKNOWN | UNKNOWN | | `honda.ridgeline.gen2.us` | `unknown_can` | — | UNKNOWN | UNKNOWN | diff --git a/docs/generated/coverage.md b/docs/generated/coverage.md index 99c296d..15b4ea7 100644 --- a/docs/generated/coverage.md +++ b/docs/generated/coverage.md @@ -7,17 +7,21 @@ _Auto-generated. Confidence labels only — not hardware claims._ | Platform | RPM | SPEED | FUEL | TEMP | GEAR | TURN | ABS | SRS | |----------|------|------|------|------|------|------|------|------| | `honda.accord.gen10.us` | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | +| `honda.accord.gen11.us` | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | | `honda.civic.gen10.us` | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | | `honda.civic.gen11.us` | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | +| `honda.civic.gen7.us` | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | | `honda.civic.gen8.us` | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | | `honda.civic.gen9.us` | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | | `honda.crv.gen5.us` | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | +| `honda.crv.gen6.us` | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | | `honda.element.gen1.us` | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | | `honda.fit.gen3.us` | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | | `honda.hrv.gen2.us` | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | | `honda.insight.gen3.us` | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | | `honda.odyssey.gen5.us` | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | | `honda.pilot.gen3.us` | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | +| `honda.pilot.gen4.us` | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | | `honda.prelude.gen5.us` | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | | `honda.ridgeline.gen2.us` | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | diff --git a/docs/generated/honda_coverage.md b/docs/generated/honda_coverage.md index 8672329..0bab360 100644 --- a/docs/generated/honda_coverage.md +++ b/docs/generated/honda_coverage.md @@ -5,16 +5,20 @@ _Auto-generated. Confidence labels only — not hardware claims._ | Platform | RPM | SPEED | FUEL | TEMP | GEAR | TURN | ABS | SRS | |----------|------|------|------|------|------|------|------|------| | `honda.accord.gen10.us` | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | +| `honda.accord.gen11.us` | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | | `honda.civic.gen10.us` | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | | `honda.civic.gen11.us` | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | +| `honda.civic.gen7.us` | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | | `honda.civic.gen8.us` | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | | `honda.civic.gen9.us` | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | | `honda.crv.gen5.us` | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | +| `honda.crv.gen6.us` | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | | `honda.element.gen1.us` | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | | `honda.fit.gen3.us` | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | | `honda.hrv.gen2.us` | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | | `honda.insight.gen3.us` | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | | `honda.odyssey.gen5.us` | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | DOCUMENTED | DOCUMENTED | UNKNOWN | UNKNOWN | | `honda.pilot.gen3.us` | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | +| `honda.pilot.gen4.us` | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | | `honda.prelude.gen5.us` | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | | `honda.ridgeline.gen2.us` | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | UNKNOWN | diff --git a/docs/generated/modules.md b/docs/generated/modules.md index a0b9c54..48c1506 100644 --- a/docs/generated/modules.md +++ b/docs/generated/modules.md @@ -26,6 +26,9 @@ | `honda.civic.gen11.us` | `eps` | vehicle_can | chassis | DOCUMENTED | | `honda.civic.gen11.us` | `adas` | vehicle_can | adas | DOCUMENTED | | `honda.civic.gen11.us` | `cluster` | vehicle_can | cluster | INFERRED | +| `honda.civic.gen7.us` | `pcm` | obd_kline | powertrain | DOCUMENTED | +| `honda.civic.gen7.us` | `micu` | b_can | body | COMMUNITY_REPORTED | +| `honda.civic.gen7.us` | `gauge` | UNKNOWN | cluster | UNKNOWN | | `honda.civic.gen8.us` | `pcm` | f_can | powertrain | DOCUMENTED | | `honda.civic.gen8.us` | `abs_vsa` | f_can | chassis | DOCUMENTED | | `honda.civic.gen8.us` | `bcm` | b_can | body | DOCUMENTED | diff --git a/docs/generated/platforms.md b/docs/generated/platforms.md index be29ae7..114a0a0 100644 --- a/docs/generated/platforms.md +++ b/docs/generated/platforms.md @@ -5,16 +5,20 @@ _Auto-generated from protocol YAML. UNKNOWN is valid._ | Platform | Years | Roles | Buses | Messages | Signals | Cluster | |----------|-------|-------|-------|----------|---------|---------| | `honda.accord.gen10.us` | 2018-2022 | target | 1 | 16 | 23 | `honda.accord.gen10.cluster.digital` | +| `honda.accord.gen11.us` | 2023-2026 | target | 1 | 0 | 0 | `—` | | `honda.civic.gen10.us` | 2016-2021 | target,source | 1 | 16 | 25 | `honda.civic.gen10.cluster.digital` | | `honda.civic.gen11.us` | 2022-2026 | target | 1 | 16 | 23 | `honda.civic.gen11.cluster.digital` | +| `honda.civic.gen7.us` | 2001-2005 | source | 3 | 0 | 8 | `—` | | `honda.civic.gen8.us` | 2006-2011 | source | 2 | 3 | 8 | `—` | | `honda.civic.gen9.us` | 2012-2015 | source | 2 | 0 | 9 | `—` | | `honda.crv.gen5.us` | 2017-2022 | target | 1 | 17 | 23 | `honda.crv.gen5.cluster.digital` | +| `honda.crv.gen6.us` | 2023-2026 | target | 1 | 0 | 0 | `—` | | `honda.element.gen1.us` | 2003-2011 | source | 1 | 0 | 0 | `—` | | `honda.fit.gen3.us` | 2015-2020 | source | 1 | 13 | 27 | `—` | | `honda.hrv.gen2.us` | 2023-2026 | source | 1 | 0 | 0 | `—` | | `honda.insight.gen3.us` | 2019-2022 | source,target | 1 | 14 | 28 | `—` | | `honda.odyssey.gen5.us` | 2018-2025 | source | 1 | 15 | 28 | `—` | | `honda.pilot.gen3.us` | 2016-2022 | source | 1 | 0 | 0 | `—` | +| `honda.pilot.gen4.us` | 2023-2026 | target | 1 | 0 | 0 | `—` | | `honda.prelude.gen5.us` | 1997-2001 | source | 1 | 0 | 0 | `—` | | `honda.ridgeline.gen2.us` | 2017-2025 | source | 1 | 0 | 0 | `—` | diff --git a/docs/generated/signals.md b/docs/generated/signals.md index 12d86fe..8e6abfd 100644 --- a/docs/generated/signals.md +++ b/docs/generated/signals.md @@ -73,6 +73,14 @@ | `honda.civic.gen11.us` | `safety.seatbelt_passenger` | `0x305` | DOCUMENTED | False | | `honda.civic.gen11.us` | `adas.acc_state` | `0x17C` | DOCUMENTED | False | | `honda.civic.gen11.us` | `vehicle.ignition_state` | `UNKNOWN` | UNKNOWN | False | +| `honda.civic.gen7.us` | `powertrain.engine_rpm` | `UNKNOWN` | UNKNOWN | False | +| `honda.civic.gen7.us` | `vehicle.speed` | `UNKNOWN` | UNKNOWN | False | +| `honda.civic.gen7.us` | `powertrain.coolant_temperature` | `UNKNOWN` | UNKNOWN | False | +| `honda.civic.gen7.us` | `fuel.level` | `UNKNOWN` | UNKNOWN | False | +| `honda.civic.gen7.us` | `transmission.gear` | `UNKNOWN` | UNKNOWN | False | +| `honda.civic.gen7.us` | `lighting.left_indicator` | `UNKNOWN` | UNKNOWN | False | +| `honda.civic.gen7.us` | `lighting.right_indicator` | `UNKNOWN` | UNKNOWN | False | +| `honda.civic.gen7.us` | `vehicle.ignition_state` | `UNKNOWN` | UNKNOWN | False | | `honda.civic.gen8.us` | `powertrain.engine_rpm` | `UNKNOWN` | UNKNOWN | False | | `honda.civic.gen8.us` | `vehicle.speed` | `UNKNOWN` | UNKNOWN | False | | `honda.civic.gen8.us` | `powertrain.coolant_temperature` | `UNKNOWN` | UNKNOWN | False | diff --git a/docs/generated/vehicles.md b/docs/generated/vehicles.md index f630b3d..2c48551 100644 --- a/docs/generated/vehicles.md +++ b/docs/generated/vehicles.md @@ -5,16 +5,20 @@ _Auto-generated from protocol YAML._ | Platform | Manufacturer | Model | Gen | Years | Roles | Validation | |----------|--------------|-------|-----|-------|-------|------------| | `honda.accord.gen10.us` | honda | accord | 10 | 2018-2022 | target | incomplete | +| `honda.accord.gen11.us` | honda | accord | 11 | 2023-2026 | target | incomplete | | `honda.civic.gen10.us` | honda | civic | 10 | 2016-2021 | target,source | incomplete | | `honda.civic.gen11.us` | honda | civic | 11 | 2022-2026 | target | incomplete | +| `honda.civic.gen7.us` | honda | civic | 7 | 2001-2005 | source | incomplete | | `honda.civic.gen8.us` | honda | civic | 8 | 2006-2011 | source | incomplete | | `honda.civic.gen9.us` | honda | civic | 9 | 2012-2015 | source | incomplete | | `honda.crv.gen5.us` | honda | crv | 5 | 2017-2022 | target | incomplete | +| `honda.crv.gen6.us` | honda | crv | 6 | 2023-2026 | target | incomplete | | `honda.element.gen1.us` | honda | element | 1 | 2003-2011 | source | incomplete | | `honda.fit.gen3.us` | honda | fit | 3 | 2015-2020 | source | incomplete | | `honda.hrv.gen2.us` | honda | hrv | 2 | 2023-2026 | source | incomplete | | `honda.insight.gen3.us` | honda | insight | 3 | 2019-2022 | source,target | incomplete | | `honda.odyssey.gen5.us` | honda | odyssey | 5 | 2018-2025 | source | incomplete | | `honda.pilot.gen3.us` | honda | pilot | 3 | 2016-2022 | source | incomplete | +| `honda.pilot.gen4.us` | honda | pilot | 4 | 2023-2026 | target | incomplete | | `honda.prelude.gen5.us` | honda | prelude | 5 | 1997-2001 | source | incomplete | | `honda.ridgeline.gen2.us` | honda | ridgeline | 2 | 2017-2025 | source | incomplete | diff --git a/opendashcan/adaptation/compiler.py b/opendashcan/adaptation/compiler.py index 3fd845a..d8d07fa 100644 --- a/opendashcan/adaptation/compiler.py +++ b/opendashcan/adaptation/compiler.py @@ -182,16 +182,14 @@ def build_adapter( # Phase 4: per-function SOURCE/TARGET/TRANSLATION/TIMING/INTEGRITY/CLUSTER RX/VALIDATION try: - from opendashcan.cluster.gaps import build_gap_report from opendashcan.cluster.environment_loader import CLUSTER_ALIASES + from opendashcan.cluster.gaps import build_gap_report gap_key = CLUSTER_ALIASES.get(target) or CLUSTER_ALIASES.get(adaptation.cluster_id) if gap_key: gap = build_gap_report( gap_key, - source_confidence_map={ - r.signal: r.source_confidence for r in adaptation.rows - }, + source_confidence_map={r.signal: r.source_confidence for r in adaptation.rows}, ) functions = [] for row in gap["rows"]: @@ -202,9 +200,7 @@ def build_adapter( "signal": row["signal"], "SOURCE": axes["A"], "TARGET": axes["B"], - "TRANSLATION": ( - plan_row.translation if plan_row else "UNKNOWN" - ), + "TRANSLATION": (plan_row.translation if plan_row else "UNKNOWN"), "PRODUCER": axes["C"], "BUS": axes["D"], "CLUSTER_RX": axes["E"], diff --git a/opendashcan/adaptation/planner.py b/opendashcan/adaptation/planner.py index 70c9ab6..a550bfd 100644 --- a/opendashcan/adaptation/planner.py +++ b/opendashcan/adaptation/planner.py @@ -76,8 +76,7 @@ def format_text(self) -> str: lines += ["", "TARGET CLUSTER"] if tgt: lines.append( - f" {tgt.cluster_id} display={tgt.display_type} " - f"status={tgt.compatibility_status}" + f" {tgt.cluster_id} display={tgt.display_type} status={tgt.compatibility_status}" ) else: lines.append(f" {self.cluster_id} (no cluster.yaml)") diff --git a/opendashcan/adaptation/scheduler.py b/opendashcan/adaptation/scheduler.py index 546e816..c98bd69 100644 --- a/opendashcan/adaptation/scheduler.py +++ b/opendashcan/adaptation/scheduler.py @@ -64,9 +64,7 @@ def schedule_from_package(pkg: PlatformPackage) -> SchedulePlan: return SchedulePlan(platform_id=pkg.platform_id, messages=msgs) -def schedule_from_specs( - platform_id: str, specs: list[PeriodicFrameSpec] -) -> SchedulePlan: +def schedule_from_specs(platform_id: str, specs: list[PeriodicFrameSpec]) -> SchedulePlan: msgs = [ ScheduledMessage( arbitration_id=hex(s.arbitration_id), diff --git a/opendashcan/analysis/conflicts_engine.py b/opendashcan/analysis/conflicts_engine.py index 49085c0..badf44a 100644 --- a/opendashcan/analysis/conflicts_engine.py +++ b/opendashcan/analysis/conflicts_engine.py @@ -96,9 +96,7 @@ def format_conflicts_text(data: dict[str, Any] | None = None) -> str: lines += ["", "## Lineage encoding"] for c in data["lineage_encoding_conflicts"][:50]: aid = c.get("arbitration_id") or c.get("name") - lines.append( - f"- {aid}: {c.get('detail')} vehicles={c.get('vehicles')}" - ) + lines.append(f"- {aid}: {c.get('detail')} vehicles={c.get('vehicles')}") lines.append("") return "\n".join(lines) diff --git a/opendashcan/analysis/dbc_inventory.py b/opendashcan/analysis/dbc_inventory.py index beffbc7..8aeb413 100644 --- a/opendashcan/analysis/dbc_inventory.py +++ b/opendashcan/analysis/dbc_inventory.py @@ -67,7 +67,7 @@ def build_dbc_inventory(*, out_dir: Path | None = None) -> dict[str, Any]: bus=entry.get("bus", "vehicle_can"), ) platform_hint = entry.get("platform") or entry.get("vehicle") - vehicle_summary = { + vehicle_summary: dict[str, Any] = { "file": entry["file"], "vehicle_id": result.vehicle_id, "bus": entry.get("bus", "vehicle_can"), diff --git a/opendashcan/analysis/lineage.py b/opendashcan/analysis/lineage.py index c00b719..0bbae3f 100644 --- a/opendashcan/analysis/lineage.py +++ b/opendashcan/analysis/lineage.py @@ -95,12 +95,7 @@ def compare_encodings(a: dict[str, Any], b: dict[str, Any]) -> dict[str, Any]: } ) identical = ( - same_id - and same_dlc - and same_name - and not only_a - and not only_b - and not encoding_mismatches + same_id and same_dlc and same_name and not only_a and not only_b and not encoding_mismatches ) return { "identical_encoding": identical, @@ -169,7 +164,7 @@ def build_lineage(*, messages: list[dict[str, Any]] | None = None) -> dict[str, # Name collisions across different IDs name_collisions = [] for name, group in sorted(by_name.items()): - ids = sorted({g.get("id_dec") for g in group if g.get("id_dec") is not None}) + ids = sorted(int(i) for i in {g.get("id_dec") for g in group} if isinstance(i, int)) if len(ids) > 1: name_collisions.append( { @@ -195,9 +190,7 @@ def build_lineage(*, messages: list[dict[str, Any]] | None = None) -> dict[str, for g in id_groups if g["identical_across_all"] and g["vehicle_count"] >= 2 ], - "divergent_ids": [ - g["arbitration_id"] for g in id_groups if not g["identical_across_all"] - ], + "divergent_ids": [g["arbitration_id"] for g in id_groups if not g["identical_across_all"]], } @@ -246,8 +239,9 @@ def write_lineage_artifacts( f"vehicles={g['vehicle_count']} variants={g['encoding_variant_count']}" ) for d in g["encoding_differences"][:3]: + n_mismatch = len(d.get("encoding_mismatches") or []) lines.append( - f" - vs `{d.get('vehicle_id')}`: mismatches={len(d.get('encoding_mismatches') or [])} " + f" - vs `{d.get('vehicle_id')}`: mismatches={n_mismatch} " f"only_a={d.get('signals_only_in_a')} only_b={d.get('signals_only_in_b')}" ) lines += ["", "## Name collisions (different IDs)", ""] diff --git a/opendashcan/analysis/network_graph.py b/opendashcan/analysis/network_graph.py index 85cdf13..a2c1d70 100644 --- a/opendashcan/analysis/network_graph.py +++ b/opendashcan/analysis/network_graph.py @@ -7,8 +7,8 @@ from pathlib import Path from typing import Any -from opendashcan.cluster.classify import classify_from_dbc_message from opendashcan.analysis.lineage import _load_indexed_messages +from opendashcan.cluster.classify import classify_from_dbc_message REPO_ROOT = Path(__file__).resolve().parents[2] @@ -49,9 +49,7 @@ def build_network_graph( msgs = messages if messages is not None else _load_indexed_messages() if cluster_platform_filter: msgs = [ - m - for m in msgs - if cluster_platform_filter in str(m.get("vehicle_id") or "") + m for m in msgs if cluster_platform_filter in str(m.get("vehicle_id") or "") ] or msgs # fall back to all if filter empty nodes: dict[str, dict[str, Any]] = {} diff --git a/opendashcan/analysis/similarity.py b/opendashcan/analysis/similarity.py index 04c2c29..9ecde1e 100644 --- a/opendashcan/analysis/similarity.py +++ b/opendashcan/analysis/similarity.py @@ -3,11 +3,11 @@ from __future__ import annotations from dataclasses import dataclass +from pathlib import Path from typing import Any from opendashcan.analysis.lineage import _load_indexed_messages, compare_encodings from opendashcan.dbc import taxonomy_implementations_from_imports -from pathlib import Path @dataclass diff --git a/opendashcan/cli.py b/opendashcan/cli.py index d00819a..513030d 100644 --- a/opendashcan/cli.py +++ b/opendashcan/cli.py @@ -110,7 +110,7 @@ def cmd_listen(args: argparse.Namespace) -> int: session = ListenSession(decoder=decoder) print_every = max(1, int(args.print_every)) try: - for frame in frame_iter: # type: ignore[attr-defined] + for frame in frame_iter: session.ingest(frame) if args.raw: print( @@ -142,7 +142,6 @@ def _print_listen_snapshot(session: object) -> None: print(f" {name}={val} confidence={conf} t={ts}") - def cmd_vehicles(_: argparse.Namespace) -> int: seen: set[str] = set() for vid, info in VEHICLES.items(): @@ -209,7 +208,7 @@ def _print_decoded_state(state: object) -> None: known = state.known_signals() # type: ignore[attr-defined] print(f"known_signals={known}") for name in known: - sig = state.get(name) if hasattr(state, "get") else getattr(state, name, None) # type: ignore[union-attr] + sig = state.get(name) if hasattr(state, "get") else getattr(state, name, None) if sig is None: continue val = getattr(sig, "value", sig) @@ -403,9 +402,7 @@ def cmd_registry(args: argparse.Namespace) -> int: print(f"no implementations for {name}") return 0 for pkg, enc in hits: - print( - f"{pkg.platform_id}\t{enc.signal}\t{enc.arbitration_id}\t{enc.confidence.value}" - ) + print(f"{pkg.platform_id}\t{enc.signal}\t{enc.arbitration_id}\t{enc.confidence.value}") return 0 packages = [reg.get(target)] if target else reg.list_platforms() @@ -472,8 +469,10 @@ def cmd_dbc(args: argparse.Namespace) -> int: out = args.output or Path("dist") / "dbc_import.json" out.parent.mkdir(parents=True, exist_ok=True) out.write_text(json.dumps(result.to_dict(), indent=2) + "\n", encoding="utf-8") - print(f"wrote {out} ({len(result.messages)} messages, " - f"{sum(len(m.signals) for m in result.messages)} signals)") + print( + f"wrote {out} ({len(result.messages)} messages, " + f"{sum(len(m.signals) for m in result.messages)} signals)" + ) if result.conflicts: report = Path("DBC_CONFLICT_REPORT.md") write_conflict_report(result, report) @@ -496,8 +495,9 @@ def cmd_dbc(args: argparse.Namespace) -> int: if signal: rows = cross.get(signal, []) if not rows: - print(f"{signal}: not found in imported DBC indexes " - f"(may be absent from public DBC)") + print( + f"{signal}: not found in imported DBC indexes (may be absent from public DBC)" + ) return 1 for row in rows: print( @@ -547,7 +547,10 @@ def cmd_cluster(args: argparse.Namespace) -> int: return 0 if not args.cluster_name: - print("cluster command requires a cluster name (civic10|civic11|accord10|crv5)", file=sys.stderr) + print( + "cluster command requires a cluster name (civic10|civic11|accord10|crv5)", + file=sys.stderr, + ) return 1 try: env = load_cluster_env(args.cluster_name) @@ -927,7 +930,9 @@ def build_parser() -> argparse.ArgumentParser: cp.set_defaults(func=cmd_cluster) sp = sub.add_parser("correlate", help="Cross-platform signal/message similarity leads") - sp.add_argument("--target", required=True, help="Target vehicle id substring e.g. honda.civic.gen10") + sp.add_argument( + "--target", required=True, help="Target vehicle id substring e.g. honda.civic.gen10" + ) sp.add_argument("--signal", help="Taxonomy signal e.g. powertrain.engine_rpm") sp.add_argument("--id", help="CAN ID (hex or int) for message-level correlate") sp.set_defaults(func=cmd_correlate) diff --git a/opendashcan/cluster/__init__.py b/opendashcan/cluster/__init__.py index 0c5d7e0..48b815b 100644 --- a/opendashcan/cluster/__init__.py +++ b/opendashcan/cluster/__init__.py @@ -1,14 +1,15 @@ -"""Isolated target cluster environments and gap analysis (Phase 4). - -Import Phase 4 modules when present; stubs keep CLI/GUI importable if a -parallel agent is still landing files. -""" +"""Isolated target cluster environments and gap analysis (Phase 4).""" from __future__ import annotations -from typing import Any - from opendashcan.cluster.classify import ClusterCandidateClass, classify_message +from opendashcan.cluster.environment_loader import ( + CLUSTER_ALIASES, + ClusterEnvPackage, + list_cluster_envs, + load_cluster_env, +) +from opendashcan.cluster.gaps import build_gap_report, format_gaps_text __all__ = [ "CLUSTER_ALIASES", @@ -20,33 +21,3 @@ "list_cluster_envs", "load_cluster_env", ] - -try: - from opendashcan.cluster.environment_loader import ( - CLUSTER_ALIASES, - ClusterEnvPackage, - list_cluster_envs, - load_cluster_env, - ) -except ImportError: - CLUSTER_ALIASES: dict[str, str] = {} - ClusterEnvPackage = Any # type: ignore[misc, assignment] - - def list_cluster_envs(**_kwargs: Any) -> list[Any]: - return [] - - def load_cluster_env(cluster_id: str, **_kwargs: Any) -> Any: - raise NotImplementedError( - "opendashcan.cluster.environment_loader is not available yet" - ) - - -try: - from opendashcan.cluster.gaps import build_gap_report, format_gaps_text -except ImportError: - - def build_gap_report(cluster_name: str, **_kwargs: Any) -> dict[str, Any]: - raise NotImplementedError("opendashcan.cluster.gaps is not available yet") - - def format_gaps_text(report: dict[str, Any]) -> str: - return str(report) diff --git a/opendashcan/cluster/classify.py b/opendashcan/cluster/classify.py index 78e9d0f..bb8f188 100644 --- a/opendashcan/cluster/classify.py +++ b/opendashcan/cluster/classify.py @@ -128,11 +128,7 @@ def classify_message( def classify_from_dbc_message(msg: dict[str, Any], *, platform_hint: str | None = None) -> str: - taxes = [ - s.get("taxonomy") - for s in (msg.get("signals") or []) - if s.get("taxonomy") - ] + taxes = [s.get("taxonomy") for s in (msg.get("signals") or []) if s.get("taxonomy")] aid = msg.get("id_dec") if aid is None and msg.get("id_hex"): try: diff --git a/opendashcan/cluster/environment_loader.py b/opendashcan/cluster/environment_loader.py index b6b7450..7dd9e2e 100644 --- a/opendashcan/cluster/environment_loader.py +++ b/opendashcan/cluster/environment_loader.py @@ -90,4 +90,6 @@ def list_cluster_envs(*, root: Path | None = None) -> list[str]: base = root or CLUSTERS_ROOT if not base.is_dir(): return [] - return sorted(p.name for p in base.iterdir() if p.is_dir() and (p / "environment.yaml").is_file()) + return sorted( + p.name for p in base.iterdir() if p.is_dir() and (p / "environment.yaml").is_file() + ) diff --git a/opendashcan/cluster/gaps.py b/opendashcan/cluster/gaps.py index a08705f..a8a40b0 100644 --- a/opendashcan/cluster/gaps.py +++ b/opendashcan/cluster/gaps.py @@ -3,12 +3,12 @@ from __future__ import annotations from dataclasses import dataclass, field +from pathlib import Path from typing import Any from opendashcan.cluster.environment_loader import ClusterEnvPackage, load_cluster_env from opendashcan.core.donor_knowledge import CLUSTER_RELEVANT_TAXONOMY, DonorKnowledge from opendashcan.dbc import taxonomy_implementations_from_imports -from pathlib import Path AXES = ("A", "B", "C", "D", "E", "F", "G", "H") @@ -127,8 +127,13 @@ def resolve_signal_axes( f"scale={donor.get('scale')} ({DonorKnowledge.VEHICLE_PROTOCOL_DOCUMENTED.value})" ), ) - elif signal in ("fuel.level", "powertrain.coolant_temperature", - "safety.srs_warning", "safety.check_engine", "brakes.abs_warning"): + elif signal in ( + "fuel.level", + "powertrain.coolant_temperature", + "safety.srs_warning", + "safety.check_engine", + "brakes.abs_warning", + ): b = _axis( "B", "ABSENT", @@ -144,7 +149,6 @@ def resolve_signal_axes( ) # C — producer ECU - sender = req.get("producer") or (donor and "see_dbc_sender") or "UNKNOWN" if req.get("producer") and req.get("producer") != "UNKNOWN": c = _axis("C", "DOCUMENTED", str(req["producer"])) elif donor: @@ -160,7 +164,9 @@ def resolve_signal_axes( # D — bus bus = req.get("bus") or env.environment.get("primary_bus") or "UNKNOWN" if bus and bus != "UNKNOWN": - d = _axis("D", "DOCUMENTED" if req.get("bus_confidence") != "UNKNOWN" else "INFERRED", str(bus)) + d = _axis( + "D", "DOCUMENTED" if req.get("bus_confidence") != "UNKNOWN" else "INFERRED", str(bus) + ) else: d = _axis("D", "UNKNOWN", "Bus UNKNOWN", "Confirm cluster tap bus (F-CAN vs B-CAN)") @@ -206,7 +212,11 @@ def resolve_signal_axes( ) # G — integrity - csum = req.get("checksum") or req.get("integrity", {}).get("checksum") if isinstance(req.get("integrity"), dict) else req.get("checksum") + csum = ( + req.get("checksum") or req.get("integrity", {}).get("checksum") + if isinstance(req.get("integrity"), dict) + else req.get("checksum") + ) if isinstance(req.get("integrity"), dict): csum = req["integrity"].get("checksum_type") or req["integrity"].get("checksum") ctr = req["integrity"].get("counter_type") or req["integrity"].get("counter") @@ -235,11 +245,14 @@ def resolve_signal_axes( "Reproduce under controlled OpenDashCAN bench with logged frames", ) else: - h = _axis("H", "UNKNOWN", "Not demonstrated in-repo", "Bench or vehicle test with artifacts") + h = _axis( + "H", "UNKNOWN", "Not demonstrated in-repo", "Bench or vehicle test with artifacts" + ) axes = {"A": a, "B": b, "C": c, "D": d, "E": e, "F": f, "G": g, "H": h} - # Overall: worst of E/B/H for cluster adaptation; source A UNKNOWN → BLOCKED_SOURCE not overall fail on target + # Overall: worst of E/B/H for cluster adaptation; + # source A UNKNOWN → BLOCKED_SOURCE, not overall fail on target statuses = [axes[x].status for x in ("B", "E", "F", "G", "H")] if "ABSENT" in statuses: overall = "ABSENT_FROM_PUBLIC_DBC" @@ -251,7 +264,9 @@ def resolve_signal_axes( overall = "UNKNOWN" if a.status == "UNKNOWN" and b.status == "DOCUMENTED": - notes = (notes + " | ").lstrip(" |") + "TRANSLATION=BLOCKED_SOURCE (target donor encoding documented)" + notes = (notes + " | ").lstrip( + " |" + ) + "TRANSLATION=BLOCKED_SOURCE (target donor encoding documented)" return SignalGapRow( signal=signal, @@ -262,7 +277,9 @@ def resolve_signal_axes( ) -def build_gap_report(cluster_name: str, *, source_confidence_map: dict[str, str] | None = None) -> dict[str, Any]: +def build_gap_report( + cluster_name: str, *, source_confidence_map: dict[str, str] | None = None +) -> dict[str, Any]: env = load_cluster_env(cluster_name) src_map = source_confidence_map or {} rows: list[SignalGapRow] = [] @@ -287,15 +304,19 @@ def build_gap_report(cluster_name: str, *, source_confidence_map: dict[str, str] for ax, st in row.axes.items(): axis_counts[ax][st.status] = axis_counts[ax].get(st.status, 0) + 1 - taxonomy_covered = sum( - 1 for r in rows if r.axes["B"].status == "DOCUMENTED" - ) + taxonomy_covered = sum(1 for r in rows if r.axes["B"].status == "DOCUMENTED") taxonomy_absent = sum(1 for r in rows if r.axes["B"].status == "ABSENT") rx_confirmed = sum(1 for r in rows if r.axes["E"].status == "DOCUMENTED") - rx_likely = sum(1 for r in rows if "LIKELY" in r.axes["E"].detail or r.axes["E"].status == "COMMUNITY_RESEARCH") + rx_likely = sum( + 1 + for r in rows + if "LIKELY" in r.axes["E"].detail or r.axes["E"].status == "COMMUNITY_RESEARCH" + ) timing_known = sum(1 for r in rows if r.axes["F"].status not in ("UNKNOWN",)) integrity_known = sum(1 for r in rows if r.axes["G"].status not in ("UNKNOWN",)) - demonstrated = sum(1 for r in rows if r.axes["H"].status in ("DOCUMENTED", "COMMUNITY_RESEARCH")) + demonstrated = sum( + 1 for r in rows if r.axes["H"].status in ("DOCUMENTED", "COMMUNITY_RESEARCH") + ) n = len(rows) or 1 return { @@ -318,9 +339,7 @@ def build_gap_report(cluster_name: str, *, source_confidence_map: dict[str, str] "integrity_algorithm_documented": integrity_known, "integrity_unknown": len(rows) - integrity_known, "demonstrated_any": demonstrated, - "demonstrated_bench": sum( - 1 for r in rows if "BENCH" in r.axes["H"].detail.upper() - ), + "demonstrated_bench": sum(1 for r in rows if "BENCH" in r.axes["H"].detail.upper()), "fractions": { "donor_encoding_documented": round(taxonomy_covered / n, 3), "cluster_rx_confirmed": round(rx_confirmed / n, 3), @@ -372,12 +391,8 @@ def _ascii(s: str) -> str: ] for row in report["rows"]: axes = row["axes"] - befgh = "/".join( - axes[x]["status"][:3] for x in ("B", "E", "F", "G", "H") - ) - lines.append( - f"{row['signal']:<36} {row['priority']:<10} {row['overall']:<24} {befgh}" - ) + befgh = "/".join(axes[x]["status"][:3] for x in ("B", "E", "F", "G", "H")) + lines.append(f"{row['signal']:<36} {row['priority']:<10} {row['overall']:<24} {befgh}") if row.get("notes"): lines.append(f" notes: {_ascii(row['notes'][:120])}") for letter in ("B", "E", "F", "G", "H"): diff --git a/opendashcan/cluster/timing.py b/opendashcan/cluster/timing.py index fa3500f..076337f 100644 --- a/opendashcan/cluster/timing.py +++ b/opendashcan/cluster/timing.py @@ -45,12 +45,8 @@ def build_timing_database() -> dict[str, Any]: return { "warning": "period_ms UNKNOWN unless independently established. DBC ≠ cluster timing.", "entries": entries, - "unknown_count": sum( - 1 for e in entries if e.get("period_ms") in (None, "UNKNOWN") - ), - "known_count": sum( - 1 for e in entries if e.get("period_ms") not in (None, "UNKNOWN") - ), + "unknown_count": sum(1 for e in entries if e.get("period_ms") in (None, "UNKNOWN")), + "known_count": sum(1 for e in entries if e.get("period_ms") not in (None, "UNKNOWN")), } diff --git a/opendashcan/cluster/trace.py b/opendashcan/cluster/trace.py index 309b66a..a3893bb 100644 --- a/opendashcan/cluster/trace.py +++ b/opendashcan/cluster/trace.py @@ -27,9 +27,11 @@ def _state_from_scenario_step(step: Any) -> VehicleState: if getattr(step, "gear", None) is not None: g = step.gear if isinstance(g, GearPosition): - state.gear = SignalValue(g, confidence=Confidence.INFERRED, validity=Validity.VALID) + state.gear_position = SignalValue( + g.value, confidence=Confidence.INFERRED, validity=Validity.VALID + ) else: - state.gear = SignalValue( + state.gear_position = SignalValue( str(g), confidence=Confidence.INFERRED, validity=Validity.VALID ) return state @@ -104,9 +106,7 @@ def write_trace( manifest_path: Path | None = None, ) -> dict[str, Path]: result = generate_trace(cluster=cluster, scenario=scenario) - out = output or ( - REPO_ROOT / "dist" / "traces" / f"{cluster}_{scenario}.candump.log" - ) + out = output or (REPO_ROOT / "dist" / "traces" / f"{cluster}_{scenario}.candump.log") out.parent.mkdir(parents=True, exist_ok=True) out.write_text( "\n".join(result["candump_lines"]) + ("\n" if result["candump_lines"] else ""), diff --git a/opendashcan/cluster/virtual_donor.py b/opendashcan/cluster/virtual_donor.py index 29b888d..04d157b 100644 --- a/opendashcan/cluster/virtual_donor.py +++ b/opendashcan/cluster/virtual_donor.py @@ -7,9 +7,9 @@ from opendashcan.adaptation.environment import ClusterEnvironment from opendashcan.adaptation.gating import EncodeMode +from opendashcan.cluster.environment_loader import load_cluster_env from opendashcan.core.frame import CANFrame from opendashcan.core.state import VehicleState -from opendashcan.cluster.environment_loader import load_cluster_env from opendashcan.registry import get_encoder, get_registry @@ -51,7 +51,7 @@ def __post_init__(self) -> None: self._encoder = get_encoder(eid) # Apply mode if encoder supports it if hasattr(self._encoder, "encode_mode"): - self._encoder.encode_mode = self.encode_mode # type: ignore[attr-defined] + self._encoder.encode_mode = self.encode_mode except KeyError: self.omissions.append(f"no encoder for {eid}") @@ -68,7 +68,7 @@ def emit(self, state: VehicleState, *, timestamp: float = 0.0) -> list[CANFrame] self.omissions.append("encoder unavailable") return [] if hasattr(self._encoder, "encode_mode"): - self._encoder.encode_mode = self.encode_mode # type: ignore[attr-defined] + self._encoder.encode_mode = self.encode_mode frames = self._encoder.encode(state, timestamp=timestamp) self.counter += 1 return list(frames) @@ -76,7 +76,9 @@ def emit(self, state: VehicleState, *, timestamp: float = 0.0) -> list[CANFrame] def summary(self) -> dict[str, Any]: return { "cluster_key": self.cluster_key, - "encode_mode": self.encode_mode.value if hasattr(self.encode_mode, "value") else str(self.encode_mode), + "encode_mode": self.encode_mode.value + if hasattr(self.encode_mode, "value") + else str(self.encode_mode), "environment": self.environment.gap_report() if self.environment else None, "omissions": list(self.omissions), "label": "SOFTWARE VALIDATION ONLY — virtual donor, no hardware", diff --git a/opendashcan/core/confidence.py b/opendashcan/core/confidence.py index 4e218e5..25ea1cf 100644 --- a/opendashcan/core/confidence.py +++ b/opendashcan/core/confidence.py @@ -41,6 +41,7 @@ class SourceType(str, Enum): COMMERCIAL_DATASHEET = "COMMERCIAL_DATASHEET" PHYSICAL_TEST = "PHYSICAL_TEST" INFERENCE = "INFERENCE" + RESEARCH_NOTE = "RESEARCH_NOTE" # Normalize Phase-1 labels onto the Phase-2 ladder for policy checks. @@ -59,6 +60,7 @@ class SourceType(str, Enum): SourceType.DBC, SourceType.ACADEMIC_RESEARCH, SourceType.COMMERCIAL_DATASHEET, + SourceType.RESEARCH_NOTE, } ), Confidence.COMMUNITY_REPORTED: frozenset({SourceType.COMMUNITY_RESEARCH}), diff --git a/opendashcan/dbc/__init__.py b/opendashcan/dbc/__init__.py index fa49aff..a51bd11 100644 --- a/opendashcan/dbc/__init__.py +++ b/opendashcan/dbc/__init__.py @@ -265,10 +265,10 @@ def parse_dbc(text: str) -> list[DbcMessage]: mid = int(m_val.group(1)) sig_name = m_val.group(2) rest = m_val.group(3).rstrip(";").strip() - msg = by_id.get(mid) - if msg is None: + msg_val = by_id.get(mid) + if msg_val is None: continue - sig = next((s for s in msg.signals if s.name == sig_name), None) + sig = next((s for s in msg_val.signals if s.name == sig_name), None) if sig is None: continue # pairs: value "label" @@ -278,17 +278,17 @@ def parse_dbc(text: str) -> list[DbcMessage]: m_cm_bo = CM_BO_RE.match(line) if m_cm_bo: - msg = by_id.get(int(m_cm_bo.group(1))) - if msg is not None: - msg.comment = m_cm_bo.group(2) + msg_cm = by_id.get(int(m_cm_bo.group(1))) + if msg_cm is not None: + msg_cm.comment = m_cm_bo.group(2) continue m_cm_sg = CM_SG_RE.match(line) if m_cm_sg: - msg = by_id.get(int(m_cm_sg.group(1))) - if msg is None: + msg_sg = by_id.get(int(m_cm_sg.group(1))) + if msg_sg is None: continue - sig = next((s for s in msg.signals if s.name == m_cm_sg.group(2)), None) + sig = next((s for s in msg_sg.signals if s.name == m_cm_sg.group(2)), None) if sig is not None: sig.comment = m_cm_sg.group(3) continue @@ -326,7 +326,9 @@ def _load_provenance(dbc_path: Path) -> dict[str, Any]: } -def _build_taxonomy_index(messages: list[DbcMessage]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: +def _build_taxonomy_index( + messages: list[DbcMessage], +) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: mapped: list[dict[str, Any]] = [] unmapped: list[dict[str, Any]] = [] seen_tax: set[str] = set() @@ -351,7 +353,9 @@ def _build_taxonomy_index(messages: list[DbcMessage]) -> tuple[list[dict[str, An if tax: # Prefer ENGINE_DATA RPM over POWERTRAIN_DATA duplicate for index if tax in seen_tax and sig.name == "ENGINE_RPM" and msg.name != "ENGINE_DATA": - unmapped.append({**entry, "taxonomy": None, "note": "duplicate_rpm_on_powertrain"}) + unmapped.append( + {**entry, "taxonomy": None, "note": "duplicate_rpm_on_powertrain"} + ) continue if tax not in seen_tax or (sig.name == "ENGINE_RPM" and msg.name == "ENGINE_DATA"): if tax in seen_tax: @@ -496,9 +500,7 @@ def write_import_artifacts(result: DbcImportResult, output_dir: Path) -> dict[st "taxonomy_mapped": result.taxonomy_mapped, "absent_from_public_dbc": result.absent_signals, "notes": result.notes, - "warning": ( - "Vehicle-bus documentation only — not CLUSTER_RX_CONFIRMED." - ), + "warning": ("Vehicle-bus documentation only — not CLUSTER_RX_CONFIRMED."), } yaml_path.write_text( yaml.safe_dump(summary, sort_keys=False, allow_unicode=True), diff --git a/opendashcan/gui/live_panel.py b/opendashcan/gui/live_panel.py index c823446..7280177 100644 --- a/opendashcan/gui/live_panel.py +++ b/opendashcan/gui/live_panel.py @@ -2,6 +2,7 @@ from __future__ import annotations +import contextlib from pathlib import Path from PySide6.QtCore import QObject, Qt, QThread, QTimer, Signal, Slot @@ -78,10 +79,8 @@ def run(self) -> None: self.error.emit(str(exc)) finally: if self._bus is not None: - try: + with contextlib.suppress(Exception): self._bus.shutdown() - except Exception: # noqa: BLE001 - pass self.finished.emit() def request_stop(self) -> None: @@ -191,9 +190,7 @@ def __init__(self, parent: QWidget | None = None) -> None: right_l = QVBoxLayout(right) right_l.addWidget(QLabel("Decoded signals (documented layouts only)")) self.sig_table = QTableWidget(0, 4) - self.sig_table.setHorizontalHeaderLabels( - ["Signal", "Value", "Confidence", "Last update"] - ) + self.sig_table.setHorizontalHeaderLabels(["Signal", "Value", "Confidence", "Last update"]) self.sig_table.setAlternatingRowColors(True) self.sig_table.horizontalHeader().setStretchLastSection(True) self.sig_table.setFont(QFont("Consolas", 10)) diff --git a/opendashcan/gui/main_window.py b/opendashcan/gui/main_window.py index 10c428b..791c322 100644 --- a/opendashcan/gui/main_window.py +++ b/opendashcan/gui/main_window.py @@ -149,9 +149,7 @@ def __init__(self) -> None: self._listen_status = QLabel("Idle — LISTEN_ONLY") self._listen_status.setObjectName("StatusIdle") self.statusBar().addWidget(self._listen_status, stretch=1) - self.statusBar().showMessage( - "LISTEN_ONLY — no CAN TX · decode documented layouts only" - ) + self.statusBar().showMessage("LISTEN_ONLY — no CAN TX · decode documented layouts only") deps = services.gui_missing_deps_message() if deps: @@ -373,9 +371,7 @@ def _populate_browser(self) -> None: self.browser.addTopLevelItem(clusters) for row in services.list_browser_rows(): if row.kind == "platform": - item = QTreeWidgetItem( - [row.platform_id, row.roles, row.years, row.display] - ) + item = QTreeWidgetItem([row.platform_id, row.roles, row.years, row.display]) item.setData( 0, Qt.ItemDataRole.UserRole, @@ -439,9 +435,7 @@ def _on_browser_select(self) -> None: "BUSES", ] for b in detail["buses"]: - lines.append( - f" {b['name']}: bitrate={b['bitrate']} confidence={b['confidence']}" - ) + lines.append(f" {b['name']}: bitrate={b['bitrate']} confidence={b['confidence']}") if "cluster" in detail: c = detail["cluster"] lines += [ @@ -490,17 +484,11 @@ def _run_lookup(self) -> None: dbc = services.lookup_dbc_index(q) self._fill_table( self.signal_table, - [ - [s.platform_id, s.signal, s.arbitration_id, s.confidence, s.notes] - for s in sigs - ], + [[s.platform_id, s.signal, s.arbitration_id, s.confidence, s.notes] for s in sigs], ) self._fill_table( self.message_table, - [ - [m.platform_id, m.name, m.arbitration_id, m.confidence, m.period_ms] - for m in msgs - ], + [[m.platform_id, m.name, m.arbitration_id, m.confidence, m.period_ms] for m in msgs], ) self._fill_table( self.dbc_table, diff --git a/opendashcan/gui/services.py b/opendashcan/gui/services.py index 230cf0d..7655417 100644 --- a/opendashcan/gui/services.py +++ b/opendashcan/gui/services.py @@ -17,8 +17,7 @@ DEFAULT_LISTEN_VEHICLE = "honda.civic.gen10.us" EVIDENCE_SUBMISSION_URL = ( - "https://github.com/theworker02/OpenDashCAN/issues/new" - "?template=evidence_submission.yml" + "https://github.com/theworker02/OpenDashCAN/issues/new?template=evidence_submission.yml" ) EVIDENCE_GUIDE_REL = "docs/submitting-evidence.md" @@ -417,9 +416,7 @@ def project_mode_banner() -> str: ) -AFFILIATION_DISCLAIMER = ( - "Not affiliated with, endorsed by, or sponsored by Honda Motor Co., Ltd." -) +AFFILIATION_DISCLAIMER = "Not affiliated with, endorsed by, or sponsored by Honda Motor Co., Ltd." TRADEMARK_NOTICE = ( "Honda®, the Honda logo, and related marks are trademarks and/or " @@ -544,7 +541,7 @@ def about_text() -> str: "No claim of cluster control or physical compatibility.\n\n" "Get frames onto the PC: hardware/can_recorder_rpi/\n" "Wiring: docs/wiring/ · GUI Wiring tab\n" - "Install: pip install -e \".[gui,hw]\"\n" + 'Install: pip install -e ".[gui,hw]"\n' "Listen: opendashcan listen --virtual\n" f"Evidence: {EVIDENCE_SUBMISSION_URL}" ) diff --git a/opendashcan/gui/splash.py b/opendashcan/gui/splash.py index ad7b835..72e10a6 100644 --- a/opendashcan/gui/splash.py +++ b/opendashcan/gui/splash.py @@ -40,9 +40,7 @@ from opendashcan import __version__ # Affiliation + trademark / copyright notice (shown on splash + About) -DISCLAIMER = ( - "Not affiliated with, endorsed by, or sponsored by Honda Motor Co., Ltd." -) +DISCLAIMER = "Not affiliated with, endorsed by, or sponsored by Honda Motor Co., Ltd." TRADEMARK_NOTICE = ( "Honda®, the Honda logo, and related marks are trademarks and/or " @@ -215,9 +213,7 @@ def __init__( parent: QWidget | None = None, ) -> None: super().__init__(parent) - self.setWindowFlags( - Qt.WindowType.FramelessWindowHint | Qt.WindowType.SplashScreen - ) + self.setWindowFlags(Qt.WindowType.FramelessWindowHint | Qt.WindowType.SplashScreen) self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, False) self.setFixedSize(540, 420) self.setStyleSheet("background-color: #050607;") @@ -236,9 +232,7 @@ def __init__( brand = QLabel("HONDA") brand.setAlignment(Qt.AlignmentFlag.AlignCenter) brand.setFont(QFont("Segoe UI", 28, QFont.Weight.Bold)) - brand.setStyleSheet( - "color: #f5f7fa; letter-spacing: 10px; background: transparent;" - ) + brand.setStyleSheet("color: #f5f7fa; letter-spacing: 10px; background: transparent;") lay.addWidget(brand) cue = QLabel("IGNITION") diff --git a/opendashcan/gui/wiring_panel.py b/opendashcan/gui/wiring_panel.py index 456b091..afe1927 100644 --- a/opendashcan/gui/wiring_panel.py +++ b/opendashcan/gui/wiring_panel.py @@ -90,16 +90,12 @@ def __init__(self, parent: QWidget | None = None) -> None: right_l.addWidget(scroll, stretch=2) right_l.addWidget(self._section("What you may need to switch")) - hint = QLabel( - "Civic8→Civic10-style interfaces — confidence labels are authoritative." - ) + hint = QLabel("Civic8→Civic10-style interfaces — confidence labels are authoritative.") hint.setObjectName("Subtitle") hint.setWordWrap(True) right_l.addWidget(hint) self.check_table = QTableWidget(0, 4) - self.check_table.setHorizontalHeaderLabels( - ["Interface", "Meaning", "Notes", "Confidence"] - ) + self.check_table.setHorizontalHeaderLabels(["Interface", "Meaning", "Notes", "Confidence"]) self.check_table.setAlternatingRowColors(True) self.check_table.horizontalHeader().setStretchLastSection(True) right_l.addWidget(self.check_table, stretch=3) @@ -167,8 +163,7 @@ def _show_diagram(self, row: int) -> None: self.diagram_title.setText(item.text() if item else path.name) if not path.is_file(): self.diagram_label.setText( - f"Missing diagram:\n{path}\n\n" - "Run: python tools/generate_wiring_diagrams.py" + f"Missing diagram:\n{path}\n\nRun: python tools/generate_wiring_diagrams.py" ) self.diagram_label.setPixmap(QPixmap()) return diff --git a/opendashcan/honda/integrity/checksums.py b/opendashcan/honda/integrity/checksums.py index a6d5a67..7b526e4 100644 --- a/opendashcan/honda/integrity/checksums.py +++ b/opendashcan/honda/integrity/checksums.py @@ -74,9 +74,7 @@ def apply_nibble_checksum( return honda_set_checksum(address, data) -def verify_nibble_checksum( - address: int, data: bytes, *, platform_id: str | None = None -) -> bool: +def verify_nibble_checksum(address: int, data: bytes, *, platform_id: str | None = None) -> bool: _guard(platform_id) return honda_verify_checksum(address, data) diff --git a/opendashcan/honda/integrity/counters.py b/opendashcan/honda/integrity/counters.py index 4ee0e3f..c3b4150 100644 --- a/opendashcan/honda/integrity/counters.py +++ b/opendashcan/honda/integrity/counters.py @@ -46,13 +46,9 @@ class CounterSpec: ) -def apply_2bit_counter( - data: bytes, counter: int, *, platform_id: str | None = None -) -> bytes: +def apply_2bit_counter(data: bytes, counter: int, *, platform_id: str | None = None) -> bytes: if platform_id is not None and platform_id not in COUNTER_2BIT_PLATFORMS: - raise ValueError( - f"honda_2bit_v1 not documented for platform {platform_id!r}" - ) + raise ValueError(f"honda_2bit_v1 not documented for platform {platform_id!r}") return honda_set_counter(data, counter) diff --git a/opendashcan/honda/integrity/patterns.py b/opendashcan/honda/integrity/patterns.py index b5e6d31..f73db29 100644 --- a/opendashcan/honda/integrity/patterns.py +++ b/opendashcan/honda/integrity/patterns.py @@ -45,7 +45,9 @@ def integrity_pattern_groups() -> dict[str, Any]: }, { "pattern_id": "honda_2bit_v1", - "description": "2-bit rolling counter in high nibble of final byte (opendbc convention)", + "description": ( + "2-bit rolling counter in high nibble of final byte (opendbc convention)" + ), "confidence": "DOCUMENTED", "cluster_acceptance": "UNKNOWN", "test_vectors": [], diff --git a/opendashcan/hw/__init__.py b/opendashcan/hw/__init__.py index 581bf1c..5e95a77 100644 --- a/opendashcan/hw/__init__.py +++ b/opendashcan/hw/__init__.py @@ -15,7 +15,12 @@ ) from opendashcan.hw.session import IdRateRow, ListenSession from opendashcan.hw.socketcan_listen import listen_socketcan -from opendashcan.hw.tx_guard import TX_ENV_FLAG, assert_tx_permitted, refuse_send_message, tx_env_enabled +from opendashcan.hw.tx_guard import ( + TX_ENV_FLAG, + assert_tx_permitted, + refuse_send_message, + tx_env_enabled, +) __all__ = [ "DEFAULT_BITRATE", diff --git a/opendashcan/hw/listen.py b/opendashcan/hw/listen.py index 554043c..6a3d4e6 100644 --- a/opendashcan/hw/listen.py +++ b/opendashcan/hw/listen.py @@ -116,11 +116,10 @@ def open_listen_bus( bustype_l = bustype.lower().strip() if bustype_l not in SUPPORTED_BUSTYPES: raise HardwareUnavailableError( - f"Unsupported interface type {bustype!r}. " - f"Supported: {', '.join(SUPPORTED_BUSTYPES)}" + f"Unsupported interface type {bustype!r}. Supported: {', '.join(SUPPORTED_BUSTYPES)}" ) try: - import can # type: ignore[import-untyped] + import can except ImportError as exc: raise HardwareUnavailableError( "python-can is not installed. " @@ -148,9 +147,11 @@ def open_listen_bus( raise HardwareUnavailableError( f"Could not open {bustype_l}:{channel} - {exc}\n" "Hints:\n" - " * Linux SocketCAN: sudo ip link set can0 up type can bitrate 500000 listen-only on\n" + " * Linux SocketCAN: sudo ip link set can0 up type can " + "bitrate 500000 listen-only on\n" " * No adapter: opendashcan listen --virtual\n" - " * Offline file: opendashcan listen --capture captures/synthetic/idle_scenario.log\n" + " * Offline file: opendashcan listen --capture " + "captures/synthetic/idle_scenario.log\n" " * Pi recorder: see hardware/can_recorder_rpi/README.md" ) from exc diff --git a/opendashcan/hw/session.py b/opendashcan/hw/session.py index 6c482b5..d94d7db 100644 --- a/opendashcan/hw/session.py +++ b/opendashcan/hw/session.py @@ -74,10 +74,8 @@ def signal_rows(self) -> list[tuple[str, str, str, str]]: for name in self.state.known_signals(): sig = self.state.get(name) val = sig.value - if hasattr(val, "value"): # Enum - val_s = str(getattr(val, "value", val)) - else: - val_s = repr(val) + # Enum → .value; otherwise repr + val_s = str(getattr(val, "value", val)) if hasattr(val, "value") else repr(val) conf = sig.confidence.value if hasattr(sig.confidence, "value") else str(sig.confidence) ts = f"{sig.timestamp:.3f}" if sig.timestamp is not None else "-" rows.append((name, val_s, conf, ts)) diff --git a/opendashcan/hw/socketcan_listen.py b/opendashcan/hw/socketcan_listen.py index 160ac89..f1f927e 100644 --- a/opendashcan/hw/socketcan_listen.py +++ b/opendashcan/hw/socketcan_listen.py @@ -9,7 +9,12 @@ from collections.abc import Iterator from opendashcan.core.frame import CANFrame -from opendashcan.hw.listen import DEFAULT_BITRATE, HardwareUnavailableError, iter_bus_frames, open_listen_bus +from opendashcan.hw.listen import ( + DEFAULT_BITRATE, + HardwareUnavailableError, + iter_bus_frames, + open_listen_bus, +) def listen_socketcan( diff --git a/opendashcan/protocols/honda/civic10/encoder.py b/opendashcan/protocols/honda/civic10/encoder.py index 831d3b2..c0aee28 100644 --- a/opendashcan/protocols/honda/civic10/encoder.py +++ b/opendashcan/protocols/honda/civic10/encoder.py @@ -8,6 +8,8 @@ from __future__ import annotations +from typing import TypeVar + from opendashcan.core.encoder import ClusterEncoder, EncodeMode, OutputKind, PeriodicFrameSpec from opendashcan.core.frame import BusRole, CANFrame, FrameDirection from opendashcan.core.state import Confidence, GearPosition, SignalValue, VehicleState @@ -15,6 +17,8 @@ from opendashcan.protocols.honda.checksum import honda_set_checksum, honda_set_counter from opendashcan.protocols.honda.civic10.signals import documented_signal_confidence +_T = TypeVar("_T") + ENGINE_DATA = 0x158 POWERTRAIN_DATA = 0x17C GEARBOX = 0x191 @@ -42,7 +46,7 @@ } -def _known(sig: SignalValue[object]) -> bool: +def _known(sig: SignalValue[_T]) -> bool: return sig.value is not None and sig.confidence != Confidence.UNKNOWN @@ -266,9 +270,9 @@ def encode(self, state: VehicleState, timestamp: float | None = None) -> list[CA _meta_frame( POWERTRAIN_DATA, _pack_powertrain( - float(pedal.value) if _known(pedal) else None, - bool(brake.value) if _known(brake) else None, - bool(acc.value) if _known(acc) else None, + float(pedal.value) if _known(pedal) and pedal.value is not None else None, + bool(brake.value) if _known(brake) and brake.value is not None else None, + bool(acc.value) if _known(acc) and acc.value is not None else None, rpm, counter, ), @@ -324,7 +328,14 @@ def encode(self, state: VehicleState, timestamp: float | None = None) -> list[CA frames.append( _meta_frame( DOORS_STATUS, - _pack_doors(*door_vals, counter), + _pack_doors( + door_vals[0], + door_vals[1], + door_vals[2], + door_vals[3], + door_vals[4], + counter, + ), ts, "DOORS_STATUS", tag, @@ -372,7 +383,7 @@ def encode(self, state: VehicleState, timestamp: float | None = None) -> list[CA ) odo = state.get("vehicle.odometer") - if _known(odo): + if _known(odo) and odo.value is not None: frames.append( _meta_frame( ODOMETER, @@ -438,7 +449,9 @@ def periodic_specs(self) -> list[PeriodicFrameSpec]: arbitration_id=GEARBOX, period_ms=100.0, name="GEARBOX classic packing (opendbc research)", - notes="Classic GEAR_SHIFTER @5|6; CONFLICT with _gearbox_common — see conflicts.yaml.", + notes=( + "Classic GEAR_SHIFTER @5|6; CONFLICT with _gearbox_common — see conflicts.yaml." + ), synthetic=True, ), PeriodicFrameSpec( diff --git a/opendashcan/registry/ids.py b/opendashcan/registry/ids.py index 9a4b1e7..73e4d18 100644 --- a/opendashcan/registry/ids.py +++ b/opendashcan/registry/ids.py @@ -143,8 +143,7 @@ def normalize_platform_id(raw: str, *, default_market: str = "us") -> str: ) raise KeyError( - f"invalid platform id: {raw!r} " - f"(expected honda.model.genN.market or legacy honda:model:N)" + f"invalid platform id: {raw!r} (expected honda.model.genN.market or legacy honda:model:N)" ) diff --git a/opendashcan/registry/models.py b/opendashcan/registry/models.py index 798b7f5..658b8a9 100644 --- a/opendashcan/registry/models.py +++ b/opendashcan/registry/models.py @@ -341,9 +341,7 @@ def from_dict(cls, data: dict[str, Any]) -> ClusterRequirement: arb = _unk(data.get("arbitration_id")) if isinstance(arb, int): arb = hex(arb) - priority = str( - data.get("priority") or data.get("kind") or data.get("type") or "UNKNOWN" - ) + priority = str(data.get("priority") or data.get("kind") or data.get("type") or "UNKNOWN") return cls( signal=_unk(data.get("signal")), priority=priority, diff --git a/opendashcan/registry/vehicles.py b/opendashcan/registry/vehicles.py index e8491fb..2a87fe7 100644 --- a/opendashcan/registry/vehicles.py +++ b/opendashcan/registry/vehicles.py @@ -151,9 +151,7 @@ def get_decoder(vehicle_id: str) -> VehicleDecoder: raise KeyError(f"no decoder for {vehicle_id!r}") -def get_encoder( - cluster_id: str, *, emit_synthetic_research: bool = False -) -> ClusterEncoder: +def get_encoder(cluster_id: str, *, emit_synthetic_research: bool = False) -> ClusterEncoder: key = _resolve_encoder_key(cluster_id) if key == "civic10": return Civic10Encoder(emit_synthetic_research=emit_synthetic_research) diff --git a/opendashcan/replay/reader.py b/opendashcan/replay/reader.py index cf908c3..dfb2433 100644 --- a/opendashcan/replay/reader.py +++ b/opendashcan/replay/reader.py @@ -66,7 +66,11 @@ def read_asc(path: Path | str) -> Iterator[CANFrame]: if not raw or raw.startswith("//") or raw.startswith(";"): continue lower = raw.lower() - if lower.startswith("date ") or lower.startswith("base ") or lower.startswith("internal"): + if ( + lower.startswith("date ") + or lower.startswith("base ") + or lower.startswith("internal") + ): continue if lower.startswith("begintriggerblock") or lower.startswith("endtriggerblock"): continue @@ -87,9 +91,7 @@ def read_asc(path: Path | str) -> Iterator[CANFrame]: data = data + bytes(dlc - len(data)) elif len(data) > dlc: data = data[:dlc] - direction = ( - FrameDirection.TX if m.group("dir").lower() == "tx" else FrameDirection.RX - ) + direction = FrameDirection.TX if m.group("dir").lower() == "tx" else FrameDirection.RX yield CANFrame( arbitration_id=arb, data=data, diff --git a/pyproject.toml b/pyproject.toml index cabd3ca..7fe5795 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,6 +107,18 @@ warn_unused_configs = true mypy_path = "." packages = ["opendashcan"] +[[tool.mypy.overrides]] +module = [ + "PySide6.*", + "can", +] +ignore_missing_imports = true + +# Qt GUI typing requires PySide6 at typecheck time; CI installs only [dev]. +[[tool.mypy.overrides]] +module = "opendashcan.gui.*" +ignore_errors = true + [tool.pytest.ini_options] testpaths = ["tests"] addopts = "-q" diff --git a/schemas/evidence_v2.schema.json b/schemas/evidence_v2.schema.json index 3dbd2c4..49d3b4f 100644 --- a/schemas/evidence_v2.schema.json +++ b/schemas/evidence_v2.schema.json @@ -22,8 +22,10 @@ "CAN_CAPTURE", "ACADEMIC_RESEARCH", "COMMUNITY_RESEARCH", + "COMMERCIAL_DATASHEET", "PHYSICAL_TEST", - "INFERENCE" + "INFERENCE", + "RESEARCH_NOTE" ] }, "claim": { "type": "string" }, diff --git a/schemas/signal_evidence.schema.json b/schemas/signal_evidence.schema.json index 7090573..5e81b22 100644 --- a/schemas/signal_evidence.schema.json +++ b/schemas/signal_evidence.schema.json @@ -39,7 +39,11 @@ "VERIFIED", "SUPPORTED_BY_MULTIPLE_SOURCES", "COMMUNITY_REPORTED", + "DOCUMENTED", + "CAPTURE_VERIFIED", + "PHYSICALLY_VERIFIED", "INFERRED", + "HYPOTHESIS", "UNKNOWN" ] }, diff --git a/tests/fixtures/synthetic_candump.log b/tests/fixtures/synthetic_candump.log new file mode 100644 index 0000000..ed41b59 --- /dev/null +++ b/tests/fixtures/synthetic_candump.log @@ -0,0 +1,4 @@ +# SYNTHETIC fixture — NOT real vehicle traffic; payloads are opaque test bytes +(0.000000) vcan0 194#0000000000000000 +(0.100000) vcan0 494#0011223344556677 +(0.200000) vcan0 694#AABBCCDDEEFF0011 diff --git a/tests/test_gui_smoke.py b/tests/test_gui_smoke.py index fffda72..737b2e5 100644 --- a/tests/test_gui_smoke.py +++ b/tests/test_gui_smoke.py @@ -98,7 +98,10 @@ def test_splash_importable() -> None: assert splash.DISCLAIMER.startswith("Not affiliated") assert "Honda Motor Co" in splash.TRADEMARK_NOTICE - assert "copyright" in splash.TRADEMARK_NOTICE.lower() or "trademark" in splash.TRADEMARK_NOTICE.lower() + assert ( + "copyright" in splash.TRADEMARK_NOTICE.lower() + or "trademark" in splash.TRADEMARK_NOTICE.lower() + ) assert callable(splash.splash_disabled) assert callable(splash.honda_splash_logo_path) assert callable(splash.rounded_logo_pixmap) @@ -111,7 +114,7 @@ def test_rounded_logo_pixmap_optional() -> None: os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") from PySide6.QtWidgets import QApplication - from opendashcan.gui.splash import rounded_logo_pixmap, repo_logo_path + from opendashcan.gui.splash import repo_logo_path, rounded_logo_pixmap _ = QApplication.instance() or QApplication([]) path = repo_logo_path() diff --git a/tests/test_listen.py b/tests/test_listen.py index 4b97dff..1495488 100644 --- a/tests/test_listen.py +++ b/tests/test_listen.py @@ -19,7 +19,12 @@ resolve_virtual_fixture, ) from opendashcan.hw.session import ListenSession -from opendashcan.hw.tx_guard import TX_ENV_FLAG, assert_tx_permitted, refuse_send_message, tx_env_enabled +from opendashcan.hw.tx_guard import ( + TX_ENV_FLAG, + assert_tx_permitted, + refuse_send_message, + tx_env_enabled, +) from opendashcan.protocols.honda.civic10.decoder import Civic10VehicleDecoder from opendashcan.protocols.honda.civic10.encoder import Civic10Encoder from opendashcan.registry import get_decoder diff --git a/tests/test_phase23_foundation.py b/tests/test_phase23_foundation.py index 4bab91d..b93f47a 100644 --- a/tests/test_phase23_foundation.py +++ b/tests/test_phase23_foundation.py @@ -27,12 +27,8 @@ def test_normalize_dotted_and_legacy() -> None: assert normalize_platform_id("honda:civic:8") == "honda.civic.gen8.us" assert normalize_platform_id("honda-civic8") == "honda.civic.gen8.us" assert normalize_cluster_id("honda:civic:10:digital") == "honda.civic.gen10.cluster.digital" - aid = adapter_id( - "honda.civic.gen8.us.r18.auto", "honda.civic.gen10.cluster.digital" - ) - assert aid == ( - "honda.civic.gen8.us.r18.auto__to__honda.civic.gen10.cluster.digital" - ) + aid = adapter_id("honda.civic.gen8.us.r18.auto", "honda.civic.gen10.cluster.digital") + assert aid == ("honda.civic.gen8.us.r18.auto__to__honda.civic.gen10.cluster.digital") def test_registry_discovers_us_packages() -> None: diff --git a/tests/test_phase4_cluster.py b/tests/test_phase4_cluster.py index 31775ff..b8a0d06 100644 --- a/tests/test_phase4_cluster.py +++ b/tests/test_phase4_cluster.py @@ -7,6 +7,7 @@ import pytest +from opendashcan.adaptation.gating import EncodeMode from opendashcan.analysis.lineage import build_lineage, compare_encodings from opendashcan.analysis.similarity import correlate_signal from opendashcan.cli import build_parser @@ -14,7 +15,6 @@ from opendashcan.cluster.environment_loader import list_cluster_envs, load_cluster_env from opendashcan.cluster.gaps import build_gap_report from opendashcan.cluster.trace import generate_trace -from opendashcan.adaptation.gating import EncodeMode def test_cluster_envs_present() -> None: @@ -56,13 +56,33 @@ def test_lineage_builds() -> None: "id_hex": "0x158", "name": "ENGINE_DATA", "dlc": 8, - "signals": [{"name": "ENGINE_RPM", "start_bit": 23, "length": 16, "byte_order": "motorola", "signed": False, "scale": 1.0, "offset": 0.0}], + "signals": [ + { + "name": "ENGINE_RPM", + "start_bit": 23, + "length": 16, + "byte_order": "motorola", + "signed": False, + "scale": 1.0, + "offset": 0.0, + } + ], } b = dict(a) assert compare_encodings(a, b)["identical_encoding"] is True b2 = { **a, - "signals": [{"name": "ENGINE_RPM", "start_bit": 22, "length": 16, "byte_order": "motorola", "signed": False, "scale": 1.0, "offset": 0.0}], + "signals": [ + { + "name": "ENGINE_RPM", + "start_bit": 22, + "length": 16, + "byte_order": "motorola", + "signed": False, + "scale": 1.0, + "offset": 0.0, + } + ], } assert compare_encodings(a, b2)["identical_encoding"] is False diff --git a/tests/test_protocol_dbc.py b/tests/test_protocol_dbc.py index 4735e42..df09bf5 100644 --- a/tests/test_protocol_dbc.py +++ b/tests/test_protocol_dbc.py @@ -27,7 +27,7 @@ def test_dbc_parse_and_conflict_detection(tmp_path: Path) -> None: dbc = tmp_path / "sample.dbc" dbc.write_text( "BO_ 344 ENGINE_DATA: 8 PCM\n" - " SG_ ENGINE_RPM : 23|16@0+ (1,0) [0|15000] \"rpm\" EON\n" + ' SG_ ENGINE_RPM : 23|16@0+ (1,0) [0|15000] "rpm" EON\n' "BO_ 999 FAKE_MSG: 8 XXX\n", encoding="utf-8", ) diff --git a/tests/test_protocol_registry.py b/tests/test_protocol_registry.py index 45272d2..8dcd7c8 100644 --- a/tests/test_protocol_registry.py +++ b/tests/test_protocol_registry.py @@ -95,7 +95,4 @@ def test_civic10_opendbc_ids_documented(): def test_list_clusters(): clusters = dict(get_registry().list_clusters()) - assert ( - "honda:civic:10:digital" in clusters - or "honda.civic.gen10.cluster.digital" in clusters - ) + assert "honda:civic:10:digital" in clusters or "honda.civic.gen10.cluster.digital" in clusters diff --git a/tools/_populate_opendbc_research.py b/tools/_populate_opendbc_research.py index 64c9de0..cc3689f 100644 --- a/tools/_populate_opendbc_research.py +++ b/tools/_populate_opendbc_research.py @@ -41,7 +41,9 @@ period_ms: UNKNOWN confidence: DOCUMENTED evidence: [{ev}] - notes: opendbc POWERTRAIN_DATA (BO_ 380). Also carries ENGINE_RPM. Cluster RX NOT PHYSICALLY_VERIFIED. + notes: > + opendbc POWERTRAIN_DATA (BO_ 380). Also carries ENGINE_RPM. + Cluster RX NOT PHYSICALLY_VERIFIED. - arbitration_id: "0x309" name: CAR_SPEED @@ -52,7 +54,9 @@ period_ms: UNKNOWN confidence: DOCUMENTED evidence: [{ev}] - notes: opendbc CAR_SPEED (BO_ 777) CAR_SPEED@7|16 scale 0.01 kph. Cluster RX NOT PHYSICALLY_VERIFIED. + notes: > + opendbc CAR_SPEED (BO_ 777) CAR_SPEED@7|16 scale 0.01 kph. + Cluster RX NOT PHYSICALLY_VERIFIED. - arbitration_id: "0x191" name: GEARBOX @@ -78,7 +82,9 @@ period_ms: UNKNOWN confidence: DOCUMENTED evidence: [{ev}] - notes: opendbc VSA_STATUS (BO_ 420). ESP_DISABLED bit present. Cluster warning mapping NOT PHYSICALLY_VERIFIED. + notes: > + opendbc VSA_STATUS (BO_ 420). ESP_DISABLED bit present. + Cluster warning mapping NOT PHYSICALLY_VERIFIED. - arbitration_id: "0x1D0" name: WHEEL_SPEEDS diff --git a/tools/build_phase4_artifactsifacts.py b/tools/build_phase4_artifactsifacts.py index 9101181..bfe2138 100644 --- a/tools/build_phase4_artifactsifacts.py +++ b/tools/build_phase4_artifactsifacts.py @@ -17,8 +17,8 @@ def main() -> int: from opendashcan.analysis.lineage import build_lineage, write_lineage_artifacts from opendashcan.analysis.network_graph import write_network_graph from opendashcan.analysis.similarity import build_cross_platform_candidates - from opendashcan.cluster.gaps import build_gap_report from opendashcan.cluster.environment_loader import list_cluster_envs + from opendashcan.cluster.gaps import build_gap_report from opendashcan.cluster.timing import write_timing_database from opendashcan.honda.integrity.patterns import write_integrity_patterns @@ -138,14 +138,14 @@ def _write_provenance_archive() -> None: | Source | Result | Cluster relevance | |--------|--------|-------------------| -| commaai/opendbc Honda DBCs (vendored) | Positive — vehicle protocol | VEHICLE_PROTOCOL_DOCUMENTED | -| CivicX 2020 Si cluster swap | Positive as COMMUNITY_RESEARCH on 0x158/0x17C | NOT CLUSTER_RX_CONFIRMED | +| commaai/opendbc Honda DBCs (vendored) | Positive — vehicle protocol | PROTOCOL_DOCUMENTED | +| CivicX 2020 Si cluster swap | COMMUNITY_RESEARCH on 0x158/0x17C | NOT CLUSTER_RX_CONFIRMED | | CivicX “Decoding the CAN BUS” | Points to opendbc | Use DBC labels, not forum | | Honda-Civic-B-CAN (GitHub) | Body bus tap claims | UNKNOWN for cluster gauges | | HondaCAN (Accord, GitHub) | Vehicle profiles | UNKNOWN cluster RX | -| Public OEM service PDFs for fuel/coolant CAN | **Negative** — no public bit layout found in Phase 4 pass | Remain ABSENT / UNKNOWN | -| Academic papers on Honda cluster RX | **Negative** — no citable open encoding found | UNKNOWN | -| Public cluster part-number ↔ CAN map | **Negative / incomplete** | See CIVIC10_CLUSTER_DEEP_DIVE.md | +| Public OEM fuel/coolant CAN PDFs | **Negative** — no public bit layout | Remain ABSENT / UNKNOWN | +| Academic papers on Honda cluster RX | **Negative** — no citable encoding | UNKNOWN | +| Public cluster part ↔ CAN map | **Negative / incomplete** | CIVIC10_CLUSTER_DEEP_DIVE.md | ## Gaps still open diff --git a/tools/capture_gui_demo.py b/tools/capture_gui_demo.py index bcdb614..f2438a7 100644 --- a/tools/capture_gui_demo.py +++ b/tools/capture_gui_demo.py @@ -77,9 +77,9 @@ def capture(with_splash: bool) -> int: from PySide6.QtGui import QFont from PySide6.QtWidgets import QApplication + from opendashcan.gui import services from opendashcan.gui.main_window import MainWindow from opendashcan.gui.splash import BootSplash - from opendashcan.gui import services _ensure_dirs() gen = ROOT / "tools" / "generate_wiring_diagrams.py" @@ -111,7 +111,7 @@ def _prep(w) -> None: splash._emblem.glow = g splash._opacity.setOpacity(0.4 + 0.6 * g) app.processEvents() - fp = DEMOS / f"_splash_frame_{int(g*10)}.png" + fp = DEMOS / f"_splash_frame_{int(g * 10)}.png" _save_widget(splash, fp) splash_frames.append(fp) splash.skip() diff --git a/tools/dbc_import.py b/tools/dbc_import.py index 6050fff..d5c1d52 100644 --- a/tools/dbc_import.py +++ b/tools/dbc_import.py @@ -11,7 +11,9 @@ def main() -> int: - p = argparse.ArgumentParser(description="Import DBC with full signal provenance (no auto-merge)") + p = argparse.ArgumentParser( + description="Import DBC with full signal provenance (no auto-merge)" + ) p.add_argument("dbc", type=Path) p.add_argument("-o", "--output", type=Path, help="Single JSON output") p.add_argument("--output-dir", type=Path, help="Write JSON+YAML index pair") diff --git a/tools/populate_fit_insight_odyssey.py b/tools/populate_fit_insight_odyssey.py index 58d6ae0..3b32488 100644 --- a/tools/populate_fit_insight_odyssey.py +++ b/tools/populate_fit_insight_odyssey.py @@ -77,7 +77,7 @@ def _messages_yaml(data: dict, ev: str) -> str: if msg.get("name") not in keep_names: continue lines.append(f' - arbitration_id: "{msg["id_hex"]}"') - lines.append(f' name: {msg["name"]}') + lines.append(f" name: {msg['name']}") lines.append(" bus: vehicle_can") lines.append(f" dlc: {msg['dlc']}") sender = SENDER_MAP.get(str(msg.get("sender") or "XXX").upper(), "UNKNOWN") @@ -94,7 +94,13 @@ def _messages_yaml(data: dict, ev: str) -> str: f" notes: >\n opendbc {src} ({sha[:12]}…). " "Vehicle-bus DOCUMENTED. Cluster RX NOT PHYSICALLY_VERIFIED." ) - if msg["name"] in ("ENGINE_DATA", "POWERTRAIN_DATA", "CAR_SPEED", "GEARBOX", "SCM_FEEDBACK"): + if msg["name"] in ( + "ENGINE_DATA", + "POWERTRAIN_DATA", + "CAR_SPEED", + "GEARBOX", + "SCM_FEEDBACK", + ): lines.append(" integrity:") lines.append(" checksum: { type: honda_nibble_v1 }") lines.append(" counter: { type: honda_2bit_v1 }") @@ -113,16 +119,16 @@ def _signals_yaml(data: dict, ev: str) -> str: lines.append(" message:") lines.append(f' arbitration_id: "{m["arbitration_id"]}"') lines.append(" bus: vehicle_can") - lines.append(f' name: {m["message"]}') + lines.append(f" name: {m['message']}") lines.append(" encoding:") - lines.append(f' start_bit: {m["start_bit"]}') - lines.append(f' length: {m["length"]}') - lines.append(f' byte_order: {m["byte_order"]}') - lines.append(f' signed: {"true" if m.get("signed") else "false"}') - lines.append(f' scale: {m["scale"]}') - lines.append(f' offset: {m["offset"]}') + lines.append(f" start_bit: {m['start_bit']}") + lines.append(f" length: {m['length']}") + lines.append(f" byte_order: {m['byte_order']}") + lines.append(f" signed: {'true' if m.get('signed') else 'false'}") + lines.append(f" scale: {m['scale']}") + lines.append(f" offset: {m['offset']}") unit = m.get("unit") - lines.append(f' unit: {unit if unit else "null"}') + lines.append(f" unit: {unit if unit else 'null'}") lines.append(" confidence: DOCUMENTED") lines.append(f" evidence: [{ev}]") lines.append(f" knowledge_level: {kl}") @@ -139,7 +145,7 @@ def _signals_yaml(data: dict, ev: str) -> str: continue lines.append(f" - signal: {tax}") lines.append(" message:") - lines.append(' arbitration_id: UNKNOWN') + lines.append(" arbitration_id: UNKNOWN") lines.append(" bus: UNKNOWN") lines.append(" encoding: {}") lines.append(" confidence: UNKNOWN") @@ -152,7 +158,7 @@ def _signals_yaml(data: dict, ev: str) -> str: lines.append(" - signal: vehicle.ignition_state") lines.append(" message:") - lines.append(' arbitration_id: UNKNOWN') + lines.append(" arbitration_id: UNKNOWN") lines.append(" bus: UNKNOWN") lines.append(" encoding: {}") lines.append(" confidence: UNKNOWN") @@ -172,12 +178,12 @@ def _evidence_yaml(ev: str, data: dict, platform: str) -> str: "evidence:\n" f" - evidence_id: {ev}\n" " source_type: DBC\n" - f" source_url: \"{prov.get('repo', 'https://github.com/commaai/opendbc')}\"\n" - f" title: \"commaai/opendbc {prov.get('source_dbc', '')} (MIT)\"\n" + f' source_url: "{prov.get("repo", "https://github.com/commaai/opendbc")}"\n' + f' title: "commaai/opendbc {prov.get("source_dbc", "")} (MIT)"\n' f" claim: opendbc vehicle-bus layouts for {platform}\n" " confidence: DOCUMENTED\n" f" license: {prov.get('license', 'MIT')}\n" - f" commit_sha: \"{prov.get('commit_sha', '')}\"\n" + f' commit_sha: "{prov.get("commit_sha", "")}"\n' f" source_dbc: {prov.get('source_dbc', '')}\n" " bus: vehicle_can\n" " notes: >\n" @@ -267,9 +273,7 @@ def main() -> None: (base / "signals.yaml").write_text(_signals_yaml(data, ev), encoding="utf-8") evid_dir = base / "evidence" evid_dir.mkdir(exist_ok=True) - (evid_dir / "catalog.yaml").write_text( - _evidence_yaml(ev, data, rel), encoding="utf-8" - ) + (evid_dir / "catalog.yaml").write_text(_evidence_yaml(ev, data, rel), encoding="utf-8") # Update vehicle notes vpath = base / "vehicle.yaml" if vpath.exists():