synthesize.py 20.6 KB
Newer Older
1 2
from __future__ import unicode_literals
import math
Pietro Saccardi's avatar
Pietro Saccardi committed
3
from collections import namedtuple
4

Pietro Saccardi's avatar
Pietro Saccardi committed
5
import pcbnew as pcb
6

7
# Leds will be named LED0, LED1...
8
LED_PREFIX = 'LED'
9
# Resistor driving LEDS will be named R0, R1, ...
10
RESISTOR_PREFIX = 'R'
11 12 13
# Other names
MOSFET_NAME = 'Q0'
PIN_NAME = 'J0'
14
# Coordinated of the center in mm
15 16
CENTER_X_MM = 100.
CENTER_Y_MM = 100.
17 18
# Radius of the circle where the center of the components
# (leds and resistor) is placed
19
RADIUS_MM = 30.
20
# Number of lines of LED (= number of resistors)
21
N_LINES = 3
22
# Number of leds per line
23
N_LEDS_PER_LINE = 3
24
# Offset angle for the whole design
25
ROTATION_OFS_RAD = -math.pi / 12.
26
# Offset angle of the LEDs
27
LED_ORIENTATION_OFS_RAD = math.pi
28 29 30
# Offset angle of the resistors
RESISTOR_ORIENTATION_OFS_RAD = 0.
# Angular resolution for synthesizing arcs
31
ANGULAR_RESOLUTION = math.pi / 40
32
# True for having the power ring on F.Cu (False resp. for B.Cu)
33
PWR_RING_FCU = True
34
# True for having the ground ring on F.Cu (False resp. for B.Cu)
35
GND_RING_FCU = True
36
# Radial offset in mm for the power ring
37
PWR_RING_DISP_MM = -4.
38
# Radial offset in mm for the ground ring
39
GND_RING_DISP_MM = 4.
40 41
# Extra portion of wire to add before connecting to a ring
_ANG_DIST_BTW_MODS = 2. * math.pi / float((1 + N_LEDS_PER_LINE) * N_LINES)
Pietro Saccardi's avatar
Pietro Saccardi committed
42
RING_OVERHANG_ANGLE = _ANG_DIST_BTW_MODS / 3.
43 44
# If >0, routes the LED strips with a copper fill
LED_FILL_WIDTH_MM = 4.
45 46
DEFAULT_TRACK_WIDTH_MM=1.

47 48 49
MOSFET_ORIENTATION = 0.
PIN_ORIENTATION = 0.

Pietro Saccardi's avatar
Pietro Saccardi committed
50 51 52
_RESIDUAL_ANGLE = (_ANG_DIST_BTW_MODS - 2. * RING_OVERHANG_ANGLE)
_TARGET_REMAINING_ANGLE = 2. * math.asin((-min(PWR_RING_DISP_MM, GND_RING_DISP_MM) - LED_FILL_WIDTH_MM / 2. - DEFAULT_TRACK_WIDTH_MM / 2.) / (2. * RADIUS_MM))
_FILL_OVERHANG_ANGLE = (_RESIDUAL_ANGLE - _TARGET_REMAINING_ANGLE) / 2.
53

Pietro Saccardi's avatar
Pietro Saccardi committed
54 55
Terminal = namedtuple('Terminal', ['module', 'pad'])

56 57 58 59 60 61 62
LayerBCu = 31
LayerFCu = 0
NetTypeLedStrip = 'led strip'
NetTypePower = 'power'
NetTypeGround = 'ground'
NetTypeUnknown = '?'

63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110

Place = namedtuple('Place', ['x', 'y', 'rot'])


def ortho(a):
    return a - math.pi / 2.

def to_cartesian(c, angle, r):
    return c.__class__(c.x + r * math.cos(angle), c.y + r * math.sin(angle))

def to_polar(c, pos):
    pos_dx = pos.x - c.x
    pos_dy = pos.y - c.y
    r = math.sqrt(pos_dx * pos_dx + pos_dy * pos_dy)
    angle = math.acos(pos_dx / r)
    if pos_dy < 0.: angle = 2. * math.pi - angle
    return (angle, r)

