Hackster is hosting Hackster Holidays, Ep. 6: Livestream & Giveaway Drawing. Watch previous episodes or stream live on Monday!Stream Hackster Holidays, Ep. 6 on Monday!
C Forde
Published © CC BY-NC-SA

Dual Spiral Marble Clock

A dual spiral rolling ball clock controlled by a RPi Pico using a combination of 3D printed and hand made parts in metal, acrylic & wood.

AdvancedFull instructions provided5 days449
Dual Spiral Marble Clock

Things used in this project

Hardware components

Pimoroni Pico LiPo 4MB
4MB flash memory, USB-C, STEMMA QT/Qwiic, debug connectors and built in LiPo charging!
×1
Pimoroni Pico display pack
1.14" IPS LCD with 4 integrated buttons
×1
Pimoroni RV3028 RTC
I2C interface and 3.3V or 5V compatible
×1
GPIO Expander
Waveshare
×1
Piezo (passive) buzzer
×1
5 VDC stepper motor controller using ULN2003 driver
×2
Pushbutton Switch, Momentary
Pushbutton Switch, Momentary
×1
Toggle Switch, On-On
Toggle Switch, On-On
×1
Texas Instruments DRV5032FCLPG
×2

Software apps and online services

Thonny
Thonny 3.3.13
Pimoroni Pirate Brand MicroPython v1.20.6
MicroPython v1.20.6

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Labists ET4

Story

Read more

Schematics

Dual Spiral Marble Clock schematic

schematic

Code

spiral_time6_2.py

MicroPython
Dual Spiral Marble clock code
# Spiral clock

# software v1.20.6
# raspberry pi pico LiPo 4MB
# pico display pack 240 x 135 SPI LCD
# Waveshare GPIO Expander For Raspberry Pi Pico
# rv3028 rtc

import math
import utime
from pimoroni import Button, RGBLED
from picographics import PicoGraphics, DISPLAY_PICO_DISPLAY, PEN_P4
disp = PicoGraphics(display=DISPLAY_PICO_DISPLAY, pen_type=PEN_P4, rotate=0)
from breakout_rtc import BreakoutRTC
from pimoroni_i2c import PimoroniI2C
from machine import Pin, PWM

# display setup
width, height = disp.get_bounds()
disp_buffer = bytearray(width * height * 2)
#disp.init(disp_buffer)
disp.set_backlight(0.5)  # backlight - 50%
disp.set_font("bitmap8")
green = disp.create_pen(0, 255, 0) 
black = disp.create_pen(0, 0, 0)
blue = disp.create_pen(0, 0, 255)
red = disp.create_pen(255, 0, 0)
white = disp.create_pen(255, 255, 255)

button_a = Button(12)
button_b = Button(13)
button_x = Button(14)
button_y = Button(15)

led_rgb = RGBLED(6, 7, 8)

# rtc setup
PINS_BREAKOUT_GARDEN = {"sda": 0, "scl": 1}
i2c_3 = PimoroniI2C(**PINS_BREAKOUT_GARDEN)
rtc = BreakoutRTC(i2c_3)
rtc.set_backup_switchover_mode(3)
rtc.set_24_hour()
rtc.update_time()

switch = Pin(11, Pin.IN, Pin.PULL_DOWN) # set enable
buzzer = PWM(Pin(21))

beatlist = [[1,0,0,0],[1,1,0,0],[0,1,0,0],[0,1,1,0],[0,0,1,0],[0,0,1,1],[0,0,0,1],[1,0,0,1],[0,0,0,0]] # stepper motor pattern
motor_pins = [2, 3, 4, 5, 9, 10] # Select [9, 10], motor [2, 3, 4, 5]
sense_pins = [22, 26, 27, 28]

for pin in motor_pins:
    machine.Pin(pin, machine.Pin.OUT)
    
for pin in sense_pins:
    machine.Pin(pin, machine.Pin.IN, Pin.PULL_UP)
    
# global variables
sum_hrs = 0
sum_min = 0
tim = 0
ttim = 0
mim = 0
mtim = 0
rtc_seconds = 0
rtc_minutes = 0
zero_mins = 0
zero_hrs = 0 
hours = 0
old_hrs = -1
old_mins = -1
old_secs = -1
diff_hrs = 0
diff_mins = 0
diff_secs = 0
home = False
spiral_hgt = 4.5 # height of spiral in circular rotations
deg_hr = (spiral_hgt*360)/12
deg_min = (spiral_hgt*360)/60
blank = False

# functions
def clkwise(sel):
    # motor direction of rotation
    for index in range(0, 8):
     set_bit(index, sel)
    return


def anti_clkwise(sel):
    # motor direction of rotation
    for index in range(7, -1, -1):
     set_bit(index, sel)
    return
    

