322 lines
11 KiB
Python
Executable file
322 lines
11 KiB
Python
Executable file
#!/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'))
|
|
|