案例分享:基于 TinyPiXOS 实现温湿度实时监控系统

一、项目概述与平台介绍

本项目是一个基于TinyPiXOS开发的温湿度实时监控系统,旨在通过传感器采集环境温湿度数据,经处理后以可视化方式实时展示,并提供超阈值警告功能。系统支持数据的实时更新、曲线绘制、设备状态监控等核心功能,适用于需要实时掌握环境温湿度变化的场景(如实验室、温室、仓储等)。

TinyPiXOS 是国产自主研发的轻量级移动嵌入式设备桌面操作系统,TinyPiXOS 旨在提供一个独立可控、架构轻量且高度定制化的嵌入式桌面操作系统开发平台。

  • 官网:https://www.tinypixos.com/
  • 开源代码仓库:https://gitee.com/organizations/tinypixos/projects

二、技术架构

系统采用模块化设计,主要分为传感器数据采集层数据处理层UI 展示层三大模块,各模块职责清晰、低耦合,确保系统稳定高效运行。

1. 传感器数据采集层

功能:通过串口与外部温湿度传感器通信,实时读取原始数据并解析。

核心类

  • SerialPort:封装串口操作(打开、关闭、读取数据),管理串口文件描述符和数据缓冲区。
  • SensorReader:启动独立线程(readerThread)执行readLoop循环,通过SerialPort读取串口数据,调用processSensorData解析数据并传递给数据处理层。

2. 数据处理层

功能:负责数据的存储、缓存、查询及持久化(文件读写),保证线程安全。

核心类

  • DataProcessor:维护dataBuffer作为数据缓冲区,通过互斥锁(dataMutex)保证多线程下的数据安全。提供addData(添加数据)、getLatestData(获取最新 n 条数据)、saveToFile/loadFromFile(数据持久化)等接口。

  • SensorData:数据结构,包含温度(temperature)、湿度(humidity)、时间戳(timestamp)和有效性标志(isValid)。

3. UI 展示层

功能:通过图形界面展示温湿度数据,包括实时数值、曲线趋势、温湿度计控件及警告信息。

核心组件

  • MainWindowService:主窗口类,负责初始化 UI 组件(曲线绘图区、温湿度计、标签)、启动定时更新线程,协调数据展示逻辑。
  • PlotWidget:绘图组件,绘制温湿度曲线及坐标轴刻度,通过独立线程(updateThread)每秒更新数据点并刷新曲线。
  • TpTemperatureWidget/TpHumidityWidget:温湿度计控件,通过onPaintEvent绘制自定义图形(温度计/水滴形状),支持颜色渐变填充和数值显示。
  • 标签控件(Label_1/Label_2):显示实时温湿度数值及超阈值警告(温度 ≥30℃ 或 ≤5℃ 时触发)。

三、核心功能说明

1. 数据采集与解析

  • 传感器通过串口(默认/dev/ttyACM0,波特率 9600)发送数据,SensorReaderreadLoop循环调用SerialPort::readLine读取数据,解析后通过DataProcessor::addData存入缓冲区。
  • 支持数据有效性校验(SensorData::isValid),确保无效数据不参与展示。

2. 数据处理与持久化

  • 数据缓冲区(dataBuffer)存储历史数据,支持获取最新 n 条数据(getLatestData)供 UI 展示曲线。

3. 可视化展示

  • 曲线绘图PlotWidget通过tempToY/humiToY将温湿度值映射为 Y 坐标,绘制折线图(温度用红线,湿度用蓝线),并实时更新坐标轴刻度标签(时间轴以秒为单位)。
  • 温湿度计控件
    • 温度计(TpTemperatureWidget):绘制柱状图形,根据温度值通过颜色渐变(蓝 → 绿 → 红)直观展示温度高低。
    • 湿度计(TpHumidityWidget):绘制水滴形状,通过填充高度反映湿度百分比,支持蓝色渐变填充。
  • 实时数值与警告Label_1显示当前温湿度,Label_2在温度超阈值时显示警告(高温红色、低温蓝色)。

四、关键代码分析

1. 打开串口

