A mechanical digital clock with twisting layers
W konkursie Timekeepers
674
548
0
19 k
zaktualizowano 27 czerwca 2021

Opis

PDF

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.

 

3D-printed parts
PartQuantity
B14
B212
B34
B416
B54
C116
C24
D0004
D0014
D12
D1008
D10116
D11112
Planet Gear60
Sun Gear20

 

Non 3d printed parts
PartQuantity
SG90 9g Micro Servo20
1.7 mm diameter  steel shaft ( I got mine from an old umbrella 😊 )12 *180 mm
Wafer head screw 4.2 x 25 mm8
Pan Head Self Tapping Screw M3  x 10-12 mm72
Arduino MEGA1
DS3231 Real Time Clock1
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 cables20

 

Programming

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();
}
Print Instructions

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.

 

Assembly Instructions
  • Slide on the digits segments D to the triangular parts C1 and C2 according to this sketch. You shouldn’t need any glue. But if it’s too loose just add a tiny amount af glue to prevent it from sliding out.

     

 

  • Attach a micro servo to part B1.
     
  • Mount part B1 to the base A1 using two wafer head screws.
  • Attach steel shafts and slide on the planet gears.
  • Press the servo horn into the Sun gear. No glue needed.
  • Attach the Sun gear to the micro servo.
  • Slide on part B2. Do not glue.
  • Place the appropriate triangular digits section on top. To make it easier to calibrate later, run the program servopos.ino.
    Connect the servo temporalily to pin 45. Rotate the layer with the rotary encoder and monitor the value with the serial monitor in the arduiono IDE. Position the face that corresponds to digit zero at front. The value should be approximately 90. Make sure you can rotate the layer to front all three faces.
  • Attach a micro servo to part B2 and slide it on. The wires from the micro servo should be inserted through one of the two holes down to the base. Secure it with small screws.
  • Continue in the same way with the other layers.
  • Finish with part B5.
  • Trim the steel shafts. They must not protrude above part B5.
  • Repeat the above procedure for the other digits.
  • Connect parts D1 and E1-4 and attach it to the base A3.
  • Connect the base parts A1-A5. It’s a tight fit. No glue.

 

Operating instructions

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[][].
 

Nagrodzony w konkursie


3
Timekeepers
188 wpisów | 19 maja – 27 czerwca 2021

Pochodzenie modelu

Autor nie podał jeszcze pochodzenia modelu.

Licencja