def shift_along_radius(c, pos, shift):
    delta = pos - c
    radius = math.sqrt(delta.x * delta.x + delta.y * delta.y)
    scale_factor = float(shift) / radius
    return pos + pos.__class__(delta.x * scale_factor, delta.y * scale_factor)

def shift_along_arc(c, pos, delta_angle):
    angle, r = to_polar(c, pos)
    return to_cartesian(c, angle + delta_angle, r)

def compute_radial_segment(c, start, end=None, angle=None, steps=None, angular_resolution=None, excess_angle=0., skip_start=True):
    assert((end is None) != (angle is None))
    # Determine polar coordinates of start
    start_angle, start_r = to_polar(c, start)

    if end is None:
        end_r = start_r
        end_angle = start_angle + angle
    else:
        end_angle, end_r = to_polar(c, end)

    # Choose the arc < 180 degrees
    if abs(end_angle - start_angle) > math.pi:
        if start_angle < end_angle:
            end_angle -= 2. * math.pi
        else:
            start_angle -= 2. * math.pi
    assert((steps is None) != (angular_resolution is None))
    if steps is None:
        steps = int(math.ceil(abs(end_angle - start_angle) / angular_resolution))
Pietro Saccardi's avatar
Pietro Saccardi committed
111
    steps = max(1, steps)
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
    if excess_angle != 0.:
        if start_angle <= end_angle:
            start_angle -= excess_angle
            end_angle += excess_angle
        else:
            start_angle += excess_angle
            end_angle -= excess_angle
    for i in range(steps + 1):
        frac = float(i) / float(steps)
        angle = start_angle + frac * (end_angle - start_angle)
        r = start_r + frac * (end_r - start_r)
        if i > 0 or not skip_start:
            yield to_cartesian(c, angle, r)


127
class Illuminator(object):
128

129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
    def guess_net_type(self, terminals):
        all_leds = True
        all_resistors = True
        for t in terminals:
            if t.module.startswith(LED_PREFIX):
                all_resistors = False
            elif t.module.startswith(RESISTOR_PREFIX):
                all_leds = False
        if all_leds != all_resistors:
            if all_resistors:
                # 2+ resistors connected: power
                return NetTypePower
            elif len(terminals) != 2:
                # 1 or 3+ led connected: ground
                return NetTypeGround
            else:
                # If it's the same pad, it's ground,
                # otherwise, Strip
                if terminals[0].pad == terminals[1].pad:
                    return NetTypeGround
                else:
                    return NetTypeLedStrip
        elif not all_leds and len(terminals) == 2:
            # Mixed resistor/led 2-terminal net. That's a strip
            return NetTypeLedStrip
        else:
            return NetTypeUnknown


    def get_nets_at_placed_modules(self):
        retval = {}
Pietro Saccardi's avatar
Pietro Saccardi committed
160 161 162 163 164
        for mod_name in self.placed_modules:
            mod = self.board.FindModule(mod_name)
            for pad in mod.Pads():
                net = pad.GetNet()
                net_code = pad.GetNetCode()
165 166 167 168
                if net_code not in retval:
                    retval[net_code] = []
                retval[net_code].append(Terminal(module=mod_name, pad=pad.GetPadName()))
        return retval
169

Pietro Saccardi's avatar
Pietro Saccardi committed
170 171 172 173
    def clear_tracks_in_nets(self, net_codes):
        for track in self.board.GetTracks():
            if track.GetNetCode() in net_codes:
                self.board.Delete(track)
174
        to_delete = []
175 176 177
        for i in range(self.board.GetAreaCount()):
            area = self.board.GetArea(i)
            if area.GetNetCode() in net_codes:
178 179 180
                to_delete.append(area)
        for area in to_delete:
            self.board.Delete(area)
181

Pietro Saccardi's avatar
Pietro Saccardi committed
182 183
    def place_module(self, name, place):
        mod = self.board.FindModule(name)
184
        if mod:
185
            print('Placing %s at %s.' % (name, str(place)))
186
            mod.SetPosition(pcb.wxPoint(place.x, place.y))
187 188
            mod.SetOrientation(-math.degrees(place.rot) * 10.)