bool SerialPort::open(const std::string& port, int baudrate) {
    // 打开串口(使用类内fd)
    fd = ::open(port.c_str(), O_RDWR | O_NOCTTY | O_SYNC);
    if (fd < 0) {
        std::cout << "无法打开串口: " << port << std::endl;
        return false;
    }
    // 配置串口(波特率、数据格式等)
    struct termios tty;
    memset(&tty, 0, sizeof(tty));
    if (tcgetattr(fd, &tty) != 0) {
        std::cout << "获取串口属性失败" << std::endl;
        return false;
    }
    // 设置波特率(示例为9600)
    cfsetospeed(&tty, B9600);
    cfsetispeed(&tty, B9600);
    // 8位数据位,无奇偶校验,1位停止位
    tty.c_cflag = (tty.c_cflag & ~CSIZE) | CS8;
    tty.c_cflag &= ~PARENB;
    tty.c_cflag &= ~CSTOPB;
    tty.c_cflag &= ~CRTSCTS;
    tty.c_cflag |= (CLOCAL | CREAD);
    // 禁用规范模式和特殊处理
    tty.c_lflag &= ~ICANON;
    tty.c_lflag &= ~(ECHO | ECHOE | ISIG);
    tty.c_iflag &= ~(IXON | IXOFF | IXANY | INLCR | ICRNL);
    tty.c_oflag &= ~OPOST;
    // 超时设置
    tty.c_cc[VMIN] = 0;
    tty.c_cc[VTIME] = 5;
    if (tcsetattr(fd, TCSANOW, &tty) != 0) {
        std::cout << "设置串口属性失败" << std::endl;
        return false;
    }
    std::cout << "串口打开成功: " << port << std::endl;
    return true;
}
  • 打开指定串口,配置波特率 9600、8 位数据位、无奇偶校验、1 位停止位等串口属性并设置超时,成功则输出提示并返回 true,失败则输出对应错误信息并返回 false。

2. 传感器数据读取

std::string SerialPort::readLine() {
    char buf[256];
    int bytesRead = ::read(fd, buf, sizeof(buf) - 1); //fd串口文件描述符
    if (bytesRead > 0) {
        buf[bytesRead] = '\0';
        buffer += buf;  // 临时缓冲区
        size_t newlinePos = buffer.find('\n');  // 查找换行符
        if (newlinePos != std::string::npos) {  // 找到完整行
            std::string line = buffer.substr(0, newlinePos);
            if (!line.empty() && line.back() == '\r') {
                line.pop_back();
            }
            buffer = buffer.substr(newlinePos + 1);
            return line;
        }
    }
    return "";
}
  • 从串口文件描述符 fd 读取数据到 256 字节缓冲区,拼接至类内 buffer,查找换行符以提取完整行并去除末尾可能的回车符,更新 buffer 保留未提取数据,成功提取则返回完整行,否则返回空字符串

3. 处理读取到的传感器数据

void SensorReader::processSensorData(const std::string& data) {
    size_t t_pos = data.find("T:");  // 查找温度标识符
    size_t h_pos = data.find("H:");  // 查找湿度标识符

    if (t_pos != std::string::npos && h_pos != std::string::npos) // 确保数据格式正确
    {
        try {
            size_t t_end = data.find(',', t_pos);
            if (t_end != std::string::npos) {
                float temperature = std::stof(data.substr(t_pos + 2, t_end - t_pos - 2));   // 从温度标识符开始到逗号位置
                float humidity = std::stof(data.substr(h_pos + 2));     // 从湿度标识符开始到字符串末尾
                SensorData newData;                     // 创建新的传感器数据对象
                newData.temperature = temperature;      // 设置温度
                newData.humidity = humidity;            // 设置湿度
                newData.timestamp = std::time(nullptr); // 设置当前时间戳
                newData.isValid = true;                 // 设置数据有效性
                // 线程安全更新数据
                {
                    std::lock_guard<std::mutex> lock(dataMutex);
                    latestData = newData;
                    newDataAvailable = true;
                }
                dataProcessor.addData(newData); // 假设dataProcessor全局可用

                std::cout << "解析成功 - 温度: " << temperature << "°C, 湿度: " << humidity << "%" << std::endl;
            }
        } catch (const std::exception& e) {
            std::cout << "解析错误: " << e.what() << " 数据: " << data << std::endl;
        }
    } else {
        std::cout << "数据格式错误: " << data << std::endl;
    }
}
  • 该函数接收字符串格式的传感器数据,查找并验证 “T:” 和 “H:” 标识符以确保格式正确,尝试解析出温度和湿度值后创建含时间戳且标记为有效的 SensorData 对象,通过互斥锁线程安全更新全局数据并添加至 dataProcessor,同时对格式错误或解析异常情况输出对应提示信息。

4. 添加数据到缓冲区

