gerbonara/coil_test_board.py
2023-10-18 17:19:05 +02:00

520 lines
24 KiB
Python

#!/usr/bin/env python3
import math
import hashlib
import re
import itertools
import datetime
import tempfile
import subprocess
import sqlite3
import json
from pathlib import Path
import tqdm
import gerbonara.cad.kicad.pcb as pcb
import gerbonara.cad.kicad.footprints as fp
import gerbonara.cad.primitives as cad_pr
import gerbonara.cad.kicad.graphical_primitives as kc_gr
cols = 5
rows = 5
coil_specs = [
{'n': 1, 's': True, 't': 1, 'c': 0.20, 'w': 5.00, 'd': 3.00, 'v': 5.00},
{'n': 2, 's': True, 't': 1, 'c': 0.20, 'w': 3.00, 'd': 1.50, 'v': 3.00},
{'n': 3, 's': True, 't': 1, 'c': 0.20, 'w': 1.50, 'd': 1.20, 'v': 2.00},
{'n': 5, 's': True, 't': 1, 'c': 0.20, 'w': 0.80, 'd': 0.40, 'v': 0.80},
{'n': 10, 's': True, 't': 1, 'c': 0.20, 'w': 0.50, 'd': 0.30, 'v': 0.60},
{'n': 25, 's': True, 't': 1, 'c': 0.15, 'w': 0.25, 'd': 0.30, 'v': 0.60},
{'n': 1, 's': False, 't': 3, 'c': 0.20, 'w': 5.00, 'd': 3.00, 'v': 5.00},
{'n': 2, 's': False, 't': 1, 'c': 0.20, 'w': 3.00, 'd': 1.50, 'v': 3.00},
{'n': 3, 's': False, 't': 1, 'c': 0.20, 'w': 2.50, 'd': 1.20, 'v': 2.00},
{'n': 5, 's': False, 't': 1, 'c': 0.20, 'w': 2.50, 'd': 1.20, 'v': 0.80},
{'n': 10, 's': False, 't': 1, 'c': 0.20, 'w': 1.50, 'd': 0.80, 'v': 0.60},
{'n': 25, 's': False, 't': 1, 'c': 0.15, 'w': 0.50, 'd': 0.30, 'v': 0.60},
{'n': 1, 's': False, 't': 4, 'c': 0.20, 'w': 5.00, 'd': 3.00, 'v': 5.00},
{'n': 2, 's': False, 't': 3, 'c': 0.20, 'w': 3.00, 'd': 1.50, 'v': 3.00},
{'n': 3, 's': False, 't': 4, 'c': 0.20, 'w': 2.50, 'd': 1.20, 'v': 2.00},
{'n': 5, 's': False, 't': 3, 'c': 0.20, 'w': 2.50, 'd': 1.20, 'v': 0.80},
{'n': 10, 's': False, 't': 3, 'c': 0.20, 'w': 1.50, 'd': 0.80, 'v': 0.60},
{'n': 25, 's': False, 't': 3, 'c': 0.15, 'w': 0.50, 'd': 0.30, 'v': 0.60},
{'n': 1, 's': False, 't': 5, 'c': 0.20, 'w': 5.00, 'd': 3.00, 'v': 5.00},
{'n': 2, 's': False, 't': 5, 'c': 0.20, 'w': 3.00, 'd': 1.50, 'v': 3.00},
{'n': 3, 's': False, 't': 4, 'c': 0.20, 'w': 2.50, 'd': 1.20, 'v': 2.00},
{'n': 5, 's': False, 't': 7, 'c': 0.20, 'w': 2.50, 'd': 1.20, 'v': 0.80},
{'n': 10, 's': False, 't': 7, 'c': 0.20, 'w': 1.50, 'd': 0.80, 'v': 0.60},
{'n': 25, 's': False, 't': 13, 'c': 0.15, 'w': 0.50, 'd': 0.30, 'v': 0.60},
{'n': 25, 's': False, 't': 23, 'c': 0.15, 'w': 0.50, 'd': 0.30, 'v': 0.60},
]
cachedir = Path('/tmp/coil_test_cache')
version_string = 'v1.0'
coil_border = 7 # mm
cut_gap = 3 # mm
tooling_border = 10 # mm
vscore_extra = 10 # mm
mouse_bite_width = 8 # mm
mouse_bite_yoff = -0.00
mouse_bite_hole_dia = 0.7
mouse_bite_hole_spacing = 0.6
hole_offset = 5
hole_dia = 3.2
coil_dia = 35 # mm
coil_inner_dia = 15 # mm
board_thickness = 0.80 # mm
pad_offset = 2 # mm
pad_dia = 2.0 # mm
pad_length = 3.5 # mm
pad_drill = 1.1 # mm
pad_pitch = 2.54 # mm
vrail_width = 10 # mm
join_trace_w = 0.35 # mm
v_cuts = False # FIXME DEBUG
mouse_bites = True # FIXME DEBUG
db = sqlite3.connect('coil_parameters.sqlite3')
db.execute('CREATE TABLE IF NOT EXISTS runs (run_id INTEGER PRIMARY KEY, timestamp TEXT, version TEXT)')
db.execute('CREATE TABLE IF NOT EXISTS coils (coil_id INTEGER PRIMARY KEY, run_id INTEGER, FOREIGN KEY (run_id) REFERENCES runs(run_id))')
db.execute('CREATE TABLE IF NOT EXISTS results (result_id INTEGER PRIMARY KEY, coil_id INTEGER, key TEXT, value TEXT, FOREIGN KEY (coil_id) REFERENCES coils(coil_id))')
cur = db.cursor()
cur.execute('INSERT INTO runs(timestamp, version) VALUES (datetime("now"), ?)', (version_string,))
run_id = cur.lastrowid
db.commit()
coil_pitch_v = coil_dia + coil_border*2 + cut_gap
coil_pitch_h = coil_dia + coil_border*2 + 2*cut_gap + vrail_width
total_width = coil_pitch_h*cols + 2*tooling_border - vrail_width
total_height = coil_pitch_v*rows + 2*tooling_border + cut_gap
tile_width = tile_height = coil_dia + 2*coil_border
drawing_text_size = 2.0
print(f'Calculated board size: {total_width:.2f} * {total_height:.2f} mm')
print(f'Tile size: {tile_height:.2f} * {tile_height:.2f} mm')
x0, y0 = 100, 100
xy = pcb.XYCoord
b = pcb.Board.empty_board(page=pcb.PageSettings(page_format='A2'))
b.add(kc_gr.Rectangle(xy(x0, y0), xy(x0+total_width, y0+total_height), layer='Edge.Cuts', stroke=pcb.Stroke(width=0.15)))
def do_line(x0, y0, x1, y1, off_x=0, off_y=0):
b.add(kc_gr.Line(xy(x0+off_x, y0+off_y),
xy(x1+off_x, y1+off_y),
layer='Edge.Cuts', stroke=pcb.Stroke(width=0.15)))
if v_cuts:
for y in range(rows):
for off_y in [0, tile_height]:
y_pos = y0 + tooling_border + cut_gap + off_y + y*coil_pitch_v
do_line(x0 - vscore_extra, y_pos, x0 + total_width + vscore_extra, y_pos)
b.add(kc_gr.Text(text='V-score',
at=pcb.AtPos(x0 + total_width + vscore_extra + drawing_text_size/2, y_pos, 0),
layer=kc_gr.TextLayer('Edge.Cuts'),
effects=pcb.TextEffect(
font=pcb.FontSpec(size=xy(drawing_text_size, drawing_text_size),
thickness=drawing_text_size/10),
justify=pcb.Justify(h=pcb.Atom.left))))
for x in range(cols):
for off_x in [0, tile_width]:
x_pos = x0 + tooling_border + cut_gap + off_x + x*coil_pitch_h
do_line(x_pos, y0 - vscore_extra, x_pos, y0 + total_height + vscore_extra)
b.add(kc_gr.Text(text='V-score',
at=pcb.AtPos(x_pos, y0 + total_height + vscore_extra + drawing_text_size/2, 90),
layer=kc_gr.TextLayer('Edge.Cuts'),
effects=pcb.TextEffect(
font=pcb.FontSpec(size=xy(drawing_text_size, drawing_text_size),
thickness=drawing_text_size/10),
justify=pcb.Justify(h=pcb.Atom.right))))
def draw_corner(x0, y0, spokes):
right, top, left, bottom = [True if c.lower() in 'y1' else False for c in spokes]
l = (tile_width - mouse_bite_width)/2 - cut_gap/2
if right:
do_line(cut_gap/2, -cut_gap/2, cut_gap/2 + l, -cut_gap/2, x0, y0)
do_line(cut_gap/2, cut_gap/2, cut_gap/2 + l, cut_gap/2, x0, y0)
b.add(kc_gr.Arc(start=xy(x0+cut_gap/2+l, y0-cut_gap/2),
end=xy(x0+cut_gap/2+l, y0+cut_gap/2),
center=xy(x0+cut_gap/2+l, y0),
layer='Edge.Cuts',
stroke=pcb.Stroke(width=0.15)))
else:
do_line(cut_gap/2, -cut_gap/2, cut_gap/2, cut_gap/2, x0, y0)
if left:
do_line(-cut_gap/2, -cut_gap/2, -cut_gap/2 - l, -cut_gap/2, x0, y0)
do_line(-cut_gap/2, cut_gap/2, -cut_gap/2 - l, cut_gap/2, x0, y0)
b.add(kc_gr.Arc(end=xy(x0-cut_gap/2-l, y0-cut_gap/2),
start=xy(x0-cut_gap/2-l, y0+cut_gap/2),
center=xy(x0-cut_gap/2-l, y0),
layer='Edge.Cuts',
stroke=pcb.Stroke(width=0.15)))
else:
do_line(-cut_gap/2, -cut_gap/2, -cut_gap/2, cut_gap/2, x0, y0)
if bottom:
do_line(-cut_gap/2, cut_gap/2, -cut_gap/2, cut_gap/2 + l, x0, y0)
do_line(cut_gap/2, cut_gap/2, cut_gap/2, cut_gap/2 + l, x0, y0)
b.add(kc_gr.Arc(end=xy(x0-cut_gap/2, y0+cut_gap/2+l),
start=xy(x0+cut_gap/2, y0+cut_gap/2+l),
center=xy(x0, y0+cut_gap/2+l),
layer='Edge.Cuts',
stroke=pcb.Stroke(width=0.15)))
else:
do_line(-cut_gap/2, cut_gap/2, cut_gap/2, cut_gap/2, x0, y0)
if top:
do_line(-cut_gap/2, -cut_gap/2, -cut_gap/2, -cut_gap/2 - l, x0, y0)
do_line(cut_gap/2, -cut_gap/2, cut_gap/2, -cut_gap/2 - l, x0, y0)
b.add(kc_gr.Arc(start=xy(x0-cut_gap/2, y0-cut_gap/2-l),
end=xy(x0+cut_gap/2, y0-cut_gap/2-l),
center=xy(x0, y0-cut_gap/2-l),
layer='Edge.Cuts',
stroke=pcb.Stroke(width=0.15)))
else:
do_line(-cut_gap/2, -cut_gap/2, cut_gap/2, -cut_gap/2, x0, y0)
def make_mouse_bite(x, y, rot=0, width=mouse_bite_width, hole_dia=mouse_bite_hole_dia, hole_spacing=mouse_bite_hole_spacing, **kwargs):
pitch = hole_dia + hole_spacing
num_holes = int(math.floor((width - hole_dia) / pitch)) + 1
actual_spacing = (width - num_holes*hole_dia) / (num_holes + 1)
pitch = hole_dia + actual_spacing
f = fp.Footprint(name='mouse_bite', _version=None, generator=None, at=fp.AtPos(x, y, rot), **kwargs)
for i in range(num_holes):
f.pads.append(fp.Pad(
number='1',
type=fp.Atom.np_thru_hole,
shape=fp.Atom.circle,
at=fp.AtPos(-width/2 + actual_spacing + hole_dia/2 + i*pitch, 0, 0),
size=xy(hole_dia, hole_dia),
drill=fp.Drill(diameter=hole_dia),
footprint=f))
return f
def make_hole(x, y, dia, **kwargs):
f = fp.Footprint(name='hole', _version=None, generator=None, at=fp.AtPos(x, y, 0), **kwargs)
f.pads.append(fp.Pad(
number='1',
type=fp.Atom.np_thru_hole,
shape=fp.Atom.circle,
at=fp.AtPos(0, 0, 0),
size=xy(dia, dia),
drill=fp.Drill(diameter=dia),
footprint=f))
return f
def make_pads(x, y, rot, n, pad_dia, pad_length, drill, pitch, **kwargs):
f = fp.Footprint(name=f'conn_gen_01x{n}', _version=None, generator=None, at=fp.AtPos(x, y, rot), **kwargs)
for i in range(n):
f.pads.append(fp.Pad(
number=str(i+1),
type=fp.Atom.thru_hole,
shape=fp.Atom.oval,
at=fp.AtPos(-pitch*(n-1)/2 + i*pitch, 0, rot),
size=xy(pad_dia, pad_length),
drill=fp.Drill(diameter=drill),
footprint=f))
return f
corner_x0 = x0 + tooling_border + cut_gap/2
corner_y0 = y0 + tooling_border + cut_gap/2
corner_x1 = x0 + total_width - tooling_border - cut_gap/2
corner_y1 = y0 + total_height - tooling_border - cut_gap/2
# Corners
draw_corner(corner_x0, corner_y0, 'YNNY')
draw_corner(corner_x0, corner_y1, 'YYNN')
draw_corner(corner_x1, corner_y0, 'NNYY')
draw_corner(corner_x1, corner_y1, 'NYYN')
# Top / bottom V rail L junctions
for x in range(1, cols):
draw_corner(corner_x0 + x*coil_pitch_h - cut_gap - vrail_width, corner_y0, 'NNYY')
draw_corner(corner_x0 + x*coil_pitch_h, corner_y0, 'YNNY')
draw_corner(corner_x0 + x*coil_pitch_h - cut_gap - vrail_width, corner_y1, 'NYYN')
draw_corner(corner_x0 + x*coil_pitch_h, corner_y1, 'YYNN')
# Left / right T junctions
for y in range(1, rows):
draw_corner(corner_x0, corner_y0 + y*coil_pitch_v, 'YYNY')
draw_corner(corner_x1, corner_y0 + y*coil_pitch_v, 'NYYY')
# Middle T junctions
for y in range(1, rows):
for x in range(1, cols):
draw_corner(corner_x0 + x*coil_pitch_h - cut_gap - vrail_width, corner_y0 + y*coil_pitch_v, 'NYYY')
draw_corner(corner_x0 + x*coil_pitch_h, corner_y0 + y*coil_pitch_v, 'YYNY')
# Mouse bites
if mouse_bites:
for x in range(0, cols):
for y in range(0, rows):
tile_x0 = x0 + tooling_border + cut_gap + x*coil_pitch_h
tile_y0 = y0 + tooling_border + cut_gap + y*coil_pitch_v
b.add(make_mouse_bite(tile_x0 + tile_width/2, tile_y0 - mouse_bite_hole_dia/2, 0))
b.add(make_mouse_bite(tile_x0 + tile_width/2, tile_y0 + tile_height + mouse_bite_hole_dia/2, 0))
b.add(make_mouse_bite(tile_x0 - mouse_bite_hole_dia/2, tile_y0 + tile_height/2, 90))
b.add(make_mouse_bite(tile_x0 + tile_width + mouse_bite_hole_dia/2, tile_y0 + tile_height/2 + mouse_bite_yoff, 90))
# Mounting holes
for x in range(0, cols):
for y in range(0, rows):
tile_x0 = x0 + tooling_border + cut_gap + x*coil_pitch_h + tile_width/2
tile_y0 = y0 + tooling_border + cut_gap + y*coil_pitch_v + tile_height/2
dx = tile_width/2 - hole_offset
dy = tile_height/2 - hole_offset
b.add(make_hole(tile_x0 - dx, tile_y0 - dy, hole_dia))
b.add(make_hole(tile_x0 - dx, tile_y0 + dy, hole_dia))
b.add(make_hole(tile_x0 + dx, tile_y0 - dy, hole_dia))
b.add(make_hole(tile_x0 + dx, tile_y0 + dy, hole_dia))
# border graphics
c = 3
for layer in ['F.SilkS', 'B.SilkS']:
b.add(kc_gr.Rectangle(start=xy(x0, y0), end=xy(x0+c, y0+total_height), layer=layer, stroke=pcb.Stroke(width=0),
fill=kc_gr.FillMode(pcb.Atom.solid)))
b.add(kc_gr.Rectangle(start=xy(x0, y0), end=xy(x0+total_width, y0+c), layer=layer, stroke=pcb.Stroke(width=0),
fill=kc_gr.FillMode(pcb.Atom.solid)))
b.add(kc_gr.Rectangle(start=xy(x0+total_width-c, y0), end=xy(x0+total_width, y0+total_height), layer=layer, stroke=pcb.Stroke(width=0),
fill=kc_gr.FillMode(pcb.Atom.solid)))
b.add(kc_gr.Rectangle(start=xy(x0, y0+total_height-c), end=xy(x0+total_width, y0+total_height), layer=layer, stroke=pcb.Stroke(width=0),
fill=kc_gr.FillMode(pcb.Atom.solid)))
a = 3
timestamp = datetime.datetime.now().strftime('%Y-%m-%d')
b.add(kc_gr.Text(text=f'Planar inductor test panel {version_string} {timestamp} © 2023 Jan Götte, FG KOM, TU Darmstadt',
at=pcb.AtPos(x0 + c + a/3, y0 + c + a/3),
layer=kc_gr.TextLayer('F.SilkS'),
effects=pcb.TextEffect(
font=pcb.FontSpec(size=xy(a, a),
thickness=a/5),
justify=pcb.Justify(h=pcb.Atom.left, v=pcb.Atom.top))))
for index, ((y, x), spec) in tqdm.tqdm(enumerate(zip(itertools.product(range(rows), range(cols)), coil_specs), start=1)):
pass
with tempfile.NamedTemporaryFile(suffix='.kicad_mod') as f:
tile_x0 = x0 + tooling_border + cut_gap + x*coil_pitch_h + tile_width/2
tile_y0 = y0 + tooling_border + cut_gap + y*coil_pitch_v + tile_height/2
for key, alias in {
'gen.inner_diameter': 'id',
'gen.outer_diameter': 'od',
'gen.trace_width': 'w',
'gen.turns': 'n',
'gen.twists': 't',
'gen.clearance': 'c',
'gen.single_layer': 's',
'gen.via_drill': 'd',
'gen.via_diameter': 'v'}.items():
if alias in spec:
spec[key] = spec.pop(alias)
if 'gen.via_diameter' not in spec:
spec['gen.via_diameter'] = spec['gen.trace_width']
if 'gen.inner_diameter' not in spec:
spec['gen.inner_diameter'] = coil_inner_dia
if 'gen.outer_diameter' not in spec:
spec['gen.outer_diameter'] = coil_dia
args = ['python', '-m', 'twisted_coil_gen_twolayer', '--no-keepout-zone']
for k, v in spec.items():
prefix, _, k = k.partition('.')
if (not isinstance(v, bool) or v) and prefix == 'gen':
args.append('--' + k.replace('_', '-'))
if v is not True:
args.append(str(v))
arg_digest = hashlib.sha3_256(' / '.join(map(str, args)).encode()).hexdigest()
cachedir.mkdir(exist_ok=True)
cache_file = cachedir / f'C-{arg_digest}.kicad_mod'
log_file = cachedir / f'Q-{arg_digest}.kicad_mod'
if not cache_file.is_file():
args.append(cache_file)
try:
res = subprocess.run(args, check=True, capture_output=True, text=True)
log_file.write_text(res.stdout + res.stderr)
except subprocess.CalledProcessError as e:
print(f'Error generating coil with command line {args}, rc={e.returncode}')
print(e.stdout)
print(e.stderr)
coil = fp.Footprint.open_mod(cache_file)
coil.at = fp.AtPos(tile_x0, tile_y0, 0)
b.add(coil)
t = [f'n={spec["gen.turns"]}',
f'{spec["gen.twists"]} twists',
f'w={spec["gen.trace_width"]:.2f}mm']
if spec.get('gen.single_layer'):
t.append('single layer')
spec['gen.board_thickness'] = board_thickness
cur.execute('INSERT INTO coils(run_id) VALUES (?)', (run_id,))
coil_id = cur.lastrowid
for key, value in spec.items():
if isinstance(value, bool):
value = str(value)
db.execute('INSERT INTO results(coil_id, key, value) VALUES (?, ?, ?)', (coil_id, key, value))
for l in log_file.read_text().splitlines():
if (m := re.fullmatch(r'Approximate inductance:\s*([-+.0-9eE]+)\s*µH', l.strip())):
val = float(m.group(1)) * 1e-6
db.execute('INSERT INTO results(coil_id, key, value) VALUES (?, "calculated_approximate_inductance", ?)', (coil_id, val))
if (m := re.fullmatch(r'Approximate track length:\s*([-+.0-9eE]+)\s*mm', l.strip())):
val = float(m.group(1)) * 1e-3
db.execute('INSERT INTO results(coil_id, key, value) VALUES (?, "calculated_trace_length", ?)', (coil_id, val))
if (m := re.fullmatch(r'Approximate resistance:\s*([-+.0-9eE]+)\s*Ω', l.strip())):
val = float(m.group(1))
db.execute('INSERT INTO results(coil_id, key, value) VALUES (?, "calculated_approximate_resistance", ?)', (coil_id, val))
if (m := re.fullmatch(r'Fill factor:\s*([-+.0-9eE]+)', l.strip())):
val = float(m.group(1))
db.execute('INSERT INTO results(coil_id, key, value) VALUES (?, "calculated_fill_factor", ?)', (coil_id, val))
db.commit()
sz = 1.5
b.add(kc_gr.Text(text='\\n'.join(t),
at=pcb.AtPos(tile_x0, tile_y0),
layer=kc_gr.TextLayer('B.SilkS'),
effects=pcb.TextEffect(
font=pcb.FontSpec(size=xy(sz, sz),
thickness=sz/5),
justify=pcb.Justify(h=None, v=None, mirror=True))))
b.add(kc_gr.Text(text=f'{version_string} {timestamp}\\nTile {index}',
at=pcb.AtPos(tile_x0, tile_y0 - tile_height/2 + sz),
layer=kc_gr.TextLayer('B.SilkS'),
effects=pcb.TextEffect(
font=pcb.FontSpec(size=xy(sz, sz),
thickness=sz/5),
justify=pcb.Justify(h=None, v=pcb.Atom.top, mirror=True))))
b.add(kc_gr.Text(text=f'{index}',
at=pcb.AtPos(tile_x0, tile_y0 - tile_height/2 + sz),
layer=kc_gr.TextLayer('F.SilkS'),
effects=pcb.TextEffect(
font=pcb.FontSpec(size=xy(sz, sz),
thickness=sz/5),
justify=pcb.Justify(h=None, v=pcb.Atom.top, mirror=False))))
pads_x0 = tile_x0 + tile_width/2 - pad_offset
pads = make_pads(pads_x0, tile_y0, 270, 2, pad_dia, pad_length, pad_drill, pad_pitch)
b.add(pads)
w = min(spec.get('gen.trace_width', pad_dia), pad_dia)
wx, wy, _r, _f = pads.pad(2).abs_pos
w2 = (wx - pad_length/2, wy)
wx, wy, _r, _f = pads.pad(1).abs_pos
w1 = (wx - pad_length/2, wy)
b.add(cad_pr.Trace(w, coil.pad(1), pads.pad(1), waypoints=[w1], orientation=['ccw'], side='top'))
b.add(cad_pr.Trace(w, coil.pad(2), pads.pad(2), waypoints=[w2], orientation=['cw'], side='bottom'))
p = cut_gap + 5
q = 3
if y == 0:
if x > 0 and x % 2 == 1:
wx, wy, _r, _f = pads.pad(1).abs_pos
w1 = (wx + p), (wy - q)
w2 = (tile_x0 + tile_width/2 + cut_gap), (tile_y0 - tile_height/2 - cut_gap - q)
w3 = (tile_x0 - coil_pitch_h + tile_width/2 + cut_gap + 2*q), (tile_y0 - tile_height/2 - cut_gap - q)
w4 = (wx + p - coil_pitch_h), (wy - q)
w5 = (wx - coil_pitch_h), (wy)
b.add(cad_pr.Trace(join_trace_w, pads.pad(1), w5, waypoints=[w1, w2, w3, w4], orientation=['cw', 'cw', 'cw', 'cw'], side='top'))
else:
wx, wy, _r, _f = pads.pad(1).abs_pos
w1 = (wx + p), (wy - q)
w2 = (wx + p), (wy - coil_pitch_v + pad_pitch + q)
w3 = wx, (wy - coil_pitch_v + pad_pitch)
b.add(cad_pr.Trace(join_trace_w, pads.pad(1), w3, waypoints=[w1, w2], orientation=['cw', 'cw', 'cw'], side='bottom'))
if y == rows-1:
if x > 0 and x % 2 == 0:
wx, wy, _r, _f = pads.pad(2).abs_pos
w1 = (wx + p), (wy + q)
w2 = (tile_x0 + tile_width/2 + cut_gap), (tile_y0 + tile_height/2 + cut_gap + q)
w3 = (tile_x0 - coil_pitch_h + tile_width/2 + cut_gap + 2*q), (tile_y0 + tile_height/2 + cut_gap + q)
w4 = (wx + p - coil_pitch_h), (wy + q)
w5 = (wx - coil_pitch_h), (wy)
b.add(cad_pr.Trace(join_trace_w, pads.pad(2), w5, waypoints=[w1, w2, w3, w4], orientation=['ccw', 'ccw', 'ccw', 'ccw', 'cw'], side='top'))
elif x == 0:
wx, wy, _r, _f = pads.pad(2).abs_pos
w1 = (wx + p), (wy + q)
w2 = (wx + p + q), (tile_y0 + tile_height/2 + cut_gap + q)
w5 = (x0 + 4*total_width/5 - coil_pitch_h/2 - pad_pitch/2), (w2[1])
b.add(cad_pr.Trace(join_trace_w, pads.pad(2), w5, waypoints=[w1, w2], orientation=['ccw', 'cw', 'ccw'], side='bottom'))
if y == 0 and x == cols-1:
wx, wy, _r, _f = pads.pad(1).abs_pos
w1 = (wx + p + q), (wy + q)
w2 = (wx + p), (x0 + tooling_border + cut_gap + coil_pitch_v*rows + q)
w5 = (x0 + 4*total_width/5 - coil_pitch_h/2 + pad_pitch/2), (w2[1])
b.add(cad_pr.Trace(join_trace_w, pads.pad(1), w5, waypoints=[w1, w2], orientation=['ccw', 'ccw', 'ccw'], side='bottom'))
pads = make_pads(x0 + 4*total_width/5 - coil_pitch_h/2, w5[1], 0, 2, pad_dia, pad_length, pad_drill, pad_pitch)
b.add(pads)
k = 3
for layer in ['F.SilkS', 'B.SilkS']:
b.add(kc_gr.Rectangle(start=xy(x-k/2, y-pad_pitch-k/2), end=xy(x+k/2, y-pad_pitch), layer=layer, stroke=pcb.Stroke(width=0),
fill=kc_gr.FillMode(pcb.Atom.solid)))
b.add(pcb.Zone(layers=['F.Cu', 'B.Cu'], hatch=pcb.Hatch(),
min_thickness=0.25, filled_areas_thickness=False,
fill=pcb.ZoneFill(island_removal_mode=1, island_area_min=10),
polygon=pcb.ZonePolygon(pts=pcb.PointList(xy=[
pcb.XYCoord(x0, y0),
pcb.XYCoord(x0, y0+total_height),
pcb.XYCoord(x0+total_width, y0+total_height),
pcb.XYCoord(x0+total_width, y0)]))))
for x in range(0, cols):
tile_x0 = x0 + tooling_border + cut_gap + x*coil_pitch_h
tile_y0 = y0 + tooling_border + cut_gap
w = coil_dia + coil_border*2
y1 = y0 + total_height - tooling_border - cut_gap
b.add(pcb.Zone(layers=['F.Cu', 'B.Cu'], hatch=pcb.Hatch(),
min_thickness=0.25, filled_areas_thickness=False,
fill=pcb.ZoneFill(island_removal_mode=1, island_area_min=10),
keepout=pcb.ZoneKeepout(copperpour_allowed=False),
polygon=pcb.ZonePolygon(pts=pcb.PointList(xy=[
pcb.XYCoord(tile_x0, tile_y0),
pcb.XYCoord(tile_x0, y1),
pcb.XYCoord(tile_x0+w, y1),
pcb.XYCoord(tile_x0+w, tile_y0)]))))
b.write('coil_test_board.kicad_pcb')