189 190 191
    def get_terminal_position(self, terminal):
        return self.board.FindModule(terminal.module).FindPadByName(terminal.pad).GetPosition()

192 193 194
    def get_module_position(self, module):
        return self.board.FindModule(module).GetPosition()

195 196 197
    def get_net_name(self, net_code):
        return self.board.FindNet(net_code).GetNetname()

198 199 200 201 202
    def _get_one_place(self, angle, orientation=0.):
        return Place(
            x=self.center.x + pcb.FromMM(RADIUS_MM) * math.cos(angle + ROTATION_OFS_RAD),
            y=self.center.y + pcb.FromMM(RADIUS_MM) * math.sin(angle + ROTATION_OFS_RAD),
            rot=ortho(angle + ROTATION_OFS_RAD) + orientation
203
        )
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220

    def place(self):
        n_elm = N_LINES * (1 + N_LEDS_PER_LINE)
        angle_step = 2. * math.pi / float(n_elm)
        angle = 0.
        for line_idx in range(N_LINES):
            mod_name = RESISTOR_PREFIX + str(line_idx)
            self.place_module(mod_name,
                self._get_one_place(angle, RESISTOR_ORIENTATION_OFS_RAD))
            angle -= angle_step
            self.placed_modules.add(mod_name)
            for led_idx in range(N_LEDS_PER_LINE):
                mod_name = LED_PREFIX + str(line_idx * N_LEDS_PER_LINE + led_idx)
                self.place_module(LED_PREFIX + str(line_idx * N_LEDS_PER_LINE + led_idx),
                    self._get_one_place(angle, LED_ORIENTATION_OFS_RAD))
                angle -= angle_step
                self.placed_modules.add(mod_name)
221
        self._place_pin_and_fet()
Pietro Saccardi's avatar
Pietro Saccardi committed
222

223
    def make_track_segment(self, start, end, net_code, layer):
Pietro Saccardi's avatar
Pietro Saccardi committed
224 225 226 227 228
        t = pcb.TRACK(self.board)
        self.board.Add(t)
        t.SetStart(start)
        t.SetEnd(end)
        t.SetNetCode(net_code)
229
        t.SetLayer(layer)
230
        t.SetWidth(pcb.FromMM(DEFAULT_TRACK_WIDTH_MM))
231
        return end
Pietro Saccardi's avatar
Pietro Saccardi committed
232

233 234 235 236 237 238
    def make_track_horizontal_segment_to_radius(self, start, radius, net_code, layer):
        angle = math.asin(float(start.y - self.center.y) / radius)
        if start.x < self.center.x: angle = math.pi - angle
        end = pcb.wxPoint(self.center.x + radius * math.cos(angle), start.y)
        return self.make_track_segment(start, end, net_code, layer)

239
    def _make_track_arc_internal(self, start, net_code, layer, *args, **kwargs):
240
        last = start
241
        for pt in compute_radial_segment(self.center, start, *args, **kwargs):
242
            self.make_track_segment(last, pt, net_code, layer)
243
            last = pt
244 245 246 247 248 249 250 251 252 253 254
        return last

    def make_track_arc_from_endpts(self, start, end, net_code, layer):
        return self._make_track_arc_internal(
            start, net_code, layer,
            end=end, angular_resolution=ANGULAR_RESOLUTION)

    def make_track_arc_from_angle(self, start, angle, net_code, layer):
        return self._make_track_arc_internal(
            start, net_code, layer,
            angle=angle, angular_resolution=ANGULAR_RESOLUTION)
255 256

    def make_track_radial_segment(self, pos, displacement, net_code, layer):
257
        end_pos = shift_along_radius(self.center, pos, displacement)
258 259
        return self.make_track_segment(pos, end_pos, net_code, layer)

260 261
    def make_fill_area(self, vertices, is_thermal, net_code, layer):
        area = self.board.InsertArea(net_code, self.board.GetAreaCount(), layer,
262
            vertices[0].x, vertices[0].y, pcb.CPolyLine.DIAGONAL_EDGE)
