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/audit.mjs

import fs from 'node:fs'
import assert from 'node:assert/strict'

const circuit = JSON.parse(fs.readFileSync('dist/index/circuit.json', 'utf8'))
const byType = type => circuit.filter(e => e.type === type)
const components = byType('source_component')
const nets = byType('source_net')
const ports = byType('source_port')
let checks = 0
function pinNet(ref, pin, expected) {
  const component = components.find(c => c.name === ref)
  assert(component, `Missing ${ref}`)
  const matches = ports.filter(p => p.source_component_id === component.source_component_id && p.pin_number === pin)
  assert(matches.length, `Missing ${ref}.${pin}`)
  const net = nets.find(n => n.name === expected)
  assert(net, `Missing net ${expected}`)
  for (const port of matches) assert.equal(port.subcircuit_connectivity_map_key, net.subcircuit_connectivity_map_key, `${ref}.${pin} must connect to ${expected}`)
  checks++
}
for (const pin of [1,9,24,36,48]) pinNet('U5', pin, 'V3V3')
for (const pin of [8,23,35,47]) pinNet('U5', pin, 'GND')
pinNet('U5',34,'SWDIO'); pinNet('U5',37,'SWCLK'); pinNet('U5',7,'NRST')
for (const pin of [5,7,24,31]) pinNet('U6',pin,'V2V5')
for (const pin of [6,14,19,26,32]) pinNet('U6',pin,'V3V3')
for (const pin of [38,43,45]) pinNet('U6',pin,'V2V7')
for (const pin of [4,16,20,21,22,37,40,41,47]) pinNet('U6',pin,'GND')
for (const [pin,net] of [[3,'VS_RESET'],[8,'VS_DREQ'],[13,'VS_XDCS'],[23,'VS_XCS'],[28,'VS_SCK'],[29,'VS_MOSI'],[30,'VS_MISO'],[42,'GBUF'],[44,'RCAP']]) pinNet('U6',pin,net)
for (const [ref,pin,net] of [['U1',6,'USB500_MODE'],['U1',5,'GND'],['R37',1,'USB500_MODE'],['R37',2,'USB5V'],['U1',17,'GND'],['U1',1,'NTC'],['J2',1,'BAT'],['J2',2,'NTC'],['J2',3,'GND'],['Q1',2,'SYS_RAW'],['Q1',3,'SYS'],['Q2',2,'BAT'],['Q2',3,'BAT_SENSE'],['J4',1,'GND'],['J4',2,'HP_L'],['J4',3,'HP_R'],['J4',4,'GND'],['C43',1,'LEFT'],['C43',2,'HP_L'],['C44',1,'RIGHT'],['C44',2,'HP_R']]) pinNet(ref,pin,net)
for (const [pin,net] of [[15,'GND'],[16,'USB5V'],[18,'CC1'],[24,'CC2'],[25,'USB5V'],[26,'GND']]) pinNet('J1',pin,net)
for (const [ref,pin,net] of [['R1',1,'CC1'],['R1',2,'GND'],['R2',1,'CC2'],['R2',2,'GND'],['C30',1,'RCAP'],['C30',2,'GND']]) pinNet(ref,pin,net)
for (const [ref,code] of [['U5','C3001172'],['U6','C9921'],['C18','C1647'],['C19','C1647'],['C31','C1663'],['C32','C1663']]) {
 assert.equal(components.find(c=>c.name===ref).supplier_part_numbers.jlcpcb[0],code,`${ref} part selection`)
 checks++
}

// Rev B has exactly four user pushbuttons, no speaker or amplifier circuitry.
for (const [ref,pin,net] of [['U5',11,'PLAY'],['U5',12,'PREV'],['U5',13,'NEXT'],['U5',18,'MODE'],['SW1',1,'PLAY'],['SW2',1,'PREV'],['SW3',1,'NEXT'],['SW4',1,'MODE']]) pinNet(ref,pin,net)
for (const ref of ['SW1','SW2','SW3','SW4']) pinNet(ref,2,'GND')
assert.deepEqual(components.filter(c => /^SW[1-6]$/.test(c.name)).map(c=>c.name).sort(), ['SW1','SW2','SW3','SW4'])
checks++
assert(!components.some(c=>['U7','LS1','R21','R22','R23','R24','C36','C37','C38','C39','SW5','SW6','R44'].includes(c.name)))
assert(!nets.some(n=>/^(AMP_|SPK_|MIX$|VOL_DOWN$|VOL_UP$)/.test(n.name)))
checks++
const isolated = ['USB5V','BAT','SYS_RAW','SYS','V3V3','V2V5','V2V7','GND','GBUF','LEFT','RIGHT','HP_L','HP_R']
assert.equal(new Set(isolated.map(name => nets.find(n => n.name === name)?.subcircuit_connectivity_map_key)).size, isolated.length, 'Power and audio nets must not be shorted')
checks++
const physical = byType('pcb_component').filter(c => !c.do_not_place && components.some(s => s.source_component_id === c.source_component_id))
assert(physical.every(c => c.layer === 'top'), 'All fitted parts must be on top')
checks++
for (const c of physical) {
  const source = components.find(s => s.source_component_id === c.source_component_id)
  if (source.ftype === 'simple_test_point') continue
  assert(source.supplier_part_numbers?.jlcpcb?.length, `Missing LCSC procurement code: ${source.name}`)
}
checks++
assert(byType('pcb_trace').length > 0, 'Board has no routed traces')
checks++
assert(byType('pcb_via').every(v => v.hole_diameter >= 0.3-1e-6), 'Via holes must be at least 0.30 mm')
checks++
assert(byType('pcb_via').every(v => v.outer_diameter >= 0.45-1e-6), 'Via copper pads must be at least 0.45 mm')
checks++
const errors = circuit.filter(e => e.type.endsWith('_error'))
const report = { checksPassed: checks, components: components.length, nets: nets.length, routedTraces: byType('pcb_trace').length, vias: byType('pcb_via').length, drcErrors: errors.map(e => ({ type: e.type, message: e.message })) }
fs.mkdirSync('validation',{recursive:true})
fs.writeFileSync('validation/electrical-audit.json',JSON.stringify(report,null,2)+'\n')
console.log(`${checks} electrical/assembly assertions passed; ${errors.length} DRC errors`)
if (process.argv.includes('--require-clean')) assert.equal(errors.length,0,'DRC must be clear before fabrication release')