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 .

Clock Count

 

The original project is from Otvinta. It is there you need to download all the 3D files and see instructions on how to assemble the 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

Clock 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.

Clock back

 

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.

Clock servos wiring

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.

Servo for hoursServo driver for minutes

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 !