krishnax12/watchy-eink-smartwatch
Unfinished prototype (not production-ready): 4 native DRC errors, 2 display-flex intersections, and missing antenna radiator copper remain
- Version
- 1.2.7
- License
- MIT
- Stars
- 1
scripts/import-watchy-symbols.ts
import { KicadSymbolToCircuitJsonConverter } from "kicad-to-circuit-json";
import type { AnyCircuitElement } from "circuit-json";
import type { SchematicSymbol, SymbolPin } from "kicadts";
// Import symbol artwork only. Sheet placement and every wire remain tscircuit-generated.
const sourcePath = process.argv[2];
if (!sourcePath) throw new Error("Pass the path to SQFMI Watchy.kicad_sch");
const source = await Bun.file(sourcePath).text();
const parts = [
["ESP32_S3FN8", "Espressif:ESP32-S3", "ESP32-S3FN8"],
["SR2HARU", "Watchy:SR2HARU", "SR2HARU"],
["RT9080_33", "Regulator_Linear:AP2112K-3.3", "RT9080-33"],
["TP4054", "Battery_Management:MCP73831-2-OT", "TP4054"],
["USBLC6_2SC6", "Power_Protection:USBLC6-2P6", "USBLC6-2SC6"],
["BMA423", "Watchy:BMA423", "BMA423"],
["SWRA117D", "Device:Antenna_Chip", "SWRA117D Antenna"],
["PagerMotor", "Motor:Motor_DC", "Vibration Motor"],
] as const;
function extractExpression(text: string, start: number) {
let depth = 0;
let quoted = false;
let escaped = false;
for (let i = start; i < text.length; i++) {
const char = text[i];
if (quoted) {
if (escaped) escaped = false;
else if (char === "\\") escaped = true;
else if (char === '"') quoted = false;
} else if (char === '"') quoted = true;
else if (char === "(") depth++;
else if (char === ")" && --depth === 0) return text.slice(start, i + 1);
}
throw new Error("Unterminated KiCad library symbol");
}
function collectPins(symbol: SchematicSymbol): SymbolPin[] {
return [...symbol.pins, ...symbol.subSymbols.flatMap(collectPins)];
}
// The library converter fits each symbol to a preview cell. Restore a uniform
// library scale: KiCad's 2.54 mm pin pitch becomes 0.2 schematic units. These
// are local symbol coordinates; no sheet positions or wires are imported.
function normalizeSymbol(
converter: KicadSymbolToCircuitJsonConverter,
pinNamesHidden: boolean,
): AnyCircuitElement[] {
const elements = converter.getOutput();
const librarySymbol = converter.ctx!.kicadSymbolLib!.symbols[0];
const pins = collectPins(librarySymbol).filter((pin) => pin.at);
const ports = elements.filter((element) => element.type === "schematic_port");
const first = pins[0];
const second = pins.find(
(pin) => pin.at!.x !== first.at!.x || pin.at!.y !== first.at!.y,
)!;
const a = ports.find(
(port) => String(port.pin_number) === first.numberString,
)!;
const b = ports.find(
(port) => String(port.pin_number) === second.numberString,
)!;
const originalDistance = Math.hypot(
second.at!.x - first.at!.x,
second.at!.y - first.at!.y,
);
const convertedDistance = Math.hypot(
b.center.x - a.center.x,
b.center.y - a.center.y,
);
const scale = originalDistance / convertedDistance / 12.7;
const origin = elements.find(
(element) => element.type === "schematic_component",
)!.center;
const point = (p: { x: number; y: number }) => ({
x: (p.x - origin.x) * scale,
y: (p.y - origin.y) * scale,
});
return elements.map((element): AnyCircuitElement => {
switch (element.type) {
case "source_port":
return pinNamesHidden && element.pin_number !== undefined
? { ...element, name: `pin${element.pin_number}` }
: element;
case "schematic_component":
return {
...element,
center: point(element.center),
size: {
width: element.size.width * scale,
height: element.size.height * scale,
},
};
case "schematic_port":
return {
...element,
center: point(element.center),
distance_from_component_edge:
(element.distance_from_component_edge ?? 0) * scale,
// KiCad's `pin_names hide` applies to the entire library symbol.
// Keep the ports for tscircuit connectivity, but hide their labels.
display_pin_label: pinNamesHidden
? undefined
: element.display_pin_label,
};
case "schematic_text":
return {
...element,
position: point(element.position),
font_size: element.font_size * scale,
};
case "schematic_line": {
const start = point({ x: element.x1, y: element.y1 });
const end = point({ x: element.x2, y: element.y2 });
return {
...element,
x1: start.x,
y1: start.y,
x2: end.x,
y2: end.y,
stroke_width: (element.stroke_width ?? 0.254) * scale,
};
}
case "schematic_rect":
return {
...element,
center: point(element.center),
width: element.width * scale,
height: element.height * scale,
stroke_width: (element.stroke_width ?? 0.254) * scale,
};
case "schematic_circle":
case "schematic_arc":
return {
...element,
center: point(element.center),
radius: element.radius * scale,
stroke_width: (element.stroke_width ?? 0.254) * scale,
};
case "schematic_path":
return {
...element,
points: element.points.map(point),
stroke_width: (element.stroke_width ?? 0.254) * scale,
};
default:
return element;
}
});
}
for (const [file, libraryId, value] of parts) {
const start = source.indexOf(`(symbol "${libraryId}"`);
if (start < 0) throw new Error(`Missing reference symbol ${libraryId}`);
const definition = extractExpression(source, start)
.replace(/\(property "Reference" "[^"]*"/, '(property "Reference" "{REF}"')
.replace(/\(property "Value" "[^"]*"/, `(property "Value" "${value}"`);
const pinNamesStart = definition.indexOf("(pin_names");
const pinNamesHidden =
pinNamesStart >= 0 &&
/\bhide\b/.test(extractExpression(definition, pinNamesStart));
const converter = new KicadSymbolToCircuitJsonConverter();
converter.addFile(
`${file}.kicad_sym`,
`(kicad_symbol_lib (version 20231120) (generator "kicad_symbol_editor") ${definition})`,
);
converter.runUntilFinished();
if (converter.getWarnings().length)
throw new Error(converter.getWarnings().join("\n"));
await Bun.write(
`symbols/${file}.json`,
`${JSON.stringify(normalizeSymbol(converter, pinNamesHidden), null, 2)}\n`,
);
console.log(`Imported ${libraryId} -> symbols/${file}.json`);
}