Skip to content

Instantly share code, notes, and snippets.

@HerrRiebmann
Last active April 30, 2022 17:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save HerrRiebmann/10d8e52783ae955aeddcc95243e32d47 to your computer and use it in GitHub Desktop.
Save HerrRiebmann/10d8e52783ae955aeddcc95243e32d47 to your computer and use it in GitHub Desktop.
UV-LED Glow Clock
//#define CALIBRATE
//#define GRID // After Calibration this mode should plot a pretty good dot grid on the screen
// Adjust these values for servo arms in position for state 1 _|
const double SERVO_LEFT_ZERO = 1600;
const double SERVO_RIGHT_SCALE = 690;// + makes rotate further left
// Adjust these values for servo arms in position for state 2 |_
const double SERVO_RIGHT_ZERO = 650;
const double SERVO_LEFT_SCALE = 650;
//#define OPTION_12_HOUR // 12 or comment out this line for 24 hour time
#define OPTION_MONTH_DAY // commented out = month/day or uncomment the line for day/month
const double DRAW_DELAY = 5; // 3
#include <Time.h> // see http://playground.arduino.cc/Code/time
#include <TimeLib.h>
#include <Servo.h> //servo controller
#include <Wire.h>
#include <DS1307RTC.h> // see http://playground.arduino.cc/Code/time
//pins
const int SERVO_LEFT_PIN = 6;
const int SERVO_RIGHT_PIN = 5;
const int LED_PIN = 12;
const int BUTTON_PIN = 7;
const int BUTTON_LED = 3;
//Sizes
const double LOWER_ARM = 35; //servo to lower arm joint
const double UPPER_ARM_LEFT = 56; //lower arm joint to led
const double LED_ARM = 13.5; //upper arm joint to led
const double UPPER_ARM = 45; //lower arm joint to upper arm joint
double cosineRule(double a, double b, double c);
const double LED_ANGLE = cosineRule(UPPER_ARM_LEFT, UPPER_ARM, LED_ARM);
//Location of servos relative to origin
const double SERVO_LEFT_X = 22;
const double SERVO_LEFT_Y = -32;
const double SERVO_RIGHT_X = SERVO_LEFT_X + 25.5;
const double SERVO_RIGHT_Y = SERVO_LEFT_Y;
// lovely macros
#define radian(angle) (M_PI*2* angle)
#define dist(x,y) sqrt(sq(x)+sq(y))
#define angle(x,y) atan2(y,x)
//digit location/size constants
const double TIME_BOTTOM = 12;
const double TIME_WIDTH = 11;
const double TIME_HEIGHT = 18; //16;
const double DAY_WIDTH = 7;
const double DAY_HEIGHT = 12;
const double DAY_BOTTOM = 5;
const double DATE_BOTTOM = 24;
const double HOME_X = 55, HOME_Y = -5;
Servo servoLeft, servoRight;
// Sunday is the first triple
const char weekDays[] = {8, 10, 12, 5, 6, 12, 9, 10, 2, 11, 2, 13, 9, 4, 10, 3, 7, 14, 8, 1, 9}; //character set: AEFHMORSTUWNDI
double lastX = HOME_X, lastY = HOME_Y;
bool lightOn = false;
const int LONG_PRESS_DURATION = 750;
uint8_t fadeValue = 0;
bool fadeRising = true;
long lastfaceMilis = 0;
uint8_t fadingintervall = 10;
uint16_t autoTimeIntervallMin = 60 * 5;
long lastautoTimeMilis = 0;
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
pinMode(BUTTON_PIN, INPUT_PULLUP);
Serial.println("Started");
//Init time once
tmElements_t tmp;
GetTime(tmp);
}
void loop() {
//CheckButton();
GlowButton();
CheckMode();
CheckAutoTime();
}
void CheckAutoTime() {
if (autoTimeIntervallMin == 0)
return;
if(lastautoTimeMilis > 0)
if (!(millis() - lastautoTimeMilis > (long)autoTimeIntervallMin * 1000))
return;
GlowButton(false);
AttachServos();
tmElements_t tm;
GetTime(tm);
drawTo(HOME_X, 0);
PrintTime(tm);
drawTo(HOME_X, HOME_Y);
DetachServos();
lastautoTimeMilis = millis();
}
void GlowButton() {
if (!(millis() - lastfaceMilis > fadingintervall))
return;
if (fadeRising)
fadeValue++;
else
fadeValue--;
if ((fadeRising && fadeValue == 255) ||(!fadeRising && fadeValue == 0))
fadeRising = !fadeRising;
analogWrite(BUTTON_LED, fadeValue);
lastfaceMilis = millis();
}
void GlowButton(bool state) {
fadeValue = state ? 255 : 0;
analogWrite(BUTTON_LED, fadeValue);
}
void CheckMode() {
if (digitalRead(BUTTON_PIN) != LOW)
return;
bool date = CheckLongpressButton();
GlowButton(false);
AttachServos();
#ifdef CALIBRATE
Calibrate();
#else //CALIBRATE
#ifdef GRID
DrawDotGrid();
#else //GRID
//Time or Date
tmElements_t tm;
GetTime(tm);
drawTo(HOME_X, 0);
if (date)
PrintDate(tm);
else
PrintTime(tm);
drawTo(HOME_X, HOME_Y);
#endif
#endif
DetachServos();
}
void AttachServos() {
if (!servoLeft.attached())
servoLeft.attach(SERVO_LEFT_PIN);
if (!servoRight.attached())
servoRight.attach(SERVO_RIGHT_PIN);
}
void DetachServos() {
servoLeft.detach();
servoRight.detach();
}
bool CheckLongpressButton() {
delay(10); // debounce
uint32_t longpress = millis() + LONG_PRESS_DURATION;
while ((!digitalRead(BUTTON_PIN)) && (millis() < longpress))
{ }; // wait
bool longpressed = false;
if (millis() >= longpress)
longpressed = true;
return longpressed;
}
bool GetTime(tmElements_t& tm) {
if (RTC.read(tm)) {
setTime(tm.Hour, tm.Minute, tm.Second, tm.Day, tm.Month, tm.Year);
SerialPrintDate(tm);
return true;
}
else {
Serial.println("RTC read -> false");
servoLeft.detach();
servoRight.detach();
return false;
}
}
void PrintTime(tmElements_t& tm) {
#ifdef OPTION_12_HOUR
if (tm.Hour >= 12) {
tm.Hour = tm.Hour - 12;
}
if (tm.Hour == 0)
tm.Hour = 12;
#endif
//draw hour
if (tm.Hour / 10)
drawDigit(3, TIME_BOTTOM, TIME_WIDTH, TIME_HEIGHT, tm.Hour / 10);
drawDigit(3 + TIME_WIDTH + 3, TIME_BOTTOM, TIME_WIDTH, TIME_HEIGHT, tm.Hour % 10);
// Draw colon
drawDigit((69 - TIME_WIDTH) / 2, TIME_BOTTOM, TIME_WIDTH, TIME_HEIGHT, 11);
//minute
drawDigit(69 - (TIME_WIDTH + 3) * 2, TIME_BOTTOM, TIME_WIDTH, TIME_HEIGHT, tm.Minute / 10);
drawDigit(72 - (TIME_WIDTH + 3), TIME_BOTTOM, TIME_WIDTH, TIME_HEIGHT, tm.Minute % 10);
}
void PrintDate(tmElements_t& tm) {
uint8_t dayOfWeek; // 0 = Sunday, 6 = Saturday
dayOfWeek = dayOfWeek(makeTime(tm)) - 1; // the minus 1 normalized so Sunday = 0
#ifdef OPTION_MONTH_DAY
int temp = tm.Day;
tm.Day = tm.Month;
tm.Month = temp;
#endif
//draw month
if (tm.Month / 10)
drawDigit(70 - (DAY_WIDTH + 3) * 5, DATE_BOTTOM, DAY_WIDTH, DAY_HEIGHT, tm.Month / 10);
drawDigit(70 - (DAY_WIDTH + 3) * 4, DATE_BOTTOM, DAY_WIDTH, DAY_HEIGHT, tm.Month % 10);
// Draw Slash
//drawDigit(70-(DAY_WIDTH+3)*3, DATE_BOTTOM, DAY_WIDTH, DAY_HEIGHT, 12);
// Draw Dot
drawDigit(70 - (DAY_WIDTH + 3) * 3, DATE_BOTTOM, DAY_WIDTH, DAY_HEIGHT, 10);
//day
if (tm.Day / 10) {
drawDigit(70 - (DAY_WIDTH + 3) * 2, DATE_BOTTOM, DAY_WIDTH, DAY_HEIGHT, tm.Day / 10);
drawDigit(70 - (DAY_WIDTH + 3), DATE_BOTTOM, DAY_WIDTH, DAY_HEIGHT, tm.Day % 10);
} else
drawDigit(70 - (DAY_WIDTH + 3) * 2, DATE_BOTTOM, DAY_WIDTH, DAY_HEIGHT, tm.Day % 10);
//weekday
drawChar(5, DAY_BOTTOM, DAY_WIDTH, DAY_HEIGHT, weekDays[dayOfWeek * 3]);
drawChar(5 + DAY_WIDTH + 5, DAY_BOTTOM, DAY_WIDTH, DAY_HEIGHT, weekDays[dayOfWeek * 3 + 1]);
drawChar(5 + (DAY_WIDTH + 5) * 2, DAY_BOTTOM, DAY_WIDTH, DAY_HEIGHT, weekDays[dayOfWeek * 3 + 2]);
}
void Calibrate() {
Serial.println("Calibrate");
// Pressing the button alternates the servo arms between 2 states.
// State one if left arm pointing to 9 o'clock and right arm pointing to 12 o'clock _|
// State two if left arm pointing to 12 o'clock and right arm pointing to 3 o'clock |_
// At the very top of the code you adjust the 4 constants to get the arms into these exact positions.
// Adjust SERVO_LEFT_ZERO so that the left servo points to 9 o'clock when in state one
// Adjust SERVO_RIGHT_SCALE so that the right servo points to 12 o'clock when in state one
// Adjust SERVO_RIGHT_ZERO so that the right servo points to 3 o'clock when in state two
// Adjust SERVO_LEFT_SCALE so that the left servo points to 12 o'clock when in state two
static bool half;
servoLeft.writeMicroseconds(floor(SERVO_LEFT_ZERO + (half ? - M_PI / 2 : 0) * SERVO_LEFT_SCALE ));
Serial.print("Left:\t");
Serial.println(floor(SERVO_LEFT_ZERO + (half ? - M_PI / 2 : 0) * SERVO_LEFT_SCALE ));
servoRight.writeMicroseconds(floor(SERVO_RIGHT_ZERO + (half ? 0 : M_PI / 2 ) * SERVO_RIGHT_SCALE ));
light(half ? LOW : HIGH);
half = !half;
delay(2000);
}
void DrawDotGrid() {
Serial.println("Grid");
for (int i = 0; i <= 70; i += 10)
for (int j = 0; j <= 40; j += 10) {
drawTo(i, j);
light(HIGH);
light(LOW);
}
return;
}
void SerialPrintDate(tmElements_t tm) {
//tm.Hour,tm.Minute,tm.Second,tm.Day,tm.Month,tm.Year
Serial.print(tm.Hour);
Serial.print(":");
Serial.print(tm.Minute);
Serial.print(":");
Serial.print(tm.Second);
Serial.print(" ");
Serial.print(tm.Day);
Serial.print(" ");
Serial.print(tm.Month);
Serial.print(" ");
Serial.println(tm.Year);
}
bool lastBtnState = false;
void CheckButton() {
bool state = !digitalRead(BUTTON_PIN);
if (lastBtnState != state) {
lastBtnState = !lastBtnState;
delay(50);
if (lastBtnState) {
//Serial.println("Button pressed!");
light(HIGH);
}
else {
light(LOW);
Serial.println("Button released!");
}
}
}
void light(bool state) {
lightOn = state;
delay(100);
digitalWrite(LED_PIN, state);
}
#define digitMove(dx, dy) drawTo(x + width*dx, y + height*dy)
#define digitStart(dx, dy) digitMove(dx, dy); light(HIGH)
#define digitArc(dx, dy, rx, ry, start, last) drawArc(x + width*dx, y + height*dy, width*rx, height*ry, radian(start), radian(last))
// Symbol is drawn with the lower left corner at (x,y) and a size of (width,height).
void drawDigit(double x, double y, double width, double height, char digit) {
//see macros for reference
switch (digit) {
case 0: //
digitStart(1 / 2, 1);
digitArc(1 / 2, 1 / 2, 1 / 2, 1 / 2, 1 / 4, -3 / 4);
//digitStart(1,1/2);
//digitArc(1/2,1/2, 1/2,1/2, 0, 1.02);
break;
case 1: //
digitStart(1 / 4, 7 / 8);
digitMove(1 / 2, 1);
digitMove(1 / 2, 0);
break;
case 2: //
digitStart(0, 3 / 4);
digitArc(1 / 2, 3 / 4, 1 / 2, 1 / 4, 1 / 2, -1 / 8);
digitArc(1, 0, 1, 1 / 2, 3 / 8, 1 / 2);
digitMove(1, 0);
break;
case 3:
digitStart(0, 3 / 4);
digitArc(1 / 2, 3 / 4, 1 / 2, 1 / 4, 3 / 8, -1 / 4);
digitArc(1 / 2, 1 / 4, 1 / 2, 1 / 4, 1 / 4, -3 / 8);
break;
case 4:
digitStart(1, 3 / 8);
digitMove(0, 3 / 8);
digitMove(3 / 4, 1);
digitMove(3 / 4, 0);
break;
case 5: //wayy too many damn lines
digitStart(1, 1);
digitMove(0, 1);
digitMove(0, 1 / 2);
digitMove(1 / 2, 1 / 2);
digitArc(1 / 2, 1 / 4, 1 / 2, 1 / 4, 1 / 4, -1 / 4);
digitMove(0, 0);
break;
case 6:
digitStart(0, 1 / 4);
digitArc(1 / 2, 1 / 4, 1 / 2, 1 / 4, 1 / 2, -1 / 2);
digitArc(1, 1 / 2, 1, 1 / 2, 1 / 2, 1 / 4);
break;
case 7:
digitStart(0, 1);
digitMove(1, 1);
digitMove(1 / 4, 0);
break;
case 8:
digitStart(1 / 2, 1 / 2);
digitArc(1 / 2, 3 / 4, 1 / 2, 1 / 4, -1 / 4, 3 / 4);
digitArc(1 / 2, 1 / 4, 1 / 2, 1 / 4, 1 / 4, -3 / 4);
break;
case 9:
digitStart(1, 3 / 4);
digitArc(1 / 2, 3 / 4, 1 / 2, 1 / 4, 0, 1);
digitMove(3 / 4, 0);
break;
case 10: //dot
digitStart(0, 0);
//digitMove(0,1);
//digitMove(1,1);
//digitMove(1,0);
break;
case 11: //colon
digitStart(1 / 2, 3 / 4);
light(LOW);
digitStart(1 / 2, 1 / 4);
break;
case 12: //slash
digitStart(3 / 4, 5 / 4);
digitMove(1 / 4, -1 / 4);
break;
}
light(LOW);
}
void drawChar(double x, double y, double width, double height, char digit) {
//see macros for reference
switch (digit) {
//letters for the day of the week
case 1: //A
digitStart(0, 0);
digitMove(1 / 2, 1);
digitMove(1, 0);
light(LOW);
digitStart(1 / 4, 1 / 2);
digitMove(3 / 4, 1 / 2);
break;
case 2: //E
digitStart(1, 0);
digitMove(0, 0);
digitMove(0, 1);
digitMove(1, 1);
light(LOW);
digitStart(0, 1 / 2);
digitMove(1, 1 / 2);
break;
case 3: //F
digitStart(0, 0);
digitMove(0, 1);
digitMove(1, 1);
light(LOW);
digitStart(0, 1 / 2);
digitMove(1, 1 / 2);
break;
case 4: //H
digitStart(0, 1);
digitMove(0, 0);
light(LOW);
digitStart(0, 1 / 2);
digitMove(1, 1 / 2);
light(LOW);
digitStart(1, 1);
digitMove(1, 0);
break;
case 5: //M
digitStart(0, 0);
digitMove(0, 1);
digitMove(1 / 2, 1 / 2);
digitMove(1, 1);
digitMove(1, 0);
break;
case 6: //O (0)
digitStart(1, 1 / 2);
digitArc(1 / 2, 1 / 2, 1 / 2, 1 / 2, 0, 1.02);
break;
case 7: //R
digitStart(0, 0);
digitMove(0, 1);
digitMove(1 / 2, 1);
digitArc(1 / 2, 3 / 4, 1 / 2, 1 / 4, 1 / 4, -1 / 4);
digitMove(0, 1 / 2);
digitMove(1, 0);
break;
case 8: //S
digitStart(0, 0);
digitMove(1 / 2, 0);
digitArc(1 / 2, 1 / 4, 1 / 2, 1 / 4, -1 / 4, 1 / 4);
digitArc(1 / 2, 3 / 4, 1 / 2, 1 / 4, 3 / 4, 1 / 4);
digitMove(1, 1);
break;
case 9: //T
digitStart(1, 1);
digitMove(-1 / 2, 1); //bad
light(LOW);
digitStart(1 / 2, 1);
digitMove(1 / 2, 0);
break;
case 10: //U
digitStart(0, 1);
digitMove(0, 1 / 4);
digitArc(1 / 2, 1 / 4, 1 / 2, 1 / 4, -1 / 2, 0);
digitMove(1, 1);
break;
case 11: //W
digitStart(0, 1);
digitMove(0, 0);
digitMove(1 / 2, 1 / 2);
digitMove(1, 0);
digitMove(1, 1);
break;
case 12: //N
digitStart(0, 0);
digitMove(0, 1);
digitMove(1, 0);
digitMove(1, 1);
break;
case 13: //D
digitStart(0, 0);
digitMove(0, 1);
digitMove(1 / 2, 1);
digitArc(1 / 2, 1 / 2, 1 / 2, 1 / 2, 1 / 4, -1 / 4);
digitMove(0, 0);
break;
case 14: //I
digitStart(1 / 2, 1);
digitMove(1 / 2, 0);
light(LOW);
digitStart(0, 0);
digitMove(1, 0);
light(LOW);
digitStart(1, 1);
digitMove(0, 1);
break;
}
light(LOW);
}
#define ARCSTEP 0.05 //0.05 //should change depending on radius...
void drawArc(double x, double y, double rx, double ry, double pos, double last) {
if (pos < last)
for (; pos <= last; pos += ARCSTEP)
drawTo(x + cos(pos) * rx, y + sin(pos) * ry);
else
for (; pos >= last; pos -= ARCSTEP)
drawTo(x + cos(pos) * rx, y + sin(pos) * ry);
}
//didn't really change this
void drawTo(double pX, double pY) {
double dx, dy, c;
int i;
// dx dy of new point
dx = pX - lastX;
dy = pY - lastY;
//path length in mm, times 4 equals 4 steps per mm
c = floor(4 * dist(dx, dy));
if (c < 1)
c = 1;
// draw line point by point
for (i = 1; i <= c; i++) {
set_XY(lastX + (i * dx / c), lastY + (i * dy / c));
if (lightOn)
delay(DRAW_DELAY);
}
lastX = pX;
lastY = pY;
}
// cosine rule for angle between c and a
double cosineRule(double a, double b, double c) {
return acos((sq(a) + sq(c) - sq(b)) / (2 * a * c));
}
void set_XY(double x, double y) {
//Calculate triangle between left servo, left arm joint, and light
//Position of pen relative to left servo
//rectangular
double penX = x - SERVO_LEFT_X;
double penY = y - SERVO_LEFT_Y;
//polar
double penAngle = angle(penX, penY);
double penDist = dist(penX, penY);
//get angle between lower arm and a line connecting the left servo and the pen
double bottomAngle = cosineRule(LOWER_ARM, UPPER_ARM_LEFT, penDist);
servoLeft.writeMicroseconds(floor(SERVO_LEFT_ZERO + (bottomAngle + penAngle - M_PI) * SERVO_LEFT_SCALE));
//calculate middle arm joint location
double topAngle = cosineRule(UPPER_ARM_LEFT, LOWER_ARM, penDist);
double lightAngle = penAngle - topAngle + LED_ANGLE + M_PI;
double jointX = x - SERVO_RIGHT_X + cos(lightAngle) * LED_ARM;
double jointY = y - SERVO_RIGHT_Y + sin(lightAngle) * LED_ARM;
bottomAngle = cosineRule(LOWER_ARM, UPPER_ARM, dist(jointX, jointY));
double jointAngle = angle(jointX, jointY);
servoRight.writeMicroseconds(floor(SERVO_RIGHT_ZERO + (jointAngle - bottomAngle) * SERVO_RIGHT_SCALE));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment