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([