Time Twister is a fully 3d printed mechanical digital clock.
The digits are produced by twisting the layers to front one of three faces.
Each layer is powered by a micro servo motor.
The servo motors are controlled by an Arduino MEGA.
A DS3231 Real Time Clock makes sure that the time is right.
Part | Quantity |
---|---|
B1 | 4 |
B2 | 12 |
B3 | 4 |
B4 | 16 |
B5 | 4 |
C1 | 16 |
C2 | 4 |
D000 | 4 |
D001 | 4 |
D1 | 2 |
D100 | 8 |
D101 | 16 |
D111 | 12 |
Planet Gear | 60 |
Sun Gear | 20 |
Part | Quantity |
---|---|
SG90 9g Micro Servo | 20 |
1.7 mm diameter steel shaft ( I got mine from an old umbrella 😊 ) | 12 *180 mm |
Wafer head screw 4.2 x 25 mm | 8 |
Pan Head Self Tapping Screw M3 x 10-12 mm | 72 |
Arduino MEGA | 1 |
DS3231 Real Time Clock | 1 |
Power Supply 5V (2A minimum) | 1 |
KY-040 Rotary encoder (optional , for calibration) | 1 |
Sensor shield (makes it easier to connect the servos) | 1 |
servo extension cables | 20 |
timetwister.ino:
#define TRANSITION_DURATION_MS 1000
//Rotary encoder
#define CLK_PIN 2
#define DT_PIN 3
#define SW_PIN 4
//DS3231
//SDA pin 20
//SCL pin 21
#define SERVO_PIN_START 22 //22 - 41, 22 = ten hour top layer, 23 = ten hour second layer and so on
#include <EEPROM.h>
#include <Servo.h>
#include <DS3231.h>
const byte digitMap[10][5] = {
{1,1,1,1,1}, //0
{0,2,0,2,0}, //1
{1,2,2,0,1}, //2
{1,2,2,2,1}, //3
{2,1,2,2,0}, //4
{1,0,2,2,1}, //5
{1,0,2,1,1}, //6
{1,2,0,2,0}, //7
{1,1,2,1,1}, //8
{1,1,2,2,1} //9
};
//calibration limits
const int servo_pos_face_min[3] = {0,50,140};
const int servo_pos_face_max[3] = {50,130,180};
const byte magic_number = 42;
const int num_servos = 20;
const int write_delay = 20;
const int rotary_ratio = 2; //number of pulses from rotary encoder to change servo position by 1 step
byte servo_target_pos[num_servos][3] = { //Adjust here if you don't have a rotary encoder
{10,90,170},
{10,90,170},
{10,90,170},
{10,90,170},
{10,90,170},
{10,90,170},
{10,90,170},
{10,90,170},
{10,90,170},
{10,90,170},
{10,90,170},
{10,90,170},
{10,90,170},
{10,90,170},
{10,90,170},
{10,90,170},
{10,90,170},
{10,90,170},
{10,90,170},
{10,90,170}
};
DS3231 rtc(SDA, SCL);
byte servo_current_pos[num_servos] = {90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90,90};
byte servo_new_pos[num_servos];
enum Mode {MODE_TIME = 0, MODE_CALIBRATION = 1};
Mode mode;
enum Transition {TRANSITION_SINGLE = 0, TRANSITION_ALL = 1};
class MyServo: public Servo {
public:
void moveTo(int pos)
{
int old_pos = read();
for(int i=0; i<TRANSITION_DURATION_MS; i+=write_delay)
{
write(map(i,0,TRANSITION_DURATION_MS,old_pos,pos));
delay(write_delay);
}
write(pos);
}
};
MyServo servo[num_servos];
void loadServoPositions() //Load calibration and current servo positions
{
int address = 0;
if(EEPROM.read(address++) == magic_number)
{
for(int i = 0; i < num_servos; i++)
for(int j = 0; j < 3; j ++)
servo_target_pos[i][j] = EEPROM.read(address++);
for(int i = 0; i < num_servos; i++)
servo_current_pos[i] = EEPROM.read(address++);
}
for(int i=0;i<num_servos;i++)
servo[i].write(servo_current_pos[i]); //write before attach to prevent servo twitch
}
void saveServoPositions() //Save calibration and current servo positions
{
int address = 0;
EEPROM.update(address++, magic_number);
for(int i = 0; i < num_servos; i++)
for(int j = 0; j < 3; j ++)
EEPROM.update(address++, servo_target_pos[i][j]);
for(int i = 0; i < num_servos; i++)
EEPROM.update(address++, servo_current_pos[i]);
}
void reset() //When long pressing button while in calibration mode
{
//set rough default values
for(int i=0;i<num_servos;i++)
{
servo_target_pos[i][0]=10;
servo_target_pos[i][1]=90;
servo_target_pos[i][2]=170;
servo[i].attach(i+SERVO_PIN_START);
servo[i].moveTo(90);
delay(500);
servo[i].detach();
}
}
class Clock
{
unsigned long old_ms;
int current_hour = -1;
int current_min = -1;
public:
void displayTime(Transition transition, int h, int m)
{
int ten_hour = h/10;
int hour = h%10;
int ten_min = m/10;
int min = m%10;
for (int i=0;i < num_servos;i++)
servo_current_pos[i] = servo[i].read();
for (int row=0;row<5;row++)
{
servo_new_pos[row] = servo_target_pos[row][digitMap[ten_hour][row]];
servo_new_pos[5 + row] = servo_target_pos[5 + row][digitMap[hour][row]];
servo_new_pos[10 + row] = servo_target_pos[10 + row][digitMap[ten_min][row]];
servo_new_pos[15 + row] = servo_target_pos[15 + row][digitMap[min][row]];
}
switch(transition)
{
case TRANSITION_SINGLE:
for(int i=0;i<num_servos;i++)
{
if(servo_current_pos[i] != servo_new_pos[i])
{
servo[i].attach(i+SERVO_PIN_START);
servo[i].moveTo(servo_new_pos[i]);
delay(500);
servo_current_pos[i] = servo_new_pos[i];
servo[i].detach();
}
}
break;
case TRANSITION_ALL:
for (int i=0; i < num_servos; i++)
if (servo_current_pos[i] != servo_new_pos[i])
servo[i].attach(SERVO_PIN_START+i);
for(int ms=0; ms<TRANSITION_DURATION_MS; ms+=write_delay)
{
for (int i=0; i < num_servos; i++)
if (servo_current_pos[i] != servo_new_pos[i])
servo[i].write(map(ms,0,TRANSITION_DURATION_MS,servo_current_pos[i],servo_new_pos[i]));
delay(write_delay);
}
for (int i=0; i<num_servos; i++)
if (servo_current_pos[i] != servo_new_pos[i])
servo[i].write(servo_new_pos[i]);
delay(500);
for (int i=0;i<num_servos;i++)
if (servo_current_pos[i] != servo_new_pos[i])
{
servo_current_pos[i] = servo_new_pos[i];
servo[i].detach();
}
break;
}
saveServoPositions();
}
void setup()
{
rtc.begin();
}
void loop()
{
if (mode == MODE_TIME)
if (millis() - old_ms > 1000)
{
old_ms = millis();
Time t = rtc.getTime();
if (t.min != current_min || t.hour != current_hour)
{
current_hour = t.hour;
current_min = t.min;
displayTime(TRANSITION_ALL, current_hour, current_min);
}
}
}
};
Clock clock;
class RotaryEncoder {
const byte pin_clk, pin_dt, pin_sw;
int previous_clk; int sw_state;
unsigned long button_down_ms;
unsigned long servo_ms;
int counter=0;
int servo_index = -1;
int face = -1;
public:
RotaryEncoder(byte clk_attach_to, byte dt_attach_to, byte sw_attach_to) :
pin_clk(clk_attach_to), pin_dt(dt_attach_to), pin_sw(sw_attach_to)
{
}
void setup() {
pinMode(pin_clk,INPUT);
pinMode(pin_dt,INPUT);
pinMode(pin_sw,INPUT_PULLUP);
sw_state = HIGH;
}
void loop()
{
int prev_sw_state = sw_state;
sw_state = digitalRead(pin_sw);
if(prev_sw_state == HIGH && sw_state == LOW)
{
button_down_ms = millis();
}
else if(prev_sw_state == LOW)
{
int button_time = millis() - button_down_ms;
if (sw_state == HIGH) // release
{
if(button_time > 50 && button_time < 2000) //short click
{
if(mode == MODE_CALIBRATION)
{
face = ++face%3;
if (face == 0)
{
if (servo_index > -1)
{
servo[servo_index].moveTo(servo_target_pos[servo_index][1]);
delay(500);
servo[servo_index].detach();
}
servo_index++;
if(servo_index == num_servos)
{
servo_index=0;
mode = MODE_TIME;
return;
}
servo[servo_index].attach(servo_index+SERVO_PIN_START);
}
servo[servo_index].moveTo(servo_target_pos[servo_index][face]);
delay(500);
}
}
}
else if (button_time > 2000) //hold
{
if (mode == MODE_TIME)
{
clock.displayTime(TRANSITION_SINGLE,0,0);
mode = MODE_CALIBRATION;
}
else if (mode == MODE_CALIBRATION)
reset();
while (digitalRead(pin_sw) == LOW);
servo_index = -1;
face = -1;
prev_sw_state = HIGH; //prevent shortclick upon release
}
}
if (mode == MODE_CALIBRATION)
{
int current_clk = digitalRead(pin_clk);
if (current_clk != previous_clk)
{
if (digitalRead(pin_dt) != current_clk)
counter--;
else
counter++;
if (abs(counter) == rotary_ratio)
{
if (counter > 0 && servo_target_pos[servo_index][face] <= servo_pos_face_max[face])
servo_target_pos[servo_index][face]++;
else if(counter < 0 && servo_target_pos[servo_index][face] >= servo_pos_face_min[face])
servo_target_pos[servo_index][face]--;
counter = 0;
}
servo[servo_index].write(servo_target_pos[servo_index][face]);
servo_ms = millis();
}
previous_clk = current_clk;
}
}
};
RotaryEncoder rotaryEncoder(CLK_PIN,DT_PIN,SW_PIN);
void setup() {
clock.setup();
rotaryEncoder.setup();
loadServoPositions();
mode = MODE_TIME;
}
void loop() {
clock.loop();
rotaryEncoder.loop();
}
servopos.ino:
#define CLK_PIN 2
#define DT_PIN 3
#define SW_PIN 4
#define SERVO_PIN 45
#include <Servo.h>
class MyServo: public Servo {
public:
void moveTo(int pos)
{
int old_pos = read();
for(int i=0;i<500;i+=20)
{
write(map(i,0,500,old_pos,pos));
delay(20);
}
write(pos);
}
};
MyServo servo;
class RotaryEncoder {
const byte pin_clk, pin_dt, pin_sw;
int previous_clk;
unsigned long servo_ms;
int counter=90;
public:
RotaryEncoder(byte clk_attach_to, byte dt_attach_to, byte sw_attach_to) :
pin_clk(clk_attach_to), pin_dt(dt_attach_to), pin_sw(sw_attach_to)
{
}
void setup() {
pinMode(pin_clk,INPUT);
pinMode(pin_dt,INPUT);
pinMode(pin_sw,INPUT_PULLUP);
}
void loop() {
int current_clk = digitalRead(pin_clk);
if(current_clk != previous_clk)
{
if(digitalRead(pin_dt) != current_clk)
{
counter = max(--counter,0);
}
else
{
counter = min (++counter,180);
}
Serial.println(counter);
servo.write(counter);
}
previous_clk = current_clk;
}
};
RotaryEncoder rotaryEncoder(CLK_PIN,DT_PIN,SW_PIN);
void setup() {
Serial.begin(9600);
servo.attach(SERVO_PIN);
rotaryEncoder.setup();
servo.write(90);
}
void loop() {
rotaryEncoder.loop();
}
I printed all parts with 0.3 mm layer height.
No support is needed.
For the base I used Prusament PLA Viva La Bronze.
For the digits I used eSUN eSilk-PLA Filament Jacinth.
The digits should have a color change at 2 mm.
First start.
The faces of the digits should be 00:00 as described in the Assembly Instructions.
Run the program timetwister.ino.
If you have a rotary encoder (recommended), press and hold the button for two secconds until the servos starts to position each layer and gets into calibration mode.
Click the button and calibrate by twisting the rotary encoder.
Click the button to continue with the next face.
When the calibration is done, the program starts to show the time.
The calibration and current positions of the servos are stored i eeprom, so you just have to do it once.
If you don't have a rotare encoder, you can alter the source code and adjust the values of servo_target_pos[][].
Autor nie podał jeszcze pochodzenia modelu.