7,697
社区成员
发帖
与我相关
我的任务
分享这次尝试做一个比较生活化的小 Demo:
实时检测环境温度、湿度,并根据当前环境自动给出穿衣建议。
例如环境为:
温度:16.8℃
湿度:82.4%
环境状态:偏凉
穿衣建议:建议长袖 + 薄外套;当前湿度较高,体感可能偏湿冷,可适当增加一层。
整个项目并不复杂,但比较适合用来理解 Arduino UNO Q 和普通 Arduino 最大的区别:
STM32 负责“感知”,Qualcomm Linux 负责“决策”。
这个 Demo 使用一个 SHT31 温湿度传感器实时采集室内环境数据。
数据首先进入 Arduino UNO Q 上的 STM32U585,然后通过 UNO Q 的 Bridge RPC 传递给运行 Debian Linux 的 Qualcomm QRB2210。
Linux 侧 Python 程序拿到温湿度以后,根据预设规则生成穿衣建议。
整体数据流如下:
SHT31 温湿度传感器
│
I2C
│
▼
┌─────────────────┐
│ STM32U585 │
│ Arduino / Zephyr│
│ │
│ 温湿度采集 │
└────────┬────────┘
│
Bridge RPC
│
▼
┌─────────────────┐
│ Qualcomm QRB2210│
│ Debian Linux │
│ │
│ Python规则引擎 │
└────────┬────────┘
│
▼
自动生成穿衣提醒
Arduino UNO Q 本身就是这样的异构架构:QRB2210 负责完整 Linux 环境和高层应用,STM32U585 则负责实时控制和 Arduino Sketch。两颗处理器之间可以通过 Arduino Router/Bridge RPC 通信。
这也是为什么这个项目我没有直接在 STM32 中写一堆 if...else。
因为后面如果要增加:
语音提醒
Web 页面
手机通知
天气 API
用户画像
AI Agent
大模型自然语言建议
这些逻辑放在 Linux 侧明显更加灵活。
这次用到的东西很少。
| 硬件 | 数量 | 作用 |
|---|---|---|
| Arduino UNO Q | 1 | 主控开发板 |
| SHT31 温湿度传感器 | 1 | 获取温度、相对湿度 |
| 杜邦线 | 4 | I2C 接线 |
| USB-C 电源/数据线 | 1 | UNO Q 供电及开发 |
我这里选择 SHT31,而没有使用比较常见的 DHT11。
主要原因是 SHT31 本身使用 I²C,UNO Q 官方已经提供标准 Wire 支持,我们甚至不用安装第三方传感器库,直接操作 I²C 就能读取数据。
UNO Q 标准 Arduino 接口中的 I²C 对应:
| UNO Q | 功能 |
|---|---|
| D20 | SDA |
| D21 | SCL |
官方用户手册明确给出了 Wire.begin() 对应 D20/D21,而板载 Qwiic 则使用另外一路 Wire1。
SHT31-DIS 本身支持 2.15V~5.5V 供电,并提供数字 I²C 温湿度输出。为了统一逻辑电平,这里直接使用 UNO Q 的 3.3V 供电。
接线如下:
| SHT31 | Arduino UNO Q |
|---|---|
| VIN / VCC | 3.3V |
| GND | GND |
| SDA | D20 / SDA |
| SCL | D21 / SCL |
这是这篇文章最重要的地方。
如果使用传统 Arduino UNO,我们可能会这么写:
SHT31
↓
Arduino
↓
if 温度 < 15
↓
输出“穿厚外套”
但到了 UNO Q,我更建议利用它原生的异构架构:
STM32 MCU
负责:
温湿度采集
I2C时序
传感器状态
实时任务
↓ Bridge RPC
Qualcomm Linux
负责:
数据分析
穿衣规则
日志
网络
UI
语音
AI
这样做的好处是硬件层和业务层被分开了。
以后即使把:
if temp < 15
升级成:
温湿度
+
天气预报
+
风力
+
降雨概率
+
个人耐寒程度
+
当天活动类型
+
大模型 Agent
STM32 那边基本不用动。
打开 Arduino App Lab,新建一个 App。
我们的项目目录可以简单理解成:
ClothingReminder/
│
├── python/
│ └── main.py
│
└── sketch/
└── sketch.ino
其中:
sketch.ino
↓
运行在 STM32U585
main.py
↓
运行在 Qualcomm QRB2210 / Debian Linux
两者通过:
Arduino Router
+
Bridge RPC
完成数据交换。
Arduino 官方提供的 UNO Q 示例同样采用这种方式,例如 Linux Python 可以直接:
Bridge.call("set_led_state", True)
调用 MCU 中通过 Bridge.provide() 注册的函数。
首先编写:
sketch/sketch.ino
为了减少第三方依赖,这里直接使用 Arduino 自带的 Wire 操作 SHT31。
完整代码如下:
#include <Wire.h>
#include <Arduino_RouterBridge.h>
#include <math.h>
#define SHT31_ADDR 0x44
float lastTemperature = 0.0f;
float lastHumidity = 0.0f;
bool sensorOK = false;
unsigned long lastReadTime = 0;
const unsigned long SENSOR_INTERVAL = 2000;
// ----------------------------------------------------
// SHT31 CRC8
// Polynomial: 0x31
// Initialization: 0xFF
// ----------------------------------------------------
uint8_t crc8(const uint8_t *data, int len) {
uint8_t crc = 0xFF;
for (int j = 0; j < len; j++) {
crc ^= data[j];
for (int i = 0; i < 8; i++) {
if (crc & 0x80) {
crc = (crc << 1) ^ 0x31;
} else {
crc <<= 1;
}
}
}
return crc;
}
// ----------------------------------------------------
// 读取 SHT31
// ----------------------------------------------------
bool readSHT31(float &temperature, float &humidity) {
// 0x2400
// Single Shot
// High Repeatability
// Clock Stretching Disabled
Wire.beginTransmission(SHT31_ADDR);
Wire.write(0x24);
Wire.write(0x00);
if (Wire.endTransmission() != 0) {
return false;
}
// 高重复性测量最大约需要 15ms
delay(20);
Wire.requestFrom(SHT31_ADDR, 6);
if (Wire.available() != 6) {
return false;
}
uint8_t data[6];
for (int i = 0; i < 6; i++) {
data[i] = Wire.read();
}
// CRC 检查
if (crc8(data, 2) != data[2]) {
return false;
}
if (crc8(data + 3, 2) != data[5]) {
return false;
}
uint16_t rawTemperature =
((uint16_t)data[0] << 8) | data[1];
uint16_t rawHumidity =
((uint16_t)data[3] << 8) | data[4];
// SHT31 官方转换公式
temperature =
-45.0f +
175.0f *
((float)rawTemperature / 65535.0f);
humidity =
100.0f *
((float)rawHumidity / 65535.0f);
return true;
}
// ----------------------------------------------------
// 给 Linux 提供 RPC 接口
// 乘以100转换成整数,避免传输浮点带来的不确定性
// ----------------------------------------------------
int32_t get_temperature_x100() {
if (!sensorOK) {
return -32768;
}
return (int32_t)lroundf(
lastTemperature * 100.0f
);
}
int32_t get_humidity_x100() {
if (!sensorOK) {
return -1;
}
return (int32_t)lroundf(
lastHumidity * 100.0f
);
}
bool get_sensor_status() {
return sensorOK;
}
// ----------------------------------------------------
// Setup
// ----------------------------------------------------
void setup() {
Serial.begin(115200);
// UNO Q 标准 I2C
// SDA = D20
// SCL = D21
Wire.begin();
// 初始化 Bridge
Bridge.begin();
// 注册 RPC
Bridge.provide_safe(
"get_temperature_x100",
get_temperature_x100
);
Bridge.provide_safe(
"get_humidity_x100",
get_humidity_x100
);
Bridge.provide_safe(
"get_sensor_status",
get_sensor_status
);
Serial.println(
"UNO Q Temperature & Humidity Sensor Ready"
);
}
// ----------------------------------------------------
// Loop
// ----------------------------------------------------
void loop() {
unsigned long now = millis();
if (now - lastReadTime >= SENSOR_INTERVAL) {
lastReadTime = now;
float temperature;
float humidity;
sensorOK = readSHT31(
temperature,
humidity
);
if (sensorOK) {
lastTemperature = temperature;
lastHumidity = humidity;
Serial.print("Temperature: ");
Serial.print(lastTemperature);
Serial.print(" C ");
Serial.print("Humidity: ");
Serial.print(lastHumidity);
Serial.println(" %");
} else {
Serial.println(
"Failed to read SHT31"
);
}
}
}
SHT31 默认 I²C 地址为 0x44,也可以通过 ADDR 引脚切换为 0x45。单次测量使用 0x2400 可以进行高重复性、关闭 clock stretching 的测量;高重复性模式最长测量时间约 15ms。
传感器返回:
Temperature MSB
Temperature LSB
Temperature CRC
Humidity MSB
Humidity LSB
Humidity CRC
因此总共读取 6 Byte。
SHT3x 官方给出的转换公式为:
Temperature
=
-45 + 175 × ST / 65535
Humidity
=
100 × SRH / 65535
CRC8 使用多项式 0x31、初始值 0xFF。
这里没有省略 CRC 检查。
因为 Demo 虽然简单,但如果以后真正用于环境监测,不能把一次 I²C 通信异常的数据当成真实温湿度。
代码里我没有直接:
return temperature;
而是:
return temperature * 100;
例如:
23.56℃
转换成:
2356
Linux 收到以后再:
2356 / 100
还原。
这样做不是因为 Bridge 不支持浮点数,而是一个比较常见的嵌入式处理习惯:
MCU
↓
定点整数
↓
IPC / 网络
↓
业务系统
↓
恢复小数
可以让协议更加明确,也方便以后扩展 MQTT、CAN、串口或其他通信方式。
接下来编写:
python/main.py
这里我们先不引入 AI。
先做一个透明、可解释的规则引擎。
from arduino.app_utils import App, Bridge, Logger
import time
logger = Logger(
"UNOQ-ClothingReminder"
)
# ----------------------------------------------------
# 穿衣建议
# ----------------------------------------------------
def get_clothing_advice(
temperature,
humidity
):
# 根据温度确定基础建议
if temperature >= 30:
status = "炎热"
advice = (
"建议短袖、短裤或轻薄透气衣物,"
"尽量选择吸汗、速干材质。"
)
elif temperature >= 26:
status = "偏热"
advice = (
"建议短袖、薄衬衫等轻薄衣物。"
)
elif temperature >= 22:
status = "舒适"
advice = (
"短袖或薄长袖均可,"
"早晚温差较大时可准备一件薄外套。"
)
elif temperature >= 18:
status = "微凉"
advice = (
"建议长袖、卫衣或薄外套。"
)
elif temperature >= 14:
status = "偏凉"
advice = (
"建议长袖 + 薄外套,"
"怕冷的话可以增加一层。"
)
elif temperature >= 8:
status = "较冷"
advice = (
"建议毛衣、卫衣搭配较厚外套。"
)
else:
status = "寒冷"
advice = (
"建议厚外套、羽绒服等保暖衣物,"
"注意头部和手部保暖。"
)
# ------------------------------------------------
# 湿度修正
# ------------------------------------------------
if humidity >= 80:
if temperature <= 20:
advice += (
" 当前湿度较高,"
"环境可能偏湿冷,"
"可适当增加一层并选择易干衣物。"
)
elif temperature >= 26:
advice += (
" 当前处于高温高湿环境,"
"建议优先选择透气、速干衣物。"
)
elif humidity <= 30:
advice += (
" 当前空气较干燥,"
"可注意补水和皮肤保湿。"
)
return status, advice
# ----------------------------------------------------
# 主循环
# ----------------------------------------------------
def loop():
try:
sensor_ok = Bridge.call(
"get_sensor_status"
)
if not sensor_ok:
logger.warning(
"SHT31 sensor is not ready"
)
time.sleep(3)
return
temperature_raw = Bridge.call(
"get_temperature_x100"
)
humidity_raw = Bridge.call(
"get_humidity_x100"
)
if temperature_raw == -32768:
raise RuntimeError(
"Invalid temperature data"
)
if humidity_raw < 0:
raise RuntimeError(
"Invalid humidity data"
)
temperature = (
temperature_raw / 100.0
)
humidity = (
humidity_raw / 100.0
)
status, advice = get_clothing_advice(
temperature,
humidity
)
logger.info(
"=============================="
)
logger.info(
f"Temperature: "
f"{temperature:.2f} °C"
)
logger.info(
f"Humidity: "
f"{humidity:.2f} %RH"
)
logger.info(
f"Environment: {status}"
)
logger.info(
f"Clothing Advice: {advice}"
)
except Exception as e:
logger.error(
f"Failed to get environment data: {e}"
)
time.sleep(5)
# ----------------------------------------------------
# 启动 App
# ----------------------------------------------------
App.run(
user_loop=loop
)
UNO Q Python 侧的 Bridge.call() 会等待 MCU 返回结果,所以:
temperature = Bridge.call(
"get_temperature_x100"
)
实际上就是在 Linux 上远程调用 STM32 中注册的 C++ 函数。Arduino 当前 Python Bridge 实现也明确支持将远程调用返回值直接作为 Python 对象返回。
这就是 UNO Q 比传统 MCU 有意思的地方。
Python 不需要:
打开串口
解析字符串
判断换行符
自己设计协议
而是直接:
Linux Python
↓
Bridge.call()
↓
STM32 Function
↓
return
看起来就像在调用一个本地函数。
这个 Demo 的规则故意没有做得特别复杂。
目前基础逻辑如下:
| 温度 | 环境判断 | 穿衣建议 |
|---|---|---|
| ≥30℃ | 炎热 | 短袖、短裤、透气衣物 |
| 26~29.9℃ | 偏热 | 短袖、薄衬衫 |
| 22~25.9℃ | 舒适 | 短袖或薄长袖 |
| 18~21.9℃ | 微凉 | 长袖、卫衣、薄外套 |
| 14~17.9℃ | 偏凉 | 长袖 + 薄外套 |
| 8~13.9℃ | 较冷 | 毛衣、卫衣 + 较厚外套 |
| <8℃ | 寒冷 | 厚外套、羽绒服 |
湿度不是简单地转换成所谓的“体感温度”。
这里采用更容易解释的修正规则:
温度 ≤ 20℃
+
湿度 ≥ 80%
↓
增加“湿冷”提醒
温度 ≥ 26℃
+
湿度 ≥ 80%
↓
增加“闷热、透气速干”提醒
湿度 ≤ 30%
↓
增加“空气干燥”提醒
这套规则主要用于 Demo 展示,并不是医学或专业气象穿衣指数。
真正做产品的话,还需要加入:
风速
太阳辐射
降雨
室内 / 室外
活动强度
年龄
个人耐寒程度
衣物保温能力
Arduino App Lab 中点击:
Run
以后,首先可以看到 STM32 侧不断采集温湿度。
例如下面是假设环境数据下的示例日志:
Temperature: 23.62 C
Humidity: 55.84 %
随后 Linux Python 会通过 Bridge 获取数据。
Arduino App Lab Console 中可以看到类似:
==============================
Temperature: 23.62 °C
Humidity: 55.84 %RH
Environment: 舒适
Clothing Advice:
短袖或薄长袖均可,
早晚温差较大时可准备一件薄外套。
如果把传感器放到湿度较大的环境,假设读取到:
Temperature: 16.80 °C
Humidity: 84.20 %RH
逻辑就会变成:
Environment: 偏凉
Clothing Advice:
建议长袖 + 薄外套,
怕冷的话可以增加一层。
当前湿度较高,
环境可能偏湿冷,
可适当增加一层并选择易干衣物。
这里的数据只是为了展示程序输出格式,最终实际数值以你的 SHT31 实测结果为准。
如果日志一直出现:
SHT31 sensor is not ready
首先检查接线:
3.3V
GND
D20 / SDA
D21 / SCL
其次检查地址。
我们代码默认:
#define SHT31_ADDR 0x44
SHT3x-DIS 默认地址是 0x44,当 ADDR 配置为高电平时也可以使用 0x45。
因此部分传感器模块如果修改过地址,需要把代码改成:
#define SHT31_ADDR 0x45
如果出现类似:
Function not found
建议先确认:
Bridge.begin();
是否正常运行,以及对应 RPC 是否已经注册:
Bridge.provide_safe(
"get_temperature_x100",
get_temperature_x100
);
函数名称必须和 Python 完全一致:
Bridge.call(
"get_temperature_x100"
)
Arduino 官方还特别提醒,Bridge.provide() 注册的函数运行在 RPC 后台线程,而 provide_safe() 会让回调在 Arduino 主循环上下文中安全执行;如果回调涉及 Arduino API,优先使用 provide_safe() 会更加稳妥。
当然也可以。
例如 MCU:
Serial.println(
"23.5,65.2"
);
Linux 再:
line.split(",")
但如果都已经使用 UNO Q 了,再自己维护串口协议其实有点可惜。
Bridge 最大的优势就是我们可以直接把 STM32 看成 Linux 的“硬件服务”。
例如以后还可以继续注册:
get_temperature
get_humidity
get_light
get_pm25
get_co2
get_noise
set_fan
set_heater
open_window
Linux 侧只需要:
Bridge.call(...)
即可。
这样整个系统会逐渐变成:
Linux
│
┌────────────┼────────────┐
│ │ │
环境判断 AI Agent Web UI
│ │ │
└────────────┼────────────┘
│
Bridge
│
STM32
│
┌───────────────┼───────────────┐
│ │ │
SHT31 CO₂ PM2.5
│
温湿度
这已经非常接近一个小型智能家居或者 AIoT 系统的架构了。
做到这里只算 Version 1.0。
下一步我认为最值得增加的是:
当前室内温湿度
+
互联网天气预报
+
当天最低/最高温
+
降雨概率
+
风力
↓
AI Agent
↓
“今天成都 18~26℃,
上午可能下雨,
当前室内23℃,
建议穿短袖并带一件轻薄防水外套。”
这样就不再只是:
温度 → 查表
而变成:
实时传感器
+
外部环境
+
个人偏好
+
AI推理
=
个性化生活建议
再进一步甚至可以给用户设置:
怕冷
正常
怕热
三个档位。
例如相同的:
18℃
对于不同用户:
怕冷用户
→ 毛衣 + 外套
普通用户
→ 长袖 + 薄外套
怕热用户
→ 长袖即可
这样这个小 Demo 就开始真正具备“智能助手”的感觉了。
这个案例本身其实非常简单:
读取温湿度,然后根据条件推荐衣服。
但如果放到 Arduino UNO Q 上,它真正值得学习的并不是几个 if...else。
而是:
Sensor
↓
STM32U585
↓
Bridge RPC
↓
Qualcomm QRB2210
↓
Linux / Python
↓
应用逻辑
STM32 处理底层硬件和实时任务。
Qualcomm Linux 处理规则、网络、UI 和未来的 AI。
两边各做自己擅长的事情。
从这个 Demo 继续往后扩展,可以很自然地演变成:
家庭环境助手
办公室舒适度助手
老人穿衣提醒
儿童穿衣提醒
户外运动建议
智能衣柜
智能家居 Agent
这也是我觉得 Arduino UNO Q 比传统 Arduino 更有意思的一点:
让 Arduino 的算力变强,也让 MCU 世界和 Linux / AI 应用真正运行在了同一块开发板上。