263 264 265 266 267 268 269
        if is_thermal:
            area.SetPadConnection(pcb.PAD_ZONE_CONN_THERMAL)
        else:
            area.SetPadConnection(pcb.PAD_ZONE_CONN_FULL)
        # area.SetIsFilled(True)
        outline = area.Outline()
        for vertex in vertices[1:]:
270 271 272 273 274 275 276
            if getattr(outline, 'AppendCorner', None) is None:
                # Kicad nightly
                outline.Append(vertex.x, vertex.y)
            else:
                outline.AppendCorner(vertex.x, vertex.y)
        if getattr(outline, 'CloseLastContour', None) is not None:
            outline.CloseLastContour()
277 278
        area.SetCornerRadius(pcbnew.FromMM(DEFAULT_TRACK_WIDTH_MM / 2.))
        area.SetCornerSmoothingType(pcb.ZONE_SETTINGS.SMOOTHING_FILLET)
279 280 281 282 283 284 285 286 287 288 289
        area.BuildFilledSolidAreasPolygons(self.board)
        return area

    def make_fill_arc(self, start, end, width, is_thermal, net_code, layer):
        # Compute the vertices
        lower_arc_start = shift_along_radius(self.center, start, -width / 2.)
        upper_arc_start = shift_along_radius(self.center, end, width / 2.)
        vertices = list(compute_radial_segment(self.center,
                lower_arc_start,
                shift_along_radius(self.center, end, -width / 2.),
                angular_resolution=ANGULAR_RESOLUTION,
290
                excess_angle=0.0001,
291 292 293 294 295
                skip_start=False)) + \
            list(compute_radial_segment(self.center,
                upper_arc_start,
                shift_along_radius(self.center, start, width / 2.),
                angular_resolution=ANGULAR_RESOLUTION,
296
                excess_angle=0.0001,
297 298
                skip_start=False))
        return self.make_fill_area(vertices, is_thermal, net_code, layer)
299 300 301 302 303 304 305 306

    def make_via(self, position, net_code):
        v = pcb.VIA(self.board)
        self.board.Add(v)
        v.SetPosition(position)
        v.SetViaType(pcb.VIA_THROUGH)
        v.SetLayerPair(LayerFCu, LayerBCu)
        v.SetNetCode(net_code)
307
        v.SetWidth(pcb.FromMM(DEFAULT_TRACK_WIDTH_MM))
308
        return position
309

310
    def _route_arc(self, net_code, start_terminal, end_terminal, layer=LayerFCu):
311 312 313 314
        print('Routing %s between %s and %s with a single arc.' % (
            self.get_net_name(net_code), start_terminal.module, end_terminal.module
        ))
        # Get the offsetted position of the pads
315
        self.make_track_arc_from_endpts(
316 317 318
            self.get_terminal_position(start_terminal),
            self.get_terminal_position(end_terminal),
            net_code,
319
            layer
320 321
        )

322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343
    def _route_fill_arc(self, net_code, start_terminal, end_terminal):
        print('Routing %s between %s and %s with filled arc region.' % (
            self.get_net_name(net_code), start_terminal.module, end_terminal.module
        ))
        start_pos = self.get_terminal_position(start_terminal)
        end_pos = self.get_terminal_position(end_terminal)
        self.make_track_arc_from_endpts(
            start_pos,
            end_pos,
            net_code,
            LayerFCu
        )
        # Get the offsetted position of the pads
        self.make_fill_arc(
            start_pos,
            end_pos,
            pcb.FromMM(LED_FILL_WIDTH_MM),
            False,
            net_code,
            LayerFCu
        )

344
    def _route_ring(self, net_code, terminals, displacement, ring_overhang, layer):
345 346 347 348 349 350 351
        log_msg = 'Routing %s between %s with' % (
            self.get_net_name(net_code), ', '.join([t.module for t in terminals])
        )
        if layer != LayerFCu:
            log_msg += ' a via and'
        if displacement == 0.:
            log_msg += ' a full circular track.'
352
        else:
353
            log_msg += ' a circular track offsetted by %f.' % displacement
354 355
        terminal_pos = []
        for terminal in terminals:
356
            term_ring_pt = self.get_terminal_position(terminal)
Pietro Saccardi's avatar
Pietro Saccardi committed
357
            _, term_ring_pt_r = to_polar(self.center, term_ring_pt)
