Skip to main content

Week 15 — Interface and Application Programming

This week I connected a graphical interface to a small microcontroller board, iterated on both the on-device UI and a web control panel, and ran into the kinds of limits that are normal on embedded hardware. Below is how I set things up, what broke, how I fixed or worked around it, and what I would do next.

Hardware setup

Board: Seeed Studio XIAO ESP32C3

Display: XIAO Round Display (SKU 104030087)

week15-15

Luckily, I prepared two displays, and one seems broken, didn't show any light at all. The another one works good after some testing.

I tried the way that GPT recommend me to do at the first time, download the TFT_eSPI library and adjust some data, it didn't works. (AI sometimes not very reliable)

Then I followed the official getting-started guide of Seeed Studio for the round display to install the environment and the libraries the wiki lists as required:

Seeed Wiki — Getting Started with the Round Display

The wiki looks very neat, when follow the steps carefully, walks through IDE setup, board support, and adding the libraries that talk to the display driver and graphics layer.

After wiring the round display to the XIAO ESP32C3 and flashing a first sketch, I tried the example sketch from the Seeed_GFX library: File > Seeed_GFX > Examples > Round Display > Arduino_Life, it flashed a second then turned black.

While debugging, I found I had not included a required header:

#include <driver.h>

Once I added it, the display started behaving successfully:

Early bring-up: flashing firmware while the round display shows a test pattern

Why a “missing library” can look like a dead display (short explanation)

On embedded displays, drawing usually goes through several layers: your sketch → a graphics library → a chip-specific driver → the physical panel. If a piece of that chain is missing or not linked correctly, the code may compile (or fail in subtle ways) but the driver never finishes the setup sequence the panel needs timings, power/config registers, or SPI/I²C initialization may never run.

So the symptom is not always a loud error message; it can be no output, a brief flicker, or garbled pixels. Including the right driver header ensures the build pulls in the symbols and setup path the round display examples expect. If something similar happens to you, compare your includes and library list line-by-line with the official working example from the wiki.

UI design

Before coding, I planned the on-screen layout using design tools starting in Figma for structure, and also trying a round-screen mockup to check proportions on a 240×240 circle.

  • A Chinese “福” (blessing / good fortune) character in the center
  • Temperature shown toward the top
  • Humidity toward the bottom

Since the Figma also provide the AI feature, so I provide my prompt to generate the UI Design:

Design a UI layout for a 240x240 round display (embedded screen, low resolution).

The theme is a digital Chinese "Fu" (fortune) decoration with a traditional aesthetic.

Requirements:
- Use a centered circular layout suitable for a round screen
- Keep all important content within a safe inner circle (about 200px diameter)
- Minimal and clean design, optimized for embedded display (no complex graphics)

Content to include:
1. A large Chinese character "福" (Fu) in the center as the main visual
2. Temperature display at the top (e.g., 26°C)
3. Humidity display at the bottom (e.g., 60%)
4. Weather information (e.g., Sunny ☀) in smaller text

Style:
- Color scheme: red, gold, and black (Chinese traditional style)
- High contrast for readability
- Symmetrical and balanced layout

Avoid:
- Small text
- Complex gradients
- Overlapping elements

Output:
- A clean static UI layout suitable for implementation using TFT display (Arduino / ESP32)

The mockup below is the “target look”: red , gold/yellow for temperature and humidity, all kept inside a safe inner circle so the edges of the physical round LCD are not clipped.

Round display design mockup: centered 福 with temperature, weather, and humidity

The visual direction was inspired by traditional Chinese culture: a calm, symbolic centerpiece with practical sensor information around it.

Why I chose this design

I am interested in projects where culture and electronics meet. A round screen already feels like a coin, a moon, or a seal — shapes that show up a lot in traditional art. Putting in the middle grounds the project emotionally (it reads as intentional and personal, not only technical), while the sensor labels make the device useful in daily life. Working in Figma (and the round preview) helped me decide spacing and hierarchy before I spent time fighting font and layout limits in code.

