Python Scripting Example: Studio Clock

I haven’t found any examples of a working and useful Python script. I will Change That!

When I got the idea of building a Studio Clock I new I had to write a script. Placing 72 LED’s in a circle with rotation and position values that have to be calculated with trigonometry and will be therefore long floats numbers, would be a job for an intern who has to be kept busy ;-). The Board on picture has currently
-144 Pads
-84 Vias
-1130 Track Segments
all set in less than a second. When I decide to change the Radius, I just change a variable in the script and its done.

The schematic looks time-consuming too, but it isn’t, its just strategic copy-pasting of component groups and using the Auto Annotation function. That’s the reason D61 to D72 are aligned like that, this way they got the right pattern for the PCB design.

I using the operators Division “/” and Modulus “%” a lot. They are so useful to sort or accesses data, that I show you a small example (thanks to c4757p it is now correct):

list = ['a1','a2','a3','b1','b2','b3','c1','c2','c3']
sublists = [[None] * 3 for _ in range(3)]
print sublists
for i in range(len(list)):
    sublists[i/3][i%3] = list[i]
print sublists

will print out:

[[None, None, None], [None, None, None], [None, None, None]]
[['a1', 'a2', 'a3'], ['b1', 'b2', 'b3'], ['c1', 'c2', 'c3']]

I decided not to use this technique to maintain it readable and created two Lists, one for the “SecondsRing” with 60 LED’s and one for the “HourRing” with 12 LED’s (Naming stuff is the must difficult thing in programming ;-)). The Rings are actually Polygons with 60 Sides, you will get a spider web when you extrapolate the connection Lines to the middle. The Lists are saving all important objects and values, that I need to assess, this way I don’t need to use all these Getters functions. I going to expand the script with rooting to a connector and 7 segment displays. So I don’t have to do anything, but creating an even more complex script).

Have fun trying to understand the net building logic, it took me 2 days to find the best solutions (and getting to learn the KiCad scripting).

#Cad Studio Clock (2016-01-30)
# execfile("/home/doug/svn/Elektronik/KiCad/StudioClockFull/makeClock.py")
#This script places leds in Clock Formation by using Patterns
#Kathode and Anode is used with the asumption you got Common Anode System. You can change it to Common Cathode System by Placing the diosds the turned by 180 deg. 

import codecs
import pcbnew
import math 
import sys
import collections

pcb = pcbnew.GetBoard()
Radius = 40
originOffsetXY = [0, 0]

MatSRing = [None]*(60)	#Led Ring of Seconds
MatHRing = [None]*(12)	#Led Ring of Hourse or 5 Minute Dividers

NetsA = []	#OuterSecondCommonAnodes
NetsB = []	#InnerCathodes
NetC = None	#OuterHoursCommonAnode

#calc rotation angle (rad) with Position: float -> float
def calcRad(pos):
	return math.pi/30*(pos%60)
#calc ratation angle (deg) with Position: float -> float 
def calcDeg(pos):
	return math.degrees(calcRad(pos))
#calc the Position(s) with the Radius and an Angle: float, float -> float/float/wxPoint
def calcX(radius, pos):
	return math.sin(calcRad(pos))*(radius)+originOffsetXY[0]
def calcY(radius, pos):
	return -math.cos(calcRad(pos))*radius+originOffsetXY[1]
def calcXY(radius, pos):
	return pcbnew.wxPointMM(calcX(radius,pos),calcY(radius,pos))
#add a track and add it: wxPoint, wxPoint, int, int -> Track
def addTrack(startPos, stopPos, net, layer):
	t = pcbnew.TRACK(pcb)
	pcb.Add(t)
	t.SetStart(startPos)
	t.SetEnd(stopPos)
	t.SetNetCode(net)
	t.SetLayer(layer)
	return t
#add an arc of tracks with the Position Idices of the SecondsLeds: float, int, int, int, int -> Track
def addTrackArc(radius, startPos, stopPos, net, layer):		
	t = None
	for i in range(startPos,stopPos-1):
		t = addTrack(calcXY(radius, i), calcXY(radius, i+1), net, layer)
	return t
#add a full Track ring with the desired Radius: float, int, int
def addTrackRing(radius, net, layer):
	addTrackArc(radius, 0, 61, net, layer)
#add a via at the Position: wxPoint -> Via
def addVia(position, net):
	v = pcbnew.VIA(pcb)
	pcb.Add(v)
	v.SetPosition(position)
	#v.SetWidth(600000)
	v.SetViaType(pcbnew.VIA_THROUGH)
	v.SetLayerPair(LayerFCu, LayerBCu)
	v.SetNetCode(net)
	return v

#Some Constants for better Readability
LayerBCu = 31
LayerFCu = 0
LayerEdgeCuts = 44

