"H-CUBE" a Retro Style Smart Watch Powered by an ESP32-S3

by Ashu503 in Circuits > Wearables

1197 Views, 9 Favorites, 0 Comments

"H-CUBE" a Retro Style Smart Watch Powered by an ESP32-S3

WhatsApp Image 2025-11-30 at 19.01.41_bf01a7d4.jpg
WhatsApp Image 2025-11-30 at 19.13.56_2f844c1c.jpg
WhatsApp Image 2025-11-30 at 19.16.27_371cbd80.jpg
WhatsApp Image 2025-11-30 at 19.16.57_e1dc054f.jpg
WhatsApp Image 2025-11-30 at 19.19.34_2ce939dd.jpg

I always wanted to build my own wristwatch, something I could design, create, and actually wear. But I never had the right inspiration to begin. The biggest challenge was choosing the display, which is the most important part of any digital watch.

I’ve always been fascinated by 80s technology, amber LEDs, simple character displays, and minimalistic design, so I naturally wanted something that captured that retro feel.

OLEDs were an option, but they didn’t match the look I had in mind. Color LCDs also didn’t fit that aesthetic. E-ink displays were interesting, but their refresh rates were too slow for a responsive watch interface.

At first, I considered using 0.2" seven-segment displays, four of them arranged in a matrix. That idea stayed with me for a while, until a few months ago, when I was searching for seven-segment modules and came across the HCMS-2971: a High Performance CMOS 5×7 alphanumeric display.

The moment I saw it, everything clicked. It had the perfect retro glow, the right size, and exactly the kind of visual character I wanted for my wristwatch. That’s when the project finally started taking shape, built on a custom PCB with an ESP32-S3 at its center and designed to incorporate features such as GPS, a step counter, temperature sensing, and more.

Supplies

2900833-40.jpg
download.jpg
download.jpg
download.jpg
download.jpg
download.jpg
download.jpg
  1. Core electronics: ESP32‑S3 microcontroller with external 128‑Mbit flash, CP2102 USB‑to‑UART, 40 MHz crystal, ESD protection, and USB‑C connector.​
  2. Display and indicators: HCMS‑2971 8‑character alphanumeric LED display plus several 0603 status LEDs in different colors for notifications and mode feedback.​
  3. Sensing and timing: DS3231 real‑time clock, BMI270 6‑axis IMU, GP‑02 GNSS module with dedicated RF and GNSS chip antennas for accurate motion, time, and position data.​
  4. Power and battery: Single‑cell Li‑ion battery, BQ24075 charger and power‑path IC, PAM2401 boost converter, LP38690 3.3 V LDO, MAX17048 fuel gauge, power inductors, Schottky diodes, and a full network of decoupling capacitors.​
  5. Audio and user input: MLT‑8530 side‑firing buzzer, LS‑1305 3‑way navigation switch, additional tactile switch, and various NPN transistors for driving loads and handling control signals.​
  6. Passives and connectors: Dozens of 0402/0603 capacitors, resistors from 0 Ω jumpers to precision feedback values, small RF inductors for antenna matching, a JST battery connector, and other board headers

I have given BOM with all the components.

What Is H-CUBE?

WhatsApp Image 2025-11-30 at 19.22.27_1c20cbc0.jpg
WhatsApp Image 2025-11-30 at 19.22.27_5fc4bb1b.jpg

H-CUBE stands for "Hardware Cube", representing both the hardware-centric design philosophy and the compact form factor. It's a wearable smartwatch featuring:

  1. Retro Display: HCMS-2971 8-character yellow alphanumeric LED (2000 nits, PWM adjustable brightness)
  2. Modern Processor: ESP32-S3 dual-core 240 MHz with 8 MB flash, integrated Bluetooth/WiFi
  3. Sensor Suite: DS3231 RTC (±2 ppm), BMI270 IMU (6-axis), GP-02 GPS, MAX17048 fuel gauge
  4. 12+ Operating Modes: Time, date, GPS, temperature, battery status, alarm, stopwatch, RGB LED control
  5. Wireless Features: Bluetooth SPP pairing with phones, WiFi connectivity, OTA updates
  6. Smart Power Management: Intelligent charging detection, 10-second sleep mode, 8-12 hour runtime
  7. Professional Hardware: 4-layer PCB, 100+ SMD components, custom 3D-printed housing

Testing the Concept on Bredboard

WhatsApp Image 2025-11-30 at 19.22.28_3687193d.jpg
WhatsApp Image 2025-11-30 at 19.22.25_a7870ce6.jpg
H-CUBE.png
WhatsApp Image 2025-11-30 at 19.22.29_faf3ba8e.jpg
WhatsApp Image 2025-11-30 at 19.22.28_67134b5d.jpg
WhatsApp Image 2025-11-30 at 19.22.19_59212ab6.jpg
WhatsApp Image 2025-11-30 at 19.22.28_3ba9776b.jpg
WhatsApp Image 2025-11-30 at 19.22.26_d2cb9f0a.jpg
WhatsApp Image 2025-11-30 at 19.22.29_4b213ade.jpg
WhatsApp Image 2025-11-30 at 19.22.29_8c6217af.jpg
WhatsApp Image 2025-11-30 at 19.22.30_7008b94b.jpg
WhatsApp Image 2025-11-30 at 19.22.31_71403e89.jpg
WhatsApp Image 2025-11-30 at 19.22.30_6a225753.jpg
WhatsApp Image 2025-11-30 at 19.22.31_23f6a8fa.jpg
WhatsApp Image 2025-11-30 at 19.22.26_4436f716.jpg

Before moving on to the PCB design, the whole circuit was first tested on a breadboard to make sure every block worked perfectly together. This stage acted as a safety check where wiring could be changed in seconds, parts could be swapped, and firmware could be debugged without risking an expensive 4‑layer board.

Understanding the Hardware

SCH.png
SS6.png
SS4.png
SS1.png
SS2.png
ss3.png
SS8.png
SS9.png
SS12.png
SS11.png
SS5.png
S12.png
S15.png
S13.png

1. ESP32-S3 (Main Microcontroller)


The ESP32-S3 is the main microcontroller of the H-CUBE watch. It handles all processing, sensing, communication, display driving, and UI control. The schematic connects the S3 to power, USB programming, I/O peripherals, and required strapping components.

  1. VDD3P3 / VDD_SPI / VDD_CPU are all powered from a 3.3V regulated output (from the LDO).
  2. Each supply pin has local 0.1 µF + 10 µF decoupling capacitors placed close to the chip, ensuring stable operation.
  3. VDDA rail (analog) is filtered by a small inductor + capacitors to reduce digital noise.

the ESP32-S3 has native USB support through its USB OTG (On-The-Go) peripheral, which allows it to act as a USB host or device. It also includes an integrated USB-Serial-JTAG controller, enabling direct firmware flashing, serial communication, and debugging via USB without an external USB-to-serial chip. But for simplicity, I went with a good and tested CP2102 USB-to-TTL.

