學完本章你將能夠:
說明「路徑 C」與路徑 A/B 的差別:喚醒與辨識由手機或音箱代勞, STM32 只收已驗證的指令。
在 STM32 實作閘道協定(文字行下行、JSON 狀態上行),且所有外部 指令仍走同一條 |g_cmd_q| 與安全策略。
用 HomeSpan 讓 Siri 控制燈、風扇與插座,並讓家庭 App 與裝置狀態 雙向同步。
用 fauxmoESP 讓 Alexa 在區網內直接發現裝置;說明 OK Google 為何 建議走 Matter 或 Home Assistant。
比較 ESP32 直橋、Home Assistant 樞紐、Matter 三種整合策略的取捨。
前十二章做出來的產品有自己的耳朵。本章給它第二種入口:使用者對手機說 「嘿 Siri,打開客廳燈」,或對音箱說「Alexa, turn on the light」、 「OK Google,關風扇」,指令穿過各家生態系,最後仍落進我們在第 6 章建好的 那條指令佇列。
路徑 C:辨識搬到生態系那一側
路徑 A/B 的喚醒詞與 ASR 都在裝置端;接上 Siri、Alexa、Google 後,這兩件 事完全由手機或智慧音箱完成,STM32 收到的是「生態系已驗證過的結構化指令」。 差異整理如下:
| 路徑 A(模組) | 路徑 B(Edge AI) | 路徑 C(助理) | |
|---|---|---|---|
| 喚醒+ASR | 外部模組 | STM32 上的 KWS | 手機/音箱/雲端 |
| 需要網路 | 否 | 否 | 是(配網一次) |
| 聆聽窗策略 | 有(5 秒) | 有 | 不適用(生態系已驗證) |
| STM32 收到 | UART 詞句 | 類別+分數 | 閘道文字行指令 |
| 新增程式 | ASR 任務 | 特徵+推論 | Gateway 任務 |
三條路徑在 |g_cmd_q| 匯合。為了讓 Device 任務知道指令從哪來, |cmd_msg_t| 新增了 |src| 欄位(|CMD_SRC_LOCAL| / |CMD_SRC_GATEWAY|),既有程式不受影響——省略欄位的舊初始化預設就是 本地來源。
三種整合策略
| 策略 | 怎麼接 | 適合 |
|---|---|---|
| ESP32 直橋 | HomeSpan(Siri)+ fauxmoESP(Alexa)各一份草稿,UART 接 STM32 | 課堂 demo 最快;無需伺服器 |
| Home Assistant 樞紐 | STM32 經 MQTT(第 11 章)→ HA → HomeKit Bridge/Alexa/Google 整合 | 一台樹莓派同時涵蓋三家 |
| Matter 統一標準 | ESP32 跑 esp-matter,三家 App 直接配對同一裝置 | 產品級;一次認證三家通吃 |
課堂主線用 ESP32 直橋(不需要額外伺服器),並示範 Home Assistant 樞紐;Matter 以導讀方式帶過,作為商品化方向。無論哪一條,STM32 端的程式 完全相同——這正是第 3 章開始堅持設備抽象與指令佇列的回報。
閘道協定:STM32 端
閘道與 STM32 之間走 115200 UART(預設 USART6,見 pin_map.h 的 |GW_UART|;若同時使用路徑 A 模組,改接 USART2 等閒置埠)。協定刻意簡單: 下行是空白分隔的文字行,上行是單行 JSON。
| 方向 | 訊息 | 意義 |
|---|---|---|
| 下行 | CMD LIGHT ON / OFF |
開關燈 |
| 下行 | CMD LIGHT PCT 70 |
絕對亮度(|CMD_LIGHT_SET|) |
| 下行 | CMD FAN ON / OFF / PCT n |
風扇 |
| 下行 | CMD DOOR OPEN / CLOSE |
門(受策略管制) |
| 下行 | CMD RELAY ON / OFF |
插座 |
| 下行 | CMD ALL OFF、CMD BEEP |
全關、蜂鳴 |
| 下行 | GET STATE |
請求立即回報狀態 |
| 下行 | PING |
連線心跳(回 PONG) |
| 上行 | {"light":1,"l_pct":70,...} |
狀態改變、被查詢或每 5 秒 |
選文字行而不是 JSON 下行,是為了讓三種閘道(HomeSpan、fauxmo、MQTT 橋) 都能一行 |printf| 出指令,STM32 也只需 |sscanf|。解析器獨立成 gw_proto.c,不含任何 HAL,可在 PC 上單元測試:
Gateway 任務的工作只有三件:收行、過策略、回狀態。狀態回推靠 Event Group 新增的 |EVT_GW_DIRTY| 位——Device 任務每次 commit 後置位,Gateway 任務 看到就推一行 JSON,另外每 5 秒也推一次當保險:
static void vTaskGateway(void *arg)
char line[GW_LINE_MAX]; int n = 0; uint8_t ch; (void)arg; gw_push_state();
for (;;) if (xEventGroupGetBits(g_evt) & EVT_GW_DIRTY) xEventGroupClearBits(g_evt, EVT_GW_DIRTY); gw_push_state(); /* 本地口令改了狀態,App 也要知道 */ if (HAL_UART_Receive(&GW_UART, &ch, 1, 20) != HAL_OK) continue; /* ... 組行,略 ... */ cmd_msg_t m; switch (gw_parse_line(line, &m)) case GW_LINE_PING: gw_send_line("PONG"); break; case GW_LINE_GET_STATE: gw_push_state(); break; case GW_LINE_CMD: if (!gw_policy_ok(&m)) break; /* 例:門未開放遠端 */ m.tick = xTaskGetTickCount(); (void)xQueueSend(g_cmd_q, &m, 0); break; default: break;
遠端指令不套用本地聆聽窗(生態系已做過喚醒與帳號驗證),但門、 鎖、插座這類高風險輸出要另立政策:教材預設 |APP_GW_ALLOW_DOOR=1| 方便 demo,產品請改為 0 並要求 App 端二次確認。HomeKit 對 Lock 類別原生 要求解鎖確認、Alexa 例程可設 PIN——善用生態系的保護,而不是繞過它。 無論來源為何,指令仍經過過期檢查與去抖,繞過 |g_cmd_q| 直接呼叫 |dev_*| 依然是禁手。
Siri:HomeKit 與 HomeSpan
HomeKit 的裝置模型是三層:Accessory(一台裝置)擁有若干 Service(功能,例如燈泡),Service 擁有若干 Characteristic(可讀寫的屬性,例如開關、亮度)。我們的映射:
| 設備 | Service | Characteristic |
|---|---|---|
| 燈 | LightBulb | On、Brightness(0–100) |
| 風扇 | Fan | Active、RotationSpeed |
| 插座 | Outlet | On、OutletInUse |
| 門 | 教學用 Switch | On(正式品用 LockMechanism) |
HomeSpan 是 ESP32 上的開源 HAP 實作:繼承 Service 類別、覆寫 |update()| 收 Siri 指令、用 |setVal()| 回推狀態即可。
struct GwLight : Service::LightBulb SpanCharacteristic *on, *lvl; GwLight() : Service::LightBulb() on = new Characteristic::On(false); lvl = new Characteristic::Brightness(60); lvl->setRange(0, 100, 1); boolean update() override /* Siri/Home App 下指令 */ if (!on->getNewVal()) gwSend("CMD LIGHT OFF"); else gwSendf("CMD LIGHT PCT return true; ;
void setup() Serial2.begin(115200, SERIAL_8N1, 16, 17); /* 接 STM32 */ homeSpan.setPairingCode("11122333"); /* 課堂務必改掉 */ homeSpan.begin(Category::Bridges, "VoiceHome Bridge"); /* Accessory + AccessoryInformation + GwLight/GwFan/... 見完整檔 */ gwSend("GET STATE");
STM32 每次狀態變更推回的 JSON,由 |loop()| 裡的 |gwPoll()| 解析後 |setVal()|——於是你對板子說「guan-deng」(路徑 A),手機的家庭 App 一秒內同步變暗。這個雙向同步是驗收重點,不是選配。
自製配件在 iOS 會顯示「未認證配件」,課堂與自用可直接忽略;要上架販售 才需要 Works with Apple Home 認證。配對用 8 位數設定碼即可,不需要 MFi 晶片。
Alexa:fauxmoESP 本地模擬與正式技能
最快的課堂做法是 fauxmoESP:ESP32 在區網內模擬 Philips Hue 裝置,Echo 音箱用「Alexa, discover devices」即可找到,開關與亮度(0–255 映射到 0–100%)都能控,完全不碰雲端開發者帳號。
fauxmo.addDevice("living room light"); fauxmo.addDevice("living room fan"); fauxmo.addDevice("smart plug");
fauxmo.onSetState([](unsigned char id, const char *name, bool state, unsigned char value) int pct = (int)value * 100 / 255; if (strcmp(name, "living room light") == 0) state ? gwSendf("CMD LIGHT PCT : gwSend("CMD LIGHT OFF"); else if (strcmp(name, "living room fan") == 0) state ? gwSendf("CMD FAN PCT : gwSend("CMD FAN OFF"); else gwSend(state ? "CMD RELAY ON" : "CMD RELAY OFF"); );
Alexa 智慧家庭對中文裝置名支援有限,裝置名請用英文;Siri 則可直接叫 「客廳燈」。Google Home 的房間+裝置命名在 App 內設定,與韌體無關。
fauxmo 的限制:只有「開關+亮度」語意、狀態回查支援有限、依賴區網發現 (部分新款 Echo 韌體會挑剔)。正式產品走 Smart Home Skill (Lambda + OAuth 帳號連結 + AWS IoT)或直接上 Matter。
OK Google:走 Matter 或 Home Assistant
Google 的自建雲端路線(Smart Home Action + fulfillment 伺服器 + HomeGraph)需要 OAuth 伺服器與 GCP 專案,超出課堂時數。實務上二選一:
Matter:Google Nest 音箱/顯示器是 Matter 控制器, 「OK Google,打開電燈」直接可用(見下節)。
Home Assistant:HA 的 Google Assistant 整合(自行設定 GCP 或訂閱 Nabu Casa 雲端)把 HA 實體暴露給 Google Home。
Matter:一次接好三家
Matter 是 CSA(連接標準聯盟)主導的智慧家庭標準,Apple Home、Alexa、 Google Home 都是控制器。裝置用標準 device type(On/Off Light、 Dimmable Light、Fan、On/Off Plug-in Unit……)描述自己,配對一次 QR code, 三家 App 都能加入同一顆裝置(multi-admin)。
對本專案,最務實的做法是 ESP32-C3/S3 跑 esp-matter SDK:把官方 light 範例的 attribute 回呼改成送 CMD 行給 STM32,狀態回推則更新 Matter attribute。Wi-Fi 版最簡單;Thread 版需要邊界路由器(HomePod mini、 Nest Hub 等)。細節與骨架見 firmware/lab12_assistant_gateway/esp32/matter_bridge.md。
2026 年的現實:DIY 裝置可用測試憑證在自家配對三家生態系;要出貨才需要 CSA 認證與正式 DAC 憑證。教學階段 Matter 的價值在於理解「device type = 標準化的 |home_state_t|」——你會發現它的 OnOff/Level cluster 與我們的 狀態機幾乎一一對應。
Home Assistant 樞紐:接第 11 章的 MQTT
若實驗室有樹莓派,Home Assistant 是一次涵蓋三家的樞紐:STM32 的 JSON 遙測經 ESP32 的 MQTT–UART 橋(esp32/mqtt_uart_bridge/,約 80 行)上到 broker,HA 用 MQTT 實體接進來,再由 HomeKit Bridge 整合暴露給 Siri、Alexa/Google 整合暴露給另外兩家。下行命令直接發布閘道文字行, 橋接器原樣轉進 UART——閘道零解析。
mqtt: light: - name: "客廳燈" state_topic: "home/living/voice/state" state_value_template: " ’ON’ if value_json.light == 1 else ’OFF’ " command_topic: "home/living/voice/cmd" payload_on: "CMD LIGHT ON" payload_off: "CMD LIGHT OFF" brightness_state_topic: "home/living/voice/state" brightness_value_template: " value_json.l_pct " brightness_command_topic: "home/living/voice/cmd" brightness_command_template: "CMD LIGHT PCT value " brightness_scale: 100
homekit: # 暴露給 Siri/家庭 App filter: include_domains: [light, fan, switch]
實驗:lab12 助理閘道
firmware/lab12_assistant_gateway/:STM32 端 app_main.c + gw_proto.c(含 PC 單元測試);ESP32 端三份草稿 homespan_siri、fauxmo_alexa、mqtt_uart_bridge 與 homeassistant/configuration.yaml。接線:ESP32 GPIO16/17 \(\leftrightarrow\) STM32 USART6 TX/RX、共地。
驗收腳本:
「嘿 Siri,打開客廳燈」→ 燈亮,VCP 印出
src=gateway。在 VCP 打
xiao-ke、guan-deng(本地路徑)→ 燈滅, 且家庭 App 內的燈同步變為關閉。「Alexa, set living room fan to fifty percent」→ 風扇 50%。
拔掉 ESP32 → 本地口令照常運作;插回後 App 於 5 秒內恢復同步。
|APP_GW_ALLOW_DOOR=0| 重編 → Siri 開門被拒,log 顯示
gw: door blocked。
三個進階練習:(1) 在 ESP32 每 3 秒送 PING,逾時 10 秒未見 PONG 就把配件標為離線(HomeSpan 可回報 not-responding)。 (2) 為遠端指令加上每來源 400 ms 去抖,防止 App 拖曳亮度滑桿灌爆佇列。 (3) 把 Gateway 任務併入 voice_home_final:加入 gw_proto.c 與 |vTaskGateway|,優先權 |PRIO_GATEWAY|, 再跑一次第 12 章的看門狗與睡眠驗收——睡眠期間 UART 喚醒是否正常?
全書終點
回頭看:同一顆 STM32,耳朵可以是離線模組、板上神經網路,或全球最大的 三個語音生態系;但設備狀態機、指令佇列、看門狗與 OTA 從第 6 章之後就沒 改過。這就是架構的意義——入口會一直換,骨架不必重寫。