项目运行于 Kindle 设备,基于多线程架构,电子墨水屏作为人机交互界面,通过 TCP 客户端连接 ESP32 服务端,实现屏幕触屏交互、本地时间日期显示、与 ESP32 硬件通信、传感器数据展示。
- 程序启动时通过命令行参数传入 ESP32 服务端 IP 地址,端口固定 8899。
- 图形界面显示采用 FBInk 绘图工具,在 C 程序中使用进程方式进行参数调用。
- 触摸使用 zforce2 驱动接口时行开发,对屏幕触摸坐标进行读取。
编译依赖:pthread 多线程库,fbink 绘图工具 /usr/bin/fbink。

一、 整体软件架构
|-----------|------------|----------------------------------------------------------------------------------|
| 模块 | 所属线程 | 功能职责 |
| 应用初始化 | main 主线程 | 封装在app_init() 函数;打开触屏设备、加载背景图片、初始化绘制全部 UI 界面 |
| 触屏事件处理 | 触屏独立线程 | 读取/dev/input/event0 触摸设备;识别屏幕点击;切换 LED 状态,触发网络下发控制指令;刷新屏幕 LED 状态显示 |
| 时间日期刷新 | 时间独立线程 | 1 秒周期检测系统时间;分钟变更时局部擦除墨水屏区域,更新时间、日期显示 |
| TCP 网络通信 | 网络独立线程 | TCP 连接 ESP32 服务端、断线自动重连;30 秒周期发送 GET 指令获取温湿度;接收触屏触发,下发 LED 控制命令;解析服务端应答更新传感器共享数据 |
| 传感器 UI 刷新 | main 守护主线程 | 循环调用refresh_sensor_display() ;对比新旧温湿度数值,仅数据发生变化时执行墨水屏局部刷新,减少屏幕刷新次数 |
共享变量全部使用互斥锁保护;LED 控制采用条件变量,触屏点击可立即唤醒网络线程发送指令,无需等待 30 秒查询周期。
二、 屏幕交互功能
1、UI 显示内容
- 顶部区域:实时时间、系统日期,分钟自动刷新。
- 传感器显示区:温度湿度、PM2.5、光照 Lux、烟雾数值。
- 状态按钮区:LED 开关状态、继电器开关状态。

fbink -g file=/mnt/us/smarthome2.png,halign=CENTER,valign=MIDDLE //显示应用的背景图片
fbink -x 15 -y 8 "15:35"//时间坐标
fbink -x 13 -y 10 "2026/08/04"//日期坐标
fbink -x 3 -y 27 "25c/48%"
fbink -x 16 -y 27 "25ug/m"
fbink -x 28 -y 27 "450Lux"
fbink -x 5 -y 43 "850"
fbink -x 17 -y 43 "Off"
fbink -x 30 -y 43 "On"
-
局部清屏坐标
fbink -k top=120,left=0,width=600,height=100
fbink -k top=405,left=45,width=120,height=50
fbink -k top=405,left=245,width=120,height=50
fbink -k top=405,left=445,width=120,height=50fbink -k top=655,left=45,width=120,height=50
fbink -k top=655,left=245,width=120,height=50
fbink -k top=655,left=445,width=120,height=50
2、 触屏点击交互
- LED 按钮区域点击
- 点击切换 LED 状态:On / Off,屏幕状态立即更新。
- LED 开启:网络线程发送指令
CMD1,ESP32 执行 WS2812 红色全亮,应答:OK:RED。 - LED 关闭:网络线程发送指令
CMD5,ESP32 执行 WS2812 全部熄灭,应答:OK:OFF。
- 继电器按钮区域点击
- 本地 UI 切换继电器 On/Off 状态;当前版本仅本地界面模拟,未实现下发网络指令,可后续扩展。
- 触摸区域坐标

