目录
[一、 系统架构与技术选型](#一、 系统架构与技术选型)
[二、 核心功能实现与个人技术贡献](#二、 核心功能实现与个人技术贡献)
[1. 驾驶模式:拟真仪表盘与多路音效并发管理](#1. 驾驶模式:拟真仪表盘与多路音效并发管理)
[2. 监测模式:多传感器高并发数据采集与线程安全](#2. 监测模式:多传感器高并发数据采集与线程安全)
[3. 娱乐模式:LRC音画精准同步(核心重难点)](#3. 娱乐模式:LRC音画精准同步(核心重难点))
[三、 难点攻坚与工程化思考](#三、 难点攻坚与工程化思考)
[四、 项目集成与团队管理](#四、 项目集成与团队管理)
前言
在本次电子信息工程专业的综合实践中,我作为项目组长与核心软件开发者 ,带领团队基于 GEC6818开发板(三星S5P6818,Cortex-A53架构) 与 嵌入式Linux 系统,从零设计并实现了一款集驾驶控制、环境监测与多媒体娱乐于一体的智能车载系统。
本项目不仅是对嵌入式Linux交叉开发全流程的深度实践,更是一次从底层驱动、多线程并发控制到上层高级GUI交互的完整工程闭环。本文将从系统架构、核心技术实现、难点攻坚等维度进行硬核复盘。
一、 系统架构与技术选型
项目以嵌入式Linux 为核心,采用轻量级、高性能的LVGL 9.0图形框架作为人机交互界面。
-
硬件平台:GEC6818开发板,外接GY39气象传感器、MQ-2烟雾传感器、ATGM336H-5N GPS模块。
-
软件技术栈:C语言、LVGL 9.0、Linux多线程编程(pthread)、多进程管理(fork/exec)、串口通信(Termios/Select)、NMEA协议解析、LRC时间轴同步算法。
整个系统划分为三大核心模式:驾驶模式 (速度控制与音效反馈)、监测模式 (多传感器并发采集与实时显示)、娱乐模式(音乐播放与歌词同步)。
二、 核心功能实现与个人技术贡献
作为项目的技术骨干,我主导了整体软件架构的设计、核心UI开发、多媒体模块以及最终的系统代码集成。
1. 驾驶模式:拟真仪表盘与多路音效并发管理
-
UI交互设计 :基于LVGL的
lv_arc控件构建了双仪表盘,实时渲染当前车速与加速度。根据车速动态调整仪表盘颜色(绿-黄-红),实现超速预警,界面帧率稳定在60FPS以上。 -
并发音效控制 :油门、刹车、喇叭音效需在极短时间内高频切换。我采用
fork()子进程配合execlp()调用madplay播放器,并通过kill()发送系统信号(SIGSTOP/SIGCONT/SIGKILL)精细化管理音效生命周期。设计了播放器状态机与资源互锁机制,确保多路音效叠加时互不干扰,响应延迟严格控制在50ms以内。
2. 监测模式:多传感器高并发数据采集与线程安全
-
非阻塞采集 :系统开启独立的
sensor_thread线程处理底层硬件数据。通过select()I/O多路复用机制实现串口超时读取,彻底解决阻塞式读取导致的系统卡死问题。 -
协议解析 :针对GY39与GPS模块,编写了严格的数据帧头校验与解析算法;利用
nmea_get_field对NMEA报文进行逗号分隔解析,并完成经纬度向标准度数的数学转换。 -
线程安全(生产者-消费者模型) :引入
pthread_mutex_t互斥锁保护全局数据结构。后台线程采集数据后加锁写入,LVGL定时器加锁读取并刷新UI,完美解决了多线程环境下的数据竞争与UI崩溃问题,确保了数据采集的精准与低延时。
3. 娱乐模式:LRC音画精准同步(核心重难点)
-
系统不仅支持MP3播放,还实现了LRC歌词的实时联动。核心逻辑在于:通过
clock_gettime(CLOCK_MONOTONIC)获取当前毫秒级高精度时间戳,将其与预解析的LRC时间轴数组进行**二分查找(时间复杂度O(logN))**比对,提取对应歌词。 -
为保证非阻塞,采用独立的LVGL定时器轮询当前进度并高亮渲染当前行。同时处理了文件读取时的UTF-8/GBK编码及缺行异常过滤,实现了"所听即所见"的极致体验。
三、 难点攻坚与工程化思考
在开发过程中,我重点攻克了以下工程难题,这也是本系统稳定运行的基石:
-
多音效并发与系统资源抢占 :
频繁触发音效极易导致音频解码卡顿和内存泄漏。我设计的解决方案是:利用操作系统层面的独立进程隔离,结合信号机制进行音效打断与平滑切换。将核心音效资源预加载至内存,极大降低了播放时的I/O延迟,确保了高负载下UI渲染与音频输出的双重流畅。
-
UI刷新与后台数据采集的解耦 :
早期方案中,传感器数据的轮询直接写在UI回调里,导致触摸响应迟钝。重构后,严格践行**"数据生产与UI消费分离"**的架构,底层线程专职采集与解析,UI层通过定时器安全拉取,极大提升了系统的鲁棒性与交互流畅度。
四、 项目集成与团队管理
作为项目组长,我不仅负责核心代码的编写,还承担了整个项目的技术统筹与进度管控:
-
架构制定:设计底层硬件交互与上层UI逻辑的接口规范,确保模块间低耦合。
-
代码审查与集成:负责最终软件代码的Review与Git仓库合并,排查解决多环境下的交叉编译依赖问题。
-
全链路闭环:统筹安排硬件电路连接、文献资料梳理、PPT排版与答辩逻辑,协调各成员技术成果的无缝对接,确保系统在GEC6818开发板上一次跑通并稳定演示。
五、核心控制代码
/* LVGL 9.0 智能车载系统 */
#include "lvgl/lvgl.h"
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <termios.h>
#include <sys/select.h>
#include <sys/time.h>
#include <time.h>
#include <signal.h>
#include <math.h>
#include <sys/wait.h>
#include "Chinese.h" // 中文库
/* UI 字符串 */
#define CHINESE_FONT &Chinese
#define UI_TITLE "传感器仪表盘"
#define UI_ENV "环境信息"
#define UI_TEMP "温度:"
#define UI_HUMIDITY "湿度:"
#define UI_PRESSURE "气压:"
#define UI_LIGHT "光照:"
#define UI_SMOKE "烟雾浓度"
#define UI_GPS "GPS 定位"
#define UI_GPS_STATUS "状态:"
#define UI_GPS_COORD "坐标:"
#define UI_GPS_TIME "时间:"
#define UI_VALID "有效"
#define UI_INVALID "无效"
#define UI_MAIN_TITLE "小米SU7车载系统"
#define UI_BTN_SPEED "油门刹车控制"
#define UI_BTN_SENSOR "传感器仪表盘"
#define UI_BTN_MUSIC "音乐播放器"
#define UI_BACK "返回主菜单"
#define UI_SPEED_TITLE "智能车载系统控制"
#define UI_SPEED_VAL "当前速度"
#define UI_MUSIC_TITLE "音乐播放器"
#define UI_PLAY "播放"
#define UI_PAUSE "暂停"
#define UI_PREV "上一首"
#define UI_NEXT "下一首"
/* 串口辅助 */
static int read_byte_timeout(int fd, unsigned char *buf, int timeout_ms)
{
fd_set fds;
struct timeval tv;
FD_ZERO(&fds);
FD_SET(fd, &fds);
tv.tv_sec = timeout_ms / 1000;
tv.tv_usec = (timeout_ms % 1000) * 1000;
int ret = select(fd + 1, &fds, NULL, NULL, &tv);
if (ret > 0)
return read(fd, buf, 1);
return 0;
}
/* 串口初始化 */
int uart_init(const char *uart_name)
{
int fd = open(uart_name, O_RDWR);
if (fd == -1)
{
perror("open uart error");
return -1;
}
struct termios myserial;
memset(&myserial, 0, sizeof(myserial));
myserial.c_cflag |= (CLOCAL | CREAD);
myserial.c_cflag &= ~CSIZE;
myserial.c_cflag &= ~CRTSCTS;
myserial.c_cflag |= CS8;
myserial.c_cflag &= ~CSTOPB;
myserial.c_cflag &= ~PARENB;
cfsetospeed(&myserial, B9600);
cfsetispeed(&myserial, B9600);
tcflush(fd, TCIFLUSH);
tcsetattr(fd, TCSANOW, &myserial);
return fd;
}
/* GY39 */
unsigned char cmd_light[] = {0xA5, 0x81, 0x26};
unsigned char cmd_tph[] = {0xA5, 0x82, 0x27};
typedef struct {
unsigned int light;
int temperature;
unsigned int pressure;
unsigned int humidity;
int altitude;
} GY39_Data;
void gy39_send_cmd(int fd, unsigned char *cmd)
{
tcflush(fd, TCIFLUSH);
write(fd, cmd, 3);
usleep(200000);
}
int gy39_recv_frame(int fd, unsigned char *recvbuf, int bufsize)
{
int i = 0, flag = 0;
while (1)
{
unsigned char buf = 0;
int ret = read_byte_timeout(fd, &buf, 3000);
if (ret <= 0) return -1;
if (flag == 0 && buf == 0x5A) {
recvbuf[0] = buf; i = 1; flag = 1;
} else if (flag == 1 && buf == 0x5A) {
recvbuf[1] = buf; i = 2; flag = 2;
} else if (flag >= 2) {
if (i < bufsize) recvbuf[i] = buf;
i++;
} else if (flag == 1 && buf != 0x5A) {
flag = 0; i = 0;
}
if (flag >= 2 && i >= 3) {
if (recvbuf[2] == 0x15 && i >= 9) return 0;
if (recvbuf[2] == 0x45 && i >= 15) return 1;
}
}
}
int gy39_read_light(int fd, unsigned int *light)
{
unsigned char recvbuf[15] = {0};
gy39_send_cmd(fd, cmd_light);
int frame_type = gy39_recv_frame(fd, recvbuf, sizeof(recvbuf));
if (frame_type < 0) return -1;
if (frame_type == 1) {
frame_type = gy39_recv_frame(fd, recvbuf, sizeof(recvbuf));
if (frame_type < 0) return -1;
}
if (frame_type != 0) return -1;
unsigned char checksum = 0;
for (int j = 0; j < 8; j++) checksum += recvbuf[j];
if (checksum != recvbuf[8]) return -1;
*light = ((unsigned int)recvbuf[4] << 24) | (recvbuf[5] << 16) |
(recvbuf[6] << 8) | recvbuf[7];
*light /= 100;
return 0;
}
int gy39_read_tph(int fd, GY39_Data *data)
{
unsigned char recvbuf[15] = {0};
gy39_send_cmd(fd, cmd_tph);
int frame_type = gy39_recv_frame(fd, recvbuf, sizeof(recvbuf));
if (frame_type < 0) return -1;
if (frame_type == 0) {
frame_type = gy39_recv_frame(fd, recvbuf, sizeof(recvbuf));
if (frame_type < 0) return -1;
}
if (frame_type != 1) return -1;
unsigned char checksum = 0;
for (int j = 0; j < 14; j++) checksum += recvbuf[j];
if (checksum != recvbuf[14]) return -1;
data->temperature = (recvbuf[4] << 8) | recvbuf[5];
data->pressure = ((unsigned int)recvbuf[6] << 24) | (recvbuf[7] << 16) |
(recvbuf[8] << 8) | recvbuf[9];
data->humidity = (recvbuf[10] << 8) | recvbuf[11];
data->altitude = (recvbuf[12] << 8) | recvbuf[13];
return 0;
}
/* MQ-2 */
unsigned char cmd_mq2[] = {0xFF, 0x01, 0x86, 0x00, 0x00, 0x00, 0x00, 0x00, 0x79};
int mq2_read(int fd, int *smoke)
{
unsigned char recvbuf[9] = {0};
int i = 0, flag = 0;
tcflush(fd, TCIFLUSH);
write(fd, cmd_mq2, 9);
while (1) {
unsigned char buf = 0;
int ret = read_byte_timeout(fd, &buf, 2000);
if (ret <= 0) return -1;
if (buf == 0xFF) { flag = 1; i = 0; }
if (flag == 1) {
recvbuf[i++] = buf;
if (i == 9) break;
}
}
*smoke = (recvbuf[2] << 8) | recvbuf[3];
return 0;
}
/* GPS */
typedef struct {
int valid;
double latitude;
double longitude;
double altitude;
double speed;
int satellites;
char time[12];
char date[12];
} GPS_Data;
static int nmea_get_field(const char *sentence, int index, char *out, int outlen)
{
int field = 0, i = 0, j = 0;
while (sentence[i] && sentence[i] != ',') i++;
if (sentence[i] == ',') i++;
while (sentence[i] && sentence[i] != '*') {
if (field == index) {
if (sentence[i] == ',') { out[j] = '\0'; return j; }
if (j < outlen - 1) out[j++] = sentence[i];
}
if (sentence[i] == ',') field++;
i++;
}
out[j] = '\0';
return j;
}
static double nmea_to_degrees(const char *str, int lat)
{
double val = atof(str);
int deg_width = lat ? 2 : 3;
int deg = 0;
char deg_str[4] = {0};
strncpy(deg_str, str, deg_width);
deg = atoi(deg_str);
double minutes = atof(str + deg_width);
return deg + minutes / 60.0;
}
static int parse_gprmc(const char *sentence, GPS_Data *gps)
{
char field[32];
nmea_get_field(sentence, 1, field, sizeof(field));
gps->valid = (field[0] == 'A') ? 1 : 0;
nmea_get_field(sentence, 0, field, sizeof(field));
if (strlen(field) >= 6) {
snprintf(gps->time, sizeof(gps->time), "%c%c:%c%c:%c%c",
field[0], field[1], field[2], field[3], field[4], field[5]);
}
nmea_get_field(sentence, 2, field, sizeof(field));
if (strlen(field) > 0) {
gps->latitude = nmea_to_degrees(field, 1);
nmea_get_field(sentence, 3, field, sizeof(field));
if (field[0] == 'S') gps->latitude = -gps->latitude;
}
nmea_get_field(sentence, 4, field, sizeof(field));
if (strlen(field) > 0) {
gps->longitude = nmea_to_degrees(field, 0);
nmea_get_field(sentence, 5, field, sizeof(field));
if (field[0] == 'W') gps->longitude = -gps->longitude;
}
nmea_get_field(sentence, 6, field, sizeof(field));
gps->speed = atof(field);
nmea_get_field(sentence, 9, field, sizeof(field));
if (strlen(field) >= 6) {
snprintf(gps->date, sizeof(gps->date), "%c%c/%c%c/20%c%c",
field[0], field[1], field[2], field[3], field[4], field[5]);
}
return 0;
}
static int parse_gpgga(const char *sentence, GPS_Data *gps)
{
char field[32];
nmea_get_field(sentence, 7, field, sizeof(field));
gps->satellites = atoi(field);
nmea_get_field(sentence, 9, field, sizeof(field));
gps->altitude = atof(field);
return 0;
}
int gps_read(int fd, GPS_Data *gps)
{
char line[128];
int li = 0, got_rmc = 0, got_gga = 0;
memset(gps, 0, sizeof(GPS_Data));
while (!(got_rmc && got_gga)) {
unsigned char ch = 0;
int ret = read_byte_timeout(fd, &ch, 5000);
if (ret <= 0) return -1;
if (ch == '$') {
li = 0;
line[li++] = ch;
} else if (li > 0 && li < sizeof(line) - 1) {
line[li++] = ch;
if (ch == '\n') {
line[li] = '\0';
if (strstr(line, "$GPRMC") || strstr(line, "$GNRMC")) {
parse_gprmc(line, gps);
got_rmc = 1;
} else if (strstr(line, "$GPGGA") || strstr(line, "$GNGGA")) {
parse_gpgga(line, gps);
got_gga = 1;
}
li = 0;
}
}
}
return 0;
}
/* 全局数据 */
typedef struct {
pthread_mutex_t mutex;
unsigned int light;
int temperature;
unsigned int pressure;
unsigned int humidity;
int altitude;
int smoke;
GPS_Data gps;
int gy39_ok;
int mq2_ok;
int gps_ok;
} SensorData;
static SensorData g_sensor_data = {0};
/* LVGL 控件 */
static lv_obj_t *temp_label;
static lv_obj_t *humidity_label;
static lv_obj_t *pressure_label;
static lv_obj_t *light_label;
static lv_obj_t *smoke_label;
static lv_obj_t *smoke_bar; // 改为进度条控件
static lv_obj_t *gps_status_label;
static lv_obj_t *gps_coord_label;
static lv_obj_t *gps_time_label;
#define SMOKE_MAX 1000
/* 多界面 */
static lv_obj_t *screen_main;
static lv_obj_t *screen_speed;
static lv_obj_t *screen_sensor;
static lv_obj_t *screen_music;
static lv_obj_t *screen_playlist;
/* 速度界面 */
static lv_obj_t *speed_value_label;
static lv_obj_t *speed_arc;
static lv_obj_t *accel_value_label;
static lv_obj_t *accel_arc;
static int current_speed = 0;
/* 音乐播放器 */
static int current_song = 0; // 0, 1, 2
static int music_playing = 0; // 0=暂停, 1=播放
static pid_t music_pid = -1; // madplay 进程 PID
static lv_obj_t *song_label;
static lv_obj_t *play_btn_label;
static lv_obj_t *lrc_label_prev;
static lv_obj_t *lrc_label_curr;
static lv_obj_t *lrc_label_next;
static const char *song_files[] = {"./1.mp3", "./2.mp3", "./3.mp3"};
static const char *song_names[] = {"晴天", "第十幕", "恋人"};
/* 传感器线程 */
static void *sensor_thread_func(void *arg)
{
int gy39_fd = uart_init("/dev/ttySAC1");
int mq2_fd = uart_init("/dev/ttySAC2");
int gps_fd = uart_init("/dev/ttySAC3");
if (gy39_fd < 0 || mq2_fd < 0 || gps_fd < 0) {
fprintf(stderr, "UART init failed\n");
return NULL;
}
while (1) {
SensorData local;
pthread_mutex_lock(&g_sensor_data.mutex);
local = g_sensor_data;
pthread_mutex_unlock(&g_sensor_data.mutex);
if (gy39_read_light(gy39_fd, &local.light) == 0)
local.gy39_ok = 1;
else
local.gy39_ok = 0;
GY39_Data gy39_tph;
if (gy39_read_tph(gy39_fd, &gy39_tph) == 0) {
local.temperature = gy39_tph.temperature;
local.pressure = gy39_tph.pressure;
local.humidity = gy39_tph.humidity;
local.altitude = gy39_tph.altitude;
local.gy39_ok = 1;
} else {
local.gy39_ok = 0;
}
if (mq2_read(mq2_fd, &local.smoke) == 0)
local.mq2_ok = 1;
else
local.mq2_ok = 0;
if (gps_read(gps_fd, &local.gps) == 0) {
local.gps_ok = 1;
printf("GPS: valid=%d lat=%.6f lon=%.6f sat=%d time=%s\n",
local.gps.valid, local.gps.latitude, local.gps.longitude,
local.gps.satellites, local.gps.time);
} else {
local.gps_ok = 0;
printf("GPS: read failed\n");
}
pthread_mutex_lock(&g_sensor_data.mutex);
memcpy(&g_sensor_data, &local, sizeof(SensorData));
pthread_mutex_unlock(&g_sensor_data.mutex);
sleep(1);
}
close(gy39_fd);
close(mq2_fd);
close(gps_fd);
return NULL;
}
/* UI 更新 */
static void ui_update_timer(lv_timer_t *timer)
{
if (lv_screen_active() != screen_sensor) return; /* 只在传感器界面更新 */
SensorData data;
pthread_mutex_lock(&g_sensor_data.mutex);
data = g_sensor_data;
pthread_mutex_unlock(&g_sensor_data.mutex);
char buf[64];
if (data.gy39_ok) {
snprintf(buf, sizeof(buf), "%.2f °C", data.temperature / 100.0);
lv_label_set_text(temp_label, buf);
snprintf(buf, sizeof(buf), "%.2f %%", data.humidity / 100.0);
lv_label_set_text(humidity_label, buf);
snprintf(buf, sizeof(buf), "%.2f kPa", data.pressure / 1000.0);
lv_label_set_text(pressure_label, buf);
snprintf(buf, sizeof(buf), "%u lux", data.light);
lv_label_set_text(light_label, buf);
} else {
lv_label_set_text(temp_label, "-- °C");
lv_label_set_text(humidity_label, "-- %%");
lv_label_set_text(pressure_label, "-- kPa");
lv_label_set_text(light_label, "-- lux");
}
/* 烟雾浓度:更新进度条和数值标签 */
if (data.mq2_ok) {
snprintf(buf, sizeof(buf), "%d", data.smoke);
lv_label_set_text(smoke_label, buf);
lv_bar_set_value(smoke_bar, data.smoke, LV_ANIM_ON);
} else {
lv_label_set_text(smoke_label, "--");
lv_bar_set_value(smoke_bar, 0, LV_ANIM_ON);
}
if (data.gps_ok && data.gps.valid) {
snprintf(buf, sizeof(buf), "%s\n%.6f, %.6f", UI_VALID, data.gps.latitude, data.gps.longitude);
lv_label_set_text(gps_coord_label, buf);
snprintf(buf, sizeof(buf), "%s %s", data.gps.time, data.gps.date);
lv_label_set_text(gps_time_label, buf);
lv_label_set_text(gps_status_label, UI_VALID);
} else {
lv_label_set_text(gps_coord_label, UI_INVALID);
lv_label_set_text(gps_time_label, "--:--:-- --/--/--");
lv_label_set_text(gps_status_label, UI_INVALID);
}
}
/* 音乐播放 */
static void lrc_load_for_song(int song_index);
static void music_timer_start(void);
static void music_timer_pause(void);
static void music_timer_reset(void);
static void music_stop(void)
{
if (music_pid > 0) {
kill(music_pid, SIGKILL);
waitpid(music_pid, NULL, 0);
music_pid = -1;
}
music_timer_pause();
music_playing = 0;
}
static void music_play(int song_index)
{
music_stop();
lrc_load_for_song(song_index);
music_timer_reset();
music_pid = fork();
if (music_pid == 0) {
/* 子进程:播放音乐 */
execlp("madplay", "madplay", "-Q", song_files[song_index], NULL);
perror("execlp madplay failed"); /* exec 失败时打印原因 */
_exit(1);
}
if (music_pid < 0) {
perror("fork failed");
} else {
printf("Playing: %s (pid=%d)\n", song_files[song_index], music_pid);
}
music_playing = 1;
current_song = song_index;
}
static void music_toggle_pause(void)
{
if (music_pid <= 0) return;
if (music_playing) {
kill(music_pid, SIGSTOP);
music_timer_pause();
music_playing = 0;
} else {
kill(music_pid, SIGCONT);
music_timer_start();
music_playing = 1;
}
}
/* LRC 歌词 */
#define LRC_MAX_LINES 128
typedef struct {
int time_ms; /* 时间戳(毫秒) */
char *text; /* 歌词文本 */
} LrcLine;
static LrcLine lrc_lines[LRC_MAX_LINES];
static int lrc_count = 0;
static int lrc_current_line = -1;
/* 播放时间追踪 */
static struct timespec play_start_time;
static int play_accumulated_ms = 0; /* 暂停前累计的毫秒数 */
static void lrc_clear(void)
{
for (int i = 0; i < lrc_count; i++) {
free(lrc_lines[i].text);
lrc_lines[i].text = NULL;
}
lrc_count = 0;
lrc_current_line = -1;
}
/* 解析 LRC 文件,path 为完整路径(如 "./1.lrc") */
static void lrc_load(const char *path)
{
lrc_clear();
FILE *fp = fopen(path, "r");
if (!fp) {
printf("LRC not found: %s\n", path);
return;
}
char line[256];
while (fgets(line, sizeof(line), fp) && lrc_count < LRC_MAX_LINES) {
/* 解析 [mm:ss.xx] 格式 */
int min, sec, ms;
if (sscanf(line, "[%d:%d.%d]", &min, &sec, &ms) >= 2) {
int time_ms = min * 60000 + sec * 1000 + ms * 10;
/* 找到 ] 后面的歌词文本 */
char *p = strchr(line, ']');
if (p) {
p++;
while (*p == ' ') p++;
/* 去掉换行符 */
size_t len = strlen(p);
while (len > 0 && (p[len-1] == '\n' || p[len-1] == '\r'))
p[--len] = '\0';
if (len > 0) {
/* 只保留汉字和ASCII字母数字(过滤所有标点符号) */
char clean[256];
int ci = 0;
unsigned char *u = (unsigned char *)p;
while (*u && ci < (int)sizeof(clean) - 4) {
if ((*u >= 'A' && *u <= 'Z') || (*u >= 'a' && *u <= 'z')
|| (*u >= '0' && *u <= '9') || *u == ' ') {
clean[ci++] = *u++;
} else if (*u >= 0xE4 && *u <= 0xE9 && u[1] >= 0x80 && u[2] >= 0x80) {
/* UTF-8 汉字(3字节),保留 */
clean[ci++] = *u++;
clean[ci++] = *u++;
clean[ci++] = *u++;
} else {
u++;
}
}
clean[ci] = '\0';
if (ci > 0) {
lrc_lines[lrc_count].time_ms = time_ms;
lrc_lines[lrc_count].text = strdup(clean);
lrc_count++;
}
}
}
}
}
fclose(fp);
printf("LRC loaded: %d lines from %s\n", lrc_count, path);
}
/* 根据 mp3 文件名自动生成 lrc 路径并加载 */
static void lrc_load_for_song(int song_index)
{
char lrc_path[256];
const char *mp3 = song_files[song_index];
snprintf(lrc_path, sizeof(lrc_path), "%s", mp3);
/* 将 .mp3 后缀替换为 .lrc */
size_t len = strlen(lrc_path);
if (len > 4 && strcmp(lrc_path + len - 4, ".mp3") == 0)
strcpy(lrc_path + len - 4, ".lrc");
lrc_load(lrc_path);
}
/* 获取当前播放时间(毫秒) */
static int music_get_playback_ms(void)
{
if (!music_playing) return play_accumulated_ms;
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
int elapsed = (now.tv_sec - play_start_time.tv_sec) * 1000
+ (now.tv_nsec - play_start_time.tv_nsec) / 1000000;
return play_accumulated_ms + elapsed;
}
/* 开始计时(播放时调用) */
static void music_timer_start(void)
{
clock_gettime(CLOCK_MONOTONIC, &play_start_time);
}
/* 暂停计时(暂停时调用) */
static void music_timer_pause(void)
{
if (music_playing) {
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
play_accumulated_ms += (now.tv_sec - play_start_time.tv_sec) * 1000
+ (now.tv_nsec - play_start_time.tv_nsec) / 1000000;
}
}
/* 重置计时(切歌时调用) */
static void music_timer_reset(void)
{
play_accumulated_ms = 0;
clock_gettime(CLOCK_MONOTONIC, &play_start_time);
}
/* 二分查找当前歌词行 */
static int lrc_find_line(int time_ms)
{
int lo = 0, hi = lrc_count - 1, result = -1;
while (lo <= hi) {
int mid = (lo + hi) / 2;
if (lrc_lines[mid].time_ms <= time_ms) {
result = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return result;
}
/* 音效播放 */
static pid_t sfx_pid = -1;
static void sfx_stop(void)
{
if (sfx_pid > 0) {
kill(sfx_pid, SIGKILL);
waitpid(sfx_pid, NULL, 0);
sfx_pid = -1;
}
}
/* 播放一次音效(不循环) */
static void sfx_play_once(const char *file)
{
sfx_stop();
sfx_pid = fork();
if (sfx_pid == 0) {
execlp("madplay", "madplay", "-Q", file, NULL);
_exit(1);
}
}
/* 循环播放音效(-z 参数) */
static void sfx_play_loop(const char *file)
{
sfx_stop();
sfx_pid = fork();
if (sfx_pid == 0) {
execlp("madplay", "madplay", "-Q", "-z", file, NULL);
_exit(1);
}
}
/* 事件回调 */
/* 主菜单按钮回调 */
static void btn_go_speed_cb(lv_event_t *e)
{
(void)e;
lv_screen_load(screen_speed);
}
static void btn_go_sensor_cb(lv_event_t *e)
{
(void)e;
lv_screen_load(screen_sensor);
}
static void btn_go_music_cb(lv_event_t *e)
{
(void)e;
lv_screen_load(screen_music);
}
/* 通用返回主菜单回调 */
static void btn_back_cb(lv_event_t *e)
{
(void)e;
sfx_stop();
lv_screen_load(screen_main);
}
/* 速度控制 */
static lv_timer_t *speed_timer = NULL;
static int speed_step = 0; /* >0 = 油门, <0 = 刹车 */
/* 更新仪表盘显示(弧形 + 数值 + 颜色) */
static void speed_display_update(void)
{
/* 左侧:速度仪表 */
char buf[16];
snprintf(buf, sizeof(buf), "%d", current_speed);
lv_label_set_text(speed_value_label, buf);
lv_arc_set_value(speed_arc, current_speed);
lv_color_t c;
if (current_speed < 80)
c = lv_color_hex(0x00ff88);
else if (current_speed < 140)
c = lv_color_hex(0xffcc00);
else
c = lv_color_hex(0xff3333);
lv_obj_set_style_arc_color(speed_arc, c, LV_PART_INDICATOR);
lv_obj_set_style_text_color(speed_value_label, c, 0);
/* 右侧:加速度仪表 */
int accel = speed_step; /* 正=加速, 负=刹车, 0=无操作 */
snprintf(buf, sizeof(buf), "%+d", accel);
lv_label_set_text(accel_value_label, buf);
/* 加速度映射到 0-40 范围供弧形显示 */
int arc_val = abs(accel);
if (arc_val > 30) arc_val = 30;
lv_arc_set_value(accel_arc, arc_val);
lv_color_t ac;
if (accel > 0)
ac = lv_color_hex(0x00ff88); /* 加速:绿 */
else if (accel < 0)
ac = lv_color_hex(0xff3333); /* 刹车:红 */
else
ac = lv_color_hex(0x7f8c8d); /* 无操作:灰 */
lv_obj_set_style_arc_color(accel_arc, ac, LV_PART_INDICATOR);
lv_obj_set_style_text_color(accel_value_label, ac, 0);
}
static int btn_held = 0; /* 1=油门按住, -1=刹车按住, 0=没按 */
static void speed_timer_cb(lv_timer_t *timer)
{
if (lv_screen_active() != screen_speed) return; /* 只在速度界面更新 */
if (btn_held != 0) {
/* 按住油门/刹车:步长逐渐递增,最大30 */
if (abs(speed_step) < 30)
speed_step += btn_held;
current_speed += speed_step;
} else {
/* 没按键:加速度缓慢归零 */
if (speed_step > 0) speed_step--;
else if (speed_step < 0) speed_step++;
/* 速度自然减速 */
if (current_speed > 0) current_speed -= 1;
}
if (current_speed > 200) current_speed = 200;
if (current_speed < 0) current_speed = 0;
speed_display_update();
/* 速度和加速度都为0时停止定时器 */
if (current_speed == 0 && speed_step == 0) {
if (speed_timer) { lv_timer_delete(speed_timer); speed_timer = NULL; }
}
}
static void btn_accel_press_cb(lv_event_t *e)
{
(void)e;
sfx_play_once("./jiasu.mp3");
btn_held = 1;
speed_step = 1;
current_speed += 1;
if (current_speed > 200) current_speed = 200;
speed_display_update();
if (speed_timer == NULL)
speed_timer = lv_timer_create(speed_timer_cb, 300, NULL);
}
static void btn_accel_release_cb(lv_event_t *e)
{
(void)e;
sfx_stop();
btn_held = 0;
/* 定时器保留,让加速度和速度自然归零 */
}
static void btn_brake_press_cb(lv_event_t *e)
{
(void)e;
if (current_speed == 0) {
speed_step = 0;
speed_display_update();
return;
}
sfx_play_once("./jiansu.mp3");
btn_held = -1;
speed_step = -5;
current_speed -= 5;
if (current_speed < 0) current_speed = 0;
speed_display_update();
if (speed_timer == NULL)
speed_timer = lv_timer_create(speed_timer_cb, 300, NULL);
}
static void btn_brake_release_cb(lv_event_t *e)
{
(void)e;
sfx_stop();
btn_held = 0;
/* 定时器保留,让加速度和速度自然归零 */
}
/* 音乐播放器回调 */
static void btn_play_pause_cb(lv_event_t *e)
{
(void)e;
if (music_pid <= 0) {
/* 未在播放,开始播放当前歌曲 */
music_play(current_song);
} else {
/* 切换暂停/恢复 */
music_toggle_pause();
}
lv_label_set_text(play_btn_label, music_playing ? UI_PAUSE : UI_PLAY);
}
static void btn_prev_cb(lv_event_t *e)
{
(void)e;
int prev = (current_song + 2) % 3;
music_play(prev);
lv_label_set_text(song_label, song_names[current_song]);
lv_label_set_text(play_btn_label, UI_PAUSE);
}
static void btn_next_cb(lv_event_t *e)
{
(void)e;
int next = (current_song + 1) % 3;
music_play(next);
lv_label_set_text(song_label, song_names[current_song]);
lv_label_set_text(play_btn_label, UI_PAUSE);
}
/* 进入音乐界面时停止播放的回调(返回主菜单时停止) */
static void btn_back_music_cb(lv_event_t *e)
{
(void)e;
music_stop();
lv_label_set_text(play_btn_label, UI_PLAY);
lv_label_set_text(lrc_label_prev, "");
lv_label_set_text(lrc_label_curr, "");
lv_label_set_text(lrc_label_next, "");
lv_screen_load(screen_main);
}
/* 歌词同步定时器 */
static void lyrics_timer_cb(lv_timer_t *timer)
{
if (lv_screen_active() != screen_music) return; /* 只在音乐界面更新 */
if (lrc_count == 0 || !music_playing) return;
int ms = music_get_playback_ms();
int line = lrc_find_line(ms);
if (line == lrc_current_line) return; /* 行没变,不更新 */
lrc_current_line = line;
/* 更新上一行 */
if (line > 0)
lv_label_set_text(lrc_label_prev, lrc_lines[line - 1].text);
else
lv_label_set_text(lrc_label_prev, "");
/* 更新当前行 */
if (line >= 0)
lv_label_set_text(lrc_label_curr, lrc_lines[line].text);
else
lv_label_set_text(lrc_label_curr, "");
/* 更新下一行 */
if (line >= 0 && line + 1 < lrc_count)
lv_label_set_text(lrc_label_next, lrc_lines[line + 1].text);
else
lv_label_set_text(lrc_label_next, "");
}
/* 喇叭按钮回调 */
static void btn_horn_press_cb(lv_event_t *e)
{
(void)e;
sfx_play_once("./laba.mp3");
}
static void btn_horn_release_cb(lv_event_t *e)
{
(void)e;
sfx_stop();
}
/* 主菜单 */
static void create_main_menu(void)
{
screen_main = lv_obj_create(NULL);
lv_obj_set_style_bg_color(screen_main, lv_color_hex(0x0d1b2a), 0);
lv_obj_set_style_bg_opa(screen_main, LV_OPA_COVER, 0);
/* 标题居中放大 */
lv_obj_t *title = lv_label_create(screen_main);
lv_label_set_text(title, UI_MAIN_TITLE);
lv_obj_set_style_text_font(title, CHINESE_FONT, 0);
lv_obj_set_style_text_color(title, lv_color_hex(0x00d4ff), 0);
lv_obj_set_style_text_letter_space(title, 12, 0);
lv_obj_set_style_text_line_space(title, 4, 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 20);
/* 副标题 */
lv_obj_t *subtitle = lv_label_create(screen_main);
lv_label_set_text(subtitle, "Xiaomi SU7 Car System");
lv_obj_set_style_text_font(subtitle, &lv_font_montserrat_28, 0);
lv_obj_set_style_text_color(subtitle, lv_color_hex(0x7f8c8d), 0);
lv_obj_align(subtitle, LV_ALIGN_TOP_MID, 0, 70);
/* 按钮样式 */
static lv_style_t btn_style;
lv_style_init(&btn_style);
lv_style_set_bg_color(&btn_style, lv_color_hex(0x1a3a5c));
lv_style_set_bg_opa(&btn_style, LV_OPA_COVER);
lv_style_set_radius(&btn_style, 20);
lv_style_set_border_color(&btn_style, lv_color_hex(0x00d4ff));
lv_style_set_border_width(&btn_style, 2);
lv_style_set_text_color(&btn_style, lv_color_hex(0xffffff));
/* 按钮1: 油门刹车控制 */
lv_obj_t *btn1 = lv_btn_create(screen_main);
lv_obj_add_style(btn1, &btn_style, 0);
lv_obj_set_size(btn1, 320, 80);
lv_obj_align(btn1, LV_ALIGN_CENTER, 0, -75);
lv_obj_t *label1 = lv_label_create(btn1);
lv_label_set_text(label1, UI_BTN_SPEED);
lv_obj_set_style_text_font(label1, CHINESE_FONT, 0);
lv_obj_center(label1);
lv_obj_add_event_cb(btn1, btn_go_speed_cb, LV_EVENT_CLICKED, NULL);
/* 按钮2: 传感器仪表盘 */
lv_obj_t *btn2 = lv_btn_create(screen_main);
lv_obj_add_style(btn2, &btn_style, 0);
lv_obj_set_size(btn2, 320, 80);
lv_obj_align(btn2, LV_ALIGN_CENTER, 0, 15);
lv_obj_t *label2 = lv_label_create(btn2);
lv_label_set_text(label2, UI_BTN_SENSOR);
lv_obj_set_style_text_font(label2, CHINESE_FONT, 0);
lv_obj_center(label2);
lv_obj_add_event_cb(btn2, btn_go_sensor_cb, LV_EVENT_CLICKED, NULL);
/* 按钮3: 音乐播放器 */
lv_obj_t *btn3 = lv_btn_create(screen_main);
lv_obj_add_style(btn3, &btn_style, 0);
lv_obj_set_size(btn3, 320, 80);
lv_obj_align(btn3, LV_ALIGN_CENTER, 0, 105);
lv_obj_t *label3 = lv_label_create(btn3);
lv_label_set_text(label3, UI_BTN_MUSIC);
lv_obj_set_style_text_font(label3, CHINESE_FONT, 0);
lv_obj_center(label3);
lv_obj_add_event_cb(btn3, btn_go_music_cb, LV_EVENT_CLICKED, NULL);
}
/* 速度控制界面 */
/* 辅助:创建半圆弧形仪表盘 */
static lv_obj_t *create_gauge_arc(lv_obj_t *parent, int x_off, int y_off,
int range, lv_color_t bg_c, lv_color_t ind_c)
{
lv_obj_t *arc = lv_arc_create(parent);
lv_obj_set_size(arc, 250, 250);
lv_obj_align(arc, LV_ALIGN_CENTER, x_off, y_off);
lv_arc_set_rotation(arc, 180); /* 起始角度:左侧(开口朝下) */
lv_arc_set_bg_angles(arc, 0, 270);
lv_arc_set_value(arc, 0);
lv_arc_set_range(arc, 0, range);
lv_obj_remove_style(arc, NULL, LV_PART_KNOB);
lv_obj_clear_flag(arc, LV_OBJ_FLAG_CLICKABLE);
lv_obj_set_style_arc_color(arc, bg_c, LV_PART_MAIN);
lv_obj_set_style_arc_width(arc, 16, LV_PART_MAIN);
lv_obj_set_style_arc_color(arc, ind_c, LV_PART_INDICATOR);
lv_obj_set_style_arc_width(arc, 16, LV_PART_INDICATOR);
return arc;
}
/* 刻度数字:从左侧(0) 顺时针到右侧(最大值),开口朝下 */
static void create_gauge_ticks(lv_obj_t *parent, int x_off, int y_off,
const char **texts, int count)
{
for (int i = 0; i < count; i++) {
int angle = 180 - i * (270 / (count - 1)); /* 180°起,顺时针递减 */
double rad = angle * 3.14159 / 180.0;
int r = 148;
int x = (int)(x_off + r * cos(rad));
int y = (int)(y_off - r * sin(rad));
lv_obj_t *tick = lv_label_create(parent);
lv_label_set_text(tick, texts[i]);
lv_obj_set_style_text_font(tick, &lv_font_montserrat_16, 0);
lv_obj_set_style_text_color(tick, lv_color_hex(0xaaaaaa), 0);
lv_obj_align(tick, LV_ALIGN_CENTER, x, y);
}
}
static void create_speed_screen(void)
{
screen_speed = lv_obj_create(NULL);
lv_obj_set_style_bg_color(screen_speed, lv_color_hex(0x0d1b2a), 0);
lv_obj_set_style_bg_opa(screen_speed, LV_OPA_COVER, 0);
/* 标题 */
lv_obj_t *title = lv_label_create(screen_speed);
lv_label_set_text(title, UI_SPEED_TITLE);
lv_obj_set_style_text_font(title, CHINESE_FONT, 0);
lv_obj_set_style_text_color(title, lv_color_hex(0x00d4ff), 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10);
/* 左侧速度仪表 */
speed_arc = create_gauge_arc(screen_speed, -200, -40, 200,
lv_color_hex(0x2c3e50), lv_color_hex(0x00ff88));
static const char *speed_ticks[] = {"0", "40", "80", "120", "160", "200"};
create_gauge_ticks(screen_speed, -200, -40, speed_ticks, 6);
speed_value_label = lv_label_create(screen_speed);
lv_label_set_text(speed_value_label, "0");
lv_obj_set_style_text_font(speed_value_label, &lv_font_montserrat_48, 0);
lv_obj_set_style_text_color(speed_value_label, lv_color_hex(0x00ff88), 0);
lv_obj_align(speed_value_label, LV_ALIGN_CENTER, -200, -45);
lv_obj_t *speed_unit = lv_label_create(screen_speed);
lv_label_set_text(speed_unit, "km/h");
lv_obj_set_style_text_font(speed_unit, &lv_font_montserrat_16, 0);
lv_obj_set_style_text_color(speed_unit, lv_color_hex(0x7f8c8d), 0);
lv_obj_align(speed_unit, LV_ALIGN_CENTER, -200, -10);
lv_obj_t *speed_lbl = lv_label_create(screen_speed);
lv_label_set_text(speed_lbl, UI_SPEED_VAL);
lv_obj_set_style_text_font(speed_lbl, CHINESE_FONT, 0);
lv_obj_set_style_text_color(speed_lbl, lv_color_hex(0x00d4ff), 0);
lv_obj_align(speed_lbl, LV_ALIGN_CENTER, -200, -80);
/* 右侧加速度仪表 */
accel_arc = create_gauge_arc(screen_speed, 200, -40, 30,
lv_color_hex(0x2c3e50), lv_color_hex(0x00ff88));
static const char *accel_ticks[] = {"0", "6", "12", "18", "24", "30"};
create_gauge_ticks(screen_speed, 200, -40, accel_ticks, 5);
accel_value_label = lv_label_create(screen_speed);
lv_label_set_text(accel_value_label, "+0");
lv_obj_set_style_text_font(accel_value_label, &lv_font_montserrat_48, 0);
lv_obj_set_style_text_color(accel_value_label, lv_color_hex(0x7f8c8d), 0);
lv_obj_align(accel_value_label, LV_ALIGN_CENTER, 200, -45);
lv_obj_t *accel_unit = lv_label_create(screen_speed);
lv_label_set_text(accel_unit, "step");
lv_obj_set_style_text_font(accel_unit, &lv_font_montserrat_16, 0);
lv_obj_set_style_text_color(accel_unit, lv_color_hex(0x7f8c8d), 0);
lv_obj_align(accel_unit, LV_ALIGN_CENTER, 200, -10);
lv_obj_t *accel_lbl = lv_label_create(screen_speed);
lv_label_set_text(accel_lbl, "Accel");
lv_obj_set_style_text_font(accel_lbl, &lv_font_montserrat_16, 0);
lv_obj_set_style_text_color(accel_lbl, lv_color_hex(0x00d4ff), 0);
lv_obj_align(accel_lbl, LV_ALIGN_CENTER, 200, -80);
/* 底部按钮 */
lv_obj_t *btn_acc = lv_btn_create(screen_speed);
lv_obj_set_size(btn_acc, 130, 50);
lv_obj_align(btn_acc, LV_ALIGN_BOTTOM_MID, -170, -20);
lv_obj_set_style_bg_color(btn_acc, lv_color_hex(0x27ae60), 0);
lv_obj_set_style_radius(btn_acc, 12, 0);
lv_obj_t *acc_label = lv_label_create(btn_acc);
lv_label_set_text(acc_label, "油门 +");
lv_obj_set_style_text_font(acc_label, CHINESE_FONT, 0);
lv_obj_center(acc_label);
lv_obj_add_event_cb(btn_acc, btn_accel_press_cb, LV_EVENT_PRESSED, NULL);
lv_obj_add_event_cb(btn_acc, btn_accel_release_cb, LV_EVENT_RELEASED, NULL);
lv_obj_add_event_cb(btn_acc, btn_accel_release_cb, LV_EVENT_PRESS_LOST, NULL);
/* 喇叭按钮(油门和刹车中间) */
lv_obj_t *btn_horn = lv_btn_create(screen_speed);
lv_obj_set_size(btn_horn, 100, 50);
lv_obj_align(btn_horn, LV_ALIGN_BOTTOM_MID, 0, -20);
lv_obj_set_style_bg_color(btn_horn, lv_color_hex(0xf39c12), 0);
lv_obj_set_style_radius(btn_horn, 12, 0);
lv_obj_t *horn_lbl = lv_label_create(btn_horn);
lv_label_set_text(horn_lbl, "喇叭");
lv_obj_set_style_text_font(horn_lbl, CHINESE_FONT, 0);
lv_obj_set_style_text_color(horn_lbl, lv_color_hex(0xffffff), 0);
lv_obj_center(horn_lbl);
lv_obj_add_event_cb(btn_horn, btn_horn_press_cb, LV_EVENT_PRESSED, NULL);
lv_obj_add_event_cb(btn_horn, btn_horn_release_cb, LV_EVENT_RELEASED, NULL);
lv_obj_add_event_cb(btn_horn, btn_horn_release_cb, LV_EVENT_PRESS_LOST, NULL);
lv_obj_t *btn_brk = lv_btn_create(screen_speed);
lv_obj_set_size(btn_brk, 130, 50);
lv_obj_align(btn_brk, LV_ALIGN_BOTTOM_MID, 170, -20);
lv_obj_set_style_bg_color(btn_brk, lv_color_hex(0xe74c3c), 0);
lv_obj_set_style_radius(btn_brk, 12, 0);
lv_obj_t *brk_label = lv_label_create(btn_brk);
lv_label_set_text(brk_label, "刹车 -");
lv_obj_set_style_text_font(brk_label, CHINESE_FONT, 0);
lv_obj_center(brk_label);
lv_obj_add_event_cb(btn_brk, btn_brake_press_cb, LV_EVENT_PRESSED, NULL);
lv_obj_add_event_cb(btn_brk, btn_brake_release_cb, LV_EVENT_RELEASED, NULL);
lv_obj_add_event_cb(btn_brk, btn_brake_release_cb, LV_EVENT_PRESS_LOST, NULL);
lv_obj_t *btn_back = lv_btn_create(screen_speed);
lv_obj_set_size(btn_back, 160, 40);
lv_obj_align(btn_back, LV_ALIGN_TOP_LEFT, 10, 5);
lv_obj_set_style_bg_color(btn_back, lv_color_hex(0x7f8c8d), 0);
lv_obj_set_style_radius(btn_back, 8, 0);
lv_obj_t *back_label = lv_label_create(btn_back);
lv_label_set_text(back_label, UI_BACK);
lv_obj_set_style_text_font(back_label, CHINESE_FONT, 0);
lv_obj_center(back_label);
lv_obj_add_event_cb(btn_back, btn_back_cb, LV_EVENT_CLICKED, NULL);
}
/* 传感器界面 */
static void create_sensor_screen(void)
{
screen_sensor = lv_obj_create(NULL);
lv_obj_set_style_bg_color(screen_sensor, lv_color_hex(0x1a2a3a), 0);
lv_obj_set_style_bg_opa(screen_sensor, LV_OPA_COVER, 0);
lv_obj_t *title = lv_label_create(screen_sensor);
lv_label_set_text(title, UI_TITLE);
lv_obj_set_style_text_font(title, CHINESE_FONT, 0);
lv_obj_set_style_text_color(title, lv_color_hex(0xffffff), 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 10);
/* ---------- 返回按钮 ---------- */
lv_obj_t *btn_back = lv_btn_create(screen_sensor);
lv_obj_set_size(btn_back, 160, 40);
lv_obj_align(btn_back, LV_ALIGN_TOP_LEFT, 10, 5);
lv_obj_set_style_bg_color(btn_back, lv_color_hex(0x7f8c8d), 0);
lv_obj_set_style_radius(btn_back, 8, 0);
lv_obj_t *back_label = lv_label_create(btn_back);
lv_label_set_text(back_label, UI_BACK);
lv_obj_set_style_text_font(back_label, CHINESE_FONT, 0);
lv_obj_center(back_label);
lv_obj_add_event_cb(btn_back, btn_back_cb, LV_EVENT_CLICKED, NULL);
/* ---------- 环境卡片 ---------- */
lv_obj_t *env_card = lv_obj_create(screen_sensor);
lv_obj_set_size(env_card, 280, 240);
lv_obj_align(env_card, LV_ALIGN_TOP_LEFT, 10, 50);
lv_obj_set_style_bg_color(env_card, lv_color_hex(0x2c3e50), 0);
lv_obj_set_style_radius(env_card, 12, 0);
lv_obj_set_style_border_color(env_card, lv_color_hex(0xffffff), 0);
lv_obj_set_style_border_width(env_card, 2, 0);
lv_obj_t *env_title = lv_label_create(env_card);
lv_label_set_text(env_title, UI_ENV);
lv_obj_set_style_text_font(env_title, CHINESE_FONT, 0);
lv_obj_set_style_text_color(env_title, lv_color_hex(0xecf0f1), 0);
lv_obj_align(env_title, LV_ALIGN_TOP_LEFT, 10, 10);
lv_obj_t *temp_unit = lv_label_create(env_card);
lv_label_set_text(temp_unit, UI_TEMP);
lv_obj_set_style_text_font(temp_unit, CHINESE_FONT, 0);
lv_obj_align(temp_unit, LV_ALIGN_TOP_LEFT, 10, 50);
temp_label = lv_label_create(env_card);
lv_obj_set_style_text_font(temp_label, &lv_font_montserrat_20, 0);
lv_obj_align_to(temp_label, temp_unit, LV_ALIGN_OUT_RIGHT_MID, 10, 0);
lv_obj_t *hum_unit = lv_label_create(env_card);
lv_label_set_text(hum_unit, UI_HUMIDITY);
lv_obj_set_style_text_font(hum_unit, CHINESE_FONT, 0);
lv_obj_align(hum_unit, LV_ALIGN_TOP_LEFT, 10, 90);
humidity_label = lv_label_create(env_card);
lv_obj_set_style_text_font(humidity_label, &lv_font_montserrat_20, 0);
lv_obj_align_to(humidity_label, hum_unit, LV_ALIGN_OUT_RIGHT_MID, 10, 0);
lv_obj_t *pres_unit = lv_label_create(env_card);
lv_label_set_text(pres_unit, UI_PRESSURE);
lv_obj_set_style_text_font(pres_unit, CHINESE_FONT, 0);
lv_obj_align(pres_unit, LV_ALIGN_TOP_LEFT, 10, 130);
pressure_label = lv_label_create(env_card);
lv_obj_set_style_text_font(pressure_label, &lv_font_montserrat_20, 0);
lv_obj_align_to(pressure_label, pres_unit, LV_ALIGN_OUT_RIGHT_MID, 10, 0);
lv_obj_t *light_unit = lv_label_create(env_card);
lv_label_set_text(light_unit, UI_LIGHT);
lv_obj_set_style_text_font(light_unit, CHINESE_FONT, 0);
lv_obj_align(light_unit, LV_ALIGN_TOP_LEFT, 10, 170);
light_label = lv_label_create(env_card);
lv_obj_set_style_text_font(light_label, &lv_font_montserrat_20, 0);
lv_obj_align_to(light_label, light_unit, LV_ALIGN_OUT_RIGHT_MID, 10, 0);
/* ---------- 烟雾卡片 ---------- */
lv_obj_t *smoke_card = lv_obj_create(screen_sensor);
lv_obj_set_size(smoke_card, 280, 240);
lv_obj_align(smoke_card, LV_ALIGN_TOP_RIGHT, -10, 50);
lv_obj_set_style_bg_color(smoke_card, lv_color_hex(0x2c3e50), 0);
lv_obj_set_style_radius(smoke_card, 12, 0);
lv_obj_set_style_border_color(smoke_card, lv_color_hex(0xffffff), 0);
lv_obj_set_style_border_width(smoke_card, 2, 0);
lv_obj_t *smoke_title = lv_label_create(smoke_card);
lv_label_set_text(smoke_title, UI_SMOKE);
lv_obj_set_style_text_font(smoke_title, CHINESE_FONT, 0);
lv_obj_set_style_text_color(smoke_title, lv_color_hex(0xecf0f1), 0);
lv_obj_align(smoke_title, LV_ALIGN_TOP_LEFT, 10, 10);
smoke_bar = lv_bar_create(smoke_card);
lv_obj_set_size(smoke_bar, 200, 30);
lv_obj_align(smoke_bar, LV_ALIGN_TOP_LEFT, 10, 50);
lv_bar_set_range(smoke_bar, 0, SMOKE_MAX);
lv_bar_set_value(smoke_bar, 0, LV_ANIM_OFF);
lv_obj_set_style_bg_color(smoke_bar, lv_color_hex(0x444444), LV_PART_MAIN);
lv_obj_set_style_bg_color(smoke_bar, lv_color_hex(0xff5722), LV_PART_INDICATOR);
smoke_label = lv_label_create(smoke_card);
lv_obj_set_style_text_font(smoke_label, &lv_font_montserrat_24, 0);
lv_obj_set_style_text_color(smoke_label, lv_color_hex(0xffaa66), 0);
lv_obj_align_to(smoke_label, smoke_bar, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
lv_obj_t *range_label = lv_label_create(smoke_card);
lv_label_set_text(range_label, "0 - 1000");
lv_obj_set_style_text_color(range_label, lv_color_hex(0xaaaaaa), 0);
lv_obj_align_to(range_label, smoke_bar, LV_ALIGN_OUT_RIGHT_MID, 10, 0);
/* ---------- GPS 卡片 ---------- */
lv_obj_t *gps_card = lv_obj_create(screen_sensor);
lv_obj_set_size(gps_card, lv_obj_get_width(screen_sensor) - 20, 150);
lv_obj_align(gps_card, LV_ALIGN_TOP_MID, 0, 300);
lv_obj_set_style_bg_color(gps_card, lv_color_hex(0x2c3e50), 0);
lv_obj_set_style_radius(gps_card, 12, 0);
lv_obj_set_style_border_color(gps_card, lv_color_hex(0xffffff), 0);
lv_obj_set_style_border_width(gps_card, 3, 0);
lv_obj_t *gps_title = lv_label_create(gps_card);
lv_label_set_text(gps_title, UI_GPS);
lv_obj_set_style_text_font(gps_title, CHINESE_FONT, 0);
lv_obj_set_style_text_color(gps_title, lv_color_hex(0xecf0f1), 0);
lv_obj_align(gps_title, LV_ALIGN_TOP_LEFT, 10, 10);
lv_obj_t *gps_status_text = lv_label_create(gps_card);
lv_label_set_text(gps_status_text, UI_GPS_STATUS);
lv_obj_set_style_text_font(gps_status_text, CHINESE_FONT, 0);
lv_obj_align(gps_status_text, LV_ALIGN_TOP_LEFT, 10, 45);
gps_status_label = lv_label_create(gps_card);
lv_obj_align_to(gps_status_label, gps_status_text, LV_ALIGN_OUT_RIGHT_MID, 10, 0);
lv_obj_t *gps_coord_text = lv_label_create(gps_card);
lv_label_set_text(gps_coord_text, UI_GPS_COORD);
lv_obj_set_style_text_font(gps_coord_text, CHINESE_FONT, 0);
lv_obj_align(gps_coord_text, LV_ALIGN_TOP_LEFT, 10, 85);
gps_coord_label = lv_label_create(gps_card);
lv_obj_set_style_text_font(gps_coord_label, &lv_font_montserrat_16, 0);
lv_obj_align_to(gps_coord_label, gps_coord_text, LV_ALIGN_OUT_RIGHT_MID, 10, 0);
lv_obj_t *gps_time_text = lv_label_create(gps_card);
lv_label_set_text(gps_time_text, UI_GPS_TIME);
lv_obj_set_style_text_font(gps_time_text, CHINESE_FONT, 0);
lv_obj_align(gps_time_text, LV_ALIGN_TOP_RIGHT, -120, 45);
gps_time_label = lv_label_create(gps_card);
lv_obj_align_to(gps_time_label, gps_time_text, LV_ALIGN_OUT_RIGHT_MID, 10, 0);
}
/* 音乐界面 */
static void btn_go_playlist_cb(lv_event_t *e);
static void create_music_screen(void)
{
screen_music = lv_obj_create(NULL);
lv_obj_set_style_bg_color(screen_music, lv_color_hex(0x0d1b2a), 0);
lv_obj_set_style_bg_opa(screen_music, LV_OPA_COVER, 0);
/* 标题 */
lv_obj_t *title = lv_label_create(screen_music);
lv_label_set_text(title, UI_MUSIC_TITLE);
lv_obj_set_style_text_font(title, CHINESE_FONT, 0);
lv_obj_set_style_text_color(title, lv_color_hex(0x00d4ff), 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 20);
/* 音乐图标区(大号文字代替图标) */
lv_obj_t *music_icon = lv_label_create(screen_music);
lv_label_set_text(music_icon, LV_SYMBOL_AUDIO);
lv_obj_set_style_text_font(music_icon, &lv_font_montserrat_48, 0);
lv_obj_set_style_text_color(music_icon, lv_color_hex(0x00d4ff), 0);
lv_obj_align(music_icon, LV_ALIGN_CENTER, 0, -110);
/* 当前歌曲名 */
song_label = lv_label_create(screen_music);
lv_label_set_text(song_label, song_names[0]);
lv_obj_set_style_text_font(song_label, CHINESE_FONT, 0);
lv_obj_set_style_text_color(song_label, lv_color_hex(0xffffff), 0);
lv_obj_align(song_label, LV_ALIGN_CENTER, 0, -55);
/* ---------- 歌词显示(3 行:上一行、当前行、下一行) ---------- */
lrc_label_prev = lv_label_create(screen_music);
lv_label_set_text(lrc_label_prev, "");
lv_obj_set_style_text_font(lrc_label_prev, CHINESE_FONT, 0);
lv_obj_set_style_text_color(lrc_label_prev, lv_color_hex(0x666666), 0);
lv_obj_align(lrc_label_prev, LV_ALIGN_CENTER, 0, -25);
lrc_label_curr = lv_label_create(screen_music);
lv_label_set_text(lrc_label_curr, "");
lv_obj_set_style_text_font(lrc_label_curr, CHINESE_FONT, 0);
lv_obj_set_style_text_color(lrc_label_curr, lv_color_hex(0x00d4ff), 0);
lv_obj_set_style_text_letter_space(lrc_label_curr, 2, 0);
lv_obj_align(lrc_label_curr, LV_ALIGN_CENTER, 0, 5);
lrc_label_next = lv_label_create(screen_music);
lv_label_set_text(lrc_label_next, "");
lv_obj_set_style_text_font(lrc_label_next, CHINESE_FONT, 0);
lv_obj_set_style_text_color(lrc_label_next, lv_color_hex(0x666666), 0);
lv_obj_align(lrc_label_next, LV_ALIGN_CENTER, 0, 35);
/* ---------- 控制按钮 ---------- */
/* 上一首 */
lv_obj_t *btn_prev = lv_btn_create(screen_music);
lv_obj_set_size(btn_prev, 80, 50);
lv_obj_align(btn_prev, LV_ALIGN_CENTER, -100, 80);
lv_obj_set_style_bg_color(btn_prev, lv_color_hex(0x1a3a5c), 0);
lv_obj_set_style_radius(btn_prev, 12, 0);
lv_obj_set_style_border_color(btn_prev, lv_color_hex(0x00d4ff), 0);
lv_obj_set_style_border_width(btn_prev, 2, 0);
lv_obj_t *prev_label = lv_label_create(btn_prev);
lv_label_set_text(prev_label, LV_SYMBOL_PREV);
lv_obj_set_style_text_font(prev_label, &lv_font_montserrat_24, 0);
lv_obj_center(prev_label);
lv_obj_add_event_cb(btn_prev, btn_prev_cb, LV_EVENT_CLICKED, NULL);
/* 播放/暂停 */
lv_obj_t *btn_play = lv_btn_create(screen_music);
lv_obj_set_size(btn_play, 80, 50);
lv_obj_align(btn_play, LV_ALIGN_CENTER, 0, 80);
lv_obj_set_style_bg_color(btn_play, lv_color_hex(0x27ae60), 0);
lv_obj_set_style_radius(btn_play, 12, 0);
play_btn_label = lv_label_create(btn_play);
lv_label_set_text(play_btn_label, UI_PLAY);
lv_obj_set_style_text_font(play_btn_label, CHINESE_FONT, 0);
lv_obj_center(play_btn_label);
lv_obj_add_event_cb(btn_play, btn_play_pause_cb, LV_EVENT_CLICKED, NULL);
/* 下一首 */
lv_obj_t *btn_next = lv_btn_create(screen_music);
lv_obj_set_size(btn_next, 80, 50);
lv_obj_align(btn_next, LV_ALIGN_CENTER, 100, 80);
lv_obj_set_style_bg_color(btn_next, lv_color_hex(0x1a3a5c), 0);
lv_obj_set_style_radius(btn_next, 12, 0);
lv_obj_set_style_border_color(btn_next, lv_color_hex(0x00d4ff), 0);
lv_obj_set_style_border_width(btn_next, 2, 0);
lv_obj_t *next_label = lv_label_create(btn_next);
lv_label_set_text(next_label, LV_SYMBOL_NEXT);
lv_obj_set_style_text_font(next_label, &lv_font_montserrat_24, 0);
lv_obj_center(next_label);
lv_obj_add_event_cb(btn_next, btn_next_cb, LV_EVENT_CLICKED, NULL);
/* 返回按钮(音乐界面专用,会停止播放) */
lv_obj_t *btn_back = lv_btn_create(screen_music);
lv_obj_set_size(btn_back, 160, 40);
lv_obj_align(btn_back, LV_ALIGN_TOP_LEFT, 10, 5);
lv_obj_set_style_bg_color(btn_back, lv_color_hex(0x7f8c8d), 0);
lv_obj_set_style_radius(btn_back, 8, 0);
lv_obj_t *back_label = lv_label_create(btn_back);
lv_label_set_text(back_label, UI_BACK);
lv_obj_set_style_text_font(back_label, CHINESE_FONT, 0);
lv_obj_center(back_label);
lv_obj_add_event_cb(btn_back, btn_back_music_cb, LV_EVENT_CLICKED, NULL);
/* 歌单按钮 */
lv_obj_t *btn_playlist = lv_btn_create(screen_music);
lv_obj_set_size(btn_playlist, 120, 40);
lv_obj_align(btn_playlist, LV_ALIGN_TOP_RIGHT, -10, 5);
lv_obj_set_style_bg_color(btn_playlist, lv_color_hex(0x1a3a5c), 0);
lv_obj_set_style_radius(btn_playlist, 8, 0);
lv_obj_set_style_border_color(btn_playlist, lv_color_hex(0x00d4ff), 0);
lv_obj_set_style_border_width(btn_playlist, 1, 0);
lv_obj_t *playlist_lbl = lv_label_create(btn_playlist);
lv_label_set_text(playlist_lbl, "歌单");
lv_obj_set_style_text_font(playlist_lbl, CHINESE_FONT, 0);
lv_obj_center(playlist_lbl);
lv_obj_add_event_cb(btn_playlist, btn_go_playlist_cb, LV_EVENT_CLICKED, NULL);
}
/* 歌单界面 */
static void btn_go_playlist_cb(lv_event_t *e)
{
(void)e;
lv_screen_load(screen_playlist);
}
static void btn_back_playlist_cb(lv_event_t *e)
{
(void)e;
lv_screen_load(screen_music);
}
/* 点击歌曲卡片回调 */
static void btn_song_click_cb(lv_event_t *e)
{
int idx = (int)(intptr_t)lv_event_get_user_data(e);
music_play(idx);
lv_label_set_text(song_label, song_names[current_song]);
lv_label_set_text(play_btn_label, UI_PAUSE);
lv_screen_load(screen_music);
}
static void create_playlist_screen(void)
{
screen_playlist = lv_obj_create(NULL);
lv_obj_set_style_bg_color(screen_playlist, lv_color_hex(0x0d1b2a), 0);
lv_obj_set_style_bg_opa(screen_playlist, LV_OPA_COVER, 0);
/* 标题 */
lv_obj_t *title = lv_label_create(screen_playlist);
lv_label_set_text(title, "歌单");
lv_obj_set_style_text_font(title, CHINESE_FONT, 0);
lv_obj_set_style_text_color(title, lv_color_hex(0x00d4ff), 0);
lv_obj_set_style_text_letter_space(title, 6, 0);
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 20);
/* 歌曲信息 */
typedef struct { const char *name; const char *artist; } SongInfo;
static const SongInfo songs[] = {
{"晴天", "周杰伦"},
{"第十幕", "迪丽热巴"},
{"恋人", "李荣浩"},
};
int card_y = 70;
for (int i = 0; i < 3; i++) {
/* 歌曲卡片(可点击) */
lv_obj_t *card = lv_btn_create(screen_playlist);
lv_obj_set_size(card, 420, 80);
lv_obj_align(card, LV_ALIGN_TOP_MID, 0, card_y);
lv_obj_set_style_bg_color(card, lv_color_hex(0x1a3a5c), 0);
lv_obj_set_style_radius(card, 12, 0);
lv_obj_set_style_border_color(card, lv_color_hex(0x00d4ff), 0);
lv_obj_set_style_border_width(card, 1, 0);
lv_obj_add_event_cb(card, btn_song_click_cb, LV_EVENT_CLICKED, (void *)(intptr_t)i);
/* 序号 */
lv_obj_t *num = lv_label_create(card);
char num_buf[8];
snprintf(num_buf, sizeof(num_buf), "%d.", i + 1);
lv_label_set_text(num, num_buf);
lv_obj_set_style_text_font(num, &lv_font_montserrat_28, 0);
lv_obj_set_style_text_color(num, lv_color_hex(0x00d4ff), 0);
lv_obj_align(num, LV_ALIGN_LEFT_MID, 10, 0);
/* 歌名 */
lv_obj_t *name = lv_label_create(card);
lv_label_set_text(name, songs[i].name);
lv_obj_set_style_text_font(name, CHINESE_FONT, 0);
lv_obj_set_style_text_color(name, lv_color_hex(0xffffff), 0);
lv_obj_align(name, LV_ALIGN_LEFT_MID, 55, -15);
/* 歌手 */
lv_obj_t *artist = lv_label_create(card);
lv_label_set_text(artist, songs[i].artist);
lv_obj_set_style_text_font(artist, CHINESE_FONT, 0);
lv_obj_set_style_text_color(artist, lv_color_hex(0x7f8c8d), 0);
lv_obj_align(artist, LV_ALIGN_LEFT_MID, 55, 15);
card_y += 95;
}
/* 返回按钮(回到音乐播放界面) */
lv_obj_t *btn_back = lv_btn_create(screen_playlist);
lv_obj_set_size(btn_back, 160, 40);
lv_obj_align(btn_back, LV_ALIGN_TOP_LEFT, 10, 5);
lv_obj_set_style_bg_color(btn_back, lv_color_hex(0x7f8c8d), 0);
lv_obj_set_style_radius(btn_back, 8, 0);
lv_obj_t *back_label = lv_label_create(btn_back);
lv_label_set_text(back_label, UI_BACK);
lv_obj_set_style_text_font(back_label, CHINESE_FONT, 0);
lv_obj_center(back_label);
lv_obj_add_event_cb(btn_back, btn_back_playlist_cb, LV_EVENT_CLICKED, NULL);
}
/* 主函数 */
int main(void)
{
lv_init();
lv_display_t *disp = lv_linux_fbdev_create();
lv_linux_fbdev_set_file(disp, "/dev/fb0");
lv_indev_t *indev = lv_evdev_create(LV_INDEV_TYPE_POINTER, "/dev/input/event0");
/* 创建所有界面 */
create_main_menu();
create_speed_screen();
create_sensor_screen();
create_music_screen();
create_playlist_screen();
/* 显示主菜单 */
lv_screen_load(screen_main);
/* 启动传感器采集线程 */
pthread_mutex_init(&g_sensor_data.mutex, NULL);
pthread_t sensor_tid;
pthread_create(&sensor_tid, NULL, sensor_thread_func, NULL);
/* 注册 UI 更新定时器 */
lv_timer_create(ui_update_timer, 200, NULL);
lv_timer_create(lyrics_timer_cb, 200, NULL);
while (1) {
lv_timer_handler();
usleep(5000);
}
music_stop();
pthread_mutex_destroy(&g_sensor_data.mutex);
return 0;
}
六、项目展示
车载系统