void DataProcessor::addData(const SensorData& data) {
    std::lock_guard<std::mutex> lock(dataMutex);  // 类内互斥锁
    dataBuffer.push_back(data);  // 类内数据缓冲区
}
  • 通过类内互斥锁确保线程安全,将传入的 SensorData 对象添加到类内的数据缓冲区 dataBuffer 中。

5. 获取最新 n 条数据

std::vector<SensorData> DataProcessor::getLatestData(size_t n) {
    std::lock_guard<std::mutex> lock(dataMutex);
    if (n >= dataBuffer.size()) {
        return dataBuffer;  // 不足n条则返回全部
    }
    // 截取最后n条数据
    return std::vector<SensorData>(dataBuffer.end() - n, dataBuffer.end());
}
  • 通过类内互斥锁确保线程安全,接收参数 n 并返回数据缓冲区 dataBuffer 中最后 n 条 SensorData 数据(若 n 不小于缓冲区大小则返回全部)。

6. 温度计的绘制

bool TpTemperatureWidget::onPaintEvent(TpPaintEvent *event)
{
    // 重置渐变
    painter->setBrush(TpBrush(Tp::NoBrush));

    // 绘制刻度线 温度范围:5℃(底部)~40℃(顶部),总温差35℃
    const int32_t minTemp = 5;
    const int32_t maxTemp = 40;
    const int32_t tickStep = 5;
    const int32_t totalTempRange = maxTemp - minTemp;
    const int32_t effectiveHeight = rectangleHeight;
    const float pixelPerDegree = static_cast<float>(effectiveHeight) / totalTempRange;
    // 1. 创建并配置刻度文本字体(不通过painter->font(),而是显式创建TpFont)
    TpFont tickFont;  // 独立的字体对象
    tickFont.setFontSize(12);  // 设置字体大小
    tickFont.setAntialias(TpFont::TINY_FONT_ANTIALIAS_GOOD);  // 可选:抗锯齿

    tickFont.setText("10°");
    int32_t textHeight = tickFont.pixelHeight();  // 获取文本总高度
    int32_t baselineOffset = 2;  // 字体基线偏移(根据实际字体微调,确保居中)

    painter->setPen(_RGB(255, 255, 255));  // 刻度线颜色

    // 内侧缩进距离(从圆柱体左侧边缘向内2px,确保在圆柱体内)
    const int32_t innerOffset = 2;
    // 刻度线长度(向圆柱体内侧延伸,如6px,不超出圆柱体)
    const int32_t tickLength = 6;

    for (int32_t temp = minTemp; temp <= maxTemp; temp += tickStep) {
        // 计算当前温度的Y坐标(5℃在底部,40℃在顶部)
        int32_t tempOffset = maxTemp - temp;
        int32_t posY = rectangleY + static_cast<int32_t>(tempOffset * pixelPerDegree);
        if (posY < rectangleY) posY = rectangleY;
        if (posY > circleCenterY) posY = circleCenterY;

        // 1. 处理5℃和40℃:仅显示数字,不显示刻度线
        if (temp == minTemp || temp == maxTemp) {
            TpString tempText = TpString::number(temp) + "°";
            tickFont.setText(tempText);
            int32_t textWidth = tickFont.pixelWidth();

            // 文本位置与中间刻度对齐(水平左移,垂直居中)
            int32_t textX = (rectangleX + innerOffset) - textWidth - 2;  // 与中间文本X对齐
            int32_t textY = posY - (textHeight / 2) + baselineOffset;    // 与刻度线位置居中对齐

            painter->drawText(tickFont, textX, textY, tempText);  // 仅绘制文本
            continue;  // 跳过刻度线绘制
        }

        // 2. 处理中间刻度(10℃~35℃):显示刻度线和数字(保持不变)
        // 绘制刻度线
        int32_t tickStartX = rectangleX + innerOffset;
        int32_t tickEndX = tickStartX + tickLength;
        painter->drawLine(tickStartX, posY, tickEndX, posY);

        // 绘制数字(与刻度线对齐)
        TpString tempText = TpString::number(temp) + "°";
        tickFont.setText(tempText);
        int32_t textWidth = tickFont.pixelWidth();
        int32_t textX = tickStartX - textWidth - 2;
        int32_t textY = posY - (textHeight / 2) + baselineOffset;
        painter->drawText(tickFont, textX, textY, tempText);
    }
    // -------------------------- 当前温度值显示 --------------------------
    TpFont curTempFont;  // 当前温度文本的字体
    curTempFont.setFontSize(14);  // 可适当大于刻度字体
    TpString curTempText = TpString::number(static_cast<int32_t>(tempData->curTemperature)) + "°";
    curTempFont.setText(curTempText);
    int32_t curTextX = rectangleX + rectangleWidth + 5;
    int32_t curTextY = circleCenterY - (circleCenterY - rectangleY) / 2;
    painter->drawText(curTempFont, curTextX, curTextY, curTempText);  // 正确传参

    // 绘制标题文本
    tempData->minMaxTemptFont.setText(tempData->titleText);
    int32_t titleTextWidth = tempData->minMaxTemptFont.pixelWidth();
    int32_t titleTextHeight = tempData->minMaxTemptFont.pixelHeight();
    int32_t titleTextX = (width() - titleTextWidth) / 2.0;
    int32_t titleTextY = height() - titleTextHeight;
    painter->drawText(tempData->minMaxTemptFont, titleTextX, titleTextY);

    return true;
}
  • 基于 TinyPiXOS 原有的 TpTemperatureWidget,在上面做了修改,更改了刻度和填充;上面只显示了更改的部分,从重绘渐变下面开始更改。
  • 重置画笔刷后,绘制 5℃-40℃、步长 5℃ 的温度刻度(首尾仅显示温度文本,中间刻度同时显示刻度线和文本),配置对应字体与坐标,显示当前温度值及控件标题文本,最终返回 true。

