AnasSarkiz/muse-voice-remote

Muse Voice Remote — bare ESP32-S3R8, 155 TOP JLCPCB parts with real supplier CAD, native 3D assembly and GLB exports. Revised 17 mm case allocation; 0.30 mm via drill /0.45 mm pad. Checked prototype PCB; mechanical, RF and MUSE qualification pending.

Version
1.3.2
License
unset
Stars
0

scripts/export-jlcpcb-footprints.py

#!/usr/bin/env python3
"""Convert retained JLCPCB/EasyEDA package records through easyeda2kicad.

No pads are drawn by this script. Library geometry is the supplier import;
native KiCad export is used for independent routing and DRC.
"""
import hashlib
import json
import pathlib
import re
from easyeda2kicad.easyeda.easyeda_importer import EasyedaFootprintImporter
from easyeda2kicad.kicad.export_kicad_footprint import ExporterFootprintKicad

root = pathlib.Path(__file__).resolve().parents[1]
out = root / "vendor/jlcpcb/library"
out.mkdir(parents=True, exist_ok=True)
manifest = []
selection = json.loads((root / "docs/jlcpcb-import-selection.json").read_text())
imports = json.loads((root / "vendor/jlcpcb/import-manifest.json").read_text())
for part in selection["parts"]:
    source = root / f"vendor/jlcpcb/{part['lcsc']}.raweasy.json"
    raw = json.loads(source.read_text())
    for container in [raw, raw["packageDetail"]]:
        if isinstance(container["dataStr"], str):
            container["dataStr"] = json.loads(container["dataStr"])
    imported = EasyedaFootprintImporter(raw).get_footprint()
    target = out / f"{part['lcsc']}.kicad_mod"
    ExporterFootprintKicad(imported).export(str(target), "")
    # EasyEDA HOLE entries are mechanical/non-plated; easyeda2kicad 1.0.1
    # emits empty-number thru_hole pads. Correct the plating metadata while
    # retaining the imported hole positions and diameters exactly.
    text = target.read_text()
    entry = next(e for e in imports["imports"] if e["lcsc"] == part["lcsc"])
    via_pins = entry.get("thermalViaPinBindings", [])
    source_vias = [s.split("~") for s in raw["packageDetail"]["dataStr"]["shape"] if s.startswith("VIA~")]
    if len(source_vias) != len(via_pins):
        raise RuntimeError(f"Unresolved supplier VIA metadata for {part['lcsc']}")
    head = raw["packageDetail"]["dataStr"]["head"]
    thermal = [( (float(v[1])-float(head["x"]))*0.254,
                 (float(v[2])-float(head["y"]))*0.254, b["pin"].removeprefix("pin"))
               for v,b in zip(source_vias,via_pins)]
    corrected_holes = 0
    corrected_thermal = 0
    def classify_empty_pad(match):
        line = match.group(0)
        at = re.search(r'\(at ([\d.-]+) ([\d.-]+)\)',line)
        if not at: raise RuntimeError("Missing imported drill position")
        x,y = map(float,at.groups())
        binding = next((p for p in thermal if abs(p[0]-x)<0.02 and abs(p[1]-y)<0.02),None)
        if binding:
            return line.replace('(pad "" thru_hole',f'(pad "{binding[2]}" thru_hole').replace('*.Paste ', '')
        return line.replace('(pad "" thru_hole','(pad "" np_thru_hole')
    original_empty = re.findall(r'\(pad "" thru_hole[^\n]+',text)
    text = re.sub(r'\(pad "" thru_hole[^\n]+',classify_empty_pad,text)
    corrected_thermal = len(thermal)
    corrected_holes = len(original_empty)-corrected_thermal
    # Filled EasyEDA pad polygons have no edge stroke. The exporter adds a
    # 0.1 mm stroke inside custom-pad primitives, inflating copper by 0.05 mm
    # on each side; remove that exporter artifact to match the original area.
    text, corrected_polygon_strokes = re.subn(
        r'(\(gr_poly\s+\(pts.*?\)\s*)\(width 0\.1\)', r'\1(width 0)', text, flags=re.S)
    # Custom copper polygons are filled regions in the supplier data. KiCad's
    # primitive parser also needs the explicit fill flag (not a zero-width line).
    text, corrected_polygon_fill = re.subn(
        r'(\(gr_poly\s+\(pts.*?\)\s*\(width 0\))\s*\)',
        r'\1 (fill yes)\n\t\t\t)', text, flags=re.S)
    target.write_text(text)
    manifest.append({"library": f"JLCPCB/{part['lcsc']}", "lcsc": part["lcsc"],
                     "mpn": part["mpn"], "file": str(target.relative_to(root)),
                     "sha256": hashlib.sha256(target.read_bytes()).hexdigest(),
                     "source_file": str(source.relative_to(root)),
                     "source_sha256": hashlib.sha256(source.read_bytes()).hexdigest(),
                     "converter": "easyeda2kicad@1.0.1",
                     "nonplated_hole_metadata_corrections": corrected_holes,
                     "plated_thermal_via_metadata_corrections": corrected_thermal,
                     "filled_polygon_stroke_corrections": corrected_polygon_strokes,
                     "filled_polygon_metadata_corrections": corrected_polygon_fill,
                     "physicalPadIds": sorted({str(p.number) for p in imported.pads})})
(root / "vendor/jlcpcb/native-library-manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
print(f"Converted {len(manifest)} original JLCPCB package footprints; no authored electronic pads.")