Files
le-mystere-professeur-zacus/hardware/projects/plip-telephone/tools/gen_schematic.py
T
Claude Worker claude2 e1a116d0f5
Repo State / repo-state (push) Failing after 48s
Validate Zacus refactor / validate (pull_request) Successful in 11m10s
Repo State / repo-state (pull_request) Failing after 12m5s
feat(hw): PLIP telephone KiCad project
Custom ESP32-WROOM-32E + Si3210 SLIC board replacing the ESP32-A1S
dev kit (final PLIP target, RJ9 handset). Generated via tools/
gen_si3210_lib.py (symbol + QFN-38 footprint), gen_schematic.py
(parses standard KiCad libs, 45 instances, 157 global labels on pin
ends) and gen_pcb.py (58x38mm outline, 41 footprints placed per the
plan, nets assigned, unrouted).

ERC: 0 violations. Schematic follows the spec GPIO map and the plan's
passive network. PCB placed not routed (Freerouting + DRC + gerbers
remain — see README). BOM in JLCPCB format; CP2102N/transformer/RJ9
LCSC parts marked TBD.

WARNINGS (README): the Si3210 -72V DC-DC stage is simplified and must
be checked against the Skyworks datasheet/AN35 before fab; line
protection is minimal; human review required before ordering.
2026-06-10 12:52:43 +02:00

452 lines
19 KiB
Python

