Arduino Build: Restroom Clock

en in code • 27 min read

Our flat has a clock in every single room but the restroom. My wife quickly fixed that issue with an old wireless clock with a meteorological station that was not working anymore. The clock was old, and not even a fresh battery could do the trick. But when the clock was working, we even knew how hot it was in there!

Anyway, it was a clear sign for an Arduino project. After the last failed attempt to make wireless dim light, I decided to combine the light and clock build together and put it into the plug instead.

Time sync

Displaying anything is a relatively easy thing to do. The tricky part, in this case, is how to sync the time. Arduino has only time since it was booted, no info about the real-time.

One way to solve this issue is using a Wi-Fi module and syncing time with the internet. I bought a few ESP8266 ESP-01 modules just for fun. They are super cheap. The issue is it’s also extremely difficult to make them work with Arduino. I spent countless hours without having anything stable before switching to the NodeMCU CP2102 board, which has an integrated Wi-Fi module. It works like a charm! At least for my meteorological stations. I didn’t have more NodeMCU boards, so I took it as a chance to play with different challenges: how to do it without the internet?

The answer is clear. I guess we all know how many kitchen appliances with time we have around when there is a time shift to or from DST or when we experience a power outage. Arduino counts the time since it started, and I just need to store somewhere information about the time when the clock was plugged in. I can compute the time like any microwave can with those two variables.

The problem occurs with the millis() function: it overflows roughly after 49 days and 17 hours. If we don’t take that into account, then the clock needs to be corrected every two months instead of twice a year. I solved it with a custom seconds counter. The crucial is how to check delays. It is best if you always do it in this form, only that way you can avoid overflow issue: (millis() - prevMillis) > delay. (At least if the delay is shorter than 50 days!)

Here is the corresponding piece of code to keep proper time offset:

#define MAX_SEC_DAY 86400

long setTimeOffset = 0; // Set by user.
long dayTimeOffset = 0; // Set automatically using millis.
unsigned long previousMillis = 0;

void updateDayTimeOffset() {
    unsigned long currentMillis = millis();
    if ((currentMillis - previousMillis) >= 1000) {
        previousMillis += 1000;
        dayTimeOffset += 1;
    }
    dayTimeOffset = dayTimeOffset % MAX_SEC_DAY;
}

That can be then printed with the following code:

void printTime() {
    long time = (setTimeOffset + dayTimeOffset) % MAX_SEC_DAY;
    int h = time / 3600;
    int m = (time/60) % 60;
    int s = time % 60;

    // ...
}

Time acceleration

I was surprised it really worked. After two months, no overflow issue happened, the time continued flawlessly. Except, every day, the time was more and more off, about 13 seconds. In two months, our time was approximately 13 minutes ahead. Thanks to the pandemic, we are always home, so we can adjust our brain every day (slowly boiled frog), but after two weeks long vacation, I could be afraid I’m two minutes late and need to force myself to poop faster! Not very useful.

I guess the crystal or ceramic resonator speed can differ for every Arduino, so I fixed it in my program with some variables to make it a bit universal. For 13 seconds a day, it means I need to slow it down by a second every 106 minutes (or 6360 seconds).

#define CORRECTION_EVERY_SEC 6360
#define CORRECTION_SEC -1

long correctionCounter = 0;

// Added to the updateDayTimeOffset:
if ((currentMillis - previousMillis) >= 1000) {
    correctionCounter += 1;
}
if (correctionCounter == CORRECTION_EVERY_SEC) {
    correctionCounter = 0;
    dayTimeOffset += CORRECTION_SEC;
}

Time settings

Time is ready! But with no option to manually set it. I wanted to create something better than found elsewhere because I often struggle with a manual. Clock settings are often soo unfriendly!

I thought having only one button had to be simple enough. Well, the whole thing is more challenging than I thought! But, even though I never tested my system on anyone else, I believe a one-button solution works excellently.

Here is the UX: short press does nothing by default. In other words: there is no way to change time accidentally. Long press (for one second) enters the user into settings mode, first the hour mode. Short press now increases hour by one. Once the requested hour is set, the long press switches mode to minute instead. The same repeats for minutes, and the last long-press exits the settings. The active mode is visible on display.

Written like that seems an easy task, but it took me some time to fine-tune the code! But I didn’t want to give up. I wanted to use the least components possible.

I found myself sometimes pressing the button like crazy because I could only increment the value, but still, I liked that I could fit the whole thing on a tiny board. Also, there is no confusion about what needs to be pressed, and there are only two options: short and long press, which sounds reasonably straightforward.