358
            if displacement != 0.:
359
                if ring_overhang != 0.:
Pietro Saccardi's avatar
Pietro Saccardi committed
360 361 362 363
                    mod_pos_angle, _ = to_polar(self.center,
                        self.get_module_position(terminal.module))
                    new_end_pt = to_cartesian(self.center,
                        mod_pos_angle + ring_overhang, term_ring_pt_r)
364 365
                    self.make_track_arc_from_endpts(term_ring_pt, new_end_pt, net_code, LayerFCu)
                    if LED_FILL_WIDTH_MM != 0.:
366
                        # Some extra fill:
Pietro Saccardi's avatar
Pietro Saccardi committed
367
                        overhang_angle = _FILL_OVERHANG_ANGLE * (1. if ring_overhang > 0. else -1.)
368
                        fill_end_pt  = shift_along_arc(self.center,
Pietro Saccardi's avatar
Pietro Saccardi committed
369
                            new_end_pt, overhang_angle)
370
                        self.make_fill_arc(term_ring_pt, fill_end_pt,
371 372 373
                            pcb.FromMM(LED_FILL_WIDTH_MM),
                            False, net_code, LayerFCu)
                    term_ring_pt = new_end_pt
374 375
                term_ring_pt = self.make_track_radial_segment(
                    term_ring_pt, displacement, net_code, LayerFCu)
376
            # Now add the via and store the position
377 378 379
            if layer != LayerFCu:
                self.make_via(term_ring_pt, net_code)
            terminal_pos.append(term_ring_pt)
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
        # Connect the terminals with an arc. Make sure
        # that all the positions at 0 and 180 are covered
        polar_term_pos = [to_polar(self.center, pos) for pos in terminal_pos]
        polar_term_pos += [
            (0, pcb.FromMM(RADIUS_MM) + displacement),
            (math.pi, pcb.FromMM(RADIUS_MM) + displacement)
        ]
        polar_term_pos.sort()

        # Now make the actual tracks. One more cartesian/polar conversion
        # because I didn't really think this through
        last_pos = polar_term_pos[-1]
        for pos in polar_term_pos:
            self.make_track_arc_from_endpts(
                to_cartesian(self.center, *last_pos),
                to_cartesian(self.center, *pos),
                net_code, layer)
397 398
            last_pos = pos

399
    def route(self):
400 401 402 403 404 405 406 407 408 409 410 411 412 413
        for net_code, terminals in self.get_nets_at_placed_modules().items():
            # Try to guess net type
            net_type = self.guess_net_type(terminals)
            print('Net %s guessed type: %s' % (self.get_net_name(net_code), net_type))
            if net_type == NetTypeUnknown:
                print('I do not know what to to with net %s between %s...' % (
                    self.get_net_name(net_code),
                    str(terminals)
                ))
                continue
            # Clear this net
            self.clear_tracks_in_nets([net_code])
            if net_type == NetTypeLedStrip:
                assert(len(terminals) == 2)
414 415 416 417
                if LED_FILL_WIDTH_MM > 0.:
                    self._route_fill_arc(net_code, terminals[0], terminals[1])
                else:
                    self._route_arc(net_code, terminals[0], terminals[1])
418
            elif net_type == NetTypePower:
419 420
                assert(self.power_net is None)
                self.power_net = net_code
421 422
                self._route_ring(net_code, terminals,
                    pcb.FromMM(PWR_RING_DISP_MM),
423
                    RING_OVERHANG_ANGLE,
424 425
                    LayerFCu if PWR_RING_FCU else LayerBCu
                )
426
            elif net_type == NetTypeGround:
427 428
                assert(self.ground_net is None)
                self.ground_net = net_code
429 430
                self._route_ring(net_code, terminals,
                    pcb.FromMM(GND_RING_DISP_MM),
431
                    -RING_OVERHANG_ANGLE,
432 433
                    LayerFCu if GND_RING_FCU else LayerBCu
                )
