Files
baby-mobile/flux-ai/ipc2581_to_kicad.py
2026-07-04 03:28:13 -07:00

404 lines
15 KiB
Python

#!/usr/bin/env python3
"""
Convert flux-ai/soic-16-n-to-8-pin-fpc-flex-adapter.ipc2581c to KiCad .kicad_pcb format.
Key design facts from IPC-2581C analysis:
- TSSOP_L1-L8: castellated through-holes at y=-15.874, x=10.555..19.445 (1.27mm pitch)
- TSSOP_R1-R8: castellated through-holes at y=-11.474, x=10.555..19.445 (1.27mm pitch)
- FPC_1-10: SMD finger pads at x=30.5, y=-15.924..-11.424 (0.5mm pitch), 3mm x 0.35mm
- Board outline: roughly 10..32 x -16.424..-10.924 (22mm x 5.5mm)
"""
import xml.etree.ElementTree as ET
import math
IPC_FILE = 'soic-16-n-to-8-pin-fpc-flex-adapter.ipc2581c'
OUT_FILE = '../soic-16-n-to-8-pin-fpc-flex-adapter.kicad_pcb'
NS = 'http://webstds.ipc.org/2581'
def Q(name): return f'{{{NS}}}{name}'
def untag(el): return el.tag.replace(f'{{{NS}}}', '')
def parse_ipc(path):
tree = ET.parse(path)
root = tree.getroot()
# --- Line width dictionary ---
line_widths = {}
for dl in root.iter(Q('DictionaryLineDesc')):
for entry in dl:
lid = entry.get('id')
ld = entry.find(Q('LineDesc'))
if ld is not None:
line_widths[lid] = float(ld.get('lineWidth', '0'))
# --- Standard primitives (CUSTOM_1 = castellation copper, CUSTOM_2 = tiny) ---
std_prims = {}
for ds in root.iter(Q('DictionaryStandard')):
for entry in ds:
sid = entry.get('id')
pts = []
for pb in entry.iter(Q('PolyBegin')):
pts.append((float(pb.get('x')), float(pb.get('y'))))
for ps in entry.iter(Q('PolyStepSegment')):
pts.append((float(ps.get('x')), float(ps.get('y'))))
std_prims[sid] = pts
# --- User primitives (UPOLY_N = copper fills, UCIRCLE_1 = castellation ring) ---
upoly_defs = {} # id -> [(x,y), ...]
ucircle_defs = {} # id -> diameter
for du in root.iter(Q('DictionaryUser')):
for entry in du:
uid = entry.get('id')
pts = []
for pb in entry.iter(Q('PolyBegin')):
pts.append((float(pb.get('x')), float(pb.get('y'))))
for ps in entry.iter(Q('PolyStepSegment')):
pts.append((float(ps.get('x')), float(ps.get('y'))))
if pts:
upoly_defs[uid] = pts
for c in entry.iter(Q('Circle')):
ucircle_defs[uid] = float(c.get('diameter'))
# --- Layer features ---
# pads: {pin_name: {x, y, prim_id, layer}}
pads = {}
# upoly placements per layer: [(uid, offset_x, offset_y)]
layer_upolys = {}
# lines per layer: [(x1,y1,x2,y2,width)]
layer_lines = {}
# through-holes: [(x, y, diameter)]
through_holes = []
for lf in root.iter(Q('LayerFeature')):
layer = lf.get('layerRef')
layer_upolys.setdefault(layer, [])
layer_lines.setdefault(layer, [])
sets = list(lf)
for s in sets:
if untag(s) != 'Set':
continue
# Pad elements (appear in Set[0] of each layer, only F.Cu has pin refs)
for pad_el in s.findall(Q('Pad')):
loc = pad_el.find(Q('Location'))
if loc is None:
continue
x = float(loc.get('x'))
y = float(loc.get('y'))
pin_ref = pad_el.find(Q('PinRef'))
pin = pin_ref.get('pin') if pin_ref is not None else ''
sp_ref = pad_el.find(Q('StandardPrimitiveRef'))
up_ref = pad_el.find(Q('UserPrimitiveRef'))
prim_id = (sp_ref.get('id') if sp_ref is not None
else (up_ref.get('id') if up_ref is not None else None))
if pin and pin not in pads:
pads[pin] = {'x': x, 'y': y, 'prim_id': prim_id, 'layer': layer}
# Features blocks (UPOLY refs and lines)
for feat in s.findall(Q('Features')):
loc = feat.find(Q('Location'))
ox = float(loc.get('x', '0')) if loc is not None else 0
oy = float(loc.get('y', '0')) if loc is not None else 0
up_ref = feat.find(Q('UserPrimitiveRef'))
if up_ref is not None:
layer_upolys[layer].append((up_ref.get('id'), ox, oy))
for line in feat.iter(Q('Line')):
if line.get('startX') is None:
continue
lw_ref = line.find(Q('LineDescRef'))
lw = line_widths.get(lw_ref.get('id'), 0.1) if lw_ref is not None else 0.1
layer_lines[layer].append((
float(line.get('startX')), float(line.get('startY')),
float(line.get('endX')), float(line.get('endY')), lw
))
for us in feat.findall(Q('UserSpecial')):
for line in us.findall(Q('Line')):
ld = line.find(Q('LineDesc'))
lw = float(ld.get('lineWidth', '0.05')) if ld is not None else 0.05
layer_lines[layer].append((
float(line.get('startX')), float(line.get('startY')),
float(line.get('endX')), float(line.get('endY')), lw
))
for poly in us.findall(Q('Polyline')):
ld = poly.find(Q('LineDesc'))
lw = float(ld.get('lineWidth', '0.05')) if ld is not None else 0.05
pts = []
pb = poly.find(Q('PolyBegin'))
if pb is not None:
pts.append((float(pb.get('x')), float(pb.get('y'))))
for ps in poly.findall(Q('PolyStepSegment')):
pts.append((float(ps.get('x')), float(ps.get('y'))))
for i in range(len(pts) - 1):
layer_lines[layer].append((*pts[i], *pts[i+1], lw))
# Through-holes (F.Cu_B.Cu layer)
if layer == 'F.Cu_B.Cu':
for s in sets:
if untag(s) != 'Set':
continue
for hole in s.iter(Q('Hole')):
through_holes.append((
float(hole.get('x')), float(hole.get('y')),
float(hole.get('diameter'))
))
return {
'pads': pads,
'upoly_defs': upoly_defs,
'ucircle_defs': ucircle_defs,
'std_prims': std_prims,
'layer_upolys': layer_upolys,
'layer_lines': layer_lines,
'through_holes': through_holes,
'line_widths': line_widths,
}
def bbox(pts):
xs = [p[0] for p in pts]
ys = [p[1] for p in pts]
return min(xs), max(xs), min(ys), max(ys)
def upoly_is_fpc(pts):
"""FPC pads span ~3mm in x (from ~29 to ~32)."""
xmin, xmax, _, _ = bbox(pts)
return (xmax - xmin) > 2.5
def upoly_center_size(pts):
xmin, xmax, ymin, ymax = bbox(pts)
cx = (xmin + xmax) / 2
cy = (ymin + ymax) / 2
w = xmax - xmin
h = ymax - ymin
return cx, cy, w, h
def generate_kicad(data, out_path):
pads = data['pads']
upoly_defs = data['upoly_defs']
layer_upolys = data['layer_upolys']
layer_lines = data['layer_lines']
through_holes = data['through_holes']
# KiCad standard layer mapping (for 2-layer board)
kicad_layers = [
(0, 'F.Cu', 'signal'),
(31, 'B.Cu', 'signal'),
(35, 'F.Paste', 'user'),
(34, 'B.Paste', 'user'),
(37, 'F.SilkS', 'user', 'F.Silkscreen'),
(36, 'B.SilkS', 'user', 'B.Silkscreen'),
(39, 'F.Mask', 'user'),
(38, 'B.Mask', 'user'),
(44, 'Edge.Cuts', 'user'),
(45, 'Margin', 'user'),
(47, 'F.CrtYd', 'user', 'F.Courtyard'),
(46, 'B.CrtYd', 'user', 'B.Courtyard'),
(49, 'F.Fab', 'user', 'F.Fabrication'),
(48, 'B.Fab', 'user', 'B.Fabrication'),
]
lines = []
lines.append('(kicad_pcb')
lines.append('\t(version 20241229)')
lines.append('\t(generator "ipc2581_to_kicad")')
lines.append('\t(generator_version "2.0")')
lines.append('\t(general')
lines.append('\t\t(thickness 1.6)')
lines.append('\t\t(legacy_teardrops no)')
lines.append('\t)')
lines.append('\t(paper "A4")')
lines.append('\t(title_block')
lines.append('\t\t(title "SOIC-16-N to 8-pin FPC Flex Adapter")')
lines.append('\t\t(date "2026-05-28")')
lines.append('\t\t(rev "1.0")')
lines.append('\t)')
# Layers
lines.append('\t(layers')
for layer_def in kicad_layers:
if len(layer_def) == 4:
lines.append(f'\t\t({layer_def[0]} "{layer_def[1]}" {layer_def[2]} "{layer_def[3]}")')
else:
lines.append(f'\t\t({layer_def[0]} "{layer_def[1]}" {layer_def[2]})')
lines.append('\t)')
lines.append('\t(net 0 "")')
lines.append('\t(net 1 "Net-(Pad1)")')
lines.append('')
# --- Board outline (Edge.Cuts) ---
# Board outline
edge_seen = set()
for (x1, y1, x2, y2, lw) in layer_lines.get('Edge.Cuts', []):
key = (round(x1, 4), round(y1, 4), round(x2, 4), round(y2, 4))
key2 = (round(x2, 4), round(y2, 4), round(x1, 4), round(y1, 4))
if key in edge_seen or key2 in edge_seen:
continue
edge_seen.add(key)
lines.append(f'\t(gr_line')
lines.append(f'\t\t(start {x1:.6f} {y1:.6f})')
lines.append(f'\t\t(end {x2:.6f} {y2:.6f})')
lines.append(f'\t\t(stroke (width 0.0500) (type solid))')
lines.append(f'\t\t(layer "Edge.Cuts")')
lines.append(f'\t)')
lines.append('')
# --- Silkscreen lines ---
# Silkscreen
silk_lines = layer_lines.get('F.SilkS', [])
# Deduplicate (some lines appear twice due to duplicate sets)
seen = set()
for (x1, y1, x2, y2, lw) in silk_lines:
key = (round(x1, 4), round(y1, 4), round(x2, 4), round(y2, 4))
key2 = (round(x2, 4), round(y2, 4), round(x1, 4), round(y1, 4))
if key in seen or key2 in seen:
continue
seen.add(key)
lw = max(lw, 0.01)
lines.append(f'\t(gr_line')
lines.append(f'\t\t(start {x1:.6f} {y1:.6f})')
lines.append(f'\t\t(end {x2:.6f} {y2:.6f})')
lines.append(f'\t\t(stroke (width {lw:.4f}) (type solid))')
lines.append(f'\t\t(layer "F.SilkS")')
lines.append(f'\t)')
lines.append('')
# --- Footprint ---
lines.append('\t(footprint "soic16-fpc-adapter"')
lines.append('\t\t(layer "F.Cu")')
lines.append('\t\t(at 0 0)')
lines.append('\t\t(fp_text reference "U?" (at 21 -13.67) (layer "F.SilkS")')
lines.append('\t\t\t(effects (font (size 1 1) (thickness 0.15)))')
lines.append('\t\t)')
lines.append('\t\t(fp_text value "SOIC16-FPC-Adapter" (at 21 -19) (layer "F.Fab")')
lines.append('\t\t\t(effects (font (size 1 1) (thickness 0.15)))')
lines.append('\t\t)')
# -- Through-hole castellation pads (TSSOP pads) --
# Collect unique hole positions and match to pad names
lines.append('')
# Copper pad diameter = 0.636396mm (from UCIRCLE_1), drill = 0.45mm
copper_d = 0.636396
drill_d = 0.45
# Build a map from (x,y) to pin name
pad_pos_to_pin = {}
for pin, p in pads.items():
key = (round(p['x'], 3), round(p['y'], 3))
pad_pos_to_pin[key] = pin
hole_set = set()
for (hx, hy, hd) in through_holes:
key = (round(hx, 3), round(hy, 3))
if key in hole_set:
continue
hole_set.add(key)
pin = pad_pos_to_pin.get(key, '?')
lines.append(f'\t\t(pad "{pin}" thru_hole circle')
lines.append(f'\t\t\t(at {hx:.6f} {hy:.6f})')
lines.append(f'\t\t\t(size {copper_d:.6f} {copper_d:.6f})')
lines.append(f'\t\t\t(drill {drill_d:.3f})')
lines.append(f'\t\t\t(layers "*.Cu" "*.Mask")')
lines.append(f'\t\t)')
# -- SMD FPC finger pads --
lines.append('')
# Collect FPC pads from F.Cu UPOLY placements
fpc_pads_added = set()
for (uid, ox, oy) in layer_upolys.get('F.Cu', []):
if uid not in upoly_defs:
continue
pts = upoly_defs[uid]
if not upoly_is_fpc(pts):
continue
cx, cy, w, h = upoly_center_size(pts)
cx += ox
cy += oy
# Find matching pin name
key = (round(cx, 3), round(cy, 3))
# FPC pad center y is between ymin and ymax of the upoly
# Match to pad by proximity
pin = '?'
for pname, p in pads.items():
if pname.startswith('FPC') and abs(p['y'] - cy) < 0.01 and abs(p['x'] - cx) < 0.1:
pin = pname
break
if pin in fpc_pads_added:
continue
fpc_pads_added.add(pin)
lines.append(f'\t\t(pad "{pin}" smd rect')
lines.append(f'\t\t\t(at {cx:.6f} {cy:.6f})')
lines.append(f'\t\t\t(size {w:.6f} {h:.6f})')
lines.append(f'\t\t\t(layers "F.Cu" "F.Paste" "F.Mask")')
lines.append(f'\t\t)')
lines.append('\t)') # end footprint
lines.append('')
# --- Copper fills as gr_poly on F.Cu and B.Cu ---
# Only output non-FPC (castellation) copper fills to avoid clutter
layer_map = {'F.Cu': 'F.Cu', 'B.Cu': 'B.Cu'}
for ipc_layer, kicad_layer in layer_map.items():
upoly_list = layer_upolys.get(ipc_layer, [])
written = set()
for (uid, ox, oy) in upoly_list:
if uid not in upoly_defs:
continue
pts = upoly_defs[uid]
if upoly_is_fpc(pts):
continue # FPC pads are already in footprint
if uid in written:
continue
written.add(uid)
cx, cy, w, h = upoly_center_size(pts)
# Output as filled polygon
lines.append(f'\t(gr_poly')
lines.append(f'\t\t(pts')
# Use simplified rect for octagonal pads to keep file clean
hw = w / 2
hh = h / 2
px = cx + ox
py = cy + oy
lines.append(f'\t\t\t(xy {px-hw:.4f} {py-hh:.4f})')
lines.append(f'\t\t\t(xy {px+hw:.4f} {py-hh:.4f})')
lines.append(f'\t\t\t(xy {px+hw:.4f} {py+hh:.4f})')
lines.append(f'\t\t\t(xy {px-hw:.4f} {py+hh:.4f})')
lines.append(f'\t\t)')
lines.append(f'\t\t(stroke (width 0) (type solid))')
lines.append(f'\t\t(fill solid)')
lines.append(f'\t\t(layer "{kicad_layer}")')
lines.append(f'\t)')
lines.append(')') # end kicad_pcb
with open(out_path, 'w') as f:
f.write('\n'.join(lines) + '\n')
print(f"Written: {out_path}")
print(f" Edge.Cuts lines: {len(edge_seen)}")
print(f" Silkscreen lines: {len(seen)}")
print(f" Through-holes: {len(hole_set)}")
print(f" FPC SMD pads: {len(fpc_pads_added)}")
if __name__ == '__main__':
import os
script_dir = os.path.dirname(os.path.abspath(__file__))
ipc_path = os.path.join(script_dir, IPC_FILE)
out_path = os.path.join(script_dir, OUT_FILE)
data = parse_ipc(ipc_path)
print(f"Parsed: {len(data['pads'])} pads, "
f"{sum(len(v) for v in data['layer_upolys'].values())} upoly placements, "
f"{len(data['through_holes'])} holes")
generate_kicad(data, out_path)