Creative Desktop Clock

by halfanelephant in Circuits > Arduino

21 Views, 0 Favorites, 0 Comments

Creative Desktop Clock

cover_product.png

The Creative Desktop Clock is a compact Wi-Fi display built around an ESP-12F ESP8266 module and a 1.3-inch 240 x 240 color screen. It combines a glanceable desk clock with stock information, image display, a Pomodoro timer and an AI-assistant entry point. A CI1302 offline voice module can send UART commands so the user can change screens without relying only on a button.\n\nThis guide converts our Group 4 presentation into a reproducible maker workflow. The presentation supplied the product concept, component list, power-circuit close-ups, system schematic, component photographs and enclosure renders. The firmware included here is starter code derived from the schematic and project goals; review the pin constants and verify it on your own hardware before final use.

Supplies

The Creative Desktop Clock is a compact Wi-Fi display built around an ESP-12F ESP8266 module and a 1.3-inch 240 x 240 color screen. It combines a glanceable desk clock with stock information, image display, a Pomodoro timer and an AI-assistant entry point. A CI1302 offline voice module can send UART commands so the user can change screens without relying only on a button.\n\nThis guide converts our Group 4 presentation into a reproducible maker workflow. The presentation supplied the product concept, component list, power-circuit close-ups, system schematic, component photographs and enclosure renders. The firmware included here is starter code derived from the schematic and project goals; review the pin constants and verify it on your own hardware before final use.

Understand the System Architecture

full_schematic.png

Trace the project as five blocks before soldering:

  1. USB-C input supplies 5 V.
  2. AMS1117-3.3 generates the 3.3 V rail.
  3. ESP-12F runs the user interface, Wi-Fi services and timer logic.
  4. ST7789 receives SPI data and draws the 240 x 240 interface.
  5. CI1302 sends recognized offline commands through UART.

Build and test the power stages first. Add the ESP8266 only after the unloaded 3.3 V rail is stable. Then connect the display, optional button and CI1302 interface.

Build the USB-C Power Input

usb_c_power.png

Use the USB-C connector only as a 5 V power input unless you design and test a complete USB data interface.

  1. Connect all VBUS pins to the fused 5 V rail.
  2. Connect all GND pins and the shield to circuit ground.
  3. Connect CC1 to GND through 5.1 kOhm and CC2 to GND through another 5.1 kOhm resistor.
  4. Add reverse-polarity or over-current protection if the board will be handled outside the lab.

Electrical correction: do not connect CC1 or CC2 to the 5 V rail. A sink-ready USB-C power breakout with the pull-down resistors already installed is the simplest option.

Add the 3.3 V Regulator

ams1117_power.png

The ESP8266 and display need a stable 3.3 V rail.

  1. Connect AMS1117 IN to the protected 5 V rail and GND to ground.
  2. Connect OUT to the 3.3 V rail.
  3. Place a 10 uF capacitor from IN to GND and another 10 uF capacitor from OUT to GND, close to the regulator.
  4. Add 100 nF ceramic capacitors near the ESP8266 and display supply pins.
  5. Keep high-current display and Wi-Fi return paths short and wide.

Power test: with the ESP8266 and display disconnected, apply 5 V and measure the output. Continue only when the rail is stable near 3.3 V and the regulator does not overheat.

Wire and Assemble the Electronics

controller_voice.png
display_module.png

Use the following wiring from the schematic as the starting point:

  1. GPIO14 -> SCL / SCK, SPI clock
  2. GPIO13 -> SDA / MOSI, SPI data
  3. GPIO15 -> CS, display select
  4. GPIO0 -> D/C, display data/command
  5. GPIO2 -> RES, display reset
  6. GPIO5 -> LEDA / BL, backlight control
  7. 3.3 V -> display VDD
  8. GND -> display GND / LEDK and all module grounds
  9. GPIO3 / RXD -> CI1302 TX
  10. GPIO1 / TXD -> CI1302 RX
  11. GPIO4 -> optional button to GND

Boot-pin caution: GPIO0, GPIO2 and GPIO15 decide the ESP8266 boot mode. The display must not force these pins to the wrong level during reset. If booting is unreliable, isolate the affected display control line with a series resistor, buffer or a different pin assignment in the next PCB revision.

Prepare the Enclosure

