lite-line-simple-beacon 開發以及 macOS CoreBluetooth 限制的 workaround

Roga Lin

2026-09-04

前言

lite-line-simple-beacon 是我拿來模擬 LINE Simple Beacon 的一個小型 Python 專案。它的工作其實很單純:接收 Hardware ID(HWID)和 Device Message,組成 Bluetooth Low Energy(BLE)廣播封包,再交給作業系統的藍牙介面送出去。

當初寫這個專案,是想在 Linux 以及 macOS 都能夠發送 LINE Simple Beacon 的訊號,但開始實作之後才發現 macOS 上面的限制還不少,所以最後 Linux 的部分是完成了,但 macOS 的部分只能算是半成品,因為作業系統本身的限制,沒辦法發送 LINE Simple Beacon 的訊號,但可以透過 USB 外接 ESP32 來實現。以下整理我重新檢查專案後做的重構,也說明 macOS 原生藍牙為什麼無法直接完成這件事,以及最後如何把實際廣播工作交給外接 ESP32。

專案目前的組成

lite-line-simple-beacon 的功能很簡單:建立符合 LINE Simple Beacon 規格的資料,然後依照執行平台選擇適合的 transmitter。

專案目錄結構如下:

lite-line-simple-beacon/
├── broadcaster.py
├── core/
│   └── beacon_core.py
├── platforms/
│   ├── linux.py
│   ├── macos.py
│   └── esp32.py
├── requirements.txt
├── requirements-linux.txt
├── requirements-macos.txt
└── tests/

各部分的責任:

CLI 的執行流程

基本的執行方式如下:

python3 broadcaster.py \
  --hwid 018741a0bd \
  --message 012345

程式收到參數後,會依序處理:

讀取 HWID 與 Device Message
        ↓
驗證長度與十六進位格式
        ↓
依平台與 --adapter 選擇 transmitter
        ↓
初始化 Bluetooth 或外接裝置
        ↓
建立完整 advertising data
        ↓
開始廣播
        ↓
持續執行,直到使用者按下 Ctrl+C
        ↓
停止廣播並清理裝置狀態

LINE Simple Beacon 的封包內容

LINE Simple Beacon 使用 BLE legacy advertising data,單一 payload 的上限是 31 bytes。程式建立的內容可以拆成三個 Advertising Data(AD)structure:

Flags + Complete List of 16-bit Service UUIDs + Service Data

實際欄位如下:

用結構表示會比較清楚:

02 01 06                         # Flags
03 03 6F FE                      # 16-bit Service UUID 0xFE6F
17 16 6F FE                      # Service Data header + UUID
02                               # LINE frame type
32 AF 51 9E 88                   # HWID,5 bytes
7F                               # Measured Tx Power
00 32 00 69 00 FA FF FF
00 01 99 FF 88                   # Device Message,13 bytes

上面是 31 bytes 的完整範例。Device Message 比較短時,程式會在 payload 尾端補 0x00,讓交給 Linux HCI 或 ESP32 的資料維持固定 31 bytes。

先驗證輸入

BeaconCore 對輸入有幾個明確限制:

我在輸入驗證的部分,會選擇直接擋下來超長資料,因為既然是廣播訊息,我還是比較希望訊息內容一開始就被控制好。

平台如何選擇 transmitter

CLI 會先讀取作業系統以及是否有 ESP32 裝置,再決定由哪一個模組負責真正的廣播工作:

Linux                 -> LinuxTransmitter
macOS + no adapter    -> MacOSTransmitter(unsupported)
macOS + ESP32 + port  -> ESP32Transmitter

Linux:直接控制 HCI payload

Linux transmitter 固定使用 hci0,執行前會確認 sudohciconfighcitool 都能使用。初始化時先啟用 adapter,再清除可能存在的 advertising 狀態:

sudo hciconfig hci0 up
sudo hciconfig hci0 noleadv

開始廣播時,BeaconCore 會先建立完整 31-byte payload,接著交給 Bluetooth HCI 的 LE Set Advertising Data command:

sudo hcitool -i hci0 cmd 0x08 0x0008 1f <31 bytes>
sudo hciconfig hci0 leadv

0x08 是 Bluetooth LE controller command group,0x0008 是 Set Advertising Data,1f 代表後面有 31 bytes。程式是用參數陣列呼叫這些命令。

使用者按下 Ctrl+C 後,transmitter 會停止 advertising 並 reset hci0。因此 Linux 執行這個工具時,需要 BlueZ 提供相關指令,也需要足以控制 Bluetooth adapter 的 sudo 權限。

macOS:沒有外接 adapter 時停止執行

在 macOS 沒有指定 --adapter esp32 時,CLI 會選擇 MacOSTransmitter。這個 transmitter 不會存取 serial port,也不會嘗試呼叫 CoreBluetooth 廣播,而是直接回報:Mac 內建 CoreBluetooth 無法送出 LINE Simple Beacon 所需的自訂 Service Data。

這個 unsupported 路徑是刻意保留的平台行為,雖然我也在考慮是不是直接拿掉,這樣好像會比較乾淨。

macOS CoreBluetooth 的限制在哪裡

另外在 macOS 上,如果要用 python 發送藍牙訊號,大概是這樣的呼叫關係:

Python
  ↓
PyObjC
  ↓
Apple CoreBluetooth

Python 負責程式邏輯,PyObjC 是呼叫 Objective-C framework 的 bridge,真正決定 Mac 能廣播哪些資料的仍然是 Apple 公開的 CoreBluetooth API。