7. 计时器

void MainWindowService::timerThreadFunc()
{
    while (isRunning_) {  // 循环直到运行标志为false
        // 休眠1秒(1000毫秒)
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        // 调用数据更新函数
        updateSensorData();
    }
}
  • 在 isRunning为 true 时持续循环,每休眠 1 秒调用一次 updateSensorData 函数更新传感器数据,直至 isRunning为 false 时停止,从而实现实时更新。

9. 数据更新

void MainWindowService::updateSensorData()
{
    auto latestData = dataProcessor.getLatestData(1);
    if (!latestData.empty()) {
        const auto& data = latestData[0];
        if (data.isValid) {
            std::stringstream ss;
            ss << std::fixed << std::setprecision(2);
            ss << "温度: " << data.temperature << "°C "
               << "湿度: " << data.humidity << "%";
            Label_1->setText(ss.str().c_str());

            // 更新温度计显示
            tempWidget_->setValue(static_cast<int32_t>(round(data.temperature)));
            // 更新湿度计显示
            humiWidget_->setValue(static_cast<int32_t>(round(data.humidity)));

            if(data.temperature >= 30.0){
                Label_1->font()->setFontColor(_RGBA(255, 0, 0, 255), 0); // 红色
                Label_2->font()->setFontColor(_RGBA(255, 0, 0, 255), 0); // 红色
                Label_2->setText("温度过高警告!");
            } else if(data.temperature <= 5.0){
                Label_1->font()->setFontColor(_RGBA(0, 0, 255, 255), 0); // 蓝色
                Label_2->font()->setFontColor(_RGBA(0, 0, 255, 255), 0); // 蓝色
                Label_2->setText("温度过低警告!");
            } else {
                Label_1->font()->setFontColor(_RGBA(0, 255, 0, 255), 0); // 白色
                Label_2->setText(" ");
            }
        } else {
            Label_1->setText("数据无效");
        }
    } else {
        Label_1->setText("暂无数据");
    }
    Label_1->update();
    Label_2->update();
}
  • 从 dataProcessor 获取最新 1 条传感器数据,若数据有效则格式化显示温湿度到 Label_1、更新温湿度计控件值,并根据温度是否 ≥30℃(红色高温警告)或 ≤5℃(蓝色低温警告)设置标签字体颜色与警告文本(正常时为绿色字体且清空警告),数据无效时显示 “数据无效”、无数据时显示 “暂无数据”,最后更新两个标签。

10. 将 ui 添加到主窗口上

