ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

App Inventor+Arduino+蓝牙,快速搞定手机温湿度计

App Inventor+Arduino+蓝牙,快速搞定手机温湿度计 前几天有人问我用App Inventor做蓝牙温湿度计到底难不难我说难的部分从来不是蓝牙而是把Arduino的串口数据变成手机屏幕上能看的温度和湿度。实际走通一次你就知道这套组合不需要你会Java不需要碰Android原生开发更不用去啃蓝牙协议栈的英文文档核心就是把三件事做好Arduino采集温湿度并格式化输出、蓝牙模块做无线透传、App Inventor端接收并解析文本。这篇文章就按这个链路展开从硬件选型到排障一条龙讲清楚适合那种想快速看到数据上屏、又不想被困在底层代码里的创客朋友。1. 先搞清楚这个5分钟到底能做什么1.1 为什么选这个组合App Inventor Arduino 蓝牙严格说这个项目的5分钟指的是把核心链路跑通而不是把产品打磨到可以拿去卖。你要做的原型是Arduino每隔两秒读一次DHT11温湿度传感器通过串口把数据交给蓝牙模块手机端的App Inventor应用连接蓝牙后把收到的文本解析出来显示在标签上。整个过程里App Inventor负责手机端Arduino负责硬件端蓝牙模块就是一根看不见的串口线。选这个组合的理由很直接成本低、出活快、资料多。Arduino Uno或Nano板子十几块钱DHT11温湿度传感器几块钱HC-05蓝牙模块十几块钱加一起不到五十块就能起步。相比之下如果用Android原生写蓝牙SPP你得处理UUID、Socket、输入输出流、线程切换还要考虑权限适配没个半天一天搞不定。App Inventor把蓝牙封装成了拖拽块你只要关心业务逻辑剩下的系统细节它帮你扛了。这里先泼一盆冷水这套方案基本只适合Android手机。无论是HC-05还是HC-06走的是经典蓝牙SPP协议iOS对SPP的支持非常差App Inventor官方也明确说iPhone上无法使用蓝牙客户端组件。如果你手边只有iPhone别在这个方向上死磕老老实实换ESP32加BLE方案。Android端则完全没有问题我实测从Android 6到Android 13都能正常跑。1.2 材料清单和选型前要做的决定先列一份最简清单避免你买一堆用不上的东西Arduino Uno或NanoNano体积小面包板上用起来方便我后面示例代码两种板子都能跑。DHT11温湿度传感器三根线VCC、GND、DATA。HC-05蓝牙模块带底板的那种底板上有稳压和指示灯适合新手。面包板一块、杜邦线七八根。Android手机一台Android 6及以上。选型的时候有个关键决定传感器用DHT11还是DHT22DHT11精度是±2℃、湿度±5%采样频率1Hz室内看个大概完全够用价格便宜。DHT22精度更高±0.5℃但价格贵两三倍而且对供电稳定性更敏感。我建议第一次做从DHT11开始跑通了再换。另一个决定是蓝牙模块选HC-05、HC-06还是JDY-31。HC-05支持AT指令修改名称和波特率主从一体后面做自动连接很方便。HC-06是从机便宜但很多版本不能改名字默认名字就是HC-06。JDY-31是兼容HC-05/06的改进方案有些版本支持3.3V供电手册里写着支持SPP协议、完全兼容HC-05/06如果你手边是这种模块代码逻辑完全一样。1.3 蓝牙模块对比别被SPP协议这四个字吓到很多人在电商平台搜蓝牙模块会被支持SPP协议经典蓝牙3.0完全兼容HC-05/06这些描述搞得头晕。我帮你理一下模块角色默认波特率主要特点适合场景HC-05主从一体9600支持AT指令可改名字、波特率有状态引脚需要自动连接的手机项目HC-06从机9600价格低不能主从切换部分版本不能改名简单透传、手动选设备也能接受JDY-31从机/兼容9600兼容HC-05/06部分带底板和指示灯替代HC-05/06买得到哪个用哪个这里说的SPPSerial Port Profile就是蓝牙串口协议手机连上模块后模块的TX/RX引脚会像USB串口一样收发数据。对用户来说SPP协议的唯一意义就是你不需要关心蓝牙底层分包、重传、配对加密只需要在App Inventor里调用连接然后把模块端当成普通串口对待。2. Arduino这端把温湿度变成一串可读的字符2.1 接线其实就四根线电源、地、串口和信号接线是整个项目里物理上最容易错的一环尤其两个模块同时接的时候。我们先看DHT11三根线很简单VCC接Arduino的5VGND接GNDDATA接数字引脚D2。DHT11的引脚顺序不同厂家不一样以丝印为准别只看网上图。蓝牙模块稍微讲究一点。如果你用HC-05模块的VCC接Arduino的5VGND接GND然后TX接Arduino的RXRX接Arduino的TX注意是交叉接。串口通信的基本原则就是发送端接接收端你的数据由Arduino发出所以要进蓝牙模块的RX模块收到后再从TX发出去。很多人第一次做把TX对TX、RX对RX接上去结果手机能连上但永远收不到数据原因就在这。我建议用软件串口而不是直接接Arduino的硬件串口0/1。原因有两个第一Arduino Uno在通过USB下载程序时板载串口被占用如果你把蓝牙接在D0/D1上下载时会互相干扰甚至导致上传失败第二软件串口可以自定义引脚灵活性高。我的接线方案是DHT11 DATA → D2HC-05 TX → D3Arduino的软串口RXHC-05 RX → D4Arduino的软串口TXHC-05 VCC → Arduino 5VHC-05 GND → Arduino GND注意等级电平的问题。很多HC-05模块是3.3V逻辑电平但带底板时往往可以用5V供电板上自带稳压RX引脚也能容忍Arduino发出的5V TX信号实测大多数底板没问题。如果你买的模块没有底板建议TX引脚上串一个1K电阻做分压避免烧模块。2.2 写个最简单的DHT11读取程序先打开Arduino IDE在库管理器里搜索并安装两个库DHT sensor library和Adafruit Unified Sensor。前者是驱动DHT系列传感器的后者是Adafruit库的依赖缺了编译会报错。然后在Arduino IDE里新建一个工程粘贴下面这段代码#include SoftwareSerial.h #include DHT.h #define DHTPIN 2 #define DHTTYPE DHT11 #define BT_RX 3 #define BT_TX 4 DHT dht(DHTPIN, DHTTYPE); SoftwareSerial bt(BT_RX, BT_TX); void setup() { Serial.begin(9600); bt.begin(9600); dht.begin(); } void loop() { float t dht.readTemperature(); float h dht.readHumidity(); if (isnan(t) || isnan(h)) { bt.println(T:err,H:err); } else { bt.print(T:); bt.print(t, 1); bt.print(,H:); bt.println(h, 1); } delay(2000); }代码逻辑很直白每两秒读一次温度和湿度拼成一行文本通过软串口发给蓝牙模块。t, 1是保留一位小数避免显示太多数字。DHT11的采样周期是1秒所以我刻意把发送间隔设成2秒太频繁会读到不稳定的数手机端也会被刷屏。如果你用的是DHT22把上面DHTTYPE改成DHT22就行其他不用动。如果你的模块实际是JDY-31代码也照样跑接线和HC-05一样。2.3 数据协议为什么选T:26.5,H:60.2这种格式这是很多人忽略但很重要的设计。Arduino端往串口里塞什么格式的数据直接决定了App Inventor端解析的难度。我从一开始就推荐发送带标签、逗号分隔、换行结尾的纯文本例如T:26.5,H:60.2为什么不用二进制浮点数因为App Inventor处理二进制要拼接字节、判断结束位非常痛苦。纯文本你可以直接用文本处理积木按逗号切一次、按冒号再切一次非常简单。为什么带T:和H:前缀因为万一某次数据解析出错你至少能知道哪一段是温度、哪一段是湿度排查问题时方便很多。为什么每行结尾要println而不是print因为换行符就是一帧数据的结束标志App Inventor端可以利用这个分隔符判断一行数据收完了。波特率方面代码里bt.begin(9600)必须和蓝牙模块的默认波特率一致。HC-05/06出厂默认都是9600如果你用AT指令改过记得同步改代码。这个数字不是随便定的9600在2米左右的距离内完全够用而且低波特率容错性更好不容易出乱码。3. App Inventor这端从配对到上屏的完整链路3.1 界面组件别放一堆按钮够用就行打开App Inventor后新建一个项目命名随意。手机端的界面我设计得非常精简ListPicker1用来显示已配对的蓝牙设备列表用户手动点选也可以用来做自动连接的来源。Button1连接/断开按钮文本初始为连接设备。Label1显示温度比如温度26.5 ℃。Label2显示湿度比如湿度60.2 %。Clock1定时器组件间隔1000毫秒用来周期性读蓝牙缓冲。BluetoothClient1在组件面板的连接分类下面这是整个App的核心负责蓝牙连接和数据收发。不要一上来就堆一堆图表、按钮、颜色设置。先把最简单的链路跑通界面美观是后面的事。3.2 核心块逻辑连接、读取、解析打开“块”视图真正需要拖的块其实不多。我按功能拆给你看。连接逻辑when Screen1.Initialize if BluetoothClient1.BluetoothEnabled false call BluetoothClient1.EnableBluetooth这个块是在应用启动时检查蓝牙是否开启如果没开就弹出系统配对请求。但注意EnableBluetooth只是跳转到系统设置页面不是强制开启Android不允许普通应用直接开蓝牙。按钮点击逻辑when Button1.Click if BluetoothClient1.IsConnected true call BluetoothClient1.Disconnect set Button1.Text to 连接设备 else if ListPicker1.Selection ! call BluetoothClient1.Connect(ListPicker1.Selection) if BluetoothClient1.IsConnected true set Button1.Text to 断开连接 else call Notifier1.ShowAlert(连接失败)这里有个细节ListPicker1.Selection在选中后看起来是设备名但里面实际保存的是列表项的完整内容。我建议初始化时就给ListPicker1设置Display项和Value项Display给人看Value存蓝牙地址。如果你图省事直接把BluetoothClient1.BluetoothAddressesAndNames扔给ListPicker1作为一个简单列表Selection里就会包含名称和地址的混合文本Connect时可能会因为格式不对连接失败。实际上App Inventor的BluetoothAddressesAndNames返回的是每个元素形如名称 地址的列表Connect的时候需要的是地址所以不能直接拿整个元素去连。比较稳的做法是写一个解析函数从元素里把最后一段数字地址提取出来。读取逻辑稍微复杂一点。蓝牙数据是流式的一次Clock触发不一定能收到完整的一行可能收到半个数据帧也可能一次收到好几帧。我在项目里习惯用一个全局变量buffer暂存文本每次把新读到的内容追加进去再从中截取一行行的完整数据。核心块逻辑是这样when Clock1.Timer if BluetoothClient1.IsConnected true and BluetoothClient1.BytesAvailableToRead 0 set global buffer to join(global buffer, BluetoothClient1.ReceiveText(-1)) while global buffer contains \n set global line to segment of global buffer before first \n set global buffer to text after first \n of global buffer call ProcessLine(global line)ProcessLine就是你自建的一个函数参数是一行字符串。里面先用split text at comma把T:26.5,H:60.2拆成两个元素再对每个元素用冒号分割取第二部分转成数字最后更新标签文本。如果某一行里包含err就直接显示传感器异常。这种用缓冲加换行判断的方式能同时解决半包和粘包问题是串口解析里的通用套路不只是App Inventor适用。3.3 自动连接的两种实现思路标题里写了手机自动连Arduino但App Inventor没有现成的一键自动连接组件需要你写一点逻辑。我试过两种思路各有优劣。第一种是启动后自动扫描已配对设备通过设备名匹配。在Screen1.Initialize里加一个延时等蓝牙栈就绪后遍历BluetoothClient1.BluetoothAddressesAndNames找到文本中包含HC-05或者你AT指令改的名字的那个元素提取地址并调用Connect。这种方案的好处是用户零操作打开App就自动连体验好。坏处是对设备名敏感你如果用HC-06且没改名默认名字可能撞车最好在模块AT指令里改成一个难重复的名字比如BT_TEMP_01。第二种是定时器轮询式连接。专门用一个Clock或复用读数Clock在连接失败后每隔几秒尝试连接一次。逻辑是如果没连接就遍历设备列表匹配到目标设备后连接。这样即使手机蓝牙启动慢、第一次没连上后面也能自动补上。我实际用的就是第二种因为有些手机蓝牙在上电唤醒后有几百毫秒延迟第一次连接失败的概率不低轮询能补齐这个空缺。但无论哪种方式有个前提必须在代码外完成手机必须先和蓝牙模块配对一次。App Inventor只能连接已经配对的设备它没有提供发起配对并输入PIN码的界面。所以第一次用的时候先去手机系统设置里的蓝牙页面搜索HC-05输入PIN码HC-05默认1234或0000完成配对然后再打开你的App。这一点文章后面排障部分会详细说。3.4 权限和Android版本很多人在这里翻车蓝牙应用在Android上有个很反直觉的权限要求Android 6.0及以上扫描蓝牙设备或者读取蓝牙相关信息需要位置信息权限。很多人写完App一点ListPicker发现列表是空的第一反应是蓝牙模块坏了其实是系统把权限拦了。解决方法是到手机设置里把App的位置权限打开或者至少在蓝牙扫描时允许仅使用期间授权。Android 12及以上又进一步把附近设备扫描权限单独拆出来叫附近的设备。App Inventor在打包时会自动在清单里声明这些权限但首次运行到相关功能时系统会弹窗索取授权你需要在弹窗时点允许。国内一些定制ROM还会把权限默认设为拒绝甚至需要额外开启后台定位才能扫描蓝牙。我的建议是真机测试前先到应用权限页面把所有和蓝牙、位置相关的权限都打开排除掉这个变量再排查别的。4. 实测排障HC-05连不上、数据乱码、自动连接失效4.1 手机搜不到HC-05先按这个顺序排查HC-05蓝牙模块连接不上是我在相关搜索词里看到频率最高的问题可以说十个做蓝牙串口项目的人里面至少有五个栽在搜不到模块上。按下面顺序排查通常几分钟就能定位。第一看模块指示灯。HC-05带底板的话上电后未连接时指示灯通常是慢闪连接成功后变成快闪或常亮。如果灯完全不亮先查电源和接线VCC和GND是否接反杜邦线是否松动。很多底板上的LED在3.3V供电时会变暗不是坏了。第二确认模块是否误入了AT模式。HC-05的底板上有按键如果上电时按住这个按键再通电模块会进入AT指令模式此时它不广播蓝牙信号手机自然搜不到。解决方法是断电后重新上电不要碰按键。如果你前面试过AT指令很可能把它留在指令模式下忘记了。第三在手机蓝牙设置里找到已经配对的旧设备删除后重新搜索。有些手机只要设备在配对列表里就不会在搜索列表里重复显示你以为搜不到其实早就配对过。第四排除距离和干扰。模块离手机太远、中间隔着金属物体或者旁边一堆2.4G设备都可能导致搜索不到。把模块放到手机旁边再试一次。如果以上都试过还是搜不到拿另一台手机试一下。如果第二台手机能搜到大概率是第一台手机的蓝牙扫描缓存或者权限问题如果第二台也搜不到模块本身可能是坏的或者一直被什么东西占用了配对名额。4.2 连上但永远收不到一行完整数据手机能连上蓝牙说明链路已经通了一半但收不到数据问题多半出在Arduino到蓝牙模块这一段。最经典的原因是TX/RX接反。前面我已经强调过交叉接线但这里还是得再重复一次模块的TX要接Arduino的RX模块的RX要接Arduino的TX。你可以在Arduino IDE的串口监视器里先看一眼如果串口监视器里能正常打印T:26.5,H:60.2说明传感器和代码没问题这时问题基本就锁定在蓝牙接线或者波特率上了。第二个原因是波特率不匹配。如果模块被改成38400或者9600以外的速率而你代码里仍是9600收到的就是一堆乱码或者根本没有数据。你可以把蓝牙模块重新进入AT模式用USB转TTL接电脑发ATUART?查询当前配置确认模块波特率再同步代码。没有USB转TTL的话直接换一个出厂设置的模块也是排查思路。第三个原因是软串口的引脚选择有讲究。并不是任意两个数字引脚都能做软串口RX/TX。Arduino Uno上软串口RX引脚不能同时被设置为输入中断用途虽然DHT11接D2不影响D3但如果你后续加了其他库占用了引脚就可能互相干扰。最简单的验证方法是临时把DHT11的数据打印到硬件串口再在软串口发固定字符串确认软件串口本身能通。4.3 每次都要手动选设备自动连接的坑如果你已经实现了自动连接逻辑但发现实际还是要手动点可能不是逻辑写错而是手机蓝牙栈没准备好。我遇到过的情况是应用启动时就立刻执行连接但那时安卓蓝牙服务还没有完全就绪BluetoothClient1.BluetoothAddressesAndNames返回的是空列表自动连接当然扑空。解决办法是在定时器里做轮询每隔两秒检查一次连接状态如果未连接就重新获取设备列表并尝试。等蓝牙栈稳定后列表自然会出现HC-05连接就能成功。另一个坑是设备名称匹配太严格。如果你用contains HC-05去匹配但模块被改名为BT_TEMP_01那肯定匹配不上。如果你用 HC-05精确匹配某些模块名字里带了空格或者不可见字符也会失败。我建议匹配的时候把前后空格去掉并且用contains而不是精确相等。如果怎么都匹配不上在ListPicker里手动选一次连接成功后把那个设备地址存到TinyDB里下次直接按地址连比按名字匹配更稳。4.4 一个完整的排查链路示例我在一次实际测试中遇到手机能连、串口监视器有数据、但手机端始终没显示的问题排查过程是这样的第一步确认Arduino串口监视器正常。我打开IDE的串口监视器看到每两秒一行T:26.5,H:60.2说明DHT11和代码没问题。第二步重新检查蓝牙模块接线。果然发现HC-05的TX接在了Arduino的D4上也就是TX对TX把两根线对调后手机端立刻有数据了。那次之后我总结出一个习惯无论多自信接线完成后一定要拿手机串口调试助手先测一发不要直接上App这样能把问题分成硬件链路和App逻辑两段定位快很多。第三步如果手机串口调试助手能收到数据但App没显示重点检查App里的解析逻辑。我这里曾因为ReceiveText(-1)读到的是空字符串导致buffer一直不长后来改成while BytesAvailableToRead 0循环读才稳定下来。所以排障时一定要先确定问题在哪一层不要从头瞎猜。5. 跑通之后还能怎么玩扩展功能和避坑建议5.1 在手机上画出温湿度变化曲线当你能稳定收到数据之后下一步自然是想看趋势。App Inventor新版自带Chart和Trendline组件你可以在Clock事件里把每次解析出的温度、湿度数值添加到图表的数据源里图上就会一点点画出曲线。需要注意的是App Inventor的图表组件属于相对新的功能老版本教程里很少讲。如果你用的App Inventor页面里找不到Chart可以在组件面板的“图表”分类下找。添加数据时用Chart1.DataSource相关的块把数值追加进去。但蓝牙传输不适合秒级持续刷曲线持续的2秒一帧对蓝牙和手机屏幕压力都不小建议把采样间隔改成5秒甚至10秒或者先在Arduino端累计几组取平均值再发。5.2 加一个风扇或舵机让看数据变成用数据数据上屏只是第一步真正好玩的是基于数据做动作。很多人做完温湿度计之后会接着用Arduino控制风扇、加湿器或者舵机。比如温度超过30℃时手机App里按一个按钮发送FAN_ONArduino端用软件串口收到这个字符串后解析出来控制接在D9引脚上的继电器从而开关风扇。Arduino端接收指令的代码可以在现有基础上扩展if (bt.available() 0) { String cmd bt.readStringUntil(\n); cmd.trim(); if (cmd FAN_ON) { digitalWrite(9, HIGH); } else if (cmd FAN_OFF) { digitalWrite(9, LOW); } }App Inventor端发送指令更简单调用BluetoothClient1.SendText(FAN_ON)就行。这个玩法也是很多Arduino智能小车Arduino控制舵机项目的基础本质是双向的串口指令交互。不过要注意App Inventor的SendText是直接发字符串Arduino端如果同时收字符串又收其他数据要处理好缓冲区分隔避免指令和数据混在一起。5.3 把数据传到云端的轻量方案蓝牙的天然限制是手机必须靠近才能看到数据。如果你想让温湿度计在无人值守的时候记录数据或者远程查看建议换思路把ESP8266或ESP32接上DHT11用Wi-Fi直接上报到MQTT服务器。这套方案的好处是彻底摆脱蓝牙配对、手机权限这类问题数据随时在线坏处是代码量比Arduino加蓝牙多一些而且需要配置Wi-Fi和MQTT。如果你还是想保留现有蓝牙硬件也可以通过手机App把数据转发到云端。App Inventor里可以用TinyWebDB组件或Web组件调用一个HTTP接口把每次读到的温湿度POST到你的服务器或云平台。不过手机不在旁边时数据链路就断了所以这只能算临时方案长期看还是ESP32方案靠谱。我做这个项目的最大体会是App Inventor最大的价值不是让你避免写代码而是让你把注意力放在数据链路上传感器怎么变成数字数字怎么变成串口字节串口怎么穿过蓝牙手机怎么还原成可读信息。想清楚这条链路后面换任何硬件和平台都能很快上手。你把这个温湿度计跑通之后下一步哪怕换成PM2.5传感器、土壤湿度传感器也只是换引脚和解析规则的事。先动手别怕接线烧了板子大不了换一块Uno成本不高但经验是自己的。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进