def set_bit(num, sel):
    # num - motor pattern, sel - motor selector
    global beatlist, motor_pins
    # motor select
    if sel == 0:
        machine.Pin(motor_pins[4]).value(0) # motor1 off
        machine.Pin(motor_pins[5]).value(0) # motor2 off
    elif sel == 1:
        machine.Pin(motor_pins[4]).value(1) # motor1 on 
        machine.Pin(motor_pins[5]).value(0) # motor2 off
    elif sel == 2:
        machine.Pin(motor_pins[4]).value(0) # motor1 off
        machine.Pin(motor_pins[5]).value(1) # motor2 on
    utime.sleep_us(1000)
    # motor pattern    
    motor_bits = beatlist[num]
    # print (motor_bits[0], motor_bits[1], motor_bits[2], motor_bits[3])
    for index in range(0, 4):
        machine.Pin(motor_pins[index]).value(motor_bits[index])
    utime.sleep_us(1000)
    return


def motor_off():
    set_bit(8, 1) # motor1
    set_bit(8, 2) # motor2
    return


def pos_min(mins, secs):
    # rotation for minute intervals
    step_min = int(((mins+(secs/60))*deg_min)*1.42222)
    if step_min > 0:
     # print (step_min)
     for index in range(step_min + 1):
      anti_clkwise(2)
    
    set_bit(8, 2) # motor2 off
    return


def pos_hrs(hrs, mins):
    # rotation for hour intervals
    # step_hrs = int(((hrs+(mins/60))*deg_hr)*1.42222)
    step_hrs = int(((hrs+(0))*deg_hr)*1.42222)
    if step_hrs > 0:
     # print (step_hrs)   
     for index in range(step_hrs + 1):
      anti_clkwise(1)

    set_bit(8, 1) # motor1 off
    return


def spiral_time():
    # update spiral positions
    global diff_hrs, diff_mins, diff_secs
    spiral_hrs = diff_hrs
    if diff_hrs >= 12:
        spiral_hrs = diff_hrs - 12
    pos_hrs(spiral_hrs, diff_mins) 
    pos_min(diff_mins, diff_secs)
    return


def homing():
    # home all motors
    global old_hrs, old_mins, old_secs
    old_hrs = 0
    old_mins = 0
    old_secs = 0
    seeking(1, 0) # motor1
    seeking(2, 2) # motor2
    return


def home_hrs():
    # home hours motor
    old_hrs = 0
    old_mins = 0
    old_secs = 0
    seeking(1, 0)
    return


def home_mins():
    # home minute motor
    old_mins = 0
    old_secs = 0    
    seeking(2, 2) 
    return


def seeking(motor, sensor):
    # ensure spirals are aligned at start up
    global home, sense_pins
    # print ("homing", motor)
    home = False
    timer = utime.ticks_ms()
    index = 0
    # rotate spiral to home position before timeout
    while (machine.Pin(sense_pins[sensor]).value() == 0 and (utime.ticks_ms() - timer) < 60000):
        anti_clkwise(motor)
        index += 1
        if index > 512:
            index = 0
    utime.sleep_ms(100)
    if machine.Pin(sense_pins[sensor]).value() == 1:
        home = True
    motor_off()
    return


def tgraph():
    # display digital time
    global tim, ttim, mim, mtim
    intime = str(tim) + str(ttim) + ":" + str(mim) + str(mtim) 
    disp.text(intime, int(width/6), int(height/4), scale=8)
    return


def hrs_adj():
    global tim, ttim, sum_hrs
    sum_hrs += 1
    if sum_hrs > 24:
        sum_hrs = 0
    tim, ttim = split_time(sum_hrs)
    tgraph()
    disp.update()
    return


def mins_adj():
    global mim, mtim, sum_min
    sum_min += 1
    if sum_min > 59:
        sum_min = 0
    mim, mtim = split_time(sum_min)    
    tgraph()
    disp.update()
    return


def split_time(value):
    if value > 0:
        new_value = str(value)
        if value < 10:
            tens = 0
            unit = new_value[0]
        else:
            tens = new_value[0]
            unit = new_value[1]
    else:
        tens = unit = 0
    return tens, unit


def clean_disp():
    disp.set_pen(black)
    disp.clear()
    disp.update()
    return


