mohan-bee/mini-mp3-player

Portable, battery-powered MP3 player hardware with USB charging, microSD storage, an MCU, VS10xx audio decoder, headphone output, status controls, and regulated power rails.

Version
1.2.0
License
unset
Stars
0

scripts/package-prototype.py

#!/usr/bin/env python3
"""Package the reviewed Rev B artifacts, without caches, credentials or toolchains."""
import csv
import hashlib
import json
from pathlib import Path
import shutil
import zipfile

root = Path(__file__).resolve().parents[1]
output = root / 'prototype-package'
output.mkdir(exist_ok=True)
package = output / 'mini-mp3-revB-prototype'
if package.exists():
    shutil.rmtree(package)
package.mkdir()

def copy(source, target=None):
    destination = package / (target or source)
    destination.parent.mkdir(parents=True, exist_ok=True)
    shutil.copy2(root / source, destination)

assert 'No shorts detected' in (root/'validation/prototype-release-shorts.txt').read_text()
assert not json.loads((root/'validation/final-drc.json').read_text())['errors']
bom = list(csv.DictReader((root/'manufacturing/bom.csv').open()))
cpl = list(csv.DictReader((root/'manufacturing/pick_and_place.csv').open()))
refs = {ref for row in bom for ref in row['Designator'].split(',')}
assert refs == {row['Designator'] for row in cpl} and len(refs) == 100
assert all(row['Layer'].lower() == 'top' for row in cpl)
assert len(bom) == 35
with zipfile.ZipFile(root/'manufacturing/mini-mp3-revB.zip') as gerbers:
    assert gerbers.testzip() is None
    assert {'F_Cu.gbr','B_Cu.gbr','F_Mask.gbr','B_Mask.gbr','F_Paste.gbr',
            'Edge_Cuts.gbr','drill-L1-L2.drl','drill_npth.drl'} <= set(gerbers.namelist())
    assert 'B_Paste.gbr' not in gerbers.namelist()
    assert gerbers.read('bom.csv') == (root/'manufacturing/bom.csv').read_bytes()
    assert gerbers.read('pick_and_place.csv') == (root/'manufacturing/pick_and_place.csv').read_bytes()

copy('PROTOTYPE.md', 'START_HERE.md')
copy('VALIDATION.md')
for name in ('mini-mp3-revB.zip','bom.csv','pick_and_place.csv','release.circuit.json'):
    copy('manufacturing/'+name)
for name in ('pcb.svg','assembly.svg','netlist.txt'):
    copy('dist/index/'+name, 'drawings/'+name)
for path in sorted((root/'dist/sheets').glob('*.svg')):
    copy(str(path.relative_to(root)), 'drawings/sheets/'+path.name)
for path in sorted((root/'firmware').rglob('*')):
    if not path.is_file():
        continue
    relative = path.relative_to(root)
    if 'build' in relative.parts and (path.parent.name != 'build' or path.suffix not in ('.elf','.bin','.hex','.map')):
        continue
    copy(str(relative))
for path in sorted((root/'mechanical/models').glob('*.glb')):
    if path.name != 'light-pipes.glb': # Superseded by lid-integrated windows.
        copy(str(path.relative_to(root)))
copy('mechanical/README.md')
for name in ('index.circuit.tsx','exploded.circuit.tsx','design.ts','parts.ts',
             'package.json','bun.lock','tsconfig.json','tscircuit.config.json','README.md','PROTOTYPE.md'):
    copy(name,'source/'+name)
for directory in ('imports','routing','scripts'):
    for path in sorted((root/directory).rglob('*')):
        if path.is_file():
            relative=str(path.relative_to(root))
            copy(relative,'source/'+relative)
for path in sorted((root/'mechanical').rglob('*')):
    if path.is_file() and (path.suffix=='.tsx' or path.name in ('model-urls.ts','models.json','README.md','J4-world-vertices.json') or 'model-data' in path.parts):
        relative=str(path.relative_to(root))
        copy(relative,'source/'+relative)
for name in ('prototype-release.txt','prototype-release-shorts.txt','final-drc.json',
             'electrical-audit.json','stencil.json','assembly-orientation.txt','schematic-sheets.json',
             'mechanical-alignment.json','led-window-retention.json','firmware-build.txt',
             'firmware-tests.txt','prototype-typecheck.txt','sheets-netlist.txt',
             'sheets-placement.txt','sheets-schematic-placement.txt'):
    copy('validation/'+name)
for name in ('via-clearance-repairs.json','via-notes-final-build.txt','via-notes-typecheck.txt',
             'via-notes-netlist.txt','via-notes-schematic.txt','via-notes-placement.txt'):
    copy('validation/'+name)

manifest=[]
for path in sorted(package.rglob('*')):
    if path.is_file():
        manifest.append(hashlib.sha256(path.read_bytes()).hexdigest()+'  '+str(path.relative_to(package)))
(package/'SHA256SUMS').write_text('\n'.join(manifest)+'\n')
archive=output/'mini-mp3-revB-prototype.zip'
with zipfile.ZipFile(archive,'w',zipfile.ZIP_DEFLATED) as bundle:
    for path in sorted(package.rglob('*')):
        if path.is_file():
            bundle.write(path,path.relative_to(output))
with zipfile.ZipFile(archive) as bundle:
    assert bundle.testzip() is None
digest=hashlib.sha256(archive.read_bytes()).hexdigest()
(output/'mini-mp3-revB-prototype.zip.sha256').write_text(digest+'  '+archive.name+'\n')
print(f'PASS: 100 top-side placements match 35 BOM lines; Gerber layers and embedded CSVs verified')
print(f'Packaged {len(manifest)} files; {archive.stat().st_size:,} bytes')
print(f'SHA256 {digest}')