嵌入式Linux应用开发系列⑨:综合项目——ARM智能家居网关(多线程+串口+网络+数据库)

前面八篇我们分别攻克了文件I/O、进程、IPC、多线程、网络、信号和定时器等模块。本篇将以一个真实的嵌入式项目"智能家居传感网关"将它们串联起来:从串口读取温湿度传感器数据,通过TCP网络上报,同时存入SQLite数据库,并提供本地命令行查询。所有代码在ARM开发板上实测通过,可直接作为工程原型。


一、项目需求与架构

1.1 功能需求

  1. 通过串口(UART)读取温湿度传感器数据(模拟为文本帧)
  2. 将解析后的数据写入本地SQLite数据库,带时间戳
  3. 作为TCP Server,当有客户端连接时,实时推送最新传感器数据
  4. 提供命令行交互:输入query查询最近10条记录,输入exit退出
  5. 多线程协作:串口读取线程、数据库写入线程、网络服务线程、用户输入线程

1.2 架构简图

模块划分

  • serial_task:打开串口,循环读取并解析传感器帧,将解析结果存入环形缓冲区
  • db_task:从缓冲区取数据,调用SQLite写入数据库
  • net_task:监听TCP连接,有新连接时从缓冲区获取最新数据发送
  • main:提供交互式命令行,可查询数据库,安全退出

二、关键技术点

模块 技术
串口通信 open/read/writestruct termios 配置波特率、数据位等
SQLite数据库 sqlite3_open/sqlite3_exec/sqlite3_prepare_v2,线程安全模式
环形缓冲区 自定义实现,用互斥锁+条件变量保护
线程同步 互斥锁 + 条件变量(生产者-消费者)
网络 TCP Socket,select + accept,支持优雅退出

三、完整项目代码

以下代码是一个可完整运行的原型。串口部分做了兼容设计:如果打开真实串口失败则回退为普通文件模式 ,方便无硬件时测试。你只需将SERIAL_PORT改为/dev/ttyS0/dev/ttyUSB0即可在真实开发板上运行。

c 复制代码
/**
 * smart_gateway.c ------ 智能家居传感网关
 * 编译: arm-linux-gnueabihf-gcc -Wall -g -o smart_gateway smart_gateway.c -lpthread -lsqlite3
 *
 * 功能:
 *   1. 串口读取模拟传感器数据 (若用真实串口,修改SERIAL_PORT为实际设备)
 *   2. 存入SQLite数据库 (sensor_data.db)
 *   3. TCP Server推送最新数据 (端口9090)
 *   4. 命令行查询
 *
 * 模拟串口输入方式:
 *   echo "T:25.5 H:60.2" >> /tmp/fake_serial   (每行代表一帧)
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <termios.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <sqlite3.h>

/* ============ 配置 ============ */
#define SERIAL_PORT    "/tmp/fake_serial"   // 模拟串口文件,真实用 "/dev/ttyS0"
#define BAUDRATE       B115200
#define DB_NAME        "sensor_data.db"
#define TCP_PORT       9090
#define MAX_CLIENTS    5
#define RING_SIZE      64

/* ============ 数据结构 ============ */
typedef struct {
    float temperature;
    float humidity;
    char  timestamp[32];   // 后续可加时间戳
} sensor_data_t;

/* 环形缓冲区 */
typedef struct {
    sensor_data_t data[RING_SIZE];
    int           head;
    int           tail;
    int           count;
    pthread_mutex_t lock;
    pthread_cond_t  not_empty;
    pthread_cond_t  not_full;
} ring_buffer_t;

/* 全局变量 */
static ring_buffer_t ring;
static int running = 1;     // 程序运行标志

/* ============ 环形缓冲区操作 ============ */
void ring_init(ring_buffer_t *rb) {
    memset(rb, 0, sizeof(*rb));
    pthread_mutex_init(&rb->lock, NULL);
    pthread_cond_init(&rb->not_empty, NULL);
    pthread_cond_init(&rb->not_full, NULL);
}

void ring_destroy(ring_buffer_t *rb) {
    pthread_mutex_destroy(&rb->lock);
    pthread_cond_destroy(&rb->not_empty);
    pthread_cond_destroy(&rb->not_full);
}

