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-usb-charging.py

"""Audit built connectivity and current-mode policy; not a USB compliance test."""
import csv
import itertools
import json
import sys
from pathlib import Path

p = Path(__file__).parent
circuit = json.loads((Path(sys.argv[1]) if len(sys.argv)>1 else p / 'dist/index/circuit.json').read_text())
components = {e['name']: e for e in circuit if e['type'] == 'source_component'}
nets = {e['source_net_id']: e['name'] for e in circuit if e['type'] == 'source_net'}
ports = [e for e in circuit if e['type'] == 'source_port']
traces = [e for e in circuit if e['type'] == 'source_trace']

def net(ref, pin):
    cid = components[ref]['source_component_id']
    port = next(e for e in ports if e['source_component_id'] == cid and e.get('pin_number') == pin)
    found = {nets[n] for t in traces if port['source_port_id'] in t['connected_source_port_ids'] for n in t.get('connected_source_net_ids', [])}
    assert len(found) <= 1, (ref, pin, found)
    return next(iter(found), None)

# Manufacturer physical pin numbers, independent of catalog labels or JSX order.
expected = {
 'U14': {1:None, 2:'USB_DM_DETECTED', 3:'USB_DP_DETECTED', 4:None, 5:'VBUS_BC', 6:'GND', 7:'BC_DP', 8:'BC_DM', 9:'VBUS_BC', 10:'BC_FAST'},
 'U15': {1:'CC1', 2:'CC2', 3:'GND', 4:'CC_VBUS_DET', 5:None, 6:None, 7:'CC_HIGH_N', 8:None, 9:None, 10:'GND', 11:'GND', 12:'USB_LOGIC_3V3'},
 'U16': {1:None, 2:'CC_HIGH_N', 3:'GND', 4:'CC_FAST', 5:'USB_LOGIC_3V3'},
 'U17': {1:'BC_FAST', 2:'GND', 3:'CC_FAST', 4:'CHG_EN1', 5:'USB_LOGIC_3V3', 6:'USB_500MA'},
 'U18': {1:'VBUS', 2:'GND', 3:'VBUS', 4:None, 5:'USB_LOGIC_3V3'},
 'U2': {4:'GND', 5:'USB_SUSPEND', 6:'CHG_EN1', 13:'VBUS'},
 'U1': {13:'USB_DM', 14:'USB_DP', 15:'USB_SUSPEND', 23:'USB_500MA'},
}
for ref, pins in expected.items():
    for pin, wanted in pins.items():
        assert net(ref, pin) == wanted, (ref, pin, wanted, net(ref, pin))
resistors = {
 'R3': (22,'USB_DP_DETECTED','USB_DP'), 'R4':(22,'USB_DM_DETECTED','USB_DM'),
 'R5':(1780,'ISET','GND'), 'R8':(100000,'USB_500MA','GND'), 'R13':(10000,'USB_SUSPEND','GND'),
 'R37':(2.2,'USB_DP_CONN','BC_DP'), 'R38':(2.2,'USB_DM_CONN','BC_DM'),
 'R39':(2.2,'VBUS','VBUS_BC'), 'R40':(887000,'VBUS','CC_VBUS_DET'),
 'R41':(10000,'USB_LOGIC_3V3','CC_HIGH_N'), 'R42':(10000,'CHG_EN1','GND'),
}
for ref,(value,a,b) in resistors.items():
    assert components[ref]['resistance'] == value, ref
    assert (net(ref,1),net(ref,2)) == (a,b), ref
assert 'R1' not in components and 'R2' not in components, 'Do not parallel external Rd with TUSB internal Rd'
# Catch alternate external CC pulls, 5 V into the MCU, or a short around the detector.
for ref,c in components.items():
    if c.get('ftype') == 'simple_resistor':
        assert not ({net(ref,1),net(ref,2)} & {'CC1','CC2'}), ref
for pin in range(1,42):
    assert net('U1',pin) not in {'BC_FAST','VBUS_BC','USB_LOGIC_3V3','CHG_EN1','BC_DP','BC_DM'}, pin
assert net('U18',1) != 'V3V3', 'Detection must work with switched V3V3 disabled'
for ref,rail in [('C38','VBUS_BC'),('C39','VBUS_BC'),('C40','USB_LOGIC_3V3'),('C41','USB_LOGIC_3V3'),('C42','USB_LOGIC_3V3'),('C43','VBUS'),('C44','USB_LOGIC_3V3')]:
    assert (net(ref,1),net(ref,2)) == (rail,'GND'), ref
skus = {'U14':'C128408','U15':'C132554','U16':'C7835','U17':'C43368','U18':'C404027'}
bom = list(csv.DictReader((p/'bill-of-materials.csv').open()))
for ref,sku in skus.items():
    assert components[ref]['supplier_part_numbers']['jlcpcb'] == [sku], ref
    pcb = next(e for e in circuit if e['type']=='pcb_component' and e['source_component_id']==components[ref]['source_component_id'])
    assert any(e['type']=='cad_component' and e.get('pcb_component_id')==pcb['pcb_component_id'] for e in circuit), ref
    row = next(r for r in bom if ref in r['Reference designators'].split('; '))
    assert sku in row['Supplier reference'], ref

# Evaluate TI truth tables through the actual built net names.
def mode(bc, cc_out1, host, suspend):
    state = {net('U14',10):bc, net('U15',7):cc_out1, net('U1',23):host, net('U1',15):suspend}
    state[net('U16',4)] = not state[net('U16',2)]
    state[net('U17',4)] = any(state[net('U17',i)] for i in (1,3,6))
    return {(False,False):'100', (False,True):'500', (True,False):'ILIM', (True,True):'suspend'}[(state[net('U2',5)],state[net('U2',6)])]
for bc,cc,host in itertools.product((False,True), repeat=3):
    assert mode(bc,cc,host,False) == ('500' if bc or not cc or host else '100')
    # Ordered firmware suspend holds host permission high even if detectors change.
    assert mode(bc,cc,True,True) == 'suspend'
assert mode(True,True,False,False) == '500', 'DCP must work with MCU OFF'
assert mode(False,False,False,False) == '500', 'CC high-current must work with MCU OFF'
assert mode(False,True,False,False) == '100', 'Unknown/SDP must start at 100mA'
assert mode(False,True,False,True) == 'ILIM', 'Document the prohibited GPIO sequence honestly'
print('PASS: USB detector pinouts, source isolation, CC termination, off-state rail, logic levels, exact parts/CAD/BOM and current-mode truth tables')
print('Bench qualification still required: attach/detach, inrush, USB enumeration/suspend, battery current, thermal behavior and charge cycle.')