A multi-mode system monitor for Raspberry Pi with support for:
- Direct LCD display (ILI9163 via SPI)
- ESP32 WiFi display (remote wireless screen)
- Windows simulation (Tkinter GUI for development)
STL files and photos for the 3D-printed enclosure are available on Thingiverse: https://www.thingiverse.com/thing:7218125
- Motivation
- Features
- Gallery
- Architecture Highlights
- Hardware Requirements
- Software Dependencies
- Configuration
- Installation
- Usage
- Troubleshooting
- Testing
- Future Improvements
- Credits
I recently got a Raspberry Pi 5 to set up my own home server. I'm hosting my website on it and have some exciting future projects lined up. I wanted a way to monitor system stats from my desk, so I developed this project to display important information like public IP, local IP, CPU temperature, memory usage, disk usage, and other metrics.
The app supports three display modes:
- Native LCD: Direct connection to ILI9163 display on Raspberry Pi
- ESP32 WiFi: Wireless streaming to an ESP32-C3 with LCD display
- Simulation: Windows development mode with Tkinter window
The project includes a custom 3D-printed enclosure designed by me to make the hardware compact, mountable and tidy. The enclosure package on Thingiverse contains all STL files and photos and is linked above. The enclosure consists of the following parts:
- Front frame that holds the 128×128 SPI display securely.
- Two interchangeable back covers: one sized for builds with an ESP32-C3 (with a USB-C cutout) and another designed for direct wiring to a Raspberry Pi (with a central hole to route cables to the Pi).
- Internal ESP32 insert: a small internal piece that isolates the ESP32 from the display, organizes wiring, and secures the board inside the housing when using the ESP32 option.
- Clamp system with matching threaded screw to attach the assembly to a monitor frame.
- Two arms and a support piece that snap into the back cover; the arms use a smaller GoPro-style mount (custom-sized) so the screen angle is adjustable and compact.
All STL files, print orientation suggestions and recommended print settings are published on Thingiverse. Use the Thingiverse page to download the ZIP of STL files and the included photos.
The application cycles through 4 screens with button navigation:
- System Metrics - Public/local IPs, CPU usage/temperature, memory, disk space, uptime, current time
- Weather Information - Current conditions, temperature, humidity, wind speed, cloud coverage (auto-refresh every 15 minutes)
- Docker Containers - Running container status, uptime, service overview (real-time updates every 5 seconds)
- Animated Display - Custom GIF animation with smooth frame transitions
- Native SPI LCD (ILI9163) - Direct connection, lowest latency
- ESP32 WiFi Display - Wireless streaming to remote ESP32-C3 with LCD
- Desktop Simulation (Windows/Tkinter) - Development mode with GUI window
- Modular screen system with base class and helper methods
- Hardware abstraction layer for multiple display types
- Centralized configuration with structured dataclasses
- Advanced logging system with colored console output and module-specific prefixes
- Resource management with singleton pattern for fonts and icons
- Input handling framework supporting both GPIO and keyboard input
Photos and the full STL set are hosted on Thingiverse (link above).
lcdstats/
├── config/
│ └── config.py # Structured configuration with dataclasses
├── devices/
│ ├── device.py # Abstract interfaces for display devices
│ ├── fake_display.py # Tkinter-based simulator for development
│ ├── ILI9163.py # Native SPI LCD driver (Raspberry Pi)
│ └── esp32_wifi_display.py # WiFi streaming client for ESP32
├── esp32_display_server/ # ESP32 firmware (Arduino IDE)
│ ├── config.h # WiFi & hardware configuration
│ ├── display.h/cpp # ILI9163 driver for ESP32
│ ├── input.h/cpp # Button input handling
│ ├── network.h/cpp # WiFi communication protocol
│ ├── state_manager.h/cpp # Device state machine
│ ├── progress.h/cpp # Progress indicator UI component
│ └── esp32_display_server.ino # Main firmware entry point
├── fonts/ # Custom TrueType fonts
├── resources/ # Graphics assets (icons, GIFs)
├── tests/ # Test suite and validation
├── utils/ # Utility classes and helpers
│ └── progress_indicator.py # Circular progress widget
├── views/ # UI Screen implementations
│ ├── screen.py # Base class with common utilities
│ ├── main_screen.py # System metrics (Screen 1)
│ ├── secondary_screen.py # Weather information (Screen 2)
│ ├── third_screen.py # Docker containers (Screen 3)
│ └── fourth_screen.py # Animated GIF display (Screen 4)
├── logger.py # Advanced logging system with colors
├── resource_manager.py # Singleton for shared resources
├── weather_data.py # Weather API data mappings
├── data_gatherer.py # System metrics collection with caching
├── input_handler.py # Unified input (GPIO + keyboard)
├── screen_manager.py # Screen state and navigation management
├── stats.py # Main application entry point
├── requirements.txt # Python dependencies (Raspberry Pi)
├── requirements-windows.txt # Python dependencies (Windows)
├── Dockerfile # Docker container configuration
└── docker-compose.yml # Docker orchestration with environment variablesStructured configuration with dataclasses and environment variable support:
- HardwareConfig: GPIO pins, SPI settings, screen dimensions
- AppConfig: Frame rate, input handling, update intervals
- APIConfig: Weather API with validation and environment variables
- DockerConfig: Container monitoring settings
- DisplayConfig: ESP32 WiFi and simulation parameters
- VisualConfig: Colors, temperature thresholds, animations
- FontConfig: Font paths and sizes
Professional logging system with colored console output, module-specific prefixes, and configurable levels (DEBUG, INFO, WARNING, ERROR, CRITICAL). Consistent format: [LEVEL] [Module] Message.
Singleton pattern for efficient resource handling with font caching, centralized icon definitions, and memory-efficient loading.
Object-oriented architecture with base Screen class providing font helpers, icon support, color gradients, drawing utilities, and consistent update()/draw() interface.
Unified input supporting GPIO buttons with debouncing, keyboard simulation (spacebar), short/long press detection, and cross-platform compatibility with graceful fallbacks.
| Component | Specification |
|---|---|
| Raspberry Pi | Model 3B+/4/5 (Tested on Pi 5) |
| Display | 1.44" 128x128 SPI TFT (ILI9163 driver) |
| Button | Momentary push button (Normally Open) |
| Component | Specification |
|---|---|
| Raspberry Pi | Model 3B+/4/5 (runs Python client) |
| ESP32 | ESP32-C3 or similar |
| Display | 1.44" 128x128 SPI TFT (ILI9163 driver) |
| Button | Momentary push button (Normally Open) |
# Core dependencies
Pillow==10.3.0 # Image processing
numpy==1.26.4 # Array operations
python-periphery==2.4.1 # GPIO control
spidev==3.6 # SPI communication
# Optional dependencies
requests==2.31.0 # Weather API (Screen 2)
docker==7.0.0 # Docker monitoring (Screen 3)# Core dependencies
Pillow==10.3.0 # Image processing
numpy==1.26.4 # Array operations
# Optional dependencies
requests==2.31.0 # Weather API (Screen 2)
docker==7.0.0 # Docker monitoring (Screen 3)Note: tkinter is included with Python on Windows, no separate installation needed.
The application supports environment variables for sensitive configuration:
# Weather API configuration
export WEATHER_API_KEY="your_api_key_here"
export WEATHER_LOCATION="37.239541,-115.812265" # Your coordinates
export WEATHER_UPDATE_INTERVAL="900.0" # 15 minutes- Get a free API key from WeatherAPI.com
- Set environment variables above or edit
config/config.py
Edit config/config.py to customize behavior:
# Application behavior
class AppConfig:
FPS: int = 30 # Frames per second
LONG_PRESS_THRESHOLD: float = 3.0 # Long press duration
STARTING_SCREEN_INDEX: int = 0 # Starting screen
# Temperature thresholds (for color coding)
class VisualConfig:
MIN_TEMP: float = 40.0 # Blue (below this)
IDLE_TEMP: float = 50.0 # Green (normal)
MAX_TEMP: float = 80.0 # Red (above this)
# Docker monitoring
class DockerConfig:
CONTAINER_UPDATE_INTERVAL: float = 5.0 # Update frequency
MAX_VISIBLE_CONTAINERS: int = 5
# Display settings
class DisplayConfig:
SCALE_FACTOR: int = 1 # Simulation mode scaling
ESP32_PORT: int = 8080 # WiFi communication portEnable debug logging with: python stats.py --debug
Log Format: [LEVEL] [Module] Message
Levels: DEBUG (cyan), INFO (green), WARNING (yellow), ERROR (red), CRITICAL (magenta)
| Pin | Physical Pin | Raspberry Pi | Function |
|---|---|---|---|
| VCC | Pin 1 | 3.3V Power | Power supply |
| GND | Pin 6 | Ground | Common ground |
| SCL | Pin 23 | GPIO 11 (SCLK) | SPI Clock |
| SDA | Pin 19 | GPIO 10 (MOSI) | SPI Data |
| RES (Reset) | Pin 22 | GPIO 25 | Display Reset |
| DC (Data/Command) | Pin 18 | GPIO 24 | Data/Command Select |
| CS | Pin 29 | GPIO 5 | Chip Select |
| LED | Pin 17 | 3.3V Power | Backlight Power |
| Button Signal | Pin 12 | GPIO 18 | Button input |
| Button Ground | Pin 9 | Ground | Button return path |
| Pin | ESP32-C3 GPIO | Function |
|---|---|---|
| VCC | 3.3V | Power supply |
| GND | GND | Common ground |
| CS | GPIO 7 | Chip Select |
| RST | GPIO 8 | Display Reset |
| RS | GPIO 10 | Data/Command Select |
| SDI | GPIO 6 | SPI Data (MOSI) |
| CLK | GPIO 4 | SPI Clock |
| LED | 3.3V | Backlight Power |
| Button Signal | GPIO 2 | Button input |
| Button Ground | GND | Button return path |
Note: ESP32 button uses internal pull-up, no external resistor needed.
# 1. Install system dependencies
sudo apt update && sudo apt install -y \
python3-dev \
libgpiod-dev \
libjpeg-dev \
zlib1g-dev \
python3-spidev
# 2. Create and activate virtual environment
python3 -m venv stats_env
source stats_env/bin/activate
# 3. Install Python packages
pip install -r requirements.txt
# 4. (Optional) Configure weather API in config.py
# Edit config.py and add your WeatherAPI.com keyImportant Note on Docker Mode:
When running in Docker, the app cannot access real system metrics (CPU temp, memory usage, etc.) because it's isolated in a container. Docker mode is primarily useful for:
- ESP32 WiFi display: Stream to remote display (works perfectly)
- Development/Testing: Simulated data for UI testing
- Portability: Easy deployment without system dependencies
For native LCD with real metrics, use native installation (Option 1).
# Quick start with docker-compose
docker-compose up -d
# Or build and run manually
docker build -t lcdstats .
docker run -d \
--name lcdstats \
--network host \
-e DISPLAY_MODE=esp32 \
-e ESP32_HOST=192.168.0.199 \
lcdstatsDocker environment variables:
DISPLAY_MODE:window(default),esp32, orraspberryESP32_HOST: IP address of ESP32 (required foresp32mode)
# 1. Create virtual environment
python -m venv stats_env
stats_env\Scripts\activate
# 2. Install dependencies
pip install -r requirements-windows.txt
# 3. (Optional) Configure weather API in config.py
# Edit config.py and add your WeatherAPI.com key-
Install Arduino IDE and ESP32 board support:
- Download Arduino IDE
- Go to Tools → Board → Board Manager
- Search for "esp32" and install "esp32 by Espressif Systems"
-
Install required libraries:
- Go to Tools → Manage Libraries (or Sketch → Include Library → Manage Libraries)
- Search and install: ArduinoJson (by Benoit Blanchon)
-
Configure WiFi in
esp32_display_server/config.h:#define WIFI_SSID "YourNetworkName" #define WIFI_PASSWORD "YourPassword" // Plain text password (to be improved in future releases) // Optional: Set static IP (recommended) #define STATIC_IP_ENABLED true #define STATIC_IP_0 192 #define STATIC_IP_1 168 #define STATIC_IP_2 0 #define STATIC_IP_3 199 // Last octet (change as needed)
-
Upload firmware:
- Connect ESP32 to your computer via USB
- Open
esp32_display_server/esp32_display_server.inoin Arduino IDE - Select board: Tools → Board → ESP32 Arduino → ESP32C3 Dev Module
- Select port: Tools → Port → (your COM port or /dev/ttyUSB0)
- Click Upload button (→)
- Wait for "Done uploading" message
-
Verify operation:
- Open Tools → Serial Monitor (set to 115200 baud)
- Press the RESET button on your ESP32
- You should see:
=== ESP32 Display System === Connecting to WiFi.... WiFi Connected! IP Address: 192.168.0.199 Server started on port 8080 System ready - State: DISCONNECTED - The LCD should display "Desconectado" with the IP address
-
Note the IP address shown on the ESP32 display - you'll need it for the Python client
python stats.py [OPTIONS]
Options:
--display {auto,raspberry,window,esp32}
Display type to use (default: auto)
--esp32-host TEXT ESP32 host IP address for WiFi display
--debug Enable debug logging with colored output
--help Show this help messagesource stats_env/bin/activate
python stats.py --display raspberry# Make sure ESP32 is powered and showing its IP
source stats_env/bin/activate
python stats.py --display esp32 --esp32-host 192.168.0.199# Edit docker-compose.yml with your ESP32 IP, then:
docker-compose up -d
# View logs with color coding
docker-compose logs -f
# Stop
docker-compose downstats_env\Scripts\activate
python stats.py --display windowEnable detailed logging for troubleshooting:
python stats.py --debugThis will show:
- Colored log output by level
- Module-specific prefixes
- Detailed error messages with stack traces
- Performance metrics and timing information
- Short press: Cycle through screens (System → Weather → Docker → Animation)
- Long press (3 seconds): Toggle display on/off (shows progress indicator)
- Spacebar (simulation mode): Emulates button press
The display cycles through 4 screens:
- System Info - CPU, memory, disk, network, uptime
- Weather - Current conditions, temperature, humidity, wind
- Docker - Running containers with status and uptime
- Animation - Custom GIF display (configurable in config.py)
The Python client and ESP32 communicate over TCP port 8080 using a JSON + binary protocol:
- Handshake: ESP32 sends capabilities (width, height, format)
- Display Command: Client sends JSON header + RGB565 binary payload
- Acknowledgment: ESP32 confirms receipt or requests retransmission
- Commands: ESP32 can request next screen or stop sending
Protocol features:
- Automatic reconnection with exponential backoff
- Fragmentation detection and retry logic
- Error codes for debugging
- Screen ID tracking for synchronization
python -m lcdstats.tests.test_suite # Interactive menu
python -m lcdstats.tests.test_suite all # Run all tests
python -m lcdstats.tests.test_suite image # Run specific testThe project follows professional software engineering practices with centralized logging, type-safe configuration, graceful error handling, and modular architecture for easy extensibility.
Problem: ESP32 shows "Disconnected" screen
Solution:
- Check WiFi credentials in
config.h(use plain text password, not PSK hash) - Verify ESP32 and Raspberry Pi are on same network
- Open Serial Monitor (115200 baud) to check for WiFi errors
- Try power cycling the ESP32 (unplug and plug back in)
Problem: Python client cannot connect ("Connection timeout" or "No handshake")
Solution:
- Verify ESP32 is showing "Desconectado" with IP address on screen
- Test connectivity from Raspberry Pi:
ping <esp32-ip> - Test TCP port:
nc -zv <esp32-ip> 8080ortelnet <esp32-ip> 8080 - Check ESP32 Serial Monitor for errors or crashes
- Power cycle the ESP32 - unplug and reconnect (common fix!)
- Verify firewall isn't blocking port 8080
- Try re-uploading the firmware with Arduino IDE
Problem: Display freezes or shows artifacts
Solution:
- Check SPI connections (loose wires between ESP32 and LCD)
- Reduce SPI frequency in
config.hif needed (try 20MHz instead of 30MHz) - Verify power supply stability (use quality USB cable, 5V 1A minimum)
- Check for loose connections on breadboard
Problem: ESP32 won't connect to WiFi
Solution:
- Double-check SSID and password in
config.h(case-sensitive!) - Ensure WiFi is 2.4GHz (ESP32-C3 doesn't support 5GHz)
- Check if MAC filtering is enabled on your router
- Try disabling static IP (
STATIC_IP_ENABLED false) to use DHCP - Open Serial Monitor to see actual error messages
Problem: Weather screen or Docker screen shows "No Docker" or error
Solution:
- Weather API key not configured in
config.py - Docker daemon not running:
sudo systemctl start docker - Docker SDK not installed:
pip install docker - Check API key is valid at WeatherAPI.com
Problem: Docker container keeps restarting
Solution:
- Check logs:
docker logs lcdstats - Common issues:
- ESP32 not reachable: verify with
ping <esp32-ip> - Wrong ESP32_HOST in docker-compose.yml
- ESP32 not powered on or firmware not uploaded
- ESP32 not reachable: verify with
- Try rebuilding:
docker-compose down && docker-compose build --no-cache && docker-compose up
Problem: Cannot connect to ESP32 from Docker
Solution:
- Verify
network_mode: hostis set in docker-compose.yml - Test connectivity from host:
ping <esp32-ip>should work - Check ESP32_HOST environment variable matches actual ESP32 IP
- Ensure ESP32 is on same network as Raspberry Pi
Problem: System metrics show weird/simulated data
Solution:
- Verify
IS_RASPBERRY: "true"in docker-compose.yml - Check that privileged mode is enabled
- Verify volumes are mounted:
/sys:/sys:roand/proc:/proc:ro - Rebuild container:
docker-compose build --no-cache
Problem: "No module named 'periphery'"
Solution:
- Install correct package:
pip install python-periphery==2.4.1 - Old package name was
periphery, new one ispython-periphery - Update requirements.txt if needed
Problem: Display doesn't turn on
Solution:
- Check all wiring connections (see wiring diagram)
- Verify SPI is enabled:
sudo raspi-config→ Interface Options → SPI → Enable - Test with:
ls /dev/spidev*(should show/dev/spidev0.0) - Check power connections (VCC to 3.3V, not 5V!)
Problem: "Permission denied" when accessing GPIO
Solution:
- Add user to gpio group:
sudo usermod -a -G gpio $USER - Logout and login again
- Or run with sudo (not recommended for production)
Problem: High CPU usage
Solution:
- This is normal during ESP32 streaming (encoding RGB565)
- Reduce FPS in config.py: change
AppConfig.FPSto lower value (e.g., 20) - Native LCD mode uses less CPU than WiFi streaming
- Environment variables for sensitive configuration
- Advanced logging system with colors and module prefixes
- Type-safe configuration with dataclasses
- Modular architecture with clean separation of concerns
- Backlight control and PWM dimming
- Configurable timeouts and themes
- ESP32 battery monitoring and low-power modes
- mDNS discovery for automatic ESP32 detection
- Multiple display support
- Additional screens (system logs, network traffic)
- Mobile app for remote control
- WiFi security improvements for ESP32
- Expanded unit test coverage
- API documentation
- Performance optimizations
- Enhanced error recovery
This project was inspired by mklements/OLED_Stats, a great starting point for displaying system info on tiny screens. Big thanks for the spark!
MIT License - see LICENSE file for details.