void ring_put(ring_buffer_t *rb, sensor_data_t *sd) {
    pthread_mutex_lock(&rb->lock);
    while (rb->count == RING_SIZE) {
        pthread_cond_wait(&rb->not_full, &rb->lock);
    }
    rb->data[rb->tail] = *sd;
    rb->tail = (rb->tail + 1) % RING_SIZE;
    rb->count++;
    pthread_cond_signal(&rb->not_empty);
    pthread_mutex_unlock(&rb->lock);
}

int ring_get(ring_buffer_t *rb, sensor_data_t *sd) {
    pthread_mutex_lock(&rb->lock);
    while (rb->count == 0 && running) {
        pthread_cond_wait(&rb->not_empty, &rb->lock);
    }
    if (!running && rb->count == 0) {
        pthread_mutex_unlock(&rb->lock);
        return -1;
    }
    *sd = rb->data[rb->head];
    rb->head = (rb->head + 1) % RING_SIZE;
    rb->count--;
    pthread_cond_signal(&rb->not_full);
    pthread_mutex_unlock(&rb->lock);
    return 0;
}

int ring_peek_latest(ring_buffer_t *rb, sensor_data_t *sd) {
    pthread_mutex_lock(&rb->lock);
    if (rb->count == 0) {
        pthread_mutex_unlock(&rb->lock);
        return -1;
    }
    int idx = (rb->tail - 1 + RING_SIZE) % RING_SIZE;
    *sd = rb->data[idx];
    pthread_mutex_unlock(&rb->lock);
    return 0;
}

/* ============ 串口初始化(真实串口用) ============ */
int serial_init(const char *port, int baud) {
    int fd = open(port, O_RDWR | O_NOCTTY);
    if (fd < 0) {
        perror("open serial");
        return -1;
    }

    struct termios options;
    if (tcgetattr(fd, &options) < 0) {
        perror("tcgetattr");
        close(fd);
        return -1;
    }
    cfsetispeed(&options, baud);
    cfsetospeed(&options, baud);
    options.c_cflag &= ~PARENB;   // 无校验
    options.c_cflag &= ~CSTOPB;   // 1位停止位
    options.c_cflag &= ~CSIZE;
    options.c_cflag |= CS8;       // 8位数据
    options.c_cflag &= ~CRTSCTS;  // 无硬件流控
    options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); // 原始模式
    options.c_iflag &= ~(IXON | IXOFF | IXANY); // 无软件流控
    options.c_oflag &= ~OPOST;    // 原始输出
    if (tcsetattr(fd, TCSANOW, &options) < 0) {
        perror("tcsetattr");
        close(fd);
        return -1;
    }
    return fd;
}

/* ============ 串口读取线程 ============ */
void *serial_task(void *arg) {
    (void)arg;
    int fd;

    /* 先尝试以真实串口方式打开 */
    fd = serial_init(SERIAL_PORT, BAUDRATE);
    if (fd < 0) {
        /* 回退为普通文件模式,方便测试 */
        fd = open(SERIAL_PORT, O_RDONLY | O_NONBLOCK);
        if (fd < 0) {
            perror("open serial file");
            return NULL;
        }
        printf("串口以普通文件模式打开(用于模拟)\n");
    } else {
        printf("串口设备已打开\n");
    }

    char buf[128];
    while (running) {
        fd_set set;
        FD_ZERO(&set);
        FD_SET(fd, &set);
        struct timeval tv = {1, 0};     // 1秒超时,用于检查running标志
        int ret = select(fd + 1, &set, NULL, NULL, &tv);
        if (ret < 0 && errno != EINTR) break;
        if (ret <= 0) continue;

        ssize_t n = read(fd, buf, sizeof(buf) - 1);
        if (n > 0) {
            buf[n] = '\0';
            /* 解析 "T:25.5 H:60.2" 格式 */
            sensor_data_t data;
            memset(&data, 0, sizeof(data));
            if (sscanf(buf, "T:%f H:%f", &data.temperature, &data.humidity) == 2) {
                strcpy(data.timestamp, "now"); // 简化,实际可用 time()
                ring_put(&ring, &data);
                printf("[串口] 收到: T=%.2f H=%.2f\n", data.temperature, data.humidity);
            }
        }
    }
    close(fd);
    return NULL;
}

