diff --git a/README.md b/README.md
index 6598c5d..e9c3203 100644
--- a/README.md
+++ b/README.md
@@ -92,7 +92,8 @@ Use tscircuit 0.0.2745 or newer, which exports these attributes as `is_output`,
`is_using_internal_pullup`, and `is_configured_for_i2c_sda` / `is_configured_for_i2c_scl`.
Capability flags such as `isGpio` alone do not select a pin function. Connected
pins with missing or unsupported functions produce an error identifying the
-MCU, physical pins, source ports and required TSX attributes. Missing MCU port
+MCU name/part number, physical pin numbers/labels and required TSX attributes,
+without generated record IDs or a converter stack trace. Missing MCU port
records or traces referencing absent source ports also fail before a new
SysConfig file is written; no behavior is inferred from signal names.
If more than one supported MCU is present, select one with `--component U1`
diff --git a/cli/sysconfig/convert.mjs b/cli/sysconfig/convert.mjs
index e2f0263..5bbd451 100644
--- a/cli/sysconfig/convert.mjs
+++ b/cli/sysconfig/convert.mjs
@@ -11,20 +11,25 @@ if (!circuitJsonPath || !optionsPath || !outputPath || !configurationPath) {
);
}
-const circuitJson = JSON.parse(await readFile(circuitJsonPath, "utf8"));
-const options = JSON.parse(await readFile(optionsPath, "utf8"));
-const converter = new CircuitJsonToSysConfigConverter(circuitJson, options);
-converter.runUntilFinished();
-const configuration = converter.getResolvedConfiguration();
-const cc2340 = configuration.cc2340;
-const converterOptions = getValidationOptions({ configuration, circuitJson });
+try {
+ const circuitJson = JSON.parse(await readFile(circuitJsonPath, "utf8"));
+ const options = JSON.parse(await readFile(optionsPath, "utf8"));
+ const converter = new CircuitJsonToSysConfigConverter(circuitJson, options);
+ converter.runUntilFinished();
+ const configuration = converter.getResolvedConfiguration();
+ const cc2340 = configuration.cc2340;
+ const converterOptions = getValidationOptions({ configuration, circuitJson });
-await writeFile(outputPath, converter.getOutput().getString());
-await writeFile(
- configurationPath,
- JSON.stringify({
- target: cc2340 ? "cc2340" : "am2434",
- component: configuration.component,
- converterOptions,
- }),
-);
+ await writeFile(outputPath, converter.getOutput().getString());
+ await writeFile(
+ configurationPath,
+ JSON.stringify({
+ target: cc2340 ? "cc2340" : "am2434",
+ component: configuration.component,
+ converterOptions,
+ }),
+ );
+} catch (error) {
+ console.error(error instanceof Error ? error.message : String(error));
+ process.exitCode = 1;
+}
diff --git a/npm/README.md b/npm/README.md
index 2690e96..c8e98c6 100644
--- a/npm/README.md
+++ b/npm/README.md
@@ -59,7 +59,8 @@ ti check-sysconfig ./board.circuit.tsx
Use tscircuit 0.0.2745 or newer for TSX export. For supported CC2340 circuits, GPIO/I2C choices come from existing MCU
`pinAttributes` exported to Circuit JSON. No separate request JSON is required
or read implicitly. Missing connected-pin functions fail with physical pin and
-source-port details and the required TSX attributes. Missing MCU port records
+signal labels and the required TSX attributes, without generated record IDs
+or a converter stack trace. Missing MCU port records
or connected source-port records also fail before a new `.syscfg` is written.
Use `--component U1` when selecting between multiple MCUs.
Connected 32768 Hz two-terminal crystals on MCU pins 14/15 select the external
diff --git a/package.json b/package.json
index bd35be8..2fc22d0 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
"snapshot:update": "tsci snapshot --update",
"start": "tsci dev",
"test": "bun test",
- "test:cli": "node --test tests/cli/*.test.mjs && bun test tests/cli/import-snapshot.test.tsx",
+ "test:cli": "node --test tests/cli/*.test.mjs && bun test tests/cli/import-snapshot.test.tsx tests/cli/sysconfig-errors-snapshot.test.ts",
"typecheck": "tsc --noEmit",
"format": "biome format --write",
"format:check": "biome format .",
@@ -35,7 +35,7 @@
"@types/bun": "^1.4.0",
"@types/react": "^19.2.17",
"bun-match-svg": "^0.0.17",
- "circuit-json-to-sysconfig": "github:tscircuit/circuit-json-to-sysconfig#cc3e6f3fa7fbfab99bf60db1c19ca1858995bdd0",
+ "circuit-json-to-sysconfig": "github:tscircuit/circuit-json-to-sysconfig#3da9ac4b464a84eac13615dc684da7105065c685",
"circuit-to-svg": "0.0.410",
"tscircuit": "0.0.2745",
"typescript": "^5.0.0"
diff --git a/scripts/test-npm-package.mjs b/scripts/test-npm-package.mjs
index fae50ca..26a7fc9 100644
--- a/scripts/test-npm-package.mjs
+++ b/scripts/test-npm-package.mjs
@@ -218,7 +218,17 @@ async function testCli({ command, cwd }) {
args: ["generate-sysconfig", "incomplete-pins.circuit.json"],
cwd,
}),
- /input_trace: missing source_port missing_input/,
+ (error) => {
+ assert.match(
+ error.message,
+ /Incomplete Circuit JSON pin records: 1 connection\(s\) reference missing pins[\s\S]*Rebuild the circuit/,
+ );
+ assert.doesNotMatch(
+ error.message,
+ /input_trace|missing_input|throw new Error|\bat \S+ \(/,
+ );
+ return true;
+ },
);
await assert.rejects(readFile(join(cwd, "incomplete-pins.syscfg")), {
code: "ENOENT",
diff --git a/tests/cli/sysconfig-errors-snapshot.test.ts b/tests/cli/sysconfig-errors-snapshot.test.ts
new file mode 100644
index 0000000..0b85437
--- /dev/null
+++ b/tests/cli/sysconfig-errors-snapshot.test.ts
@@ -0,0 +1,130 @@
+import { expect, test } from "bun:test";
+import { spawnSync } from "node:child_process";
+import {
+ existsSync,
+ mkdtempSync,
+ rmSync,
+ symlinkSync,
+ writeFileSync,
+} from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { fileURLToPath } from "node:url";
+import type { CircuitJson } from "circuit-json";
+
+function generateSysconfig({
+ inputName,
+ source,
+}: {
+ inputName: string;
+ source: string;
+}) {
+ const cwd = mkdtempSync(join(tmpdir(), "ti-sysconfig-snapshot-"));
+ try {
+ symlinkSync(
+ new URL("../../node_modules/", import.meta.url),
+ join(cwd, "node_modules"),
+ "junction",
+ );
+ writeFileSync(
+ join(cwd, "package.json"),
+ '{"private":true,"type":"module"}',
+ );
+ writeFileSync(join(cwd, inputName), source);
+ const result = spawnSync(
+ "node",
+ [
+ fileURLToPath(new URL("../../cli/ti.mjs", import.meta.url)),
+ "generate-sysconfig",
+ inputName,
+ ],
+ { cwd, encoding: "utf8" },
+ );
+ if (result.error) throw result.error;
+ return {
+ status: result.status,
+ stdout: result.stdout,
+ stderr: result.stderr,
+ sysconfigWritten: existsSync(join(cwd, "board.syscfg")),
+ };
+ } finally {
+ rmSync(cwd, { recursive: true, force: true });
+ }
+}
+
+test("Circuit JSON error output names unresolved pins without IDs or a stack", () => {
+ const circuitJson: CircuitJson = [
+ {
+ type: "source_component",
+ ftype: "simple_chip",
+ source_component_id: "source_component_123",
+ name: "U1",
+ manufacturer_part_number: "CC2340R52E0RGER",
+ },
+ {
+ type: "source_port",
+ source_component_id: "source_component_123",
+ source_port_id: "source_port_456",
+ name: "DIO11",
+ pin_number: 4,
+ is_gpio: true,
+ },
+ {
+ type: "source_port",
+ source_component_id: "source_component_123",
+ source_port_id: "source_port_789",
+ name: "DIO13",
+ pin_number: 6,
+ is_gpio: true,
+ },
+ {
+ type: "source_trace",
+ source_trace_id: "source_trace_321",
+ connected_source_port_ids: ["source_port_456", "source_port_789"],
+ connected_source_net_ids: [],
+ },
+ ];
+ const { stderr, ...result } = generateSysconfig({
+ inputName: "board.circuit.json",
+ source: JSON.stringify(circuitJson),
+ });
+ expect(result).toEqual({ status: 1, stdout: "", sysconfigWritten: false });
+ expect(stderr).toMatchInlineSnapshot(`
+ "Failed to generate SysConfig: SysConfig conversion failed:
+ Unresolved CC2340 pin configuration for U1 (CC2340R52E0RGER):
+ - U1 pin 4 (DIO11): no GPIO direction or supported peripheral selected
+ - U1 pin 6 (DIO13): no GPIO direction or supported peripheral selected
+ Update U1's TSX pinAttributes with the intended function for each listed pin: set exactly one of isInput: true or isOutput: true for GPIO, or activeCapability: "i2c_sda" / "i2c_scl" for I2C.
+ Datasheet capabilities such as isGpio describe what a pin supports; they do not select how this board uses it.
+ "
+ `);
+});
+
+test("TSX error output explains board choices without a request file or stack", () => {
+ const { stderr, ...result } = generateSysconfig({
+ inputName: "board.circuit.tsx",
+ source: `export default () => (
+
+
+
+
+
+
+ );`,
+ });
+ expect(result).toEqual({ status: 1, stdout: "", sysconfigWritten: false });
+ expect(stderr).toMatchInlineSnapshot(`
+ "Failed to generate SysConfig: SysConfig conversion failed:
+ Unresolved CC2340 pin configuration for U1 (CC2340R52E0RGER):
+ - U1 pin 4 (DIO11): no GPIO direction or supported peripheral selected
+ - U1 pin 6 (DIO13): no GPIO direction or supported peripheral selected
+ Update U1's TSX pinAttributes with the intended function for each listed pin: set exactly one of isInput: true or isOutput: true for GPIO, or activeCapability: "i2c_sda" / "i2c_scl" for I2C.
+ Datasheet capabilities such as isGpio describe what a pin supports; they do not select how this board uses it.
+ "
+ `);
+});
diff --git a/tests/cli/sysconfig-from-circuit.test.mjs b/tests/cli/sysconfig-from-circuit.test.mjs
index aeb672a..7f73a92 100644
--- a/tests/cli/sysconfig-from-circuit.test.mjs
+++ b/tests/cli/sysconfig-from-circuit.test.mjs
@@ -103,8 +103,13 @@ test("connected pins with missing roles fail without a request-file suggestion",
writeFileSync(join(f.cwd, "board.circuit.json"), JSON.stringify(incomplete));
assert.equal(await f.run(["generate-sysconfig", "board.circuit.json"]), 1);
assert.match(f.stderr.join("\n"), /U1.*CC2340R52E0RGER/);
- assert.match(f.stderr.join("\n"), /pin 4.*pinAttributes/);
- assert.doesNotMatch(f.stderr.join("\n"), /request file is missing/);
+ assert.match(f.stderr.join("\n"), /pin 4[\s\S]*pinAttributes/);
+ assert.doesNotMatch(
+ f.stderr.join("\n"),
+ /request file is missing|source_component_id|source_port_id|\bat \S+|throw new Error|convert\.mjs:\d/,
+ );
+ assert.doesNotMatch(f.stderr.join("\n"), /\(mcu,|\(output,/);
+ assert.match(f.stderr.join("\n"), /isInput: true or isOutput: true/);
assert.equal(existsSync(join(f.cwd, "board.syscfg")), false);
});
@@ -121,7 +126,7 @@ test("a missing connected pin record fails instead of writing partial SysConfig"
});
writeFileSync(join(f.cwd, "board.circuit.json"), JSON.stringify(incomplete));
assert.equal(await f.run(["generate-sysconfig", "board.circuit.json"]), 1);
- assert.match(f.stderr.join("\n"), /input_trace: missing source_port input/);
+ assert.match(f.stderr.join("\n"), /1 connection\(s\) reference missing pins/);
assert.match(f.stderr.join("\n"), /Rebuild the circuit/);
assert.equal(existsSync(join(f.cwd, "board.syscfg")), false);
});
@@ -133,10 +138,7 @@ test("an MCU without pin records reports the component and required attributes",
JSON.stringify([circuit()[0]]),
);
assert.equal(await f.run(["generate-sysconfig", "board.circuit.json"]), 1);
- assert.match(
- f.stderr.join("\n"),
- /U1.*CC2340R52E0RGER.*no source_port records/,
- );
+ assert.match(f.stderr.join("\n"), /U1.*CC2340R52E0RGER.*no MCU pin records/);
assert.match(f.stderr.join("\n"), /pinAttributes/);
assert.equal(existsSync(join(f.cwd, "board.syscfg")), false);
});
@@ -211,3 +213,31 @@ test("TSX pinAttributes export selected GPIO roles without a request file", asyn
true,
);
});
+
+test("a TSX pin failure prints actionable labels without the converter stack", async (t) => {
+ const f = fixture(t);
+ writeFileSync(
+ join(f.cwd, "board.circuit.tsx"),
+ `export default () => (
+
+
+
+
+
+
+ );`,
+ );
+ assert.equal(await f.run(["generate-sysconfig", "board.circuit.tsx"]), 1);
+ const message = f.stderr.join("\n");
+ assert.match(message, /U1 \(CC2340R52E0RGER\)/);
+ assert.match(message, /U1 pin 4 \(DIO11\)/);
+ assert.match(message, /U1 pin 6 \(DIO13\)/);
+ assert.match(message, /Update U1's TSX pinAttributes/);
+ assert.doesNotMatch(
+ message,
+ /source_component_\d|source_port_\d|\bat \S+|throw new Error/,
+ );
+ assert.equal(existsSync(join(f.cwd, "board.syscfg")), false);
+});
diff --git a/tests/cli/sysconfig.test.mjs b/tests/cli/sysconfig.test.mjs
index aa4b3e0..fc525f7 100644
--- a/tests/cli/sysconfig.test.mjs
+++ b/tests/cli/sysconfig.test.mjs
@@ -323,6 +323,10 @@ test("missing pin functions are reported without requiring a firmware request fi
const message = f.stderr.join("\n");
assert.match(message, /Unresolved CC2340 pin configuration/);
assert.match(message, /pinAttributes/);
+ assert.doesNotMatch(
+ message,
+ /source_component_\d|source_port_\d|throw new Error|\bat \S+/,
+ );
assert.match(message, /pin 9/);
assert.doesNotMatch(
message,