Process: A working display

Firstly, I thought using the simple code the round display will work like what I designed:

#include <driver.h> // Hardware-specific library
#include <TFT_eSPI.h>
TFT_eSPI tft = TFT_eSPI();
void setup() {tft.init();tft.setRotation(e);tft.fil1Screen(TFT_BLACK);
void drawUI(float temp, int hum) {
ft.fil1Screen(TFT_BLACK);
ft.setTextDatum(MC_DATUM);//居中对齐
//温度
tft.setTextColor(TFT_YELLOW);
tft.drawString(String(temp,0) + "C", 120, 40, 4);
//天气(可选)
tft.setTextColor(TFT_YELLOW);
tft.drawString("Sunny", 120, 70, 2);
福字(重点)tft.setTextColor(TFT_RED);
ft.drawString("FU"120,120,7);// 后面换成中文或bitmap
//湿度
tft.setTextColor(TFT_YELLOW);
tft.drawString(String(hum) + "%", 120, 190, 5);
}
void loop() {
drawUI(26, 60);delay(2eee);
}

But the display only shows the temp data:

week15-13

I realized maybe it refreshed to fast, then I changed; but it still didn't work, the problem maybe the font to large, then I adjust the size from 7 to 4. Finally it works:

Round display showing “Hello Alison” with temperature, weather line, and humidity

When I moved from the mockup to the actual round display, Chinese characters did not render properly with my first approach. To keep the week moving and verify the pipeline end-to-end, I simplified the UI and used English text instead, for example “Hello Alison” which I could display reliably on the round screen.

And this is the final code:

#include <driver.h> // Hardware-specific library
#include <TFT_eSPI.h>
TFT_eSPI tft = TFT_eSPI();
void setup() {
tft.init();
tft.setRotation(e);
drawUI(2660);//只画一次
}
void loop() {//暂时不要反复刷新
}
void drawUI(float temp, int hum) {
ft.fil1Screen(TFT_BLACK);
tft.setTextDatum(MC_DATUM);
//温度(上)
tft.setTextColor(TFT_YELLOW);
tft.drawString(String(temp,0) + "C",120, 40,4);
(稍微下移)tft.drawString("Sunny", 120, 75, 2);
tft.setTextColor(TFT_RED);
//福字(缩小一点
tft.drawString("Hello Alison", 120, 120, 4);
//湿度(下)tft.setTextColor(TFT_YELLOW);
tft.drawString(String(hum) + "%", 120, 200, 4);
}

WiFi and web control (ESP32C3)

The ESP32C3 includes WiFi, so I built a simple HTTP server on the board and a small web page with buttons. The idea: change what appears on the round display from a browser on the same network.

First version:
The basic flow worked: I could trigger updates on the screen from the web UI. That was an exciting moment because it connects physical UI ↔ network UI in one stack.

The first browser UI was intentionally simple: a white page with three actions (福 / temperature / Hello) so I could prove routing and state before styling.

Early web UI: local IP in the browser with buttons to switch round-display content

Handheld check of the same path: the page on the laptop and the round display showing Connected! in red once the link was live.

Laptop web control page and handheld round display showing “Connected!”

Screen recording of the WiFi control loop (browser → ESP32 HTTP server → round display). The file is compressed for the site (H.264, 256×144, 10 fps, no audio, faststart, under 200 KB) so the page stays light while the interaction is still visible.


#include <WiFi.h>
#include <WebServer.h>
#include <TFT_eSPI.h> // 你之前已经装好的屏幕库


// ====== WiFi 信息 ======
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";

// ====== 创建对象 ======
WebServer server(80);
TFT_eSPI tft = TFT_eSPI();

// ====== 当前显示内容 ======
String currentText = "Hello";

// ====== 网页内容 ======
String getHTML() {
String html = "<!DOCTYPE html><html>";
html += "<head><meta charset='utf-8'><title>XIAO Control</title></head>";
html += "<body style='text-align:center;font-family:sans-serif;'>";
html += "<h2>控制屏幕显示</h2>";

html += "<p>当前内容: " + currentText + "</p>";

html += "<a href='/f'><button style='font-size:20px;'>福</button></a><br><br>";
html += "<a href='/temp'><button style='font-size:20px;'>温度</button></a><br><br>";
html += "<a href='/hello'><button style='font-size:20px;'>Hello</button></a>";

html += "</body></html>";
return html;
}

// ====== 屏幕显示函数 ======
void drawText(String text) {
tft.fillScreen(TFT_BLACK);
tft.setTextColor(TFT_RED, TFT_BLACK);
tft.setTextSize(3);
tft.setCursor(20, 60);
tft.println(text);
}

// ====== 路由 ======
void handleRoot() {
server.send(200, "text/html", getHTML());
}

void handleFu() {
currentText = "FU";
drawText("FU"); // 中文可能不显示,先用FU测试
server.send(200, "text/html", getHTML());
}

void handleTemp() {
currentText = "Temp:25C";
drawText("Temp:25C");
server.send(200, "text/html", getHTML());
}

void handleHello() {
currentText = "Hello";
drawText("Hello");
server.send(200, "text/html", getHTML());
}

// ====== 初始化 ======
void setup() {
Serial.begin(115200);

// 屏幕初始化
tft.init();
tft.setRotation(0);
drawText("Starting...");

// WiFi连接
WiFi.begin(ssid, password);
Serial.print("Connecting");

while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}

Serial.println("\nConnected!");
Serial.println(WiFi.localIP());

drawText("Connected!");

// 路由绑定
server.on("/", handleRoot);
server.on("/f", handleFu);
server.on("/temp", handleTemp);
server.on("/hello", handleHello);

server.begin();
}