LED 灯( x 坐标:230--330,y 坐标: 500--700)
继电器( x 坐标:430--530,y 坐标: 500--700)
所有屏幕绘制均使用 fbink 局部擦除,只刷新变化区域,降低电子墨水屏损耗。
三、网络通信功能
1、TCP 通信协议
Kindle 客户端 ↔ ESP32 服务端,端口 8899。
每条指令末尾必须带换行符\n,适配 ESP32 readStringUntil('\n')读取逻辑。
|----------|-------------|------------------------|
| 发送指令 | 功能 | ESP32 返回应答示例 |
| GET | 查询温湿度 | TEMP:24.50,HUM:47.20 |
| CMD1 | WS2812 红色全亮 | OK:RED |
| CMD5 | WS2812 全部熄灭 | OK:OFF |
2、 网络线程工作逻辑
- 程序启动自动尝试连接 ESP32;连接失败每 3 秒自动重试。
- 每 30s 自动发送
GET指令获取温湿度,解析返回数据更新程序内部传感器变量。 - 触屏点击 LED,通过条件变量立即触发网络线程发送 CMD1/CMD5 控制指令,不等待 30s 定时周期。
- TCP 断开、读写超时,自动关闭 socket,进入断线重连流程。
四、 关键函数说明
app_init():应用初始化函数。打开触屏设备,绘制背景图,完成界面初始渲染。refresh_sensor_display():传感器 UI 刷新独立函数,main 主线程循环调用;仅温湿度数值变化才刷新屏幕。touch_thread_entry():触屏线程入口,阻塞读取输入设备,处理点击事件。time_thread_entry():时间线程入口,负责时间日期更新。net_thread_entry():网络线程入口,TCP 连接管理、定时查询、下发控制指令、应答解析。send_command():底层 TCP 发送指令,循环读取直到换行符获取完整应答。parse_get_response():解析 GET 返回的温湿度字符串,更新全局共享传感器变量。
- 触摸区域坐标

