muse/book-reading-clip-lamp
A Wi‑Fi/USB‑C ESP32‑S3 reading lamp with OV5640 camera, microphone, speaker amplifier, rechargeable Li‑Po power management, six-LED boost light, USB protection/charging detection, and a clip/gooseneck enclosure with external FlexPIFA antenna.
- Version
- 0.4.3
- License
- unset
- Stars
- 0
verify-base-controls.py
"""Audit the single-PCB design and remote switch connections in compiled JSON."""
import csv,json,math,sys
from pathlib import Path
p=Path(__file__).parent
j=json.loads((p/(sys.argv[1] if len(sys.argv)>1 else 'dist/index/circuit.json')).read_text())
components={x['name']:x for x in j if x['type']=='source_component'}
ports={x['source_port_id']:x for x in j if x['type']=='source_port'}
nets={x['source_net_id']:x['name'] for x in j if x['type']=='source_net'}
def port(ref,pin):
matches=[x for x in ports.values() if x.get('source_component_id')==components[ref]['source_component_id'] and x.get('pin_number')==pin]
assert len(matches)==1,(ref,pin,matches)
return matches[0]
def expect(ref,pin,net):
pid=port(ref,pin)['source_port_id']
actual={nets[nid] for x in j if x['type']=='source_trace' and pid in x['connected_source_port_ids'] for nid in x.get('connected_source_net_ids',[])}
assert actual=={net},(ref,pin,actual,net)
assert 'SW1' not in components and 'SW3' not in components
assert len([x for x in j if x['type']=='pcb_board'])==1
assert 'J4' not in components
for pin,net in enumerate(['VBAT','GND','NTC','GND','SPK_P','SPK_N','ASK_REMOTE','PWR_REMOTE'],1):expect('J3',pin,net)
assert components['J3']['manufacturer_part_number']=='B8B-XH-A(LF)(SN)'
assert components['J3']['supplier_part_numbers']['jlcpcb']==['C157972']
for ref,pin,net in [('R18',1,'ASK'),('R18',2,'V3V3'),('R35',1,'ASK_REMOTE'),('R35',2,'ASK'),('C36',1,'ASK'),('C36',2,'GND'),('U1',36,'ASK'),('R36',1,'PWR_REMOTE'),('R36',2,'SYS_EN'),('R25',1,'SYS'),('R25',2,'SYS_EN'),('C37',1,'SYS_EN'),('C37',2,'GND'),('U3',12,'SYS_EN'),('SW2',1,'LIGHT_KEY'),('SW2',2,'GND')]:expect(ref,pin,net)
for ref,value in [('R18',10000),('R25',100000),('R35',1000),('R36',1000)]:assert components[ref]['resistance']==value
for ref,value in [('C36',1e-8),('C37',1e-7)]:assert math.isclose(components[ref]['capacitance'],value)
for pin in range(1,9):
pp=next(e for e in j if e['type']=='pcb_port' and e.get('source_port_id')==port('J3',pin)['source_port_id'])
assert math.isclose(pp['x'],(pin-4.5)*2.5,abs_tol=0.0001) and math.isclose(pp['y'],-20.3),(pin,pp)
pc=next(e for e in j if e['type']=='pcb_component' and e['source_component_id']==components['J3']['source_component_id'])
assert any(e['type']=='cad_component' and e.get('pcb_component_id')==pc['pcb_component_id'] and 'C157972' in e.get('model_obj_url','') for e in j)
header_holes=[e for e in j if e['type']=='pcb_plated_hole' and e['pcb_component_id']==pc['pcb_component_id']]
assert len(header_holes)==8
assert len({h['pcb_plated_hole_id'] for h in header_holes})==8
assert all(h['pcb_plated_hole_id'] and math.isclose(h['hole_diameter'],0.9000236) for h in header_holes)
pin1=next(h for h in header_holes if h['pcb_port_id']==next(e['pcb_port_id'] for e in j if e['type']=='pcb_port' and e.get('source_port_id')==port('J3',1)['source_port_id']))
assert pin1['shape']=='circular_hole_with_rect_pad'
assert math.isclose(pin1['rect_pad_width'],1.6999966) and math.isclose(pin1['rect_pad_height'],1.6999966)
assert pin1['hole_offset_x']==0 and pin1['hole_offset_y']==0, 'J3 pin 1 needs explicit centered drill offsets for in-memory DRC'
rows=list(csv.DictReader((p/'base-controls-wiring.csv').open()))
assert len(rows)==8 and {r['Head terminal'] for r in rows}=={f'J3.{i}' for i in range(1,9)}
bom=list(csv.DictReader((p/'bill-of-materials.csv').open(encoding='utf-8-sig')))
for ref,part in [('SW1','PV5S24011G'),('SW3','100SP1T1B1M1REH')]:
r=next(r for r in bom if r['Reference designators']==ref)
assert r['Assembly location']=='Clip base lid' and r['Manufacturer part number']==part
models={e['name'] for e in j if e['type']=='cad_component' and 'name' in e}
# Mechanical inventory is independent of netlist component numbering.
parts=json.loads((p/'mechanical/parts.json').read_text())
assert len([x for x in parts if x['id'].startswith('harness-wire-')])==8
assert {'base-chat-switch','base-power-switch','light-cap','head-harness-plug'} <= {x['id'] for x in parts}
assert 'ask-cap' not in {x['id'] for x in parts}
print('PASS: one PCB; SW1/SW3 off-board; SW2 remains; eight harness wires; remote Chat and power filter topology/values; J3 keyed eight-pin header and cable plug; panel-switch BOM/models')
print('Nominal OFF EN at SYS=4.4 V: %.4f V; ON RC time constant: %.3f s' % (4.4*1000/101000,100000*1e-7))
print('Hardware fit, debounce, interference immunity and switch operation require physical tests.')