Friday, 5 December 2025

Absolute Overkill for a Bedside Clock


Our ancient bedside clock displayed very comforting dim red digits until it died. An iPhone in standby mode would have been a great replacement, except mine seems to cut out in the middle of the night on its magnetic stand, and other times I leave it in my pants pocket by mistake. The new bedside clock we bought turned out to be too bright and too blue, making us think dawn was coming at 3 AM, so I decided it should be easy to build a clock that meets our very simple needs:

  • It should be dim but readable in the dark without glasses.
  • The digits should be a nice friendly red.
  • The time should be accurate and not need resetting.

And ideally I should be able to build it from parts I have around the studio, and not need any new infrastructure.


The Overkill

If the clock could only just pull the correct time off the internet, it wouldn't need any hardware or code or human effort to always be right. If it checked the time often enough, it wouldn't even need to remember what time it was from one second to the next, or keep track of how much time went by. It turns out Adafruit IO has a service that will notify you every second just to let you know what time it is. 

I find it absolutely stupefying to think it might make sense to have a server in NYC call my little clock display every second of every day just to let it know another second has gone by. But the service is there, and it's free, so it's worth a try. It's also really easy to access through Arduino with File/Examples/Adafruit IO Arduino/adafruitio_17_time_subscribe.ino just by following the example code. (You will, of course, need to create an Adafruit IO id to be able to connect.)

The Microcontroller

