#!/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)