"""Rebuild editable architectural figures (standard-library Python). Export the resulting .drawio files with the installed draw.io desktop renderer. Node/edge records are retained as a source-traceable connection manifest. """ from pathlib import Path import json import xml.etree.ElementTree as ET OUT = Path(__file__).resolve().parent COLORS = {'power': '#fff1d6', 'signal': '#e8f0fc', 'device': '#e8f4ed', 'ground': '#eef0f3', 'note': '#ffffff'} manifest = {} class Figure: def __init__(self, name, title, source, width=1040, height=620): self.name, self.width, self.height = name, width, height self.data = {'title': title, 'source': source, 'nodes': {}, 'connections': []} self.root = ET.Element('mxfile', host='app.diagrams.net') diagram = ET.SubElement(self.root, 'diagram', name=title, id=name) model = ET.SubElement(diagram, 'mxGraphModel', page='1', pageWidth=str(width), pageHeight=str(height), grid='1', gridSize='10') self.cells = ET.SubElement(model, 'root') ET.SubElement(self.cells, 'mxCell', id='0') ET.SubElement(self.cells, 'mxCell', id='1', parent='0') self.node('title', title, 30, 18, width-60, 42, 'note', text=True, size=26) def node(self, key, label, x, y, w=220, h=74, kind='signal', text=False, size=18): style = f'rounded=1;html=0;fontFamily=DejaVu Sans;fontSize={size};fontColor=#172b40;strokeColor=#577086;fillColor={COLORS[kind]};spacing=10;' if text: style += 'strokeColor=none;fillColor=none;align=left;' cell = ET.SubElement(self.cells, 'mxCell', id='node_'+key, value=label, style=style, vertex='1', parent='1') ET.SubElement(cell, 'mxGeometry', x=str(x), y=str(y), width=str(w), height=str(h), **{'as':'geometry'}) self.data['nodes'][key] = {'label': label, 'kind':kind, 'bounds':[x,y,w,h]} return key def edge(self, a, b, label='', points=(), exit=(1,.5), entry=(0,.5), kind='signal', arrows=True): assert a in self.data['nodes'] and b in self.data['nodes'] idx = 'e'+str(len(self.data['connections'])) color = '#946221' if kind=='power' else '#416680' bidirectional = label == 'Bidirectional' if bidirectional: label = '' style = f'edgeStyle=orthogonalEdgeStyle;rounded=0;html=0;strokeWidth=2;strokeColor={color};fontSize=16;fontFamily=DejaVu Sans;labelBackgroundColor=#ffffff;endArrow={"block" if arrows else "none"};endFill=1;exitX={exit[0]};exitY={exit[1]};entryX={entry[0]};entryY={entry[1]};' if bidirectional: style += 'startArrow=block;startFill=1;' cell = ET.SubElement(self.cells, 'mxCell', id=idx, source='node_'+a, target='node_'+b, value=label, style=style, edge='1', parent='1') geo = ET.SubElement(cell, 'mxGeometry', relative='1', **{'as':'geometry'}) if points: arr = ET.SubElement(geo, 'Array', **{'as':'points'}) for x,y in points: ET.SubElement(arr, 'mxPoint', x=str(x), y=str(y)) self.data['connections'].append({'from':a,'to':b,'label':label,'kind':kind}) def note(self, label, y): self.node('note'+str(y), label, 30, y, self.width-60, 60, 'note', text=True, size=16) def save(self): ET.indent(self.root) ET.ElementTree(self.root).write(OUT/(self.name+'.drawio'), encoding='utf-8', xml_declaration=True) manifest[self.name] = self.data def chain(name, title, source, labels, note='', kind='signal'): f = Figure(name,title,source,1040,270) w = (960-(len(labels)-1)*40)/len(labels) for i,label in enumerate(labels): f.node(str(i),label,40+i*(w+40),95,w,85,kind) if i: f.edge(str(i-1),str(i),kind=kind) if note: f.note(note,195) f.save() f = Figure('power-sources','Power sources and battery loads','POWER_ARCHITECTURE.md: Rail Tree',1040,790) f.node('battery','4S LiPo\n14.8 V nominal · 16.8 V full',30,90,290,80,'power') f.node('protection','Input fuse · reverse protection\nTVS / transient protection',390,90,310,80,'power') f.node('vbat','VBAT_PROT',770,90,230,80,'power') f.edge('battery','protection',kind='power'); f.edge('protection','vbat',kind='power') for i,(key,label,out) in enumerate([ ('loads','Protected battery branches','Ignition · injector\nLow-side outputs · VBAT sense'), ('five','Synchronous buck · 5 V','+5V_MAIN\nInternal 5 V circuitry'), ('three','Synchronous buck · 3.3 V','+3V3_MAIN\nMCU / digital / communications'), ('twelve','Regulated buck-boost · 12 V','+12V_SENS\nProtected Hall-sensor supply')]): y=245+i*120 f.node(key+'in','VBAT_PROT',30,y,190,74,'power') f.node(key,label,300,y,310,74,'power'); f.node(key+'out',out,690,y,310,74,'power') f.edge(key+'in',key,kind='power'); f.edge(key,key+'out',kind='power') f.note('Repeated VBAT_PROT labels denote the same rail. Protected branch details are shown separately.',725); f.save() f = Figure('power-branches','Protected and filtered rail branches','POWER_ARCHITECTURE.md: rail descriptions',1040,700) for i,(a,b,c) in enumerate([ ('+5V_MAIN','eFuse / protected load switch','+5V_AUX\nExternal logic outputs'), ('+5V_MAIN','eFuse / load switch\nFerrite bead + local filter','+5V_SENS\n5 V sensor excitation'), ('+3V3_MAIN','Ferrite bead + local filter','+3V3_ANA\nAnalogue supplies /\nADC reference'), ('12 V buck-boost output','Current limit / protected switch\nHarness filter + decoupling','+12V_SENS\nCrank / cam Hall sensors')]): y=95+i*135 f.node(f'a{i}',a,30,y,240,90,'power'); f.node(f'b{i}',b,340,y,320,90,'power'); f.node(f'c{i}',c,730,y,280,90,'power') f.edge(f'a{i}',f'b{i}',kind='power'); f.edge(f'b{i}',f'c{i}',kind='power') f.note('Optional analogue LDO and final current limits remain validation items; see the rail descriptions.',630); f.save() f=Figure('ratiometric','Ratiometric sensor acquisition','POWER_ARCHITECTURE.md: Ratiometric 5 V Sensor Measurements',1040,540) for key,label,x,y,w,kind in [('rail','+5V_SENS',30,90,220,'power'),('sensor','5 V sensor\nExcitation input',340,90,280,'device'),('sig','Sensor output',30,240,220,'signal'),('div','Matched divider / filter',340,240,280,'signal'),('adc','ADC sensor channel',730,240,280,'signal'),('rail2','+5V_SENS',30,380,220,'power'),('sense','Matched divider / filter',340,380,280,'signal'),('adc2','ADC 5-V-sense channel',730,380,280,'signal')]: f.node(key,label,x,y,w,74,kind) f.edge('rail','sensor',kind='power'); f.edge('sig','div'); f.edge('div','adc'); f.edge('rail2','sense'); f.edge('sense','adc2') f.note('Sensor output is the sensor’s signal pin. Both ADC channels use +3V3_ANA as their reference.',470); f.save() chain('dac-output','Optional analogue-output provision','IO_ARCHITECTURE.md: Analogue-output provision',['MCU DAC','Rail-to-rail buffer','Protection /\nseries impedance','Connector'],'Provision only; final electrical range, load and protection remain to be defined.') f=Figure('deadman-inputs','Deadman contacts and observations','IO_MODULES/DEAD_MAN.md: Logic domains',1040,650) for i,c in enumerate('AB'): y=100+i*245 f.node(c+'s','Protected 5 V\nIndependent NO contact '+c,30,y,260,80,'power') f.node(c+'p','Protected input '+c+'\nOwn filter + default-low bias',370,y,290,80,'signal') f.node(c+'h','DEADMAN_A_HELD\n5 V · non-inverting' if c=='A' else 'DEADMAN_B_RELEASED\n5 V · inverting',750,y,260,80,'signal') f.node(c+'d','DEADMAN_'+c+'_STATUS\n3.3 V · held = high → MCU',370,y+120,330,75,'device') f.edge(c+'s',c+'p'); f.edge(c+'p',c+'h'); f.edge(c+'p',c+'d',exit=(.5,1),entry=(.5,0)) f.note('Separate harness paths. MCU status observations are diagnostic; the 5 V signals feed the permit gates.',575);f.save() for name,title,part,pin,load,ground,source in [ ('ignition-power','Ignition power path','VBG08H-E','HVC','Coil primary','IGN_PGND','IGNITION'), ('injector-power','Injector power path','VNL5050S5-E','DRAIN','Injector coil','INJ_PGND','INJECTON'), ('sink-power','Generic low-side power path','TLE9104SH','OUTx','External load\nvia SINK_OUTx','OUT_PGND','DIGITAL_OUTPUTS')]: f=Figure(name,title,'IO_MODULES/'+source+'.md: Power path',1040,460) f.node('rail','VBAT_PROT',30,100,230,80,'power'); f.node('load',load,350,100,280,80,'device');f.node('driver',part+'\n'+pin,720,100,290,80,'device') f.node('switch','Internal IGBT' if source=='IGNITION' else 'Internal low-side MOSFET',720,230,290,70,'device') f.node('return',('PGND1 + PGND2' if source=='IGNITION' else 'SOURCE' if source=='INJECTON' else 'Power return')+' → '+ground,350,350,660,60,'ground') f.edge('rail','load',kind='power');f.edge('load','driver',kind='power');f.edge('driver','switch',exit=(.5,1),entry=(.5,0),kind='power');f.edge('switch','return',exit=(.5,1),entry=(.8,0),kind='power') f.note('Functional current path. '+ground+' returns separately to the power-entry ground star.',410);f.save() chain('ignition-flag','Ignition current-flag diagnostic','IO_MODULES/IGNITION.md: Current-flag diagnostic',['VBG08H-E C.F.\nOpen-drain output','Reference-style R/C filter\n+3V3_MAIN pull-up','STM32 GPIO'],'Diagnostic current-threshold observation; separate from the hardware inhibit path.') f=Figure('ignition-ground','Ignition ground and Kelvin reference','IO_MODULES/IGNITION.md: Ground and layout',1040,480) for i,(key,label) in enumerate([('p1','VBG08H-E PGND1'),('p2','VBG08H-E PGND2'),('g','VBG08H-E GND')]): f.node(key,label,30,100+i*115,230,64,'ground') f.node('join','IGN_PGND\nLocal joining point',490,170,220,105,'ground');f.node('star','Power-entry\nground star',800,170,210,105,'ground') f.edge('p1','join','Matched heavy copper',points=[(400,132),(400,190)],entry=(0,.2),arrows=False) f.edge('p2','join','Matched heavy copper',entry=(0,.65),arrows=False) f.edge('g','join','Separate Kelvin trace',points=[(600,362)],entry=(.5,1),arrows=False) f.edge('join','star',arrows=False) f.note('GND carries the local reference, not coil current. No separate remote-DGND connection.',410);f.save() chain('injector-ground','Injector ground return','IO_MODULES/INJECTON.md: Ground and layout',['VNL5050S5-E SOURCE','INJ_PGND\nShort, low-impedance copper','Power-entry ground star'],'SOURCE is both power return and local logic reference. No separate Kelvin pin.',kind='ground') f=Figure('sink-interfaces','Low-side driver supplies and diagnostics','IO_MODULES/DIGITAL_OUTPUTS.md: Protected low-side outputs',1040,610) for i,(a,b) in enumerate([('VBAT_PROT','TLE9104SH VS'),('+5V_MAIN','TLE9104SH VDD'),('+3V3_MAIN','TLE9104SH VIO')]): y=95+i*110 f.node('s'+str(i),a,30,y,230,70,'power'); f.node('p'+str(i),b,380,y,280,70,'device');f.edge('s'+str(i),'p'+str(i),kind='power') f.node('mcu','MCU SPI',30,440,230,80);f.node('spi','TLE9104SH SPI\nCommands + diagnostics',380,440,280,80,'device');f.edge('mcu','spi','Bidirectional') f.note('INx / EN default-low networks are shown in the companion circuit. Decouple supplies locally.',540);f.save() (OUT/'block-connections.json').write_text(json.dumps(manifest,indent=2)+'\n') print(f'Wrote {len(manifest)} editable block figures.')