434 435 436 437 438 439 440 441 442 443 444 445 446 447
        self._route_pin_and_fet()

    def _place_pin_and_fet(self):
        self.pin = self.board.FindModule(PIN_NAME)
        self.fet = self.board.FindModule(MOSFET_NAME)
        if self.pin is None or self.fet is None:
            return
        # Ok place first the pin centered and rotated
        if not self.pin.IsFlipped():
            self.pin.Flip(self.pin.GetPosition())
        if not self.fet.IsFlipped():
            self.fet.Flip(self.pin.GetPosition())
        print('Found pin and mosfet, placing them at opposite sides of the board.')
        self.place_module(self.pin.GetReference(),
448
            Place(self.center.x - pcb.FromMM(RADIUS_MM), self.center.y, PIN_ORIENTATION))
449
        self.place_module(self.fet.GetReference(),
450
            Place(self.center.x + pcb.FromMM(RADIUS_MM), self.center.y, MOSFET_ORIENTATION))
451 452 453 454 455

    def _route_pin_and_fet(self):
        if self.pin is None or self.fet is None:
            return
        print('Found pin and mosfet, adding connection rings')
456 457 458 459 460 461 462
        routed_nets = set()
        for pin_pad in self.pin.Pads():
            for fet_pad in self.fet.Pads():
                if fet_pad.GetNetCode() != pin_pad.GetNetCode(): continue
                net_code = fet_pad.GetNetCode()
                routed_nets.add(net_code)
                self.clear_tracks_in_nets([net_code])
463 464 465 466 467
                # Make a horizontal segment to the right radius
                fet_pt = self.make_track_horizontal_segment_to_radius(
                    fet_pad.GetPosition(), pcb.FromMM(RADIUS_MM), net_code, LayerBCu)
                pin_pt = self.make_track_horizontal_segment_to_radius(
                    pin_pad.GetPosition(), pcb.FromMM(RADIUS_MM), net_code, LayerBCu)
468 469
                print('Adding ring from the mosfet pad %s (net %s)' % (
                    fet_pad.GetName(), self.get_net_name(net_code)))
470
                self.make_track_arc_from_endpts(fet_pt, pin_pt, net_code, LayerBCu)
471 472 473
        print('Adding missing vias to known nets.')
        # Find the third pad
        for pad in self.fet.Pads():
474 475 476 477 478 479 480 481 482 483 484 485
            if pad.GetNetCode() != self.ground_net: continue
            print('Connecting pad %s of the mosfet to net %s' % (
                pad.GetName(), self.get_net_name(self.ground_net)))
            # We know there is a point in this net at theta = 0
            # Drop a via from there
            known_pt = to_cartesian(self.center, 0.,
                pcb.FromMM(RADIUS_MM + GND_RING_DISP_MM))
            self.make_via(known_pt, self.ground_net)
            # and then straight to this pad
            self.make_track_segment(
                known_pt, pad.GetPosition(),
                self.ground_net, LayerBCu)
486 487
        # Find the third pin
        for pad in self.pin.Pads():
488 489 490 491 492 493 494 495 496 497 498 499
            if pad.GetNetCode() != self.power_net: continue
            print('Connecting pad %s of the mosfet to net %s' % (
                pad.GetName(), self.get_net_name(self.power_net)))
            # We know there is a point in this net at theta=180
            # Drop a via from there
            known_pt = to_cartesian(self.center, math.pi,
                pcb.FromMM(RADIUS_MM + PWR_RING_DISP_MM))
            self.make_via(known_pt, self.power_net)
            # and then straight to this pad
            self.make_track_segment(
                known_pt, pad.GetPosition(),
                self.power_net, LayerBCu)
500

501 502

    def __init__(self):
503
        super(Illuminator, self).__init__()
Pietro Saccardi's avatar
Pietro Saccardi committed
504 505
        self.placed_modules = set()
        self.board = pcb.GetBoard()
506
        self.center = pcb.wxPoint(pcb.FromMM(CENTER_X_MM), pcb.FromMM(CENTER_Y_MM))
507 508 509 510
        self.pin = None
        self.fet = None
        self.ground_net = None
        self.power_net = None
Pietro Saccardi's avatar
Pietro Saccardi committed
511 512

if __name__ == '__main__':
513 514 515
    a = Illuminator()
    a.place()
    a.route()