The Adafruit ESP32 Feather (I think it was this one https://www.adafruit.com/product/3405 but it's soldered in now and impossible to check.) meets the two most important criteria: I had one and it talks wifi. I broke off the battery connector to give it a lower profile so it all fit together nicely.

The Display

The Adafruit 1.2" Four Digit, Seven Segment Display with BackPack https://www.adafruit.com/product/1270 comes in a nice comforting red and the digits are big enough to read from across the room without my glasses. It can be dimmed in 16 steps from 15 down to 0. 0 is still pretty bright in a dark room, but there doesn't seem to be a way to go any dimmer with the Holtek HT16K33 used on these backpacks, unlike the MAX7219 used on some others.

For some reason I can only guess at, the display refused to communicate by I2C until I also connected an Adafruit BME280 breakout board https://www.adafruit.com/product/2652, although the one I had was a much older version with no Stemma connectors. So the clock is also capable of some environmental monitoring, sort of by accident. My guess at the fault is that these old components may have had some mismatch on termination resistors for I2C, or maybe the stars just didn't align, but the fault and solution was repeatable.


Connecting and Packaging

A full size permaproto https://www.adafruit.com/product/590 provided a great support for all the components. The Feather USB supply pin provides 5V to the backpack, with the 3V pin supplying everything else, including the IO pin on the backpack. SDA and SCL are all connected and two pushbuttons are connected so they can pull pins 14 and 32 of the Feather to ground for signalling from the user.

After some extended testing in breadboard form, the whole package now fits in a 3D printed box that mounts to the dresser beside the bed. There's no structural mounting for the separate components, so it's not suitable for the g forces that would go with a mobile environment, or even shipping, but should be fine just sitting in one place.


Software

The basics are just a mash-up of the time subscription example and the BME280 example both provided by Adafruit as part of the libraries. That provides us with Temperature, Pressure, Humidity, and Zulu Time in an ISO format, but doesn't get us the current local time. The code below will need a config.h tab with your own network and Adafruit IO details in it, just like their examples.

Time zone correction is easy with a hard coded location setting int tz = -5. You can get time since local 1970 in seconds by adding tz*3600 to the UTC seconds value. I didn't allow for non-integer time zone offsets and don't know if there would be weird consequences of making tz floating point.

Daylight Savings Time is a little more complicated, since the time always changes at two AM on a Sunday. You need to know what day and date it is, as well as the time, which needs another library. Using the Arduino Library Manager, install "*Time* by *Michael Margolis*", which can be a little hard to find in the list. The include file is TimeLib.h even though the code is in Time.cpp. My own custom dsAdjust() function uses that library to break down the time into elements like day and date, then test them to see if it is North American DST at any particular time. It returns the number of hours (0 or 1) that should be applied to account for DST. ts gets adjusted in handleSecs() every second of every day.

The actual time to be displayed is generated by hhmmCalc(),  pulling out the time as a 1234 integer based on seconds since 1970 local, DST adjusted. 

Initially the plan was just to have it be dim all the time, but the buttons should do something, so the top button will make the display brighter and the bottom button will make it dimmer. Or if you don't touch the buttons, it will get brighter and dimmer according to the time of day. Brightest around two in the afternoon  and dimmest between 9 PM and 7 AM. To prevent it from automatically resetting the level as soon as you take your finger off the button, the automatic reset only happens if you have kept your fingers off the buttons for 12 hours.

The buttons can also surface some of the BME280 data. When the top button is pushed the raw pressure in millibars is displayed and the bottom button will show relative humidity. The pressure is not altitude adjusted, so it will read about 9 or 10 millibars low here on the third floor in Kingston. I'm not so sure about interpreting the humidity, as the temperature of the sensor may be higher than the room and the air in the box may be more or less humid than the room. These are mostly there because I could... not because I should.

Questions

Will it need software updates in the future? It is certainly set up to only run on our home network with a fixed SSID, but what will happen if I get a new router with better security, but still give it the same SSID? It also assumes we remain in the same time zone and the rules for daylight saving time remain the same.

What if I live in Newfoundland? You will need to write some more general (or more specific) software to deal with the half hour time zone effect. This is not a problem I anticipate in my own bedroom, but I think it may be as simple as make tz a double instead of an int and setting it to 3.5.

// Bedroom Clock (RWS) converted from Adafruit IO Time Topic Subscription Example
// and BME280 Example, and display example.
// This code is running on hardware with Adafruit ESP32 Feather processor,
// 7 seg display with I2C backpack, and BME280.
//
// Adafruit invests time and resources providing this open source code.
// Please support Adafruit and open source hardware by purchasing
// products from Adafruit!
//
// Written by Adam Bachman, Brent Rubell for Adafruit Industries
// Copyright (c) 2018 Adafruit Industries
// Licensed under the MIT license.
//
// All text above must be included in any redistribution.

/************************** Configuration ***********************************/

// edit the config.h tab and enter your Adafruit IO credentials
// and any additional configuration needed for WiFi, cellular,
// or ethernet clients.
#include "config.h"

/************************ Example Starts Here *******************************/

// set up the 'time/seconds' topic
AdafruitIO_Time *seconds = io.time(AIO_TIME_SECONDS);

#include <Wire.h> // Enable this line if using Arduino Uno, Mega, etc.
#include <Adafruit_GFX.h>
#include "Adafruit_LEDBackpack.h"
#include "TimeLib.h"
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>

Adafruit_7segment matrix = Adafruit_7segment();
Adafruit_BME280 bme; // I2C

int hhmm = 3333; // hours and minutes as a four digit number hhmm
int tz = -5; // time zone adjustment
int ts = 0; // daylight savings time adjustment
unsigned long lastBrightnessAdjustment = 0; // time in millis()

void setup() {
// start the serial connection
Serial.begin(115200);
// wait for serial monitor to open
while(! Serial);
Serial.print("BedroomClock V1.0\n\nStarting Display, init BME, and Connecting to Adafruit IO");
matrix.begin(0x70);
matrix.setBrightness(0);
unsigned status = bme.begin();
if (!status) {
Serial.println("Could not find a valid BME280 sensor, check wiring, address, sensor ID!");
Serial.print("SensorID was: 0x"); Serial.println(bme.sensorID(),16);
Serial.print(" ID of 0xFF probably means a bad address, a BMP 180 or BMP 085\n");
Serial.print(" ID of 0x56-0x58 represents a BMP 280,\n");
Serial.print(" ID of 0x60 represents a BME 280.\n");
Serial.print(" ID of 0x61 represents a BME 680.\n");
}

// start MQTT connection to io.adafruit.com
io.connect();
// attach message handler for the seconds feed
seconds->onMessage(handleSecs);
// wait for an MQTT connection
while(io.mqttStatus() < AIO_CONNECTED) {
Serial.print(".");
delay(500);
}
// we are connected
Serial.println();
Serial.println(io.statusText());

pinMode(14,INPUT_PULLUP);
pinMode(32,INPUT_PULLUP);
}

double temperature = 0.0, pressure = 0.0, humidity = 0.0;

void loop() {
static int userBrightness = 0;
// increase up to 15 in steps every 1/4 second if top button pressed
if(userBrightness < 15 && millis()-lastBrightnessAdjustment > 250 && !digitalRead(14)){
userBrightness++;
lastBrightnessAdjustment = millis();
Serial.printf("userBrightness raised to: %d\n",userBrightness);
}
// increase down to 0 in steps every 1/4 second if bottom button pressed
if(userBrightness > 0 && millis()-lastBrightnessAdjustment > 250 && !digitalRead(32)){
userBrightness--;
lastBrightnessAdjustment = millis();
Serial.printf("userBrightness lowered to: %d\n",userBrightness);
}
// if no past adjustment, or it's at least 12 hours in the past, make it bright/dim by time
if(lastBrightnessAdjustment == 0 || (millis()-lastBrightnessAdjustment)/3600000 > 12 ){
userBrightness = abs(1400-hhmm)/50; // 28 just past midnight, 20 just before, 14 at 7AM and 9PM
userBrightness = 14 - userBrightness; // 14 in the afternoon, dropping to 0 between 2100 and 0700
if(userBrightness < 0) userBrightness = 0; // don't go below zero
Serial.printf("userBrightness auto set to: %d\n",userBrightness);
}
matrix.setBrightness(userBrightness);

io.run();
temperature = bme.readTemperature();
pressure = bme.readPressure();
humidity = bme.readHumidity();
matrix.writeDigitNum(0, (hhmm / 1000), false); // draw digit by digit to force leading zeros
matrix.writeDigitNum(1, (hhmm / 100) % 10, false);
matrix.drawColon(true);
matrix.writeDigitNum(3, (hhmm / 10) % 10, false);
matrix.writeDigitNum(4, hhmm % 10, false);
if(!digitalRead(14)) matrix.print((int) (pressure/100));
if(!digitalRead(32)) matrix.print((int) (humidity+0.5));
matrix.writeDisplay();
//delay(1);
}


// message handler for the seconds feed
void handleSecs(char *data, uint16_t len) {
Serial.print("Seconds Feed: ");
Serial.println(data);
ts = dsAdjust(atoi(data) + tz *3600); // set daylight savings time status based on seconds since 1970 local standard time
hhmm = hhmmCalc(atoi(data) + (ts+tz)*3600); // set hours and minutes based on DST and zone
Serial.printf("Temp: %6.2f°C Press: %6.0f hPa Hum: %6.2f%%\n",temperature,pressure/100,humidity);
if(!digitalRead(14)) Serial.println("Green Button Pushed -- Pin 14 pulled low\n");
if(!digitalRead(32)) Serial.println("Red Button Pushed -- Pin 32 pulled low\n");
}

int hhmmCalc(time_t secs){ // return an integer like 1234 for 12:34
TimeElements els; // time library breakdown of latest time in elements
breakTime(secs, els); // secs as passed must be adjusted to local time, including DST if applicable
int hhmm = els.Hour*100 + els.Minute;
while(hhmm < 0) hhmm += 2400; // force back into 0-2359 if needed
while(hhmm >= 2400) hhmm -= 2400;
return hhmm;
}

int dsAdjust(time_t secs) { // Calculate the Daylight Savings Time adjustment for North America
TimeElements els; // time library breakdown of latest time in elements
breakTime(secs, els); // secs as passed must be adjusted to local standard time to get the moments of transition right
// Serial.printf("%d %d %d\n",els.Hour,els.Minute,els.Second);
if(els.Month > 3 && els.Month < 11) return 1; // April through October is daylight savings
if(els.Month == 3 && els.Wday == 1 && els.Day > 7 && els.Day < 15) // or if it is the second Sunday in March
if(els.Hour >= 2) return 1; // yes if after 2AM standard
else return 0; // otherwise not if before 2AM standard
if(els.Month == 3 && ((els.Day-els.Wday)-7) >= 0) return 1; // or if it is March on or after the second Sunday
if(els.Month == 11 && (els.Day-els.Wday) < 0) return 1; // or if it is November and before the first Sunday
if(els.Month == 11 && els.Wday == 1 && els.Day < 8 && els.Hour < 3) return 1; // or if it is the first Sunday in November and before 3AM standard (2AM DST)
return 0; // otherwise not
}