"""Rebuild documentation schematics with Schemdraw. Run with ~/.venvs/codex-schematics/bin/python. Topology JSON is emitted beside each figure. References identify documentation symbols, not CAD placements. """ from pathlib import Path import json import xml.etree.ElementTree as ET import schemdraw import schemdraw.elements as e import schemdraw.logic as logic import cairosvg OUT = Path(__file__).resolve().parent class Circuit: def __init__(self,name,title,source): self.name=name self.d=schemdraw.Drawing(show=False) self.d.config(unit=2.5,fontsize=12,font='DejaVu Sans',lw=1.5,color='#20394e',bgcolor='white') self.top={'title':title,'source':source,'reference_scope':'documentation only','components':[],'nets':[],'no_connects':[]} self.nets={} def component(self,ref,kind,pins,value=''): assert ref not in [c['ref'] for c in self.top['components']],ref self.top['components'].append({'ref':ref,'kind':kind,'pins':list(pins),'value':value}) for pin,net in pins.items(): self.nets.setdefault(net,[]).append(ref+'.'+pin) def label(self,xy,text,loc='top',size=12): self.d.add(e.Label().at(xy).label(text,loc=loc,fontsize=size)) def wire(self,a,b): self.d.add(e.Line().at(a).to(b)) def dot(self,xy): self.d.add(e.Dot().at(xy)) def port(self,ref,xy,net,loc='left'): self.component(ref,'port',{'P':net},net) self.d.add(e.Dot(open=True).at(xy).label(net,loc=loc)) def res(self,ref,a,b,n1,n2,label=None): self.component(ref,'resistor',{'1':n1,'2':n2},label or ref) self.d.add(e.Resistor().at(a).to(b).label(label or ref)) def cap(self,ref,a,b,n1,n2,label=None): self.component(ref,'capacitor',{'1':n1,'2':n2},label or ref) self.d.add(e.Capacitor().at(a).to(b).label(label or ref)) def zener(self,ref,a,b,n1,n2): self.component(ref,'zener',{'K':n1,'A':n2},'Low-C clamp\nVoltage TBD') self.d.add(e.Zener().at(a).to(b).reverse().label('Low-C Zener\nVoltage TBD')) def ground(self,ref,xy,net='SENSOR_GND'): self.component(ref,'ground-port',{'P':net},net) self.d.add(e.Ground().at(xy).label(net,loc='bottom')) def block(self,ref,xy,label,ins,outs,w=3.5,h=2): pins=[] for side,ports in [('L',ins),('R',outs)]: for i,(pin,net) in enumerate(ports): pins.append(e.IcPin(name=pin,anchorname=pin,side=side,pos=(i+1)/(len(ports)+1))) self.component(ref,'functional-block',dict(ins+outs),label) return self.d.add(e.Ic(size=(w,h),pins=pins).at(xy).theta(0).label(label,loc='top')) def save(self): for net,ends in self.nets.items(): entry={'name':net,'connections':ends} if len(ends)==1: entry.update(allow_single=True,note='Named boundary or supply shown in the documentation figure') self.top['nets'].append(entry) (OUT/(self.name+'.topology.json')).write_text(json.dumps(self.top,indent=2)+'\n') self.d.save(str(OUT/(self.name+'.svg'))) svg_path = OUT/(self.name+'.svg') ET.register_namespace('', 'http://www.w3.org/2000/svg') tree = ET.parse(svg_path); root = tree.getroot() x,y,w,h = map(float,root.get('viewBox').split()) root.set('viewBox', f'{x-16} {y-16} {w+32} {h+32}') root.set('width', f'{w+32}pt'); root.set('height', f'{h+32}pt') tree.write(svg_path, encoding='utf-8', xml_declaration=True) cairosvg.svg2png(url=str(OUT/(self.name+'.svg')),write_to=str(OUT/(self.name+'.png')),background_color='white',output_width=1200) # Shunt protection and ADC reservoir are deliberately drawn as branches. c=Circuit('analog-input','Passive 0–5 V ADC channel','IO_MODULES/ANALOG_INPUTS.md: General 0--5 V channels') c.port('J',(0,0),'Sensor signal') c.wire((0,0),(2,0));c.dot((2,0)) c.component('CLAMP','transient-protection',{'SIG':'Sensor signal','RET':'SENSOR_GND'},'Connector clamp\nPart / rating TBD') cl=c.d.add(e.Ic(size=(2,1),pins=[e.IcPin(name='SIG',side='T',anchorname='SIG'),e.IcPin(name='RET',side='B',anchorname='RET')]).at((1,-3)).theta(0).label('Transient clamp',loc='left')) c.wire((2,0),cl.SIG);c.ground('GC',cl.RET) c.res('R_TOP',(2,0),(6,0),'Sensor signal','DIVIDED') c.dot((6,0));c.res('R_BOTTOM',(6,0),(6,-3),'DIVIDED','SENSOR_GND');c.ground('GR',(6,-3)) c.res('R_ISO',(6,0),(10,0),'DIVIDED','ADC_IN','Isolation R\nValue TBD') c.dot((10,0));c.cap('C_HOLD',(10,0),(10,-3),'ADC_IN','SENSOR_GND');c.ground('GH',(10,-3)) c.wire((10,0),(13,0));c.port('ADC',(13,0),'ADC_IN',loc='right') c.label((6,2),'Passive analogue input',size=18);c.save() c=Circuit('thermistor','NTC excitation and acquisition','IO_MODULES/ANALOG_INPUTS.md: NTC temperature channels') c.port('V',(0,0),'+5V_SENS');c.res('R_PULLUP',(0,0),(4,0),'+5V_SENS','NTC_NODE','R_PULLUP\nSelected for sensor curve');c.dot((4,0)) c.component('NTC','thermistor',{'1':'NTC_NODE','2':'SENSOR_GND'},'NTC') c.d.add(e.Thermistor().at((4,0)).to((4,-3)).label('NTC'));c.ground('GN',(4,-3)) b=c.block('AFE',(7,-1),'Attenuation / local filter',[('IN','NTC_NODE')],[('OUT','ADC_NTC')],w=4) c.wire((4,0),b.IN);c.wire(b.OUT,(14,0));c.port('ADC',(14,0),'ADC_NTC',loc='right') c.port('VS',(0,-6),'+5V_SENS');b2=c.block('SENSE',(7,-7),'Matched divider / filter',[('IN','+5V_SENS')],[('OUT','ADC_5V_SENSE')],w=4) c.wire((0,-6),b2.IN);c.wire(b2.OUT,(14,-6));c.port('ADCS',(14,-6),'ADC_5V_SENSE',loc='right') c.label((7,2),'Thermistor + excitation-supply measurement',size=18) c.label((7,-9),'Both ADC channels use +3V3_ANA as reference. Filter and protection details remain in the text.',size=11);c.save() c=Circuit('trigger-input','Crank / cam Hall input','IO_MODULES/ENGINE_POSITION_INPUTS.md: Electrical interface') c.port('J',(0,0),'TRIG_x');c.wire((0,0),(3,0));c.dot((3,0)) c.port('V',(3,4),'+12V_SENS',loc='top');c.res('R_PULLUP',(3,4),(3,0),'+12V_SENS','TRIG_x','R_PULLUP\n4.7 kΩ provision') c.component('CLAMP','transient-protection',{'SIG':'TRIG_x','RET':'SENSOR_GND'},'Harness clamp / ratings TBD') b=c.d.add(e.Ic(size=(2,1),pins=[e.IcPin(name='SIG',side='T',anchorname='SIG'),e.IcPin(name='RET',side='B',anchorname='RET')]).at((2,-3)).theta(0).label('Transient\nprotection',loc='left')) c.wire((3,0),b.SIG);c.ground('GC',b.RET) c.res('R_DIV_TOP',(3,0),(7,0),'TRIG_x','DIVIDED');c.dot((7,0)) c.res('R_DIV_BOTTOM',(7,0),(7,-3),'DIVIDED','SENSOR_GND');c.ground('GB',(7,-3)) c.wire((7,0),(10,0));c.dot((10,0));c.cap('C_FILTER',(10,0),(10,-3),'DIVIDED','SENSOR_GND','C_FILTER\n1 nF initial');c.ground('GF',(10,-3)) c.wire((10,0),(12,0));s=c.d.add(logic.Schmitt().at((12,0)).right().label('SN74LVC2G17-Q1\nSupply: +3V3_MAIN',loc='top')) c.component('U','schmitt-buffer',{'IN':'DIVIDED','OUT':'TIMER_3V3'},'SN74LVC2G17-Q1') c.wire(s.out,(16,0));c.port('MCU',(16,0),'TIMER_3V3',loc='right') c.label((8,6),'Hall input · repeated for crank and cam',size=18) c.label((8,-5.5),'Sensor connector: supply = +12V_SENS; return = SENSOR_GND; output = TRIG_x. Pin numbers unassigned.',size=11);c.save() c=Circuit('ignition-command','Ignition timing and inhibit protection','IO_MODULES/IGNITION.md: Command and inhibit path') for i,(net,out) in enumerate([('STM32 timing (3.3 V)','INP'),('SAFE_IGNITION_INHIBIT','EN')]): y=-i*7 c.port('J'+str(i),(0,y),net) if i==0: b=c.block('BUFFER',(2,y-1),'Non-inverting\n3.3 V → 5 V',[('IN',net)],[('OUT','TIMING_5V')],w=3) c.wire((0,y),b.IN);c.wire(b.OUT,(7,y));net2='TIMING_5V' else:c.wire((0,y),(7,y));net2=net c.res('RP'+str(i),(7,y),(11,y),net2,out,'1 kΩ') c.dot((11,y));c.zener('Z'+str(i),(11,y),(11,y-3),out,'VBG_LOCAL_KELVIN');c.ground('G'+str(i),(11,y-3),'VBG_LOCAL_KELVIN') c.wire((11,y),(14,y));c.port('U'+str(i),(14,y),out,loc='right') c.label((7,2.5),'VBG08H-E command pins',size=18) c.label((6,-12),'INP: high = dwell; falling edge = spark. EN: high = inhibit; low = permission.',size=11);c.save() c=Circuit('injector-command','Injector command protection','IO_MODULES/INJECTON.md: Command path') c.port('J',(0,0),'SAFE_INJECTOR_SIG');c.res('Rprot',(0,0),(4,0),'SAFE_INJECTOR_SIG','INPUT','Rprot · 1 kΩ');c.dot((4,0)) c.res('R_PD',(4,0),(4,-3),'INPUT','INJ_PGND','10 kΩ');c.ground('GR',(4,-3),'INJ_PGND') c.wire((4,0),(8,0));c.dot((8,0));c.zener('Z',(8,0),(8,-3),'INPUT','INJ_PGND');c.ground('GZ',(8,-3),'INJ_PGND') c.wire((8,0),(12,0));c.port('U',(12,0),'INPUT',loc='right');c.label((6,2),'VNL5050S5-E · active-high command',size=18);c.save() c=Circuit('injector-status','Injector status acquisition','IO_MODULES/INJECTON.md: Status diagnostic') c.port('U',(0,0),'STATUS');c.res('Rprot',(0,0),(5,0),'STATUS','MCU_STATUS','Rprot · 1 kΩ');c.dot((5,0)) c.port('V',(5,4),'+3V3_MAIN',loc='top');c.res('R_PULLUP',(5,4),(5,0),'+3V3_MAIN','MCU_STATUS','Pull-up\nValue TBD') c.wire((5,0),(10,0));c.port('M',(10,0),'MCU_STATUS',loc='right');c.label((5,6),'VNL5050S5-E · open-drain diagnostic',size=18);c.save() c=Circuit('logic-output','Protected 5 V logic output','IO_MODULES/DIGITAL_OUTPUTS.md: Protected 5 V logic outputs') c.port('J',(0,0),'MCU GPIO');c.wire((0,0),(3,0));c.dot((3,0)) c.res('R_IN_PD',(3,0),(3,-3),'MCU GPIO','GND');c.ground('GI',(3,-3),'GND') b=c.block('U',(6,-1),'TPS4H000-Q1\nVBB = +5V_AUX',[('INx','MCU GPIO')],[('OUTx','LOGIC_OUTx')],w=4) c.wire((3,0),b.INx);c.wire(b.OUTx,(13,0));c.dot((13,0));c.res('R_OUT_PD',(13,0),(13,-3),'LOGIC_OUTx','GND');c.ground('GO',(13,-3),'GND') c.wire((13,0),(17,0));c.port('O',(17,0),'LOGIC_OUTx',loc='right') c.label((8,3),'5 V high-side logic output',size=18) c.label((8,-5.5),'+5V_MAIN → eFuse / load switch → +5V_AUX. Connector TVS selection is specified in the text.',size=11);c.save() c=Circuit('sink-command','Low-side command default states','IO_MODULES/DIGITAL_OUTPUTS.md: Protected low-side outputs') for i,(net,pin) in enumerate([('MCU GPIO','INx'),('MCU enable','EN')]): y=-i*6 c.port('J'+str(i),(0,y),net);c.wire((0,y),(4,y));c.dot((4,y)) c.res('R_IN_PD' if i==0 else 'R_EN_PD',(4,y),(4,y-2.5),net,'LOCAL_GND') c.ground('G'+str(i),(4,y-2.5),'LOCAL_GND') c.wire((4,y),(9,y));c.component('U'+str(i),'driver-pin',{pin:net},'TLE9104SH '+pin) c.d.add(e.Dot(open=True).at((9,y)).label('TLE9104SH '+pin,loc='right')) c.label((4,2),'TLE9104SH · default-low commands',size=18);c.save() def equation_gate(c,ref,xy,a,b,out,invert_b=False): x,y=xy gate=c.d.add(logic.And(inputs=2,inputnots=[2] if invert_b else []).at(xy).right()) c.component(ref,'and-with-inverted-B' if invert_b else 'and',{'A':a,'B':b,'Y':out}) for idx,(anchor,net) in enumerate([(gate.in1,a),(gate.in2,b)]): yy=y+1.4 if idx==0 else y-1.4 start=(x-5,yy); c.port(ref+'P'+str(idx),start,net) c.wire(start,(x-1.2,yy));c.wire((x-1.2,yy),(x-1.2,anchor.y));c.wire((x-1.2,anchor.y),anchor) c.wire(gate.out,(x+5,y));c.port(ref+'O',(x+5,y),out,loc='right') c=Circuit('deadman-permit','Single-input hardware engine permission','IO_MODULES/DEAD_MAN.md: Hardware gating') equation_gate(c,'PERMIT',(0,0),'DEADMAN_OK','MCU_RUN_PERMIT','ENGINE_PERMIT') c.label((0,3),'Single deadman input · hardware permission',size=18) c.label((0,-3),'DEADMAN_OK: conditioned shared input. MCU_RUN_PERMIT: 3.3 V. Gate output: 5 V.',size=11);c.save() c=Circuit('deadman-outputs','Hardware permitted output commands','IO_MODULES/DEAD_MAN.md: Logic equations') equation_gate(c,'INJECT',(0,0),'ENGINE_PERMIT','MCU_INJECTOR_SIG','SAFE_INJECTOR_SIG') gate=c.d.add(logic.Not().at((0,-6)).right());c.component('INHIBIT','not',{'A':'ENGINE_PERMIT','Y':'SAFE_IGNITION_INHIBIT'}) c.port('I',(-5,-6),'ENGINE_PERMIT');c.wire((-5,-6),gate.in1);c.wire(gate.out,(5,-6));c.port('O',(5,-6),'SAFE_IGNITION_INHIBIT',loc='right') c.label((0,3),'Injector permission and ignition inhibit',size=18) c.label((0,-8),'SAFE_IGNITION_INHIBIT: high = inhibited. Supply-loss default states require the documented bias networks.',size=11);c.save() print('Wrote 10 SVG circuits, previews and topology manifests.')