abse/museair-usb
USB-C–powered ESP32-C3 controller board with AP63203 buck conversion, AP2112K 3.3V regulation, I²C SEN62 sensor interface, status LED, buttons, UART/I²C headers, and extensive decoupling.
- Version
- 0.5.7
- License
- unset
- Stars
- 0
scripts/make-preview.mjs
import fs from 'node:fs'
import path from 'node:path'
import {fileURLToPath} from 'node:url'
import {Resvg} from '@resvg/resvg-js'
import {convertCircuitJsonToSchematicSvg,convertCircuitJsonToStackedSchematicSheetsSvg} from 'circuit-to-svg'
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..')
const cs=JSON.parse(fs.readFileSync(path.join(root,'dist/index/circuit.json')))
const sc=new Map(cs.filter(x=>x.type==='source_component').map(x=>[x.source_component_id,x]))
const placed=cs.filter(x=>x.type==='pcb_component'&&!x.do_not_place&&sc.get(x.source_component_id)?.name!=='U3'&&sc.get(x.source_component_id)?.supplier_part_numbers?.jlcpcb?.length).map(x=>({...sc.get(x.source_component_id),placement:x}))
const csv=rows=>rows.map(r=>r.map(v=>'"'+String(v??'').replaceAll('"','""')+'"').join(',')).join('\r\n')+'\r\n'
const packageName=x=>x.name.startsWith('R')?'0603':x.name.startsWith('C')?(x.name==='C14'?'1206':['C6','C7','C9','C11','C13','C15','C16','C17','C18','C19','C21','C22','C23'].includes(x.name)?'0603':'0805'):({U1:'ESP32-C3-WROOM-02',U2:'TSOT26 synchronous buck',L1:'4 x 4mm shielded power inductor',U4:'SOT-23-5',U5:'SOT-23-5',U6:'TQFN-16-EP 3x3mm',U7:'TSSOP-16',MIC1:'MEMS top-port 2.75x1.85mm',J7:'Molex PicoBlade 2P 1.25mm right-angle SMT',J8:'FPC 10P 0.5mm dual-contact',J1:'TYPE-C-31-M-12',J3:'JST GH 6P 1.25mm side-entry SMT',J6:'JST SH 3P 1.0mm side-entry SMT',D1:'0805',SW1:'TS-1102S-B-H-B 6x6mm SMT H7.5mm',SW2:'TS-1088-AR02016',SW3:'TS-1088-AR02016'})[x.name]
const value=x=>x.inductance?(typeof x.inductance==='string'?x.inductance:`${x.inductance*1e6}uH`):x.resistance?`${x.resistance/1000}k`:x.capacitance?`${+(x.capacitance*1e6).toFixed(3)}uF`:x.manufacturer_part_number
const groups=new Map()
for(const x of placed){const part=x.supplier_part_numbers.jlcpcb[0];if(!groups.has(part))groups.set(part,{part,refs:[],comment:value(x),mfr:x.manufacturer_part_number,footprint:packageName(x)});groups.get(part).refs.push(x.name)}
const bom=[...groups.values()]
const order=path.join(root,'order');fs.mkdirSync(order,{recursive:true})
fs.writeFileSync(path.join(order,'bom-jlcpcb.csv'),csv([['Comment','Designator','Footprint','LCSC Part #'],...bom.map(x=>[x.comment,x.refs.join(','),x.footprint,x.part])]))
// Shared native plot origin: lower-left, X right and Y up.
fs.writeFileSync(path.join(order,'cpl-jlcpcb.csv'),csv([['Designator','Mid X','Mid Y','Layer','Rotation'],...placed.map(x=>[x.name,`${(x.placement.center.x+23).toFixed(4)}mm`,`${(x.placement.center.y+35).toFixed(4)}mm`,x.placement.layer,x.placement.rotation])]))
fs.writeFileSync(path.join(order,'parts-all.csv'),csv([['References','Quantity per board','Manufacturer part','Value','Package','JLCPCB part','Assembly'],...bom.map(x=>[x.refs.join(','),x.refs.length,x.mfr,x.comment,x.footprint,x.part,'Top-side PCBA']),['U3',1,'Sensirion SEN62-SIN-T','Laser PM1/PM2.5/PM4/PM10, RH and T','55.2 x 25.6 x 21.3 mm','','Buy separately; install after reflow'],['W1',1,'JST GHR-06V-S to GHR-06V-S cable','6 wires, AWG26, one-to-one, <=100mm','','','Buy separately; verify continuity'],['MC1',1,'MuseAir SEN62 carrier','mechanical/sen62-carrier.stl','Insulating printed prototype','','Separate fabrication'],['PCB1-PCB4',4,'M2 x 5 screws, insulating washers, blind inserts','PCB attaches internally to enclosure','','','Manual installation'],['H1-H2',2,'M2 x 5 screws and blind inserts','Sensor holder attaches to base; access after PCB removal','','','Manual installation'],['LID1-LID4',4,'Recessed M2 enclosure screws','Separate removable top lid','','','Manual installation'],['LCD1',1,'BuyDisplay ER-TFT2.79-1','428x142 colour IPS, factory 60mm 10pin flex','69.43x24.38x2.1mm','','Buy separately; connect to J8'],['SPK1',1,'Same Sky CMS-28588N-L152A','8 ohm, 2W rated','28mm diameter x 5.5mm','','Buy separately; supplied Molex51021-0200 plug'],['W2',1,'Included stock speaker lead','Shorten red27mm black23mm; retain51021-0200 plug','','','Recrimp with compatible standard contacts; direct J7 connection'],['LP1',1,'Bivar VLP-350-F','Flat clear light pipe, 8.89mm body height','3mm lens, two PCB press-fit pins','','Buy separately; install after reflow'],['T1,T2',2,'Panduit PLT2M-C or equivalent','Nylon 2.5 x 1.1mm, 203mm long','','','Manual retention; keep air openings clear']]))
const preview=path.join(root,'preview');fs.mkdirSync(preview,{recursive:true})
for(const name of ['fabrication-top','fabrication-bottom']){const svg=fs.readFileSync(path.join(preview,`${name}.svg`));fs.writeFileSync(path.join(preview,`${name}.png`),new Resvg(svg,{fitTo:{mode:'width',value:1000}}).render().asPng())}
const sheets=cs.filter(x=>x.type==='schematic_sheet').sort((a,b)=>a.sheet_index-b.sheet_index)
for(const sheet of sheets){
const svg=convertCircuitJsonToSchematicSvg(cs,{schematicSheetId:sheet.schematic_sheet_id,width:1800,height:1260})
const stem=`schematic-${sheet.sheet_index}-${sheet.name.toLowerCase()}`
fs.writeFileSync(path.join(preview,`${stem}.svg`),svg)
fs.writeFileSync(path.join(preview,`${stem}.png`),new Resvg(svg).render().asPng())
}
const sch=convertCircuitJsonToStackedSchematicSheetsSvg(cs,{width:1800,height:1260})
fs.writeFileSync(path.join(preview,'schematic-all-sheets.svg'),sch)
fs.writeFileSync(path.join(preview,'schematic.png'),new Resvg(sch).render().asPng())
const kd=JSON.parse(fs.readFileSync(path.join(root,'validation/kicad-drc.json')))
const tc=JSON.parse(fs.readFileSync(path.join(root,'validation/tscircuit-checks.json')))
const stock=JSON.parse(fs.readFileSync(path.join(root,'validation/stock-live.json')))
const stockMap=new Map(stock.pcb_parts.map(x=>[x.part,x]))
const html=`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>MuseAir USB v0.5</title><style>body{margin:0;background:#eef4f4;color:#16363c;font:16px/1.6 system-ui}main{max-width:1100px;margin:auto;padding:32px 24px}h1{font-size:48px}a{color:#007a8b}.card{padding:24px;background:white;border-radius:14px;margin:20px 0}.grid{display:grid;grid-template-columns:1fr 1fr;gap:24px}img{max-width:100%}.pcb{height:630px;width:100%;object-fit:contain;background:#152020}table{border-collapse:collapse;width:100%;font-size:14px}th,td{border-bottom:1px solid #dce7e7;padding:8px;text-align:left}.scroll{overflow:auto}button{font:inherit;padding:5px 16px;cursor:pointer}.status{background:#e4f4ed;padding:14px}@media(max-width:750px){.grid{grid-template-columns:1fr}}</style><main><h1>MuseAir USB v0.5</h1><p>46 × 70 mm · two layers · USB-C · ESP32-C3 Wi-Fi / BLE · Sensirion SEN62 laser PM, temperature and humidity.</p>
<section class="card"><p class="status">tscircuit: ${tc.status}, ${tc.findings.length} findings. KiCad: ${kd.violations.length} violations, ${kd.unconnected_items.length} unconnected items.</p><p>PCB fabrication and ${placed.length}-part assembly files are prepared. Buy the sensor and cable separately. Real sensor fit, cable continuity, power ripple and firmware need prototype checks.</p><p><a href="order/gerbers.zip">Gerber ZIP</a> · <a href="order/bom-jlcpcb.csv">Assembly BOM</a> · <a href="order/cpl-jlcpcb.csv">Placement CPL</a> · <a href="README.txt">Build notes</a></p></section>
<section class="card grid"><div><h2>Manufacturing layers</h2><button data-layer="top">Top</button> <button data-layer="bottom">Bottom</button><img id="pcb" class="pcb" src="preview/fabrication-top.svg" alt="Manufacturing copper and drills"></div><div><h2>Sensor connection</h2><p>J3: genuine JST SM06B-GHS-TB(LF)(SN), C133065, six pins, 1.25 mm. A GH-to-GH cable is a separate item. Verify one-to-one continuity: 1=3.3V, 2=GND, 3=SDA, 4=SCL, 5=GND, 6=3.3V.</p><p>U4 supplies the dedicated sensor 3.3 V rail. R8/R9 are 10 kΩ pullups. SDA=GPIO1, SCL=GPIO3. The old 5 V UART sensor wiring is removed.</p><p>Four Ø2.4 mm holes match the carrier: (−18,−24), (+18,−24), (−18,+19), (+18,+19). The SEN62 has no screw holes. Two nylon ties retain it in the insulating guided cradle.</p><p>The ESP antenna fits completely inside the PCB outline. Its top band stays clear of copper on both layers. Wi-Fi / BLE range remains a prototype test. The sensor housing is 4 mm below the PCB underside.</p><p>ASK, LED, RESET, BOOT, UART programming and optional I²C contacts remain. PM1/PM2.5/PM4/PM10, RH and T are measured; firmware and agent integration are separate work.</p><p><a href="mechanical/sen62-carrier.stl">Carrier STL</a> · <a href="mechanical/sen62-carrier.step">Carrier STEP</a> · <a href="order/harness.txt">Cable map</a> · <a href="order/mechanical-assembly.txt">Mounting instructions</a></p></div></section>
<section class="card"><h2>Mounting geometry</h2><img src="mechanical/mounting-drawing.svg" alt="Dimensioned carrier holes"><img src="mechanical/assembly-preview.png" alt="Sensor and carrier assembly CAD"><p>Official sensor CAD with a derived prototype carrier. The carrier clears the maximum body envelope. Air openings face away from the PCB. Enclosure ducts and sealing require a separate design check.</p></section>
<section class="card"><h2>JLCPCB inventory</h2><p>Existing part inventory was checked October 4, 2026; newly added J6, buck regulator and inductor availability is unverified. Stock is not reserved. Sensor, cable and mounting hardware are outside JLC assembly.</p><div class="scroll"><table><tr><th>References</th><th>Part / value</th><th>JLCPCB</th><th>Orderable</th></tr>${bom.map(x=>`<tr><td>${x.refs.join(', ')}</td><td>${x.mfr}<br>${x.comment}</td><td><a href="https://jlcpcb.com/partdetail/${x.part}">${x.part}</a></td><td>${stockMap.get(x.part)?.available_order_qty.toLocaleString('en-US')??'Not checked'}</td></tr>`).join('')}</table></div></section>
<section class="card"><h2>Validation</h2><p><a href="validation/tscircuit-checks.json">Netlist / routing</a> · <a href="validation/kicad-drc.json">Native DRC</a> · <a href="mechanical/model-validation.json">Mechanical clearance</a> · <a href="validation/import-provenance.json">Exact imports</a></p>${sheets.map(s=>`<details><summary>${s.display_name}</summary><img src="preview/schematic-${s.sheet_index}-${s.name.toLowerCase()}.png" alt="${s.display_name}"></details>`).join('')}</section><p>Hardware has not been built or tested. Meta/Muse API integration is not established. No order has been placed.</p></main><script>for(const b of document.querySelectorAll('[data-layer]'))b.onclick=()=>document.getElementById('pcb').src='preview/fabrication-'+b.dataset.layer+'.svg';</script></html>`
fs.writeFileSync(path.join(root,'review.html'),html)
console.log(`Saved previews, BOM and CPL: ${placed.length} parts; ${bom.length} unique JLCPCB parts`)