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`);
}