#!/usr/bin/env python3
"""Génère plip-telephone.kicad_sch (format 20231120).
Pipeline :
1. extrait les blocs (symbol "NOM" ...) des libs standard KiCad + lib locale Si3210 ;
2. les embarque dans (lib_symbols ...) sous l'id "Lib:Nom" (aplatissement des extends) ;
3. place les instances en rotation 0 et connecte par labels globaux posés
exactement à l'extrémité des broches (positions absolues calculées depuis
les (pin ... (at x y angle)) du symbole) ;
4. pose des (no_connect) sur les broches libres ;
5. exporte tools/netmap.json (ref -> pad -> net) réutilisé par gen_pcb.py.
"""
import copy
import json
import os
import re
import sys
import uuid
KICAD_SYMS = "/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols"
HERE = os.path.dirname(os.path.abspath(__file__))
PROJ = os.path.dirname(HERE)
ROOT_UUID = "5a3c9d2e-1f47-4b8a-9c61-7e2d84f0b3a1"
PROJECT_NAME = "plip-telephone"
NC = "NC" # marqueur broche libre -> (no_connect)
# --------------------------------------------------------------------------- sexpr
def tokenize(text):
return re.findall(r'"(?:[^"\\]|\\.)*"|\(|\)|[^\s()"]+', text)
def parse(tokens):
pos = [0]
def rec():
assert tokens[pos[0]] == "("
pos[0] += 1
out = []
while tokens[pos[0]] != ")":
if tokens[pos[0]] == "(":
out.append(rec())
else:
out.append(tokens[pos[0]])
pos[0] += 1
pos[0] += 1
return out
return rec()
def serialize(node, indent=0):
if isinstance(node, str):
return node
inner = [serialize(c, indent + 1) for c in node]
one = "(" + " ".join(inner) + ")"
if len(one) <= 110 and "\n" not in one:
return one
pad = " " * (indent + 1)
head = []
i = 0
while i < len(node) and isinstance(node[i], str):
head.append(inner[i])
i += 1
out = "(" + " ".join(head)
for j in range(i, len(inner)):
out += "\n" + pad + inner[j].replace("\n", "\n")
out += ")"
return out
def q(s):
return '"%s"' % s
def unq(s):
return s[1:-1] if isinstance(s, str) and s.startswith('"') else s
def fnum(v):
s = ("%.4f" % float(v)).rstrip("0").rstrip(".")
return s if s not in ("-0",) else "0"
def new_uuid():
return str(uuid.uuid4())
# --------------------------------------------------------------------------- libs
_lib_cache = {}
def load_lib(path):
if path not in _lib_cache:
_lib_cache[path] = parse(tokenize(open(path).read()))
return _lib_cache[path]
def lib_path(libname):
if libname == "Si3210":
return os.path.join(PROJ, "libs", "Si3210.kicad_sym")
return os.path.join(KICAD_SYMS, libname + ".kicad_sym")
def find_symbol(tree, name):
for n in tree:
if isinstance(n, list) and n and n[0] == "symbol" and unq(n[1]) == name:
return n
return None
def strip_nodes(node, names):
if not isinstance(node, list):
return node
return [strip_nodes(c, names) for c in node
if not (isinstance(c, list) and c and c[0] in names)]
def get_prop(sym, pname):
for n in sym:
if isinstance(n, list) and n and n[0] == "property" and unq(n[1]) == pname:
return n
return None
def set_prop_value(sym, pname, value):
p = get_prop(sym, pname)
if p is not None:
p[2] = q(value)
else:
sym.append(["property", q(pname), q(value),
["at", "0", "0", "0"],
["effects", ["font", ["size", "1.27", "1.27"]], "hide"]])
def extract_symbol(libname, symname):
"""Retourne le bloc symbole aplati (extends résolu), renommé Lib:Nom."""
tree = load_lib(lib_path(libname))
sym = find_symbol(tree, symname)
if sym is None:
raise SystemExit("symbole introuvable: %s:%s" % (libname, symname))
sym = copy.deepcopy(sym)
ext = [n for n in sym if isinstance(n, list) and n and n[0] == "extends"]
if ext:
base_name = unq(ext[0][1])
base = copy.deepcopy(find_symbol(tree, base_name))
if base is None:
raise SystemExit("base extends introuvable: %s" % base_name)
# part des graphismes/broches/attributs de la base, propriétés du dérivé
merged = [n for n in base if not (isinstance(n, list) and n and n[0] == "property")]
merged[1] = q(symname)
# renomme les sous-symboles BASE_x_y -> DERIVE_x_y
for n in merged:
if isinstance(n, list) and n and n[0] == "symbol":
n[1] = q(unq(n[1]).replace(base_name, symname))
props = [n for n in sym if isinstance(n, list) and n and n[0] == "property"]
# insère les propriétés après le nom
merged[2:2] = props
sym = merged
sym[1] = q("%s:%s" % (libname, symname))
sym = strip_nodes(sym, {"embedded_fonts"})
return sym
def expand_numbers(numstr):
if numstr.startswith("[") and numstr.endswith("]"):
return [x.strip() for x in numstr[1:-1].split(",")]
return [numstr]
def symbol_pins(sym):
"""[(numbers[], name, etype, x, y, angle)] sur tous les sous-symboles."""
pins = []
for sub in sym:
if not (isinstance(sub, list) and sub and sub[0] == "symbol"):
continue
for p in sub:
if not (isinstance(p, list) and p and p[0] == "pin"):
continue
at = next(x for x in p if isinstance(x, list) and x[0] == "at")
name = next(unq(x[1]) for x in p if isinstance(x, list) and x[0] == "name")
nums = next(unq(x[1]) for x in p if isinstance(x, list) and x[0] == "number")
pins.append((expand_numbers(nums), name, p[1],
float(at[1]), float(at[2]), float(at[3])))
return pins
# --------------------------------------------------------------------------- composants
# (ref, lib, symbole, valeur, footprint, LCSC, (x, y) schéma, {pad: net|NC})
COMPONENTS = [
("U1", "RF_Module", "ESP32-WROOM-32E", "ESP32-WROOM-32E-N16",
"RF_Module:ESP32-WROOM-32E", "C701341", (170, 160), {
"1": "GND", "15": "GND", "38": "GND", "39": "GND",
"2": "3V3", "3": "ESP_EN", "25": "ESP_IO0",
"30": "SPI_SCLK", "37": "SPI_MOSI", "31": "SPI_MISO",
"29": "SLIC_CS", "23": "SD_CS", "26": "SLIC_INT", "24": "SLIC_RST",
"11": "PCM_PCLK", "10": "PCM_FSYNC", "36": "PCM_DRX", "7": "PCM_DTX",
"28": "UART_UI_TX", "27": "UART_UI_RX",
"35": "ESP_TXD0", "34": "ESP_RXD0", "16": "LED_STATUS",
"4": NC, "5": NC, "6": NC, "8": NC, "9": NC, "12": NC, "13": NC,
"14": NC, "17": NC, "18": NC, "19": NC, "20": NC, "21": NC,
"22": NC, "32": NC, "33": NC,
}),
("U2", "Si3210", "Si3210", "Si3210-E-FM",
"Si3210:Si3210_QFN38", "C6295850", (260, 200), {
"34": "SPI_SCLK", "33": "SPI_MOSI", "32": "SPI_MISO", "35": "SLIC_CS",
"37": "PCM_PCLK", "2": "PCM_FSYNC", "1": "PCM_DTX", "38": "PCM_DRX",
"3": "SLIC_RST", "36": "SLIC_INT", "28": "GND",
"6": "3V3", "23": "3V3", "26": "3V3",
"19": "GND", "27": "GND", "9": "GND", "39": "GND",
"16": "TIP_AC", "17": "RING_AC",
"30": "DCDRV", "14": "SVBAT", "29": "DCFF",
"7": "IREF", "8": "CAPP", "10": "CAPM",
"11": NC, "12": NC, "13": NC, "15": NC, "18": NC, "20": NC,
"21": NC, "22": NC, "24": NC, "25": NC, "4": NC, "5": NC, "31": NC,
}),
("U3", "Interface_USB", "CP2102N-Axx-xQFN20", "CP2102N-A02-GQFN20",
"Package_DFN_QFN:QFN-20-1EP_3x3mm_P0.4mm_EP1.65x1.65mm", "TBD", (90, 110), {
"3": "GND", "12": "GND", "21": "GND",
"6": "CP_VDD", "7": "5V", "8": "VBUS",
"4": "CP_DP", "5": "CP_DM",
"17": "ESP_TXD0", "18": "ESP_RXD0", "16": "CP_RTS",
"1": NC, "2": NC, "9": NC, "10": NC, "11": NC, "13": NC,
"14": NC, "15": NC, "19": NC, "20": NC,
}),
("U4", "Power_Protection", "USBLC6-2SC6", "USBLC6-2SC6",
"Package_TO_SOT_SMD:SOT-23-6", "C7519", (90, 55), {
"1": "USB_DP", "3": "USB_DM", "6": "USB_DP_R", "4": "USB_DM_R",
"5": "VBUS", "2": "GND",
}),
("U5", "Regulator_Linear", "ME6211C33M5", "ME6211C33M5G-N",
"Package_TO_SOT_SMD:SOT-23-5", "C82942", (140, 50), {
"1": "5V", "2": "GND", "3": "5V", "5": "3V3", "4": NC,
}),
("J1", "Connector", "USB_C_Receptacle_USB2.0_16P", "USB-C 16P",
"Connector_USB:USB_C_Receptacle_HRO_TYPE-C-31-M-12", "C2765186", (45, 60), {
"A4": "VBUS", "A9": "VBUS", "B4": "VBUS", "B9": "VBUS",
"A1": "GND", "B1": "GND", "A12": "GND", "B12": "GND", "SH": "GND",
"A5": "CC1", "B5": "CC2",
"A6": "USB_DP", "B6": "USB_DP", "A7": "USB_DM", "B7": "USB_DM",
"A8": NC, "B8": NC,
}),
("J2", "Connector", "4P4C", "RJ9_4P4C",
"Si3210:RJ9_4P4C", "TBD", (375, 245), {
"1": NC, "2": "PHONE_TIP", "3": "PHONE_RING", "4": NC,
}),
("J3", "Connector_Generic", "Conn_01x04", "JST_SH_UI_LINK",
"Connector_JST:JST_SH_BM04B-SRSS-TB_1x04-1MP_P1.00mm_Vertical", "C265021", (375, 200), {
"1": "3V3", "2": "UART_UI_TX", "3": "UART_UI_RX", "4": "GND",
}),
("SD1", "Connector", "Micro_SD_Card", "Micro_SD_SPI",
"Connector_Card:microSD_HC_Hirose_DM3AT-SF-PEJM5", "C585353", (260, 90), {
"1": NC, "2": "SD_CS", "3": "SPI_MOSI", "4": "3V3",
"5": "SPI_SCLK", "6": "GND", "7": "SPI_MISO", "8": NC, "SH": "GND",
}),
("TR1", "Device", "Transformer_1P_1S", "600R:600R",
"Si3210:Transformer_600_600", "TBD", (340, 245), {
"1": "TIP_AC", "2": "RING_AC", "3": "PHONE_RING", "4": "PHONE_TIP",
}),
("FB1", "Device", "FerriteBead", "FB_600R@100MHz",
"Inductor_SMD:L_0805_2012Metric", "TBD", (120, 45), {
"1": "VBUS", "2": "5V",
}),
("L1", "Device", "L", "100uH",
"Inductor_SMD:L_1210_3225Metric", "TBD", (310, 185), {
"1": "DCDRV", "2": "SVBAT",
}),
# --- résistances
("R1", "Device", "R", "5.1k", "Resistor_SMD:R_0402_1005Metric", "TBD", (105, 75),
{"1": "CC1", "2": "GND"}),
("R2", "Device", "R", "5.1k", "Resistor_SMD:R_0402_1005Metric", "TBD", (112, 75),
{"1": "CC2", "2": "GND"}),
("R3", "Device", "R", "22R", "Resistor_SMD:R_0402_1005Metric", "TBD", (115, 55),
{"1": "USB_DP_R", "2": "CP_DP"}),
("R4", "Device", "R", "22R", "Resistor_SMD:R_0402_1005Metric", "TBD", (122, 55),
{"1": "USB_DM_R", "2": "CP_DM"}),
("R5", "Device", "R", "10k", "Resistor_SMD:R_0402_1005Metric", "C25744", (140, 120),
{"1": "3V3", "2": "ESP_EN"}),
("R6", "Device", "R", "10k", "Resistor_SMD:R_0402_1005Metric", "C25744", (147, 120),
{"1": "3V3", "2": "ESP_IO0"}),
("R7", "Device", "R", "10k", "Resistor_SMD:R_0402_1005Metric", "C25744", (154, 120),
{"1": "3V3", "2": "SPI_MOSI"}),
("R8", "Device", "R", "10k", "Resistor_SMD:R_0402_1005Metric", "C25744", (161, 120),
{"1": "3V3", "2": "SPI_MISO"}),
("R9", "Device", "R", "10k", "Resistor_SMD:R_0402_1005Metric", "C25744", (168, 120),
{"1": "3V3", "2": "SD_CS"}),
("R15", "Device", "R", "10k", "Resistor_SMD:R_0402_1005Metric", "C25744", (175, 120),
{"1": "3V3", "2": "SLIC_INT"}),
("R10", "Device", "R", "20k", "Resistor_SMD:R_0402_1005Metric", "TBD", (300, 235),
{"1": "IREF", "2": "GND"}),
("R11", "Device", "R", "100k", "Resistor_SMD:R_0402_1005Metric", "TBD", (320, 185),
{"1": "SVBAT", "2": "DCFF"}),
("R12", "Device", "R", "4.7k", "Resistor_SMD:R_0402_1005Metric", "TBD", (330, 185),
{"1": "DCFF", "2": "GND"}),
("R13", "Device", "R", "1k", "Resistor_SMD:R_0402_1005Metric", "TBD", (210, 170),
{"1": "3V3", "2": "LED_PWR_A"}),
("R14", "Device", "R", "1k", "Resistor_SMD:R_0402_1005Metric", "TBD", (210, 160),
{"1": "LED_STATUS", "2": "LED_ST_A"}),
# --- condensateurs
("C1", "Device", "C", "100nF", "Capacitor_SMD:C_0402_1005Metric", "C307331", (200, 120),
{"1": "3V3", "2": "GND"}),
("C2", "Device", "C", "100nF", "Capacitor_SMD:C_0402_1005Metric", "C307331", (340, 120),
{"1": "3V3", "2": "GND"}),
("C3", "Device", "C", "100nF", "Capacitor_SMD:C_0402_1005Metric", "C307331", (347, 120),
{"1": "3V3", "2": "GND"}),
("C4", "Device", "C", "100nF", "Capacitor_SMD:C_0402_1005Metric", "C307331", (354, 120),
{"1": "3V3", "2": "GND"}),
("C5", "Device", "C", "100nF", "Capacitor_SMD:C_0402_1005Metric", "C307331", (110, 125),
{"1": "CP_VDD", "2": "GND"}),
("C6", "Device", "C", "100nF", "Capacitor_SMD:C_0402_1005Metric", "C307331", (290, 90),
{"1": "3V3", "2": "GND"}),
("C7", "Device", "C", "100nF", "Capacitor_SMD:C_0402_1005Metric", "C307331", (120, 125),
{"1": "CP_RTS", "2": "ESP_EN"}),
("C8", "Device", "C", "100nF", "Capacitor_SMD:C_0402_1005Metric", "C307331", (310, 225),
{"1": "CAPP", "2": "CAPM"}),
("C9", "Device", "C", "1uF", "Capacitor_SMD:C_0402_1005Metric", "TBD", (155, 50),
{"1": "5V", "2": "GND"}),
("C10", "Device", "C", "1uF", "Capacitor_SMD:C_0402_1005Metric", "TBD", (165, 50),
{"1": "3V3", "2": "GND"}),
("C11", "Device", "C", "10uF", "Capacitor_SMD:C_0805_2012Metric", "C19702", (175, 50),
{"1": "3V3", "2": "GND"}),
("C12", "Device", "C", "10uF", "Capacitor_SMD:C_0805_2012Metric", "C19702", (185, 50),
{"1": "3V3", "2": "GND"}),
# --- LEDs
("D1", "Device", "LED", "LED_PWR", "LED_SMD:LED_0402_1005Metric", "C2286", (220, 170),
{"2": "LED_PWR_A", "1": "GND"}),
("D2", "Device", "LED", "LED_STATUS", "LED_SMD:LED_0402_1005Metric", "C2286", (220, 160),
{"2": "LED_ST_A", "1": "GND"}),
# --- PWR_FLAG (pas sur 3V3 : déjà piloté par V_OUT du LDO, power output)
("PF1", "power", "PWR_FLAG", "PWR_FLAG", "", "", (200, 40), {"1": "GND"}),
("PF3", "power", "PWR_FLAG", "PWR_FLAG", "", "", (212, 40), {"1": "5V"}),
("PF4", "power", "PWR_FLAG", "PWR_FLAG", "", "", (218, 40), {"1": "VBUS"}),
("PF5", "power", "PWR_FLAG", "PWR_FLAG", "", "", (224, 40), {"1": "CP_VDD"}),
]
LABEL_ANGLE = {0: 180, 180: 0, 90: 270, 270: 90}
def main():
# 1+2. lib_symbols embarqués
needed = {}
for (_, libname, symname, *_rest) in [(c[0], c[1], c[2]) for c in COMPONENTS]:
needed[(libname, symname)] = True
lib_symbols = ["lib_symbols"]
pins_by_sym = {}
for (libname, symname) in needed:
sym = extract_symbol(libname, symname)
lib_symbols.append(sym)
pins_by_sym[(libname, symname)] = symbol_pins(sym)
body = []
labels = {} # (x, y) -> (net, angle) déduplication par position
no_connects = [] # (x, y)
netmap = {}
for (ref, libname, symname, value, footprint, lcsc, (X, Y), conns) in COMPONENTS:
# snap sur la grille 1.27 mm (les offsets de pins sont des multiples de 1.27)
X = round(round(X / 1.27) * 1.27, 2)
Y = round(round(Y / 1.27) * 1.27, 2)
pins = pins_by_sym[(libname, symname)]
all_nums = [n for p in pins for n in p[0]]
missing = set(all_nums) - set(conns)
extra = set(conns) - set(all_nums)
if missing or extra:
raise SystemExit("%s: broches manquantes=%s en-trop=%s" % (ref, sorted(missing), sorted(extra)))
inst = ["symbol", ["lib_id", q("%s:%s" % (libname, symname))],
["at", fnum(X), fnum(Y), "0"], ["unit", "1"],
["exclude_from_sim", "no"], ["in_bom", "yes" if libname != "power" else "no"],
["on_board", "yes"], ["dnp", "no"], ["uuid", q(new_uuid())],
["property", q("Reference"), q(ref),
["at", fnum(X), fnum(Y - 2.54), "0"],
["effects", ["font", ["size", "1.27", "1.27"]]]],
["property", q("Value"), q(value),
["at", fnum(X), fnum(Y + 2.54), "0"],
["effects", ["font", ["size", "1.27", "1.27"]]]],
["property", q("Footprint"), q(footprint),
["at", fnum(X), fnum(Y), "0"],
["effects", ["font", ["size", "1.27", "1.27"]], "hide"]],
["property", q("Datasheet"), q("~"),
["at", fnum(X), fnum(Y), "0"],
["effects", ["font", ["size", "1.27", "1.27"]], "hide"]],
]
if lcsc:
inst.append(["property", q("LCSC"), q(lcsc),
["at", fnum(X), fnum(Y), "0"],
["effects", ["font", ["size", "1.27", "1.27"]], "hide"]])
inst.append(["instances",
["project", q(PROJECT_NAME),
["path", q("/" + ROOT_UUID),
["reference", q(ref)], ["unit", "1"]]]])
body.append(inst)
# labels / no_connect aux extrémités de broches (rotation 0, pas de miroir :
# absolu = (X + px, Y - py))
for (nums, _name, _etype, px, py, pang) in pins:
ax = round(X + px, 2)
ay = round(Y - py, 2)
nets = {conns[n] for n in nums}
if len(nets) != 1:
raise SystemExit("%s broches empilées %s avec nets divergents %s" % (ref, nums, nets))
net = nets.pop()
if net == NC:
no_connects.append((ax, ay))
else:
key = (ax, ay)
if key in labels and labels[key][0] != net:
raise SystemExit("collision de labels en %s: %s vs %s" % (key, labels[key][0], net))
labels[key] = (net, LABEL_ANGLE[int(pang) % 360])
if footprint:
netmap[ref] = {
"footprint": footprint, "value": value, "lcsc": lcsc,
"pads": {n: (None if v == NC else v) for n, v in conns.items()},
}
for (x, y), (net, ang) in sorted(labels.items()):
body.append(["global_label", q(net), ["shape", "passive"],
["at", fnum(x), fnum(y), str(ang)],
["fields_autoplaced", "yes"],
["effects", ["font", ["size", "1.27", "1.27"]],
["justify", "right" if ang == 180 else "left"]],
["uuid", q(new_uuid())]])
for (x, y) in sorted(no_connects):
body.append(["no_connect", ["at", fnum(x), fnum(y)], ["uuid", q(new_uuid())]])
doc = ["kicad_sch", ["version", "20231120"], ["generator", q("gen_schematic.py")],
["generator_version", q("8.0")], ["uuid", q(ROOT_UUID)], ["paper", q("A3")],
["title_block", ["title", q("PLIP Telephone - Zacus")],
["date", q("2026-06-10")], ["rev", q("A")],
["company", q("Le Mystere du Professeur Zacus")]],
lib_symbols] + body + [
["sheet_instances", ["path", q("/"), ["page", q("1")]]]]
out = os.path.join(PROJ, "plip-telephone.kicad_sch")
with open(out, "w") as f:
f.write(serialize(doc) + "\n")
with open(os.path.join(HERE, "netmap.json"), "w") as f:
json.dump(netmap, f, indent=1, ensure_ascii=False, sort_keys=True)
print("écrit:", out)
print("écrit:", os.path.join(HERE, "netmap.json"))
print("labels: %d, no_connect: %d, composants: %d" % (len(labels), len(no_connects), len(COMPONENTS)))
if __name__ == "__main__":
main()