145 lines
11 KiB
Python
145 lines
11 KiB
Python
"""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.')
|