The Following Pins are in use from ESP32-S3:

Display (HCMS-2971)

  1. GPIO10 → RS
  2. GPIO11 → Enable
  3. GPIO12 → Clock
  4. GPIO13 → Register Select
  5. GPIO14 → Data

Buttons

  1. GPIO4 → Center
  2. GPIO5 → Up
  3. GPIO6 → Down

RGB LED (PWM Channels)

  1. GPIO38 → R
  2. GPIO39 → G
  3. GPIO40 → B

Buzzer

  1. GPIO41 → Piezo buzzer

I²C pins

  1. GPIO8 → SDA
  2. GPIO9 → SCL
  3. Used by DS3231 RTC, BMI270 IMU, and MAX17048 fuel gauge.

GPS

  1. GPIO16 → RX
  2. GPIO17 → TX

2. Winbond W25Q128JV (16 MB)

The external flash IC stores the ESP32-S3’s firmware. It connects through a 4-bit SPI bus (QSPI mode) using CS, SCK, and IO0–IO3.

This allows fast code execution and large program storage needed for menus, GPS parsing, motion processing, and animations.

3. USB-C Connector

The USB-C port provides 5V power and programming access.

Although USB-C has up to 16 pins, the watch only needs:

  1. VBUS, GND (power)
  2. D+, D− (data)
  3. CC1 / CC2 each with a 5.1kΩ pull-down (Rd) so the host recognizes the watch as a 5V device.
  4. Other pins are unused but needed for physical compatibility.

4. CP2102N USB-to-UART Converter

This chip converts USB data into UART for the ESP32-S3 during programming.

It connects to the ESP32’s TX/RX, and its control pins toggle EN and BOOT to enter flashing mode automatically.

5. BQ24075 Charger IC (Li-Ion Charging + Power Management)

The BQ24075 handles safe charging of the Li-ion battery from the USB-C port.

Key features:

  1. 1A Li-ion charging with thermal regulation
  2. Power-path management → can run the watch directly from USB even without a battery
  3. SYS output gives a stable 4.4V system rail
  4. STAT1/STAT2 LEDs indicate charging status
  5. Safety timers + battery temperature input (TS pin)

This ensures safe and efficient charging, especially important for wearables.

6. PAM2401 Boost Converter (3.7V → 5V)

The PAM2401 boosts the battery voltage (3.0–4.2V) up to a stable 5V supply used for peripherals like the HCMS-2971 display.

It includes:

  1. Internal MOSFET switch
  2. Feedback network for output setting
  3. Input/output capacitors for stable operation
  4. Efficiency up to ~90%

This guarantees the watch gets a reliable 5V rail even when the battery is low.

7. 3.3V LDO Regulator

The ESP32-S3 and sensors require 3.3V, so the system voltage (battery or USB) is fed into a Low Dropout Regulator.

The LDO ensures:

  1. Clean, noise-free 3.3V
  2. Low quiescent current for better battery life
  3. Stable power during Wi-Fi/BLE peaks

All sensors and logic circuits use this regulated output.

8. HCMS-2971 Alphanumeric LED Display

The HCMS-2971 is an 8-character alphanumeric LED module with onboard driver circuitry.

Connections used:

  1. DATA, CLK, RS (Register Select), EN, RESET
  2. A current-set resistor (ISET) to define LED brightness
  3. Powered from 5V

Using the LedDisplay library, the ESP32 sends serial commands to update characters quickly and efficiently. Brightness is set via ISET + software control.

9. DS3231 Real-Time Clock (RTC)

The DS3231 keeps track of accurate time and date. It communicates over I²C (0x68) and includes:

  1. Built-in temperature-compensated crystal (high accuracy)
  2. Backup coin cell (CR2032) to keep time when battery is removed
  3. Two programmable alarms (INT/SQW output)
  4. Internal temperature sensor

The watch uses the RTClib library to read time and temperature.

10. MAX17048 Fuel Gauge

The MAX17048 measures battery voltage and percentage (SOC).

Features:

  1. I²C address 0x36
  2. Ultra-low power (~23 µA)
  3. ModelGauge™ algorithm for very accurate SOC
  4. No calibration or sense resistor needed
  5. Firmware uses QuickStart() for instant recalibration

This ensures stable and reliable battery readings in the watch.

11. BMI270 6-Axis IMU (Step Counter)

The BMI270 provides accelerometer + gyroscope data and includes a built-in hardware step counter.

Working:

  1. Detects wrist motion and gait cycles
  2. Internal motion engine increments a step counter register
  3. ESP32 reads steps using:
uint32_t stepCount;
imu.getStepCount(&stepCount);

Because step detection runs inside the chip, it uses very little power and is highly accurate.

12. GP-02 GPS Module (UART GPS Receiver)

The GP-02 GPS module communicates with the ESP32 through UART (Serial2).

It outputs standard NMEA sentences (e.g., GPGGA, GPRMC) at 9600 baud.

Using the TinyGPS++ library, the watch reads:

  1. Latitude
  2. Longitude
  3. Fix status
  4. Satellites
  5. Time (optional)

Connections:

  1. GPS TX → ESP32 RX
  2. GPS RX → ESP32 TX
  3. Power from 3.3V
  4. Backup battery (optional) for faster warm starts

13. RGB LED (PWM Control)

A three-channel RGB LED is driven using ESP32 LEDC PWM:

  1. LED_R = 38
  2. LED_G = 39
  3. LED_B = 40
  4. Each channel uses PWM to blend colors, allowing custom themes and animations.

14. Buttons (UP, DOWN, CENTER)

Three pushbuttons are connected with internal pull-ups and read using software debouncing.

They handle menu navigation, mode switching, stopwatch/timer control, and alarm canceling.

15. Buzzer (MLT-8650)

A piezo buzzer connected to GPIO 41 provides alarms and timer tones.

The ESP32’s tone() function plays melodies such as countdown completion or wake alarms.

16. Li-Ion Battery

A single-cell 3.7V Li-ion battery powers the entire watch.

Its voltage is monitored by the MAX17048 and charged safely by the BQ24075.


Auto-Programmer (Auto-Boot / Auto-Reset Circuit) — ESP32-S3

The ESP32-S3 requires a specific sequence on its EN (reset) and GPIO0 (boot mode) pins to enter download (UART flashing) mode. Instead of pressing buttons manually, the watch uses an auto-programmer circuit, controlled by the CP2102N USB-to-UART chip.

How it Works

The CP2102N has two modem control signals:

  1. DTR (Data Terminal Ready)
  2. RTS (Request To Send)

These pins are toggled automatically by the Arduino IDE or ESP-IDF when uploading code.

Using two small transistors, these signals generate the correct ESP32 boot sequence:

EN (Reset)

  1. Connected to RTS through an NPN transistor
  2. When RTS pulses low → ESP32-S3 resets

GPIO0 (Boot Mode)

  1. Connected to DTR through another NPN transistor
  2. When DTR pulses low during reset → ESP32 enters Bootloader mode

