404 lines
15 KiB
Python
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)
|