Web app 如何操作硬體功能(Firefox OS 的內部溝通 – 藍牙篇)
我們在 Firefox OS 手機的設定選單中,可以選擇開啟藍牙或是 GPS 這種手機硬體相關的功能。只要輕鬆點擊網頁語法寫出的按鈕,就可以操作裝置硬體,這在 web app 中是怎麼做到的呢? Web app 開發者通常可以查詢相關的 web API 網頁,看是否有適合的 API 可以幫忙執行想要的工作。有些 API 是 W3C 標準,有些則是不同瀏覽器提供不同的界面,還需一段時間慢慢發展為標準。
當你點擊了某個控制硬體的按鍵,它可能會呼叫相應的 web API 讓瀏覽器內核幫你處理。
例如 Bluetooth API, Geolocation API, Vibration API 等等,都提供了 web 界面的 API 來讓 web 開發者使用 Javascript 來執行相應的硬體能力。大家可以到 MDN-Web API 或是 MozillaWiki-WebAPI 查詢 Firefox OS 所提供的 web API 與其用法。
Firefox OS 的硬體操作
相信不少讀者都聽過 Firefox OS 可以分成三個 layer。
上中下三層分別是 Gaia, Gecko 與 Gonk。簡單來說,它們負責以下不同的任務。
Gaia – HTML5 層和使用者介面系統。
Gecko – 排版引擎和應用執行服務層。
Gonk – Linux 核心和硬體抽象層。
想了解更多的朋友可以參考 MDN 的這篇 「Firefox OS 架構」。
Web app 都是在 Gaia 層執行的,當 web API 被呼叫時,都會交給瀏覽器內核 Gecko 來處理。由於 Gecko 本身無法直接控制裝置硬體,所以若是該 web API 需要控制硬體,還須透過 Gonk 層幫忙。例如,一個 app 想要搜尋附近的藍牙裝置,呼叫了 Bluetooth API BluetoothAdapter.startDiscovery() ,概念上會以這樣的方式進行。
[Gaia] ---(請幫我處理)---> [Gecko]
[Gecko] ---(請幫我處理)---> [Gonk]
然後任務就完成了~ 本質上就是這麼一回事。
等等,如果這種 web API 只是叫下一個人幫我處理,那我怎麼知道它做好了呢?
非同步的 web API
像是 startDiscovery() 這樣的 API 呼叫完後,其實只表示它向 Gecko 提出了要求。在 Javascript 中不需要等到這個要求執行完成,就會開始跑到下一行程式碼。像這樣的 API 可以說它是一種 asynchronous call,在 Firefox OS 通常會回傳 DOMRequest 或是 Promise ,用以提供一個機制告知呼叫者執行完成與否。並不是所有的 web API 都是 asynchronous call,通常是無法保證能快速完成的 API 會設計成 asynchronous 的形式。由於許多需要裝置硬體參與的 API 都有這樣的特性,故本篇文章以這種 API 做為例子。
在我們的想像中,API 呼叫後大概會發生這樣的事情。
[Gaia] ---(請幫我處理)---> [Gecko]
[Gecko] ---(請幫我處理)---> [Gonk]
[Gecko] <---(我完成了)--- [Gonk]
[Gaia] <---(我完成了)--- [Gecko]
以 BluetoothAdapter.startDiscovery() 為例,該函式會回傳一個 DOMRequest 物件。web 開發者可以利用
onsuccess和
onerror,在 request 完成時用對應的 callback handler 來處理後續的工作。
var request = BluetoothAdapter.startDiscovery(); request.onsuccess = function() { /* 完成了~~ */ }; request.onerror = function() { /* 失敗了~~ */ };
比較新設計的 web API ,也常利用 Promise 來處理 asynchronous request。
未來 Firefox OS 的 Bluetooth asynchronous API,亦打算用 Promise 物件來取代 DOMRequest 物件。目前 Firefox OS 的 web API 以 DOMRequest 為主,故本文都以 DOMRequest 為例。想了解 Promise 相關用法的朋友可以參考這篇謀智台客文「使用 Promise 模式,寫出簡單易懂的 marionette test case」。
現在我們認識到,多數的 web API 概念上是這樣被執行的。
在這個系統架構的流程中,上層會向下層請求協助。大家可以注意到這過程會需要一些溝通,這是怎麼作到的呢?
Firefox OS 的內部溝通
之前我都用 “Gaia 向 Gecko 請求” 這樣的說法,實際上這個動作是由 Javascript 和 C++ 間的 binding 關係驅動的。當 Gaia 中 Javascript 版的 startDiscovery() 被呼叫時,Gecko 中對應的 C++ 版 startDiscovery() 也會跟著被呼叫。 它們之間的 binding 關係是由 Web IDL 檔所定義的,之後兩個 layer API 的 binding 實作方式會由 Gecko 自動搞定。Web IDL binding 是目前 Firefox OS 中被推薦使用的 binding 方式,但不是唯一方式。想了解更多的朋友可以參考謀智台客文 「New DOM bindings」和「WebIDL Extended Attribute 大解密」。
Gecko 和 Gonk 間也會透過某種溝通方式來傳遞 request。相較於 DOM binding,Gecko 和 Gonk 間的溝通方式比較多元,不同的 component 間可能有不同的機制。常見的做法是 Gecko 透過某組 HAL (hardware abstraction layer) interface 的 method 和 callback function,跟 Gonk 內該 component 使用的 open source 的函式庫溝通。以藍牙為例,Firefox OS 支援 BlueZ 和 BlueDroid 兩種 Bluetooth stack,Gecko 和 BlueZ 透過 DBus 來溝通,而 BlueDroid 則是提供 HAL interface 讓 Gecko 可以直接使用。而 Gonk 和硬體之間的溝通,更是需要不同的 domain knowledge,都會有專門的函式庫、硬體層堆疊或是 OEM driver 來負責,像是 libusb, telephony stack, Bluetooth stack 等等,故 Firefox OS 的開發者不需要直接處理這部份。以本文的例子來看,Bluetooth stack 是使用 HCI (Host Controller Interface) 這種 command interface 透過 physical bus 和 Bluetooth controller 溝通。
大家可以想像,Gecko 層要求完成某個任務,但是 Gonk 層可以有不同的實作方式。
有的裝置硬體可能只有單一的函式庫,有的可能有不同的函式庫可以選擇。以藍牙為例, BluetoothService 的實作可以是 BlueZ 或是 BlueDroid。
有時溝通的方向未必是由上層 app 傳向下層硬體,例如自己的藍牙裝置被別人的藍牙裝置要求配對時,就需要由硬體向上通知 app,甚至去喚醒尚未開啟的 app。由於這種需求是隨時可能被動的被觸發,所以藍牙模組在 Gecko 中使用常駐的 BluetoothService 來處理。BluetoothService 在 Gecko 中代表了和藍牙硬體的溝通窗口,主動與被動的操作都會透過這個 singleton class 來完成。大家可以想像,Gaia 層要求完成某個任務時,在 Gonk 層可以有不同的實作方式。Web app 開發者呼叫了 Bluetooth API 後,Gecko 會判斷運行的裝置是使用的是 BlueZ 還是 BlueDroid,來實作相應的 BluetoothService,並透過多型 (Polymorphism) 的方式供 web API 使用。( 這樣的方式用於 Gonk 函式庫們的 interface 不同的情況下。如果 Gonk 實作不同,但 HAL interface 都是一致的,則 Gecko 無須判斷是哪種實作方式。)
讓我們用 BluetoothAdapter API 為例,畫一下這個流程。
( 黃色區塊內的檔案是參照 BluetoothAdapter.webidl 而自動產生的程式碼。)
呼,終於搞懂了 Web API 是怎麼操控裝置硬體了。
好像…哪裡怪怪的?
咦,可是追了一下程式碼後,發現有時候執行的流程和想像中的不太一樣。
這是因為只有 b2g process 可以使用 BluetoothService,也就是「淺談 Firefox OS 的多程序架構與程序間通訊」所提到的 chrome process。由於一般的 app 都是跑在自己獨立的 content process 上,沒有權限直接執行上述的動作,所以還需要請 chrome process 幫忙處理。
這個過程我們可以這樣理解。
[Gaia content] ---(請幫我處理)---> [Gecko content]
[Gecko content] ---(請幫我處理)---> [Gecko chrome]
[Gecko chrome] ---(請幫我處理)---> [Gonk]
[Gecko chrome] <---(我完成了)--- [Gonk]
[Gecko content] <---(我完成了)--- [Gecko chrome]
[Gaia content] <---(我完成了)--- [Gecko content]
這裡
[Gecko content]與
[Gecko chrome]所用到的溝通方式,稱為 IPC (inter-process communications),有 IPC 需求的 web API 需要寫 IPDL (IPC protocol definition language) 檔來定義 IPC 的 protocol ,如果要定義可讓不同 IPC protocols 共用的資料結構,要再寫 IPDLH (IPDL header)檔定義結構,相關的細節可以參考參考謀智台客文「快來幫忙找,IPDL 在哪裡?」 和 MDN – IPDL Tutorial 。
讓我們加入 IPC 的觀念,重新畫一張圖。
( 黃色區塊內的檔案是參照 *.webidl 或 *.ipdl 而自動產生的程式碼。)
這張圖的名詞縮寫請參考:
BSc: BluetoothService, Child Process Implementation
BC: Bluetooth Child
BRC: Bluetooth Request Child
BRP: Bluetooth Request Parent
BP: Bluetooth Parent
BSp: BluetoothService, BlueZ or BlueDroid Implementation
如果呼叫 BluetoothAdapter API 的 app 是跑在 content process (child process) 上,則它透過多型 (Polymorphism) 取得的 BluetoothService 實作會是
BluetoothServiceChildProcess這個類別的物件。而這個物件會透過 BluetoothChild 來向 chrome process (parent process) 請求執行,更明白的說,它透過 IPDL 定義的 Bluetooth Request Protocol 通知 chrome process 上的 BluetoothParent 來執行本來的 web API。此時因為已經跑在 chrome process 上了,因此取得 BluetoothService 實體會是 BlueZ 或 BlueDroid 實作的物件,進而和裝置硬體溝通。
雖然這裡是以 Bluetooth API 作為例子,但其他 web API 在 Firefox OS 的運作方式也類似。由於本篇文章著重在 web API 在 Firefox OS 系統架構上不同 layer 之間的溝通,而不是藍牙模組開發,故所舉的例子中省略了部份實作細節與類別名稱。在對整個流程有初步了解後,可以參考本文最後一張示意圖的四條垂直的虛線,針對自己有興趣的模組去看實作細節。Web 世界一個小小的硬體控制按鍵中,可以說是一個點擊一世界,很多有趣的內容值得大家去了解。