案例分享:基于 TinyPiXOS 实现温湿度实时监控系统
本文介绍了一个基于TinyPiXOS的温湿度实时监控系统,该系统通过模块化设计实现传感器数据采集、处理和可视化功能。系统采用串口通信读取温湿度数据,经解析后存储于缓冲区,并通过图形界面展示实时数值、曲线趋势和阈值警告。核心代码包括串口配置、数据读取解析和可视化组件,支持温度计/湿度计控件绘制和多线程安全操作。该系统适用于实验室、温室等需要环境监测的场景,具有实时性强、稳定性高的特点。
案例分享:基于 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)发送数据,SensorReader的readLoop循环调用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
源码获取。
更多推荐

所有评论(0)