#!/usr/bin/env python3 """Génère matrice_3x3.fzz : maquette 3x3 sur plaque à pastilles (vue platine) + schéma électrique (vue schéma). Coordonnées en px Fritzing (90 dpi).""" import math, sys, zipfile, os from xml.sax.saxutils import escape PARTS = "/usr/share/fritzing/fritzing-parts/core/" P = 9.0 # pas 0,1 pouce # ---------------------------------------------------------------- modèle instances = [] # dict: id, module, path, title, props, views{view:{...}} idx = [1000] def nid(): idx[0] += 1 return idx[0] conns = {} # (inst_id, view, connector) -> list of (other_id, other_connector, other_layer) def link(view, a, ca, la, b, cb, lb): conns.setdefault((a, view, ca), []).append((b, cb, lb)) conns.setdefault((b, view, cb), []).append((a, ca, la)) # ---------------------------------------------------------------- géométries des pièces LED = dict(module="d4d5af9700923b8a114f57961f29a8a0ColorLEDModuleID", path=PARTS + "led-rgb-4pin-cathode_v5.fzp", legs={"connector0": (1.35, 40.6875), "connector1": (10.35, 40.6875), "connector2": (19.35, 40.6875), "connector3": (28.35, 40.6875)}, size=(0.33 * 90, 0.4521 * 90)) NPN = dict(module="2244EBEC10e", path=PARTS + "transistor_signal_NPN_TO92_CBE.fzp", # connector2 = C (gauche), connector1 = B, connector0 = E (droite) legs={"connector2": (1.35, 30.101), "connector1": (10.326, 30.101), "connector0": (19.309, 30.101)}, size=(0.22954 * 90, 0.3344583 * 90)) RES = dict(module="ResistorModuleID", path=PARTS + "resistor.fzp", legs={"connector0": (2.619, 4.5405), "connector1": (36.0063, 4.5405)}, size=(0.42917 * 90, 0.0971 * 90)) ARDUINO = dict(module="arduino_Uno_Rev3(fix)", path=PARTS + "arduino_Uno_Rev3(fix).fzp", size=(2.94961 * 90, 2.1 * 90)) ARD_PIN = { # broche -> (connector id, x, y) dans le repère de la carte (non tournée) "D2": ("connector63", 229.47, 9.0), "D3": ("connector64", 220.46, 9.0), "D4": ("connector65", 211.46, 9.0), "D5": ("connector66", 202.46, 9.0), "D6": ("connector67", 193.46, 9.0), "D7": ("connector68", 184.47, 9.0), "D8": ("connector51", 170.06, 9.0), "D9": ("connector52", 161.06, 9.0), "D10": ("connector53", 152.06, 9.0), "D11": ("connector54", 143.06, 9.0), "D12": ("connector55", 134.06, 9.0), "D13": ("connector56", 125.06, 9.0), "GND": ("connector57", 116.06, 9.0), } def rot_matrix(deg, w, h): """Rotation autour du centre (w/2, h/2), au format QTransform de Fritzing.""" cx, cy = w / 2, h / 2 a = math.radians(deg) c, s = round(math.cos(a), 9), round(math.sin(a), 9) m11, m12, m21, m22 = c, s, -s, c m31 = cx - (m11 * cx + m21 * cy) m32 = cy - (m12 * cx + m22 * cy) return (m11, m12, m21, m22, m31, m32) def apply(m, x, y): m11, m12, m21, m22, m31, m32 = m return (m11 * x + m21 * y + m31, m12 * x + m22 * y + m32) def inv_apply(m, u, v): m11, m12, m21, m22, m31, m32 = m u, v = u - m31, v - m32 det = m11 * m22 - m12 * m21 return ((m22 * u - m21 * v) / det, (-m12 * u + m11 * v) / det) # ---------------------------------------------------------------- vue platine (plaque à pastilles) COLS, ROWS = 32, 34 BOARD = dict(id=nid(), module=f"{COLS}.{ROWS}PerfboardModuleID", path=":/resources/parts/core/perfboard.fzp", title="Plaque à pastilles", props={"size": f"{COLS}.{ROWS}"}, views={}) BOARD["views"]["breadboardView"] = dict(layer="breadboardbreadboard", x=0, y=0, z=1.5) instances.append(BOARD) def hole(c, r): return (9 + P * c, 9 + P * r) def hole_id(c, r): return f"connector{r * 1000 + c}" def add_part(spec, title, bb_pos, bb_rot=0, props=None, sch=None, legs_to=None): """legs_to : {connector: (col, row)} trous de la plaque où arrivent les pattes.""" inst = dict(id=nid(), module=spec["module"], path=spec["path"], title=title, props=props or {}, views={}, legs={}) w, h = spec["size"] m = rot_matrix(bb_rot, w, h) if bb_rot else None inst["views"]["breadboardView"] = dict(layer="breadboard", x=bb_pos[0], y=bb_pos[1], z=2.5, m=m) if legs_to: for con, (c, r) in legs_to.items(): sx, sy = spec["legs"][con] hx, hy = hole(c, r) u, v = hx - bb_pos[0], hy - bb_pos[1] lx, ly = inv_apply(m, u, v) if m else (u, v) inst["legs"][con] = ((sx, sy), (lx - sx, ly - sy)) link("breadboardView", inst["id"], con, "breadboard", BOARD["id"], hole_id(c, r), "breadboardbreadboard") inst["sch"] = sch inst["holes"] = legs_to or {} instances.append(inst) return inst def place_on_holes(spec, title, holes, rot=0, lift=1.0, props=None): """Place la pièce pour que ses pattes soient au-dessus des trous, puis plie les pattes vers les trous. lift = longueur de patte visible, en pas de 0,1 po.""" cons = list(holes) # centre des trous visés, centre des départs de pattes hx = sum(hole(*holes[c])[0] for c in cons) / len(cons) hy = sum(hole(*holes[c])[1] for c in cons) / len(cons) sx = sum(spec["legs"][c][0] for c in cons) / len(cons) sy = sum(spec["legs"][c][1] for c in cons) / len(cons) w, h = spec["size"] m = rot_matrix(rot, w, h) if rot else None tx, ty = apply(m, sx, sy) if m else (sx, sy) if rot == 0: pos = (hx - tx, hy - ty - lift * P) else: pos = (hx - tx, hy - ty) return add_part(spec, title, pos, rot, props, legs_to=holes) wires = [] board_wires = [] # (trou a, trou b, couleur) pour le plan d'implantation def bb_wire(a, b, color, title=None): """Fil entre deux trous de la plaque (a, b = (col, row)).""" (x1, y1), (x2, y2) = hole(*a), hole(*b) board_wires.append((a, b, color)) w = dict(id=nid(), title=title or f"Fil{len(wires) + 1}", view="breadboardView", x=x1, y=y1, dx=x2 - x1, dy=y2 - y1, color=color, flags=64, layer="breadboardWire", z=(1.8 if color == NU else 3.5)) for k, (c, r) in enumerate((a, b)): link("breadboardView", w["id"], f"connector{k}", "breadboardWire", BOARD["id"], hole_id(c, r), "breadboardbreadboard") wires.append(w) return w ARD_POS = (40.0, -280.0) ARD_M = rot_matrix(180, *ARDUINO["size"]) ARD = dict(id=nid(), module=ARDUINO["module"], path=ARDUINO["path"], title="Arduino Uno", props={}, views={}, legs={}) ARD["views"]["breadboardView"] = dict(layer="breadboardbreadboard", x=ARD_POS[0], y=ARD_POS[1], z=1.6, m=ARD_M) instances.append(ARD) def ard_pin_xy(name): _, px, py = ARD_PIN[name] u, v = apply(ARD_M, px, py) return (ARD_POS[0] + u, ARD_POS[1] + v) def ard_wire(pin, hole_cr, color): x1, y1 = ard_pin_xy(pin) x2, y2 = hole(*hole_cr) w = dict(id=nid(), title=f"Fil {pin}", view="breadboardView", x=x1, y=y1, dx=x2 - x1, dy=y2 - y1, color=color, flags=64, layer="breadboardWire") link("breadboardView", w["id"], "connector0", "breadboardWire", ARD["id"], ARD_PIN[pin][0], "breadboardbreadboard") link("breadboardView", w["id"], "connector1", "breadboardWire", BOARD["id"], hole_id(*hole_cr), "breadboardbreadboard") wires.append(w) # couleurs de fils Fritzing NU = "#a8a8a8" # fil nu étamé, côté soudure ROUGE, VERT, BLEU = "#cc1414", "#47cc79", "#418dd9" NOIR, JAUNE, ORANGE = "#404040", "#ffe24d", "#ff7033" # --- implantation ------------------------------------------------------------- PAS = 6 # pas entre LED, en trous (6 × 2,54 = 15,24 mm) X0, Y_LED = 2, (18, 24, 30) # LED (ligne r, colonne c) : pattes R K G B COUL = {"R": 0, "G": 2, "B": 3} # décalage de la patte dans la LED PIN_ANODE = {(c, k): f"D{2 + 3 * c + i}" for c in range(3) for i, k in enumerate("RGB")} RES_VAL = {"R": "220Ω", "G": "150Ω", "B": "150Ω"} COUL_FIL = {"R": ROUGE, "G": VERT, "B": BLEU} ard_jobs = [] # (broche, trou, couleur) routés à la fin leds = {} for r in range(3): for c in range(3): x = X0 + PAS * c leds[(r, c)] = place_on_holes( LED, f"LED L{r}C{c}", {"connector0": (x, Y_LED[r]), "connector1": (x + 1, Y_LED[r]), "connector2": (x + 2, Y_LED[r]), "connector3": (x + 3, Y_LED[r])}, lift=0.6) res_anode = {} for c in range(3): for k, off in COUL.items(): col = X0 + PAS * c + off top, bot = (2, 6) if k in "RG" else (7, 11) res_anode[(c, k)] = place_on_holes( RES, f"R {k}{c}", {"connector0": (col, top), "connector1": (col, bot)}, rot=90, props={"resistance": RES_VAL[k]}) bb_wire((col, 1), (col, top), NU) ard_jobs.append((PIN_ANODE[(c, k)], (col, 1), COUL_FIL[k])) bb_wire((col, bot), (col, Y_LED[0]), NU) bb_wire((col, Y_LED[0]), (col, Y_LED[1]), NU) bb_wire((col, Y_LED[1]), (col, Y_LED[2]), NU) XC, XB, XE = 21, 22, 23 # pattes C, B, E X_GND = 30 trans, res_base = {}, {} for r in range(3): y = Y_LED[r] + 1 for c in range(3): xk = X0 + PAS * c + 1 bb_wire((xk, Y_LED[r]), (xk, y), NU) bb_wire((X0 + 1, y), (X0 + 1 + PAS, y), NOIR) bb_wire((X0 + 1 + PAS, y), (X0 + 1 + 2 * PAS, y), NOIR) bb_wire((X0 + 1 + 2 * PAS, y), (XC - 1, y), NOIR) bb_wire((XC - 1, y), (XC, y), NU) trans[r] = place_on_holes(NPN, f"T{r} (BC337)", {"connector2": (XC, y), "connector1": (XB, y), "connector0": (XE, y)}, lift=0.6) res_base[r] = place_on_holes(RES, f"Rb{r}", {"connector0": (XB, y + 1), "connector1": (XB + 4, y + 1)}, lift=0.0, props={"resistance": "1kΩ"}) bb_wire((XB, y), (XB, y + 1), NU) xw = XB + 5 + r bb_wire((XB + 4, y + 1), (xw, y + 1), NU) ard_jobs.append((f"D{11 + r}", (xw, y + 1), JAUNE)) bb_wire((XE, y), (X_GND, y), NU) yg = Y_LED[0] - 1 bb_wire((X_GND, yg), (X_GND, Y_LED[0] + 1), NU) bb_wire((X_GND, Y_LED[0] + 1), (X_GND, Y_LED[1] + 1), NU) bb_wire((X_GND, Y_LED[1] + 1), (X_GND, Y_LED[2] + 1), NU) ard_jobs.append(("GND", (X_GND, yg), NOIR)) # routage des fils Arduino : descente, palier horizontal, descente (fils coudés) def chain(points, color, pin, hole_cr): ids = [] for i in range(len(points) - 1): (x1, y1), (x2, y2) = points[i], points[i + 1] w = dict(id=nid(), title=f"Fil {pin}", view="breadboardView", x=x1, y=y1, dx=x2 - x1, dy=y2 - y1, color=color, flags=64, layer="breadboardWire") wires.append(w) ids.append(w["id"]) link("breadboardView", ids[0], "connector0", "breadboardWire", ARD["id"], ARD_PIN[pin][0], "breadboardbreadboard") link("breadboardView", ids[-1], "connector1", "breadboardWire", BOARD["id"], hole_id(*hole_cr), "breadboardbreadboard") for a_, b_ in zip(ids, ids[1:]): link("breadboardView", a_, "connector1", "breadboardWire", b_, "connector0", "breadboardWire") header_y = ard_pin_xy("D2")[1] jobs = [(pin, h, col, ard_pin_xy(pin)[0], hole(*h)[0]) for pin, h, col in ard_jobs] droite = sorted([j for j in jobs if j[4] > j[3] + 0.5], key=lambda j: j[3]) gauche = sorted([j for j in jobs if j[4] < j[3] - 0.5], key=lambda j: -j[3]) niveau = {} for groupe in (droite, gauche): n = len(groupe) for i, j in enumerate(groupe): # le plus à l'extérieur descend le plus bas niveau[j[0]] = n - i for pin, h, col, xp, xh in jobs: yp = header_y if pin in niveau: yl = header_y + 18 + 9 * niveau[pin] chain([(xp, yp), (xp, yl), (xh, yl), hole(*h)], col, pin, h) else: chain([(xp, yp), hole(*h)], col, pin, h) # ---------------------------------------------------------------- vue schéma K = 90 / 96 # px Inkscape -> px Fritzing (SVG en pouces) SCH = { # terminaux dans le repère du symbole (px Fritzing) "LED": {"connector0": (5.126, 35.457), "connector2": (23.172, 35.457), "connector3": (41.297, 35.457), "connector1": (23.225, 1.5)}, "NPN": {"connector1": (0.4375, 18.4375), "connector2": (18.4375, 0.4375), "connector0": (18.4375, 36.4375)}, "RES": {"connector0": (0.4375, 3.87), "connector1": (36.4375, 3.87)}, "RES_SIZE": (0.409722 * 90, 0.086 * 90), } def ard_sch(name): con = ARD_PIN[name][0] if name == "GND": return (72.0, 207.0) n = {"D2": 0, "D3": 1, "D4": 2, "D5": 3, "D6": 4, "D7": 5, "D8": 6, "D9": 7, "D10": 8, "D11": 9, "D12": 10, "D13": 11}[name] return (135.0, 54.0 + 9 * n) def set_sch(inst, pos, rot=0, size=None): m = rot_matrix(rot, *size) if rot else None inst["views"]["schematicView"] = dict(layer="schematic", x=pos[0], y=pos[1], z=2.5, m=m) inst["sch_pos"], inst["sch_m"] = pos, m def place_sch(inst, table, con, target, rot=0, size=None): """Place le symbole pour que le terminal `con` tombe sur `target`.""" m = rot_matrix(rot, *size) if rot else None lx, ly = SCH[table][con] if m: lx, ly = apply(m, lx, ly) set_sch(inst, (target[0] - lx, target[1] - ly), rot, size) def term(inst, table, con): lx, ly = SCH[table][con] if inst.get("sch_m"): lx, ly = apply(inst["sch_m"], lx, ly) return (inst["sch_pos"][0] + lx, inst["sch_pos"][1] + ly) ends = {} # point arrondi -> [(wire_id, connector)] def sch_poly(points, start=None, end=None): ids = [] for (x1, y1), (x2, y2) in zip(points, points[1:]): w = dict(id=nid(), title="Fil", view="schematicView", x=x1, y=y1, dx=x2 - x1, dy=y2 - y1, color="#404040", flags=128, layer="schematicTrace", mils="9.7222", z=5.5) wires.append(w) ids.append(w["id"]) for a_, b_ in zip(ids, ids[1:]): link("schematicView", a_, "connector1", "schematicTrace", b_, "connector0", "schematicTrace") if start: link("schematicView", ids[0], "connector0", "schematicTrace", start[0]["id"], start[1], "schematic") else: ends.setdefault((round(points[0][0], 2), round(points[0][1], 2)), []).append( (ids[0], "connector0")) if end: link("schematicView", ids[-1], "connector1", "schematicTrace", end[0]["id"], end[1], "schematic") else: ends.setdefault((round(points[-1][0], 2), round(points[-1][1], 2)), []).append( (ids[-1], "connector1")) A_POS = (0.0, 450.0) ARD["views"]["schematicView"] = dict(layer="schematic", x=A_POS[0], y=A_POS[1], z=2.5) def apin(name): x, y = ard_sch(name) return (A_POS[0] + x, A_POS[1] + y) ARDP = lambda name: (ARD, ARD_PIN[name][0]) MX, MY, PASS = 225.0, 60.0, 90.0 BUS_OFF = {"R": -27.0, "G": -18.0, "B": -9.0} PROF = {"R": 9.0, "G": 18.0, "B": 27.0} LEDCON = {"R": "connector0", "G": "connector2", "B": "connector3"} RES_TOP = {"R": 315.0, "G": 360.0, "B": 405.0} for (r, c), led in leds.items(): set_sch(led, (MX + PASS * c, MY + PASS * r)) for c in range(3): for k in "RGB": bx = MX + PASS * c + BUS_OFF[k] res = res_anode[(c, k)] # patte 1 (connector0) côté Arduino, en bas ; patte 2 côté LED, en haut place_sch(res, "RES", "connector1", (bx, RES_TOP[k]), rot=270, size=SCH["RES_SIZE"]) top, bot = term(res, "RES", "connector1"), term(res, "RES", "connector0") pin = PIN_ANODE[(c, k)] px, py = apin(pin) sch_poly([(px, py), (bx, py), bot], start=ARDP(pin), end=(res, "connector0")) # bus : résistance -> jonctions des lignes 2, 1, 0 pts = [top] + [(bx, MY + PASS * r + 35.457 + PROF[k]) for r in (2, 1, 0)] for i, (a_, b_) in enumerate(zip(pts, pts[1:])): sch_poly([a_, b_], start=(res, "connector1") if i == 0 else None) for r in range(3): led = leds[(r, c)] tx, ty = term(led, "LED", LEDCON[k]) jy = MY + PASS * r + 35.457 + PROF[k] sch_poly([(tx, ty), (tx, jy), (bx, jy)], start=(led, LEDCON[k])) XT = 545.0 XV = {0: 462.0, 1: 471.0, 2: 480.0} X_GND_S, Y_GND_S = XT + 45.0, 690.0 for r in range(3): yrl = MY + PASS * r + 1.5 - 9.0 ks = [term(leds[(r, c)], "LED", "connector1") for c in range(3)] for c in range(3): sch_poly([ks[c], (ks[c][0], yrl)], start=(leds[(r, c)], "connector1")) t = trans[r] set_sch(t, (XT, yrl + 18.0 - 0.4375)) cpt, bpt, ept = term(t, "NPN", "connector2"), term(t, "NPN", "connector1"), term(t, "NPN", "connector0") sch_poly([(ks[0][0], yrl), (ks[1][0], yrl)]) sch_poly([(ks[1][0], yrl), (ks[2][0], yrl)]) sch_poly([(ks[2][0], yrl), (cpt[0], yrl), cpt], end=(t, "connector2")) rb = res_base[r] # patte 1 (connector0) côté base, patte 2 côté Arduino place_sch(rb, "RES", "connector0", (bpt[0] - 9.0, bpt[1]), rot=180, size=SCH["RES_SIZE"]) rbase, rard = term(rb, "RES", "connector0"), term(rb, "RES", "connector1") sch_poly([rbase, bpt], start=(rb, "connector0"), end=(t, "connector1")) pin = f"D{11 + r}" px, py = apin(pin) sch_poly([(px, py), (XV[r], py), (XV[r], rard[1]), rard], start=ARDP(pin), end=(rb, "connector1")) sch_poly([ept, (X_GND_S, ept[1])], start=(t, "connector0")) yE = ept[1] if r == 0: y_e0 = yE else: sch_poly([(X_GND_S, prev_e), (X_GND_S, yE)]) prev_e = yE gx, gy = apin("GND") sch_poly([(X_GND_S, prev_e), (X_GND_S, Y_GND_S), (gx, Y_GND_S), (gx, gy)], end=ARDP("GND")) for pt, lst in ends.items(): for i in range(len(lst)): for j in range(i + 1, len(lst)): link("schematicView", lst[i][0], lst[i][1], "schematicTrace", lst[j][0], lst[j][1], "schematicTrace") # ---------------------------------------------------------------- écriture XML def fmt(v): return f"{v:.4f}".rstrip("0").rstrip(".") if isinstance(v, float) else str(v) def connects_xml(inst_id, view, con, indent): out = [] for (oid, ocon, olayer) in conns.get((inst_id, view, con), []): out.append(f'{indent}') return "\n".join(out) def part_xml(inst): L = [] L.append(f'') for k, v in inst["props"].items(): L.append(f' ') L.append(f' {escape(inst["title"])}') L.append(' ') for view, g in inst["views"].items(): L.append(f' <{view} layer="{g["layer"]}">') if g.get("m"): m11, m12, m21, m22, m31, m32 = g["m"] L.append(f' ') L.append(f' ') L.append(' ') else: L.append(f' ') cons = sorted({c for (i, v, c) in conns if i == inst["id"] and v == view} | (set(inst.get("legs", {})) if view == "breadboardView" else set())) if cons: L.append(' ') for con in cons: L.append(f' ') if view == "breadboardView" and con in inst.get("legs", {}): (sx, sy), (lx, ly) = inst["legs"][con] L.append(f' ') L.append(' ' f'') else: L.append(' ') L.append(' ') L.append(connects_xml(inst["id"], view, con, " ")) L.append(' ') L.append(' ') L.append(' ') L.append(f' ') L.append(' ') L.append('') return "\n".join(L) def wire_xml(w): view, layer = w["view"], w["layer"] L = [f'', f' {escape(w["title"])}', ' ', f' <{view} layer="{layer}">', f' ', f' ', ' '] for k in ("connector0", "connector1"): L.append(f' ') L.append(' ') L.append(' ') L.append(connects_xml(w["id"], view, k, " ")) L.append(' ') L.append(' ') L += [' ', f' ', ' ', ''] return "\n".join(L) def build(out_path, ino_path=None): head = ''' ''' body = "\n".join(part_xml(i) for i in instances) + "\n" + "\n".join(wire_xml(w) for w in wires) xml = head + body + "\n\n\n" name = os.path.splitext(os.path.basename(out_path))[0] with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as z: z.writestr(name + ".fz", xml) if ino_path: z.write(ino_path, os.path.basename(ino_path)) if __name__ == "__main__" and sys.argv[1] != "/dev/null": build(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else None) print(len(instances), "pièces,", len(wires), "fils")