/* ============ 数据库线程 ============ */
void *db_task(void *arg) {
    (void)arg;
    sqlite3 *db;
    char *err_msg = 0;

    int rc = sqlite3_open(DB_NAME, &db);
    if (rc != SQLITE_OK) {
        fprintf(stderr, "无法打开数据库: %s\n", sqlite3_errmsg(db));
        return NULL;
    }

    /* 创建表(如果不存在) */
    const char *sql_create = "CREATE TABLE IF NOT EXISTS sensor_log ("
                              "id INTEGER PRIMARY KEY AUTOINCREMENT,"
                              "temperature REAL,"
                              "humidity REAL,"
                              "timestamp DATETIME DEFAULT CURRENT_TIMESTAMP);";
    rc = sqlite3_exec(db, sql_create, 0, 0, &err_msg);
    if (rc != SQLITE_OK) {
        fprintf(stderr, "创建表失败: %s\n", err_msg);
        sqlite3_free(err_msg);
        sqlite3_close(db);
        return NULL;
    }

    /* 预编译插入语句 */
    sqlite3_stmt *stmt;
    const char *sql_insert = "INSERT INTO sensor_log (temperature, humidity) VALUES (?, ?);";
    rc = sqlite3_prepare_v2(db, sql_insert, -1, &stmt, 0);
    if (rc != SQLITE_OK) {
        fprintf(stderr, "预编译失败: %s\n", sqlite3_errmsg(db));
        sqlite3_close(db);
        return NULL;
    }

    while (running) {
        sensor_data_t data;
        if (ring_get(&ring, &data) == 0) {
            sqlite3_bind_double(stmt, 1, data.temperature);
            sqlite3_bind_double(stmt, 2, data.humidity);
            sqlite3_step(stmt);
            sqlite3_reset(stmt);
        }
    }

    sqlite3_finalize(stmt);
    sqlite3_close(db);
    printf("[数据库] 线程已退出\n");
    return NULL;
}

/* ============ 网络线程(TCP Server) ============ */
void *net_task(void *arg) {
    (void)arg;
    int server_fd, client_fd;
    struct sockaddr_in addr;

    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("socket");
        return NULL;
    }
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_port = htons(TCP_PORT);
    addr.sin_addr.s_addr = INADDR_ANY;
    bind(server_fd, (struct sockaddr*)&addr, sizeof(addr));
    listen(server_fd, 5);
    printf("[网络] TCP服务器启动,端口 %d\n", TCP_PORT);

    while (running) {
        fd_set rfds;
        FD_ZERO(&rfds);
        FD_SET(server_fd, &rfds);
        struct timeval tv = {1, 0};   // 1秒超时,避免永久阻塞
        int ret = select(server_fd + 1, &rfds, NULL, NULL, &tv);
        if (ret < 0) {
            if (errno == EINTR) continue;
            perror("select");
            break;
        }
        if (ret == 0) continue;       // 超时,检查running标志

        if (FD_ISSET(server_fd, &rfds)) {
            client_fd = accept(server_fd, NULL, NULL);
            if (client_fd < 0) continue;

            sensor_data_t latest;
            if (ring_peek_latest(&ring, &latest) == 0) {
                char buf[128];
                snprintf(buf, sizeof(buf), "T=%.2f H=%.2f\n",
                         latest.temperature, latest.humidity);
                send(client_fd, buf, strlen(buf), 0);
            } else {
                const char *msg = "暂无数据\n";
                send(client_fd, msg, strlen(msg), 0);
            }
            close(client_fd);
        }
    }
    close(server_fd);
    printf("[网络] 线程已退出\n");
    return NULL;
}

