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/verify.mjs
import fs from 'node:fs'
import path from 'node:path'
import crypto from 'node:crypto'
import { fileURLToPath } from 'node:url'
import { runAllChecks } from '@tscircuit/checks'
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
const input = fs.readFileSync(path.join(root, 'dist/index/circuit.json'))
const circuit = JSON.parse(input)
const findings = [...circuit.filter(x => /error|warning/.test(x.type)), ...await runAllChecks(circuit)]
const unique = [...new Map(findings.map(x => [JSON.stringify(x), x])).values()]
const counts = Object.fromEntries([...new Set(circuit.map(x => x.type))].map(t => [t, circuit.filter(x => x.type === t).length]))
const components = circuit.filter(x => x.type === 'source_component')
const sourceByName=new Map(components.map(x=>[x.name,x.source_component_id]))
const netNames=new Map(circuit.filter(x=>x.type==='source_net').map(x=>[x.subcircuit_connectivity_map_key,x.name]))
const netAt=(name,pin)=>netNames.get(circuit.find(x=>x.type==='source_port'&&x.source_component_id===sourceByName.get(name)&&x.pin_number===pin)?.subcircuit_connectivity_map_key)
const requiredPins={
U1:{1:'V3V3',2:'EN',3:'ASK',7:'STRAP8',8:'BOOT',9:'GND',10:'STATUS',11:'UART_RX',12:'UART_TX',15:'SCL',16:'STRAP2',17:'SDA',19:'GND'},
J6:{1:'UART_RX',2:'GND',3:'UART_TX'},
U2:{1:'V3V3',2:'USB5V',3:'USB5V',4:'GND',5:'ESP_SW',6:'ESP_BST'},
L1:{1:'ESP_SW',2:'V3V3'},C11:{1:'ESP_BST',2:'ESP_SW'},C12:{1:'V3V3',2:'GND'},
J1:{1:'GND',2:'GND',3:'GND',4:'GND',6:'CC1',12:'CC2',13:'GND',14:'GND',15:'USB5V',16:'USB5V'},
U4:{1:'USB5V',2:'GND',3:'USB5V',5:'PM3V3'},
J3:{1:'PM3V3',2:'GND',3:'SDA',4:'SCL',5:'GND',6:'PM3V3'},
R1:{1:'CC1',2:'GND'},R2:{1:'CC2',2:'GND'},R8:{1:'V3V3',2:'SDA'},R9:{1:'V3V3',2:'SCL'},D1:{1:'LED_STATUS',2:'GND'},
C4:{1:'USB5V',2:'GND'},C8:{1:'PM3V3',2:'GND'},C9:{1:'PM3V3',2:'GND'},C10:{1:'PM3V3',2:'GND'},
}
const pinAudit=Object.entries(requiredPins).flatMap(([name,pins])=>Object.entries(pins).map(([pin,expected])=>({name,pin:Number(pin),expected,actual:netAt(name,Number(pin))??null})))
const sheets=circuit.filter(x=>x.type==='schematic_sheet')
const annotationManifest=JSON.parse(fs.readFileSync(path.join(root,'validation/schematic-annotations.json')))
const u1pcb=circuit.find(x=>x.type==='pcb_component'&&x.source_component_id===sourceByName.get('U1'))
const u1Courtyard=circuit.find(x=>x.type==='pcb_courtyard_outline'&&x.pcb_component_id===u1pcb?.pcb_component_id)
const antenna=circuit.find(x=>x.type==='pcb_keepout'&&x.shape==='rect'&&x.excluded_pcb_component_ids?.includes(u1pcb?.pcb_component_id))
const stock=JSON.parse(fs.readFileSync(path.join(root,'validation/stock-live.json')))
const stockByPart=new Map(stock.pcb_parts.map(x=>[x.part,x]))
const populate=components.filter(x=>x.name!=='U3'&&x.supplier_part_numbers?.jlcpcb?.length)
const quantityByPart=new Map()
for(const x of populate){const p=x.supplier_part_numbers.jlcpcb[0];quantityByPart.set(p,(quantityByPart.get(p)??0)+1)}
const assertions = {
seven_named_schematic_sheets: sheets.length===7&&['POWER','CONTROLLER','SENSOR','INTERFACES','MICROPHONE','SPEAKER','DISPLAY'].every(n=>sheets.some(x=>x.name===n)),
every_component_has_a_brief_schematic_explanation: components.every(x=>{
const expected=annotationManifest.components[x.name]
const sheet=sheets.find(s=>s.name===expected?.sheet)
return expected&&circuit.some(t=>t.type==='schematic_text'&&t.schematic_sheet_id===sheet?.schematic_sheet_id&&t.text===`${x.name}: ${expected.explanation}`)
}),
every_component_is_on_the_expected_sheet: components.every(x=>{
const sheet=sheets.find(s=>s.name===annotationManifest.components[x.name]?.sheet)
return circuit.some(t=>t.type==='schematic_component'&&t.source_component_id===x.source_component_id&&t.schematic_sheet_id===sheet?.schematic_sheet_id)
}),
complete_esp_module_courtyard_inside_board: u1Courtyard?.outline.every(p=>Math.abs(p.x)<=23&&Math.abs(p.y)<=35)??false,
internal_antenna_has_both_layer_keepout: antenna?.width===46&&antenna?.height===7.35&&antenna?.center.y===31.325&&['top','bottom'].every(l=>antenna.layers.includes(l)),
pcb_46_by_70_mm: circuit.some(x => x.type === 'pcb_board' && x.width === 46 && x.height === 70),
two_copper_layers: circuit.some(x => x.type === 'pcb_board' && x.num_layers === 2),
usb_c_present: components.some(x => x.name === 'J1' && x.manufacturer_part_number === 'TYPE-C-31-M-12'),
no_battery_charger_or_boost: !components.some(x => /battery|BQ240|TPS610|TPS630|LiPo/i.test(JSON.stringify(x))),
esp_buck_present: components.some(x=>x.name==='U2'&&x.manufacturer_part_number==='AP63203WU-7'&&x.supplier_part_numbers?.jlcpcb?.[0]==='C780769'),
esp32_c3_present: components.some(x => x.manufacturer_part_number === 'ESP32-C3-WROOM-02-N4'),
plug_in_uart_header_present: components.some(x=>x.name==='J6'&&x.manufacturer_part_number==='SM03B-SRSS-TB(LF)(SN)'&&x.supplier_part_numbers?.jlcpcb?.[0]==='C160403'),
sensor_cable_header_present: components.some(x => x.name === 'J3' && x.manufacturer_part_number === 'SM06B-GHS-TB(LF)(SN)' && x.supplier_part_numbers.jlcpcb[0]==='C133065'),
critical_pin_mapping_matches_datasheets: pinAudit.every(x=>x.actual===x.expected),
mechanical_header_tabs_floating: netAt('J3',7)===undefined && netAt('J3',8)===undefined,
audio_and_display_esp_pins_match_design: Object.entries({4:'AUDIO_BCLK',5:'AUDIO_WS',6:'AUDIO_DIN',13:'LCD_SCLK',14:'LCD_MOSI',18:'MIC_ADC'}).every(([p,n])=>netAt('U1',Number(p))===n),
selected_sensor_is_real_component: components.some(x=>x.name==='U3'&&x.manufacturer_part_number==='SEN62-SIN-T'&&!x.supplier_part_numbers?.jlcpcb?.length),
sensor_has_embedded_cad_model: circuit.some(x=>x.type==='cad_component'&&x.source_component_id===sourceByName.get('U3')&&x.model_glb_url?.startsWith('data:model/gltf-binary;base64,')),
carrier_mounting_holes_match_cad: [-18,18].every(x=>[-24,19].every(y=>circuit.some(h=>h.type==='pcb_hole'&&Math.abs(h.x-x)<1e-6&&Math.abs(h.y-y)<1e-6&&h.hole_diameter===2.4))),
sensor_separate_from_jlc_assembly: stock.sensor_module.manufacturer_part==='SEN62-SIN-T'&&stock.sensor_module.excluded_from_jlc_assembly,
i2c_has_external_10k_pullups: ['R8','R9'].every(n=>components.find(x=>x.name===n)?.resistance===10000),
esp_buck_bootstrap_and_output_caps: netAt('C11',1)==='ESP_BST'&&netAt('C11',2)==='ESP_SW'&&netAt('C3',1)==='V3V3'&&netAt('C12',1)==='V3V3',
sensor_regulator_is_separate: netAt('L1',2)==='V3V3'&&netAt('U4',5)==='PM3V3',
each_usb_cc_pin_has_own_5k1_resistor: components.filter(x=>['R1','R2'].includes(x.name)).every(x=>x.resistance===5100),
every_populated_part_has_stock_for_five_boards: [...quantityByPart].every(([part,qty])=>(stockByPart.get(part)?.available_order_qty??0)>=qty*5),
no_reported_design_errors: unique.every(x => !x.type.includes('error')),
}
const report = {
generated_at: new Date().toISOString(),
circuit_json_sha256: crypto.createHash('sha256').update(input).digest('hex'),
tool: '@tscircuit/checks 0.0.171',
scope: 'tscircuit netlist, pin specification, placement, routing and schematic checks; not a substitute for physical prototype validation',
status: Object.values(assertions).every(Boolean) ? 'pass' : 'fail',
design_status: Object.entries(assertions).filter(([key])=>key!=='every_populated_part_has_stock_for_five_boards').every(([,value])=>value)?'pass':'fail',
unverified_stock: [...quantityByPart.keys()].filter(part=>!stockByPart.has(part)),
assertions, counts, critical_pin_audit:pinAudit, stock_required_for_five_boards:Object.fromEntries([...quantityByPart].map(([p,n])=>[p,n*5])), findings: unique,
}
fs.mkdirSync(path.join(root, 'validation'), { recursive: true })
fs.writeFileSync(path.join(root, 'validation/tscircuit-checks.json'), JSON.stringify(report, null, 2) + '\n')
console.log(`${report.status}: ${counts.source_net} nets, ${counts.pcb_trace} routed trace objects; ${unique.length} findings`)
if (report.status !== 'pass') process.exitCode = 1