The ESP32 then waits for firmware over UART, and uploading begins automatically.

4-Layer PCB Stackup – Technical Overview

I1.png
I2.png
I3.png
I4.png

A 4-layer PCB stackup is used to support high-density routing, controlled impedance, and stable power distribution for high-speed digital and mixed-signal circuits. This type of structure helps maintain signal integrity for components such as microcontrollers, memory, GPS receivers, and various sensors commonly used in compact embedded systems.

Total PCB thickness: approximately 1.6 mm

Surface finish: ENIG (Nickel/Gold)

Functional Purpose of Each Layer:

Top Layer (L1) – Critical and High-Speed Routing

The top layer is used for components and short routing paths requiring minimum delay and impedance variation. Typical uses include:

  1. High-speed digital communication (e.g., flash memory buses)
  2. UART, SPI, and display interface traces
  3. Routing around power-management ICs and converters

This layer generally carries the densest routing.

Inner Layer 1 (L2) – Controlled-Impedance Signal Layer

L2 is dedicated to signals that require consistent impedance or reduced crosstalk. It is commonly used for:

  1. I²C buses
  2. Secondary SPI/QSPI routing
  3. USB differential pair routing
  4. The placement between dielectric layers provides stable impedance characteristics.

Inner Layer 2 (L3) – Low-Noise Signal and Power Routing

L3 supports low-noise or analog-related paths, as well as supplemental power routing. Typical usage includes:

  1. Battery sensing lines
  2. IMU auxiliary connections
  3. GPS-related reference signals
  4. It can also function as a semi-power plane to improve return-path integrity.

Bottom Layer (L4) – Auxiliary and Interface Routing

The bottom layer is used for less critical routing such as:

  1. Button and interface traces
  2. Buzzer and LED control paths
  3. Miscellaneous breakout routing for bottom-mounted devices

This layer provides additional routing capacity without impacting critical signal integrity.

Electrical Performance Considerations:

Controlled Impedance

The stackup supports:

  1. 50-ohm single-ended impedance (approximately 0.16 mm trace width depending on reference plane)
  2. 90-ohm differential impedance (approximately 0.16 mm width with 0.22 mm spacing)

This is suitable for:

  1. USB D+/D− lines
  2. High-speed QSPI interfaces
  3. Other differential or high-speed signaling environments

Electromagnetic Compatibility

The symmetric structure with internal signal layers helps:

  1. Reduce electromagnetic emissions
  2. Minimize coupling between analog and digital subsystems
  3. Improve overall noise performance

Power Integrity

Internal layers provide:

  1. Short, predictable return paths
  2. Reduced ground noise
  3. Improved voltage stability during high-load conditions (such as wireless transmission or display switching)

Via Structure:

The configuration supports:

  1. Through vias (L1–L4)
  2. Blind vias (L1–L2, L1–L3, L4–L3)
  3. Buried vias (L2–L3)

This provides routing flexibility and supports compact QFN/BGA device breakout.

Designing the PCB

L1.png
L2.png
L3.png
L4.png
WhatsApp Image 2025-12-01 at 01.26.16_db2b036b.jpg
WhatsApp Image 2025-12-01 at 01.26.16_b91f49ed.jpg
WhatsApp Image 2025-12-01 at 01.26.16_d659248d.jpg

Top Side Overview :

The top side contains the primary active components and high-speed interfaces. Major elements include:

Microcontroller Section

A central ESP32-S3 module is placed near the middle of the PCB. Its surrounding area includes the required decoupling capacitors, crystal oscillator, bootstrap components, and associated power-conditioning elements. The placement allows short, direct connections to the internal copper layers for signal routing and return-path integrity.

Display Interface

A large edge connector or pad array is positioned at the upper section, intended for interfacing with a segmented display or LED module. The interface lines are routed predominantly on the top and inner layers using controlled-width traces to minimize skew and maintain consistent impedance.

USB Type-C Port

The USB Type-C receptacle is mounted on the bottom left of the top side. It is surrounded by ESD protection components, configuration resistors, and a USB bridge IC. The differential USB data pair is routed with matched lengths and controlled impedance across the stackup.

Power-Management Components

Charging and voltage-boost circuitry occupy the upper-right region. This includes:

  1. Battery charger IC
  2. Power-path MOSFETs
  3. Inductors and diodes for the boost converter
  4. Output filtering capacitors

Their placement maintains short high-current loops, with wide copper pours and multiple via stitching for thermal and electrical performance.

Sensor and Peripherals

A GPS module is positioned at the upper left, isolated from noisy digital circuits. Nearby filtering and matching components support the module’s RF performance.

Additional ICs such as the RTC, fuel gauge, and IMU are distributed around the center and lower sections, each accompanied by local capacitors and pull-up resistors.

Mechanical and Interface Devices

Pushbuttons, tactile switches, and connectors are placed along the edges for accessibility from the enclosure. An RGB LED and buzzer footprint are also included.

Bottom Side Overview :

The bottom side contains smaller passive components, supplemental routing, and secondary functional blocks.

RTC Backup and Battery Interface

A coin-cell or supercapacitor footprint supports RTC backup. Passives around this area handle filtering, charge control, and battery isolation.

Additional Decoupling and Filtering

Many resistors, capacitors, and small passives populate the bottom side to support signal integrity and power filtering for the top-side ICs. Their placement directly underneath higher-speed digital ICs shortens via paths and reduces loop inductance.

Switches, Connectors, and Mechanical Features

Additional user-interface elements, such as toggle switches or rear-mounted buttons, are located along the edges. Their routing is isolated from analog and RF sections to avoid interference.

Layer Utilization:

The four layers are utilized efficiently:

Layer 1 (Top Copper)

High-speed, high-priority, and dense component-to-component routing. Most signal fan-out and sensitive nets begin here.

Layer 2 (Inner 1)

Used for structured, impedance-controlled routing and critical digital buses that require stable dielectric environments.

Layer 3 (Inner 2)

Carries filtered analog traces, power-distribution paths, and low-noise nets.

Layer 4 (Bottom Copper)

Used for non-critical routing, breakout paths for bottom-side components, and longer interface connections.

Copper pours on top and bottom assist in grounding, thermal dissipation, and shielding.

Routing Characteristics:

  1. Differential pairs (USB) are length-matched and routed with consistent spacing.
  2. Power traces near inductors and switching nodes are kept short to reduce EMI.
  3. High-density areas around the MCU use micro vias and via-in-pad structures where required.
  4. RF-sensitive sections are isolated and separated from high current loops.
  5. Via stitching surrounds the board perimeter to provide additional shielding and return path control.

The whole PCB is 50 x 45 mm in dimensions.

Soldering the Components

SOL1.jpg
SOL.jpg
SOL3.jpg
SOL4.jpg
SOL5.jpg
SOL6.jpg
WhatsApp Image 2025-12-01 at 08.22.17_e4908d09.jpg

