🔑 關鍵洞察
✦ AI·GEN作者家裡的吸頂燈是搬進來就有的,用的是一顆來路不明、沒有型號的自發電無線開關。為了讓 Home Assistant 控制它,作者先用 Broadlink RM4 Pro 試了一整個通宵:868MHz 的「學到了」是雜訊、裝置回報的 433.46MHz 是掃頻器閒置時的停駐值、自己寫的工具還少了一整個頻率欄位,最後確認 Broadlink 的射頻學習是只認特定協定的解調器,收不到這種開關。改用 NT$220 的 ESP32-C3 加 CC1101 後,真正的關鍵是 symbol_rate:設錯時錄到的假碼變異係數只有 7.3%,比真碼的 9.4% 還低,讓作者學到「一致」不等於「正確」。文末記錄三週後的後遺症:restore_mode 預設值讓每次重開都切換燈、實體開關隔牆收不到,以及 WiFi 每天斷 17 次的元兇其實是伺服器背後的 USB 孔。
我房間的燈是搬進來就有的,牆上配的是一顆沒有電線的無線開關。誰裝的、哪裡買的、什麼牌子,我一概不知道。
家裡的冷氣、空氣清淨機陸續接進 Home Assistant 之後,我很自然地想:燈應該也行吧?手上剛好有一台為了冷氣買的 Broadlink RM4 Pro,規格上寫著支援 433MHz 射頻學習,我以為錄一下就好。
結果前前後後錄了兩天,牆上那顆開關被我按了幾百次,按到我都怕它先壞掉。RM4 Pro 從頭到尾沒錄到過一次,最後是靠一塊 NT$220 的 ESP32-C3 加 CC1101 才搞定。中間還踩了一個很陰險的坑:我錄到一組統計上看起來完美的碼,燒進去之後燈完全不理我。
這篇把整條路走一遍:這種開關到底是什麼、為什麼 RM4 Pro 錄不到、CC1101 怎麼接怎麼設定,以及錄波時最容易騙到自己的地方。如果你家也是這種開關,後半段的設定可以直接抄,碼換成你自己錄的就好。
先搞懂這顆開關#
拆下來一看,開關背面什麼都沒有,沒有火線,也沒有電池。它是一顆,按下去的那一下會發電,剛好夠它送出一組無線訊號。按的時候會「喀、喀」兩聲,按下一次、彈起一次。
真正切斷電源的是吸頂燈旁邊一顆後裝的小盒子,叫。上面只有簡體字的「進線/出線」端子、一顆鑰匙形狀的按鈕和一顆藍色 LED,一樣沒有任何品牌或型號。
換智慧燈泡這條路一開始就是死的,燈具是舞光的 24W 吸頂燈,光源是 24 顆焊死的 SMD LED,沒有燈泡可以換。所以要讓 HA 控制它,只剩一個辦法:假裝自己是那顆開關,發一模一樣的訊號給通斷器。
問題是我連它用什麼頻率、什麼格式都不知道。
RM4 Pro:冷氣很順,燈完全不行#
冷氣那邊確實很順。錄一組紅外線碼,拿去比對 SmartIR 的碼庫,命中一組 Hisense 的碼,171 個 bit 有 93% 相同,冷氣很快就進了 HA。所以我對燈的期待很高。
然後花了一整個通宵,得到的結論是 RM4 Pro 收不到這顆開關。這中間錯得最離譜的幾次值得記一下,因為每一次當下看起來都像進展。
868 MHz 的「學到了」#
RM4 Pro 的射頻學習第一步要長按遙控器讓它掃頻,但自發電開關按一下只發幾毫秒,根本沒辦法長按。翻 python-broadlink 的原始碼發現 find_rf_packet() 可以直接指定頻率跳過掃頻,我就寫了支小工具,在 433.92、315、868 MHz 輪流監聽。
第一次跑就顯示在 868.35 MHz 學到 114 bytes。我高興了大概十秒,然後發現封包類型是 0x00(合法的只有紅外線 0x26、433 的 0xb2、315 的 0xd7),脈衝寬度有 15 種、從 131µs 跨到 49260µs。RM4 Pro 根本不支援 868MHz,你指定一個它不支援的頻率,它不會報錯,會直接把雜訊吐給你。
裝置自己回報的頻率也不能信#
改用 RM4 Pro 自己掃頻,它很有自信地回報 433.460 MHz。我信了,把工具全改成用 433.46 重錄重發,又跑了好幾輪。
後來想找個對照組,拿家裡 T100 音響的遙控器去掃,它也很有自信地回報了 305.000 MHz。那支遙控器是紅外線的。
把掃頻的原始回應 dump 出來才看懂:
check_frequency 原始回應:00 68 a7 04 00
↑ └─────────┘
│ 0x0004a768 = 305000 kHz
└─ is_found = 0(其實沒找到)第一個位元組是 0,代表根本沒找到,305 MHz 只是掃頻器閒置時停的位置。前面那個 433.46 很可能也是同一回事,我拿一個假數字推論了好幾輪。
通斷器長按十秒會刪光配對#
既然錄不到,我換個方向:讓通斷器進入學習模式,改成讓它來學 RM4 Pro 發的新碼。但沒有型號就沒有說明書,只能猜。
按一下鑰匙形按鈕,燈關了,原來那只是手動開關。改成長按,我想等 LED 開始閃,結果它閃了一下、兩下、三下、四下,然後牆上的開關就失效了。後來找到一張同類通斷器的說明書才知道發生什麼事:
WARNING
這類沒品牌的通斷器,常見的操作是:長按 3 秒閃 1 次進學習、長按 10 秒閃 4 次刪除全部配對。我當時還很小心地避開網路上說的「連按 8 下會清除」,結果是被長按刪光的。
刪光了也不用慌,長按 3 秒等它閃一下,放手,再按一下牆上的開關,配對就回來了。
拆開開關#
猜不到就拆。
板子上有四顆整流二極體、一顆升壓電感、一顆儲能用的鉭質電容,底下那圈銅線圈就是發電機,左邊蛇行的銅箔是 PCB 天線。最有用的是那顆晶振:
Y1 = 26.2982 MHz
26.2982 × 16.5 = 433.9203 MHz頻率是 433.92,從硬體上確認了,433.46 可以丟掉了。可惜旁邊那顆主晶片沒有印字,從晶振值推,大概是華普微 CMT21xx 那一族。這類晶片的是模組廠燒進 EEPROM 的,0.5 到 40 kbps 都有可能,datasheet 不會告訴你這顆設成多少。
我的工具本身也是壞的#
到後半夜,頻率、脈衝寬度、封包類型、極性能掃的都掃了,全部失敗。回頭檢查才發現,有好幾輪根本是我自己的工具在扯後腿:
pulses_to_data預設的時間單位是 32.84µs,但官方文件寫的是 2⁻¹⁵ 秒,也就是 30.52µs,差了 7.7%。- 換算時用無條件捨去,25µs 會被捨成 0,而
0x00在 Broadlink 的格式裡是跳脫字元,整包資料從那裡開始錯位。 - 射頻封包其實要多 4 個位元組。我在 python-broadlink 的 #778 找到別人貼出來能用的封包,開頭標著
b1c0代表 RF 433MHz,後面除了長度還多了00 9f 06 00,用小端序換算剛好是 433920,也就是 433.92MHz。我一整晚發的都少了這段。 - 最後補自我測試時才抓到,我在改寫另一個指令時,把最接近正確答案的那個發射指令整段刪掉了,它從來沒有用正確的封包測過。
全部修好、17 項測試都過了之後再試一次,燈還是沒動。
為什麼收不到#
最後把事情解釋清楚的是兩個 issue。python-broadlink 的 #739 裡有人說得很直接:Broadlink 能理解的「射頻」只是 433 和 315 MHz 上的某些特定協定,其他協定你可能要用 Flipper Zero 之類的東西。也就是說,它的紅外線學習是真的在錄脈衝,射頻學習卻是韌體裡的協定解調器,不認得的格式就收不到。
rtl_433 的 #1790「Help decoding Kinetic light switch」開頭就是我的症狀:發問者試過 Sonoff RF bridge 和 Broadlink RM,兩個都收不到,改用 RTL-SDR 才看到訊號。
原廠 App 我也試過,一開始偶爾錄到一次,錄到的碼按了燈也不動,後來試很久乾脆錄不到。
我自己的工具、原廠 App、別人的經驗都是零,到這裡我才甘願承認 RM4 Pro 這條路走不通。老實說最浪費時間的不是技術本身,是那幾輪我拿著假數字一直往下推,沒先停下來確認數字是不是真的。
433MHz 只是頻段#
我一開始最想不通的是:433MHz 不是很常見嗎?鐵捲門、車鑰匙、無線門鈴都是,一台號稱支援 433 的設備怎麼會錄不到?
後來才理解「433MHz」只說明了最外面那一層:
| 這顆開關 | RM4 Pro | |
|---|---|---|
| 載波頻率 | 433.92 MHz | ✅ |
| 調變 | ✅ | |
| 編碼格式與時序 | 最短的脈衝約 37µs | ❌ |
就像 WiFi 和藍牙「都是 2.4GHz」一樣,頻率對了只代表兩邊在同一個頻道上,對方講什麼格式、講多快是另一回事。RM4 Pro 收的那端只認它韌體裡內建的協定,這顆開關不在裡面。發的那端理論上勉強摸得到,但我用格式驗證過的封包試到最後都沒成功,手上也沒有儀器能確認它到底有沒有發出去。
要知道真相,得有個能直接看到原始波形的東西。
選硬體#
我考慮過的選項(價格是 2026 年 10 月查的):
| 方案 | 價格 | 我為什麼沒選 |
|---|---|---|
| Shelly 1PM Mini 串在通斷器上游 ×2 | 台灣少見,官網一顆約 €15 | 要爬天花板接兩次市電,我沒碰過市電接線 |
| SwitchBot 開關機器人 ×2 | 兩入約 NT$2,370 | 要黏牆、要換電池 |
| LILYGO T-Embed CC1101 | NT$3,400 到 4,200 | 貴,螢幕、NFC、紅外線對我都用不到 |
| Flipper Zero | 台灣沒有正式通路,官網 US$169 | 能抓能發,但不能常駐進 HA |
| RTL-SDR Blog V4 | NT$3,000 到 3,700 | 只能收,不能發 |
| 蝦皮上的 USB CC1101 棒 | NT$291 | 晶片其實是閹割版的 CC110L |
TIP
想買 RTL-SDR 的話,蝦皮上標「V4」卻只賣一千出頭、寫著 R860 的,很可能是山寨品,正品 V4 用的調諧器是 R828D。
最後選了最便宜的:裸的 模組接一塊 ESP32-C3 SuperMini,用 寫韌體。它唯一的缺點是醜,兩塊裸板加八條杜邦線。
CC1101 433MHz 模組(附 SMA 天線) $110
ESP32-C3 SuperMini $90
母對母杜邦線 $20
──────────────────────────────────────
$220,免運選 C3 是因為它有 WiFi,而這個用途只要 6 支腳,S3 太大材小用。醜的問題是同事解決的,他有 3D 印表機,零件到了之後幫我焊好,殼也是他印的。
殼印了五個版本。網路上找得到 ESP32-S3 SuperMini 加 CC1101 的殼,也有一般 ESP32 開發板的,偏偏就是沒有 C3 SuperMini 加 CC1101 這個組合,只能自己量、自己改,一版一版印到裝得進去為止。
接線與設定#
照 ESPHome 官方文件的雙針接法,SPI 走板子絲印本來就標好的腳,兩支資料腳挑沒有開機職責的 GPIO:
| CC1101 | ESP32-C3 SuperMini | 用途 |
|---|---|---|
| VCC | 3V3 | ⚠️ 不能接 5V |
| GND | GND | |
| SCK | GPIO4 | SPI |
| MISO | GPIO5 | SPI |
| MOSI | GPIO6 | SPI |
| CSN | GPIO7 | SPI |
| GDO0 | GPIO10 | 發射 |
| GDO2 | GPIO3 | 接收 |
C3 SuperMini 要避開 GPIO2/8/9(開機用的 strapping 腳)和 GPIO20/21(UART)。接對的話開機 log 會出現 CC1101 found! Chip ID: 0x0004。
設定檔的核心是這幾段(簡化過,完整版在下面):
logger:
hardware_uart: USB_SERIAL_JTAG # C3 預設走 UART0,不加的話 USB 讀不到 log
wifi:
power_save_mode: NONE # 不關省電的話區網 ping 會飆到 147ms
spi:
clk_pin: GPIO4
miso_pin: GPIO5
mosi_pin: GPIO6
cc1101:
id: transceiver
cs_pin: GPIO7
frequency: 433.92MHz
modulation_type: ASK/OOK
filter_bandwidth: 200kHz
symbol_rate: 40000 # 這個值是整篇最重要的一行,後面會講
remote_receiver:
pin: GPIO3
dump: raw # 錄波時打開,錄完可以關掉
filter: 20us
idle: 25ms
tolerance: 45%
remote_transmitter:
pin: GPIO10
carrier_duty_percent: 100%
on_transmit:
- cc1101.begin_tx: transceiver
on_complete:
- cc1101.begin_rx: transceiver
switch:
- platform: template
name: 房間燈
optimistic: true
restore_mode: DISABLED # 這個也很重要,後面會講
turn_on_action: &toggle
- remote_transmitter.transmit_raw:
code: [<你錄到的碼>]
repeat:
times: 8
turn_off_action: *toggle這種開關只有「切換」一個動作,所以開和關送的是同一組碼。
實際在用的版本多了排隊發射、反向偵測和幾個診斷感測器。完整的設定檔放在這裡,兩組碼換成了佔位符,錄到自己的碼之後填進 transmit_raw 和 on_raw 那兩個比對值就能用:
完整的 rf-bridge.yaml
# ─────────────────────────────────────────────────────────────────────────
# rf-bridge —— ESP32-C3 SuperMini + CC1101 的 433MHz 收發閘道
#
# 目的:解決客廳/房間的「自發電無線開關」無法進 HA 的問題。
# • RM4 Pro 收不到這種開關(它的射頻只認特定協定,且時間解析度不夠細)
# • CC1101 能側錄任意 OOK 原始波形 → 錄下開關的碼 → 用同一片發射給通斷器
#
# 用法分兩階段:
# 階段一(錄波):dump: raw,按牆上開關,看 log 印出的原始時序。
# 這時把本裝置拿到開關旁邊(行動電源供電)確保收得到。
# 階段二(控燈):把錄到的碼填進下面 script 的 transmit_raw,找個收得到兩間房的位置常駐。
#
# 腳位對照 ESP32-C3 SuperMini 的絲印:
# SPI 走板子本來就標好的 SCK(IO4) / MISO(IO5) / MOSI(IO6) / SS(IO7)
# 兩支 GDO 走板上「完全沒有開機或系統職責」的自由腳:IO3 和 IO10
# (另外兩支自由腳 IO0 / IO1 留著備用)
# 避開 IO2 / IO8 / IO9 這三支 strapping,以及 IO20 / IO21 這兩支 UART。
# 註:IO4~IO7 在資料上標為 JTAG(MTMS/MTDI/MTCK/MTDO),但 C3 內建 USB-JTAG,
# 不接外部 JTAG 的話拿來當 SPI 是正常用法,那也是板廠標示的用途。
# ─────────────────────────────────────────────────────────────────────────
substitutions:
name: rf-bridge
friendly_name: "RF Bridge (CC1101)"
esphome:
name: ${name}
friendly_name: ${friendly_name}
min_version: 2024.6.0
on_boot:
# 保險:明確讓 CC1101 開機就進接收模式。
# 官方範例只在「發射完成後」才 begin_rx,沒交代開機初始狀態,
# 錄波階段收不到東西的話會很難分辨是沒訊號還是沒在收。
priority: -100
then:
- cc1101.begin_rx: transceiver
# 把記住的燈狀態「顯示」回 HA,只 publish 不執行動作,不會送任何 RF 訊號
- lambda: |-
id(room_light).publish_state(id(room_light_saved));
id(mom_light).publish_state(id(mom_light_saved));
esp32:
board: esp32-c3-devkitm-1 # C3 SuperMini 用這個 board 設定即可
framework:
type: esp-idf
logger:
# 平常用 INFO。要再錄新的遙控器時改回 DEBUG,並把下面 remote_receiver
# 的 dump: raw 打開,才看得到原始 µs 時序。
level: INFO
# ⚠️ ESP32-C3 的 logger 預設輸出到 UART0(實體腳 GPIO20/21),
# 那兩支沒接任何東西 → 從 USB 讀 log 會是一片空白(我第一次燒就踩到)。
# 要明確指定走晶片原生的 USB Serial/JTAG 才看得到開機訊息。
hardware_uart: USB_SERIAL_JTAG
api:
# 給 HA 的「只改顯示、不發射」指令:HA 用光線感測器發現狀態對不上時呼叫,
# publish_state 會觸發 on_state,所以快閃裡記的狀態也會一起更新。
actions:
- action: set_room_light_state
variables:
state: bool
then:
- lambda: id(room_light).publish_state(state);
- action: set_mom_light_state
variables:
state: bool
then:
- lambda: id(mom_light).publish_state(state);
encryption:
key: !secret rf_bridge_api_key
ota:
- platform: esphome
password: !secret rf_bridge_ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# ⚠️ 必須關省電。ESP32-C3 的 WiFi light sleep 會造成兩個問題(都實際踩過):
# 1. 區網內 ping 延遲飆到 67~147ms 且劇烈抖動 —— 對要即時反應的 RF 橋接器不能接受
# 2. USB Serial/JTAG 會跟著失效,序列埠變成一片空白,害我一度以為晶片當機
power_save_mode: NONE
# ⚠️ 不要長期插在伺服器 / 電腦的 USB 3.0 孔上:那裡的 2.4GHz 雜訊讓它一天斷 17 次,
# 改插手機充電頭後 2 天 0 次。
# 試過 output_power: 8.5dB → 斷電重開後也完全連不上,比預設更糟,已拿掉。
# 斷線時記一筆,連回來後 HA 就看得到這次開機斷了幾次
on_disconnect:
- lambda: id(wifi_drops) += 1;
# 收訊不穩時的後援熱點(第一次設定用)
ap:
ssid: "rf-bridge-setup"
password: !secret rf_bridge_ap_password
globals:
- id: wifi_drops
type: int
restore_value: no
initial_value: '0'
# 兩盞燈最後的狀態,存在快閃裡,重開機後拿來還原 HA 的顯示
- id: room_light_saved
type: bool
restore_value: yes
initial_value: 'false'
- id: mom_light_saved
type: bool
restore_value: yes
initial_value: 'false'
# 記錄最後一次發射的時間,給 on_raw 做冷卻判斷用
- id: last_tx_ms
type: uint32_t
initial_value: '0'
captive_portal:
# 內建網頁介面 —— 用來直接測試按鈕,不必先接進 HA。
# 之後要拿掉就刪這段(會省下一點 flash)。
web_server:
port: 80
version: 3
# 首次用 USB 序列燒錄後,之後可走 WiFi OTA
improv_serial:
# ─── SPI 匯流排:軟體 SPI,用避開雷腳的一般 GPIO ───
spi:
clk_pin: GPIO4
miso_pin: GPIO5
mosi_pin: GPIO6
# ─── CC1101 本體 ───
# 頻率鎖 433.92(我的開關晶振 26.2982×16.5 換算出來的值)。
cc1101:
id: transceiver
cs_pin: GPIO7
frequency: 433.92MHz
modulation_type: ASK/OOK
# 350kHz 太寬,AGC 會把底噪也放大進來。收窄到 200kHz 壓雜訊。
# (實測 325kHz 時每按一次開關會收到 8 筆長度不一的碎片)
filter_bandwidth: 200kHz
# ⚠️ symbol_rate 會影響 CC1101 的資料濾波器頻寬,不是「只在發射時有用」。
# 原本設 5000(5 kbaud = 200µs/符號),但實測訊號的基本時間單位是 25µs
# (所有脈衝寬度都是 25 的整數倍:25/50/75/100/.../475/875),
# 等於快了 8 倍,濾波器會把快速變化抹掉 → 每次解調出來都不一樣。
# 40000 baud = 25µs/符號,剛好對上訊號的最小特徵。
symbol_rate: 40000
output_power: 10 # dBm,最大約 +11
# 註:cc1101 沒有 rssi / lqi 這兩個設定項(我一開始寫錯,compile 直接擋下來)。
# RSSI 和 LQI 只在 packet_mode 的 on_packet 觸發器裡當變數用,
# async 模式(我們用的這個)拿不到,也掛不出感測器實體。
# ─── 接收(雙針接法,GDO2)───
remote_receiver:
id: rx
pin:
number: GPIO3 # 接 CC1101 GDO2
# 錄波時打開 `dump: raw` 才看得到原始 µs 時序。
# ⚠️ 不要用 dump: all —— 那只啟用「協定解碼器」,不含原始波形,
# 而且會把雜訊硬套成 Pronto / Beo4 等紅外線協定,洗版又沒用。
# 平常關掉:反向偵測靠的是下面的 on_raw,不需要 dump 就能運作,
# 開著會把鄰居的遙控器和雜訊也全部印進 log。
# dump: raw
# ⚠️ filter 不只是「丟掉短脈衝」,它會把短脈衝兩側的訊號「合併」起來:
# 真實 185高 / 60低 / 185高 → filter 90µs 之後變成單一個 430µs 高
# 所以 90µs 時錄到的 477 / 327 很可能是這樣造出來的假值。
# 近距離錄波時訊號遠高於雜訊,filter 可以放低才錄得到真實結構。
filter: 20us
# idle 拉長,確保一次按壓的所有重複框都收在同一筆裡,不會被切斷
idle: 25ms
tolerance: 45%
# 註:接收端不需要(也不接受)on_transmit / on_complete。
# 模式切換全部由 remote_transmitter 那邊負責,發射完會自己切回 rx。
#
# ── 反向偵測:有人按實體開關時,翻轉 HA 裡的狀態 ──
# 不用 ESPHome 內建的 raw 比對,因為那要求從框的開頭對齊,
# 而我們的擷取起始相位不固定(同一次按壓會收到 2~4 次重複,起點不一定)。
# 改成自己解成 26 bit 再比數值,不管從哪裡開始都認得出來。
on_raw:
then:
- lambda: |-
// 發射後 1 秒內忽略,避免自己發的訊號把狀態翻兩次
static uint32_t last_seen = 0;
if (millis() - id(last_tx_ms) < 1000) return;
uint32_t code = 0; int nbits = 0;
for (size_t i = 0; i + 1 < x.size(); i += 2) {
int32_t m = x[i], sp = -x[i+1];
if (m < 20 || sp < 20) { code = 0; nbits = 0; continue; }
if (sp > 400) { // 同步間隔 → 一個框結束
code = 0; nbits = 0; continue;
}
code = (code << 1) | (m > sp ? 1 : 0);
if (++nbits > 26) { code &= 0x3FFFFFF; nbits = 26; }
if (nbits == 26) {
if (millis() - last_seen < 800) break; // 同一次按壓的重複框只算一次
if (code == 0x0000000UL) { // 換成房間燈的 26 bit 數值
last_seen = millis();
ESP_LOGI("rf", "偵測到實體開關:房間燈");
id(room_light).publish_state(!id(room_light).state);
break;
} else if (code == 0x0000001UL) { // 換成媽媽房間燈的 26 bit 數值
last_seen = millis();
ESP_LOGI("rf", "偵測到實體開關:媽媽房間燈");
id(mom_light).publish_state(!id(mom_light).state);
break;
}
}
}
# ─── 發射(雙針接法,GDO0)───
remote_transmitter:
id: tx
pin:
number: GPIO10 # 接 CC1101 GDO0
carrier_duty_percent: 100%
on_transmit:
then:
- lambda: 'id(last_tx_ms) = millis();'
- cc1101.begin_tx: transceiver
on_complete:
then:
- cc1101.begin_rx: transceiver
# ─── 燈的開關實體 ───
# 用 switch 而不是 button,這樣 HA 有「開/關」狀態可以顯示與自動化。
# RF 這種開關只有「切換」一個動作(沒有分開的開/關指令),
# 所以 turn_on 和 turn_off 送的是同一組碼,靠 optimistic 維持狀態。
# 有人按實體開關時,上面 remote_receiver 的 on_raw 會把狀態翻過來。
#
# 兩顆開關的碼(2026-09-09 錄,各自貼著模組按十次取共識):
# 房間燈 26 bit = <你的碼>
# 媽媽房間燈 26 bit = <你的碼>
# 前 10 bit 相同(同型號廠商碼),之後分歧 → 不會互相干擾
switch:
- platform: template
name: "房間燈"
id: room_light
icon: "mdi:ceiling-light"
optimistic: true
# ⚠️ 預設 restore_mode 是 ALWAYS_OFF,開機時會「真的執行一次 turn_off」,
# 而 turn_off 送的是切換碼 → 每次重開機燈都被切換一次。所以必須 DISABLED,
# 狀態改由上面 globals 記住、on_boot 只顯示不發射。
restore_mode: DISABLED
on_state:
- lambda: id(room_light_saved) = x;
turn_on_action: &toggle_room
- script.execute: tx_room_light
turn_off_action: *toggle_room
- platform: template
name: "媽媽房間燈"
id: mom_light
icon: "mdi:ceiling-light"
optimistic: true
# ⚠️ 預設 restore_mode 是 ALWAYS_OFF,開機時會「真的執行一次 turn_off」,
# 而 turn_off 送的是切換碼 → 每次重開機燈都被切換一次。所以必須 DISABLED,
# 狀態改由上面 globals 記住、on_boot 只顯示不發射。
restore_mode: DISABLED
on_state:
- lambda: id(mom_light_saved) = x;
turn_on_action: &toggle_mom
- script.execute: tx_mom_light
turn_off_action: *toggle_mom
# ─── 發射排隊 ───
# HA 上連按時,兩串訊號若黏在一起,燈只會當成按一次,但 HA 已經翻了兩次 → 不同步。
# 所以每盞燈一個 queued script:一次只發一串,發完至少隔 700ms 才發下一串。
script:
- id: tx_room_light
mode: queued
max_runs: 6
then:
- remote_transmitter.transmit_raw:
code: [<換成你錄到的碼>]
repeat:
times: 8
wait_time: 0s
- delay: 700ms
- id: tx_mom_light
mode: queued
max_runs: 6
then:
- remote_transmitter.transmit_raw:
code: [<換成你錄到的碼>]
repeat:
times: 8
wait_time: 0s
- delay: 700ms
# ─── 診斷 ───
sensor:
- platform: wifi_signal
name: "${friendly_name} WiFi"
update_interval: 60s
entity_category: diagnostic
- platform: uptime
name: "${friendly_name} 開機時間"
update_interval: 60s
entity_category: diagnostic
- platform: template
name: "${friendly_name} WiFi 斷線次數"
lambda: return id(wifi_drops);
accuracy_decimals: 0
state_class: measurement
update_interval: 60s
entity_category: diagnostic
text_sensor:
- platform: wifi_info
ip_address:
name: "${friendly_name} IP"
bssid:
name: "${friendly_name} BSSID"
- platform: debug
reset_reason:
name: "${friendly_name} 重開原因"
debug:
update_interval: 5min密碼和金鑰走 ESPHome 的 secrets.yaml,需要的欄位是 wifi_ssid、wifi_password、rf_bridge_api_key、rf_bridge_ota_password、rf_bridge_ap_password。
另外 C3 這塊板子第一次燒錄有個小坑:esptool 最後那句「Hard resetting via RTS」對它沒用,燒完要自己按一下 RESET,不然它會一直停在下載模式,看起來像當機。
錄波:最容易騙到自己的地方#
硬體接好,真正花時間的是錄波。我錄了三輪,第二輪差點讓我以為做完了。
第一輪:目測「看起來一樣」#
一開始用的是 dump: all,log 裡全是 Received Pronto,一秒十筆。那是 ESPHome 把雜訊硬套成紅外線協定在解碼,dump: all 只開協定解碼器,不包含原始波形,要改成 dump: raw 才看得到真正的時序。
按的時候每秒收到 8 筆,不按時 30 秒才 1 筆,至少確定開關有在發、CC1101 有在收。我挑了兩筆擺在一起看,同一個位置的值幾乎一樣,心想就是它了。
然後跑了統計:
長度 41 那組(25 筆)逐位置變異係數:中位 45% 最大 374%
擷取長度:15 到 67,標準差 9.4同一組遙控碼,每次擷取的同一個位置應該幾乎一樣,要在 10% 以下才算數。45% 代表我那兩筆只是剛好挑到像的。
第二輪:漂亮的假碼#
我把 25、50、75µs 這些很短的脈衝當成雜訊,把 filter 從 10µs 拉到 90µs 濾掉。這次的數字漂亮多了:
160 個完整框,134 個長度相同
逐位置變異:中位 7.3% 最大 12.2%
框:186, -110, 477, -105, 184, -847, 182, -111, 327, -552, 186, -13057.3%,低於門檻。我開開心心做成 HA 按鈕,連按了六次,燈一次都沒動。
一開始懷疑距離,但這說不通:那顆開關我拆下來拿到很遠的地方按都有用,它那點功率都夠了,CC1101 沒道理不夠。所以問題一定在碼本身。
回頭看,錯在兩個參數。第一個是 filter,它不只是把太短的脈衝丟掉,還會把短脈衝兩邊的訊號黏在一起:
真實: 185µs 高 → 60µs 低 → 185µs 高
filter 90µs 後: 430µs 高框裡那個 477 和 327,很可能就是這樣黏出來的。
第二個更根本。把開關貼著模組重錄的時候,我發現所有脈衝寬度都是約 25µs 的整數倍,而設定檔裡寫的是 symbol_rate: 5000,也就是每個符號 200µs。CC1101 的資料濾波器頻寬是跟著符號速率走的,設得太慢,快速的變化會直接被濾掉。我當初還在設定檔裡寫了一行註解說「錄波階段不影響 raw 擷取」,完全寫反了。
cc1101:
frequency: 433.92MHz
symbol_rate: 5000
symbol_rate: 40000
remote_receiver:
filter: 90us
filter: 20us
idle: 25msfilter 敢降到 20µs,是因為改成把開關貼著模組按,訊號遠強過雜訊。那時候我是一手抓著天線、一手按開關在錄的。
第三輪:54 個值#
改完之後,每次擷取的長度從 19 到 63 一下子變成 93 到 169,而且分成兩群,剛好對應同一個框重複兩次和三次。算出來週期是 54 個值,逐位置變異中位 9.4%。
結構也終於說得通了:一個同步頭,接著 26 個 bit,每個 bit 是一短一長兩個脈衝,短的約 37µs、長的是三倍,典型的 一族。燒進去,按下 HA 的按鈕,燈關了。那天晚上七點四十分左右。
WARNING
注意第二輪和第三輪:假碼的變異係數比真碼還低。
濾波器設錯的時候,每次都會用同樣的方式把訊號抹壞,所以錯得非常一致,統計上完全看不出來。變異係數低於 10% 只能當必要條件。讓假碼露餡的是另外兩件事:結構太短(只有 6 組高低脈衝,一般遙控碼至少二十幾個 bit),以及「開關拿到遠處都能用」這個生活常識。
媽媽房間,和反過來聽#
成功之後順手把媽媽房間的燈也做了。同樣的方法,錄到 9 個框、變異中位 6.7%。兩顆碼前 10 個 bit 完全一樣(應該是同一家廠商),後面有 11 個 bit 不同,不會互相干擾。中間有一次算出變異 0.0%,高興不到一秒就發現只有一個樣本,沒東西可比當然是 0。
既然 CC1101 收得到開關,也可以反過來:有人按實體開關時,讓 HA 的狀態跟著翻。ESPHome 內建的比對要求從框的開頭對齊,但每次擷取的起點不固定,我改成在 on_raw 裡自己把波形解成 26 個 bit 再比數值,加上發射後 1 秒的冷卻(不然會聽到自己),以及 800ms 內同一次按壓只算一次。第一天測試兩間房分得清清楚楚。
用了三週才發現的問題#
每次重開,燈就自己切換一次#
9 月 26 日,HA 上的燈狀態開始跟實際對不上。查下來是 ESPHome template switch 的 restore_mode 預設值 ALWAYS_OFF,它的意思是開機時真的執行一次 turn_off。偏偏這顆燈只有切換碼,turn_off 送出去就是切換。於是板子每重開一次(拔插、更新韌體、WiFi 斷太久自己重開),燈就被切換一次,HA 還顯示成關。
改成 restore_mode: DISABLED,狀態存在快閃記憶體裡,開機只把狀態回報給 HA、不發射。順便把發射改成排隊執行,每次之間隔 700ms,免得連按兩下黏在一起被通斷器當成一次。上面那份設定已經是改好的版本。
實體開關幾乎聽不到#
反向偵測第一天測得好好的,但從 9 月 9 日到 26 日只偵測到一次實體開關,我慢慢按了 10 下也是 0 次。那時板子插在客廳伺服器上,離兩間房的開關 4 到 5 公尺還隔一道牆。當初錄碼是貼著模組錄的,自發電開關那點功率隔牆就不夠了。
這個我沒有硬解,改用 SwitchBot Hub 3 的光線感測器補:只在晚上動作,房間亮度 4 級以上而 HA 顯示關,就改成開;2 級以下而顯示開,就改成關。它只更新 HA 的顯示、不發射。感測器大約 7.5 分鐘回報一次,所以狀態最慢 8 分鐘內會跟上。
每天斷 17 次的 WiFi#
同一段時間它開始斷線。9 月 25 日起一天斷十幾次,26 日傍晚乾脆掃不到 IoT 那組 SSID,只有拔插 USB 才會好。接下來三天我把能想到的都試了一遍:
| 假說 | 結果 |
|---|---|
| 路由器的 IoT 網路設定被改了 | ✗ 手機看得到,2.4GHz、頻道 6 都正常 |
| 路由器把它的 MAC 擋了 | ✗ 過濾、封鎖清單、AiProtection 都是乾淨的 |
| 路由器的頻段導引不願意重新接納 2.4GHz 裝置 | ✗ 把 IoT 網路改成只開 2.4GHz,其他 8 台(包括同樣只支援 2.4GHz 的 C210 攝影機)都連回來了,只有它不行 |
調低發射功率 output_power: 8.5dB | ✗ 更糟,斷電重開都連不上 |
| C3 SuperMini 的設計缺陷(晶振離天線太近) | △ 板子確實偏弱,但不是主因 |
C3 SuperMini 有一批板子的設計缺陷還蠻有名的,done.land 有整理:2024 年有一批改版把晶振移到離陶瓷天線只剩 0.3mm(正常至少要 1mm),症狀是掃得到 WiFi 卻連不上,有人甚至要用手摸著天線才連得上。我一度很確定就是這個,差點去買一塊外接天線的板子。好笑的是那篇建議的解法正好是把發射功率調到 8.5dB,而我調了之後反而更連不上,現在回頭看,這其實已經在暗示問題不只是板子。
真正讓我轉向的是另一個細節:某次拔插之後它被插到隔壁的 USB 孔,怎樣都連不上,插回原本那個孔馬上就好。
差幾公分就差這麼多,比較像環境問題。USB 3.0 的孔和線本身就會發出 2.4GHz 的干擾,伺服器背後又是金屬機殼加主機板雜訊,對一塊 WiFi 本來就偏弱的板子來說,那大概是全家最糟的位置。說來好笑,規劃的時候我就知道放伺服器旁要離機殼遠一點,結果它還是在那裡住了三週。
本來想改插房間的電腦,但電腦一樣有 USB 3.0,關機睡眠時 USB 還可能斷電,半夜最需要關燈的時候反而最容易沒電。最後插在一個普通的手機充電頭上:
板子不用換了。
現在的樣子#
這就是同事印的第五版殼,插在我房間牆上的充電頭,天線立起來。從這個位置,我房間和媽媽房間的燈都控制得到。RM4 Pro 退回去只管冷氣,它在紅外線上一直都很稱職。
還沒驗證的是狀態同步:現在這個位置聽不聽得到兩顆實體開關。如果聽得到,HA 的燈狀態就能即時跟著實體開關走;聽不到的話就繼續靠光線感測器校正。我自己猜大概聽不到,自發電開關的功率實在太小,離得還是有點遠。
不過不管狀態同不同步,躺在床上關燈這件事是真的做到了。
- python-broadlink:RF learning 只認特定協定的討論issue #739
- python-broadlink:用 b1c0 前綴發射 RF 的可用封包issue #778
- rtl_433:Help decoding Kinetic light switchissue #1790
- ESP-Home CC1101 Kinetic Switch Transceiverbillmaterial
- ESP32-C3 SuperMini:晶振太靠近天線的設計缺陷done.land
- ESPHome:CC1101 元件esphome.io
還沒有留言
✨ 成為第一個留言的人吧