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.
Story
- The Discovery: When "Smart" Means "Insecure"
- The Device
- The Investigation
- The Solution: Custom Firmware
- Hardware Quirk: Swapped I2C Pins
- Version History
Use it
- How to Flash This Firmware
- Web Interface
- API Documentation
- Testing
- Security Improvements
- Project Structure
For internal architecture notes (state machines, memory, EEPROM layout), see docs/ARCHITECTURE.md.
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:
- You connect to the AP (192.168.4.1)
- You configure your home WiFi credentials
- Device connects to your network
- The open AP stays active in parallel
- 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.
Product: ESP8266 Mini Weather Clock Kit Model: TJ-56-654 Price: ~€5 EUR Source: AliExpress Link
| 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 |
- 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)
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 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
To flash custom firmware, you need:
- FTDI USB-to-Serial adapter (3.3V! Not 5V - you'll fry the ESP8266)
- Jumper wires
- Steady hands
Wiring:
FTDI ESP-01S
────────────────────
3V3 → 3V3
GND → GND
TX → RX
RX → TX
GND → GPIO0 (for programming mode)
Boot into flash mode:
- Connect GPIO0 to GND
- Power on the device
- Remove GPIO0 to GND connection after boot
- 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.
I decided to write a complete replacement firmware with:
- Security First: No hardcoded credentials, no open networks, WiFiManager with proper AP timeout
- Privacy: Use free, open APIs (Open-Meteo instead of QWeather)
- Maintainability: OTA updates for painless improvements
- Performance: Fully async architecture, no blocking operations
- Reliability: Proper error handling, exponential backoff, memory safety
- 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/
- 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)
- 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!)
Three rotating display screens (configurable interval):
-
Time Mode
- Large HH:MM display
- Blinking colon animation
- Day of week and date
- 12/24 hour format support
-
Weather Mode
- Temperature with superscript °c
- City name
- Clean, minimalist layout
-
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.
/- Home page with live time/config- Full configuration form/debug- System diagnostics/update- OTA firmware upload
All endpoints return JSON:
GET /api/time- Current timeGET /api/status- System status (WiFi, uptime, heap)GET /api/debug- Detailed diagnosticsGET /api/weather- Weather + sunrise/sunsetGET /api/config- Export configurationPOST /api/config- Import configurationPOST /api/eeprom-clear- Factory resetPOST /api/reboot- Remote reboot
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.
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.
- 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
-
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
-
Install Required Libraries
- Sketch → Include Library → Manage Libraries
- Install:
Adafruit GFX LibraryAdafruit SSD1306NTPClientWiFiManager(by tzapu)AsyncHTTPRequest_GenericESPAsyncTCPArduinoJson(by Benoit Blanchon)
-
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"
-
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) -
Compile and Upload:
- Open
weather_clock.ino - Sketch → Upload
- Wait for "Done uploading"
- Remove GPIO0-to-GND jumper
- Press reset or power cycle
- Open
-
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
-
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)
- Browse to
-
Via Arduino IDE:
- Tools → Port → Select "tj56654-clock at 192.168.x.x"
- Sketch → Upload
- No wires needed!
Current time display with live updates via JavaScript (fetches /api/time every second).
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.
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.
All endpoints return JSON (except /update which is for file upload).
Current time information.
Response:
{
"current": "14:23:45",
"date": "2026-01-03",
"day": "Friday",
"timezone_offset": 0,
"dst_active": false
}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"
}
}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
}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
}Import configuration from JSON.
Request Body: Same structure as export response (password field optional for security).
Response:
{
"status": "ok"
}Device automatically reboots after import.
Factory reset (clears EEPROM).
Response:
{
"status": "cleared"
}Device reboots to WiFiManager captive portal.
Remote reboot.
Response:
{
"status": "rebooting"
}Device reboots immediately.
| 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!) |
- WiFiManager Timeout: AP automatically closes after 180 seconds if no configuration
- Fallback AP Mode: If credentials fail, device creates secure AP ("TJ56654-Clock" with password)
- EEPROM Validation: Magic number check prevents loading corrupted data
- Input Sanitization: Buffer overflow protection on all user inputs
- Memory Safety: No dynamic String allocations in loops, fixed-size buffers
- Error Handling: Graceful degradation (e.g., display shows time even if weather fails)
-
Change OTA password: Edit line ~60 in
.inofile:ArduinoOTA.setPassword("admin"); // Change this!
-
Change web admin password: Edit line ~430:
if (!server.authenticate("admin", "admin")) { // Change this!
-
Set strong WiFi AP fallback password: Edit line ~780:
WiFi.softAP("TJ56654-Clock", "12345678"); // Change this!
-
Disable unnecessary features: If you don't need weather, disable it in
/configto save bandwidth
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 ☕
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
# 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.xThe test suite covers:
- All REST API endpoints (functional validation)
- Boundary fuzz against
/config(oversized SSID, invalid intervals, etc.) - Malformed JSON fuzz against
/api/configimport - Heap stability check (must stay >20KB, drift <2KB across runs)
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