582 lines
27 KiB
Python
582 lines
27 KiB
Python
#!/usr/bin/env python3
|
|
"""Erzeugt Schaltplan (.kicad_sch) + Projekt-Libs aus design.py (eine Quelle für Schaltplan und Platine)."""
|
|
import re, uuid, os, sys, json
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from design import PARTS, PROJECT, NOCONNECT
|
|
|
|
SYMDIR = '/usr/share/kicad/symbols'
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
U = lambda: str(uuid.uuid5(uuid.NAMESPACE_URL, PROJECT + ':' + '|'.join(map(str, _ctr.__next__()))))
|
|
def _gen():
|
|
i = 0
|
|
while True:
|
|
i += 1; yield (i,)
|
|
_ctr = _gen()
|
|
|
|
# ---------- minimal s-expression helpers ----------
|
|
def tokenize(s):
|
|
return re.findall(r'\(|\)|"(?:\\.|[^"\\])*"|[^\s()"]+', s)
|
|
|
|
def parse(tokens, i=0):
|
|
out = []
|
|
while i < len(tokens):
|
|
t = tokens[i]
|
|
if t == '(':
|
|
sub, i = parse(tokens, i + 1); out.append(sub)
|
|
elif t == ')':
|
|
return out, i + 1
|
|
else:
|
|
out.append(t); i += 1
|
|
return out, i
|
|
|
|
def dump(x, ind=0):
|
|
if not isinstance(x, list):
|
|
return x
|
|
if all(not isinstance(e, list) for e in x):
|
|
return '(' + ' '.join(x) + ')'
|
|
s = '(' + ' '.join(e for e in x[:2] if not isinstance(e, list))
|
|
rest = x[2:] if len(x) > 1 and not isinstance(x[1], list) else x[1:]
|
|
if isinstance(x[1], list):
|
|
s = '(' + x[0]; rest = x[1:]
|
|
for e in rest:
|
|
s += '\n' + ' ' * (ind + 1) + (dump(e, ind + 1) if isinstance(e, list) else e)
|
|
return s + ')'
|
|
|
|
def q(s):
|
|
return '"' + str(s).replace('\\', '\\\\').replace('"', '\\"') + '"'
|
|
|
|
_libcache = {}
|
|
def lib_symbol(libid):
|
|
lib, name = libid.split(':')
|
|
if lib == PROJECT:
|
|
path = os.path.join(HERE, PROJECT + '.kicad_sym')
|
|
else:
|
|
path = os.path.join(SYMDIR, lib + '.kicad_sym')
|
|
if path not in _libcache:
|
|
_libcache[path] = parse(tokenize(open(path).read()))[0][0]
|
|
for e in _libcache[path]:
|
|
if isinstance(e, list) and e[0] == 'symbol' and e[1] == q(name):
|
|
if any(isinstance(k, list) and k[0] == 'extends' for k in e):
|
|
raise SystemExit('extends not supported: ' + libid)
|
|
import copy
|
|
s = copy.deepcopy(e); s[1] = q(libid)
|
|
return s
|
|
raise SystemExit('symbol not found ' + libid)
|
|
|
|
def pins_of(sym):
|
|
res = {}
|
|
def walk(x):
|
|
for e in x:
|
|
if isinstance(e, list):
|
|
if e[0] == 'pin':
|
|
at = next(k for k in e if isinstance(k, list) and k[0] == 'at')
|
|
num = next(k for k in e if isinstance(k, list) and k[0] == 'number')[1].strip('"')
|
|
res[num] = (float(at[1]), float(at[2]), int(float(at[3])) if len(at) > 3 else 0)
|
|
else:
|
|
walk(e)
|
|
walk(sym)
|
|
return res
|
|
|
|
# ---------- project symbol library (Wemos, ADS1115 module) ----------
|
|
def rect_symbol(name, ref, left, right, w=15.24, desc=''):
|
|
"""left/right: list of (number, name, type). pins 2.54 apart."""
|
|
n = max(len(left), len(right))
|
|
h = (n + 1) * 2.54
|
|
top = h / 2
|
|
pins = []
|
|
for i, (num, nm, tp) in enumerate(left):
|
|
y = top - 2.54 * (i + 1)
|
|
pins.append(f'(pin {tp} line (at {-w/2-2.54:.2f} {y:.2f} 0) (length 2.54) (name {q(nm)} (effects (font (size 1.27 1.27)))) (number {q(num)} (effects (font (size 1.27 1.27)))))')
|
|
for i, (num, nm, tp) in enumerate(right):
|
|
y = top - 2.54 * (i + 1)
|
|
pins.append(f'(pin {tp} line (at {w/2+2.54:.2f} {y:.2f} 180) (length 2.54) (name {q(nm)} (effects (font (size 1.27 1.27)))) (number {q(num)} (effects (font (size 1.27 1.27)))))')
|
|
return f'''(symbol {q(name)} (pin_names (offset 1.016)) (exclude_from_sim no) (in_bom yes) (on_board yes)
|
|
(property "Reference" {q(ref)} (at 0 {top+1.27:.2f} 0) (effects (font (size 1.27 1.27))))
|
|
(property "Value" {q(name)} (at 0 {-top-1.27:.2f} 0) (effects (font (size 1.27 1.27))))
|
|
(property "Footprint" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
|
(property "Datasheet" "" (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
|
(property "Description" {q(desc)} (at 0 0 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
|
(symbol {q(name + '_0_1')} (rectangle (start {-w/2:.2f} {top:.2f}) (end {w/2:.2f} {-top:.2f}) (stroke (width 0.254) (type default)) (fill (type background))))
|
|
(symbol {q(name + '_1_1')} {' '.join(pins)}))'''
|
|
|
|
def write_projlib():
|
|
wemos = rect_symbol('WEMOS_D1_mini', 'A',
|
|
[('1', 'RST', 'input'), ('2', 'A0', 'input'), ('3', 'D0/GPIO16', 'bidirectional'), ('4', 'D5/GPIO14', 'bidirectional'),
|
|
('5', 'D6/GPIO12', 'bidirectional'), ('6', 'D7/GPIO13', 'bidirectional'), ('7', 'D8/GPIO15', 'bidirectional'), ('8', '3V3', 'power_out')],
|
|
[('9', 'TX/GPIO1', 'bidirectional'), ('10', 'RX/GPIO3', 'bidirectional'), ('11', 'D1/GPIO5', 'bidirectional'), ('12', 'D2/GPIO4', 'bidirectional'),
|
|
('13', 'D3/GPIO0', 'bidirectional'), ('14', 'D4/GPIO2', 'bidirectional'), ('15', 'GND', 'power_in'), ('16', '5V', 'power_in')],
|
|
w=30.48, desc='Wemos D1 mini (ESP8266) auf 2x 1x8 Buchsenleisten')
|
|
ads = rect_symbol('ADS1115_Modul', 'A',
|
|
[('1', 'VDD', 'power_in'), ('2', 'GND', 'power_in'), ('3', 'SCL', 'input'), ('4', 'SDA', 'bidirectional'), ('5', 'ADDR', 'input')],
|
|
[('6', 'ALRT', 'output'), ('7', 'A0', 'input'), ('8', 'A1', 'input'), ('9', 'A2', 'input'), ('10', 'A3', 'input')],
|
|
w=12.7, desc='ADS1115 16-bit ADC Breakout (HW-ADS1115/CJMCU, 1x10 Pin)')
|
|
ct = rect_symbol('ZMCT103C', 'T',
|
|
[('3', 'P1 (L_SW)', 'passive'), ('4', 'P2 (L_PUMP)', 'passive')],
|
|
[('1', 'S1', 'passive'), ('2', 'S2', 'passive')],
|
|
w=15.24, desc='Stromwandler ZMCT103C 1000:1, Primär = Drahtbrücke durchs Loch')
|
|
mx = rect_symbol('MAX6675_Modul', 'A',
|
|
[('1', 'GND', 'power_in'), ('2', 'VCC', 'power_in'), ('3', 'SCK', 'input'), ('4', 'CS', 'input'), ('5', 'SO', 'output')],
|
|
[], w=12.7, desc='MAX6675 Thermoelement-Modul (Typ K), 1x5 Pin')
|
|
open(os.path.join(HERE, PROJECT + '.kicad_sym'), 'w').write(
|
|
'(kicad_symbol_lib (version 20231120) (generator "gen_sch.py")\n' + wemos + '\n' + ads + '\n' + ct + '\n' + mx + '\n)\n')
|
|
|
|
# ---------- geometry ----------
|
|
import math, heapq
|
|
from sch_layout import BLOCKS, LABELS, PSYM, NOTES as LNOTES
|
|
|
|
GRID = 1.27
|
|
POWERNETS = ('GND', '+5V', '+3V3')
|
|
|
|
def pin_lengths(sym):
|
|
res = {}
|
|
def walk(x):
|
|
for e in x:
|
|
if isinstance(e, list):
|
|
if e[0] == 'pin':
|
|
num = next(k for k in e if isinstance(k, list) and k[0] == 'number')[1].strip('"')
|
|
ln = next((k for k in e if isinstance(k, list) and k[0] == 'length'), ['length', '2.54'])
|
|
res[num] = float(ln[1])
|
|
else:
|
|
walk(e)
|
|
walk(sym)
|
|
return res
|
|
|
|
def graphic_points(sym):
|
|
pts = []
|
|
def walk(x):
|
|
for e in x:
|
|
if isinstance(e, list):
|
|
if e[0] in ('rectangle',):
|
|
for k in e:
|
|
if isinstance(k, list) and k[0] in ('start', 'end'):
|
|
pts.append((float(k[1]), float(k[2])))
|
|
elif e[0] in ('polyline', 'bezier'):
|
|
for k in e:
|
|
if isinstance(k, list) and k[0] == 'pts':
|
|
pts.extend((float(p[1]), float(p[2])) for p in k[1:])
|
|
elif e[0] == 'circle':
|
|
c = next(k for k in e if isinstance(k, list) and k[0] == 'center')
|
|
r = float(next(k for k in e if isinstance(k, list) and k[0] == 'radius')[1])
|
|
pts.extend([(float(c[1]) - r, float(c[2]) - r), (float(c[1]) + r, float(c[2]) + r)])
|
|
elif e[0] == 'arc':
|
|
for k in e:
|
|
if isinstance(k, list) and k[0] in ('start', 'mid', 'end'):
|
|
pts.append((float(k[1]), float(k[2])))
|
|
elif e[0] in ('pin', 'property'):
|
|
pass
|
|
else:
|
|
walk(e)
|
|
walk(sym)
|
|
return pts
|
|
|
|
def xf(px, py, rot, mirror):
|
|
"""lib point (y up) -> screen offset (y down)"""
|
|
x, y = px, -py
|
|
if mirror == 'y':
|
|
x = -x
|
|
elif mirror == 'x':
|
|
y = -y
|
|
r = math.radians(rot)
|
|
c, s = round(math.cos(r)), round(math.sin(r))
|
|
return (x * c + y * s, -x * s + y * c)
|
|
|
|
DIRV = {0: (1, 0), 90: (0, -1), 180: (-1, 0), 270: (0, 1)} # screen
|
|
def vdir(v):
|
|
return {(1, 0): 0, (0, -1): 90, (-1, 0): 180, (0, 1): 270}[(int(round(v[0])), int(round(v[1])))]
|
|
|
|
def G(v):
|
|
return int(round(v / GRID))
|
|
|
|
# ---------- router ----------
|
|
class Grid:
|
|
def __init__(self):
|
|
self.block = {} # cell -> 'hard'
|
|
self.soft = set()
|
|
self.occ = {} # cell -> {net: set(dirs 'h'/'v')}
|
|
self.nodes = {} # cell -> net (pins, label anchors, wire corners/ends: no foreign wire may pass)
|
|
self.edges = {} # net -> set of (cellA, cellB)
|
|
|
|
def add_edge(self, net, a, b):
|
|
e = (min(a, b), max(a, b))
|
|
self.edges.setdefault(net, set()).add(e)
|
|
d = 'h' if a[1] == b[1] else 'v'
|
|
for c in (a, b):
|
|
self.occ.setdefault(c, {}).setdefault(net, set()).add(d)
|
|
|
|
def tree(self, net):
|
|
cells = set()
|
|
for a, b in self.edges.get(net, ()):
|
|
cells.add(a); cells.add(b)
|
|
cells |= {c for c, n in self.nodes.items() if n == net}
|
|
return cells
|
|
|
|
def passable(self, c, net, d):
|
|
"""may net pass straight through c in direction d ('h'/'v')?"""
|
|
if c in self.block:
|
|
return False
|
|
n = self.nodes.get(c)
|
|
if n is not None and n != net:
|
|
return False
|
|
o = self.occ.get(c, {})
|
|
for other, dirs in o.items():
|
|
if other == net:
|
|
continue
|
|
if d in dirs or len(dirs) != 1:
|
|
return False
|
|
# other must go straight through (2 edges same axis)
|
|
deg = sum(1 for (a, b) in self.edges[other] if a == c or b == c)
|
|
if deg != 2:
|
|
return False
|
|
return True
|
|
|
|
def can_turn_or_end(self, c, net):
|
|
o = self.occ.get(c, {})
|
|
return all(other == net for other in o) and self.nodes.get(c, net) == net and c not in self.block
|
|
|
|
def route(self, net, src, targets, bounds):
|
|
"""A* from src to any target cell; returns list of cells"""
|
|
(x0, y0, x1, y1) = bounds
|
|
def h(c):
|
|
return min(abs(c[0] - t[0]) + abs(c[1] - t[1]) for t in targets) if len(targets) < 40 else 0
|
|
start = (src, None)
|
|
pq = [(h(src), 0, src, None)]
|
|
best = {start: 0}
|
|
prev = {}
|
|
while pq:
|
|
f, g, c, d = heapq.heappop(pq)
|
|
if g > best.get((c, d), 1e9):
|
|
continue
|
|
if c in targets and c != src:
|
|
if self.can_turn_or_end(c, net) or (d is not None and self._t_ok(c, net)):
|
|
path = [c]; k = (c, d)
|
|
while k in prev:
|
|
k = prev[k]; path.append(k[0])
|
|
return path[::-1]
|
|
for nd, (dx, dy) in DIRV.items():
|
|
n = (c[0] + dx, c[1] + dy)
|
|
if not (x0 <= n[0] <= x1 and y0 <= n[1] <= y1):
|
|
continue
|
|
if d is not None and nd != d and (nd - d) % 180 == 0:
|
|
continue # no reversing
|
|
turning = d is not None and nd != d
|
|
if turning and c != src and not self.can_turn_or_end(c, net):
|
|
continue
|
|
if c == src and d is None:
|
|
pass
|
|
ax = 'h' if dx else 'v'
|
|
if n in targets:
|
|
cost = 1
|
|
else:
|
|
if not self.passable(n, net, ax):
|
|
continue
|
|
cost = 1 + (6 if n in self.soft else 0) + (3 if self.occ.get(n) else 0)
|
|
cost += 4 if turning else 0
|
|
ng = g + cost
|
|
if ng < best.get((n, nd), 1e9):
|
|
best[(n, nd)] = ng; prev[(n, nd)] = (c, d)
|
|
heapq.heappush(pq, (ng + h(n), ng, n, nd))
|
|
return None
|
|
|
|
def _t_ok(self, c, net):
|
|
return self.can_turn_or_end(c, net)
|
|
|
|
# ---------- schematic ----------
|
|
def main():
|
|
write_projlib()
|
|
root = str(uuid.uuid5(uuid.NAMESPACE_URL, PROJECT + ':root'))
|
|
parts = {p['ref']: p for p in PARTS}
|
|
place = {}
|
|
blockof = {}
|
|
for b, d in BLOCKS.items():
|
|
for ref, v in d.items():
|
|
place[ref] = v; blockof[ref] = b
|
|
missing = set(parts) - set(place)
|
|
if missing:
|
|
raise SystemExit('nicht platziert: %s' % sorted(missing))
|
|
|
|
libsyms = {}
|
|
grid = Grid()
|
|
items = []
|
|
symuuids = {}
|
|
pinpos = {} # (ref, num) -> (cell, outward dir)
|
|
textboxes = []
|
|
|
|
for ref, p in parts.items():
|
|
lid = p['sym']
|
|
if lid not in libsyms:
|
|
libsyms[lid] = lib_symbol(lid)
|
|
sym = libsyms[lid]
|
|
x0, y0, rot, mir = place[ref]
|
|
pins = pins_of(sym); plen = pin_lengths(sym)
|
|
# body
|
|
gp = [xf(px, py, rot, mir) for px, py in graphic_points(sym)]
|
|
if gp:
|
|
bx0 = min(v[0] for v in gp) + x0; bx1 = max(v[0] for v in gp) + x0
|
|
by0 = min(v[1] for v in gp) + y0; by1 = max(v[1] for v in gp) + y0
|
|
for i in range(G(bx0), G(bx1) + 1):
|
|
for j in range(G(by0), G(by1) + 1):
|
|
grid.block[(i, j)] = ref
|
|
else:
|
|
bx0 = bx1 = x0; by0 = by1 = y0
|
|
for num, (px, py, ang) in pins.items():
|
|
ox, oy = xf(px, py, rot, mir)
|
|
cx, cy = x0 + ox, y0 + oy
|
|
ldx, ldy = xf(math.cos(math.radians(ang)), math.sin(math.radians(ang)), rot, mir) # toward body
|
|
out = vdir((-ldx, -ldy))
|
|
cell = (G(cx), G(cy))
|
|
pinpos[(ref, num)] = (cell, out)
|
|
# pin line cells (between endpoint and body) are blocked
|
|
L = int(round(plen.get(num, 2.54) / GRID))
|
|
for k in range(1, L + 1):
|
|
grid.block[(cell[0] + int(round(ldx)) * k, cell[1] + int(round(ldy)) * k)] = ref
|
|
grid.nodes[cell] = None # filled with net below
|
|
su = str(uuid.uuid5(uuid.NAMESPACE_URL, PROJECT + ':sym:' + ref))
|
|
symuuids[ref] = su
|
|
# fields: right of body for vertical/narrow parts, above for wide parts
|
|
pc = [xf(px, py, rot, mir) for px, py, a in pins.values()]
|
|
wide = len(pc) == 2 and abs(pc[0][1] - pc[1][1]) < 0.1 and not lid.startswith(PROJECT)
|
|
big = lid.startswith(PROJECT) or lid.startswith('Regulator') or lid.startswith('Connector') or lid.startswith('Jumper') or ref.startswith('H')
|
|
if ref.startswith('#'):
|
|
rpos = vpos = None
|
|
elif big:
|
|
rpos = (round((bx0 + bx1) / 2, 2), round(by0 - 3.81, 2), 'center'); vpos = (round((bx0 + bx1) / 2, 2), round(by0 - 1.6, 2), 'center')
|
|
if ref.startswith('H'):
|
|
rpos = (x0 + 3.81, y0 - 1.27, 'left'); vpos = (x0 + 3.81, y0 + 1.27, 'left')
|
|
elif wide:
|
|
rpos = (round((bx0 + bx1) / 2, 2), round(by0 - 1.9, 2), 'center'); vpos = (round((bx0 + bx1) / 2, 2), round(by1 + 2.0, 2), 'center')
|
|
else:
|
|
rpos = (round(bx1 + 1.6, 2), round((by0 + by1) / 2 - 0.8, 2), 'left'); vpos = (round(bx1 + 1.6, 2), round((by0 + by1) / 2 + 1.6, 2), 'left')
|
|
for pos in (rpos, vpos):
|
|
if pos:
|
|
for i in range(G(pos[0]) - (3 if pos[2] == 'center' else 0), G(pos[0]) + 5):
|
|
grid.soft.add((i, G(pos[1]))); grid.soft.add((i, G(pos[1]) - 1))
|
|
props = [('Reference', ref, rpos), ('Value', p['val'], vpos), ('Footprint', p['fp'], None), ('Datasheet', '', None),
|
|
('Description', p.get('desc', ''), None)]
|
|
if p.get('mpn'):
|
|
props.append(('Bezugsquelle', p['mpn'], None))
|
|
pr = ''
|
|
for k, v, pos in props:
|
|
if pos is None:
|
|
pr += f' (property {q(k)} {q(v)} (at {x0} {y0} 0) (effects (font (size 1.27 1.27)) (hide yes)))\n'
|
|
else:
|
|
just = '' if pos[2] == 'center' else ' (justify left)'
|
|
fa = 90 if rot in (90, 270) else 0
|
|
pr += f' (property {q(k)} {q(v)} (at {pos[0]:.2f} {pos[1]:.2f} {fa}) (effects (font (size 1.27 1.27)){just}))\n'
|
|
pinstr = ''.join(f' (pin {q(n)} (uuid {U()}))\n' for n in pins)
|
|
dnp = 'yes' if p.get('dnp') else 'no'
|
|
onb = 'no' if p.get('board') is False else 'yes'
|
|
inbom = 'no' if ref.startswith('H') or ref.startswith('#') else 'yes'
|
|
mirs = f' (mirror {mir})' if mir else ''
|
|
items.append(f'''(symbol (lib_id {q(lid)}) (at {x0} {y0} {rot}){mirs} (unit 1) (exclude_from_sim no) (in_bom {inbom}) (on_board {onb}) (dnp {dnp}) (uuid {su})
|
|
{pr}{pinstr} (instances (project {q(PROJECT)} (path {q('/' + root)} (reference {q(ref)}) (unit 1)))))''')
|
|
|
|
for (ref, num), (cell, _o) in pinpos.items():
|
|
grid.block.pop(cell, None)
|
|
dx, dy = DIRV[_o]
|
|
for k in range(1, 4): # Grafik (z.B. LED-Pfeile) darf den Pinausgang nicht zustellen
|
|
c2 = (cell[0] + dx * k, cell[1] + dy * k)
|
|
if grid.block.get(c2) == ref:
|
|
grid.block.pop(c2)
|
|
if parts[ref]['sym'].startswith('power:'):
|
|
for dx in (-1, 0, 1):
|
|
grid.block.pop((cell[0] + dx, cell[1]), None)
|
|
# nets -> pins grouped per block
|
|
netpins = {}
|
|
for ref, p in parts.items():
|
|
for num, net in p['pins'].items():
|
|
cell, out = pinpos[(ref, num)]
|
|
if net is None:
|
|
grid.nodes[cell] = '__nc__'
|
|
cx, cy = cell[0] * GRID, cell[1] * GRID
|
|
items.append(f'(no_connect (at {cx:.2f} {cy:.2f}) (uuid {U()}))')
|
|
continue
|
|
grid.nodes[cell] = net
|
|
netpins.setdefault((net, blockof[ref] if net in POWERNETS else '*'), []).append((ref, num))
|
|
for ref, p in parts.items(): # pins without a net entry (shouldn't exist)
|
|
pass
|
|
|
|
def W(a, b):
|
|
items.append(f'(wire (pts (xy {a[0]*GRID:.2f} {a[1]*GRID:.2f}) (xy {b[0]*GRID:.2f} {b[1]*GRID:.2f})) (stroke (width 0) (type default)) (uuid {U()}))')
|
|
|
|
pwr_n = [0]
|
|
def power_symbol(net, cell, out=None):
|
|
"""power symbol with its pin at cell. out = direction of the stub that leads to it (None = free)."""
|
|
plid = 'power:' + net
|
|
if plid not in libsyms:
|
|
libsyms[plid] = lib_symbol(plid)
|
|
if net == 'GND':
|
|
rot = 180 if out == 90 else ((out - 270) % 360 if out in (0, 180) else 0)
|
|
else:
|
|
rot = 180 if out == 270 else ((out - 90) % 360 if out in (0, 180) else 0)
|
|
pwr_n[0] += 1
|
|
ref = '#PWR' + str(pwr_n[0]).zfill(3)
|
|
ex, ey = cell[0] * GRID, cell[1] * GRID
|
|
bodyd = ({0: 270, 90: 0, 180: 90, 270: 180} if net == 'GND' else {0: 90, 90: 180, 180: 270, 270: 0})[rot]
|
|
bdx, bdy = DIRV[bodyd]
|
|
tdx, tdy = bdx * (3.3 + 0.45 * len(net)), bdy * 4.0
|
|
items.append(f'''(symbol (lib_id {q(plid)}) (at {ex:.2f} {ey:.2f} {rot}) (unit 1) (exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no) (uuid {U()})
|
|
(property "Reference" {q(ref)} (at {ex:.2f} {ey:.2f} 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
|
(property "Value" {q(net)} (at {ex + tdx:.2f} {ey + tdy:.2f} {90 if rot in (90, 270) else 0}) (effects (font (size 1.0 1.0))))
|
|
(property "Footprint" "" (at {ex:.2f} {ey:.2f} 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
|
(property "Datasheet" "" (at {ex:.2f} {ey:.2f} 0) (effects (font (size 1.27 1.27)) (hide yes)))
|
|
(pin "1" (uuid {U()}))
|
|
(instances (project {q(PROJECT)} (path {q('/' + root)} (reference {q(ref)}) (unit 1)))))''')
|
|
for k in range(1, 3):
|
|
for w in (-1, 0, 1):
|
|
c2 = (cell[0] + bdx * k + (w if bdy else 0), cell[1] + bdy * k + (w if bdx else 0))
|
|
grid.block.setdefault(c2, 'pwr')
|
|
|
|
def label(net, cell, d):
|
|
ang, just = {'right': (0, 'left'), 'left': (180, 'right'), 'up': (90, 'left'), 'down': (270, 'right')}[d]
|
|
items.append(f'(label {q(net)} (at {cell[0]*GRID:.2f} {cell[1]*GRID:.2f} {ang}) (effects (font (size 1.27 1.27)) (justify {just} bottom)) (uuid {U()}))')
|
|
grid.nodes[cell] = net
|
|
dx, dy = DIRV[{'right': 0, 'left': 180, 'up': 90, 'down': 270}[d]]
|
|
for k in range(1, len(net) + 2):
|
|
grid.block.setdefault((cell[0] + dx * k, cell[1] + dy * k), 'lbl')
|
|
if dx:
|
|
grid.block.setdefault((cell[0] + dx * k, cell[1] - 1), 'lbl')
|
|
|
|
# explicit labels + power symbols become nodes of their (net, block)
|
|
extra = {}
|
|
for b, net, x, y, d in LABELS:
|
|
c = (G(x), G(y)); label(net, c, d); extra.setdefault((net, b), []).append(c)
|
|
for b, net, x, y in PSYM:
|
|
c = (G(x), G(y)); grid.nodes[c] = net; power_symbol(net, c, None); extra.setdefault((net, b), []).append(c)
|
|
|
|
for (nx, ny, size, text) in LNOTES: # Überschriften nicht überfahren
|
|
for i in range(G(nx) - 1, G(nx + len(text) * size * 0.95) + 2):
|
|
for j in range(G(ny - size * 1.3), G(ny) + 2):
|
|
grid.block.setdefault((i, j), 'note')
|
|
|
|
# 1) stubs for everything that is not wired inside its block
|
|
wired = []
|
|
for (net, b), pl in sorted(netpins.items()):
|
|
nodes = [pinpos[x][0] for x in pl] + extra.get((net, b), [])
|
|
if net in POWERNETS and (net, b) not in extra:
|
|
for x in pl:
|
|
cell, out = pinpos[x]
|
|
dx, dy = DIRV[out]; e = (cell[0] + 2 * dx, cell[1] + 2 * dy)
|
|
grid.add_edge(net, cell, (cell[0] + dx, cell[1] + dy)); grid.add_edge(net, (cell[0] + dx, cell[1] + dy), e)
|
|
grid.nodes[e] = net
|
|
W(cell, e); _emitted.add((net, cell, e)); power_symbol(net, e, out)
|
|
elif len(nodes) == 1:
|
|
cell, out = pinpos[pl[0]]
|
|
dx, dy = DIRV[out]; e = (cell[0] + 2 * dx, cell[1] + 2 * dy)
|
|
grid.add_edge(net, cell, (cell[0] + dx, cell[1] + dy)); grid.add_edge(net, (cell[0] + dx, cell[1] + dy), e)
|
|
W(cell, e); _emitted.add((net, cell, e))
|
|
label(net, e, {0: 'right', 180: 'left', 90: 'up', 270: 'down'}[out])
|
|
else:
|
|
wired.append((net, b, nodes, pl))
|
|
|
|
# 2) route wired nets: shortest nets first
|
|
def span(n):
|
|
xs = [c[0] for c in n[2]]; ys = [c[1] for c in n[2]]
|
|
return (max(xs) - min(xs)) + (max(ys) - min(ys))
|
|
failed = []
|
|
for net, b, nodes, pl in sorted(wired, key=span):
|
|
xs = [c[0] for c in nodes]; ys = [c[1] for c in nodes]
|
|
bounds = (min(xs) - 40, min(ys) - 40, max(xs) + 40, max(ys) + 40)
|
|
# start from the node nearest to the centroid
|
|
cx, cy = sum(xs) / len(xs), sum(ys) / len(ys)
|
|
order = sorted(nodes, key=lambda c: abs(c[0] - cx) + abs(c[1] - cy))
|
|
connected = {order[0]}
|
|
for c in order[1:]:
|
|
if c in grid.tree(net) & connected or c in connected:
|
|
continue
|
|
tgt = (grid.tree(net) & (connected | {cc for e in grid.edges.get(net, ()) for cc in e})) | connected
|
|
# pins must leave in their outward direction: start one step out if it is a pin
|
|
path = grid.route(net, c, tgt, bounds)
|
|
if path is None:
|
|
if os.environ.get('DBG'):
|
|
print('DBG', net, 'src', c, 'block', grid.block.get(c), 'node', grid.nodes.get(c), 'occ', grid.occ.get(c), 'ntgt', len(tgt))
|
|
for dx,dy in ((1,0),(-1,0),(0,1),(0,-1)):
|
|
n=(c[0]+dx,c[1]+dy); print(' nb', n, grid.block.get(n), grid.nodes.get(n), grid.occ.get(n))
|
|
failed.append((net, b, c)); continue
|
|
for a, bb in zip(path, path[1:]):
|
|
grid.add_edge(net, a, bb)
|
|
connected |= set(path)
|
|
if failed:
|
|
print('ROUTING FAILED:', failed)
|
|
|
|
# 2b) name every wired signal net with a small label on a straight wire run (keeps PCB net names)
|
|
labelled = {(net, b) for b, net, x, y, d in LABELS} | {(net, b) for b, net, x, y in PSYM}
|
|
for net, b, nodes, pl in wired:
|
|
if net in POWERNETS or (net, b) in labelled:
|
|
continue
|
|
es = grid.edges.get(net, set())
|
|
hcells = {}
|
|
for a, c in es:
|
|
if a[1] == c[1]:
|
|
hcells.setdefault(a, set()).add(c); hcells.setdefault(c, set()).add(a)
|
|
need = int(len(net) * 1.0) + 1
|
|
best = None
|
|
for c in sorted(hcells, key=lambda c: (c[1], c[0])):
|
|
run = [c]
|
|
while (run[-1][0] + 1, run[-1][1]) in hcells.get(run[-1], ()) and len(run) <= need:
|
|
run.append((run[-1][0] + 1, run[-1][1]))
|
|
if len(run) > need and all((r[0], r[1] - 1) not in grid.block and (r[0], r[1] - 1) not in grid.soft for r in run):
|
|
best = c; break
|
|
if best is None:
|
|
# fallback: first pin of the net
|
|
best = nodes[0]
|
|
print('label on pin for', net)
|
|
cx, cy = best[0] * GRID, best[1] * GRID
|
|
items.append(f'(label {q(net)} (at {cx:.2f} {cy:.2f} 0) (effects (font (size 1.0 1.0)) (justify left bottom)) (uuid {U()}))')
|
|
for k in range(need + 1):
|
|
grid.block.setdefault((best[0] + k, best[1] - 1), 'lbl')
|
|
|
|
# 3) emit wires from edge sets (split at corners, junctions, nodes)
|
|
for net, es in grid.edges.items():
|
|
adj = {}
|
|
for a, b in es:
|
|
adj.setdefault(a, set()).add(b); adj.setdefault(b, set()).add(a)
|
|
def is_break(c):
|
|
nb = adj[c]
|
|
if len(nb) != 2 or grid.nodes.get(c) == net:
|
|
return True
|
|
a, b = list(nb)
|
|
return not (a[0] == b[0] or a[1] == b[1])
|
|
done = set()
|
|
stub_set = set()
|
|
for start in adj:
|
|
if not is_break(start):
|
|
continue
|
|
for n in adj[start]:
|
|
e = (min(start, n), max(start, n))
|
|
if e in done:
|
|
continue
|
|
path = [start, n]; done.add(e)
|
|
while not is_break(path[-1]):
|
|
nxt = [m for m in adj[path[-1]] if m != path[-2]][0]
|
|
done.add((min(path[-1], nxt), max(path[-1], nxt))); path.append(nxt)
|
|
# stubs already emitted for power/single labels: skip if both ends are stub cells
|
|
if (net, path[0], path[-1]) in _emitted or (net, path[-1], path[0]) in _emitted:
|
|
continue
|
|
W(path[0], path[-1])
|
|
for c, nb in adj.items():
|
|
if len(nb) >= 3:
|
|
items.append(f'(junction (at {c[0]*GRID:.2f} {c[1]*GRID:.2f}) (diameter 0) (color 0 0 0 0) (uuid {U()}))')
|
|
|
|
for (x, y, size, text) in LNOTES:
|
|
items.append(f'(text {q(text)} (exclude_from_sim no) (at {x} {y} 0) (effects (font (size {size} {size}) (bold yes)) (justify left bottom)) (uuid {U()}))')
|
|
|
|
libtxt = '\n'.join(dump(v, 1) for v in libsyms.values())
|
|
sch = f'''(kicad_sch (version 20231120) (generator "eeschema") (generator_version "8.0") (uuid {root}) (paper "A3")
|
|
(title_block (title "Ofensteuerung (Wemos D1 mini)") (date "2026-11-07") (rev "1.0") (company "Ramiro / OpenClaw")
|
|
(comment 1 "Pumpenrelais 230 V + Thermostat parallel + Strommessung, Abgas Typ K, 3x DS18B20, Servo-Luftklappe, PWM-Pumpe")
|
|
(comment 2 "Erzeugt mit pcb/gen_sch.py aus design.py + sch_layout.py - Änderungen dort, dann neu erzeugen"))
|
|
(lib_symbols
|
|
{libtxt})
|
|
{chr(10).join(items)}
|
|
(sheet_instances (path "/" (page "1")))
|
|
)
|
|
'''
|
|
open(os.path.join(HERE, PROJECT + '.kicad_sch'), 'w').write(sch)
|
|
json.dump(symuuids, open(os.path.join(HERE, 'symuuids.json'), 'w'), indent=1)
|
|
print('ok', len(PARTS), 'parts', 'failed' if failed else '')
|
|
|
|
_emitted = set()
|
|
|
|
if __name__ == '__main__':
|
|
main()
|