#Deleting all Nets and Drawings is esier than finding exisitng ones
print '---------------------------------------------------------------'
print '---Delete-Exisitng-Nets-and Drawings---------------------------'
print '---------------------------------------------------------------'
for t in pcb.GetTracks():
        pcb.Delete(t)
for d in pcb.GetDrawings():
	pcb.Remove(d)

#Find all Diods and save often needed Information or Objects in Array
#Sort them into HourRing and Secods Ring
#Find all Nets add them to their Netgroup list.
print '---------------------------------------------------------------'
print '---Gathering-Diods-and-Info------------------------------------'
print '---------------------------------------------------------------'
for modu in pcb.GetModules():
	ref = modu.GetReference().encode('utf-8')
	if(ref.startswith('D')):
		pos = int(ref.split('D')[-1])-1
		pad1 = None	#kathode
		net1 = None
		pad2 = None	#anode
		net2 = None
		for pad in modu.Pads():
			if int(pad.GetPadName()) == 1:
				pad1 = pad
				net1 = pad.GetNetCode()
				if pos <= 59:
					if net1 not in NetsB:
						NetsB.append(net1)
			else:
				pad2 = pad
				net2 = pad.GetNetCode()
				if pos <= 59:
					if net2 not in NetsA:
						NetsA.append(net2)
				elif pos <= 71 and NetC == None:
					NetC = net2	
		if pos <= 59:
			MatSRing[pos] = [pos, modu, pad1, net1, pad2, net2, ref]
			print 'Read: Second %s,  Position %d, Net1 %d, Net2 %d' % (MatSRing[pos][6], MatSRing[pos][0], net1, net2)
		elif pos <= 71:
			pos = pos%60
			MatHRing[pos] = [pos, modu, pad1, net1, pad2, net2, ref]
			print 'Read: Hour %s, Poition %d, Net1 %d, Net2 %d' % (ref, MatHRing[pos][0], net1, net2)
print '-----------------------------------------------------'
print 'NetsA:', NetsA
print 'NetsB:', NetsB
print 'NetC :', NetC
	
#This is just moving modules with a pattern
print '---------------------------------------------------------------'
print '---Calculating-and-Setting-Module-Positions--------------------'
print '---------------------------------------------------------------'
for i in range(len(MatSRing)):	#60
	MatSRing[i][1].SetOrientation(-(calcDeg(i)-90)*10)
	MatSRing[i][1].SetPosition(calcXY(Radius, i))
	print 'Placed: Second %s at %s with rot %s' % (MatSRing[i][6], str(MatSRing[i][1].GetPosition()), str(MatSRing[i][1].GetOrientation()))
for i in range(len(MatHRing)):	#12
	MatHRing[i][1].SetOrientation(-(calcDeg(i*5)-90)*10)
	MatHRing[i][1].SetPosition(calcXY(Radius+4, i*5))
	print 'Placed: Hour %s at %s with rot %s' % (MatSRing[i][6], str(MatHRing[i][1].GetPosition()), str(MatHRing[i][1].GetOrientation()))

print '---------------------------------------------------------------'
print '---Build-Net---------------------------------------------------'
print '---------------------------------------------------------------'
#Inner Kathode Rings and Connections to Pads
for i in range(len(NetsB)):
	print "Adding NetB", NetsB[i]
	r = Radius-3-i	#this can be replaced by a more advanced equation
	addTrackRing(r, NetsB[i], LayerBCu)
	filtered = filter(lambda x: x[3] == NetsB[i], MatSRing)
	for f in filtered:
		addTrack(calcXY(r, f[0]), f[2].GetPosition(), NetsB[i], LayerFCu)
		print 'Added: Connection to %s with Net %d' % (f[6], f[3])
		addVia(calcXY(r,f[0]), NetsB[i])
#Outer Anode Rings of Seconds
for i in range(len(NetsA)):
	var = filter(lambda x: x[5] == NetsA[i], MatSRing)
	sor = sorted(var, key = lambda x: x[3])
	print "Adding NetA", NetsA[i]
	for j in range(0,len(sor)-1):
		addTrack(sor[j][4].GetPosition(),sor[j+1][4].GetPosition(), NetsA[i], LayerFCu)
