Files
NeoECU-Hardware/Architecture/diagrams/build_blocks.py
T

145 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 sensors 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.')