enclosure_front.png
enclosure_rear.png

The presentation includes front and rear shell renders, but no dimensioned CAD file. Rebuild or obtain the CAD source, then fit it to your measured components.

  1. Measure the real display PCB, connector height, ESP-12F antenna keep-out area and voice-module height.
  2. Keep the ESP8266 antenna away from metal, dense wiring and conductive coating.
  3. Provide access to USB-C and leave room for strain relief.
  4. Prototype the front and rear shells at low quality before a final 3D print or resin cure.
  5. Check button travel, ventilation, cable clearance and module removal before closing the case.


Set Up the Arduino Environment

  1. Install Arduino IDE.
  2. Add ESP8266 board support and select Generic ESP8266 Module for an ESP-12F-based custom board.
  3. Install Adafruit GFX Library and Adafruit ST7735 and ST7789 Library from Library Manager.
  4. Select the correct serial port and use a 3.3 V USB-to-UART adapter if the custom PCB does not contain one.
  5. Set flash mode and upload speed conservatively for the first test.

Enter Wi-Fi credentials only in your private working copy. Keep passwords, tokens and API keys out of screenshots and public files. For an AI service, call a controlled gateway that keeps the provider key on a server.

Upload the Starter Firmware

The firmware initializes the ST7789, connects to Wi-Fi, synchronizes time through NTP, draws five screens, runs a Pomodoro countdown and accepts UART commands.

#include <ESP8266WiFi.h>#include <Adafruit_GFX.h>#include <Adafruit_ST7789.h>#include <SPI.h>#include <time.h>

// ST7789 connections from the project schematic.constexpr uint8_t TFT_CS = 15;constexpr uint8_t TFT_DC = 0;constexpr uint8_t TFT_RST = 2;constexpr uint8_t TFT_BACKLIGHT = 5;constexpr uint8_t MODE_BUTTON = 4; // Optional button to GND.

// ESP8266 hardware SPI uses GPIO14 for SCK and GPIO13 for MOSI.Adafruit_ST7789 tft(TFT_CS, TFT_DC, TFT_RST);

const char WIFI_SSID[] = "";const char WIFI_PASSWORD[] = "";const char STOCK_ENDPOINT[] = "";const char AI_ENDPOINT[] = "";

enum ScreenMode { CLOCK_SCREEN, STOCK_SCREEN, IMAGE_SCREEN, POMODORO_SCREEN, AI_SCREEN };ScreenMode currentScreen = CLOCK_SCREEN;

constexpr uint32_t WORK_SECONDS = 25UL * 60UL;constexpr uint32_t BREAK_SECONDS = 5UL * 60UL;uint32_t pomodoroSeconds = WORK_SECONDS;uint32_t lastPomodoroTick = 0;bool pomodoroRunning = false;bool workSession = true;

uint32_t lastDraw = 0;uint32_t buttonDownAt = 0;bool previousButton = HIGH;String voiceLine;

void setText(uint16_t color, uint8_t size) { tft.setTextColor(color); tft.setTextSize(size); tft.setTextWrap(false);}

void drawHeader(const char *title) { tft.fillScreen(ST77XX_BLACK); tft.fillRect(0, 0, 240, 34, tft.color565(24, 110, 116)); setText(ST77XX_WHITE, 2); tft.setCursor(10, 10); tft.print(title);}

void drawFooter(const char *message) { tft.fillRect(0, 214, 240, 26, tft.color565(18, 28, 33)); setText(tft.color565(190, 220, 222), 1); tft.setCursor(8, 224); tft.print(message);}

void connectWiFi() { if (WIFI_SSID[0] == '\0') return;

WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); uint32_t started = millis(); while (WiFi.status() != WL_CONNECTED && millis() - started < 12000) { delay(250); }}