Soldering all the components on the PCB was challenging due to the high component density and the variety of package sizes. To streamline the process, all resistors and capacitors were first soldered using a hot-plate reflow method. This allowed the smaller SMD components to settle evenly as the solder paste melted uniformly across the board. The top side, which contains the major ICs and fine-pitch parts, was also soldered using reflow, but this time with a hot-air gun to provide more control over individual components and ensure proper alignment during heating.

After completing the reflow stages on both sides, the remaining through-holes and mechanically sensitive parts, such as buttons, switches, and connectors, were soldered last using a standard soldering iron. This approach prevents heat damage to nearby components and allows precise manual placement where reflow is not ideal. Overall, the combination of hot-plate reflow, hot-air reflow, and manual soldering provided a reliable way to assemble the board despite its complexity.

Testing the PCB

WhatsApp Image 2025-11-30 at 19.21.05_e773dc2b.jpg
WhatsApp Image 2025-12-01 at 08.22.17_07f1d1e5.jpg

Once the assembly was completed, the PCB underwent a series of basic functional tests to verify that each subsystem was operating correctly. Power tests were performed first to ensure the charging circuit, voltage regulation, and battery connections were stable without overheating or abnormal current draw. Communication interfaces such as I²C, UART, and SPI were then checked to confirm proper response from the RTC, IMU, GPS module, and fuel gauge.

Programming the H‑CUBE

WhatsApp Image 2025-12-01 at 10.19.40_cb8b8572.jpg

Programming and firmware development were carried out using the Arduino IDE; I initially considered PlatformIO but ultimately chose the Arduino environment for its simplicity and direct integration with the board.

The code begins by including all the necessary libraries for handling the real-time clock, display, IMU, GPS module, preferences storage, and battery fuel gauge. These libraries allow each hardware block to work independently without having to write low-level drivers. After that, all display pins, sensor objects, and communication interfaces such as I²C and UART are defined. The HCMS-2971 LED display is created using its dedicated library so that text can be printed in simple commands.

Next, the code sets up core modules like the RTC, IMU, and GPS. The DS3231 RTC keeps accurate time, while the BMI270 IMU provides step counts. TinyGPS++ is used to read GPS data from the connected module through Serial2. PWM-based RGB LED control is also initialized using the ESP32’s LEDC hardware, so the color can be adjusted smoothly. The buzzer pin is configured for alarms and timer beeps.

A set of buttons (UP, DOWN, and CENTER) serves as the user interface for navigating menus and selecting options. The code also uses the ESP32 Preferences library to save user settings such as RGB color, alarm time, and default timer duration. This ensures that values remain stored even after powering off the watch. The MAX17048 fuel-gauge driver reads battery voltage and state-of-charge so these can be shown in the Battery menu.

To manage all the watch features, the code defines many state variables. These variables track whether the device is in the menu, adjusting brightness, viewing date, using the stopwatch, setting the alarm, running the timer, and more. Each main feature has its own display function, such as showing time, date, steps, GPS coordinates, temperature, or battery level. The stopwatch and timer include their own timing logic to handle start, pause, and reset operations.

The setup() function initializes everything: it starts communication buses, configures pin modes, powers up the display, starts the GPS module, checks the IMU connection, loads saved settings from flash, and prepares all features for use. Once setup is complete, the loop continuously runs the watch interface. The main loop checks button inputs, updates the GPS parser, and activates the correct mode depending on what the user has selected. Each mode is handled separately to keep the interface simple and predictable.

The menu system lets the user switch between different features. Holding the center button opens the menu, while UP and DOWN scroll through options such as Date, Steps, GPS, RGB settings, Alarm, Battery, Timer, and more. Pressing the center button selects an item and switches into that mode. The display is updated through a helper function that formats all text to fit the 8-character LED module.

Finally, the code includes full handling for alarms and the countdown timer. Both use the buzzer to play a sequence of tones, and the user can stop the sound with a button press. Every feature is kept modular, making the firmware easier to maintain and extend.

#include <Wire.h>
#include <RTClib.h>
#include <LedDisplay.h>
#include "SparkFun_BMI270_Arduino_Library.h"
#include <TinyGPSPlus.h>
#include <Preferences.h>
#include <SparkFun_MAX1704x_Fuel_Gauge_Arduino_Library.h>

// -------------------------
// HCMS-2971 connections
// -------------------------
#define dataPin 14
#define registerSelect 13
#define clockPin 12
#define enable 11
#define reset 10
#define displayLength 8
LedDisplay myDisplay = LedDisplay(dataPin, registerSelect, clockPin, enable, reset, displayLength);

// -------------------------
// RTC & IMU setup
// -------------------------
RTC_DS3231 rtc;
BMI270 imu;
uint8_t bmiAddress = BMI2_I2C_SEC_ADDR; // 0x69

// -------------------------
// GPS setup
// -------------------------
TinyGPSPlus gps;
#define GPS_RX 16
#define GPS_TX 17
#define gpsSerial Serial2

// -------------------------
// RGB LED (PWM outputs)
// -------------------------
#define LED_R 38
#define LED_G 39
#define LED_B 40
#define LEDC_FREQ 1000
#define LEDC_RES 8

int rgbR = 255, rgbG = 70, rgbB = 0; // default amber

// -------------------------
// Buzzer (Alarm output)
// -------------------------
#define BUZZER_PIN 41

// -------------------------
// Buttons
// -------------------------
#define BTN_UP 5
#define BTN_DOWN 6
#define BTN_CENTER 4

// -------------------------
// Preferences (Flash storage)
// -------------------------
Preferences prefs;

// -------------------------
// Battery Fuel Gauge (MAX17048)
// -------------------------
SFE_MAX1704X lipo(MAX1704X_MAX17048);
float lastVoltage = 0;
float lastSOC = 0;
bool isCharging = false;
unsigned long lastBatteryCheck = 0;
bool showBatteryMode = false;
bool chargingDisplayActive = false;
unsigned long lastChargeCheck = 0;

// -------------------------
// State variables
// -------------------------
bool inMenu = false;
bool brightnessSettingMode = false;
bool showDateMode = false;
bool showStepsMode = false;
bool showGPSMode = false;
bool showTempMode = false;
bool rgbMode = false;
bool stopwatchMode = false;
bool alarmMode = false;
bool alarmActive = false;
bool justEnteredMenu = false;

int menuIndex = 0;
const int totalMenus = 11; // includes Battery + Timer

// Timing
unsigned long lastInteraction = 0;
unsigned long pressStartTime = 0;
bool longPressTriggered = false;

const unsigned long holdTime = 5000;
const unsigned long sleepTimeout = 10000;

// Stopwatch
unsigned long stopwatchStart = 0;
unsigned long stopwatchElapsed = 0;
bool stopwatchRunning = false;

// Alarm
int alarmHour = 7;
int alarmMinute = 0;
bool alarmSet = false;
int alarmField = 0;
unsigned long lastBeep = 0;
bool beepState = false;