LED 灯( x 坐标:230--330,y 坐标: 500--700)
继电器( x 坐标:430--530,y 坐标: 500--700)
五、 程序实现
程序采用 C 语言的多线程框架进行开发实现。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <linux/input.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <time.h>
#include <string.h>
#include <math.h>
#include <pthread.h>
#define TOUCH_DEV "/dev/input/event0"
#define FBINK_BIN "/usr/bin/fbink"
#define KV_DATETIME "top=120,left=0,width=600,height=100"
#define KV_SENSOR_TEMP "top=405,left=45,width=120,height=50"
#define KV_SENSOR_PM25 "top=405,left=245,width=120,height=50"
#define KV_SENSOR_LUX "top=405,left=445,width=120,height=50"
#define KV_SENSOR_SMOKE "top=655,left=45,width=120,height=50"
#define KV_SENSOR_LED "top=655,left=245,width=120,height=50"
#define KV_SENSOR_RELAY "top=655,left=445,width=120,height=50"
#define LED_X_MIN 230
#define LED_X_MAX 330
#define LED_Y_MIN 500
#define LED_Y_MAX 700
#define RELAY_X_MIN 430
#define RELAY_X_MAX 530
#define RELAY_Y_MIN 500
#define RELAY_Y_MAX 700
/* TCP config */
#define SERVER_PORT 8899
#define BUF_SIZE 256
#define RECV_TIMEOUT_SEC 3
#define QUERY_INTERVAL_SEC 30
/* 全局共享状态 */
static pthread_mutex_t g_state_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_net_cond = PTHREAD_COND_INITIALIZER;
static int g_led_on = 0;
static int g_relay_on = 0;
static int last_minute = -1;
static int last_day = -1;
/* 传感器数据,网络线程更新,主线程读取刷新UI */
static float g_temp = 25.0f;
static float g_humi = 48.0f;
static int g_pm25 = 25;
static int g_lux = 450;
static int g_smoke = 850;
static float g_last_temp = 0.0f;
static float g_last_humi = 0.0f;
/* 网络线程触发命令标记 */
#define NET_CMD_NONE 0
#define NET_CMD_LED_ON 1
#define NET_CMD_LED_OFF 2
static int g_net_cmd_flag = NET_CMD_NONE;
static int g_touch_fd = -1;
static int sock_fd = -1;
static char g_esp32_ip[64] = {0};
static int fbink_exec(char *const argv[])
{
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return -1;
}
if (pid == 0) {
execvp(argv[0], argv);
perror("execvp fbink");
_exit(127);
}
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
}
return -1;
}
static int fbink_local_erase(const char *kv_base)
{
char argbuf[256];
snprintf(argbuf, sizeof(argbuf), "%s", kv_base);
char * const argv[] = {
FBINK_BIN,
"-k", argbuf,
NULL
};
return fbink_exec(argv);
}
static void draw_background(void)
{
char * const argv[] = {
FBINK_BIN,
"-g", "file=/mnt/us/smarthome2.png,halign=CENTER,valign=MIDDLE",
NULL
};
fbink_exec(argv);
}
static void draw_time(int hour, int minute)
{
char buf[64];
snprintf(buf, sizeof(buf), "%02d:%02d", hour, minute);
char x_str[] = "15";
char y_str[] = "8";
char * const argv[] = {
FBINK_BIN,
"-x", x_str,
"-y", y_str,
buf,
NULL
};
fbink_exec(argv);
}
static void draw_date(int y, int m, int d)
{
char buf[64];
snprintf(buf, sizeof(buf), "%04d/%02d/%02d", y, m, d);
char x_str[] = "13";
char y_str[] = "10";
char * const argv[] = {
FBINK_BIN,
"-x", x_str,
"-y", y_str,
buf,
NULL
};
fbink_exec(argv);
}
static void draw_temp_humi(void)
{
char buf[128];
pthread_mutex_lock(&g_state_mutex);
float t = g_temp;
float h = g_humi;
pthread_mutex_unlock(&g_state_mutex);
int it = (int)roundf(t);
int ih = (int)roundf(h);
snprintf(buf, sizeof(buf), "%dc/%d%%", it, ih);
char x[]="3",y[]="27";
char *argv[]={FBINK_BIN,"-x",x,"-y",y,buf,NULL};
fbink_local_erase(KV_SENSOR_TEMP);
fbink_exec(argv);
}
static void draw_pm25(void)
{
char buf[128];
snprintf(buf, sizeof(buf), "%dug/m", g_pm25);
char x[]="16",y[]="27";
char *argv[]={FBINK_BIN,"-x",x,"-y",y,buf,NULL};
fbink_local_erase(KV_SENSOR_PM25);
fbink_exec(argv);
}
static void draw_lux(void)
{
char buf[128];
snprintf(buf, sizeof(buf), "%dLux", g_lux);
char x[]="28",y[]="27";
char *argv[]={FBINK_BIN,"-x",x,"-y",y,buf,NULL};
fbink_local_erase(KV_SENSOR_LUX);
fbink_exec(argv);
}
static void draw_smoke(void)
{
char buf[128];
snprintf(buf, sizeof(buf), "%d", g_smoke);
char x[]="5",y[]="43";
char *argv[]={FBINK_BIN,"-x",x,"-y",y,buf,NULL};
fbink_local_erase(KV_SENSOR_SMOKE);
fbink_exec(argv);
}
static void draw_led_status(void)
{
pthread_mutex_lock(&g_state_mutex);
int led = g_led_on;
pthread_mutex_unlock(&g_state_mutex);
char led_str[8];
strcpy(led_str, led ? "On" : "Off");
char x[]="17",y[]="43";
char *argv[]={FBINK_BIN,"-x",x,"-y",y,led_str,NULL};
fbink_local_erase(KV_SENSOR_LED);
fbink_exec(argv);
}
static void draw_relay_status(void)
{
pthread_mutex_lock(&g_state_mutex);
int relay = g_relay_on;
pthread_mutex_unlock(&g_state_mutex);
char rel_str[8];
strcpy(rel_str, relay ? "On" : "Off");
char x[]="30",y[]="43";
char *argv[]={FBINK_BIN,"-x",x,"-y",y,rel_str,NULL};
fbink_local_erase(KV_SENSOR_RELAY);
fbink_exec(argv);
}
static void draw_all_sensor(void)
{
draw_temp_humi();
draw_pm25();
draw_lux();
draw_smoke();
draw_led_status();
draw_relay_status();
}
/* 独立传感器刷新函数,main守护线程调用;仅温湿度变化才刷新屏幕 */
static void refresh_sensor_display(void)
{
pthread_mutex_lock(&g_state_mutex);
float curr_t = g_temp;
float curr_h = g_humi;
float last_t = g_last_temp;
float last_h = g_last_humi;
pthread_mutex_unlock(&g_state_mutex);
if(curr_t != last_t || curr_h != last_h)
{
draw_temp_humi();
pthread_mutex_lock(&g_state_mutex);
g_last_temp = curr_t;
g_last_humi = curr_h;
pthread_mutex_unlock(&g_state_mutex);
}
}
static void handle_tap(int x, int y)
{
printf("TAP event x=%d y=%d\n", x, y);
if (x >= LED_X_MIN && x <= LED_X_MAX && y >= LED_Y_MIN && y <= LED_Y_MAX)
{
pthread_mutex_lock(&g_state_mutex);
g_led_on = !g_led_on;
if(g_led_on)
{
g_net_cmd_flag = NET_CMD_LED_ON;
}
else
{
g_net_cmd_flag = NET_CMD_LED_OFF;
}
pthread_cond_signal(&g_net_cond);
int newval = g_led_on;
pthread_mutex_unlock(&g_state_mutex);
printf("TOGGLE_LED, new state=%s\n", newval?"On":"Off");
draw_led_status();
return;
}
if (x >= RELAY_X_MIN && x <= RELAY_X_MAX && y >= RELAY_Y_MIN && y <= RELAY_Y_MAX)
{
pthread_mutex_lock(&g_state_mutex);
g_relay_on = !g_relay_on;
int newval = g_relay_on;
pthread_mutex_unlock(&g_state_mutex);
printf("TOGGLE_RELAY, new state=%s\n", newval?"On":"Off");
draw_relay_status();
return;
}
}
static void *touch_thread_entry(void *arg)
{
(void)arg;
struct input_event ev;
int touch_x = -1, touch_y = -1;
int tap_pending = 0;
for(;;)
{
ssize_t n = read(g_touch_fd, &ev, sizeof(struct input_event));
if(n != sizeof(struct input_event))
{
usleep(100*1000);
continue;
}
switch(ev.type)
{
case EV_ABS:
if(ev.code == ABS_MT_POSITION_X){
touch_x = ev.value;
}else if(ev.code == ABS_MT_POSITION_Y){
touch_y = ev.value;
}else if(ev.code == ABS_MT_TRACKING_ID){
if(ev.value != -1){
tap_pending = 1;
}else{
if(tap_pending && touch_x != -1 && touch_y != -1){
handle_tap(touch_x, touch_y);
}
tap_pending = 0;
}
}
break;
case EV_SYN:
break;
default:
break;
}
}
return NULL;
}
static void *time_thread_entry(void *arg)
{
(void)arg;
for(;;)
{
sleep(1);
time_t now = time(NULL);
struct tm *tm_now = localtime(&now);
int curr_min = tm_now->tm_min;
int curr_day = tm_now->tm_mday;
if (curr_min != last_minute)
{
fbink_local_erase(KV_DATETIME);
draw_time(tm_now->tm_hour, curr_min);
if(curr_day != last_day){
draw_date(tm_now->tm_year+1900, tm_now->tm_mon+1, tm_now->tm_mday);
last_day = curr_day;
}
draw_date(tm_now->tm_year+1900, tm_now->tm_mon+1, tm_now->tm_mday);
last_minute = curr_min;
}
}
return NULL;
}
/* TCP 网络辅助函数 */
static void set_sock_timeout(int fd, int sec)
{
struct timeval timeout;
timeout.tv_sec = sec;
timeout.tv_usec = 0;
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
}
static int connect_server(void)
{
struct sockaddr_in serv_addr;
if (sock_fd > 0)
{
printf("已经连接,无需重连\n");
return 0;
}
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd < 0)
{
perror("socket create failed");
return -1;
}
set_sock_timeout(sock_fd, RECV_TIMEOUT_SEC);
memset(&serv_addr, 0, sizeof(serv_addr));
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERVER_PORT);
if (inet_pton(AF_INET, g_esp32_ip, &serv_addr.sin_addr) <= 0)
{
printf("IP地址格式错误:%s\n",g_esp32_ip);
close(sock_fd);
sock_fd = -1;
return -1;
}
if (connect(sock_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)
{
perror("connect failed");
close(sock_fd);
sock_fd = -1;
return -1;
}
printf("✅ 成功连接 ESP32 TCP Server %s:%d\n", g_esp32_ip, SERVER_PORT);
return 0;
}
static int send_command(const char *cmd, char *resp, int resp_len)
{
char send_buf[BUF_SIZE];
char tmp[BUF_SIZE];
int total = 0;
int i = 0;
ssize_t ret;
memset(resp, 0, resp_len);
if (sock_fd < 0)
{
printf("❌ 未连接服务器,请先连接!\n");
return -1;
}
snprintf(send_buf, sizeof(send_buf), "%s\n", cmd);
if (write(sock_fd, send_buf, strlen(send_buf)) < 0)
{
perror("send fail");
close(sock_fd);
sock_fd = -1;
return -1;
}
while (1)
{
ret = read(sock_fd, tmp, sizeof(tmp)-1);
if (ret <= 0)
{
printf("❌ 读取应答失败 / 超时 / 服务器断开\n");
close(sock_fd);
sock_fd = -1;
return -1;
}
for (; i < ret; i++)
{
if (total < resp_len - 1)
{
resp[total++] = tmp[i];
}
if (tmp[i] == '\n')
{
resp[total] = '\0';
return 0;
}
}
}
}
/* 解析GET应答 TEMP:XX.XX,HUM:XX.XX */
static void parse_get_response(const char *rsp)
{
float t,h;
if(sscanf(rsp,"TEMP:%f,HUM:%f",&t,&h)==2)
{
pthread_mutex_lock(&g_state_mutex);
g_temp = t;
g_humi = h;
pthread_mutex_unlock(&g_state_mutex);
printf("Parsed temp=%.2f hum=%.2f\n",t,h);
}
}
/* 网络线程入口:断线重连、30s GET查询、处理LED命令标记 */
static void *net_thread_entry(void *arg)
{
(void)arg;
char response[BUF_SIZE];
struct timespec ts;
for(;;)
{
/* 没有连接就尝试重连 */
if(sock_fd < 0)
{
connect_server();
sleep(3);
continue;
}
pthread_mutex_lock(&g_state_mutex);
/* 等待条件变量,最多等待30秒,到期执行GET查询 */
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_sec += QUERY_INTERVAL_SEC;
int wait_ret = pthread_cond_timedwait(&g_net_cond, &g_state_mutex, &ts);
int local_cmd = g_net_cmd_flag;
g_net_cmd_flag = NET_CMD_NONE;
pthread_mutex_unlock(&g_state_mutex);
/* 如果有LED命令标记,发送对应指令 */
if(local_cmd != NET_CMD_NONE)
{
if(local_cmd == NET_CMD_LED_ON)
{
if(0 == send_command("CMD1", response, sizeof(response)))
{
printf("ESP32 rsp: %s",response);
}
}
else if(local_cmd == NET_CMD_LED_OFF)
{
if(0 == send_command("CMD5", response, sizeof(response)))
{
printf("ESP32 rsp: %s",response);
}
}
}
/* 超时:执行GET查询温湿度 */
if(wait_ret == ETIMEDOUT)
{
if(0 == send_command("GET", response, sizeof(response)))
{
printf("GET rsp: %s",response);
parse_get_response(response);
}
}
}
return NULL;
}
static int app_init(void)
{
g_touch_fd = open(TOUCH_DEV, O_RDONLY);
if (g_touch_fd < 0) {
perror("open touch dev root required");
return -1;
}
draw_background();
usleep(600*1000);
{
time_t now = time(NULL);
struct tm *tm_now = localtime(&now);
fbink_local_erase(KV_DATETIME);
draw_time(tm_now->tm_hour, tm_now->tm_min);
draw_date(tm_now->tm_year+1900, tm_now->tm_mon+1, tm_now->tm_mday);
last_minute = tm_now->tm_min;
last_day = tm_now->tm_mday;
draw_all_sensor();
}
printf("SmartHome eInk UI init ok.\n");
return 0;
}
int main(int argc, char *argv[])
{
pthread_t th_touch, th_time, th_net;
int ret;
if(argc != 2)
{
fprintf(stderr,"Usage: %s <ESP32-IP>\nExample: %s 192.168.4.10\n",argv[0],argv[0]);
return EXIT_FAILURE;
}
strncpy(g_esp32_ip, argv[1], sizeof(g_esp32_ip)-1);
ret = app_init();
if(ret != 0)
{
fprintf(stderr,"app init failed\n");
return EXIT_FAILURE;
}
ret = pthread_create(&th_touch, NULL, touch_thread_entry, NULL);
if(ret != 0){ fprintf(stderr,"touch thread fail:%s\n",strerror(ret)); return 1; }
pthread_detach(th_touch);
ret = pthread_create(&th_time, NULL, time_thread_entry, NULL);
if(ret != 0){ fprintf(stderr,"time thread fail:%s\n",strerror(ret)); return 1; }
pthread_detach(th_time);
ret = pthread_create(&th_net, NULL, net_thread_entry, NULL);
if(ret != 0){ fprintf(stderr,"net thread fail:%s\n",strerror(ret)); return 1; }
pthread_detach(th_net);
/* 主线程守护,循环调用传感器刷新函数 */
for(;;)
{
refresh_sensor_display();
sleep(1);
}
close(g_touch_fd);
if(sock_fd>0) close(sock_fd);
pthread_mutex_destroy(&g_state_mutex);
pthread_cond_destroy(&g_net_cond);
return 0;
}
程序编译:

