pogojig/renderer/support/generate_kicad.py

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'))