#Outer Anode Ring of Hours
print "Adding NetC", NetC
RadiusPadH2 =  (float(MatHRing[6][4].GetPosition().y))/1000000
addTrackRing(RadiusPadH2,NetC,LayerFCu)
#Hours kathode sonnections to inner Rings
RadiusPadH1 = (float(MatHRing[6][2].GetPosition().y)/1000000)
RadiusPadS1 = (float(MatSRing[30][2].GetPosition().y)/1000000)
for i in range(len(MatHRing)):
	positionOffset = {0:0.5, 1:1.5, 2:2.5, 3:3.5}[i/3]
	positionIndex = MatHRing[i][0]*5
	tempPosXY1 = calcXY(RadiusPadH1, positionIndex+positionOffset)
	tempPosXY2 = calcXY(RadiusPadS1, positionIndex+positionOffset)
	if i/3 == 0:
		addTrack(MatHRing[i][2].GetPosition(), tempPosXY1, MatHRing[i][3], LayerFCu)
	else:
		tempTrack = addTrackArc(RadiusPadH1, positionIndex, positionIndex+int(positionOffset+0.5), MatHRing[i][3], LayerFCu)
		tempPosXY3 = tempTrack.GetEnd()
		addTrack(tempPosXY3, tempPosXY1, MatHRing[i][3], LayerFCu)
	addVia(tempPosXY1, MatHRing[i][3])
	addTrack(tempPosXY1, tempPosXY2,  MatHRing[i][3], LayerBCu)
	addVia(tempPosXY2, MatHRing[i][3])
	sPadsNet = filter(lambda x: x[3] == MatHRing[i][3], MatSRing)
	addTrack(tempPosXY2, sPadsNet[i/3][2].GetPosition(), MatHRing[i][3], LayerFCu)

print '---------------------------------------------------------------'
print '---Set Edge Cut------------------------------------------------'
print '---------------------------------------------------------------'
print "Setting Board Dimensions too:"
corners = [[-1,-1],[-1,1],[1,1],[1,-1]]
l = Radius*1.2
for i in range(4):
	seg = pcbnew.DRAWSEGMENT(pcb)
	pcb.Add(seg)
	seg.SetStart(pcbnew.wxPointMM(corners[i][0]*l, corners[i][1]*l))
	seg.SetEnd(pcbnew.wxPointMM(corners[(i+1)%4][0]*l, corners[(i+1)%4][1]*l))
	seg.SetLayer(LayerEdgeCuts)
	print "Corner:", seg.GetStart()

print '---------------------------------------------------------------'
print '---The-End-----------------------------------------------------'
print '---------------------------------------------------------------'
16 Likes

Yo that’s really cool, man. It’s impressive enough when people manage to do anything with the scripting API…but this looks awesome, I want one. Pretty clean script too :+1:

Edit: Deleted offtopic post under this pointing out error in example, as it’s been fixed in OP.

Nice script, Doug. I was able to get the same result you showed (although I think I used a smaller 0603 LED) using the following SKiDL script to generate the diode circuit netlist:

from skidl import *

anodes = Bus('a', 6)    # 6-bit bus, but only use a[1]..a[5].
cathodes = Bus('k', 16) # 16-bit bus, but only use k[1]..k[15].

# Create an LED template.
diode = Part('device', 'D', footprint='Diodes_SMD:D_0603', dest=TEMPLATE)

# Connect the seconds LEDs.
for a in anodes[1:4]:
    for k in cathodes[1:15]:
        d = diode(1)
        d['A', 'K'] += a, k

# Connect the hours LEDs.
for i in range(2,6):
    for k in cathodes[i:i+10:5]: # Connect k[2,7,12], k[3,8,13], k[4,9,14] and k[5,10,15].
        d = diode(1)
        d['A', 'K'] += anodes[5], k

ERC()  # Look for rule violations.
generate_netlist()  # Generate netlist file.

4 Likes

what’s the smallest arduino-type board with WiFi that could sit in the middle, synchronize to NTP servers, and drive the LEDs?
OTOH it requires power over a wire anyway, so maybe wired ethernet and PoE is more elegant?

Manufacturing would hate you for this design, with all of these strange rotations.
The appearance would be very similar with all LEDs horizontal

2 Likes

You could use a PnP machine with a rotating table, then the component head only has to travel between two positions - the feeder and a single point on the radius! :stuck_out_tongue:

1 Like

Great script, thanks for sharing. I want to learn how to script too and having examples to look at really helps.

1 Like

Gosh, a guy writes a Python script for pcbnew (one of the few that have appeared on this forum), and we debate whether the resulting design is manufacturable? Are we missing that his script is a gold mine of techniques for automating pcbnew to perform operations that can be used in a wide variety of applications? I don’t care if his end result is a clock with precisely rotated LEDs or a farting unicorn.

7 Likes

@DougE Great design, great example, the result is both functional and aesthetically pleasing. :thumbsup: I was planning a clock using WS2812 LEDs, didn’t quite get round to it yet.

@Anders_Wallin Intel Ediison is small and fully supported by Arduino, but it is not cheap. Also small but a lot cheaper are the ESP8266 modules. They have limited IO, so you would need to drive the LEDs via SPI and some external logic chips…

