#!/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()