- KiCad project generated by pcb/tools (design, route, schematic, fab outputs). - v0.4 fixes the v0.3 review findings: low-dropout 3.3 V regulator, OR'd 5 V supply, polyfuse, USB ESD protection, input clamps, ceramic decoupling at the drivers, A4988/DRV8825 jumper, ground stitching. DRC/ERC clean. - v0.3 (as ordered) kept in pcb/archive/v0.3. - FluidNC config.yaml for the board; rho hard limit on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
101 lines
2.7 KiB
Python
101 lines
2.7 KiB
Python
"""Tiny S-expression reader/writer for KiCad files.
|
|
|
|
Lists are Python lists; atoms are str. Quoted strings are kept as QStr so they
|
|
round-trip with quotes. Numbers stay as plain str atoms (KiCad cares about text).
|
|
"""
|
|
|
|
|
|
class QStr(str):
|
|
"""A string that was quoted in the source (and must be written quoted)."""
|
|
|
|
|
|
def parse(text):
|
|
pos = 0
|
|
n = len(text)
|
|
stack = [[]]
|
|
while pos < n:
|
|
c = text[pos]
|
|
if c in ' \t\r\n':
|
|
pos += 1
|
|
elif c == '(':
|
|
stack.append([])
|
|
pos += 1
|
|
elif c == ')':
|
|
done = stack.pop()
|
|
stack[-1].append(done)
|
|
pos += 1
|
|
elif c == '"':
|
|
pos += 1
|
|
buf = []
|
|
while True:
|
|
ch = text[pos]
|
|
if ch == '\\':
|
|
buf.append(text[pos:pos + 2])
|
|
pos += 2
|
|
continue
|
|
if ch == '"':
|
|
pos += 1
|
|
break
|
|
buf.append(ch)
|
|
pos += 1
|
|
raw = ''.join(buf)
|
|
# keep escapes as-is (we re-emit them unchanged)
|
|
stack[-1].append(QStr(raw))
|
|
else:
|
|
start = pos
|
|
while pos < n and text[pos] not in ' \t\r\n()"':
|
|
pos += 1
|
|
stack[-1].append(text[start:pos])
|
|
assert len(stack) == 1
|
|
return stack[0][0] if len(stack[0]) == 1 else stack[0]
|
|
|
|
|
|
def dumps(node, indent=0):
|
|
"""Serialize back to KiCad-style text (one sub-list per line when nested)."""
|
|
if isinstance(node, QStr):
|
|
return '"' + str(node) + '"'
|
|
if isinstance(node, str):
|
|
return node
|
|
if isinstance(node, (int, float)):
|
|
return fmt(node)
|
|
# list
|
|
simple = all(not isinstance(x, list) for x in node)
|
|
if simple:
|
|
return '(' + ' '.join(dumps(x) for x in node) + ')'
|
|
pad = ' ' * (indent + 1)
|
|
parts = []
|
|
head = []
|
|
i = 0
|
|
while i < len(node) and not isinstance(node[i], list):
|
|
head.append(dumps(node[i]))
|
|
i += 1
|
|
out = '(' + ' '.join(head)
|
|
for x in node[i:]:
|
|
if isinstance(x, list):
|
|
out += '\n' + pad + dumps(x, indent + 1)
|
|
else:
|
|
out += ' ' + dumps(x)
|
|
out += '\n' + ' ' * indent + ')'
|
|
return out
|
|
|
|
|
|
def fmt(v):
|
|
if isinstance(v, str):
|
|
return v
|
|
s = f'{v:.4f}'.rstrip('0').rstrip('.')
|
|
if s in ('-0', ''):
|
|
s = '0'
|
|
return s
|
|
|
|
|
|
def find(node, key):
|
|
"""First child list whose head is key."""
|
|
for x in node:
|
|
if isinstance(x, list) and x and x[0] == key:
|
|
return x
|
|
return None
|
|
|
|
|
|
def findall(node, key):
|
|
return [x for x in node if isinstance(x, list) and x and x[0] == key]
|