if (lastButtonPress > 0 && (millis() - lastButtonPress) >= BUTTON_LONG_PRESS_MS) {
    settingsMode += 1;
    if (settingsMode > 2) {
        settingsMode = 0;
    }
    lastButtonPress = 0;
    render();
}

int buttonState = digitalRead(PIN_BUTTON);
if (buttonState != prevButtonState) {
    prevButtonState = buttonState;
    if (buttonState == HIGH) {
        lastButtonPress = millis();
    } else {
        if (lastButtonPress > 0 && (millis() - lastButtonPress) < BUTTON_LONG_PRESS_MS) {
            if (settingsMode == 1) {
                setTimeOffset += 3600;
                render();
            } else if (settingsMode == 2) {
                setTimeOffset += 60;
                render();
            }
            if (setTimeOffset > MAX_SEC_DAY) {
                setTimeOffset = setTimeOffset % MAX_SEC_DAY;
            }
            lastButtonPress = 0;
        }
    }
}

Displaying time

Time is set! Now, how to display it? I used a 128x64 OLED display connected over I2C. I could use a 7-segment display, but you usually get it as 4 or 8 digits, yet I wanted 6 (hour + minute + seconds), and I wanted some way to display also settings mode. With OLED, it’s just nicer. And with the U8glib library, it’s super easy to do:

#include "U8glib.h"
U8GLIB_SSD1306_128X64 display(U8G_I2C_OPT_NONE);

void render() {
    display.firstPage();
    do {
        printTime();
    } while( display.nextPage() );
}

void printTime() {
    ...

    display.setFont(u8g_font_fub25r);
    printNumber(h);
    display.print(":");
    printNumber(m);
    display.setFont(u8g_font_fub20r);
    display.print(":");
    printNumber(s);
}

void printNumber(int number) {
    if (number < 10) {
        display.print("0");
    }
    display.print(number);
}

There is one thing to be aware of: the library does not render the whole screen in one go. In my case, it updates in eight cycles, but it is different for every specific display type. That means if your variables can change during rendering, you can see a partially wrong result. It happened to me with the seconds. If the render occurred on the edge of two seconds, the top of the number was the previous one, and the bottom part was the new one. Such output doesn’t seem professional! Therefore, it is better first to calculate variables and then do the render (at least it will save some time computing it all over again).

The other issue I encountered was that I rendered the display in a super-fast loop and fried it by mistake, even though it was rendering a blank screen. After a few days, it stayed black. Be careful how often you do the render! It’s a nasty bug that is hard to spot until it’s too late.

Motion detector with light

Finally, it is time to connect the motion detector and light. It’s nothing fancy; just turn on the dim light and show the time when someone enters the room. I used a white LED that is dim enough yet contains too many unwanted blue waves, which I fixed with a thin colored paper.

In the corridor, we have a bulb with a motion detector, and it always takes several attempts before it turns off because when the light goes off, the motion detector sees that as a change and lights it up again. Quite frustrating, to be honest. I was thinking about how to avoid this issue in my build.

I found out that it is pretty easy. The essential part is not having the light close to the motion detector. I tilted the sensor a bit behind the LED, and all was good. This way, light illuminates the wall producing nice dim light, and the motion detector looks for any movement in front of the door.

The last hack I did was to keep it compact: use 5V from digital pins to not need to solder the build or to have too many cables around. It all fits nicely on the electrical adapter I happen to have. By the way, this adapter can provide juice for Arduino for up to five seconds, enough to survive a minor power outage. A good thing to have for the build where the internal state is set manually.

The full code:

#include "U8glib.h"
U8GLIB_SSD1306_128X64 display(U8G_I2C_OPT_NONE);

#define PIN_BUTTON 13
#define PIN_MOVE 11
#define PIN_LIGHT 2
#define AWAKE_TIME_MS 60000
#define BUTTON_LONG_PRESS_MS 1000
#define MAX_SEC_DAY 86400

// Arduino clock might run faster or slower than the real clocks.
// In my case, I have to slow it down by a sec every 106 minutes.
#define CORRECTION_EVERY_SEC 6360 // 106 minutes.
#define CORRECTION_SEC -1

unsigned long lastDetectedMove = 0;
unsigned long lastButtonPress = 0;
unsigned long lastRender = 0;
int prevButtonState = LOW;
int settingsMode = 0; // 0: nothing, 1: hour, 2: minute

