imrishabh18/rp2040-motor-controller

An RP2040-based NEMA17 stepper-motor controller with USB-C programming and PD motor-power negotiation, DRV8825 dual H-bridge drive, current/temperature telemetry, protected power filtering, status RGB LED, buzzer alarm, and optional magnetic encoder.

Version
1.0.41
License
unset
Stars
3

scripts/export-assembly.py

"""Export fabrication files, omitting DNP parts from all assembly CSVs."""
import csv
import hashlib
import io
import json
from pathlib import Path
import subprocess
import zipfile

root = Path(__file__).resolve().parents[1]
subprocess.run(['bunx', '--no-install', 'tsci', 'export', 'dist/index/circuit.json',
                '--format', 'gerbers', '--output', 'gerbers.zip'], cwd=root, check=True)
folder = root / 'dist/index'
circuit = json.loads((folder / 'circuit.json').read_text())
sources = {e['source_component_id']: e for e in circuit if e['type'] == 'source_component'}
dnp = {sources[e['source_component_id']]['name'] for e in circuit
       if e['type'] == 'pcb_component' and e.get('do_not_place')}
bare = {s['name'] for s in sources.values() if s['name'].startswith(('TP_', 'MH'))}

# Supplier placement orientation is independent of the local CAD-model offset.
# Verified against the live JLCPCB lead-to-pad preview on 2026-09-15:
# Q_BUZZER / C20917 uses a zero offset. Q_PD_ENABLE now uses the exact
# C85202 supplier footprint rotated 270 degrees; its former 180-degree generic
# footprint correction is no longer needed. The resulting placement remains 270.
rotation_corrections = {'Q_BUZZER': ('C20917', 0), 'Q_PD_ENABLE': ('C85202', 0)}
pcb_by_name = {sources[e['source_component_id']]['name']: e for e in circuit
               if e['type'] == 'pcb_component' and e['source_component_id'] in sources}

def correct_jlcpcb_rotation(data):
    rows = list(csv.DictReader(io.StringIO(data.decode().replace('\\r\\n', '\n'))))
    for row in rows:
        if row['Designator'] not in rotation_corrections:
            continue
        part, offset = rotation_corrections[row['Designator']]
        pcb = pcb_by_name[row['Designator']]
        source = sources[pcb['source_component_id']]
        assert part in source['supplier_part_numbers']['jlcpcb']
        assert pcb['layer'] == 'top'
        # Fail rather than double-correct if a newer CLI starts correcting CPLs.
        assert float(row['Rotation']) % 360 == pcb['rotation'] % 360
        row['Rotation'] = str((float(row['Rotation']) + offset) % 360)
    out = io.StringIO(newline='')
    writer = csv.DictWriter(out, fieldnames=list(rows[0]))
    writer.writeheader()
    writer.writerows(rows)
    return out.getvalue().encode()

def filtered(data, excluded):
    # This CLI version emits literal escape sequences in its CSV line endings.
    text = data.decode().replace('\\r\\n', '\n')
    rows = list(csv.DictReader(io.StringIO(text)))
    kept = [r for r in rows if r['Designator'] not in excluded]
    out = io.StringIO(newline='')
    writer = csv.DictWriter(out, fieldnames=list(rows[0]))
    writer.writeheader()
    writer.writerows(kept)
    return out.getvalue().encode(), kept

archive = folder / 'gerbers.zip'
with zipfile.ZipFile(archive) as z:
    entries = {n: z.read(n) for n in z.namelist()}
entries['pick_and_place.csv'] = correct_jlcpcb_rotation(entries['pick_and_place.csv'])
assembly_designators = {}
for name, target in [('bom.csv', 'assembly-bom.csv'), ('pick_and_place.csv', 'assembly-pick-and-place.csv')]:
    data, rows = filtered(entries[name], dnp | bare)
    entries[name] = data
    (folder / target).write_bytes(data)
    assembly_designators[name] = {r['Designator'] for r in rows}
    assert not (assembly_designators[name] & (dnp | bare))
    if name == 'pick_and_place.csv':
        placements = rows
assert assembly_designators['bom.csv'] == assembly_designators['pick_and_place.csv'], 'BOM/CPL designators differ'
with zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED) as z:
    for name, data in entries.items():
        z.writestr(name, data)
audit = {
    'circuitJsonSha256': hashlib.sha256((folder / 'circuit.json').read_bytes()).hexdigest(),
    'gerbersSha256': hashlib.sha256(archive.read_bytes()).hexdigest(),
    'excludedDoNotPlaceComponents': sorted(dnp),
    'excludedBarePcbAssemblyFeatures': sorted(bare),
    'assemblyTopCount': sum(r['Layer'] == 'top' for r in placements),
    'assemblyBottomComponents': [r['Designator'] for r in placements if r['Layer'] == 'bottom'],
    'jlcpcbRotationCorrectionsDegrees': {name: value[1] for name, value in rotation_corrections.items()},
}
(root / 'reports/fabrication-audit.json').write_text(json.dumps(audit, indent=2) + '\n')
print(json.dumps(audit, indent=2))