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/package-assembly.ts
import JSZip from "jszip"
import { readFile, writeFile, readdir } from "node:fs/promises"
import { createHash } from "node:crypto"
const proof=JSON.parse(await readFile("docs/assembly-browser-verification.json","utf8"))
if(proof.status!=="passed")throw new Error("Browser verification must pass before packaging")
const zip=new JSZip()
const printProof=JSON.parse(await readFile("hardware/enclosure/verification.json","utf8"))
const currentPrintArtifacts=new Set<string>(printProof.parts.flatMap((p:{artifacts:Record<string,string>})=>Object.keys(p.artifacts)))
const options={date:new Date("1980-01-01T00:00:00Z"),createFolders:false}
async function add(path:string,name:string){zip.file(name,await readFile(path),options)}
async function scan(directory:string){
for(const e of (await readdir(directory,{withFileTypes:true})).sort((a,b)=>a.name.localeCompare(b.name))){
const path=`${directory}/${e.name}`
if(directory==="dist/assembly/enclosure"&&/\.(stl|scad)$/.test(e.name)&&!currentPrintArtifacts.has(e.name))continue
if(e.isDirectory())await scan(path)
else if(!(directory==="dist/assembly/lib"&&["OrbitControls.js","GLTFLoader.js","BufferGeometryUtils.js"].includes(e.name)))
await add(path,path.slice("dist/".length))
}
}
await scan("dist/assembly")
await scan("dist/tscircuit")
for(const name of ["review.html","circuit.json","pcb-top.svg","pcb-top.png","schematic.svg","bom.csv","placement.json",
"MuseVoiceRemote.kicad_pcb","MuseVoiceRemote.kicad_pro","native-drc.txt","diagnostics.json"])
await add(`dist/${name}`,name)
await add("manufacturing/MuseVoiceRemote-fabrication-v1.3.1.zip","manufacturing/MuseVoiceRemote-fabrication-v1.3.1.zip")
await add("docs/3d-assembly.md","README.md")
await add("docs/cable-routing.md","cable-routing.md")
await add("docs/3d-viewer-improvements.md","3d-viewer-improvements.md")
await add("docs/control-redesign-review.md","control-redesign-review.md")
await add("docs/control-clearance-verification.json","assembly/control-clearance-verification.json")
await add("docs/enclosure-printing-and-assembly.md","enclosure-printing-and-assembly.md")
await add("docs/assembly-browser-verification.json","assembly/browser-verification.json")
for(const name of ["enclosure-render-verification.json","viewer-asset-verification.json","viewer-performance-baseline.json","viewer-performance-refined.json"])
await add(`docs/${name}`,`assembly/${name}`)
await add("docs/tscircuit-viewer-verification.json","tscircuit/verification.json")
const bytes=await zip.generateAsync({type:"nodebuffer",compression:"DEFLATE",compressionOptions:{level:9}})
await JSZip.loadAsync(bytes,{checkCRC32:true})
const file="dist/MuseVoiceRemote-3d-assembly.zip"
await writeFile(file,bytes)
const report={file,bytes:bytes.length,sha256:createHash("sha256").update(bytes).digest("hex"),browser_verification:"passed",
usage:"Unzip, run python3 -m http.server 8080 in the extracted folder, open http://localhost:8080/tscircuit/#tab=cad (official viewer) or /assembly/ (detailed assembly controls). Native GLB models can also be imported directly into a 3D viewer."}
await writeFile("docs/assembly-bundle.json",JSON.stringify(report,null,2)+"\n")
console.log(JSON.stringify(report,null,2))
// Small enclosure-only download, with the same verified sources as the assembly.
const enclosureZip=new JSZip()
for(const name of (await readdir("hardware/enclosure")).sort()){
if(/\.(stl|scad)$/.test(name)&&!currentPrintArtifacts.has(name))continue
enclosureZip.file(name,await readFile(`hardware/enclosure/${name}`),options)
}
enclosureZip.file("README.md",await readFile("docs/enclosure-printing-and-assembly.md"),options)
const enclosureBytes=await enclosureZip.generateAsync({type:"nodebuffer",compression:"DEFLATE",compressionOptions:{level:9}})
await JSZip.loadAsync(enclosureBytes,{checkCRC32:true})
await writeFile("dist/MuseVoiceRemote-enclosure-prototype.zip",enclosureBytes)
await writeFile("docs/enclosure-bundle.json",JSON.stringify({file:"dist/MuseVoiceRemote-enclosure-prototype.zip",
bytes:enclosureBytes.length,sha256:createHash("sha256").update(enclosureBytes).digest("hex"),
status:"fit prototype; read README before printing"},null,2)+"\n")