diff --git a/index.ts b/index.ts index 8fca69a..380688c 100644 --- a/index.ts +++ b/index.ts @@ -9,4 +9,5 @@ export { checkSourceTracesHavePcbTraces } from "./lib/check-source-traces-have-p export { checkPcbTracesOutOfBoard } from "./lib/check-trace-out-of-board/checkTraceOutOfBoard" export { checkPcbComponentOverlap } from "./lib/check-pcb-components-overlap/checkPcbComponentOverlap" export { checkPinMustBeConnected } from "./lib/check-pin-must-be-connected" +export { checkI2cSdaConnectedToSclMisconfigured } from "./lib/check-i2c-sda-connected-to-scl" export { runAllChecks } from "./lib/run-all-checks" diff --git a/lib/check-i2c-sda-connected-to-scl.ts b/lib/check-i2c-sda-connected-to-scl.ts new file mode 100644 index 0000000..c32e97c --- /dev/null +++ b/lib/check-i2c-sda-connected-to-scl.ts @@ -0,0 +1,87 @@ +import type { + AnyCircuitElement, + SourceI2cMisconfiguredError, +} from "circuit-json" +import { getSourcePortConnectivityMapFromCircuitJson } from "circuit-json-to-connectivity-map" + +type SourceComponent = Extract< + AnyCircuitElement, + { source_component_id: string; name: string } +> + +export function checkI2cSdaConnectedToSclMisconfigured( + circuitJson: AnyCircuitElement[], +): SourceI2cMisconfiguredError[] { + const errors: SourceI2cMisconfiguredError[] = [] + + const sourceComponents = circuitJson.filter( + (el): el is SourceComponent => + "source_component_id" in el && + (el.type === "source_component" || el.type.startsWith("source_simple_")), + ) + + // Get all source ports to easily look up their attributes + const sourcePorts = circuitJson.filter( + (el): el is Extract => + el.type === "source_port", + ) + const portMap = new Map(sourcePorts.map((p) => [p.source_port_id, p])) + + const connMap = getSourcePortConnectivityMapFromCircuitJson(circuitJson) + + for (const [netId, connectedPortIds] of Object.entries(connMap.netMap)) { + let hasSda = false + let hasScl = false + + const conflictingPortIds: string[] = [] + + for (const portId of connectedPortIds) { + const port = portMap.get(portId) + if (!port) continue + + let shouldAddPort = false + if (port.is_configured_for_i2c_sda) { + hasSda = true + shouldAddPort = true + } + if (port.is_configured_for_i2c_scl) { + hasScl = true + shouldAddPort = true + } + if (shouldAddPort && !conflictingPortIds.includes(portId)) { + conflictingPortIds.push(portId) + } + } + + if (hasSda && hasScl) { + // Sort conflicting port IDs to ensure deterministic ID generation + const sortedConflicts = [...conflictingPortIds].sort() + const conflictIdStr = sortedConflicts.join("_") + + const portDetails = sortedConflicts.map((portId) => { + const port = portMap.get(portId) + const component = sourceComponents.find( + (c) => c.source_component_id === port?.source_component_id, + ) + const componentName = component?.name ?? "Unknown" + const portName = port?.name ?? "Unknown" + const i2cRole = port?.is_configured_for_i2c_sda + ? "I2C SDA" + : port?.is_configured_for_i2c_scl + ? "I2C SCL" + : "Unknown" + return `${componentName}.${portName} (${i2cRole})` + }) + + errors.push({ + type: "source_i2c_misconfigured_error", + source_i2c_misconfigured_error_id: `source_i2c_misconfigured_error_${conflictIdStr}`, + error_type: "source_i2c_misconfigured_error", + message: `${portDetails.join(" is connected to ")} on the same net. To fix this, ensure SDA and SCL are routed to separate nets.`, + source_port_ids: conflictingPortIds, + }) + } + } + + return errors +} diff --git a/lib/run-all-checks.ts b/lib/run-all-checks.ts index d98a46b..3efc7cc 100644 --- a/lib/run-all-checks.ts +++ b/lib/run-all-checks.ts @@ -9,6 +9,7 @@ import { checkSourceTracesHavePcbTraces } from "./check-source-traces-have-pcb-t import { checkPcbTracesOutOfBoard } from "./check-trace-out-of-board/checkTraceOutOfBoard" import { checkPcbComponentOverlap } from "./check-pcb-components-overlap/checkPcbComponentOverlap" import { checkPinMustBeConnected } from "./check-pin-must-be-connected" +import { checkI2cSdaConnectedToSclMisconfigured } from "./check-i2c-sda-connected-to-scl" import type { AnyCircuitElement } from "circuit-json" export async function runAllChecks(circuitJson: AnyCircuitElement[]) { @@ -24,5 +25,6 @@ export async function runAllChecks(circuitJson: AnyCircuitElement[]) { ...checkPcbTracesOutOfBoard(circuitJson), ...checkPcbComponentOverlap(circuitJson), ...checkPinMustBeConnected(circuitJson), + ...checkI2cSdaConnectedToSclMisconfigured(circuitJson), ] } diff --git a/package.json b/package.json index 08e9ad1..48eb8db 100644 --- a/package.json +++ b/package.json @@ -19,18 +19,18 @@ "@types/bun": "^1.2.8", "@types/debug": "^4.1.12", "bun-match-svg": "^0.0.11", + "circuit-json": "^0.0.387", "circuit-to-svg": "^0.0.166", - "circuit-json": "^0.0.380", "debug": "^4.3.5", "tscircuit": "^0.0.525", - "zod": "^3.23.8", - "tsup": "^8.2.3" + "tsup": "^8.2.3", + "zod": "^3.23.8" }, "peerDependencies": { + "@flatten-js/core": "*", "circuit-json": "*", "@tscircuit/math-utils": "*", "circuit-json-to-connectivity-map": "*", - "@flatten-js/core": "*", "typescript": "^5.5.3" } -} +} \ No newline at end of file diff --git a/tests/lib/check-i2c-sda-connected-to-scl.test.ts b/tests/lib/check-i2c-sda-connected-to-scl.test.ts new file mode 100644 index 0000000..e158e45 --- /dev/null +++ b/tests/lib/check-i2c-sda-connected-to-scl.test.ts @@ -0,0 +1,71 @@ +import { expect, test } from "bun:test" +import { checkI2cSdaConnectedToSclMisconfigured } from "../../lib/check-i2c-sda-connected-to-scl" +import type { AnyCircuitElement } from "circuit-json" + +test("checkI2cSdaConnectedToSclMisconfigured detects SDA connected to SCL", () => { + const circuitJson: AnyCircuitElement[] = [ + { + type: "source_port", + source_port_id: "port_sda", + name: "SDA", + is_configured_for_i2c_sda: true, + source_component_id: "comp_1", + }, + { + type: "source_component", + source_component_id: "comp_1", + name: "U1", + }, + { + type: "source_port", + source_port_id: "port_scl", + name: "SCL", + is_configured_for_i2c_scl: true, + source_component_id: "comp_2", + }, + { + type: "source_component", + source_component_id: "comp_2", + name: "U2", + }, + { + type: "source_trace", + source_trace_id: "trace_1", + connected_source_port_ids: ["port_sda", "port_scl"], + connected_source_net_ids: [], + }, + ] as AnyCircuitElement[] + + const errors = checkI2cSdaConnectedToSclMisconfigured(circuitJson) + expect(errors).toHaveLength(1) + expect(errors[0].message).toEqual(expect.stringContaining("U2.SCL (I2C SCL)")) + expect(errors[0].message).toEqual(expect.stringContaining("U1.SDA (I2C SDA)")) + expect(errors[0].source_port_ids).toContain("port_sda") + expect(errors[0].source_port_ids).toContain("port_scl") +}) + +test("checkI2cSdaConnectedToSclMisconfigured allows SDA connected to SDA", () => { + const circuitJson: AnyCircuitElement[] = [ + { + type: "source_port", + source_port_id: "port_sda1", + name: "SDA1", + is_configured_for_i2c_sda: true, + }, + { + type: "source_port", + source_port_id: "port_sda2", + name: "SDA2", + is_configured_for_i2c_sda: true, + }, + { + type: "source_trace", + source_trace_id: "trace_1", + connected_source_port_ids: ["port_sda1", "port_sda2"], + connected_source_net_ids: [], + }, + ] as AnyCircuitElement[] + + const errors = checkI2cSdaConnectedToSclMisconfigured(circuitJson) + expect(errors).toHaveLength(0) +})