#!/usr/bin/env python3 import os import sys import time from os import path from textwrap import dedent import pkgutil import subprocess import xml.etree.ElementTree as xe import ezdxf __version__ = '0.1' PIN_TS_BASE = 0x23420000 TEDIT_BASE = 0x23430000 PATH_BASE = 0x23440000 def sch_template(name, num_pins, yspace=200): templ = f''' EESchema Schematic File Version 5 EELAYER 30 0 EELAYER END $Descr A3 16535 11693 encoding utf-8 Sheet 1 1 Title "{name}" Date "{time.strftime("%d %b %Y")}" Rev "" Comp "" Comment1 "" Comment2 "" Comment3 "" Comment4 "" Comment5 "" Comment6 "" Comment7 "" Comment8 "" Comment9 "" $EndDescr {{components}} $EndSCHEMATC ''' components = [] for i in range(num_pins): identifier = f'TP{i}' value = 'pogopin' x, y = 1000, 1000 + i*yspace components.append(dedent(f''' $Comp L Connector:Conn_01x01_Female {identifier} U 1 1 {PIN_TS_BASE + i:08X} P {x} {y} F 0 "{identifier}" H {x-50} {y+50} 50 0000 R CNN F 1 "{value}" H {x+50} {y} 50 0000 L CNN F 2 "Pogopin:AutogeneratedPogopinFootprint" H {x} {y} 50 0001 C CNN F 3 "~" H {x} {y} 50 0001 C CNN 1 {x} {y} -1 0 0 1 $EndComp ''').strip()) return dedent(templ).lstrip().format(components='\n'.join(components)) def pcb_template(outline, pins, annular=0.5): pcb_templ = f''' (kicad_pcb (version 20190605) (host pogojig "({__version__})") (general (thickness 1.6) (drawings {len(pins)}) (tracks 0) (modules {len(pins)}) (nets {len(pins)+1}) ) (page "A4") (layers (0 "F.Cu" signal) (31 "B.Cu" signal) (32 "B.Adhes" user) (33 "F.Adhes" user) (34 "B.Paste" user) (35 "F.Paste" user) (36 "B.SilkS" user) (37 "F.SilkS" user) (38 "B.Mask" user) (39 "F.Mask" user) (40 "Dwgs.User" user) (41 "Cmts.User" user) (42 "Eco1.User" user) (43 "Eco2.User" user) (44 "Edge.Cuts" user) (45 "Margin" user) (46 "B.CrtYd" user) (47 "F.CrtYd" user) (48 "B.Fab" user) (49 "F.Fab" user) ) (setup (last_trace_width 0.25) (trace_clearance 0.2) (zone_clearance 0.508) (zone_45_only no) (trace_min 0.2) (via_size 0.8) (via_drill 0.4) (via_min_size 0.4) (via_min_drill 0.3) (uvia_size 0.3) (uvia_drill 0.1) (uvias_allowed no) (uvia_min_size 0.2) (uvia_min_drill 0.1) (max_error 0.005) (defaults (edge_clearance 0.01) (edge_cuts_line_width 0.05) (courtyard_line_width 0.05) (copper_line_width 0.2) (copper_text_dims (size 1.5 1.5) (thickness 0.3) keep_upright) (silk_line_width 0.12) (silk_text_dims (size 1 1) (thickness 0.15) keep_upright) (other_layers_line_width 0.1) (other_layers_text_dims (size 1 1) (thickness 0.15) keep_upright) ) (pad_size 3.14159 3.14159) (pad_drill 1.41421) (pad_to_mask_clearance 0.051) (solder_mask_min_width 0.25) (aux_axis_origin 0 0) (visible_elements FFFFFF7F) (pcbplotparams (layerselection 0x010fc_ffffffff) (usegerberextensions false) (usegerberattributes false) (usegerberadvancedattributes false) (creategerberjobfile false) (excludeedgelayer true) (linewidth 0.100000) (plotframeref false) (viasonmask false) (mode 1) (useauxorigin false) (hpglpennumber 1) (hpglpenspeed 20) (hpglpendiameter 15.000000) (psnegative false) (psa4output false) (plotreference true) (plotvalue true) (plotinvisibletext false) (padsonsilk false) (subtractmaskfromsilk false) (outputformat 1) (mirror false) (drillshape 1) (scaleselection 1) (outputdirectory "")) ) (net 0 "") {{net_defs}} (net_class "Default" "This is the default net class." (clearance 0.2) (trace_width 0.25) (via_dia 0.8) (via_drill 0.4) (uvia_dia 0.3) (uvia_drill 0.1) {{net_class_defs}} ) {{module_defs}} {{edge_cuts}} )''' module_defs = [] for i, pin in enumerate(pins): (x, y), hole_dia = pin # all dimensions in mm here pad_dia = hole_dia + 2*annular mod = f''' (module "Pogopin:AutogeneratedPogopinFootprint" (layer "F.Cu") (tedit {TEDIT_BASE + i:08X}) (tstamp {PIN_TS_BASE + i:08X}) (at {x} {y}) (descr "Pogo pin {i}") (tags "test point plated hole") (path "/{PATH_BASE + i:08X}") (attr virtual) (fp_text reference "TP{i}" (at 0 -{pad_dia/2 + 1}) (layer "F.SilkS") (effects (font (size 1 1) (thickness 0.15))) ) (fp_text value "pogo pin {i}" (at 0 {pad_dia/2 + 1}) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))) ) (fp_text user "%R" (at 0 -{pad_dia/2 + 1}) (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))) ) (fp_circle (center 0 0) (end {pad_dia} 0) (layer "F.CrtYd") (width 0.05)) (fp_circle (center 0 0) (end 0 -{pad_dia}) (layer "F.SilkS") (width 0.12)) (pad "1" thru_hole circle (at 0 0) (size {pad_dia} {pad_dia}) (drill {hole_dia}) (layers *.Cu *.Mask) (net {i+1} "pogo{i}")) )''' module_defs.append(mod) edge_cuts = [ f'(gr_line (start {x1} {y1}) (end {x2} {y2}) (layer "Edge.Cuts") (width 0.05))' for (x1, y1), (x2, y2) in outline ] net_defs = [ f'(net {i+1} "pogo{i}")' for i, _pin in enumerate(pins) ] net_class_defs = [ f'(add_net "pogo{i}")' for i, _pin in enumerate(pins) ] return pcb_templ.format( net_defs='\n'.join(net_defs), net_class_defs='\n'.join(net_class_defs), module_defs='\n'.join(module_defs), edge_cuts='\n'.join(edge_cuts)) def inkscape_query_all(filename): proc = subprocess.run([ os.environ.get('INKSCAPE', 'inkscape'), filename, '--query-all'], capture_output=True) proc.check_returncode() data = [ line.split(',') for line in proc.stdout.decode().splitlines() ] return { id: (float(x), float(y), float(w), float(h)) for id, x, y, w, h in data } SVG_NS = { 'svg': 'http://www.w3.org/2000/svg', 'inkscape': 'http://www.inkscape.org/namespaces/inkscape', 'sodipodi': 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd' } def svg_find_elements(doc, tag, layer=None): for i, g in enumerate(doc.findall('svg:g', SVG_NS)): if g.attrib.get(f'{{{SVG_NS["inkscape"]}}}groupmode') != 'layer': continue label = g.attrib.get(f'{{{SVG_NS["inkscape"]}}}label', '') if not layer or label == layer: yield from g.iter(tag) # def svg_get_scale(doc): # w = doc.attrib['width'] # h = doc.attrib['height'] # # if not w.endswith('mm') and h.endswith('mm'): # raise ValueError('Document dimensions in SVG must be set to millimeters') # # w, h = float(w[:-2]), float(h[:-2]) # _x, _y, vb_w, vb_h = map(float, doc.attrib['viewBox'].split()) # scale_x, scale_y = vb_w / w, vb_h / h # assert abs(1 - scale_x/scale_y) < 0.001 # return scale_x def svg_get_viewbox_mm(doc): w = doc.attrib['width'] h = doc.attrib['height'] if not w.endswith('mm') and h.endswith('mm'): raise ValueError('Document dimensions in SVG must be set to millimeters') w, h = float(w[:-2]), float(h[:-2]) x, y, vb_w, vb_h = map(float, doc.attrib['viewBox'].split()) scale_x, scale_y = vb_w / w, vb_h / h return x/scale_x, y/scale_y, w, h if __name__ == '__main__': import argparse parser = argparse.ArgumentParser() parser.add_argument('svg', metavar='pogo_map.svg', help='Input inkscape SVG pogo pin map (use provided template!)') parser.add_argument('outline', metavar='outline.dxf', help='Board outline DXF generated by OpenSCAD') parser.add_argument('output', default='kicad', help='Output directory/project name and path') parser.add_argument('-y', '--yspace', type=int, default=200, help='Schematic pin Y spacing in mil (default: 200)') parser.add_argument('-a', '--annular', type=float, default=0.5, help='Pogo pin annular ring width in mm (default: 0.5)') parser.add_argument('-l', '--svg-layer', type=str, default='Test Points', help='Name of SVG layer containing pogo pins') parser.add_argument('-n', '--name', default='jig', help='Output KiCAD project name') args = parser.parse_args() if not path.exists(args.output): os.mkdir(args.output) if not path.isdir(args.output): raise SystemError(f'Output path "{args.output}" is not a directory') with open(args.svg, 'r') as f: doc = xe.fromstring(f.read()) pogo_circle_ids = [ circle.attrib['id'] for circle in svg_find_elements(doc, f'{{{SVG_NS["svg"]}}}circle', args.svg_layer) ] # scale = svg_get_scale(doc) page_x, page_y, page_w, page_h = svg_get_viewbox_mm(doc) MM_PER_IN = 25.4 SVG_DEF_DPI = 96 px_to_mm = lambda px: px/SVG_DEF_DPI * MM_PER_IN query = inkscape_query_all(args.svg) dims = [ query[id] for id in pogo_circle_ids ] assert all( abs(1 - w/h) < 0.001 for _x, _y, w, h in dims ) print('origin:', page_x, page_y) print('dims:', page_w, page_h) pins = [ ( (page_x + px_to_mm(x) + px_to_mm(w)/2, page_y - page_h + px_to_mm(y) + px_to_mm(w)/2), px_to_mm(w)) for x, y, w, h in dims ] doc = ezdxf.readfile(args.outline) outline = [] for line in doc.modelspace().query('LINE'): (x1, y1, _z1), (x2, y2, _z2) = line.dxf.start, line.dxf.end outline.append(((x1, -y1), (x2, -y2))) with open(path.join(args.output, f'{args.name}.sch'), 'w', encoding='utf8') as sch: sch.write(sch_template(f'{args.name} generated schematic (PogoJig v{__version__})', len(pins), yspace=args.yspace)) with open(path.join(args.output, f'{args.name}.kicad_pcb'), 'w', encoding='utf8') as pcb: pcb.write(pcb_template(outline, pins, annular=args.annular)) with open(path.join(args.output, f'{args.name}.pro'), 'w', encoding='utf8') as f: f.write(pkgutil.get_data('pogojig.kicad', 'kicad.pro').decode('utf8')) with open(path.join(args.output, f'{args.name}-cache.lib'), 'w', encoding='utf8') as f: f.write(pkgutil.get_data('pogojig.kicad', 'kicad-cache.lib').decode('utf8'))