Using an ESP32 to create a WiFi enabled smart clock, weather, and date device all while reviving these lovely little vintage HP displays.

Just a quick post here to show how I designed and created a little clock device that uses up the many 8-digit HP bubble displays I bought for a project long ago. I had originally used one to integrate with my RC2014 MircoComputer and worked out well. You can check that video out here.
To be sure, I am not altogether certain these exact displays are HP manufacturing, I see the HP ones documented in much smaller form factor and 4-digit mostly on the web when I look around. But I found these 8-digit same-technology displays on eBay some time back so here we are.


I used Fusion 360 to design the device organically. I wanted also to make the device battery powered just because I am always trying to practice more with ion-lithium integration and power regulation in my projects.
The design of the circuit is very simple and uses two 74HC595 serial registers, daisy chained for 16 bit input, to control the 8 “segment” anodes and 8 “digit” cathodes of the device. It is vital to use either HC or HCT series 595(s) due to the current requirements of the segments.

A long while back I had to manually work out the pinouts of this particular 8-digit bubble display as there is no documentation I could find.

I used a sliding circuit board module design because I felt it easiest to assemble and— should it ever be necessary— repair. A rare few things are designed to be reparable anymore!
Other such design features include some simple mid-century-modern-esque fluting on the sides for pure aesthetic and a hole around the rear for charging via USB. The esp32 does draw quite a bit of current when constantly polling the web for time etc. But I’ve used this setup in a design before and you can get a good 2-4 hour out of the device with an 18550 battery.

Below is the code that runs the device. I redacted my API keys and location but you can see where they would go. The clock is designed for American eyes ergo imperial units and mm/dd/yy date format. To the engineering world, I am terribly sorry for this— I ask God’s forgiveness every day.
Multiplexing was used which does affect overall brightness of the digits. But in order to make the displays usable at all with as few pins/resources as possible this was required. I tried to compensate for the lower brightness with very low current limiting resistors on the segment anodes (22 ohms).
In very bright daylight the device does get washed out and lowers visibility. But this device is planned to live in a moody, low-light indoor setting near the vinyl turn table for the *vibes*.
Thanks for reading!