def read_rtc():
    global tim, ttim, mim, mtim, rtc_seconds, rtc_minutes, old_hrs, old_mins, old_secs, diff_hrs, diff_mins, zero_mins, zero_hrs
    if rtc.read_periodic_update_interrupt_flag():
        rtc.clear_periodic_update_interrupt_flag()

        if rtc.update_time():
            # rtc_date = rtc.string_date()
            # rtc_time = rtc.string_time()
            rtc_hours = rtc.get_hours()
            rtc_minutes = rtc.get_minutes()
            rtc_seconds = rtc.get_seconds() # used in cycle_led
            # print (rtc_time, rtc_hours, rtc_minutes, rtc_seconds)
            
            diff_hrs = rtc_hours - old_hrs
            zero_hrs = diff_hrs
            if diff_hrs < 0:
                diff_hrs = 0
            if old_hrs != rtc_hours:
                old_hrs = rtc_hours
            
            diff_mins = rtc_minutes - old_mins
            zero_mins = diff_mins            
            if diff_mins <= 0:
                diff_mins = 0
            if old_mins != rtc_minutes:
                old_mins = rtc_minutes
            if zero_mins == -59: 
                if rtc_hours == 0 or rtc_hours == 12:
                    zero_hrs = -23
                       
            diff_secs = rtc_seconds - old_secs
            if diff_secs < 0:
                diff_secs = 0
            if old_secs != rtc_seconds:
                old_secs = rtc_seconds                
            
            # print (diff_hrs, zero_hrs)

            if zero_mins == -59:
                chime(4) # tone on the hour
            
            tim, ttim = split_time(rtc_hours)
            mim, mtim = split_time(rtc_minutes)
    return


def set_time():
    # seconds, minutes, hours, weekday, day, month, year
    # rtc.set_time(55,59,22,0,14,2,2022)
    global sum_min, sum_hrs, old_hrs, old_mins
    rtc.set_time(0, sum_min, sum_hrs, 0, 1, 1, 2023)
    utime.sleep(1)
    # reset old time values
    old_hrs = -1
    old_mins = -1
    homing()
    return


def cycle_led(sec):
    global width, height
    # cycle_led: represents 0 to 60 seconds as a varying horizontal line
    disp.set_clip(0, 120, width, 5)
    tick = int((sec / 60) * width) # horizontal line as a %age of screen width
    disp.pixel_span(0, 120, tick)
    disp.update()
    disp.remove_clip()
    utime.sleep(1)
    return


def chime(count):
    for b in range(1,count):
        for a in range(500, 800):
            buzzer.duty_u16(1000)
            buzzer.freq(a)
            utime.sleep(0.005)
        
        buzzer.duty_u16(0)
        return

def beep():
    buzzer.freq(1000)
    buzzer.duty_u16(1000)
    utime.sleep(0.25)
    buzzer.duty_u16(0)
    return

def menu():
    # button labels
    disp.text("H", 10, 20, 24, 2)   # hours
    disp.text("M", 220, 20, 24, 2)  # minutes
    disp.text("U", 220, 100, 24, 2) # update time
    disp.text("B", 10, 100, 24, 2)  # display mode (blank or time)
    return
   

# initialization
read_rtc()
rtc.enable_periodic_update_interrupt(True)
clean_disp()
disp.set_pen(white)
# menu()
# tgraph()
disp.text("Spiral Time",10,25,220,3)
disp.update()
utime.sleep(2)
clean_disp()
disp.set_pen(green)
disp.text("Please Wait" + " Homing....",10,25,220,3)
disp.update()
homing()
clean_disp()
if home == False:
    disp.set_pen(red)
    disp.text("Homing Failed!",10,50,220,3)
    beep()
else:
    disp.set_pen(green) 
    disp.text("Homing Passed",10,50,220,3)    
disp.update()
utime.sleep(2)

while True:
    if switch.value():         # enable set time
        led_rgb.set_rgb(0, 0, 4) # set mode
        disp.set_pen(blue)
        menu()
        tgraph()
         
        if button_a.read():    # enter hours
            beep()
            clean_disp()
            hrs_adj()
  
        elif button_b.read():
            beep()
            blank = not(blank)
            utime.sleep(1)
           
        elif button_x.read():  # enter minutes
            beep()
            clean_disp() 
            mins_adj()
           
        elif button_y.read():  # set time
            beep()
            set_time()
        
        disp.update()
    
    else:
               
        read_rtc()
        clean_disp()
        if home == True:
            led_rgb.set_rgb(0, 4, 0) # homing good
            if blank == False:                  
                disp.set_pen(green)
        else:
            led_rgb.set_rgb(4, 0, 0) # homing error 
            if blank == False:    
                disp.set_pen(red)            
        menu()
        tgraph()
        disp.update()
        if home == True:
            spiral_time()
            # if zero_hrs == -23:
                # home_hrs() # home hrs at 12 & 24
            if zero_mins == -59:
                # home_hrs()
                # home_mins() # home mins at 60
                homing()
        cycle_led(rtc_seconds)

Credits

C Forde

C Forde

9 projects • 3 followers

Comments