根據 LINE Simple Beacon 官方規格,BLE advertising data 必須同時包含:

Apple Core Bluetooth Programming GuideCBPeripheralManager.startAdvertising() 的說明很明確:peripheral manager 的 advertisement data dictionary 只支援兩個 key:

CBAdvertisementDataLocalNameKey
CBAdvertisementDataServiceUUIDsKey

這裡沒有可以讓應用程式任意放入內容的 CBAdvertisementDataServiceDataKey。也就是說,Mac 可以宣告「我提供 0xFE6F 這個 Service UUID」,卻不能透過這個公開 API 把 HWID、Measured Tx Power 與 Device Message 組成的 LINE frame 一起放進 Service Data 廣播。

兩者的差別可以簡化成:

CoreBluetooth 可以送:
Service UUID = 0xFE6F

LINE Simple Beacon 需要:
Service UUID = 0xFE6F
+ Service Data = UUID + frame type + HWID + Tx Power + Device Message

所以 PyObjC 至此也無能為力,畢竟 PyObjC 只能存取 CoreBluetooth 已提供的介面,不能替系統 framework 增加 raw advertising payload 或也沒有自訂 Service Data 的能力。

這也是我在 MacOSTransmitter 直接回報 unsupported 的原因:只送出 Service UUID 並不是完整的 LINE Simple Beacon。程式如果只確認 startAdvertising() 沒有報錯,很容易把「CoreBluetooth 接受了它支援的欄位」誤認成「LINE 需要的封包已經透過無線送出」。

Workaround:使用 USB 外接 ESP32

既然 Mac 內建藍牙的公開 API 無法設定完整 payload,我採用的做法是把 BLE 廣播交給外接 ESP32。Mac 仍然負責參數驗證和封包建立,只是最後不走 CoreBluetooth:

Python on macOS
        ↓
USB serial,115200 baud
        ↓
ESP32 running ESP-AT
        ↓
BLE advertising data

這條路徑能避開 CoreBluetooth,是因為真正操作 BLE controller 的裝置已經變成 ESP32。Python 會把完整 31-byte payload 轉成十六進位字串,再透過 serial 交給 ESP-AT。

ESP32 必須有相容的 ESP-AT firmware

這個 backend 使用的是 ESP-AT 指令,不是任意 ESP32 firmware 都能接受。接上的開發板必須執行包含 Bluetooth LE AT commands 的 ESP-AT firmware;如果板子跑的是 Arduino sketch、MicroPython,或沒有編入 BLE AT commands 的 firmware,就不能直接使用這組流程。

初始化與廣播會使用:

AT
AT+BLEINIT?
AT+BLEINIT=2
AT+BLEADVDATA="<advertising data>"
AT+BLEADVSTART
AT+BLEADVSTOP

程式先用 AT 確認裝置有回應,再以 AT+BLEINIT? 取得 BLE role。未初始化時使用 AT+BLEINIT=2 切換成 server role;若裝置處於不相容的 role,則先清除再設定。

接著 AT+BLEADVDATA 寫入完整十六進位 payload,收到 OK 後才執行 AT+BLEADVSTART。使用者按下 Ctrl+C 時,程式會送出 AT+BLEADVSTOP 再結束。

Espressif 的 Bluetooth LE AT command 文件 說明 AT+BLEADVDATA 接受 HEX string,最大長度為 31 bytes,剛好可以承載這個專案建立的 BLE legacy advertising data。

安裝 macOS 所需套件

requirements-macos.txt 使用 pyserial==3.5。這個 dependency 只負責控制 USB serial port,和 Mac 內建藍牙的 CoreBluetooth 能力無關。

python3 -m venv venv
source venv/bin/activate
python3 -m pip install -r requirements.txt -r requirements-macos.txt

接上 ESP32 後,可以先列出 pyserial 偵測到的裝置:

python3 -m serial.tools.list_ports

macOS 上常見的名稱是 /dev/cu.usbserial-xxxx/dev/cu.usbmodemxxxx,但不能直接照抄範例,必須以自己的 Mac 實際列出的 port 為準。

在 macOS 啟動 ESP32 transmitter

完整指令如下:

python3 broadcaster.py \
  --adapter esp32 \
  --port /dev/cu.usbserial-xxxx \
  --hwid 018741a0bd \
  --message 012345

--adapter esp32 會讓 macOS 選擇 ESP32Transmitter--port 是必要參數,而且 CLI 會在開啟硬體前檢查它是否存在。Serial port 無法開啟、AT command 在 3 秒內沒有完成,或 ESP32 回覆 ERROR 時,程式都會回報失敗,不會顯示已經開始廣播。

自動化測試與硬體驗證的界線

專案的測試可以從根目錄執行:

python3 -m unittest discover -s tests -v

目前共有 19 個測試,涵蓋:

ESP32 測試使用 mock serial device,所以它能證明 Python 送出的設定、payload 與 command sequence 符合程式設計,也能確認錯誤時不會宣稱廣播成功。

不過完整的驗證還是建議使用實體 ESP32,再由第二台 BLE scanning device 或 packet sniffer 擷取封包,確認 0xFE6F Service Data、HWID、Measured Tx Power 與 Device Message 都和程式產生的 bytes 一致。

小結

lite-line-simple-beacon 的邏輯就是先可靠地建立符合 LINE 規格的 31-byte advertising data,再找到能完整控制這些 bytes 的 BLE controller。

參考資料

專案原始碼