1. 为什么 Android Things 做智能家居是个"看起来很美"的选择
2016 年前后,智能家居赛道涌进来一大批开发者,手里攥着树莓派、各种开发板,脑子里想的都是"我要做一个自己的中控"。当时摆在面前的路无非几条:要么用 ESP8266/ESP32 这类 Wi-Fi 模组配 Arduino 框架,要么上 STM32 跑裸机或 RTOS,要么干脆用一台旧手机或者树莓派跑 Linux。Android Things 是 Google 在 2016 年底端出来的另一条路——把 Android 框架裁剪后塞进嵌入式设备,让写过 Android App 的人能直接上手做硬件。
这个卖点当年确实打动了不少人。你不需要重新学一套工具链,Android Studio 还是那个 Android Studio,Java/Kotlin 还是那套语言,Activity、Service、Handler这些概念原封不动搬过来,只是把findViewById换成了PeripheralManager去拿 GPIO、I2C、UART 的句柄。对于移动端转过来的开发者,学习曲线几乎是平的。
但这里有个必须先说清楚的前提:Android Things 的官方支持周期已经结束。Google 在 2019 年前后就停止了对新项目的推荐,2020 年后官方镜像和云服务逐步下线。所以今天再谈"基于 Android Things 构建智能家居系统",更现实的意义是两类人:一类是手里还有旧开发板(比如 NXP i.MX7D、Raspberry Pi 3 Model B 的 AT 版本)想物尽其用的;另一类是想理解"用高级操作系统做 IoT 网关"这套架构思路的。架构思路本身不会过时,STM32 方案和 Android Things 方案在系统分层上的思考是可以互相借鉴的。
我个人的判断是:如果你今天从零开始做一个量产级别的智能家居中控,Android Things 不是首选,ESP32 + ESP-IDF 或者 STM32 + FreeRTOS 在成本、功耗、供货稳定性上都更靠谱。但如果你是想学"网关型设备怎么设计",或者手上正好有块 AT 开发板,那这套东西依然值得动手跑一遍。下面我按一个真实可落地的智能家居系统来拆,从硬件选型一直讲到场景联动逻辑,中间会穿插大量我在实际调试中踩过的坑。
2. 系统分层:把"智能家居"拆成能动手的四块
2.1 网关层、设备层、通信层、应用层的职责边界
很多人一上来就想"我要做个 App 控制灯",结果做到一半发现设备怎么发现、状态怎么同步、断网了怎么办,全都没想清楚。智能家居系统本质上是一个分布式系统,先分层再动手,能省掉后面 80% 的重构。
我习惯把它拆成四层:
- 设备层:真正干活的末端节点,比如灯、插座、温湿度传感器、窗帘电机。它们通常资源极其有限,用 MCU(STM32、ESP8266 之类)跑,只负责采集和执行。
- 通信层:设备层和网关层之间的数据通道。短距离常用 Zigbee、BLE、433MHz,Wi-Fi 设备则直接走局域网。这一层决定了你的系统能挂多少设备、响应有多快。
- 网关层:也就是 Android Things 开发板所在的位置。它负责协议转换、设备管理、本地自动化规则执行、和云端同步。这是整个系统的"大脑",也是本文的主角。
- 应用层:手机 App、Web 面板、语音助手。用户看到和操作的部分。
Android Things 的价值集中在网关层。它比 STM32 网关强的地方在于:能跑完整的网络协议栈、能开本地数据库、能做复杂的规则引擎、能直接对接云 API。而 STM32 做网关,往往受限于内存和算力,规则引擎只能做很简单的 if-else。
2.2 为什么网关要选"能跑操作系统"的方案
这里要解释一个关键取舍。假设你有 30 个设备,每个设备每秒上报一次状态,网关要做的处理包括:解析协议、更新本地状态表、判断是否触发联动规则、把变化推给 App、定期同步云端。这一套下来,裸机 MCU 用状态机硬写也能实现,但代码会迅速变成一团乱麻,而且一旦要加一个新协议(比如从 Zigbee 加一个 BLE 设备),整个状态机都要动。
跑操作系统的网关,可以把这些任务拆成独立的线程或进程:一个线程收数据,一个线程跑规则引擎,一个线程管云端同步,彼此通过消息队列通信。Android Things 基于 Android,天然有HandlerThread、Service、LiveData这些工具,写起来比裸机舒服太多。代价是功耗和成本上去了——一块 i.MX7D 开发板功耗在瓦级,而 STM32 网关可以做到毫瓦级。所以如果你的设备是电池供电、要求几个月续航,网关这层就不该用 Android Things,而应该让设备直连云端或者用低功耗 Mesh。
2.3 一张表看清 Android Things 网关和 STM32 网关的取舍
| 维度 | Android Things 网关 | STM32 网关 |
|---|---|---|
| 典型主控 | i.MX7D、RPi3 | STM32F4/F7/H7 |
| 内存 | 512MB~1GB | 128KB~1MB |
| 功耗 | 瓦级 | 毫瓦级 |
| 开发语言 | Java/Kotlin | C |
| 协议栈丰富度 | 高(可跑完整 TCP/IP、MQTT、HTTP) | 中(需自己裁剪) |
| 规则引擎复杂度 | 可做复杂逻辑 | 一般只做简单联动 |
| 成本 | 高 | 低 |
| 适合场景 | 常供电中控、协议转换网关 | 电池设备、低成本节点 |
这张表不是要分高下,而是帮你判断自己的项目该站哪一边。我见过有人拿 STM32 硬做复杂规则引擎,最后代码维护不动;也见过有人拿 Android Things 做电池传感器,两天就没电。选型错了,后面全是坑。
3. 硬件准备:PeripheralManager 能碰到的和碰不到的
3.1 开发板与外围器件的实际搭配
Android Things 官方支持过的板子里,最常用的是 NXP i.MX7D 和 Raspberry Pi 3 Model B。i.MX7D 的优点是自带 NXP 的引脚扩展,GPIO、I2C、PWM 都规整;RPi3 的优点是便宜好买,但它的 GPIO 在 Android Things 下的映射和树莓派原生 Linux 不完全一样,需要查官方文档的引脚表。
外围器件我建议这样配一套最小系统:
- 一块 OLED 屏(I2C 接口,SSD1306 驱动),用来显示网关状态和 IP。
- 一个温湿度传感器(DHT22 或 SHT30,前者用 GPIO 单总线,后者用 I2C)。
- 一个继电器模块,控制一盏灯。
- 一个按钮,做本地手动开关。
- 一个 RGB LED,做状态指示。
这套下来成本不高,但能把 GPIO 输入输出、I2C 读写、PWM 调光这几类操作全跑一遍。等你把这几个跑通,接真实设备只是换驱动的事。
3.2 引脚映射的坑:别照抄树莓派教程
这是我最想提醒的一点。网上大量"树莓派 GPIO 控制"的教程是给 Raspbian 写的,用的是 BCM 编号或者 WiringPi 编号。Android Things 用的是自己的命名体系,比如PeripheralManagerService里通过"GPIO_12"这样的字符串去拿引脚,这个编号对应的是板子的物理引脚还是 SoC 引脚,不同板子不一样。
我当年第一次接 DHT22,照着某篇树莓派教程接了 BCM4,结果 Android Things 里死活读不到数据。折腾了半天才发现,AT 下 RPi3 的引脚命名和 BCM 编号不是一一对应的,得查官方那张Raspberry Pi 3 pinout表。所以我的建议是:接线前先把官方引脚表打印出来贴在桌上,每接一根线就在表上标一下,别凭记忆。
3.3 供电和电平匹配:3.3V 与 5V 混用的处理
Android Things 开发板的 GPIO 基本都是 3.3V 逻辑。但很多继电器模块、传感器是 5V 供电和 5V 逻辑。直接把 5V 信号灌进 3.3V 的 GPIO,轻则读不准,重则烧引脚。
处理办法有两个:一是选 3.3V 兼容的模块,现在很多继电器模块支持 3.3V 触发;二是加电平转换电路,比如用 TXS0108E 这类双向电平转换芯片。我一般优先选前者,省事。如果非要用 5V 模块,至少加个分压电阻或者光耦隔离,别省这几毛钱。
供电上还有个细节:开发板和继电器最好分开供电。继电器吸合瞬间电流冲击比较大,如果和开发板共用一路电源,可能导致开发板复位。我吃过这个亏,网关跑着跑着突然重启,查了半天是继电器把电压拉下去了。
4. 从点亮一个 LED 到跑通完整外设链路
4.1 用 PeripheralManager 打开 GPIO 的标准姿势
Android Things 里操作外设的入口是PeripheralManager。拿一个 GPIO 输出引脚的代码大概长这样:
PeripheralManagerManager manager = PeripheralManager.getInstance(); Gpio ledGpio = manager.openGpio("GPIO_12"); ledGpio.setDirection(Gpio.DIRECTION_OUT_INITIALLY_LOW); ledGpio.setValue(true); // 点亮看起来简单,但有几个点新手容易忽略。第一,openGpio的字符串必须和板子文档一致,写错了会抛IOException。第二,setDirection要在setValue之前调用,否则行为未定义。第三,用完记得close(),不然这个引脚会被一直占用,下次打开就失败。
我建议把每个外设的打开和关闭封装成一个AutoCloseable的包装类,用 try-with-resources 管理,避免忘记释放。
4.2 I2C 读传感器:寄存器地址和时序
I2C 设备操作比 GPIO 复杂一点,因为要理解"寄存器"这个概念。以 SHT30 温湿度传感器为例,它的 I2C 地址是 0x44,读温度要先发一个测量命令(比如 0x2C06),等一段时间,再读 6 个字节,前 3 个是温度,后 3 个是湿度,每个值 2 字节数据加 1 字节 CRC。
I2cDevice device = manager.openI2cDevice("I2C1", 0x44); byte[] command = {(byte)0x2C, (byte)0x06}; device.write(command, 2); Thread.sleep(20); byte[] buffer = new byte[6]; device.read(buffer, 6); // 解析 buffer...这里的坑在于:不同传感器的命令字和时序完全不同,必须对着数据手册写。而且Thread.sleep的时间不能太短,SHT30 高重复性模式下测量要 15ms 左右,你睡 5ms 读出来就是脏数据。我一般会留 20~30ms 余量。
4.3 把外设封装成可复用的驱动层
当你接了五六个外设之后,如果每个都直接在主逻辑里openGpio、openI2cDevice,代码会非常乱。我的做法是给每个外设写一个驱动类,比如LedDriver、Sht30Driver、RelayDriver,对外只暴露read()、write()这样的方法,内部处理引脚、时序、CRC 校验。
这样做的好处是:主业务逻辑只关心"读温度"这个动作,不关心它是 I2C 还是单总线。将来换传感器,只改驱动类,业务代码不动。这也是从 STM32 那套 HAL 库学来的思路——分层永远是对的。
4.4 一个真实的调试案例:DHT22 读出来全是 0
DHT22 是单总线协议,对时序要求很苛刻,微秒级。Android Things 跑在 Linux 内核上,GPIO 的读写延迟受调度影响,抖动可能到毫秒级。我当时读 DHT22,返回的数据全是 0 或者校验失败。
排查过程是这样的:先用逻辑分析仪抓了一下波形,发现 Android Things 发出的起始信号宽度就不对,本该 1ms 的低电平,实际有 1.5ms 还带抖动。这说明在用户态做微秒级时序控制本身就不靠谱。
最后的解决方案是换传感器,改用 I2C 的 SHT30。I2C 有时钟线同步,对时序抖动容忍度高得多。这个教训很深刻:在跑操作系统的平台上,别碰对时序要求到微秒级的单总线协议,除非你能写内核驱动。这也是为什么很多 Android Things 项目最后都选 I2C 和 UART 设备。
5. 通信协议选型:设备怎么把数据送到网关
5.1 Wi-Fi、Zigbee、BLE 在网关侧的接入差异
设备层和网关层之间的通信,直接决定了系统能挂多少设备、响应多快、功耗多低。
Wi-Fi 设备接入最简单,设备直接连路由器,网关通过局域网 socket 或者 MQTT 和它通信。优点是协议通用,缺点是 Wi-Fi 设备功耗高、路由器能挂的终端数有限(家用路由器一般 20~30 个就吃力了)。
Zigbee 设备功耗低、能自组网,一个网关能挂几十上百个。但 Android Things 开发板本身没有 Zigbee 射频,你得外接一个 Zigbee 协调器模块(比如 CC2530 或 CC2652),通过 UART 和网关通信。这就多了一层协议转换。
BLE 介于两者之间,适合低功耗小数据量场景,但连接数也有限。
我的建议是:小规模(10 个设备以内)用 Wi-Fi 直连,中大规模用 Zigbee 外挂协调器。BLE 更适合做配网辅助,比如用手机给设备配 Wi-Fi 密码。
5.2 MQTT 主题设计:别用一堆平铺的 topic
网关和云端、App 之间,我强烈建议用 MQTT。它轻量、支持发布订阅、断线重连机制成熟。但主题(topic)设计有讲究。
新手常犯的错是设计成device1/temp、device2/temp这种平铺结构,设备一多就乱。我习惯用层级结构:
home/{room}/{deviceType}/{deviceId}/state home/livingroom/light/001/state home/bedroom/sensor/003/telemetry这样订阅的时候可以用通配符,比如home/livingroom/#订阅客厅所有设备,home/+/sensor/+/telemetry订阅所有传感器数据。规则引擎处理起来也方便,按房间、按类型批量操作。
5.3 本地优先:断网时系统还得能跑
这是智能家居系统设计里最容易被忽视、但用户最在意的一点。用户按开关,灯必须立刻亮,不能等云端转一圈。所以联动规则必须在网关本地执行,云端只做远程控制和数据备份。
我的做法是:网关本地维护一份设备状态表和规则表,规则引擎在本地跑。云端下发的指令走 MQTT 到网关,网关执行后再把结果同步回云端。断网时,本地规则照常工作,只是 App 远程控制失效。等网络恢复,网关把离线期间的状态变化批量同步上去。
这个"本地优先"的原则,无论你用 Android Things 还是 STM32 网关,都必须遵守。我见过太多系统把联动逻辑放云端,结果一断网整个家就"傻"了。
6. 本地规则引擎:让设备自己"会思考"
6.1 规则的数据结构设计
规则引擎的核心是把"如果……就……"这种逻辑变成可存储、可执行的数据。我一般用这样的结构:
{ "id": "rule_001", "enabled": true, "trigger": { "type": "device_state", "deviceId": "sensor_003", "field": "temperature", "operator": ">", "value": 28 }, "condition": { "type": "time_range", "start": "22:00", "end": "06:00" }, "action": { "type": "device_command", "deviceId": "ac_001", "command": "on", "params": {"mode": "cool", "temp": 26} } }这样一条规则表示:卧室温度超过 28 度,且在夜间时段,就打开空调制冷到 26 度。用 JSON 存,好处是 App 端可以直接编辑,网关端解析执行,云端也能同步。
6.2 触发、条件、动作三段式执行
执行逻辑分三步:先判断触发条件是否满足(温度是否超阈值),再判断附加条件(是否在时间段内),最后执行动作(发指令给空调)。任何一步不满足就跳过。
这里有个性能细节:如果每次设备上报都遍历所有规则,设备一多就会卡。我的优化是给规则建索引,按deviceId分组,设备上报时只检查涉及这个设备的规则。这样即使有几百条规则,单次检查也是毫秒级。
6.3 防抖与去重:避免规则被反复触发
温度在 28 度上下浮动时,规则会被反复触发,空调开开关关,体验极差。解决办法是加滞回(hysteresis):触发阈值和恢复阈值分开。比如超过 28 度开空调,但要降到 26 度以下才认为"事件结束",中间这段不重复触发。
另外还要加时间窗去重:同一条规则在 N 秒内只执行一次。我用的是lastExecutedAt时间戳,执行前检查距上次执行是否超过冷却时间。这两个机制加上,规则引擎就稳了。
7. 云端同步与 App 通信的工程细节
7.1 状态同步:全量还是增量
网关和云端同步状态,有两种策略。全量同步是定期把整个状态表推上去,简单但流量大;增量同步是只推变化的部分,省流量但需要处理丢包和乱序。
我的做法是混合:状态变化时立即推增量,同时每隔几分钟做一次全量校验。这样既保证实时性,又能纠正长时间运行后的状态漂移。增量消息里带上版本号或者时间戳,云端收到旧版本就丢弃,避免乱序覆盖。
7.2 设备发现与配网流程
新设备加入系统,需要一个配网流程。Wi-Fi 设备常见的是 SmartConfig 或者 AP 配网:设备进入配网模式,App 把 Wi-Fi 密码传过去,设备连上后向网关注册。
网关这边要维护一个"待认领设备"列表。设备注册时带上自己的类型和 ID,网关先把它放进待认领区,App 上弹出提示让用户确认"这是客厅的灯",确认后才正式纳入系统。这个确认步骤很重要,否则设备一多你根本分不清哪个是哪个。
7.3 安全:别让智能家居变成"裸奔"
安全这块我必须多说几句。很多 DIY 智能家居系统,MQTT 不设密码、局域网内谁都能发指令,这在真实家庭环境里是灾难。
最低限度的安全措施包括:MQTT 启用用户名密码和 TLS;设备注册时用一次性 token 验证;网关的本地 API 只监听局域网,不暴露到公网;App 和网关之间的通信加密。这些都不难做,但省掉任何一条,你的系统就是个敞开的门。
8. 我在实际项目里踩过的几个典型坑
8.1 内存泄漏:Service 没停导致的 OOM
Android Things 虽然裁剪过,但毕竟还是 Android,内存管理那套东西还在。我有个项目跑了两天就崩,查 log 发现是 OOM。原因是我的设备监听 Service 在设备重连时反复创建,旧的没销毁,每个都持有PeripheralManager的引用。
解决办法是给 Service 加单例控制,重连时复用已有实例,只重新注册回调。另外用LeakCanary在开发阶段监控内存,能提前发现这类问题。嵌入式设备内存本来就紧张,泄漏积累起来比手机崩得还快。
8.2 看门狗与自动恢复:网关死机了怎么办
网关是常供电设备,跑几个月不死机是基本要求。但现实是,再稳的系统也可能因为某个驱动异常卡住。所以必须加看门狗机制。
Android Things 层面,我用了两个手段:一是硬件看门狗(如果开发板支持),定时喂狗,超时自动重启;二是软件层面的心跳检测,主线程定期检查各子线程是否存活,发现卡死就主动重启对应 Service。双保险下来,系统连续跑几个月基本没问题。
8.3 固件升级:OTA 在 Android Things 上的现实
Android Things 官方提供过 OTA 更新机制,但官方服务下线后,这套东西就得自己搭。我的做法是:网关启动时检查一个固定的更新服务器,有新版就下载 APK 静默安装,然后重启应用。
这里要注意的是,升级过程要能回滚。如果新版 APK 启动就崩,设备会陷入"升级-崩溃-再升级"的死循环。所以我在升级前会备份当前版本,新版本启动后如果 N 秒内没上报心跳,就自动回滚到旧版。这个机制救过我好几次。
9. 从 Android Things 迁移到其他平台的思路
如果你看完上面这些,觉得 Android Things 官方支持没了不放心,想迁移到别的平台,其实架构思路是通用的。网关层的职责——协议转换、设备管理、本地规则引擎、云端同步——在任何平台上都一样。
迁移到 Linux(比如树莓派跑 Raspberry Pi OS)是最平滑的,因为 Android Things 本身就是 Linux 内核,你的外设驱动逻辑、MQTT 通信、规则引擎几乎可以照搬,只是把 Java 换成 Python 或者 C++,把PeripheralManager换成gpiod、smbus这些库。
迁移到 STM32 则要重新考虑资源约束,规则引擎得简化,协议栈要裁剪,但"本地优先、分层解耦"这些原则不变。我甚至见过有人把网关拆成两级:STM32 做实时性要求高的设备接入,Android Things 或 Linux 做上层规则和云同步,各取所长。
说到底,Android Things 只是实现智能家居网关的一种手段,真正值钱的是你对这套系统分层的理解。工具会过时,架构思路不会。把上面这些跑通一遍,你换任何平台都能快速搭起来。