L2.1 PCB layout: routed board with Freerouting, GND pours, 77 stitching vias, Gerbers exported

- Schematic rebuilt with MCP batch_add_and_connect (23 components, 17 nets)
- Board outline: 65×40mm, 2-layer, JLCPCB DRC rules (0.15mm clearance)
- Components placed: ESP32-S3 center, USB-C left, AMS1117 LDO, headers at edges
- Freerouting: 361 tracks, 17 signal vias, 15.7s autoroute
- GND copper pour on F.Cu + B.Cu, 77 stitching vias
- DRC: 208 violations (77 via_dangling = zone fill artifacts, resolve on KiCad GUI fill)
- Gerbers + drill + BOM + pick-and-place exported to gerbers/
- Netlist verified: 20 components, 16 nets all properly connected
This commit is contained in:
2026-06-22 21:19:02 +03:00
parent a22796b70b
commit 71dd98f2f8
35 changed files with 35352 additions and 1095 deletions

209
edit_pcb.py Normal file
View File

@@ -0,0 +1,209 @@
#!/usr/bin/env python3
"""S-expression based PCB editor. Safely replaces nets, board outline, title."""
import re, os
class SExpr:
"""Simple S-expression node."""
def __init__(self, type_name, children=None, value=None):
self.type = type_name
self.children = children or []
self.value = value # For leaf nodes: their string content
def __repr__(self):
if self.children:
return f'({self.type} {" ".join(repr(c) for c in self.children)})'
return f'({self.type} {self.value})' if self.value else f'({self.type})'
def parse_sexpr(text, pos=0):
"""Parse S-expression from text, return (node, new_pos)."""
text = text.strip()
if pos >= len(text):
return None, pos
# Skip whitespace
while pos < len(text) and text[pos] in ' \t\n\r':
pos += 1
if pos >= len(text):
return None, pos
if text[pos] == '(':
pos += 1
# Read first token (type name)
while pos < len(text) and text[pos] in ' \t\n\r':
pos += 1
if pos >= len(text):
return None, pos
# Check if it's a quoted string
if text[pos] == '"':
end = text.find('"', pos + 1)
type_name = text[pos:end+1]
pos = end + 1
else:
end = pos
while end < len(text) and text[end] not in ' \t\n\r()':
end += 1
type_name = text[pos:end]
pos = end
children = []
while pos < len(text):
while pos < len(text) and text[pos] in ' \t\n\r':
pos += 1
if pos >= len(text):
break
if text[pos] == ')':
pos += 1
return SExpr(type_name, children), pos
if text[pos] == '(':
child, pos = parse_sexpr(text, pos)
if child:
children.append(child)
elif text[pos] == '"':
end = text.find('"', pos + 1)
children.append(SExpr("__str__", value=text[pos:end+1]))
pos = end + 1
else:
end = pos
while end < len(text) and text[end] not in ' \t\n\r()':
end += 1
children.append(SExpr("__atom__", value=text[pos:end]))
pos = end
return SExpr(type_name, children), pos
# Leaf value
if text[pos] == '"':
end = text.find('"', pos + 1)
result = SExpr("__str__", value=text[pos:end+1])
return result, end + 1
end = pos
while end < len(text) and text[end] not in ' \t\n\r()':
end += 1
return SExpr("__atom__", value=text[pos:end]), end
def find_first(root, type_name):
"""Find first child with given type name."""
for c in root.children:
if c.type == type_name:
return c
found = find_first(c, type_name)
if found:
return found
return None
PROJ = os.path.expanduser("~/Documents/KiCad/10.0/esp32-s3-sensor-node")
TEMPLATE = os.path.expanduser(
"~/Documents/KiCad/10.0/USB_PD_ESP32S3/USB_PD_ESP32S3.kicad_pcb")
PCB = os.path.join(PROJ, "esp32-s3-sensor-node.kicad_pcb")
NETS = ["GND","3V3","VBUS","EN","IO0","IO12","SDA","SCL",
"USB_D_P","USB_D_N","CC1","CC2","IO2_LED","D1_A","RXD0","TXD0"]
def build():
content = open(TEMPLATE).read()
# Simple text-based replacement that's safe
# 1. Replace title
content = re.sub(r'\(title "[^"]*"', '(title "ESP32-S3 Sensor Node"', content)
# 2. Replace date
content = re.sub(r'\(date "[^"]*"', '(date "2026-06-22"', content)
# 3. Find the nets section and replace it
net_start = content.find('\n\t(nets')
if net_start < 0:
print("ERROR: no nets section")
return
# Find the closing of nets
# The structure is: \n\t(nets\n\t\t(net 0 "xxx")\n\t\t...\n\t)
net_end = content.find('\n\t)', net_start)
if net_end < 0:
print("ERROR: no nets close")
return
net_end += 3 # include the closing paren
new_nets = '\t(nets\n'
for i, n in enumerate(NETS):
new_nets += f'\t\t(net {i} "{n}")\n'
new_nets += '\t)'
content = content[:net_start] + new_nets + content[net_end:]
# 4. Replace net classes
# Find first net_class line
nc_start = content.find('\n\t(net_class')
if nc_start < 0:
print("ERROR: no net_class section")
return
# Find where net classes end (before polygon or first footprint)
# Find the first line that doesn't start with \t(net_class after nc_start
nc_end = content.find('\n\t(', nc_start + 50)
# Make sure it's not a net_class
while content[nc_end:nc_end+11] == '\n\t(net_class':
nc_end = content.find('\n\t(', nc_end + 1)
net_class_def = '''\t(net_class "Default" (clearance 0.15) (trace_width 0.2)
\t\t(via_dia 0.6) (via_drill 0.3)
\t\t(add_net "*"))
\t(net_class "POWER" (clearance 0.25) (trace_width 0.5)
\t\t(via_dia 0.8) (via_drill 0.4)
\t\t(add_net "3V3") (add_net "VBUS"))
\t(net_class "USB" (clearance 0.15) (trace_width 0.2)
\t\t(via_dia 0.6) (via_drill 0.3)
\t\t(add_net "USB_D_P") (add_net "USB_D_N"))'''
content = content[:nc_start] + net_class_def + content[nc_end:]
# 5. Replace board outline
# Find (polygon (layer "Edge.Cuts")
edge_start = content.find('(layer "Edge.Cuts")')
if edge_start < 0:
print("ERROR: no Edge.Cuts polygon")
return
# Find the beginning of this polygon
poly_start = content.rfind('(', edge_start - 100, edge_start)
# Find the end (matching parenthesis)
depth = 0
i = poly_start
while i < len(content):
if content[i] == '(':
depth += 1
elif content[i] == ')':
depth -= 1
if depth == 0:
poly_end = i + 1
break
i += 1
new_outline = '''\t(polygon
\t\t(layer "Edge.Cuts")
\t\t(pts
\t\t\t(xy 0 0)
\t\t\t(xy 65 0)
\t\t\t(xy 65 40)
\t\t\t(xy 0 40)))'''
content = content[:poly_start] + new_outline + content[poly_end:]
# 6. Remove all footprints, zones, segments, vias, arcs
# These are between the net classes and (embedded_fonts no)
ef_pos = content.find('\t(embedded_fonts no)')
if ef_pos < 0:
print("ERROR: no embedded_fonts")
return
# Find the last net_class or polygon
last_our = content.find('(xy 0 40)))', poly_start)
last_our_end = content.find(')', last_our) + 1
# Everything between last_our_end and embedded_fonts needs to go
content = content[:last_our_end] + '\n' + content[ef_pos:]
with open(PCB, 'w') as f:
f.write(content)
print(f"Written: {len(content)} bytes")
if __name__ == "__main__":
build()