An ESP32-S3 based e-paper clock project with integrated SCD41 CO2/temperature/humidity sensor, designed for low power consumption and long battery life.
EPDEnvClock is a clock application using the CrowPanel ESP32-S3 E-Paper 5.79" display (792x272 pixels). It provides the following features:
- Time & Date Display: Shows time and date with large number fonts
- Environmental Sensor: Measures and displays CO2, temperature, and humidity using SCD41 sensor
- Low Power Design: Long battery life with Deep Sleep mode (updates approximately every minute)
- Wi-Fi Connectivity: NTP time synchronization via Wi-Fi
- Battery Monitoring: Real-time battery percentage, voltage, and charging state display using MAX17048 fuel gauge and 4054A CHRG pin
- Button Wake-up: Wake from Deep Sleep with HOME button for full screen refresh
- Time Display: Large number font with kerning support
- Date Display: Medium-sized number font in YYYY.MM.DD format
- Sensor Values: Temperature, humidity, and CO2 concentration with icons
- Status Display: Battery % and voltage, Wi-Fi status, NTP sync status, uptime, and free memory
- SCD41 Integration: Measures CO2 (400-5000ppm), temperature (-10~+60Β°C), and humidity (0-100%RH)
- Low Power Mode: Single-Shot mode with approximately 1.5mA current consumption (waits in Light Sleep)
- Temperature Compensation: Temperature offset function to compensate for self-heating (4.0Β°C)
- Auto Calibration: ASC (Automatic Self-Calibration) support
- Deep Sleep: Enters Deep Sleep at approximately 1-minute intervals to minimize current consumption
- Dual-Core Parallel Processing: WiFi/NTP sync and sensor reading run simultaneously on separate cores
- EPD Deep Sleep: Display enters Deep Sleep mode to reduce power consumption
- Frame Buffer Persistence: Saves frame buffer to SD card or SPIFFS fallback, restores on wake
- SD Card Power Control: Powers off SD card during Deep Sleep to reduce current consumption
- Wi-Fi Power Saving: NTP sync runs at the top of every hour
- Wi-Fi Connection: Connects to configured Wi-Fi (requires recompile to change SSID/password)
- NTP Sync: Syncs time from NTP server at the top of every hour (maintains RTC time between syncs)
- Sensor Log: Automatically records sensor values to SD card in JSONL format
- Recorded Data: Date, time, Unix timestamp, RTC drift (residual
rtc_drift_ms,drift_ratems/min, clamped Β±600), temperature, humidity, CO2, battery voltage, battery %, charge rate, charging state - File Format:
/sensor_logs/sensor_log_YYYYMMDD.jsonl(files split by date)
- HOME Button (GPIO 2): Wake from Deep Sleep and perform full screen refresh
- Other Buttons: EXIT (GPIO 1), PRV (GPIO 6), NEXT (GPIO 4), OK (GPIO 5) - for future expansion
- All buttons are active LOW with internal pullup
- ESP32-S3 Dev Module
- EPD Display: 792x272 pixels (controlled by two SSD1683 ICs in master/slave configuration)
- SD Card Slot: For frame buffer storage (optional, longer write lifespan than SPIFFS)
- SCD41 Sensor: CO2/temperature/humidity sensor
- MAX17048 Fuel Gauge: Battery state-of-charge monitor (Adafruit breakout recommended)
| Pin | GPIO |
|---|---|
| SDA | 38 |
| SCL | 20 |
| VDD | 3.3V |
| GND | GND |
Note: Pull-up resistors are built into the SCD41 module, no additional hardware required.
| Pin | GPIO / Connection |
|---|---|
| SDA | 14 |
| SCL | 16 |
| VIN | 3.3V |
| GND | GND |
| CELL+ | LiPo Battery + |
| CELL- | LiPo Battery - (GND) |
Note: MAX17048 is powered by the battery, not VIN. The chip will not respond to I2C if battery is not connected.
| Pin | GPIO |
|---|---|
| CHRG | 8 |
Note: CHRG is an open-drain output. LOW = charging, HIGH = not charging (pulled up by internal pullup). Read before I2C operations to avoid noise interference.
| Pin | GPIO |
|---|---|
| MOSI | 40 |
| MISO | 13 |
| SCK | 39 |
| CS | 10 |
| Power Enable | 42 |
| Pin | GPIO |
|---|---|
| MOSI | 11 |
| SCK | 12 |
| CS | 45 |
| DC | 46 |
| RST | 47 |
| BUSY | 48 |
| Button | GPIO |
|---|---|
| HOME | 2 |
| EXIT | 1 |
| PRV | 6 |
| NEXT | 4 |
| OK | 5 |
CrowPanel ESP32-S3 uses the CH340 USB serial chip.
- macOS: Built-in driver supports serial console only. Official driver required for firmware upload.
- Windows/Linux: See driver installation guide.
For all platforms, see: SparkFun CH340 Driver Guide
Device appears as /dev/cu.usbserial-* or /dev/cu.wchusbserial* (macOS) when connected.
macOS:
brew install arduino-cliFor other platforms, see: arduino-cli Installation Guide
This project requires ESP32 Arduino Core 2.0.17 (not 3.x). To avoid conflicts with other projects that may use 3.x, a project-specific Arduino environment is used.
First-time setup:
cd /path/to/EPDEnvClock
# Create project-specific Arduino config
cat > arduino-cli.yaml << 'EOF'
board_manager:
additional_urls:
- https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json
directories:
data: /path/to/EPDEnvClock/.arduino15
downloads: /path/to/EPDEnvClock/.arduino15/staging
user: ~/Documents/Arduino
EOF
# Install ESP32 core 2.0.17 in project-specific directory
arduino-cli --config-file arduino-cli.yaml core update-index
arduino-cli --config-file arduino-cli.yaml core install esp32:esp32@2.0.17Note: Replace /path/to/EPDEnvClock with your actual project path. Version 3.x has breaking changes in SPI and other APIs that cause compatibility issues with Adafruit libraries (BusIO, MAX1704X, etc.).
# Sensirion SCD4x library (Sensirion Core dependency is installed automatically)
arduino-cli lib install "Sensirion I2C SCD4x@0.4.0"
# Adafruit MAX17048 fuel gauge library
arduino-cli lib install "Adafruit MAX1704X"| Component | Version | Notes |
|---|---|---|
| arduino-cli | Latest recommended | brew install arduino-cli (macOS) |
| ESP32 Core | 2.0.17 | esp32:esp32@2.0.17 (v3.x not compatible with Adafruit libs) |
| Sensirion I2C SCD4x | 0.4.0 | CO2/temperature/humidity sensor library |
| Sensirion Core | 0.7.2 | Dependency (auto-installed) |
| Adafruit MAX1704X | 1.0.3 | Battery fuel gauge library |
arduino-cli lib listarduino-cli core listCopy EPDEnvClock/wifi_config.h.example to EPDEnvClock/wifi_config.h and set your Wi-Fi credentials:
#define WIFI_SSID "your_wifi_ssid"
#define WIFI_PASSWORD "your_wifi_password"Note: wifi_config.h is included in .gitignore and will not be committed.
Set server IP address and port in EPDEnvClock/server_config.h:
#define ENABLE_IMAGEBW_EXPORT 1 // 1 to enable, 0 to disable
#define IMAGEBW_SERVER_IP "192.168.1.100" // Server IP address
#define IMAGEBW_SERVER_PORT 8080 // Server portarduwrap is an arduino-cli wrapper that manages serial port during compile/upload. It runs a background server that monitors the serial port, automatically pauses monitoring during upload, and reconnects after the device resets.
# Compile and upload (while arduwrap serve is running in another terminal)
scripts/arduwrap compile --fqbn esp32:esp32:esp32s3:PartitionScheme=huge_app,PSRAM=opi EPDEnvClockFeatures:
- Uses project-specific config (
arduino-cli.yaml) automatically --uploadflag is added automatically- Port is managed by the running server (no
-pneeded) - Serial monitoring pauses during upload, then reconnects automatically
Additional commands:
# Get buffered serial log (last 64KB)
scripts/arduwrap log
# Get filtered log with regex
scripts/arduwrap log --filter "ERROR|WARN"
# Get last N lines
scripts/arduwrap log -n 50
# Stop the server
scripts/arduwrap stopConfiguration Parameters:
- FQBN:
esp32:esp32:esp32s3 - PartitionScheme:
huge_app(Huge APP: 3MB No OTA/1MB SPIFFS) - PSRAM:
opi(OPI PSRAM)
arduino-cli board list- Startup: Automatically starts when power is supplied to ESP32-S3
- Initialization: Sensor and Wi-Fi initialization (on first boot)
- Display Update: Display updates approximately every minute
- Deep Sleep: Enters Deep Sleep mode after display update
- Button Wake: Press HOME button to wake from Deep Sleep and perform full screen refresh
Screen layout (792x272 pixels):
- Top (y=4): Status information (battery % and voltage, Wi-Fi status, NTP sync status, uptime, free memory)
- Upper Left (y=45): Date (YYYY.MM.DD format, medium-sized numbers)
- Center Left (y=123): Time (H:MM or HH:MM format, large numbers)
- Upper Right (y=33): Temperature (icon + value + Β°C unit)
- Center Right (y=114): Humidity (icon + value + % unit)
- Lower Right (y=193): CO2 concentration (icon + value + ppm unit)
To send display data to a server via Wi-Fi:
-
Start Server (Python 3 required):
cd /path/to/EPDEnvClock python3 scripts/imagebw_server.py --port 8080 -
Arduino Configuration: Set server IP address in
server_config.h -
Auto Send: ImageBW data is automatically sent whenever the display updates
Received data is saved as PNG files in the output/ directory.
Note: The server runs on any platform with Python 3 (Windows, macOS, Linux, etc.).
See docs/README_IMAGEBW.md for details.
EPDEnvClock/
βββ EPDEnvClock/ # Arduino/Firmware code (sketch directory)
β βββ EPDEnvClock.ino # Main sketch (setup/loop)
β βββ parallel_tasks.* # Dual-core parallel WiFi/NTP + sensor reading
β βββ EPD.h / EPD.cpp # Low-level EPD driver
β βββ EPD_Init.h / EPD_Init.cpp # EPD initialization
β βββ spi.h / spi.cpp # Bit-banging SPI for EPD
β βββ display_manager.* # Display rendering, layout
β βββ fuel_gauge_manager.* # MAX17048 fuel gauge + 4054A charging detection
β βββ font_renderer.* # Glyph drawing with kerning support
β βββ sensor_manager.* # SCD41 sensor (single-shot mode with light sleep)
β βββ sensor_logger.* # Sensor data logging to SD card
β βββ network_manager.* # Wi-Fi connection, NTP sync
β βββ deep_sleep_manager.* # Deep sleep, RTC state, SD/SPIFFS frame buffer
β βββ imagebw_export.* # ImageBW Export (debug)
β βββ logger.* # Logging with levels (DEBUG/INFO/WARN/ERROR)
β βββ wifi_config.h # Wi-Fi credentials (gitignored)
β βββ secrets.h # API keys (gitignored)
β βββ server_config.h # Server configuration
β βββ bitmaps/ # Number fonts, icons, units, kerning table
βββ scripts/ # Python scripts
β βββ arduwrap # Arduino CLI wrapper (shell script)
β βββ arduwrap.py # Arduino CLI wrapper implementation
β βββ create_number_bitmaps.py # Number bitmap generation from TTF font
β βββ convert_numbers.py # Convert PNG numbers to C header
β βββ convert_icon.py # Convert PNG icons to C header
β βββ imagebw_server.py # ImageBW receiver server (debug)
β βββ upload_sensor_data.py # Upload JSONL logs to dashboard API
βββ assets/ # Assets (image files, etc.)
β βββ Number L/ # Large number font images
β βββ Number M/ # Medium number font images
βββ web/ # Web dashboard (Astro + Cloudflare Pages)
βββ docs/ # Documentation
β βββ README.md # Documentation index
β βββ README_IMAGEBW.md # ImageBW feature guide
β βββ README_SCD41.md # SCD41 sensor guide
β βββ reviews/ # Code reviews
βββ output/ # Generated image output (gitignore)
βββ README.md # This file
- docs/README.md - Documentation index
- docs/README_IMAGEBW.md - ImageBW Wi-Fi Export feature guide
- docs/README_SCD41.md - SCD41 sensor integration guide
- web/README.md - Web dashboard documentation
The project includes a web dashboard for viewing sensor data, deployed on Cloudflare Pages.
cd web
bun install
bun run devAccess at http://localhost:4321/
cd web
bun run build
bunx wrangler pages deploy dist --branch=mainNote: --branch=main is required to deploy to production domain. Without it, deploys to preview URL only.
| State | Current Consumption |
|---|---|
| SCD41 Idle Single-Shot | ~1.5mA |
| MAX17048 Hibernate | ~3Β΅A |
| ESP32-S3 Deep Sleep | ~0.2-0.3mA |
| ESP32-S3 Light Sleep (sensor measurement wait) | ~2-3mA |
| ESP32-S3 Active (including Wi-Fi) | ~80-150mA |
- Average Current Consumption: ~4.5mA (Wi-Fi sync once per hour)
- Actual Runtime: ~224 hours (~9.3 days) to 3.375V
- Usable Capacity: ~1000-1100mAh (battery labeled 1500mAh, actual ~1200-1400mAh)
See Battery Discharge Report for detailed analysis.
- Update Interval: ~1 minute (updates at minute boundary)
- Active Time: ~6-8 seconds (parallel WiFi/NTP + sensor measurement + display update)
- Deep Sleep Time: ~52-54 seconds
- Wi-Fi Connection: At the top of every hour for NTP sync
- Dual-Core Parallel Processing: WiFi/NTP (Core 0) and sensor reading (Core 1) run simultaneously, reducing active time by ~2-3 seconds
- Single Screen Update: Both time and sensor data are ready before display update, eliminating intermediate refresh
- Minimized Wi-Fi Connection: NTP sync once per hour, uses RTC time otherwise
- SD Card Power Control: Powers off SD card during Deep Sleep (GPIO 42 LOW)
- EPD Deep Sleep: Transitions display to Deep Sleep mode
- I2C Pins Held HIGH: Keeps sensor in idle mode during deep sleep
Number fonts are generated using scripts/create_number_bitmaps.py.
Important: All number fonts use the following font file:
- Font Name: Baloo Bhai 2
- Style: Extra Bold
cd /path/to/EPDEnvClock
python3 scripts/create_number_bitmaps.py \
--font-path "/path/to/fonts/BalooBhai2-ExtraBold.ttf" \
--font-size-px 90 \
--output-dir "assets/Number M"- Board: ESP32S3 Dev Module
- Partition Scheme: Huge APP (3MB No OTA/1MB SPIFFS)
- PSRAM: OPI PSRAM
- CPU Frequency: 240MHz (Wi-Fi)
- Flash Mode: QIO 80MHz
- Flash Size: 4MB (32Mb)
- Upload Speed: 921600
- Actual Resolution: 792x272 pixels
- Controller: Two SSD1683 ICs in master/slave configuration
- Each controller: Handles 396x272 pixels
- 8px address offset in center (handled by software)
- Program Definition:
EPD_W = 800,EPD_H = 272(for address offset) - Buffer Size: 27,200 bytes (800Γ272 pixels Γ· 8 bits)
- Interface: Bit-banging SPI (MOSI=11, SCK=12, CS=45, DC=46, RST=47, BUSY=48)
- I2C Address: 0x62 (default)
- I2C Bus: Wire (Bus 0) - SDA=GPIO 38, SCL=GPIO 20
- Measurement Mode: Single-Shot (waits 5 seconds in Light Sleep)
- Temperature Offset: 4.0Β°C (self-heating compensation)
- Measurement Range:
- CO2: 400-5000ppm
- Temperature: -10~+60Β°C
- Humidity: 0-100%RH
- Accuracy:
- CO2: Β±(40ppm+5%)
- Temperature: Β±0.8Β°C (in 15-35Β°C range)
- Humidity: Β±6%RH (in 15-35Β°C, 20-65%RH range)
- I2C Address: 0x36 (default)
- I2C Bus: Wire1 (Bus 1) - SDA=GPIO 14, SCL=GPIO 16
- Power Source: Powered by battery (requires battery connection to function)
- Sleep Mode: Hibernate mode (~3Β΅A current consumption)
- Measurements:
- Battery Voltage: 0-5V
- State of Charge: 0-100%
- Charge Rate: %/hr (positive=charging, negative=discharging)
- Algorithm: ModelGaugeβ’ for accurate SOC without current sensing
- NTP Server:
ntp.nict.jp - Timezone: JST (UTC+9)
- Sync Interval: At the top of every hour
- RTC Persistence: Time saved to RTC memory before sleep, restored on wake
- Log Levels: DEBUG, INFO, WARN, ERROR
- Timestamp: Boot time, date/time, or both
- Tags: Setup, Loop, Network, Sensor, Display, Font, DeepSleep, ImageBW
- ANSI Colors: Color-coded display by log level
This project is licensed under the MIT License - see the LICENSE file for details.
If you have questions or issues with the project, please report them on GitHub Issues.


