Skip to content

Repository files navigation

Reverse Engineering a €5 AliExpress Weather Clock: A Security Story

Release License Issues Hardware Status

TL;DR

I bought a cute weather clock kit from AliExpress (TJ-56-654) and discovered it was leaking my WiFi password in plaintext to anyone within radio range. So I ripped out the firmware, wrote my own, and ended up with a fully async, OTA-updatable, Home Assistant-ready smart clock that's actually secure.


Table of Contents

Story

Use it

For internal architecture notes (state machines, memory, EEPROM layout), see docs/ARCHITECTURE.md.


The Discovery: When "Smart" Means "Insecure"

It started innocently enough. I ordered what looked like a fun DIY electronics project: an ESP8266-based weather clock with a transparent acrylic case and an OLED display. The listing promised:

  • ✅ WiFi weather updates
  • ✅ 3-day forecast
  • ✅ Temperature, humidity, date/time
  • ✅ "Intelligent connected to WIFI"

What they didn't mention:

🚨 CRITICAL SECURITY FLAW 🚨

When you first set up the device, it creates an access point with a default password. Fair enough - that's how WiFiManager works. But here's where it gets bad:

  1. You connect to the AP (192.168.4.1)
  2. You configure your home WiFi credentials
  3. Device connects to your network
  4. The open AP stays active in parallel
  5. Your WiFi password is displayed in plaintext on the config page

Anyone within WiFi range could:

  • Connect to the device's AP (weak default password)
  • Browse to 192.168.4.1
  • Read your WiFi password in plaintext
  • Access your network

This is a textbook example of poor IoT security design. No thanks.


The Device

Product: ESP8266 Mini Weather Clock Kit Model: TJ-56-654 Price: ~€5 EUR Source: AliExpress Link

Original Hardware Specifications

Component Details
MCU ESP-01S (ESP8266EX, 1MB flash, 80KB RAM)
Display GM009605v4.3 OLED (128x64, I2C)
Power 5V USB (Micro-USB)
Case Transparent acrylic (40x40x43mm)
PCB TJ-56-654 main board

What It Came With

  • Acrylic case parts (6 pieces)
  • ESP-01S WiFi module
  • OLED display module
  • Main PCB with headers
  • USB power cable
  • Brass standoffs and screws
  • Pin headers (soldering required)

Original Firmware Issues

Beyond the password leak:

  • Dependency on QWeather API: Requires account registration, project setup, API key management
  • Chinese cloud service: All weather data routes through proprietary servers
  • No OTA updates: Firmware updates require disassembly and FTDI connection
  • Limited features: Fixed display modes, no customization
  • Unknown code: Closed-source firmware, no way to audit what it's doing

The Investigation

Opening It Up

The transparent case made inspection easy - just unscrew the brass standoffs. Inside:

  • ESP-01S module clearly labeled with pinout
  • I2C OLED display connected via 4 pins (VCC, GND, SDA, SCL)
  • No additional sensors (temperature/humidity were from weather API, not local)

The ESP-01S pinout is printed right on the PCB:

3V3  |  GND
 TX  |  GPIO0  (I2C SDA)
 RX  |  GPIO2  (I2C SCL)
EN   |  GND

Connecting FTDI

