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}')