// Brightness
int currentBrightness = 15;

// Splash flag
bool showSplashNext = false;

// -------------------------
// Timer variables (NEW)
// -------------------------
bool timerMode = false;
bool timerRunning = false;
bool timerFinished = false;
unsigned long timerStartMillis = 0;
unsigned long timerDurationMillis = 0; // total countdown in ms (remaining when paused)
int timerSetMinutes = 0;
int timerSetSeconds = 30;
int timerSelectField = 0; // 0 = minutes, 1 = seconds
unsigned long timerLastUpdate = 0;

// -------------------------
// Helper
// -------------------------
void printDisplay(const char* str) {
char buf[9];
snprintf(buf, 9, "%-8s", str);
myDisplay.home();
myDisplay.print(buf);
}

void timerSavePrefs() {
prefs.begin("watchprefs", false);
prefs.putInt("timerMin", timerSetMinutes);
prefs.putInt("timerSec", timerSetSeconds);
prefs.end();
}

// -------------------------
// Setup
// -------------------------
void setup() {
Serial.begin(115200);
Wire.begin(8, 9);

pinMode(BUZZER_PIN, OUTPUT);
digitalWrite(BUZZER_PIN, LOW);

pinMode(BTN_UP, INPUT_PULLUP);
pinMode(BTN_DOWN, INPUT_PULLUP);
pinMode(BTN_CENTER, INPUT_PULLUP);

if (!rtc.begin()) {
Serial.println("Couldn't find RTC!");
while (1);
}
//rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));

myDisplay.begin();
myDisplay.setBrightness(currentBrightness);
myDisplay.clear();

while (imu.beginI2C(bmiAddress) != BMI2_OK) {
Serial.println("BMI270 not connected!");
delay(1000);
}
imu.enableFeature(BMI2_STEP_COUNTER);

gpsSerial.begin(9600, SERIAL_8N1, GPS_RX, GPS_TX);

// --- Battery gauge init ---
if (lipo.begin()) {
lipo.quickStart();
lastVoltage = lipo.getVoltage();
lastSOC = lipo.getSOC();
lastBatteryCheck = millis();
Serial.println("MAX17048 OK");
} else {
Serial.println("MAX17048 not detected!");
}

ledcAttach(LED_R, LEDC_FREQ, LEDC_RES);
ledcAttach(LED_G, LEDC_FREQ, LEDC_RES);
ledcAttach(LED_B, LEDC_FREQ, LEDC_RES);

prefs.begin("watchprefs", false);
rgbR = prefs.getInt("rgbR", 255);
rgbG = prefs.getInt("rgbG", 70);
rgbB = prefs.getInt("rgbB", 0);
alarmHour = prefs.getInt("alarmH", 7);
alarmMinute = prefs.getInt("alarmM", 0);
alarmSet = prefs.getBool("alarmOn", false);
// load timer prefs
timerSetMinutes = prefs.getInt("timerMin", 0);
timerSetSeconds = prefs.getInt("timerSec", 30);
prefs.end();

timerDurationMillis = (unsigned long)timerSetMinutes * 60000UL + (unsigned long)timerSetSeconds * 1000UL;

ledcWrite(LED_R, rgbR);
ledcWrite(LED_G, rgbG);
ledcWrite(LED_B, rgbB);

Serial.println("System Ready");
}

// -------------------------
// Loop
// -------------------------
void loop() {
unsigned long now = millis();
handleButtons(now);

while (gpsSerial.available() > 0)
gps.encode(gpsSerial.read());

// Alarm trigger
if (alarmSet && !alarmActive) {
DateTime nowt = rtc.now();
if (nowt.hour() == alarmHour && nowt.minute() == alarmMinute && nowt.second() == 0) {
alarmActive = true;
lastBeep = millis();
}
}
if (alarmActive) handleAlarmBeep();

// Timer finish buzzer handling (if timer finished and still in timerMode)
if (timerFinished) {
int notes[] = {523, 587, 659, 698, 784, 880, 988, 1047};
int count = sizeof(notes) / sizeof(notes[0]);
bool stoppedByButton = false;

for (int i = 0; i < count; i++) {
tone(BUZZER_PIN, notes[i], 500);
delay(650);
noTone(BUZZER_PIN);

// If user presses center, stop melody and consume the press
if (digitalRead(BTN_CENTER) == LOW) {
noTone(BUZZER_PIN);
timerFinished = false;
timerRunning = false;
timerMode = false; // <-- important: exit timer mode so it won't restart
digitalWrite(BUZZER_PIN, LOW);
myDisplay.clear();
lastInteraction = millis(); // keep UI awake briefly
stoppedByButton = true;
// small debounce delay to avoid immediate re-detection by button polling
delay(120);
break;
}
}

// If melody ended without button, still keep timerMode=false so user must re-enter timer menu
if (!stoppedByButton) {
timerMode = false; // require user to explicitly re-enter timer to restart
lastInteraction = millis();
}

printDisplay("00:00:00");
}


// Modes
if (alarmMode) { handleAlarmMode(); return; }
if (rgbMode) { handleRGBMode(); return; }
if (stopwatchMode) { showStopwatch(); return; }
if (timerMode) { showTimer(); return; } // Timer display & logic
if (showBatteryMode) { showBattery(); return; }

if (showTempMode) {
static unsigned long lastTempUpdate = 0;
if (millis() - lastTempUpdate > 1000) {
showTemp();
lastTempUpdate = millis();
}
if (digitalRead(BTN_CENTER) == LOW) {
delay(150);
showTempMode = false;
myDisplay.clear();
}
return;
}

if (showDateMode) { showDate(); if (now - lastInteraction > 5000) { showDateMode = false; myDisplay.clear(); } return; }
if (showStepsMode) { showSteps(); if (now - lastInteraction > 5000) { showStepsMode = false; myDisplay.clear(); } return; }
if (showGPSMode) { showGPS(); if (now - lastInteraction > 5000) { showGPSMode = false; myDisplay.clear(); } return; }

if (!inMenu && !brightnessSettingMode) {
if (millis() - lastInteraction < sleepTimeout) showTime();
else myDisplay.clear();
} else showMenu();
}

