欢迎光临
我们一直在努力

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

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


在这里插入图片描述

一、项目需求与架构

1.1 功能需求

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

    在这里插入图片描述

    模块划分:

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

    二、关键技术点

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

    三、完整项目代码

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

    /**
    * 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 交叉编译

    arm-linux-gnueabihf-gcc -Wall -g -o smart_gateway smart_gateway.c -lpthread -lsqlite3

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

    4.2 在开发板上运行

    # 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应用开发的典型流程。你可以将其作为自己项目的起点,逐步丰富功能。

    思考题:

  • 环形缓冲区有无锁实现的可能?在什么条件下可以不用互斥锁?
  • 如果串口数据频率非常高(如1000帧/秒),当前架构会有瓶颈吗?如何优化?
  • 如何修改代码让网络连接支持长连接,实时推送所有更新而非仅最新一条?
  • 欢迎在评论区留下你的思路。本系列应用层教程至此收官,后续可按需扩展驱动、内核模块或项目专题。


    参考资料

    • SQLite官方文档:https://www.sqlite.org/
    • man termios, man pthread_cond_wait
    • 《嵌入式Linux应用开发完全手册》(配套代码可参考)
    赞(0)
    未经允许不得转载:171主机测评 » 嵌入式Linux应用开发系列⑨:综合项目——ARM智能家居网关(多线程+串口+网络+数据库)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址