// ====== 主循环 ======
void loop() {
server.handleClient();
}

Iterations on the web interface (and current instability)

I tried to make the web side more visual: clearer layout, nicer buttons, and a stronger “product” feeling. Below is a later pass: dark theme, circular preset buttons (FU / TEMP / HELLO), optional text field, and a line that echoes the current string on the display — closer to how I imagined the final product.

Later web iteration: “Display Control Panel” with presets and manual text entry

As I iterated further:

  • Later versions became unstable
  • Control felt inconsistent sometimes the screen updated as expected, sometimes not

I am still debugging this part, hopefully I could update more beautiful pages soon.

Problems & debugging (summary)

SymptomWhat I checkedOutcome
Black screen after brief flashIncludes vs wiki example; library setupAdded #include <driver.h>; display OK
Chinese text not showing wellFont / encoding / memory realitiesSimplified to English to validate pipeline
Web control flaky after UI polishServer logic, memory, timingStill investigating; hypotheses above

Debugging habit that helped me: reduce the system confirm hardware with the smallest official example, then add one feature at a time (text only → sensors → WiFi → web).

What I would improve next

  • More stable web control — smaller HTML responses, clearer request handler, avoid blocking the main loop during long draws, and test with repeated clicks / multiple browser tabs.
  • Better font rendering — a planned subset or bitmap “福” so the design mockup survives on hardware without needing a full desktop font engine.

What I found interesting

Bridging UI design (Figma and mockups) and a physical round display makes the project feel tangible: the same circle is a layout on a laptop and then an object on my desk. The browser as a remote control adds another layer — three UIs in one story: design tool, web panel, and the device itself. Even the frustrating flaky WiFi phase taught me that reliability is part of the interface, not an afterthought.

Files (download)

Paths below are repository-relative (same style as my other weekly pages). In GitLab, open the file and use Download or Raw, or clone the repo to get everything.

Sketches (XIAO ESP32C3 + Round Display)

Libraries (ZIP archives in this repo)

Screen recording