// -------------------------
// Displays
// -------------------------
void showTime() {
if (showSplashNext) {
myDisplay.clear();
delay(50);
myDisplay.home();
myDisplay.print(" H-CUBE ");
delay(1200);
myDisplay.clear();
showSplashNext = false;
}

DateTime now = rtc.now();
char buffer[9];
snprintf(buffer, 9, "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
printDisplay(buffer);
}
void showDate() {
DateTime now = rtc.now();
char buffer[9];

// Step 1: Show Date
snprintf(buffer, 9, "%02d/%02d/%02d", now.day(), now.month(), now.year() % 100);
printDisplay(buffer);
delay(2000);

// Step 2: Show Day
const char* daysOfWeek[] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"};
int dayIndex = now.dayOfTheWeek(); // from RTClib
snprintf(buffer, 9, "%-8s", daysOfWeek[dayIndex]);
printDisplay(buffer);
delay(2000);

// Step 3: Clear & exit
showDateMode = false;
myDisplay.clear();
}

void showSteps() {
uint32_t stepCount = 0;
imu.getStepCount(&stepCount);
char buffer[9];
snprintf(buffer, 9, "%8lu", stepCount);
printDisplay(buffer);
}
void showTemp() {
float tempC = rtc.getTemperature() - 1.0;
char buf[9];
snprintf(buf, 9, "TMP:%02.1fC", tempC);
printDisplay(buf);
}
void showGPS() {
if (gps.location.isValid()) {
char latStr[9], lonStr[9];
snprintf(latStr, 9, "LT:%05.2f", gps.location.lat());
snprintf(lonStr, 9, "LO:%05.2f", gps.location.lng());
printDisplay(latStr);
delay(2000);
printDisplay(lonStr);
delay(2000);
} else {
printDisplay("NO GPS");
delay(2000);
}
showGPSMode = false;
}

// -------------------------
// Timer display & logic
// -------------------------
void showTimer() {
unsigned long now = millis();

// If finished -> show 00:00 (handled in loop buzzer)
if (timerFinished) {
printDisplay("00:00:00");
return;
}

if (!timerRunning) {
// Setting mode display: show "SETMM:SS", selected field blinks OFF
static unsigned long blinkT = 0;
bool blinkOn = ((now / 400) & 1) == 0; // blink 400ms

char buf[9];
int displayMin = timerSetMinutes;
int displaySec = timerSetSeconds;

if (timerSelectField == 0 && !blinkOn) displayMin = -1; // use -1 to indicate blank
if (timerSelectField == 1 && !blinkOn) displaySec = -1;

if (displayMin == -1) {
// blank minutes during blink OFF
snprintf(buf, 9, "SET :%02d", displaySec);
} else if (displaySec == -1) {
snprintf(buf, 9, "SET%02d: ", displayMin);
} else {
snprintf(buf, 9, "SET%02d:%02d", displayMin, displaySec);
}
printDisplay(buf);
} else {
// timer running -> show remaining time (update fast enough)
unsigned long elapsed = millis() - timerStartMillis;
if (elapsed >= timerDurationMillis) {
// finished
timerRunning = false;
timerFinished = true;
printDisplay("00:00:00");
return;
} else {
unsigned long remaining = timerDurationMillis - elapsed;
unsigned int mins = (remaining / 60000UL) % 60;
unsigned int secs = (remaining / 1000UL) % 60;
unsigned int hund = (remaining % 1000UL) / 10;
char buf[9];
snprintf(buf, 9, "%02d:%02d:%02d", mins, secs, hund);
printDisplay(buf);
}
}
}

// -------------------------
// Stopwatch
// -------------------------
void showStopwatch() {
static unsigned long lastUpdate = 0;
unsigned long now = millis();
if (now - lastUpdate >= 100) {
unsigned long elapsed = stopwatchRunning ? millis() - stopwatchStart + stopwatchElapsed : stopwatchElapsed;
unsigned int minutes = (elapsed / 60000) % 60;
unsigned int seconds = (elapsed / 1000) % 60;
unsigned int hundredths = (elapsed % 1000) / 10;
char buf[9];
snprintf(buf, 9, "%02d:%02d:%02d", minutes, seconds, hundredths);
printDisplay(buf);
lastUpdate = now;
}
static bool centerPrev = HIGH;
bool center = digitalRead(BTN_CENTER);
if (centerPrev == HIGH && center == LOW) {
unsigned long pressStart = millis();
while (digitalRead(BTN_CENTER) == LOW) delay(10);
unsigned long pressDuration = millis() - pressStart;
if (pressDuration < 800) {
if (!stopwatchRunning) { stopwatchRunning = true; stopwatchStart = millis(); }
else { stopwatchRunning = false; stopwatchElapsed += millis() - stopwatchStart; }
} else {
stopwatchRunning = false;
stopwatchElapsed = 0;
stopwatchMode = false;
myDisplay.clear();
}
}
centerPrev = center;
}

// -------------------------
// Battery Menu
// -------------------------
void showBattery() {
float voltage = lipo.getVoltage();
float soc = lipo.getSOC();
char buf[9];
snprintf(buf, 9, "BAT:%.2f", voltage);
printDisplay(buf);
delay(2000);
snprintf(buf, 9, "SOC:%02d%%", (int)soc);
printDisplay(buf);
delay(2000);
showBatteryMode = false;
myDisplay.clear();
}

// -------------------------
// RGB Mode
// -------------------------
void handleRGBMode() {
static int colorIndex = 0;
int *color = (colorIndex == 0 ? &rgbR : colorIndex == 1 ? &rgbG : &rgbB);
const char *label = (colorIndex == 0 ? "RED" : colorIndex == 1 ? "GRN" : "BLU");
static bool upPrev = HIGH, downPrev = HIGH, centerPrev = HIGH;
static unsigned long lastChangeTime = 0;
static unsigned long holdStartUp = 0, holdStartDown = 0;
const unsigned long slowDelay = 150, fastDelay = 40;
bool up = digitalRead(BTN_UP), down = digitalRead(BTN_DOWN), center = digitalRead(BTN_CENTER);
unsigned long now = millis();
if (up == LOW) {
if (holdStartUp == 0) holdStartUp = now;
unsigned long delayVal = map(constrain(now - holdStartUp, 0, 3000), 0, 3000, slowDelay, fastDelay);
if (now - lastChangeTime > delayVal) { *color = min(255, *color + 1); lastChangeTime = now; }
} else holdStartUp = 0;
if (down == LOW) {
if (holdStartDown == 0) holdStartDown = now;
unsigned long delayVal = map(constrain(now - holdStartDown, 0, 3000), 0, 3000, slowDelay, fastDelay);
if (now - lastChangeTime > delayVal) { *color = max(0, *color - 1); lastChangeTime = now; }
} else holdStartDown = 0;
ledcWrite(LED_R, rgbR);
ledcWrite(LED_G, rgbG);
ledcWrite(LED_B, rgbB);
char buf[9]; snprintf(buf, 9, "%s:%03d", label, *color); printDisplay(buf);
if (centerPrev == HIGH && center == LOW) {
colorIndex++;
if (colorIndex > 2) {
prefs.begin("watchprefs", false);
prefs.putInt("rgbR", rgbR);
prefs.putInt("rgbG", rgbG);
prefs.putInt("rgbB", rgbB);
prefs.end();
rgbMode = false;
colorIndex = 0;
myDisplay.clear();
return;
}
delay(200);
}
upPrev = up; downPrev = down; centerPrev = center;
}

// -------------------------
// Alarm Logic
// -------------------------
void handleAlarmMode() {
static bool upPrev = HIGH, downPrev = HIGH, centerPrev = HIGH;
bool up = digitalRead(BTN_UP), down = digitalRead(BTN_DOWN), center = digitalRead(BTN_CENTER);
if (upPrev == HIGH && up == LOW) {
if (alarmField == 0) alarmHour = (alarmHour + 1) % 24;
else alarmMinute = (alarmMinute + 1) % 60;
}
if (downPrev == HIGH && down == LOW) {
if (alarmField == 0) alarmHour = (alarmHour + 23) % 24;
else alarmMinute = (alarmMinute + 59) % 60;
}
char buf[9]; snprintf(buf, 9, "%02d:%02d", alarmHour, alarmMinute); printDisplay(buf);
if (centerPrev == HIGH && center == LOW) {
alarmField++;
if (alarmField > 1) {
alarmField = 0;
alarmMode = false;
alarmSet = true;
prefs.begin("watchprefs", false);
prefs.putInt("alarmH", alarmHour);
prefs.putInt("alarmM", alarmMinute);
prefs.putBool("alarmOn", true);
prefs.end();
myDisplay.clear();
}
}
upPrev = up; downPrev = down; centerPrev = center;
}

void handleAlarmBeep() {
// melody notes (example scale)
int notes[] = {523, 587, 659, 698, 784, 880, 988, 1047};
int count = sizeof(notes) / sizeof(notes[0]);

for (int i = 0; i < count; i++) {
tone(BUZZER_PIN, notes[i], 500);
delay(650);
noTone(BUZZER_PIN);

// allow stopping alarm by button press
if (digitalRead(BTN_UP) == LOW ||
digitalRead(BTN_DOWN) == LOW ||
digitalRead(BTN_CENTER) == LOW) {
noTone(BUZZER_PIN);
alarmActive = false;
return;
}
}
}


// -------------------------
// Buttons + Menu (includes Timer handling)
// -------------------------
void handleButtons(unsigned long now) {
// Timer mode override: handle timer-specific button logic first
if (timerMode) {
static bool upPrev = HIGH, downPrev = HIGH, centerPrev = HIGH;
bool up = digitalRead(BTN_UP), down = digitalRead(BTN_DOWN), center = digitalRead(BTN_CENTER);

// UP/DOWN adjust selected field when not running and not finished
if (!timerRunning && !timerFinished) {
if (upPrev == HIGH && up == LOW) {
if (timerSelectField == 0) timerSetMinutes = min(59, timerSetMinutes + 1);
else timerSetSeconds = min(59, timerSetSeconds + 1);
timerDurationMillis = (unsigned long)timerSetMinutes * 60000UL + (unsigned long)timerSetSeconds * 1000UL;
timerSavePrefs();
}
if (downPrev == HIGH && down == LOW) {
if (timerSelectField == 0) timerSetMinutes = max(0, timerSetMinutes - 1);
else timerSetSeconds = max(0, timerSetSeconds - 1);
timerDurationMillis = (unsigned long)timerSetMinutes * 60000UL + (unsigned long)timerSetSeconds * 1000UL;
timerSavePrefs();
}
}

// CENTER: short press toggles start/pause; long press (>=1000ms) stops & exit; while setting, center cycles field on quick release
static unsigned long centerDownAt = 0;
if (centerPrev == HIGH && center == LOW) centerDownAt = now;

if (centerPrev == LOW && center == HIGH) {
unsigned long held = now - centerDownAt;
if (held < 1000) { // short
if (!timerFinished) {
if (!timerRunning) {
// start: set the start time reference and keep timerDurationMillis as total duration
timerRunning = true;
timerStartMillis = millis();
} else {
// pause: compute remaining duration and store it in timerDurationMillis
unsigned long elapsed = millis() - timerStartMillis;
if (elapsed < timerDurationMillis) timerDurationMillis -= elapsed;
else timerDurationMillis = 0;
timerRunning = false;
}
} else {
// finished: stop buzzer, clear done, exit timer
timerFinished = false;
timerMode = false;
digitalWrite(BUZZER_PIN, LOW);
myDisplay.clear();
}
} else { // long press -> reset & exit
timerRunning = false;
timerFinished = false;
timerMode = false;
// reload saved values
prefs.begin("watchprefs", false);
timerSetMinutes = prefs.getInt("timerMin", 0);
timerSetSeconds = prefs.getInt("timerSec", 30);
prefs.end();
timerDurationMillis = (unsigned long)timerSetMinutes * 60000UL + (unsigned long)timerSetSeconds * 1000UL;
myDisplay.clear();
}
}

// cycle selected field on short tap of center while not running (we detect quick tap by checking rising edge and held < 300ms)
// To avoid interfering with start, we instead detect a quick press-release where we didn't start the timer.
static unsigned long lastCenterReleased = 0;
if (centerPrev == LOW && center == HIGH) {
unsigned long held = now - centerDownAt;
if (held < 300 && !timerRunning && !timerFinished) {
// cycle field
timerSelectField = (timerSelectField + 1) % 2;
}
lastCenterReleased = now;
}

upPrev = up;
downPrev = down;
centerPrev = center;
if (up == LOW || down == LOW || center == LOW) lastInteraction = now;
return;
}

// Non-timer path: keep original behavior but retain timer check earlier
if (stopwatchMode) {
if (digitalRead(BTN_UP) == LOW || digitalRead(BTN_DOWN) == LOW) lastInteraction = now;
return;
}

static bool upPrev = HIGH, downPrev = HIGH, centerPrev = HIGH;
bool up = digitalRead(BTN_UP), down = digitalRead(BTN_DOWN), center = digitalRead(BTN_CENTER);
if (centerPrev == HIGH && center == LOW) { pressStartTime = now; longPressTriggered = false; }
if (centerPrev == LOW && center == LOW && (now - pressStartTime) > holdTime && !longPressTriggered && !inMenu) {
inMenu = true; menuIndex = 0; longPressTriggered = true; justEnteredMenu = true; myDisplay.clear(); delay(200);
}
if (centerPrev == LOW && center == HIGH) {
if (justEnteredMenu) justEnteredMenu = false;
else if (brightnessSettingMode) brightnessSettingMode = false, inMenu = false;
else if (inMenu && !longPressTriggered) { selectMenu(); showSplashNext = true; }
}
if (brightnessSettingMode) {
if (upPrev == HIGH && up == LOW && currentBrightness < 15) myDisplay.setBrightness(++currentBrightness);
if (downPrev == HIGH && down == LOW && currentBrightness > 0) myDisplay.setBrightness(--currentBrightness);
} else if (inMenu) {
if (upPrev == HIGH && up == LOW) menuIndex = (menuIndex - 1 + totalMenus) % totalMenus;
if (downPrev == HIGH && down == LOW) menuIndex = (menuIndex + 1) % totalMenus;
}
upPrev = up; downPrev = down; centerPrev = center;
if (up == LOW || down == LOW || center == LOW) lastInteraction = now;
}

// -------------------------
// Menu
// -------------------------
void showMenu() {
const char *menus[11] = {"Date","Steps","Bright","GPS","RGB","StopWtch","Alarm","Temp","Battery","Timer","Back"};
if (brightnessSettingMode) { char buf[9]; snprintf(buf, 9, "BR:%02d", currentBrightness); printDisplay(buf); }
else printDisplay(menus[menuIndex]);
}

void selectMenu() {
switch (menuIndex) {
case 0: inMenu=false; showDateMode=true; break;
case 1: inMenu=false; showStepsMode=true; break;
case 2: brightnessSettingMode=true; break;
case 3: inMenu=false; showGPSMode=true; break;
case 4: inMenu=false; rgbMode=true; break;
case 5: inMenu=false; stopwatchMode=true; stopwatchRunning=false; stopwatchElapsed=0; break;
case 6: inMenu=false; alarmMode=true; break;
case 7: inMenu=false; showTempMode=true; break;
case 8: inMenu=false; showBatteryMode=true; break;
case 9: inMenu=false; timerMode=true; timerRunning=false; timerFinished=false; timerSelectField=0; timerDurationMillis = (unsigned long)timerSetMinutes * 60000UL + (unsigned long)timerSetSeconds * 1000UL; break;
case 10: inMenu=false; break;
}
myDisplay.clear();
}

Downloads

Designing the Case

hhh.jpg
WhatsApp Image 2025-12-01 at 08.55.05_b1fedd39.jpg
WhatsApp Image 2025-12-01 at 08.55.05_8e6efad0.jpg
Screenshot 2025-12-01 084742.png
Screenshot 2025-12-01 084723.png
sdfd.png
Screenshot 2025-12-01 084840.png
Screenshot 2025-12-01 085133.png
Screenshot 2025-12-01 085239.png
Screenshot 2025-12-01 085341.png
Screenshot 2025-12-01 085436.png
WhatsApp Image 2025-12-01 at 08.59.00_10d78f65.jpg
WhatsApp Image 2025-12-01 at 10.16.09_7a0cc01c.jpg
WhatsApp Image 2025-12-01 at 10.16.09_3f4cb786.jpg

The designing was done using Fusion 360, while the rendering was carried out in KeyShot and Rhino. The design was inspired by a teenage engineering, minimalistic style that emphasizes clean geometry and strong functional cues.

Design tools

Fusion 360 was used to model all the enclosure parts as separate solid bodies, including the main case, back plate, button caps, and decorative inserts, ensuring everything is dimensionally accurate for PCB and strap integration. KeyShot and Rhino were then used to assign materials, lighting, and environments, which helped visualize the final watch in different colorways such as matte black, brushed metal, and bright accent pieces.

Visual style

The overall look takes cues from teenage engineering’s products, focusing on simple blocks, rounded edges, and precise perforation patterns to keep the surface visually light while remaining robust. The layout of the vented speaker grid, recessed display window, and stepped side details creates a balanced front face that feels retro yet refined rather than overly busy.

Functional detailing

Every exterior feature corresponds to a real interaction: the vent grid aligns with the buzzer/speaker, the side cutouts house the slider and push buttons, and the USB‑C opening is centered to match the PCB connector. The removable top plates and back cover are designed for easy assembly and servicing, with clearances around the PCB, standoffs, and strap interface so the watch can be printed, assembled, and modified without redesigning the core body.

3D Printing and Assembly

WhatsApp Image 2025-12-01 at 09.06.40_006b8f06.jpg
WhatsApp Image 2025-12-01 at 09.06.16_f6a9e7f3.jpg
WhatsApp Image 2025-12-01 at 09.06.16_49940ccb.jpg
WhatsApp Image 2025-12-01 at 09.06.15_ceb78117.jpg
WhatsApp Image 2025-12-01 at 09.06.14_e8a804d8.jpg
WhatsApp Image 2025-12-01 at 09.06.12_6fb5c145.jpg
WhatsApp Image 2025-12-01 at 09.06.12_1d98a646.jpg

3D printing was used to turn the H‑CUBE CAD model into a physical, wearable prototype, with tuned slicer settings to balance surface quality, strength, and print time. The case and inserts were printed separately, then assembled and lightly post‑processed to highlight the two‑tone, retro aesthetic.

Printer and material

For this kind of enclosure, an FDM printer with a 0.4 mm nozzle and PLA filament is ideal because PLA is easy to print, has low warping, and gives crisp detail on small features like perforation holes and button slots. PLA typically prints best with a nozzle temperature around 190–210 °C and a heated bed at 40–60 °C, so those values are a good starting point before fine‑tuning per filament brand.​

Core slicer settings

Key slicer parameters were chosen to match the small size of the watch and the need for strong walls around the strap slot. A layer height around 0.16–0.2 mm offers a good compromise between detail and print time for a 0.4 mm nozzle, while 2–3 perimeter walls (about 0.8–1.2 mm total) give the housing enough stiffness without wasting material. An infill of roughly 20–35 % in a grid or gyroid pattern is usually sufficient for non‑load‑bearing electronics enclosures, since most of the strength comes from the outer walls.​

Orientation and supports

The main body is best printed with the back face on the bed so that the visible top surface and perforated plate come out smoother and require minimal support removal. Overhangs steeper than about 45° and the recessed display cavity may still need support structures, so enabling automatic supports with a 45° overhang threshold and a low support density makes removal easier and reduces scarring on the visible faces.​

Speed, cooling, and adhesion

Moderate print speeds (about 40–60 mm/s for outer walls) help small features like the speaker holes print cleanly and reduce ringing on the sharp edges. A cooling fan set fairly high after the first few layers, combined with proper bed adhesion (clean surface, light glue or tape if needed), keeps PLA from warping and preserves the tight tolerances needed for push‑fit inserts and buttons.​

Post‑processing and assembly

After printing, light sanding of the mating faces and corners removes layer lines and helps the silver top plates and orange buttons slide into place with a snug fit. Any residual support material around the strap slot, side sliders, and display window is trimmed away so that the strap feeds cleanly and the mechanical controls move freely without binding.

A Small Demo Video

Custom Smartwatch Showcase &quot; H-CUBE &quot;

This video is a short showcase of the custom-built H‑CUBE smartwatch, walking through the main firmware features. The NO GPS is because the video was shot indoors.

Conclusion

WhatsApp Image 2025-12-01 at 08.22.16_12ea14f2.jpg
WhatsApp Image 2025-12-01 at 08.22.17_742585a8.jpg
WhatsApp Image 2025-11-30 at 19.22.29_a219ab0c.jpg

Over the past two months, I’ve learned a lot, especially about 4-layer PCB stack-ups, CAD design, impedance control, and matching circuits. It has been a challenging but rewarding journey. Component selection alone took nearly two weeks, as I carefully studied each datasheet to ensure every part was suitable for the design. There were a few mistakes in the initial PCB layout as well, but they were corrected with a bit of creativity and improvisation. The board can definitely be made smaller in the next revision, and there’s room to add more features, such as biosensors for SpO₂ and other health metrics.