Hello all.
For some time now I have this project and now it's time to share it with the world so others can create their one.
This is a 3D-Printable Servo clock - or a 3D 7 segment clock .
The original project, like it is on the site, uses Windows 10 IOT on a Raspberry PI and servo drivers (Maestro) from Pololu. They are good, but expensive and I'm not a fan of Windows.
So, I decided to take it on and use Open Source software and a more cheap servo drivers.
All the rights are from the owners and designers at Otvinta. They have done a marvelous job creating and sharing this clock with the world.
Requirements
- 1x Raspberry PI (I'm using a Raspberry PI 2 B v1.1)
- 28x SG90 servos
- 2x PCA9685 16 Channel 12bit PWM Servo motor Driver I2C Module
- 2x 5v power source - 1 for the Raspberry PI and another for the servos
Wiring
Here's a photo of the back. Because the holes are for other servo drivers, I had to get creative and create new holes.
We're going to wire each servo to a channel on the servo drivers. We have two servo drivers - one for hours and one for minutes . I'm using a 24h clock.
The code is prepared for the servo channels indicated above.
On the servo drivers, the one you choose for hours, wire the top servo, to the channel 0, the 1 to the channel 1 and so on. For the minutes the same thing.
As you can see from the photo above, on the servo driver for the minutes, you need to put a blob of solder (the position with a red rectangle) in A0 - so that the address is not the same on both .
You can see more info about this on Adafruit site. They are awesome in explaining this.
You don't need to mount the segments just yet. I have a piece of code that will put the servos on its 90 degrees position for segment assembly that will display 8888 !
Power
You'll need two 5v power sources with at least 2 amps for the servos. For the Raspberry PI, depending on the version you have, 2 amps or 2.5 amps will suffice - unless your RPi is a version 3 or above. For those, 3 amps or above is the recommended.
Software
Set Python 3 default version
To set Python3 the default version in Raspbian OS, execute the following commands
Check if there's already an alternative for python
pi@raspberrypi:~ $ sudo update-alternatives --config python update-alternatives: error: no alternatives for python
If not, let's create and set Python3 the default
pi@raspberrypi:~ $ sudo update-alternatives --install /usr/bin/python python /usr/bin/python2.7 1 pi@raspberrypi:~ $ sudo update-alternatives --install /usr/bin/python python /usr/bin/python3.7 2
This will automatically set Python3 as the default.
If you execute python, it will be version 3
pi@raspberrypi:~ $ python Python 3.7.3 (default, Jul 25 2020, 13:03:44) [GCC 8.3.0] on linux Type "help", "copyright", "credits" or "license" for more information. >>>
Servo Drivers
Let's configure and install the libraries for the servo drivers
Install some necessary tools
sudo apt-get install python3-smbus python3-pip i2c-tools
Activate i2c using raspi-config
sudo raspi-config
Choose options 3 -> P5 -> YES
reboot the Raspberry
sudo reboot
After the reboot, check if the servo drivers are beeing detected
pi@raspberrypi:~ $ sudo i2cdetect -y 1 0 1 2 3 4 5 6 7 8 9 a b c d e f 00: -- -- -- -- -- -- -- -- -- -- -- -- -- 10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 40: 40 41 -- -- -- -- -- -- -- -- -- -- -- -- -- -- 50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 70: 70 -- -- -- -- -- -- --
We can see that the drivers are being detected in the addresses 0x40 and 0x41 (we're going to need those later)
Adafruit PCA9685
To work with the servo drivers, we're going to use Adafruit's PCA9685 Python libraries. These instructions are taken from their website. You can learn more about them here and here.
sudo pip3 install adafruit-circuitpython-pca9685 sudo pip3 install adafruit-circuitpython-servokit sudo pip3 install python3-rpi.gpio
After this, we're ready to control the servos.
I've create a couple of scripts to test and reset the servos to the middle position to be easier to attach the servo horns in the correct positions.
The most important lines on all the scripts are these:
from adafruit_servokit import ServoKit hours_servos = ServoKit (address=0x40, channels=16) minutes_servos = ServoKit (address=0x41, channels=16)
We need two servo drivers and they can't have the same address. So, all the servos are divided by both drivers.
The hours servos are in the first driver and the minutes servos are in the second driver.
Assembly of the segments
Execute the following code to put the servos at 90 degrees and then assemble the segments - view assembly instructions on the original project website.
#!/usr/bin/env python import time from adafruit_servokit import ServoKit hours_servos = ServoKit (address=0x40, channels=16) minutes_servos = ServoKit (address=0x41, channels=16) #num_servos num_servos = 7 #0 - 6 hours_1 = 0, 1, 2, 3, 4, 5, 6 hours_0 = 7, 8, 9, 10, 11, 12, 13 minutes_1 = 0, 1, 2, 3, 4, 5, 6 minutes_0 = 7, 8, 9, 10, 11, 12, 13 # tuple with hide angles hide = 180, 180, 0, 180, 0, 180, 180 # number_definitons number_0 = 90, 90, 90, hide[3], 90, 90, 90 number_1 = hide[0], hide[1], 90, hide[3], hide[4], 90, hide[6] number_2 = 90, hide[1], 90, 90, 90, hide[5], 90 number_3 = 90, hide[1], 90, 90, hide[4], 90, 90 number_4 = hide[0], 90, 90, 90, hide[4], 90, hide[6] number_5 = 90, 90, hide[2], 90, hide[4], 90, 90 number_6 = 90, 90, hide[2], 90, 90, 90, 90 number_7 = 90, hide[1], 90, hide[3], hide[4], 90, hide[6] number_8 = 90, 90, 90, 90, 90, 90, 90 number_9 = 90, 90, 90, 90, hide[4], 90, 90 def set_default_position(i,kit): # 90 degrees is the default position for all servos kit.servo[i].angle = 90 def reset_some(group, servos): for i in range(group[0], group[6]+1): print(i) set_default_position(i, servos) def reset_servos(): for i in range(hours_1[0], hours_1[6]+1): print(i) set_default_position(i, hours_servos) for i in range(hours_0[0], hours_0[6]+1): print(i) set_default_position(i, hours_servos) for i in range(minutes_1[0], minutes_1[6]+1): print(i) set_default_position(i, minutes_servos) for i in range(minutes_0[0], minutes_0[6]+1): print(i) set_default_position(i, minutes_servos) def hide_all(): #hides all the foots for i in range(hours_1[0], hours_1[6]+1): print(i) hours_servos.servo[i].angle = hide[i] x = 0 for i in range(hours_0[0], hours_0[6]+1): print (i) hours_servos.servo[i].angle = hide[x] x = x + 1 for i in range(minutes_1[0], minutes_1[6]+1): print(i) minutes_servos.servo[i].angle = hide[i] x = 0 for i in range(minutes_0[0], minutes_0[6]+1): print (i) minutes_servos.servo[i].angle = hide[x] x = x + 1 reset_servos()
Explaining the code
Lines 6 and 7 define the servos for the hours and minutes. The first one is for hours, address 0x40 and the second one is for the minutes, address 0x41 (set by putting a blog of solder in the A0 position like explained above).
Line 10 sets a variable with the number of servos of each digit.
Now, we set 4 variables - tuples, lines 12 to 15 - that will hold the servo channels on each digit (4 digits - 2 for hours and 2 for minutes).
On line 18 we set another tuple with the hide angles .
What are the hide angles ?
A digit is composed of 7 servos, like you see in the pictures above. I'm not talking about the servo channel on the Servo Driver, where 0-6 is the first digit and 7-13 is the second. They all are composed of 7 servos, and in a tuple, the index is always 0 to 6 .
The first servo - 0 - the segment is hidden when at position 180 degrees. The second servo - 1 - the same. The third segment gets hidden when the servo is at 0 degrees. The rest is the same principle.
At lines 21 to 30, we set tuples with angles positions that will define the numbers.
For number 0 (line 21) we show all segments (90 degrees positions) and hide the segment for the third servo . This will give us number 0.
For number 4 (line 25) we hide servos 0, 4 and 6 and put at 90 degrees position (showing the segments) for all the others. This will give number 4.
Line 32 we define a function to set the default position for a given servo in a given servo driver (first and second argument of the function).
Line 42 defines a function to reset all servos. It uses a for loop to go from the first servo to the last and sets it to its default position using the function defined above.
Line 56 defines a function to hide all the segments.
We finally execute the function to reset all servos.
Clock
Now, let's see the code that will display the current time
#!/usr/bin/env python #Clock import time import datetime from adafruit_servokit import ServoKit hours_servos = ServoKit (address=0x40, channels=16) minutes_servos = ServoKit (address=0x41, channels=16) #num_servos num_servos = 7 #0 - 6 # Channels for hours and minutes in the servo controller # servo controller 1 hours_1 = 0, 1, 2, 3, 4, 5, 6 hours_0 = 7, 8, 9, 10, 11, 12, 13 # servo controller 2 minutes_1 = 0, 1, 2, 3, 4, 5, 6 minutes_0 = 7, 8, 9, 10, 11, 12, 13 # tuple with hide angles - how the servos will hide # according to its position hide = 180, 180, 0, 180, 0, 180, 180 # number_definitons for servo positions number_0 = 90, 90, 90, hide[3], 90, 90, 90 number_1 = hide[0], hide[1], 90, hide[3], hide[4], 90, hide[6] number_2 = 90, hide[1], 90, 90, 90, hide[5], 90 number_3 = 90, hide[1], 90, 90, hide[4], 90, 90 number_4 = hide[0], 90, 90, 90, hide[4], 90, hide[6] number_5 = 90, 90, hide[2], 90, hide[4], 90, 90 number_6 = 90, 90, hide[2], 90, 90, 90, 90 number_7 = 90, hide[1], 90, hide[3], hide[4], 90, hide[6] number_8 = 90, 90, 90, 90, 90, 90, 90 number_9 = 90, 90, 90, 90, hide[4], 90, 90 def set_default_position(i, kit): # 90 degrees is the default position for all servos kit.servo[i].angle = 90 def reset_servos(): for i in range(hours_1[0], hours_1[6]+1): print(i) set_default_position(i, hours_servos) for i in range(hours_0[0], hours_0[6]+1): print(i) set_default_position(i, hours_servos) for i in range(minutes_1[0], minutes_1[6]+1): print(i) set_default_position(i, minutes_servos) for i in range(minutes_0[0], minutes_0[6]+1): print(i) set_default_position(i, minutes_servos) def hide_all(): #hides all the foots for x in range(num_servos): #print(i) kit.servo[x].angle = hide[x] # show number # set number to show and what group of servos def show_number (number, group_servo,kit): if (number == 0): x = 0 for t in range(group_servo[0], group_servo[6]+1): kit.servo[t].angle = number_0[x] x = x + 1 if (number == 1): x = 0 for t in range(group_servo[0], group_servo[6]+1): kit.servo[t].angle = number_1[x] x = x + 1 if (number == 2): x = 0 for t in range(group_servo[0], group_servo[6]+1): kit.servo[t].angle = number_2[x] x = x + 1 if (number == 3): x = 0 for t in range(group_servo[0], group_servo[6]+1): kit.servo[t].angle = number_3[x] x = x + 1 if (number == 4): x = 0 for t in range(group_servo[0], group_servo[6]+1): kit.servo[t].angle = number_4[x] x = x + 1 if (number == 5): x = 0 for t in range(group_servo[0], group_servo[6]+1): kit.servo[t].angle = number_5[x] x = x + 1 if (number == 6): x = 0 for t in range(group_servo[0], group_servo[6]+1): kit.servo[t].angle = number_6[x] x = x + 1 if (number == 7): x = 0 for t in range(group_servo[0], group_servo[6]+1): kit.servo[t].angle = number_7[x] x = x + 1 if (number == 8): x = 0 for t in range(group_servo[0], group_servo[6]+1): kit.servo[t].angle = number_8[x] x = x + 1 if (number == 9): x = 0 for t in range(group_servo[0], group_servo[6]+1): kit.servo[t].angle = number_9[x] x = x + 1 time.sleep(0.5) while True: # Get time now = datetime.datetime.now() # save hours hh = list(str(now.hour)) # save minutes mm = list(str(now.minute)) # check if we have one digit if (len(mm) == 1): # insert 0 in first position mm.insert(0,"0") if (len(hh) == 1): # insert 0 in first position hh.insert(0,"0") # print ("h0: %s" % (hh[0])) # print ("h1: %s" % (hh[1])) # print ("m0: %s" % (mm[0])) # print ("m1: %s" % (mm[1])) # Show hours show_number(int(hh[0]),hours_1,hours_servos) show_number(int(hh[1]),hours_0,hours_servos) # show minutes show_number(int(mm[0]),minutes_1,minutes_servos) show_number(int(mm[1]),minutes_0,minutes_servos) # we dont need a sleep because we already have 1/2 seconds
Explaining of the code
The first 60 lines are already explained and the same of the previous code.
Lines 64 to 116 define a function that will show (move the servos) to the desired number.
The function accepts 3 parameters:
- number: the number to display
- group_servo: what digit to change - hours0, hours_1, minutes_0, minutes_1
- servo_kit: what servo driver to call
Inside the function, we start by checking what is the digit to display, We use if functions to check the number to display.
Once we're inside the if, we initialize a variable - x - to 0. We will use this variable to loop through the tuple of the number.
x = 0
Next, we use a for loop to go from the first channel of the digit to change (hours_1, hours_0, minutes_1, minutes_0) to the last. Once inside, we set the angle of that servo to the one set in the number tuple. We then increase the x variable by a factor of 1.
for t in range(group_servo[0], group_servo[6]+1): kit.servo[t].angle = number_0[x] x = x + 1
We couldn't use the t variable to replace the x one because for hours_0 and minutes_0 the channel is from 7 to 13.
It seems complicated, but it really isn't.
we then sleep 1/2 second .
Main function
Inside the main, we start by getting the current time - line 122.
Lines 125 to 137 save the current hour and minute.
We create a couple of variables - hh and mm - to save the current hour and minute. We then use the function datetime.datetime.now().hour and .minute to get the current hour and minute. We then transform them into lists. More on that later.
Because we have two digits for hours and minutes, we can't have just one digit. So, we check the length and if just have size 1, insert a 0 in the first position - hence transform the variables into lists.
By having lists, we can also access each digit invididually - hh[0] for the first, and hh[1] for the second.
Lines 146 to 151 call the function show_number - already explained above - to display the digits of hh and mm - also transforming them into a int .
show_number(int(hh[0], hours_1, hours_servos) will sho the number at hh[0] in the first digit for hours, using the channels of hours_1, in the Servo driver we configured for hours - were we connected the first 14 servos.
Here's a video - accelerated - displaying the time - 10 minutes in 2:30m
NOTE: Because of the camera angle, it appears that some segments aren't hidden, but they are. If you're live, you could tell that.
Happy coding !
Top Comments