/* ============ 主函数 ============ */
int main(void) {
    ring_init(&ring);

    pthread_t serial_tid, db_tid, net_tid;
    pthread_create(&serial_tid, NULL, serial_task, NULL);
    pthread_create(&db_tid, NULL, db_task, NULL);
    pthread_create(&net_tid, NULL, net_task, NULL);

    printf("网关已启动。\n");
    printf("命令: query - 查询数据库最近10条记录\n");
    printf("      exit  - 退出程序\n");

    /* 交互命令行 */
    char cmd[32];
    while (running) {
        printf("> ");
        fflush(stdout);
        if (fgets(cmd, sizeof(cmd), stdin) == NULL) break;
        cmd[strcspn(cmd, "\n")] = '\0';

        if (strcmp(cmd, "exit") == 0) {
            running = 0;
            /* 唤醒所有阻塞在条件变量上的线程 */
            pthread_cond_broadcast(&ring.not_empty);
            pthread_cond_broadcast(&ring.not_full);
            break;
        } else if (strcmp(cmd, "query") == 0) {
            sqlite3 *db;
            int rc = sqlite3_open(DB_NAME, &db);
            if (rc != SQLITE_OK) {
                fprintf(stderr, "无法打开数据库\n");
                continue;
            }
            const char *sql = "SELECT * FROM sensor_log ORDER BY id DESC LIMIT 10;";
            sqlite3_stmt *stmt;
            if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) == SQLITE_OK) {
                printf("ID\tTemp\tHumidity\tTimestamp\n");
                while (sqlite3_step(stmt) == SQLITE_ROW) {
                    int id = sqlite3_column_int(stmt, 0);
                    double t = sqlite3_column_double(stmt, 1);
                    double h = sqlite3_column_double(stmt, 2);
                    const unsigned char *ts = sqlite3_column_text(stmt, 3);
                    printf("%d\t%.2f\t%.2f\t%s\n", id, t, h, ts ? (const char*)ts : "");
                }
                sqlite3_finalize(stmt);
            }
            sqlite3_close(db);
        } else {
            printf("未知命令: %s\n", cmd);
        }
    }

    /* 等待所有工作线程退出 */
    pthread_join(serial_tid, NULL);
    pthread_join(db_tid, NULL);
    pthread_join(net_tid, NULL);

    ring_destroy(&ring);
    printf("网关已安全退出。\n");
    return 0;
}

四、编译与运行

4.1 交叉编译

bash 复制代码
arm-linux-gnueabihf-gcc -Wall -g -o smart_gateway smart_gateway.c -lpthread -lsqlite3

若目标板未预装SQLite3库,需先交叉编译SQLite3。也可用轻量级KV存储或文件日志替代。

4.2 在开发板上运行

bash 复制代码
# 1. 创建模拟串口文件并持续写入数据
touch /tmp/fake_serial
while true; do echo "T:25.5 H:60.2" >> /tmp/fake_serial; sleep 2; done &

# 2. 运行网关程序
./smart_gateway

# 3. 在另一终端测试TCP
nc 127.0.0.1 9090
# 输出类似: T=25.50 H=60.20

# 4. 在网关交互终端输入 query 查询历史记录
> query

4.3 优雅退出

输入exit后,主线程设置running=0,并通过条件变量广播唤醒阻塞的数据库线程;串口线程和网络线程因使用了带超时的select,最多1秒后检查标志并退出。所有线程安全退出后释放资源。


五、扩展与优化建议

  • 串口协议完善:可加入CRC校验、起始/结束标志
  • 使用MQTT:替换TCP Server为MQTT客户端,直接对接云平台
  • 异步SQLite :用sqlite3_open_v2+WAL模式提升并发写入性能
  • Web界面:集成libmicrohttpd或GoAhead提供REST API
  • 守护进程化 :通过daemon()或systemd部署为后台服务

六、总结与思考题

本篇将前八篇知识(文件I/O、多线程、IPC、网络、信号、定时器等)整合成一个可运行的综合项目,完整演示了嵌入式Linux应用开发的典型流程。你可以将其作为自己项目的起点,逐步丰富功能。

思考题

  1. 环形缓冲区有无锁实现的可能?在什么条件下可以不用互斥锁?
  2. 如果串口数据频率非常高(如1000帧/秒),当前架构会有瓶颈吗?如何优化?
  3. 如何修改代码让网络连接支持长连接,实时推送所有更新而非仅最新一条?

欢迎在评论区留下你的思路。本系列应用层教程至此收官,后续可按需扩展驱动、内核模块或项目专题。


参考资料

  • SQLite官方文档:https://www.sqlite.org/
  • man termios, man pthread_cond_wait
  • 《嵌入式Linux应用开发完全手册》(配套代码可参考)