void configureClock() { // UTC+8 is used for the team location. Change the first argument if needed. configTime(8 * 3600, 0, "pool.ntp.org", "time.nist.gov");}

void drawClock() { drawHeader("DESKTOP CLOCK");

time_t now = time(nullptr); struct tm localTime; localtime_r(&now, &localTime);

char timeText[9]; char dateText[20]; if (now < 100000) { snprintf(timeText, sizeof(timeText), "--:--:--"); snprintf(dateText, sizeof(dateText), "Waiting for NTP"); } else { strftime(timeText, sizeof(timeText), "%H:%M:%S", &localTime); strftime(dateText, sizeof(dateText), "%Y-%m-%d", &localTime); }

setText(ST77XX_WHITE, 4); tft.setCursor(22, 78); tft.print(timeText); setText(tft.color565(255, 196, 74), 2); tft.setCursor(58, 134); tft.print(dateText);

setText(tft.color565(120, 210, 215), 1); tft.setCursor(58, 168); tft.print(WiFi.status() == WL_CONNECTED ? "Wi-Fi connected" : "Offline mode"); drawFooter("Short press: next screen");}

void drawStock() { drawHeader("STOCK DISPLAY"); setText(tft.color565(86, 220, 140), 2); tft.setCursor(16, 66); tft.print("API extension"); setText(ST77XX_WHITE, 1); tft.setCursor(16, 104); tft.print("Set STOCK_ENDPOINT and"); tft.setCursor(16, 122); tft.print("parse your provider's JSON."); tft.drawRect(16, 150, 208, 42, tft.color565(60, 130, 136)); tft.setCursor(28, 167); tft.print(STOCK_ENDPOINT[0] ? "Endpoint configured" : "No endpoint configured"); drawFooter("Use HTTPS and protect API keys");}

void drawImageDemo() { drawHeader("IMAGE DISPLAY"); uint16_t cyan = tft.color565(64, 190, 196); uint16_t gold = tft.color565(255, 190, 70); tft.fillRoundRect(60, 55, 120, 120, 12, cyan); tft.fillRoundRect(72, 67, 96, 82, 8, ST77XX_BLACK); tft.drawCircle(120, 108, 30, ST77XX_WHITE); tft.drawLine(120, 108, 120, 86, gold); tft.drawLine(120, 108, 137, 117, gold); tft.fillRect(88, 180, 64, 8, cyan); drawFooter("Replace drawing with a bitmap");}

void resetPomodoro() { workSession = true; pomodoroSeconds = WORK_SECONDS; pomodoroRunning = false;}

void drawPomodoro() { drawHeader(workSession ? "FOCUS SESSION" : "SHORT BREAK"); uint32_t minutes = pomodoroSeconds / 60; uint32_t seconds = pomodoroSeconds % 60; char timerText[8]; snprintf(timerText, sizeof(timerText), "%02lu:%02lu", static_cast<unsigned long>(minutes), static_cast<unsigned long>(seconds));

setText(tft.color565(255, 112, 72), 5); tft.setCursor(42, 82); tft.print(timerText); setText(ST77XX_WHITE, 2); tft.setCursor(64, 154); tft.print(pomodoroRunning ? "RUNNING" : "PAUSED"); drawFooter("Long press: start or pause");}

void drawAI() { drawHeader("AI ASSISTANT"); tft.drawCircle(120, 98, 42, tft.color565(70, 165, 255)); tft.fillCircle(105, 92, 5, ST77XX_WHITE); tft.fillCircle(135, 92, 5, ST77XX_WHITE); tft.drawLine(103, 118, 137, 118, ST77XX_WHITE); setText(ST77XX_WHITE, 1); tft.setCursor(34, 162); tft.print(AI_ENDPOINT[0] ? "Gateway configured" : "Set a secure AI gateway"); drawFooter("Never store private keys in code");}

void drawCurrentScreen() { switch (currentScreen) { case CLOCK_SCREEN: drawClock(); break; case STOCK_SCREEN: drawStock(); break; case IMAGE_SCREEN: drawImageDemo(); break; case POMODORO_SCREEN: drawPomodoro(); break; case AI_SCREEN: drawAI(); break; }}

void nextScreen() { currentScreen = static_cast<ScreenMode>((currentScreen + 1) % 5); drawCurrentScreen();}

void processCommand(String command) { command.trim(); command.toUpperCase();

if (command == "NEXT") nextScreen(); else if (command == "CLOCK") currentScreen = CLOCK_SCREEN; else if (command == "STOCK") currentScreen = STOCK_SCREEN; else if (command == "IMAGE") currentScreen = IMAGE_SCREEN; else if (command == "POMODORO") currentScreen = POMODORO_SCREEN; else if (command == "AI") currentScreen = AI_SCREEN; else if (command == "START") pomodoroRunning = true; else if (command == "STOP") pomodoroRunning = false; else if (command == "RESET") resetPomodoro();

drawCurrentScreen();}

void readVoiceUart() { while (Serial.available()) { char c = static_cast<char>(Serial.read()); if (c == '\n' || c == '\r') { if (voiceLine.length()) processCommand(voiceLine); voiceLine = ""; } else if (voiceLine.length() < 32) { voiceLine += c; } }}

void updateButton() { bool pressed = digitalRead(MODE_BUTTON) == LOW;

if (pressed && previousButton == HIGH) buttonDownAt = millis(); if (!pressed && previousButton == LOW) { uint32_t held = millis() - buttonDownAt; if (held >= 800 && currentScreen == POMODORO_SCREEN) { pomodoroRunning = !pomodoroRunning; drawPomodoro(); } else if (held > 30) { nextScreen(); } } previousButton = pressed ? LOW : HIGH;}

void updatePomodoro() { if (!pomodoroRunning || millis() - lastPomodoroTick < 1000) return; lastPomodoroTick = millis();

if (pomodoroSeconds > 0) { --pomodoroSeconds; } else { workSession = !workSession; pomodoroSeconds = workSession ? WORK_SECONDS : BREAK_SECONDS; pomodoroRunning = false; }}

void setup() { pinMode(TFT_BACKLIGHT, OUTPUT); digitalWrite(TFT_BACKLIGHT, HIGH); pinMode(MODE_BUTTON, INPUT_PULLUP);

// The project UART header uses GPIO3/RXD and GPIO1/TXD. // Disconnect an external UART transmitter if it prevents firmware upload. Serial.begin(9600);

SPI.begin(); tft.init(240, 240); tft.setRotation(0); drawHeader("STARTING"); drawFooter("Connecting to Wi-Fi...");

connectWiFi(); configureClock(); drawCurrentScreen(); lastPomodoroTick = millis();}

void loop() { readVoiceUart(); updateButton(); updatePomodoro();

if (millis() - lastDraw >= 1000) { lastDraw = millis(); if (currentScreen == CLOCK_SCREEN || currentScreen == POMODORO_SCREEN) { drawCurrentScreen(); } } delay(5);}

Test and Use the Clock

Run these checks before closing the enclosure:

  1. Check 5 V-to-GND and 3.3 V-to-GND for shorts before power.
  2. Power through a current-limited 5 V source and confirm a stable 5 V input rail.
  3. Measure the regulator output before installing modules; it should be about 3.3 V.
  4. Reset with the programming strap removed and confirm normal flash boot.
  5. Confirm display text, colors and backlight.
  6. Enter Wi-Fi credentials and wait for NTP to show the current local time.
  7. Short press the button to cycle screens; long press on Pomodoro to start or pause.
  8. Send newline-terminated UART commands from CI1302, such as NEXT, CLOCK, STOCK, IMAGE, POMODORO, AI, START, STOP and RESET.

The stock and AI screens in the starter firmware show safe configuration messages. Choose a permitted data provider and a secure AI gateway before adding HTTPS requests and response parsing.

Troubleshooting and Future Improvements

Common problems:

  1. ESP8266 will not boot: verify GPIO0/GPIO2/GPIO15 levels, rail voltage under load and whether the display is loading a boot pin.
  2. Upload fails: disconnect CI1302 TX from RXD, enter bootloader again and lower upload speed.
  3. Blank display: measure backlight power, verify ST7789 pin order, reset pin and 240 x 240 initialization.
  4. Random resets: improve the regulator thermal area, decoupling and wiring.
  5. Voice command ignored: confirm 3.3 V logic, 9600 baud and newline-terminated commands.

Future improvements from the presentation:

  1. Replace the development board with a Raspberry Pi Zero 2 W and install Ubuntu for a larger software stack.
  2. Add Matter support and use Home Assistant as a smart-home control entry point.
  3. Connect Codex or Claude Code to a controlled monitoring workflow for development and diagnostics.
  4. Expand the USB-C interface beyond basic power input after implementing the required data and protection circuitry.

Before publishing, compile and upload the sketch on the exact hardware, record final enclosure dimensions, use real assembly photos alongside renders, and remove all private credentials from public files.