/*------------------------------------------------------------------
Bubble-Clock with Wi-Fi portal, TimeZoneDB & OpenWeatherMap data
• ESP32-WROOM-32
• HP 8-digit bubble display via 2×74HC595
– 1st 595 → segment anodes (22 Ω)
– 2nd 595 → digit cathodes (active-LOW)
• Shows:
– Time (HH.MM.SS) for seconds 0–44
– Date (MM-DD-YY) for seconds 45–49
– Temp (“ °F ”) for seconds 50–59
• On startup: 60 s “CONN-XX” countdown → if still offline:
display “CONFIG” 2 s, then open portal
• Multiplex at 4 kHz per digit (≈1 kHz full frame)
------------------------------------------------------------------*/
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiManager.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
/* GPIO assignments */
constexpr uint8_t DATA_PIN = 25;
constexpr uint8_t CLOCK_PIN = 26;
constexpr uint8_t LATCH_PIN = 27;
/* BASIC bubble-font (41 entries) */
static const uint8_t FONT[41] = {
0,128, 63, 6, 91, 79,102,109,125, 7,
127,111, 95,124, 57, 94,121,113,111,116,
48, 30,117, 56, 21, 55, 63,115,103, 49,
109,120, 62, 62, 42,118,110, 91, 64, 83,
2
};
/* ASCII→7-segment conversion */
uint8_t asciiToSeg(char c) {
int16_t C = uint8_t(c);
if (C==45) C+=78;
else if (C==63) C+=61;
else if (C==39) C+=86;
else if (C==44) C+=2;
if (C>96) C-=32;
if (C>64) C-=7;
if (C==46) C+=1;
else if (C==32) C+=14;
C -= 46;
return (C<0||C>40)?0:FONT[C];
}
/* Display buffer & driver */
uint8_t dispBuf[8];
inline void latch595() {
digitalWrite(LATCH_PIN, HIGH);
digitalWrite(LATCH_PIN, LOW);
}
inline void send16(uint8_t cath, uint8_t seg) {
shiftOut(DATA_PIN, CLOCK_PIN, MSBFIRST, cath);
shiftOut(DATA_PIN, CLOCK_PIN, MSBFIRST, seg);
latch595();
}
void refreshDisplay() {
static uint8_t d = 0;
uint8_t hw = 7 - d;
uint8_t cath = uint8_t(~(1U << hw));
send16(cath, dispBuf[d]);
d = (d + 1) & 7;
}
void displayText(const char* s) {
for (uint8_t i = 0; i < 8; ++i) {
if (s[i] == '*')
dispBuf[i] = 0b01100011; // degree symbol
else
dispBuf[i] = asciiToSeg(s[i]);
}
}
/* Wi-FiManager parameters */
char bufApiKey[48] = "yourtimekeyhere";
char bufZone[48] = "yourtimezonehere";
char bufOwmKey[48] = "yourweatherkey";
char bufOwmCity[48] = "yourlocationhere";
WiFiManagerParameter pApiKey ("tzdb_key", "TimeZoneDB Key", bufApiKey, sizeof(bufApiKey));
WiFiManagerParameter pZone ("tzdb_zone", "TimeZoneDB Zone", bufZone, sizeof(bufZone));
WiFiManagerParameter pOwmKey ("owm_key", "OWM Key", bufOwmKey, sizeof(bufOwmKey));
WiFiManagerParameter pOwmCity("owm_city", "OWM City", bufOwmCity, sizeof(bufOwmCity));
/* Time & date state */
uint16_t baseYear;
uint8_t baseMonth, baseDay, baseH, baseM, baseS;
unsigned long lastTimeFetch = 0;
bool fetchTime() {
if (!strlen(bufApiKey) || !strlen(bufZone)) return false;
String url = String("http://api.timezonedb.com/v2.1/get-time-zone?key=")
+ bufApiKey + "&format=json&by=zone&zone=" + bufZone;
HTTPClient http; http.begin(url);
if (http.GET() != 200) { http.end(); return false; }
StaticJsonDocument<1024> doc;
deserializeJson(doc, http.getString());
http.end();
String f = doc["formatted"]; // "YYYY-MM-DD HH:MM:SS"
baseYear = f.substring(0,4).toInt();
baseMonth = f.substring(5,7).toInt();
baseDay = f.substring(8,10).toInt();
baseH = f.substring(11,13).toInt();
baseM = f.substring(14,16).toInt();
baseS = f.substring(17,19).toInt();
lastTimeFetch = millis();
return true;
}
/* Weather state */
float ambientF = 0;
unsigned long lastWxFetch = 0;
bool fetchWeather() {
if (!strlen(bufOwmKey) || !strlen(bufOwmCity)) return false;
String url = String("http://api.openweathermap.org/data/2.5/weather?q=")
+ bufOwmCity + "&appid=" + bufOwmKey + "&units=imperial";
HTTPClient http; http.begin(url);
if (http.GET() != 200) { http.end(); return false; }
StaticJsonDocument<1536> doc;
deserializeJson(doc, http.getString());
http.end();
ambientF = doc["main"]["temp"].as<float>();
lastWxFetch = millis();
return true;
}
/* Setup: 60 s CONN-xx countdown → CONFIG → portal */
void setup() {
pinMode(DATA_PIN, OUTPUT);
pinMode(CLOCK_PIN, OUTPUT);
pinMode(LATCH_PIN, OUTPUT);
WiFiManager wm;
wm.addParameter(&pApiKey);
wm.addParameter(&pZone);
wm.addParameter(&pOwmKey);
wm.addParameter(&pOwmCity);
// 60-second connect countdown
WiFi.begin();
for (int t = 60; t >= 0; --t) {
char tmp[9];
snprintf(tmp, sizeof(tmp), "CONN-%02d", t);
displayText(tmp);
unsigned long t0 = millis();
while (millis() - t0 < 1000) refreshDisplay();
if (WiFi.status() == WL_CONNECTED) {
// success: show GOOD for 2 s
unsigned long end = millis() + 2000;
while (millis() < end) {
displayText("GOOD ");
refreshDisplay();
}
break;
}
}
// expired without connecting → indicate & open portal
if (WiFi.status() != WL_CONNECTED) {
unsigned long end = millis() + 2000;
while (millis() < end) {
displayText("CONFIG ");
refreshDisplay();
}
wm.startConfigPortal("BubbleClockConfig");
}
// save any updated parameters
strncpy(bufApiKey, pApiKey.getValue(), sizeof(bufApiKey)-1);
strncpy(bufZone, pZone.getValue(), sizeof(bufZone)-1);
strncpy(bufOwmKey, pOwmKey.getValue(), sizeof(bufOwmKey)-1);
strncpy(bufOwmCity, pOwmCity.getValue(), sizeof(bufOwmCity)-1);
fetchTime();
fetchWeather();
}
/* Main loop: cycle display & multiplex */
void loop() {
unsigned long now = millis();
if (now - lastTimeFetch > 300000UL) fetchTime();
if (now - lastWxFetch > 1800000UL) fetchWeather();
unsigned long elapsed = (now - lastTimeFetch) / 1000UL;
unsigned long totalSec = baseH*3600UL + baseM*60UL + baseS + elapsed;
uint8_t h = (totalSec/3600UL)%24, m = (totalSec/60UL)%60, s = totalSec%60;
enum { SHOW_TIME, SHOW_DATE, SHOW_TEMP };
uint8_t mode = (s < 45 ? SHOW_TIME
: (s < 50 ? SHOW_DATE
: SHOW_TEMP));
static int8_t lastMode = -1, lastSec = -1;
if (mode == SHOW_TIME) {
if (mode != lastMode || s != lastSec) {
int h12 = h % 12; if (!h12) h12 = 12;
char txt[9];
snprintf(txt, sizeof(txt), "%02d.%02d.%02d", h12, m, s);
displayText(txt);
if (h >= 12) dispBuf[7] |= 0x80; // PM dot
lastSec = s;
}
}
else if (mode == SHOW_DATE) {
if (mode != lastMode) {
int yy = baseYear % 100;
char txt[9];
snprintf(txt, sizeof(txt), "%02d-%02d-%02d", baseMonth, baseDay, yy);
displayText(txt);
}
}
else { // SHOW_TEMP
if (mode != lastMode) {
int ti = int(ambientF + 0.5f);
char txt[9];
snprintf(txt, sizeof(txt), " %3d*F ", ti);
displayText(txt);
}
}
lastMode = mode;
// multiplex @16 kHz per digit (≈1 kHz frame)
static unsigned long tDisp = 0;
if (micros() - tDisp >= 62) {
refreshDisplay();
tDisp = micros();
}
}
6 thoughts on “HP Bubble Display Clock”