long setTimeOffset = 0; // Set by user.
long dayTimeOffset = 0; // Set automatically using millis.
long correctionCounter = 0; // Set automatically using millis.
unsigned long previousMillis = 0;

void setup() {
    Serial.begin(9600);
    pinMode(PIN_BUTTON, INPUT);
    pinMode(PIN_MOVE, INPUT);
    pinMode(PIN_LIGHT, OUTPUT);
    digitalWrite(PIN_LIGHT, HIGH);

    // Hack: 5V on pins.
    pinMode(3, OUTPUT);
    pinMode(10, OUTPUT);
    digitalWrite(3, HIGH);
    digitalWrite(10, HIGH);
}

void loop() {
    updateDayTimeOffset();

    bool moveDetected = digitalRead(PIN_MOVE) == 1;
    if (moveDetected) {
        lastDetectedMove = millis();
    }
    if (lastDetectedMove == 0) {
        delay(10);
        return;
    }

    bool isAwake = (lastDetectedMove + AWAKE_TIME_MS) > millis();
    if (!isAwake) {
        lastDetectedMove = 0;
        digitalWrite(PIN_LIGHT, LOW);
        clearOLED();
        delay(10);
        return;
    }
    digitalWrite(PIN_LIGHT, HIGH);

    if (lastButtonPress > 0 && (millis() - lastButtonPress) >= BUTTON_LONG_PRESS_MS) {
        settingsMode += 1;
        if (settingsMode > 2) {
            settingsMode = 0;
        }
        lastButtonPress = 0;
        render();
    }

    int buttonState = digitalRead(PIN_BUTTON);
    if (buttonState != prevButtonState) {
        prevButtonState = buttonState;
        if (buttonState == HIGH) {
            lastButtonPress = millis();
        } else {
            if (lastButtonPress > 0 && (millis() - lastButtonPress) < BUTTON_LONG_PRESS_MS) {
                if (settingsMode == 1) {
                    setTimeOffset += 3600;
                    render();
                } else if (settingsMode == 2) {
                    setTimeOffset += 60;
                    render();
                }
                if (setTimeOffset > MAX_SEC_DAY) {
                    setTimeOffset = setTimeOffset % MAX_SEC_DAY;
                }
                lastButtonPress = 0;
            }
        }
    }

    if (lastRender == 0 || millis()-lastRender >= 1000) {
        render();
    }

    delay(1);
}

void updateDayTimeOffset() {
    unsigned long currentMillis = millis();
    if ((currentMillis - previousMillis) >= 1000) {
        previousMillis += 1000;
        dayTimeOffset += 1;
        correctionCounter += 1;
    }
    if (correctionCounter == CORRECTION_EVERY_SEC) {
        correctionCounter = 0;
        dayTimeOffset += CORRECTION_SEC;
    }
    dayTimeOffset = dayTimeOffset % MAX_SEC_DAY;
}

void clearOLED(){
    display.firstPage();
    do {
    } while( display.nextPage() );
}

void render() {
    lastRender = millis();
    display.firstPage();
    do {
        renderPage();
    } while( display.nextPage() );
}

void renderPage() {
    if (settingsMode == 1) {
        display.setPrintPos(13, 10);
        display.print(".");
    } else if (settingsMode == 2) {
        display.setPrintPos(63, 10);
        display.print(".");
    }

    display.setPrintPos(0, 50);
    printTime();
}

void printTime() {
    long time = (setTimeOffset + dayTimeOffset) % MAX_SEC_DAY;
    int h = time / 3600;
    int m = (time/60) % 60;
    int s = time % 60;

    display.setFont(u8g_font_fub25r);
    printNumber(h);
    display.print(":");
    printNumber(m);
    display.setFont(u8g_font_fub20r);
    display.print(":");
    printNumber(s);
}

void printNumber(int number) {
    if (number < 10) {
        display.print("0");
    }
    display.print(number);
}







You may also like

en Makefile with Python, November 6, 2017
en Fast JSON Schema for Python, October 1, 2018
en Deployment of Python Apps, August 15, 2018
cs Jasně, umím Git…, August 6, 2014
cs Checklist na zabezpečení webových aplikací, March 1, 2016

More posts from category code.
Do not miss new posts thanks to Atom/RSS feed.



Recent posts

cs Mami, tati, přejde to, December 9, 2023 in family
cs Co vše bychom měli dělat s dětmi?, November 24, 2023 in family
cs O trávicí trubici, November 7, 2023 in family
cs Na šestinedělí se nevyspíš, October 28, 2023 in family
cs Copak to bude?, October 20, 2023 in family