176 lines
12 KiB
Python
176 lines
12 KiB
Python
"""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.')
|