#!/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' {view}>')
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' {view}>', ' ', '']
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")