MainWindowService::MainWindowService()
    : isRunning_(true)
{
    setStyleSheet(applicationDirPath() + "/../data/style.css"); // 设置样式表路径(确保路径正确)
    setBackGroundColor(_RGB(128, 128, 128));           // 设置背景颜色为灰色

    // 绘图组件
    PlotWidget* plot = new PlotWidget(this);
    plot->setRect(0, 0, this->width(), this->height());  // 设置绘图区域大小为主窗口大小

    // 绘制温度计和湿度计
    tempWidget_ = new TpTemperatureWidget(this);  // 父窗口为主窗口
    tempWidget_->setRange(5, 40);  // 温度范围5~40℃(符合之前设置)
    tempWidget_->setColorList({_RGB(0,0,255), _RGB(0,255,0), _RGB(255,0,0)});  // 蓝→绿→红渐变
    tempWidget_->setLineWidth(2);  // 加粗线条(刻度更清晰)
    tempWidget_->setFixedSize(200, 220);  // 控件大小150x200
    // 计算位置:X=温度折线图X起点 +(折线图宽度-温度计宽度)/2(居中);Y=温度折线图底部 + 20(间距)
    int tempX = AXIS_LEFT_X + (PLOT_WIDTH - 150) / 2;  // 温度折线图X起点为AXIS_LEFT_X,宽度PLOT_WIDTH
    int tempY = AXIS_BASE_Y + 80;  // 温度折线图底部Y为AXIS_BASE_Y,下方80像素
    tempWidget_->move(tempX, tempY);  // 新位置

    // 3. 创建并初始化湿度计(湿度80%)
    humiWidget_ = new TpHumidityWidget(this);  // 父窗口为主窗口
    humiWidget_->setRange(0, 100);  // 湿度范围0~100%
    humiWidget_->setColorList({_RGB(0, 160, 255)});  // 固定蓝色(突出填充面积)
    humiWidget_->setLineWidth(2);   // 加粗轮廓
    humiWidget_->setFixedSize(120, 180);  // 控件大小120x180
    // 计算位置:X=湿度折线图X起点 +(折线图宽度-湿度计宽度)/2(居中);Y=湿度折线图底部 + 20(间距)
    int humiX = AXIS_RIGHT_X + (PLOT_WIDTH - 120) / 2;  // 湿度折线图X起点为AXIS_RIGHT_X,宽度PLOT_WIDTH
    int humiY = AXIS_BASE_Y + 120;  // 湿度折线图底部Y为AXIS_BASE_Y,下方120像素
    humiWidget_->move(humiX, humiY);  // 新位置

    //实时显示温湿度Label
    Label_1 = new TpLabel(this);
    Label_1 ->font()->setFontColor(_RGBA(255, 255, 255, 255), 0);
	Label_1 ->font()->setAntialias(TpFont::TINY_FONT_ANTIALIAS_GOOD);
	Label_1 ->font()->setFontWeight(TpFont::TINY_FONT_WEIGHT_ULTRALIGHT);
	Label_1 ->font()->setFontSize(18);
	Label_1 ->setRect(350, 40, 300, 30);
	Label_1 ->setVisible(true);
	Label_1 ->update();

    // 温度警告Labe2
    Label_2 = new TpLabel(this);
    Label_2 ->font()->setFontColor(_RGBA(255, 255, 255, 255), 0);
	Label_2 ->font()->setAntialias(TpFont::TINY_FONT_ANTIALIAS_GOOD);
	Label_2 ->font()->setFontWeight(TpFont::TINY_FONT_WEIGHT_ULTRALIGHT);
	Label_2 ->font()->setFontSize(18);
	Label_2 ->setRect(350, 70, 300, 30);
	Label_2 ->setVisible(true);
	Label_2 ->update();

    // 初始化并启动传感器读取器(关键添加)
    bool initSuccess = sensorReader.initialize("/dev/ttyACM0", 9600);  // 端口可能需要根据实际设备修改
    if (initSuccess) {
        sensorReader.start();  // 启动读取线程
    } else {
        Label_1->setText("传感器初始化失败");
        Label_1->update();
    }

    //启动定时线程(1秒更新一次)
    timerThread_ = std::thread(&MainWindowService::timerThreadFunc, this);
}
  • isRunning为 true,设置主窗口样式表与灰色背景,创建并配置 PlotWidget、温度范围 5~40℃ 且蓝绿红渐变的 TpTemperatureWidget、湿度范围 0~100% 且固定蓝色的 TpHumidityWidget(均指定大小与位置),初始化显示温湿度的 Label_1 和温度警告 Label_2(配置字体相关属性),尝试初始化 /dev/ttyACM0 端口、波特率 9600 的传感器读取器(成功则启动读取线程,失败则在 Label_1 提示),最后启动 1 秒更新一次的定时线程 timerThread。

  • 温湿度的折线图

五、总结

本项目实现了温湿度数据的实时采集、处理、可视化展示及警告功能,模块化设计保证了系统的可维护性和可扩展性。通过多线程与线程安全机制,平衡了数据实时性与 UI 流畅性,可作为环境监控类应用的基础框架进一步扩展。


本文由开发者联盟成员提供,原文地址
https://blog.csdn.net/twistzz_4/article/details/154589314

源码获取

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