From 95b2df05ba6e9cc15fe8a77ce83ca8d6de291b16 Mon Sep 17 00:00:00 2001 From: Aarnav Koushik Date: Thu, 30 Jul 2026 21:23:23 -0700 Subject: [PATCH] Source physical topology from GRCAN.CANdo and remove can_topology.json The physical bus wiring now lives in the physical topology section of GRCAN.CANdo (Firmware), which is the single source of truth. Teach GrcanDocument to parse that section and expose getPhysicalTopology(), repoint PhysicalTopology at the document instead of fetching the JSON, refresh it on each ref load, and delete the now-duplicate can_topology.json. --- README.md | 29 ++++++++--------- Web/can_topology.json | 44 ------------------------- Web/candoDocument.js | 71 ++++++++++++++++++++++++++++++++++++++--- Web/physicalTopology.js | 56 ++++++++++++++------------------ Web/viewer.js | 13 ++++++-- 5 files changed, 115 insertions(+), 98 deletions(-) delete mode 100644 Web/can_topology.json diff --git a/README.md b/README.md index 8902e8e..04d6101 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Edits are **not** saved to a backend. They exist in browser memory until the use | `candoDocument.js` | Semantic document model with cross-section invariants (`window.GrcanDocument`) | | `editor.js` | In-memory mutation engine, raw text state (`window.GrcanEditor`) | | `viewer.js` | Main controller: rendering, navigation, edit/delete wiring | -| `physicalTopology.js` | Parses `can_topology.json` to enforce physical bus-to-node constraints | +| `physicalTopology.js` | Reads the `physical topology` section of `GRCAN.CANdo` (via `GrcanDocument`) to enforce physical bus-to-node constraints | | `physicalGroups.js` | Derives functional groupings dynamically from prefixes for the Graph View renderer (`window.PhysicalGroups`) | | `layoutPhysicalBus.js` | Pure SVG layout for the physical-bus Graph View (`window.LayoutPhysicalBus`) | | `graphView.js` | Physical-bus SVG graph visualization (`window.GrcanGraphView`) | @@ -46,12 +46,6 @@ Edits are **not** saved to a backend. They exist in browser memory until the use | `editor.css` | Modal/form/icon/diff/editor state styles | | `graphView.css` | Graph view overlay styles | -### Data - -| File | Purpose | -|---|---| -| `can_topology.json` | Physical CAN bus topology: which nodes are wired to which bus. Node names must match `GR ID` entries in `GRCAN.CANdo`. | - ### Vendored (retained for rollback, no longer loaded) | File | Purpose | @@ -91,20 +85,25 @@ against the browser scripts: for f in Web/*.js; do node --check "$f" || exit 1; done ``` -## Editing `can_topology.json` +## Editing the physical topology + +Physical bus wiring — which devices are physically connected to each CAN bus — lives in the `physical topology` section of `GRCAN.CANdo` itself (in the [Firmware](https://github.com/Gaucho-Racing/Firmware) repo), so it sits alongside the logical bus and routing definitions and is the single source of truth. The viewer reads it directly from the loaded CANdo; there is no separate file to edit here. -This file defines which devices are physically connected to each CAN bus. The format is a JSON object keyed by bus name, with arrays of node names: +The section is keyed by bus name, with a list of node names: -```json -{ - "Primary": ["ECU", "ACU", "..."], - "Data": ["ECU", "SAMM_Mag_1", "..."] -} +```yaml +physical topology: + Primary: + - ECU + - ACU + Data: + - ECU + - TireTemp FL ``` - Bus keys must exactly match entries in the `Bus ID:` section of `GRCAN.CANdo` (e.g. `Primary`, `Data`, `Charger`). - Node names must exactly match `GR ID` entries in `GRCAN.CANdo`. - `Debugger` and `ALL` are always exempt and should not be listed. -- JSON has no comment syntax. Rationale for entries should go in this README instead. +- Buses omitted from the section are treated as unrestricted. - Hardware note: both `GR Inv` and `DTI Inv` share `Primary`. Whichever isn't physically connected has its messages go nowhere — no firmware switch needed. diff --git a/Web/can_topology.json b/Web/can_topology.json deleted file mode 100644 index 6ec4898..0000000 --- a/Web/can_topology.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "Primary": [ - "ECU", - "TCM", - "ACU", - "DGPS", - "GR Inv", - "DTI Inv", - "Fan Ctrl 1", - "Fan Ctrl 2", - "Fan Ctrl 3", - "EM", - "Dash Panel" - ], - "Data": [ - "ECU", - "TCM", - "ACU", - "DGPS", - "TireTemp FL", - "TireTemp FR", - "TireTemp RL", - "TireTemp RR", - "BrakeTemp FL", - "BrakeTemp FR", - "BrakeTemp RL", - "BrakeTemp RR", - "Suspension FL", - "Suspension FR", - "Suspension RL", - "Suspension RR", - "InboardFloor FL", - "InboardFloor FR", - "InboardFloor RL", - "InboardFloor RR" - ], - "Charger": [ - "CCU", - "ACU", - "Charger", - "IMD", - "EM" - ] -} diff --git a/Web/candoDocument.js b/Web/candoDocument.js index d2ae0f2..acd1b88 100644 --- a/Web/candoDocument.js +++ b/Web/candoDocument.js @@ -30,6 +30,7 @@ let _byteOrderText = ""; // verbatim byte order line text let _customCanIds = new Map(); // Map let _busIds = new Map(); // Map derived from Bus ID section + let _physicalTopology = new Map(); // Map> from "physical topology" section // ==================== CAN ID format utilities ==================== // Custom CAN ID section canonical form: bare uppercase hex, no 0x prefix (e.g. "2416"). @@ -59,6 +60,7 @@ _byteOrderText = ""; _customCanIds = new Map(); _busIds = new Map(); + _physicalTopology = new Map(); if (!rawText) return; const lines = rawText.split("\n"); @@ -68,7 +70,8 @@ byteOrderStart = -1, msgIdStart = -1, customCanIdStart = -1, - grIdStart = -1; + grIdStart = -1, + physicalTopologyStart = -1; for (let i = 0; i < lines.length; i++) { const l = lines[i]; @@ -77,12 +80,24 @@ else if (l.startsWith("Message ID:")) msgIdStart = i; else if (l.startsWith("Custom CAN ID:")) customCanIdStart = i; else if (l.startsWith("GR ID:")) grIdStart = i; + else if (l.startsWith("physical topology:")) physicalTopologyStart = i; } - // Verbatim: Bus ID = everything before routing section header. + // Verbatim: Bus ID = everything before routing section header. This span + // also contains the "physical topology" section, which is preserved + // verbatim here and additionally parsed into _physicalTopology below. if (routingStart > 0) { _busIdsText = lines.slice(0, routingStart).join("\n").replace(/\n+$/, ""); - _parseBusIdsSection(lines, 0, routingStart); + // Bus ID header block ends where physical topology begins (if present). + const busIdEnd = + physicalTopologyStart > -1 ? physicalTopologyStart : routingStart; + _parseBusIdsSection(lines, 0, busIdEnd); + } + + // Read-only: physical topology section (sits between Bus ID and routing). + if (physicalTopologyStart > -1) { + const end = routingStart > -1 ? routingStart : lines.length; + _parsePhysicalTopologySection(lines, physicalTopologyStart, end); } // Verbatim: byte order line through the blank line before Message ID. @@ -161,6 +176,39 @@ } } + // Parses the "physical topology:" section into _physicalTopology + // (busName → Set). Records which devices are physically wired to + // each CAN bus. Hand-editable, read-only here — the section is preserved + // verbatim on serialize as part of the Bus ID span, so this parse never + // feeds back into output. Format: + // physical topology: + // BusName: + // - NodeName + function _parsePhysicalTopologySection(lines, start, end) { + let curBus = null; + for (let i = start; i < end; i++) { + const line = lines[i]; + if (line.startsWith("physical topology:")) continue; + if (line.trim() === "" || line.trim().startsWith("#")) continue; + // Stop at next top-level (non-indented) line. + if (/^\S/.test(line)) break; + // " BusName:" (indent 2, no value) + const busMatch = line.match(/^ (\S[^:]*):\s*$/); + if (busMatch) { + curBus = busMatch[1].trim(); + if (!_physicalTopology.has(curBus)) { + _physicalTopology.set(curBus, new Set()); + } + continue; + } + // " - NodeName" (list item under current bus) + const nodeMatch = line.match(/^\s+-\s+(.+?)\s*$/); + if (nodeMatch && curBus !== null) { + _physicalTopology.get(curBus).add(nodeMatch[1].trim()); + } + } + } + function _parseRouting(lines, start, end) { let curDevice = null, curBus = null, @@ -653,7 +701,8 @@ } // V8: PHYSICAL_BUS_VIOLATION - // Only runs when PhysicalTopology has successfully loaded can_topology.json. + // Only runs when PhysicalTopology has successfully loaded the physical + // topology section from the .CANdo document. const _topo = (typeof window !== "undefined" ? window : {}) .PhysicalTopology; if (_topo && _topo.isLoaded()) { @@ -1319,6 +1368,19 @@ return _busIds.has(busPort); } + // Returns the physical bus wiring from the "physical topology:" section as + // Map>. This is the source of truth for which devices + // are physically wired to which CAN bus — consumed by PhysicalTopology. + // Returns a fresh copy so callers cannot mutate internal state. + function getPhysicalTopology() { + _ensureParsed(); + const out = new Map(); + for (const [bus, nodes] of _physicalTopology) { + out.set(bus, new Set(nodes)); + } + return out; + } + function getGrIds() { _ensureParsed(); return new Map(_grIds); @@ -1457,6 +1519,7 @@ grIdExists, getDeviceNames, getBusNames, + getPhysicalTopology, getGrIds, getGrId, getMessageDef, diff --git a/Web/physicalTopology.js b/Web/physicalTopology.js index 2ccf986..9e11aac 100644 --- a/Web/physicalTopology.js +++ b/Web/physicalTopology.js @@ -1,8 +1,9 @@ // Purpose: Physical CAN bus topology enforcement module. -// Loads and parses Web/can_topology.json — the human-editable source of truth -// for which devices are physically wired to which CAN bus. +// Sources the "physical topology" section of the .CANdo document (via +// GrcanDocument) — the human-editable source of truth for which devices are +// physically wired to which CAN bus. // All exemption logic (Debugger, ALL) lives here and nowhere else. -// No other file knows about storage, parsing, or fetch internals. +// No other file knows about parsing or document internals. // Callers use only the four public methods below. // Exposed as: window.PhysicalTopology @@ -16,21 +17,9 @@ let _topology = new Map(); let _loaded = false; - // ==================== Parser ==================== - // Pure function: JSON text → Map> - - function _parse(text) { - const result = new Map(); - const data = JSON.parse(text); - for (const [bus, nodes] of Object.entries(data)) { - if (Array.isArray(nodes)) result.set(bus, new Set(nodes)); - } - return result; - } - - // Surface entries in can_topology.json that don't exist as devices in the - // .CANdo GR ID block. Drift here = unreachable receivers in the form. - // Idempotent: warns once per call, no internal state mutation. + // Surface entries in the physical topology section that don't exist as + // devices in the .CANdo GR ID block. Drift here = unreachable receivers in + // the form. Idempotent: warns once per call, no internal state mutation. function _validateAgainstDeviceRegistry() { const doc = window.GrcanDocument; if (!doc || typeof doc.getDeviceNames !== "function") return; @@ -44,7 +33,7 @@ } if (unknown.length) { console.warn( - "[PhysicalTopology] can_topology.json lists nodes not in the .CANdo GR ID registry:", + "[PhysicalTopology] physical topology section lists nodes not in the .CANdo GR ID registry:", unknown, ); } @@ -53,20 +42,23 @@ // ==================== Public API ==================== window.PhysicalTopology = { - // Fetch and parse can_topology.json. Call once at startup. - // Resolves even on failure — isLoaded() will return false in that case. + // Read the physical topology from the currently-parsed .CANdo document. + // Call once the document is loaded, and again whenever it changes (e.g. + // after switching refs). Resolves even on failure — isLoaded() will + // return false if no topology section is present. load: async function () { + _topology = new Map(); + _loaded = false; + const doc = window.GrcanDocument; + if (!doc || typeof doc.getPhysicalTopology !== "function") return; try { - // no-cache: re-validate with server every load so a topology JSON - // edit reaches users on next page open, not on next hard-refresh. - const resp = await fetch("can_topology.json", { cache: "no-cache" }); - if (!resp.ok) return; - const text = await resp.text(); - _topology = _parse(text); - _loaded = true; + const topo = doc.getPhysicalTopology(); + if (topo && topo.size > 0) { + _topology = topo; + _loaded = true; + } } catch (_) { - // Silently no-op: fetch unavailable or malformed JSON - // (e.g. file:// local mode, or hand-edit syntax error). + // Silently no-op: document unavailable or unparsed. } }, @@ -85,7 +77,7 @@ return busSet.has(nodeName); }, - // All node names registered for busPort in the topology file. + // All node names registered for busPort in the topology section. // Returns [] if not loaded or bus unknown. getNodesForBus: function (busPort) { if (!_loaded) return []; @@ -95,7 +87,7 @@ // Caller should invoke this once after the .CANdo has been parsed so // GrcanDocument.getDeviceNames() returns a populated set. Safe to call - // even before load() resolves — it short-circuits. + // even before load() has run — it short-circuits. validate: _validateAgainstDeviceRegistry, }; })(); diff --git a/Web/viewer.js b/Web/viewer.js index f03371f..4cb6e9f 100644 --- a/Web/viewer.js +++ b/Web/viewer.js @@ -1438,6 +1438,10 @@ window.addEventListener("DOMContentLoaded", function () { } } + // Refresh physical topology from the now-loaded .CANdo before rendering, + // so the Graph View reflects this ref's wiring. + if (window.PhysicalTopology) await window.PhysicalTopology.load(); + if (HIERARCHY_MODE === "NODE_BUS") { await renderNodeBus(ref, localText); } else { @@ -1481,6 +1485,9 @@ window.addEventListener("DOMContentLoaded", function () { const text = editor.getRawText(); if (!text) return; loadNodeIdsFromText(text); + // Local edits may have changed the physical topology section; refresh + // before re-rendering the Graph View. + if (window.PhysicalTopology) await window.PhysicalTopology.load(); if (HIERARCHY_MODE === "NODE_BUS") { await renderNodeBus(null, text); } else { @@ -1576,9 +1583,9 @@ window.addEventListener("DOMContentLoaded", function () { setHierarchyHeaders(); wireEditModeButtons(); setPlaceholder(firstList, "Loading..."); - // Load physical topology + functional groups in the background; - // non-blocking. Both feed the Graph View renderer. - if (window.PhysicalTopology) window.PhysicalTopology.load(); + // Load functional groups in the background; non-blocking. Physical + // topology is sourced from the .CANdo itself and refreshed inside + // renderHierarchy once a ref is loaded. if (window.PhysicalGroups) window.PhysicalGroups.load(); const [branches, tags] = await Promise.all([