Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 14 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`) |
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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.

44 changes: 0 additions & 44 deletions Web/can_topology.json

This file was deleted.

71 changes: 67 additions & 4 deletions Web/candoDocument.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
let _byteOrderText = ""; // verbatim byte order line text
let _customCanIds = new Map(); // Map<name, {name, canId, length, signals[]}>
let _busIds = new Map(); // Map<busName, numericId: number> derived from Bus ID section
let _physicalTopology = new Map(); // Map<busName, Set<nodeName>> from "physical topology" section

// ==================== CAN ID format utilities ====================
// Custom CAN ID section canonical form: bare uppercase hex, no 0x prefix (e.g. "2416").
Expand Down Expand Up @@ -59,6 +60,7 @@
_byteOrderText = "";
_customCanIds = new Map();
_busIds = new Map();
_physicalTopology = new Map();

if (!rawText) return;
const lines = rawText.split("\n");
Expand All @@ -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];
Expand All @@ -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.
Expand Down Expand Up @@ -161,6 +176,39 @@
}
}

// Parses the "physical topology:" section into _physicalTopology
// (busName → Set<nodeName>). 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,
Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -1319,6 +1368,19 @@
return _busIds.has(busPort);
}

// Returns the physical bus wiring from the "physical topology:" section as
// Map<busName, Set<nodeName>>. 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);
Expand Down Expand Up @@ -1457,6 +1519,7 @@
grIdExists,
getDeviceNames,
getBusNames,
getPhysicalTopology,
getGrIds,
getGrId,
getMessageDef,
Expand Down
56 changes: 24 additions & 32 deletions Web/physicalTopology.js
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -16,21 +17,9 @@
let _topology = new Map();
let _loaded = false;

// ==================== Parser ====================
// Pure function: JSON text → Map<bus, Set<name>>

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;
Expand All @@ -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,
);
}
Expand All @@ -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.
}
},

Expand All @@ -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 [];
Expand All @@ -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,
};
})();
13 changes: 10 additions & 3 deletions Web/viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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([
Expand Down