They have limited IO, so you would need to drive the LEDs via SPI and some external logic chips…

I haven’t tested it yet, but I’m planning to use a 16-Channel Constant-Current LED Driver like the TLC 59281 from Texas Instruments. Thats the reason I made it an Common Anode.

that his script is a gold mine

Thanks. Got some more code snippets from my test project to build digits with single LEDs for you. It was the easiest way for building digits. Haven’t decided how to wire the rest (I’m waiting till I know how I will do the rest. The samples are already ordered.).

#uses the common 7-Segment numbering with 0=a, 1=b, ...

#scaling factor for Segments spacing
f=0.33 
#relative position offset of Horizontal and Vertical Segments to center
OffDig = [[0,  -2*f], [f,-f], [f, f], [0,2*f], [-f,f], [-f,-f], [0, 0]]

def buildHSeg(mod,  offX,  offY):
    print "buildHSeg",  mod[0][6]
    for i in range(len(mod)):
        mod[i][1].SetOrientation([-900,  900][i%2])
        mod[i][1].SetPosition(pcbnew.wxPointMM((-1.5+i)*Scale/8+offX,  0+offY))
        
def buildVSeg(mod,  offX,  offY):
    print "buildVSeg", mod[0][6]
    for i in range(len(mod)):
        mod[i][1].SetOrientation([0,  1800][i%2])
        mod[i][1].SetPosition(pcbnew.wxPointMM(0+offX,  (-1.5+i)*Scale/8+offY))
         
def build7Seg(mods, offsetX,  offsetY):
    print "build7Seg"
    for i in range(len(mods)/4):
        if i in [0, 6, 3]:
            buildHSeg(mods[i*4:i*4+4],  OffDig[i][0]*Scale + offsetX, OffDig[i][1]*Scale + offsetY)
        else:
            buildVSeg(mods[i*4:i*4+4],  OffDig[i][0]*Scale + offsetX, OffDig[i][1]*Scale + offsetY)

build7Seg(MatrixDigitLeds, 0, 0) 
for i in range(len(MatrixDigitLeds)):
    if i%4 != 3:
        addTrack(MatrixDigitLeds[i][2].GetPosition(), MatrixDigitLeds[i+1][4].GetPosition(), MatrixDigitLeds[i+1][5],  0)

It will get you something like this. The series-circuits shouldn’t be a problem when you use a Constant Current Driver.

3 Likes

Very nice to see real world examples of using skidl and pcbnew scripting. Just what I needed. Thank you very much for sharing it’s so much easier to work from examples.

Hi,
Great idea, I like it a lot. I tried to do this in KiCAD 4.0.6(I’m on a nightly build), but I didn’t have any luck with it.
However you can create circular array, which can be utilized to do the same layout. :slight_smile:

Cheers,
Tibor

I working on a much cleaner Script at the Moment. It won’t be as fast as my original one, but is better to read and fixes some bugs (concerning the order of the Elements).

It will also include the routing for four 7-Segment Displays and two 16 pin connectors.
My goal is to script the whole clock without any control circuits on the board. This way I can build the controls external and don’t have to decide jet, what micro-controller and LED-Drivers I will use.

Maybe that one will work and if its not, it will be a lot easer to debug.

As promised a cleaner script with a full routed StudioClock. Here are the source files.
KiCadStudioClock.zip (37.8 KB)

Some Experiences
-most of the scripting it is quite easy as the patterns are simple
-the exceptions from the patterns create the big problems
-keeping track of components you created is not easy, so don’t do it
-storing the Modules and Nets in a Sorted Dictionary, this way I can filter out what I need
-when you change the radius, you might need to adjust some variables in the functions
-hitting the Polygon lines requires some maths
-my motivation felt very deep
-some List are not sorted and the function uses there existing patterns
-the functions GetPosition and GetLayer are your best friend
-define your reference system properly (can be different from the native) it saves a lot of time

5 Likes

That is a beautiful board!

Are you sure this has anything to do with this script? You might want to ask for help on a windows specialized forum instead of one for an open source project.

@DougE from your example, can I assume that it is possible to create a complete PCB from start to finish in python?

That’s right. The script already does that for the PCB part. At this level of automation, using parameters, it would even be absolutely counterproductive to do something by hand.
Also the Netlist can be generated automatically as @devbisme showed in a previous post post.

The placement of footprints is easy, but the routing is a lot of effort because you have to script each track individually, even if in loops, and collision free. However, I can imagine scripting the well scriptable first and leaving the rest of the routing to an Autorouter.

Hello
I tried to download the KicadStudioClock.zip, example but it says access denied. Is it just me? If not, would it be possible to repost the link? Thanks.

@DougE hasn’t posted since 2019. Kicad is at least 2 versions newer since that script was used.