This project contains:
- ESP8266 firmware in modern C++20 using the Arduino framework
- a small HTTP API for text and raw framebuffer updates
- a local Python helper client for text, image, and Pillow-based rendering workflows
The target device is an ESP8266 with a 128x64 SSD1306 OLED over I2C.
The firmware exposes a simple REST API on port 80.
- Text mode: send lines or plain text to the display
- Title mode: large centered title with optional scrolling
- Frame mode: upload a full
1024-byte monochrome framebuffer - Status mode: query Wi-Fi state, display mode, and current text lines
The current implementation is intentionally conservative about memory use:
- fixed-size request and response buffers
- fixed-size JSON document for
/api/display - no repeated app-level heap allocation in normal request handling
- one expected display-buffer allocation at OLED driver startup
src/,include/: ESP8266 firmwareplatformio.ini: PlatformIO build configurationscripts/load_wifi_secrets.py: injects Wi-Fi credentials into the firmware buildoled_client.py: local helper CLI for API access and image/text renderingPSK.txt: optional local Wi-Fi password file used at build time
The default firmware configuration is in include/app_config.h:
- OLED width:
128 - OLED height:
64 - OLED I2C address:
0x3C - SDA pin:
12 - SCL pin:
14 - HTTP port:
80
The default PlatformIO board is esp12e in platformio.ini.
- Python 3
- PlatformIO installed in your environment or project venv
- USB serial access to the ESP8266 board
- local Wi-Fi credentials for the target network
For the Python helper client:
- Pillow
- fonts available to Pillow/fontconfig if you use the
rendercommand
Wi-Fi credentials are injected at build time by scripts/load_wifi_secrets.py.
The script uses:
OLED_WIFI_SSIDenvironment variable, defaulting toastroOLED_WIFI_PSKenvironment variablePSK.txtin the project root ifOLED_WIFI_PSKis not set
Both SSID and PSK are compiled into the firmware at build time via preprocessor defines.
Examples:
export OLED_WIFI_SSID="my-wifi"
export OLED_WIFI_PSK="my-password"
./.venv/bin/pio runOr with a local password file:
printf '%s\n' 'my-password' > PSK.txt
./.venv/bin/pio runIf no password is found, the firmware falls back to change-me.
Build the firmware with:
./.venv/bin/pio runThe compiled firmware is written to:
.pio/build/esp8266/firmware.bin
The current default serial port in platformio.ini is /dev/ttyUSB1.
Flash with:
./.venv/bin/pio run -t uploadOr override the port explicitly:
./.venv/bin/pio run -t upload --upload-port /dev/ttyUSB1Open the serial monitor with:
./.venv/bin/pio device monitorOr:
./.venv/bin/pio device monitor --port /dev/ttyUSB1 --baud 115200On successful boot, the firmware will attempt Wi-Fi connection and then print the HTTP listener address.
The local helper app is oled_client.py.
Global options:
python3 oled_client.py --host 192.168.1.124 --port 80 --timeout 5 statusAvailable commands:
statuscleardemoapi-helpdisplayframerendercheckerboard
Show current status:
python3 oled_client.py statusDisplay small text lines:
python3 oled_client.py display --mode small "Hello" "from ESP8266"Display a scrolling title:
python3 oled_client.py display --mode title --scroll fast "Very Long Title"Send plain text from stdin:
printf 'stdin text example\n' | python3 oled_client.py display --stdinUpload an image as a framebuffer:
python3 oled_client.py frame ./image.png --fit contain --dither floydRender UTF-8 text locally with Pillow, then upload the result:
python3 oled_client.py render --preserve-header "Hello" "๐"Restore the firmware demo screen:
python3 oled_client.py demoThe board listens on:
http://<board-ip>:80
Returns plain-text API help.
Example:
curl http://192.168.1.124/Returns JSON status.
Example:
curl http://192.168.1.124/api/statusTypical response:
{
"ssid": "<configured-ssid>",
"ip": "192.168.1.124",
"mode": "small",
"scroll": false,
"header": ["wifi <ssid>", "192.168.1.124"],
"lines": ["Display ready", "Send text via", "POST /api/", "display", "", "curl works well"],
"uptime_s": 123
}Accepts either:
application/jsontext/plain
Supported JSON fields:
mode:small,large,double,title,frametext: stringlines: array of stringsscroll:true,false, or one ofslow,normal,fastscroll_speed: one ofslow,normal,fast
Notes:
- If
linesis present, it is used instead oftext frameis not meaningful for/api/display; it is normalized back to text modetitleplus scroll enables the title-scrolling behavior
Example:
curl -X POST http://192.168.1.124/api/display \
-H 'Content-Type: application/json' \
-d '{"mode":"title","text":"Hello world","scroll":"normal"}'Multi-line example:
curl -X POST http://192.168.1.124/api/display \
-H 'Content-Type: application/json' \
-d '{"mode":"small","lines":["line 1","line 2","line 3"]}'Plain text is accepted as text/plain and displayed in small mode.
Example:
curl -X POST http://192.168.1.124/api/display \
-H 'Content-Type: text/plain' \
--data-binary 'plain text body'Accepts a raw 1024-byte monochrome framebuffer.
- preferred content type:
application/octet-stream text/plainis also accepted- payload must be exactly
1024bytes
Example:
curl -X POST http://192.168.1.124/api/frame \
-H 'Content-Type: application/octet-stream' \
--data-binary @frame.binFramebuffer layout matches the SSD1306 page layout used by the firmware:
128columns64rows8pages of128bytes each- total
1024bytes
Clears the main display area and returns:
ok
Example:
curl -X POST http://192.168.1.124/api/clearRestores the built-in demo text and returns:
ok
Example:
curl -X POST http://192.168.1.124/api/demoSupported display modes are implemented in include/display_ui.h.
small: up to 6 small text lineslarge: up to 3 larger linesdouble: up to 2 centered larger linestitle: single large title, optionally scrollingframe: raw framebuffer mode
The OLED header area is always two small lines at the top, separated from the main content area by a horizontal line.
Important firmware limits from include/app_config.h:
- max total request bytes:
1600 - max header bytes:
512 - max body bytes:
1088 - max plain text characters:
192 - raw frame size:
1024
Requests larger than these limits are rejected with HTTP 400.
- The firmware handles one client at a time. Under heavy concurrent request bursts, some requests may time out on the client side.
- Normal single-client and low-concurrency usage is stable.
- The SSD1306 library allocates its framebuffer once at startup.
- The app code avoids repeated heap churn during normal request handling.
- If the display shows a broken pattern after frame testing, switch back to text mode with:
python3 oled_client.py clear
python3 oled_client.py display --mode small "OLED OK"- C++ standard:
gnu++2a - Framework: Arduino for ESP8266
- Board target:
esp12e - Main entry point: src/main.cpp
- Set Wi-Fi credentials via environment variables or
PSK.txt. - Build with
./.venv/bin/pio run. - Flash with
./.venv/bin/pio run -t upload. - Open serial monitor to find the board IP.
- Query
python3 oled_client.py status. - Send text with
python3 oled_client.py display --mode small "hello".