ChameleonUltra 也支援透過 BLE 進行連線,雖然 BLE 的傳輸速度會比 Serial 慢,但在 Android 上沒有內建支援 Web Serial API,為了方便網頁可以在 Android 上使用,所以這個專案也支援透過 Web BLE API 進行連線。
目前專案中使用的 Web BLE API 支援 ChromeOS、Chrome for Android 6.0、macOS (Chrome 56 以上) 以及 Windows 10 (Chrome 70 以上),可以參考 MDN 的 Browser compatibility 來查看詳細的瀏覽器支援度資料。
因為藍牙會有可能曝露個人資料以及定位,所以 Web BLE API 只能在 HTTPS 網站上使用。
在本地開發過程中也會有這個限制,筆者會在接下來的文章提到要如何解決這個限制。
我們在這個專案中會使用 navigator.bluetooth.requestDevice
的 API,但由於安全性考量,Chrome 限制這個 API 只能在特定的使用者互動的事件中使用,例如 pointerup
、click
、touchend
…等。
使用者需要給予 Chrome 藍牙及定位權限,才能夠使用 Web BLE API。如果是比較新的 Android 可能還需要給予精確定位的權限。
要透過 BLE 連線到 ChameleonUltra,首先我們要先找到 GATT 所需的 Service UUID 及 Characteristic UUID,這資料我們可以從原始碼或使用 nRF Connect for Mobile 這個 APP 的藍牙掃描結果中找到:
6e400001-b5a3-f393-e0a9-e50e24dcca9e // BLE GATT Service UUID
6e400002-b5a3-f393-e0a9-e50e24dcca9e // BLE GATT Characteristic UUID (Write No Response)
6e400003-b5a3-f393-e0a9-e50e24dcca9e // BLE GATT Characteristic UUID (Notify)
這個專案筆者會使用 navigator.bluetooth.requestDevice
來尋找 ChameleonUltra,有了上面的 UUID 後,就可以透過 filters 來搜尋裝置:
// 以 Service UUID 來搜尋
const device = await navigator.bluetooth.requestDevice({
filters: [{ services: ['6e400001-b5a3-f393-e0a9-e50e24dcca9e'] }],
})
// 以 device name 來搜尋
const device = await navigator.bluetooth.requestDevice({
filters: [{ name: 'ChameleonUltra' }],
optionalServices: ['6e400001-b5a3-f393-e0a9-e50e24dcca9e'],
})
成功取得 device 以後,就可以連線並取得 service 及 characteristic:
const server = await device.gatt.connect()
const service = await server.getPrimaryService('6e400001-b5a3-f393-e0a9-e50e24dcca9e')
const sendCharacteristic = await service.getCharacteristic('6e400002-b5a3-f393-e0a9-e50e24dcca9e')
const readCharacteristic = await service.getCharacteristic('6e400003-b5a3-f393-e0a9-e50e24dcca9e')
BLE 的 Notify 會主動通知新的資料,所以我們需要監聽 characteristicvaluechanged
事件:
readCharacteristic.addEventListener('characteristicvaluechanged', (event) => {
const buf = Buffer.from(event.target.value.buffer)
console.log(buf.toString('hex'))
})
由於 BLE 的封包有 ATT_MTU (Maximum Transmission Unit) 的問題,不同的平台及不同的藍牙版本的預設限制不同,為了最大程度相容不同的藍牙版本,所以我們會將資料切成多個封包來傳送,每個封包最長長度為 20
,以下是筆者寫的範例程式碼:
const data = Buffer.alloc(512)
let buf1 = null
for (let i = 0; i < buf.length; i += 20) {
const buf2 = data.subarray(i, i + 20)
if (_.isNil(buf1) || buf1.length !== buf2.length) buf1 = Buffer.allocUnsafe(buf2.length)
buf1.set(buf2)
await sendCharacteristic.writeValueWithoutResponse(buf1.buffer)
}