tony@tony-PC:~/work/kindle-dev$ arm-linux-gnueabi-gcc -march=armv7-a -mtune=cortex-a9 -msoft-float -mabi=aapcs-linux -lpthread -lm smart-tcp.c -o smart-tcp
tony@tony-PC:~/work/kindle-dev$ scp ./smart-tcp root@192.168.1.4:/mnt/us/gcc-test
[root@kindle gcc-test]# ./smart-tcp 192.168.1.11
六、系统 UI 管理脚本
自定义的 Kindle 的图形界面运行后,Kindle 系统原生的 UI 应用还在后台运行,会定期刷新屏幕,例如左上角的时间。需要通过下述脚本停止系统 UI 运行。
停止系统 UI 脚本:
#!/bin/bash
FBINK_CMD="/usr/bin/fbink"
# 暂停指定进程
stop_kindle_ui() {
local pname="$1"
pids=$(pidof "$pname")
if [ -n "$pids" ];then
echo "SIGSTOP $pname pid:$pids"
kill -SIGSTOP $pids
fi
}
# ============ 停止原生UI ============
stop_kindle_ui KPPMainAppV2
stop_kindle_ui awesome
stop_kindle_ui cvm
stop_kindle_ui mesquite
# 清理临时IO进程
killall gzip tar 2>/dev/null
# 清屏,消除原生界面残影
$FBINK_CMD -k -f -q
sleep 1
echo "原生UI已暂停,启动自定义应用"
# 启动你的程序
# /mnt/us/smarthome 192.168.4.10
恢复系统 UI 脚本:
恢复系统 UI 运行后,有可能屏幕还是清屏状态,主 UI 没有更新,可在屏幕上点击几个触发更新。
#!/bin/bash
FBINK_CMD="/usr/bin/fbink"
# 恢复原生UI
resume_kindle_ui() {
local pname="$1"
pids=$(pidof "$pname")
if [ -n "$pids" ];then
echo "SIGCONT $pname pid:$pids"
kill -SIGCONT $pids
fi
}
# 清屏,消除原生界面残影
$FBINK_CMD -k -f -q
sleep 1
# ============ 需要恢复Kindle原生界面时调用 ============
resume_kindle_ui cvm
resume_kindle_ui awesome
resume_kindle_ui KPPMainAppV2
resume_kindle_ui mesquite