资讯动态

ESP32接入Alexa:Smart Home Skill与AWS IoT Core实战指南

发布时间:2026/8/26 1:50:09 来源:尧图企业网站定制
如果你自己做过一阵子智能家居大概有过和我一样的体验手里一堆 ESP32、继电器、红外发射器、温湿度传感器都能在本地网页或手机 App 里控制但每天出门后想远程关个灯还得掏出 App、走二级菜单。家里放着 Echo却始终没法用“Alexa, turn off the light”这种最自然的指令控制自己做的东西。后来我认真把 Amazon Alexa 和自定义 IoT 设备的对接链路走了一遍才发现这条路没有想象中复杂关键在于选对接口协议以及把“Alexa 云 - 你的后端 - 设备”这条链路里每一步都打通。这篇文章我会从技术路线选型开始讲重点讲怎么用 Smart Home Skill 配合 AWS IoT Core 控制一个 ESP32 继电器再补上设备发现、状态同步、OTA 和安全策略这些容易踩坑的点。适合有 Arduino 基础、想把手头 DIY 设备接入 Alexa 的玩家也适合没接触过 AWS IoT 但想快速上手的开发者。1. 先分清三种接入路线别一开始就被“技能”两个字带偏1.1 三条路线对比Alexa 控制自定义设备并不是只有一种做法。官方文档里把几种方式分得很细但落到 DIY 场景我理解下来就是三条路线路线工作方式适合场景开发成本典型延迟Smart Home Skill API语音指令到 Alexa 云再通过 Lambda 或 HTTPS 后端转发到设备开关、灯、插座、温控器等标准智能家居设备中取决于云端和网络普通 1~2 秒Custom Skill 自定义技能你设计一套交互语句和意图由技能后端处理任意自定义命令非标设备如“让设备播报”“启动某个传感器采集流程”高一些要设计 interaction model同样走云端Alexa Gadgets ToolkitEcho 通过蓝牙 BLE 直接连接设备设备作为附属配件接收指令桌面氛围灯、语音提醒硬件、需要低延迟本地交互的设备中且对 Echo 型号和蓝牙有要求很低本地链路Smart Home Skill 和 Custom Skill 都是云上语音链路区别是前者偏向“标准设备控制”后者偏向“自定义意图”。Alexa Gadgets Toolkit 则完全不一样它走本地蓝牙适合那种必须在设备上快速响应、不依赖网络的场景。1.2 为什么大多数 DIY 项目都选 Smart Home Skill我最初以为是 Custom Skill 更灵活后来实际做下来发现只要是控制“开关”这件事Smart Home Skill 的优势非常明显。Alexa 天然理解“turn on”“turn off”“set brightness”这类语句不需要你定义一遍。你把设备能力声明成Alexa.PowerController用户只要说“Alexa, turn on desk lamp”Alexa 就知道要调用你后端的TurnOn指令。设备还能被 Alexa App 里的“智能场景”引用比如“回家模式”里把你自制的灯打开。Custom Skill 虽然能做到“Alexa, ask my gadget to start xxx”这种自定义指令但它交互起来不自然而且无法被 Alexa 的日常自动化、定时场景无缝接纳。如果你想做的是一个真正能融入家庭环境的设备Smart Home Skill 基本是唯一合理选择。2. Smart Home Skill 接入的前置准备账号、权限与设备模型2.1 需要提前准备的东西在开始写代码之前先把下面这些账号和环境准备好一个 AWS 账号后续会用 Lambda 和 AWS IoT Core一个 Amazon Developer Console 账号用来创建 Alexa 技能一台 Echo 设备或者手机上的 Alexa App用于最终语音测试ESP32 或 ESP8266 开发板继电器模块、电源、照明设备或其它要控制的负载设备能正常访问 AWS IoT Core 的 443 端口MQTT 走加密通道这里有个很实际的建议AWS 区域最好一开始就固定下来。Lambda、AWS IoT Core、IAM Role 都放在同一个区域后续排查问题时不会因为跨区域权限而白白浪费时间。我自己习惯用us-east-1原因不是性能而是资料最多、排错最方便。2.2 把设备“类型”定义清楚Smart Home Skill 要求设备符合 Alexa 能理解的模型。你手里的是一个继电器控制的台灯那它就是一个“LIGHT”或者“SMARTPLUG”如果是个风扇那就是“FAN”如果是个温湿度传感器那就要用Alexa.TemperatureSensor和Alexa.HumiditySensor能力。当时我给自己的继电器设备定的是endpointId:relay-001friendlyName:Desk LampdisplayCategories:LIGHT能力集合Alexa.PowerControllerAlexa.EndpointHealthendpointId是设备在 Alexa 侧的身份证必须稳定不变。你可以在本地代码里把它对应到设备名称但不要每次 Discovery 都换一个 ID否则 Alexa App 里会出现一个旧的“无响应”设备。2.3 创建 Lambda 和 IAM 角色时最容易忽略的点Lambda 是 Alexa 和 AWS IoT Core 之间的中转站。创建一个 Python 3.9 运行时的 Lambda 函数给它的 IAM Role 至少加上 CloudWatch Logs 权限和 AWS IoT Data Plane 的发布权限。Lambda 要调用 AWS IoT Core 发消息需要类似下面的策略{ Version: 2012-10-17, Statement: [ { Effect: Allow, Action: [ iot:Publish, iot:GetThingShadow, iot:UpdateThingShadow ], Resource: * } ] }如果是自己做Resource 写*问题不大。但如果你打算长期用尽量收敛到具体 topic 和 thing。至于 CloudWatch Logs 权限直接用AWSLambdaBasicExecutionRole这个托管策略即可缺少它你会什么都看不到。3. 从 Alexa 到设备动作的核心链路Discovery 与 PowerController3.1 Alexa 发现设备Discovery 请求与响应当用户在 Alexa App 里点击“Discover devices”时Alexa 会向你配置的 Lambda 发一条Alexa.Discovery请求。你的后端需要返回设备列表Alexa 才会把这些设备显示出来。这段代码非常关键我当初就是因为payloadVersion写错导致发现不到任何设备。import json import boto3 iot_data boto3.client(iot-data, region_nameus-east-1) def discovery_response(): return { event: { header: { namespace: Alexa.Discovery, name: Discover.Response, messageId: uuid, payloadVersion: 3 }, payload: { endpoints: [ { endpointId: relay-001, friendlyName: Desk Lamp, description: ESP32 relay lamp, displayCategories: [LIGHT], capabilities: [ { type: AlexaInterface, interface: Alexa.PowerController, version: 3, properties: { supported: [ {name: powerState} ], proactivelyReported: False } } ] } ] } } }注意proactivelyReported先设成False后面再决定要不要做状态主动上报。它决定了 Alexa 是否需要额外处理设备状态变化事件。Discovery 返回成功后Alexa 会缓存这个设备。之后你修改了设备名称或能力需要重新触发一次 Discovery 才能让 Alexa 更新缓存。实际操作中往往还要在 Alexa App 里删除旧设备否则会出现一堆幽灵设备。3.2 执行开关指令处理 PowerController 请求当用户说“Alexa, turn on desk lamp”时Alexa 会发一条Alexa.PowerController.TurnOn指令到 Lambda。你需要在 handler 里判断namespace和name然后去操作设备。def power_controller_response(directive): endpoint_id directive[endpoint][endpointId] header_name directive[header][name] power_state ON if header_name TurnOn else OFF iot_data.publish( topicfdevice/{endpoint_id}/cmd, payloadjson.dumps({ action: powerState, value: power_state }) ) return { event: { header: { namespace: Alexa, name: Response, messageId: uuid, payloadVersion: 3, correlationToken: directive[header].get(correlationToken) }, endpoint: { scope: { type: BearerToken, token: directive[endpoint][scope][token] }, endpointId: endpoint_id }, payload: {} } }在真正的项目里powerState应该从命令发出后的设备状态反查并放进context返回给 Alexa否则 Alexa App 可能显示旧状态。我们后面会讲设备状态同步。3.3 把 Lambda 接到 Alexa Developer Console在 Developer Console 创建一个 Smart Home Skill然后在“Endpoint”里填写 Lambda 的 ARN。这里最容易踩坑的是选错区域Lambda 在哪里建的ARN 的 region 段就要和技能配置里的区域一致。Smart Home 技能需要配置 Account Linking简单说就是让 Alexa 云替换你的“用户身份”。你可以在 Developer Console 里填一个 OAuth 服务地址也可以自用一个非常简单的固定 token 后端。DIY 阶段不需要做复杂登录只要保证 Alexa 请求头里的 token 你能在 CloudWatch 日志里看到即可。配置完成后在 Alexa App 里启用这个技能再点“Discover devices”就能看到Desk Lamp。如果看不到优先查 Lambda 的 CloudWatch Logs看 Discovery 请求是否到了你的代码。4. ESP32 设备端接入 AWS IoT Core从订阅 MQTT 到控制继电器4.1 硬件连接与基础固件结构我这里使用的是一块 ESP32 开发板和一个 5V 继电器模块。ESP32 的某个 GPIO 接继电器 IN 脚继电器 COM 接电源火线NO 接灯具。注意继电器模块如果是高电平触发GPIO 输出高电平就会吸合。固件端整体结构非常简单连接 Wi-Fi用WiFiClientSecure 设备证书连接 AWS IoT Core订阅device/relay-001/cmd这个 topic收到 MQTT 消息后解析 JSON控制 GPIO状态变化后把设备状态上报到 AWS IoT Shadow4.2 MQTT 主题设计和设备证书配置我采用的是标准做法指令 topic 用device/{thingName}/cmd状态上报直接走 AWS IoT Shadow 预置的$aws/things/{thingName}/shadow/updatetopic。为什么要用 Shadow 而不是自己再定义一个状态 topic因为 AWS IoT Shadow 天然具备“云上状态存储”能力。Lambda 后来收到 Alexa 的ReportState请求时可以直接调用get_thing_shadow拿设备最新状态不需要自己维护数据库。设备端固件核心片段如下#include WiFi.h #include WiFiClientSecure.h #include PubSubClient.h #include ArduinoJson.h const char* WIFI_SSID your-ssid; const char* WIFI_PASS your-password; const char* AWS_IOT_ENDPOINT xxxxxxxx-ats.iot.us-east-1.amazonaws.com; const char* THING_NAME relay-001; #define RELAY_PIN 4 WiFiClientSecure net; PubSubClient mqtt(net); const char* CMD_TOPIC device/relay-001/cmd; unsigned long lastReconnectAt 0; void callback(char* topic, byte* payload, unsigned int length) { payload[length] 0; StaticJsonDocument128 doc; deserializeJson(doc, (char*)payload); if (strcmp(doc[action], powerState) 0) { bool on strcmp(doc[value], ON) 0; digitalWrite(RELAY_PIN, on ? HIGH : LOW); StaticJsonDocument128 shadow; shadow[state][reported][powerState] on ? ON : OFF; char buffer[128]; serializeJson(shadow, buffer); mqtt.publish($aws/things/relay-001/shadow/update, buffer); } } void connectAWS() { net.setCACert(rootCA); net.setCertificate(clientCert); net.setPrivateKey(privateKey); mqtt.setServer(AWS_IOT_ENDPOINT, 8883); mqtt.setCallback(callback); while (!mqtt.connected()) { if (mqtt.connect(THING_NAME)) { mqtt.subscribe(CMD_TOPIC); } else { delay(3000); } } } void setup() { pinMode(RELAY_PIN, OUTPUT); digitalWrite(RELAY_PIN, LOW); Serial.begin(115200); WiFi.begin(WIFI_SSID, WIFI_PASS); while (WiFi.status() ! WL_CONNECTED) { delay(500); } connectAWS(); } void loop() { if (!mqtt.connected()) { connectAWS(); } mqtt.loop(); }这里的rootCA、clientCert、privateKey是设备证书和 AWS IoT 根证书。把证书以字符串常量形式直接放进固件对于个人项目来说是可以接受的但如果担心固件被反编译后续应该换 Secure Element 或使用 Greengrass 设备端方案那是更深的话题了。AWS IoT 的设备策略需要覆盖Connect、Subscribe、Receive三个维度。尤其要注意 Shadow topic 的权限不能漏{ Version: 2012-10-17, Statement: [ { Effect: Allow, Action: iot:Connect, Resource: arn:aws:iot:us-east-1:123456789012:client/relay-001 }, { Effect: Allow, Action: [iot:Subscribe, iot:Receive], Resource: [ arn:aws:iot:us-east-1:123456789012:topicfilter/device/relay-001/cmd, arn:aws:iot:us-east-1:123456789012:topicfilter/$aws/things/relay-001/shadow/update/accepted, arn:aws:iot:us-east-1:123456789012:topicfilter/$aws/things/relay-001/shadow/update/rejected ] }, { Effect: Allow, Action: [ iot:Publish, iot:UpdateThingShadow, iot:GetThingShadow ], Resource: [ arn:aws:iot:us-east-1:123456789012:topic/device/relay-001/cmd, arn:aws:iot:us-east-1:123456789012:topic/$aws/things/relay-001/shadow/update ] } ] }很多人在设备端卡住问题并不是代码写错而是证书没有和 Thing 绑定或者 Policy 里没有允许Connect。你可以在 AWS IoT Console 的 Test 页直接向device/relay-001/cmd发布一条 JSON如果设备没反应先别碰 Alexa大概率是设备侧 MQTT 链路本身有问题。4.3 设备状态同步让 Alexa 知道灯是开还是关Alexa 要查询 (ReportState) 时Lambda 会收到Alexa.ReportState指令。你要在 Lambda 里读一次设备 Shadow返回设备最新状态。def report_state(directive): endpoint_id directive[endpoint][endpointId] response iot_data.get_thing_shadow(thingNameendpoint_id) payload json.loads(response[payload].read()) reported payload.get(state, {}).get(reported, {}) power_state reported.get(powerState, UNKNOWN) return { context: { properties: [ { namespace: Alexa.PowerController, name: powerState, value: power_state, timeOfSample: 2025-01-01T00:00:00.000Z, uncertaintyInMilliseconds: 500 } ] }, event: { header: { namespace: Alexa, name: StateReport, messageId: uuid, payloadVersion: 3, correlationToken: directive[header].get(correlationToken) }, endpoint: { scope: { type: BearerToken, token: directive[endpoint][scope][token] }, endpointId: endpoint_id }, payload: {} } }这里有一个容易被忽视的细节ReportState响应里的context不仅用于状态查询还会被用来做语音应答的最终状态确认。如果你在TurnOn的响应里不带contextAlexa 可能回答说“好的”但 App 里的状态仍然停留在旧值。5. 实测中我被 Alexa 卡住的三次“无响应”完整排查链路5.1 现象一设备发现不了第一次启用技能后我在 Alexa App 里点 Discover devices等了一分钟都没有新设备。当时我第一反应是 Lambda 权限有问题但打开 CloudWatch 日志后发现Lambda 根本没有执行。原因很蠢我在 Developer Console 里填的 Lambda ARN 是旧函数的而旧函数已经被我删掉了只保留了一个新函数。虽然都是同一个技能但 ARN 填错或函数被删Alexa 请求就会直接失败。如果你也遇到同样情况按这个顺序排查在 Developer Console 技能配置页确认 Lambda ARN 和区域没填错在 CloudWatch Logs 里筛选Discover记录看有没有请求进来在 Lambda 测试面板手动构造一个 Discovery 请求确认返回报文没有 JSON 格式错误检查payloadVersion是否等于3在 Alexa App 里关闭技能重新启用再触发一次 Discover5.2 现象二指令下发后设备无动作但 Alexa 说已执行这是最诡异的情况。Alexa 语音已经回应“OK”但灯完全没有反应。此时直接去 AWS IoT Core 的 Test 页面向device/relay-001/cmd发布同样的 JSON。如果设备端也没有反应问题一定在设备侧比如连接断开、证书不对、GPIO 接错。如果设备端有反应那问题就出在 Lambda 的 MQTT publish 环节重点查 IAM 策略是否包含iot:Publish以及topic名称是否大小写匹配。事后我想了一下为什么 Alexa 会反馈成功因为 Smart Home Skill 的响应只要是一个合法的Alexa.ResponseAlexa 就会认为控制指令已经执行完。如果你在 Lambda 里接口调试还没完全打通就直接返回成功设备的真实状态就会和语音反馈脱节。所以我会建议在 Lambda 的power_controller_response里先把 publish 动作执行成功再返回响应至少加一层 try exceptpublish 失败时返回Alexa.ErrorResponse这样语音端会明确告诉你设备不可控。5.3 现象三设备状态在 App 里一直显示旧状态当我用手动按钮把灯打开后Alexa App 里仍然显示“关”。这是因为设备的 Shadow 状态没有被更新。如果你只是用 Alexa 语音控制状态第一次是准的手动改变设备状态后你必须让设备自己上报。对 ESP32 来说就是在 GPIO 电平变化时向 Shadow update topic 发布最新状态shadow[state][reported][powerState] digitalRead(RELAY_PIN) ? ON : OFF;不是只有语音控制才需要状态同步。Alexa 的定时任务、智能场景、App 首页展示都会调用ReportState如果这些位置状态不准你会觉得系统像“半残废”。6. 再深入一点设备安全、OTA 升级与自定义语音指令6.1 给设备做 OTA 升级时要注意的 AWS IoT 策略设备一旦接入 AWS IoT Core固件升级就是绕不开的事。尤其当你控制的是灯具、插座这类长期通电设备总不能每次改 bug 都拆壳接串口线。AWS IoT 的 OTA 能力依赖 IoT Jobs。设备需要能访问 Jobs 相关的 MQTT topic并响应 Job 执行。设备策略里至少要有iot:DescribeJobExecutioniot:UpdateJobExecutioniot:StartNextPendingJobExecution同时固件包通常放在 S3 上设备需要具备从 S3 下载的权限。不要直接把明文固件放在公开桶里应该用 IoT 预签名 URL 配合格式校验确保下载内容确实是可信固件。这些策略一开始听着繁琐但如果你在设备第一次接入时就把 policy 设计成“按 Thing 收敛”后面加 OTA 权限只是追加几条 statement不需要重做架构。6.2 想用自定义语句控制Custom Skill / AGT 路线如果你已经跑通了 Smart Home Skill后面还想加自定义指令比如“Alexa, tell my gadget to take a photo”那就需要 Custom Skill。Custom Skill 要设计 interaction model把“take a photo”这类短语映射到某个 intent。Alexa 识别 intent 后同样会把请求发到 Lambda。你在同一个 Lambda 里可以同时写两套 handler一部分处理 Smart Home 指令一部分处理 Custom Skill intent复用同一个发布 MQTT 的逻辑。还有一种偏硬核的玩法是 Alexa Gadgets Toolkit。Echo 设备通过 BLE 直接连接你的硬件设备端运行 Alexa 提供的 Gadgets SDK可以接收本地音频、动画和自定义指令。这个方案更适合桌面场景延迟很低但要求 Echo 支持蓝牙而且设备不能离 Echo 太远。如果你的项目是氛围灯、语音提醒器这类本地交互硬件很值得试一下。最后分享一个小技巧固定好你的endpointId并且设备改名时把 friendlyName 一起改。Alexa 对设备缓存很“顽固”改 ID 或者频繁变更能力集合会让 App 里留下一个永远无响应的旧设备。我踩了几次坑后现在所有 DIY 设备的endpointId都直接写在固件配置文件里从设计之初就禁止自动生成省掉了后面一大半排错时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价