To flash custom firmware, you need:

  1. FTDI USB-to-Serial adapter (3.3V! Not 5V - you'll fry the ESP8266)
  2. Jumper wires
  3. Steady hands

Wiring:

FTDI        ESP-01S
────────────────────
 3V3    →   3V3
 GND    →   GND
 TX     →   RX
 RX     →   TX
 GND    →   GPIO0  (for programming mode)

Boot into flash mode:

  1. Connect GPIO0 to GND
  2. Power on the device
  3. Remove GPIO0 to GND connection after boot
  4. Device is now in programming mode

Programming:

  • Use Arduino IDE with ESP8266 board support
  • Select board: "Generic ESP8266 Module"
  • Flash size: 1MB (FS:64KB OTA:~470KB)
  • Upload speed: 115200 baud

After the first flash with OTA support, you never need wires again - all updates happen over WiFi.


The Solution: Custom Firmware

I decided to write a complete replacement firmware with:

Core Principles

  1. Security First: No hardcoded credentials, no open networks, WiFiManager with proper AP timeout
  2. Privacy: Use free, open APIs (Open-Meteo instead of QWeather)
  3. Maintainability: OTA updates for painless improvements
  4. Performance: Fully async architecture, no blocking operations
  5. Reliability: Proper error handling, exponential backoff, memory safety

Features Implemented

🌐 Network & Time

  • WiFiManager captive portal for secure first-time setup
  • Hybrid WiFi: Synchronous on boot (ensures proper init), async reconnect during operation
  • NTP time sync with configurable server and interval
  • Timezone support with automatic European DST calculation
  • mDNS: Access via http://tj56654-clock.local/

🌦️ Weather Data

  • Open-Meteo API: Free, no registration, no API key
  • Configurable location: Latitude/longitude + city name
  • Data: Temperature, sunrise, sunset, daylight duration
  • Smart updates: Async fetch every 30 minutes (configurable)

🔄 OTA Updates

  • Web-based OTA: Upload .bin files via browser at /update
  • ArduinoOTA: Update directly from Arduino IDE
  • Non-blocking: System stays responsive during updates
  • Secure: Password-protected upload (admin/admin - change it!)

📺 Display Modes

Three rotating display screens (configurable interval):

  1. Time Mode

    • Large HH:MM display
    • Blinking colon animation
    • Day of week and date
    • 12/24 hour format support
  2. Weather Mode

    • Temperature with superscript °c
    • City name
    • Clean, minimalist layout
  3. Sunrise/Sunset Mode

    • Sunrise time with ↑ arrow
    • Sunset time with ↓ arrow
    • Daylight duration (e.g., "Day 9h 41m")

All modes are center-aligned, rotation-aware, and gracefully handle missing data.

🌐 Web Interface

  • / - Home page with live time
  • /config - Full configuration form
  • /debug - System diagnostics
  • /update - OTA firmware upload

🔌 REST API

All endpoints return JSON:

  • GET /api/time - Current time
  • GET /api/status - System status (WiFi, uptime, heap)
  • GET /api/debug - Detailed diagnostics
  • GET /api/weather - Weather + sunrise/sunset
  • GET /api/config - Export configuration
  • POST /api/config - Import configuration
  • POST /api/eeprom-clear - Factory reset
  • POST /api/reboot - Remote reboot

Hardware Quirk: Swapped I2C Pins

ESP-01S exposes only GPIO0 and GPIO2. The TJ-56-654 board designer used them as I2C — but swapped from typical breakouts:

Wire.begin(0, 2);  // SDA=GPIO0, SCL=GPIO2 (non-standard!)

Standard ESP8266 boards use GPIO4=SDA, GPIO5=SCL. Easy to miss if you're porting code from a different ESP8266 setup. The display is a GM009605v4.3 OLED (SSD1306-compatible) at I2C address 0x3C.

For architecture details (async state machines, memory budget, EEPROM layout, factory reset), see docs/ARCHITECTURE.md.

Version History

The clock has gone through many iterations — display hardware discovery (v1.5–v1.7), stability and security fixes (v1.8), full async refactor (v1.9.0), and a long series of bug fixes informed by community reports and AI-assisted code review (v1.9.1–v1.9.9).

See CHANGELOG.md for the full version history with technical details.


How to Flash This Firmware

Requirements

  • Hardware: TJ-56-654 weather clock or compatible ESP-01S + OLED setup
  • FTDI Adapter: 3.3V USB-to-Serial (CP2102, FT232RL, CH340)
  • Software: Arduino IDE 1.8.x or 2.x

Arduino IDE Setup

  1. Install ESP8266 Board Support

    • File → Preferences
    • Additional Board Manager URLs: http://arduino.esp8266.com/stable/package_esp8266com_index.json
    • Tools → Board → Boards Manager → Search "ESP8266" → Install
  2. Install Required Libraries

    • Sketch → Include Library → Manage Libraries
    • Install:
      • Adafruit GFX Library
      • Adafruit SSD1306
      • NTPClient
      • WiFiManager (by tzapu)
      • AsyncHTTPRequest_Generic
      • ESPAsyncTCP
      • ArduinoJson (by Benoit Blanchon)
  3. Board Configuration

    • Board: "Generic ESP8266 Module"
    • Flash Size: "1MB (FS:64KB OTA:~470KB)"
    • Flash Mode: "DIO"
    • Flash Frequency: "40MHz"
    • CPU Frequency: "80MHz"
    • Upload Speed: "115200"

First Flash (via FTDI)

  1. Wire the ESP-01S:

    FTDI 3.3V  →  ESP-01S 3V3
    FTDI GND   →  ESP-01S GND
    FTDI TX    →  ESP-01S RX
    FTDI RX    →  ESP-01S TX
    FTDI GND   →  ESP-01S GPIO0 (boot mode)
    
  2. Compile and Upload:

    • Open weather_clock.ino
    • Sketch → Upload
    • Wait for "Done uploading"
    • Remove GPIO0-to-GND jumper
    • Press reset or power cycle
  3. Initial Setup:

    • Device creates AP: "TJ56654-Setup"
    • Connect to it (password: 12345678)
    • Captive portal opens automatically
    • Select your WiFi network and enter password
    • Device reboots and connects

Subsequent Updates (OTA)

  1. Via Web Interface (easiest):

    • Browse to http://192.168.x.x/update (find IP from router)
    • Or use mDNS: http://tj56654-clock.local/update
    • Login: admin / admin
    • Choose .bin file from build/ folder
    • Click "Update"
    • Device reboots automatically (~15 seconds)
  2. Via Arduino IDE:

    • Tools → Port → Select "tj56654-clock at 192.168.x.x"
    • Sketch → Upload
    • No wires needed!

Web Interface

Home Page (/)

Current time display with live updates via JavaScript (fetches /api/time every second).

Configuration Page (/config)

Comprehensive settings form:

WiFi Settings

  • SSID
  • Password
  • Hostname (for mDNS)

Time Settings

  • Timezone offset (seconds from UTC)
  • DST enabled (European rules)
  • NTP server address
  • NTP sync interval (seconds)
  • Hour format (12h/24h)

Weather Settings

  • Enabled/disabled toggle
  • Latitude
  • Longitude
  • City name (for display)
  • Update interval (seconds)

Display Settings

  • Brightness (0-7)
  • Rotation (0°, 90°, 180°, 270°)
  • Display rotation interval (seconds)
  • Show weather screen (toggle)
  • Show sunrise/sunset screen (toggle)

All settings persist to EEPROM and survive reboots.

Debug Page (/debug)

Real-time diagnostics:

  • System: Uptime, free heap, chip ID, flash size
  • WiFi: SSID, IP, signal strength, MAC address, gateway, DNS
  • Time: Current time, timezone, DST status, NTP sync status
  • NTP Stats: Last sync, attempts, successes, failures
  • Weather: Temperature, sunrise/sunset, last update, API status
  • Network Tests: Internet connectivity, DNS resolution
  • Display: Current mode, rotation, brightness

Perfect for troubleshooting connectivity or API issues.


API Documentation

All endpoints return JSON (except /update which is for file upload).

GET /api/time

Current time information.

Response:

{
  "current": "14:23:45",
  "date": "2026-01-03",
  "day": "Friday",
  "timezone_offset": 0,
  "dst_active": false
}

GET /api/status

System status overview.

Response:

{
  "wifi": {
    "ssid": "MyNetwork",
    "ip": "192.168.1.47",
    "rssi": -38,
    "hostname": "tj56654-clock"
  },
  "time": {
    "current": "14:23:45",
    "timezone_offset": 0,
    "ntp_synced": true
  },
  "system": {
    "uptime": 3627,
    "free_heap": 35104,
    "chip_id": "f77134"
  }
}

GET /api/weather

Current weather data.

Response:

{
  "temperature": 15.4,
  "city": "Portimao",
  "sunrise": "07:48",
  "sunset": "17:29",
  "daylight_hours": 9,
  "daylight_minutes": 41,
  "last_update": "14:20:00",
  "valid": true
}

GET /api/config

Export full configuration as JSON.

Response:

{
  "ssid": "MyNetwork",
  "timezone_offset": 0,
  "dst_enabled": true,
  "brightness": 5,
  "ntp_server": "pool.ntp.org",
  "ntp_interval": 3600,
  "hour_format_24": true,
  "hostname": "tj56654-clock",
  "latitude": 37.19,
  "longitude": -8.54,
  "city_name": "Portimao",
  "weather_enabled": true,
  "weather_interval": 1800,
  "display_rotation_sec": 5,
  "show_weather": true,
  "show_sunrise_sunset": true,
  "display_orientation": 0
}

POST /api/config

Import configuration from JSON.

Request Body: Same structure as export response (password field optional for security).

Response:

{
  "status": "ok"
}

Device automatically reboots after import.

POST /api/eeprom-clear

Factory reset (clears EEPROM).

Response:

{
  "status": "cleared"
}

Device reboots to WiFiManager captive portal.

POST /api/reboot

Remote reboot.

Response:

{
  "status": "rebooting"
}

Device reboots immediately.


Security Improvements

What Changed from Original Firmware

Issue Original Custom Firmware
WiFi Password Leak Plaintext in open AP No open AP after setup
Persistent AP Always active Only on first boot or failure
API Keys QWeather requires registration Open-Meteo (no key needed)
Cloud Dependency Chinese servers Direct API calls, no intermediary
Firmware Updates Manual FTDI only OTA via WiFi (password-protected)
Config Access No authentication Admin password required
Code Transparency Closed source Open source (you're reading it!)

Best Practices Implemented

  1. WiFiManager Timeout: AP automatically closes after 180 seconds if no configuration
  2. Fallback AP Mode: If credentials fail, device creates secure AP ("TJ56654-Clock" with password)
  3. EEPROM Validation: Magic number check prevents loading corrupted data
  4. Input Sanitization: Buffer overflow protection on all user inputs
  5. Memory Safety: No dynamic String allocations in loops, fixed-size buffers
  6. Error Handling: Graceful degradation (e.g., display shows time even if weather fails)

Recommended Post-Flash Steps

  1. Change OTA password: Edit line ~60 in .ino file:

    ArduinoOTA.setPassword("admin");  // Change this!
  2. Change web admin password: Edit line ~430:

    if (!server.authenticate("admin", "admin")) {  // Change this!
  3. Set strong WiFi AP fallback password: Edit line ~780:

    WiFi.softAP("TJ56654-Clock", "12345678");  // Change this!
  4. Disable unnecessary features: If you don't need weather, disable it in /config to save bandwidth


Credits

Hardware: TJ-56-654 Weather Clock Kit (AliExpress)

Firmware: Written from scratch with love and frustration

Libraries Used:

APIs:

  • Open-Meteo - Free weather API, no registration required

Tools:

  • Arduino IDE 2.x
  • FTDI FT232RL USB-to-Serial adapter
  • Lots of coffee ☕

Project Structure

esp8266-weather-clock/
├── firmware/
│   └── weather_clock/              # Modular firmware
│       ├── weather_clock.ino       # setup() and loop()
│       ├── config.h                # Config struct, EEPROM layout
│       ├── globals.h               # Shared state
│       ├── display.cpp             # OLED rendering
│       ├── ntp_client.cpp          # Async NTP + DST
│       ├── weather.cpp             # Open-Meteo API
│       ├── web_server.cpp          # HTTP UI + REST API
│       └── wifi_manager.cpp        # WiFi resilience
├── tests/
│   └── test_device.py              # HW-in-the-loop test suite (73 cases)
├── docs/
│   ├── HARDWARE.md                 # Hardware specifications
│   └── INSTALLATION.md             # Flashing guide
├── CHANGELOG.md                    # Version history
└── README.md                       # This file

Testing

# Hardware-in-the-loop test suite — verifies API, fuzzes config endpoint,
# checks heap stability over 5 samples
python3 tests/test_device.py 192.168.x.x

The test suite covers:

  • All REST API endpoints (functional validation)
  • Boundary fuzz against /config (oversized SSID, invalid intervals, etc.)
  • Malformed JSON fuzz against /api/config import
  • Heap stability check (must stay >20KB, drift <2KB across runs)

License

This project is released into the public domain. Do whatever you want with it. If you improve it, consider sharing your changes - that's how we make IoT better.



Author: apetrochenko · License: MIT · Firmware: v1.9.9

About

Open-source firmware for TJ-56-654 ESP8266 weather clock (ESP-01S + OLED). Fixes WiFi password leak in original firmware. NTP time, Open-Meteo weather, OTA updates, web UI.

Topics

Resources

Contributing

Stars

13 stars

Watchers

3 watching

Forks

Releases

Packages

Contributors

Languages