在 iOS 11 中使用 Core Bluetooth

SwiftGG翻譯組發表於2019-04-15

作者:Andrew Jaffee,原文連結,原文日期:2018-04-17
譯者:灰s;校對:Ceenumbbbbb;定稿:Forelax

作為 iOS 開發,我們十分清楚人們都喜歡互通性。我們喜歡通過無線裝置與其他人進行溝通這一點是顯而易見的。最近,我們開始希望能夠與那些曾經被認為是獨立的普通裝置進行通訊。我們開始喜歡,甚至是期望,部分無線裝置可以收集並且分析自己的資料(通常稱為“可穿戴裝置”)。許多裝置已經成為我們生活裡的一部分,還為還有一個專門的術語來描述它:“Internet of Things” 或者 “IoT”(物聯網)。現在地球上有數十億的無線通訊裝置。在這篇教程中,我們將聚焦 IoT 其中的一部分:藍芽。

我將說明藍芽技術背後的基本概念,以及:

  • 展示如何精通藍芽方向的軟體開發,從而為你提供巨大的職業機遇
  • 提醒你必須去確認在釋出一個使用藍芽技術的應用時是否需要通過“資格審查”
  • 給你提供 Apple 的Core Bluetooth 框架概述 (也可以參閱這裡)
  • 最後,帶領你使用 Swift 4 並通過 Core Bluetooth 和一個藍芽裝置來開發一款用於監控心率的 iOS 應用程式

提示:注意跟隨閱讀文章中包含的超連結。對於開發者這是重要的資料,它確保你完全理解藍芽的工作方式以及蘋果是如何支援藍芽這種技術的。

藍芽 - 一項迅速發展的技術

在一篇文章中不可能說清楚如何為整個物聯網開發軟體,但實際上,對所有這些無線裝置進行資料分析是很有啟發性的 - 實際上是很不可思議的。連線著的東西無處不在並且可以預測這個小東西的增長速度將是驚人的。如果你觀察一下我們今天討論的內容,在“短程段”中,使用如藍芽和無線網的技術,然後新增上“廣域類別”中,使用如電話的技術(比如: CDMA),你將看到 ~ 2014 年的 125 億裝置迅速增加到 2022 年預計的 300 億。

藍芽是一種短距離無線通訊技術的標準化規範。Bluetooth Special Interest Group(藍芽技術聯盟) 管理和保護這種短程無線技術背後的研發、發展還有智慧財產權。SIG 確保關於藍芽的製造商,開發者和銷售者他們的硬體和軟體都是基於標準化規範。

根據 Bluetooth SIG 報導,“今年有將近 40 億臺裝置使用藍芽進行連線。藍芽將連線手機、平板電腦、個人電腦,藍芽將會將我們彼此連線。”。一家對短程通訊技術進行深度投資的公司 Ellisys 對此表示認同,並 “預估 2018 年將有近 40 億臺新的藍芽裝置上市”。請記住,僅在今年就有 40 億藍芽裝置上市。

根據這個趨勢,一家收集“市場和消費資料”的公司 Statista 認為全球的藍芽裝置 將從 2012 年的 35 億增長到 2018 年預估的 100 億

對於你的職業生涯,藍芽意味著什麼

Dogtown Media 有限責任公司,一家 iOS 端“物聯網藍芽應用”精品開發商,該公司聲稱 “根據麥肯錫全球研究所(McKinsey Global Institute)的專家預測,在未來 9 年內,物聯網將對全球經濟產生超過 6 萬億美元的影響” 。這對於像你我這樣的 iOS 開發意味著什麼?Dogtown 說 “未來幾年,對那些有遠見的初創企業和創業者來說,將是令人興奮的、多產的,而且非常有利可圖的。”
翻譯:作為一個有前瞻性或者想創業的青年,應該學習使用藍芽來進行應用程式的開發,因為在這個迅速擴大的市場,你的下個任務或者崗位有很大可能需要這個技能。

免責宣告

  • 我與 Dogtown Media, LLC 之間沒有任何的從屬關係。在搜尋到這篇文章的期間,我發現了這家公司的網站,看到他們專門從事 iOS 端的藍芽開發。
  • 我是 Bluetooth SIG 的一名 “Adopter” 級別成員。

在提交你使用 Core Bluetooth 開發的應用程式被稽核之前

在藍芽技術剛展露頭角之際,我經常看到開發者們找一些參考資料,然後立即投入到涉及無線裝置的應用開發中,並提交藍芽應用到 Apple 的 AppStore 中。我想說:別那麼快,夥計。

Bluetooth SIG 規定,“所有使用藍芽技術的產品必須完成 Bluetooth Qualification Process(藍芽資格稽核)。” 我聽到有人說,“市面上有太多基於藍芽的應用;沒有人會注意到我的”。呃,並不是這樣。藍芽技術有 版權,專利,並且授權 給應用開發者。如果你想讓你的應用程式被聚焦並且展示你整合了藍芽技術的事實,請記住:

Bluetooth 商標 - 包括 BLUETOOTH 文字商標,圖形商標(符文 B 和橢圓形設計),還有組合商標(藍芽文字商標和設計)- 這些都被 Bluetooth SIG 所擁有。只有 Bluetooth SIG 的成員並且擁有對應資格和申報過的產品才可以展示,相關功能或者使用任何商標。為了保護這些商標,Bluetooth SIG 管理了一套執行程式,監控市場並進行稽核,以確保會員使用商標的行為符合藍芽品牌指南,並確保最終釋出的產品與已通過資格審查程式的商品和服務相對應。

來看一下 Bluetooth SIG 的 資質 FAQ

如果我沒有給我的產品申請相應的資質會怎麼樣?

如果你沒有給你的產品申請相應的資質,你將成為執法行動的物件。請閱讀這裡的 更新策略,其中我們概述了升級計劃。如果沒有采取糾正措施,您的 Bluetooth SIG 會員資質可能被暫停或撤銷。

別傻了,別去冒險。最重要的一點是,我們所有人都應該努力追求最高的誠信和誠實,在應該給予信任的時候給予信任,並促進遵守標準,使協同工作成為規範,而不是例外。數千個人貢獻了數千個小時的工作和數百萬美元用於發展藍芽的標準和 多項專利,從而創造了一套明顯有用的知識財產。

別讓我嚇著你

人們常常被「商標」、「專利」、「版權」、「資質」、「會員」、等嚴厲的詞語所嚇倒,尤其是 “強制執行。”不要開始擔心使用藍芽進行開發的事。加入 Bluetooth SIG!它是免費的! 點選這裡,然後:

首先成為一個 Adopter 級別的會員。使用藍芽技術開發一款產品,會員資格是必須的,Adopter 級別會員擁有以下這些福利:
• 根據 Bluetooth Patent/Copyright License Agreement(藍芽專利/版權許可協議) 使用藍芽技術生產產品的許可
• 根據 Bluetooth Trademark License Agreement(藍芽商標許可協議) 在符合條件的產品上使用藍芽商標的許可
• 能夠與數以萬計的 Bluetooth SIG 成員建立網路,並在各種各樣的行業中合作 — 從晶片製造商到應用程式的開發者,裝置製造商和服務提供商
• 能夠參加 SIG 專家組、研究小組和工作組中的子小組
• 訪問諸如 Profile Tuning Suite(PTS)之類的工具,提供協議和協同測試……

成為 Bluetooth SIG 的一員

成為 SIG 的一員 會包含很多好處。你可以免費使用教育工具包、培訓視訊、網路研討會、開發人員論壇、開發人員支援服務、白皮書、產品測試工具,並幫助確保您的應用程式滿足國際監管要求(主要是關於 射頻排放)。

你只要成為會員就會得到一些曝光。我的公司是它的一個成員,所以在 Bluetooth SIG’s Member Directory 中可以 被看到

在 iOS 11 中使用 Core Bluetooth

一旦你開發了一款應用,使其通過 SIG 認證,並獲得 Apple App Store 的許可,那麼你的產品同時也會被 SIG 公開上市,這時你將獲得更多的曝光。

對應用程式進行資格認證既簡單又便宜

當你對自己基於 Core Bluetooth 開發的應用程式感到滿意,並準備將其提交到 Apple App Store 進行稽核,請停下,然後前往 Bluetooth SIG 的網頁對你的應用程式進行 認證。SIG 將為您提供一個整潔的 “Launch Studio”,它是您用來完成 Bluetooth Qualification Process 的線上工具。”

對於大多數應用程式,比如我將在本教程中介紹的 “GATT - based Profile Client(app),”認證和上市的費用是 100 美元。花一些精力來確保您的程式碼符合 Bluetooth 規範和做一些測試,將是非常值得的。最後,可以給你的應用程式印上藍芽的商標。這個 商標 “在全球範圍內都是可識別的,消費者認知度高達92%。”

請不要擔心 100 美元的問題。你更有可能獲得一份擁有豐厚薪水或者時薪的工作,併為公司處理這些藍芽的合規問題。

理解 Core Bluetooth

大多數情況下,使用藍芽裝置是非常簡單的。開發與藍芽通訊的軟體卻有可能非常複雜。這就是為什麼 Apple 創造了 Core Bluetooth 框架

Core Bluetooth 框架讓您的 iOS 和 Mac 應用程式與藍芽低能耗裝置通訊。例如,您的應用程式可以發現、搜尋低能量的外圍裝置還有與之互動,比如心率監視器、數字恆溫器,甚至其他 iOS 裝置。

該框架是藍芽 4.0 規範中關於使用低能耗裝置的抽象。就是說,它為你,也就是開發者,隱藏了規範中很多底層的細節,使你更容易開發與低能耗裝置進行互動的應用程式。因為該框架是基於標準規範的,所有規範中的很多概念和術語被採用了……

請注意是“低能量裝置”。當使用 Core Bluetooth 我們並不是處理如無線揚聲器這樣的經典藍芽裝置。與這類裝置的通訊會很快的耗盡電池能量。Core Bluetooth 是針對“Bluetooth Low Energy”(BLE)的 API,也稱為“Bluetooth 4.0”。BLE 使用的電力要少得多,因為它的設計目的是通訊少量的資料。BLE 裝置的一個很好的例子是心率監測器(HRM)。它幾乎每秒鐘只傳送幾個位元組的資料。這就是為什麼人們可以帶著一個 HRM 或者帶著他們的 iPhone 跑一個小時,記錄跑步期間心率的變化,而看不到電池電量的巨大消耗。注意,隨著本文的進行,像 BLE 這種首字母縮略詞的數量正在增加。

為了我們能夠一起流暢的討論 Core Bluetooth 你需要學習一個新的詞彙表。

分別從 客戶端/服務端和生產者/消費者模型 的角度考慮 BLE 協議。

在 iOS 11 中使用 Core Bluetooth

The Peripheral(外圍裝置)

外圍裝置是硬體/軟體的一部分,就像 HRM。大多數 HRM 裝置蒐集或/和計算資料,如每分鐘心跳、HRM 的電池電量水平、以及所謂的“RR-Interval”。裝置傳輸這些資料到另一個需要它們的實體或實體組。外圍裝置是服務者生產者。市場上比較流行的 HRM 有 Wahoo TICKR,Polar H7,Scosche Rhythm+

在 iOS 11 中使用 Core Bluetooth

我將通過編寫連線到這三種裝置的 Swift 4 程式碼來展示 BLE 等標準的重要性。

Core Bluetooth 視角

來自 蘋果的文件

CBPeripheralDelegate

CBPeripheral 物件的代理必須遵守 CBPeripheralDelegate 協議。代理使用這個協議的方法來對一個遠端外圍裝置的服務和屬性,進行發現、探索、還有互動方面的監控。這個協議裡面沒有必須遵守的方法。

The Central(中央裝置)

中央裝置是硬體/軟體的一部分,就像 iPhone、iPad、MacBook、 iMac 等。這些裝置可以使用應用程式掃描像 HRM 這樣的藍芽外圍裝置。中央裝置是一個客戶以及 消費者。它們與 HRM 是連通的,所以它們可以使用從外圍裝置中取出的像每分鐘心跳、電池的電量水平、還有“RR-Interval”這樣的資料。中央裝置接收這些資料,可以對資料執行增值計算,或者只是通過使用者介面顯示資料,或者是儲存資料以供將來分析、展示,或者是聚合和資料分析(就像統計分析需要足夠的資料來確定重要的和有意義的趨勢),或其他類似的操作。

Core Bluetooth 視角

來自 蘋果的文件

CBCentralManagerDelegate 協議定義了方法,CBCentralManager 物件的代理必須遵守它。協議中的可選方法允許代理來監控對外圍裝置的發現、連線、還有檢索。唯一必須實現的方法表明中央裝置的可用性,並且當中央裝置的狀態發生更新時被呼叫。

通過廣播找到外圍裝置

如果你的 iPhone 或 iPad 找不到這些外設從而不能連線到它們,那麼 HRM 之類的外設就沒什麼用了。因此,它們不斷通過無線頻段傳送著資料的小片段(包),說著類似這樣的話:“嘿,我是 Scosche Rhythm+ 心率檢測器;我能提供類似我的穿戴者每分鐘心率的功能;我能提供類似我的電池電量水平的資訊。”當一個對心率感興趣的中央裝置通過掃描找到了這個外圍裝置,中央裝置將連線到它並且它會停止廣播。

你可能已經使用過 iPhone -> 設定 -> 藍芽 來開啟或關閉藍芽(包括傳統的和 BLE)。當切換到開啟,你可以看到你的 iPhone 掃描裝置並與它們建立連線,就像下面我所截的兩張圖,搜尋,並且將我的 iPhone 連線到一個 Scosche Rhythm+ HRM:

在 iOS 11 中使用 Core Bluetooth

依照 蘋果 的說法:

外圍裝置以廣播包的形式廣播一些資料。一個廣播包是一個相對較小的資料束,其中可能包含外圍裝置所能提供的有用資訊,比如外圍裝置的名字還有主要功能。例如,數字恆溫器可能會廣播它能提供房間的當前溫度。在 BLE 中,廣播是外圍裝置展示其存在的主要方式。另一方面,中央裝置可以掃描和監聽任何外圍裝置,只要這些裝置的廣播資訊是它感興趣的……

在這篇教程中,過一會我會向你展示怎樣使用 Swift 4 來編碼進行外圍裝置的掃描並連線它們。

外圍裝置的各種服務

服務可能不是你認為的那樣。服務描述外圍裝置提供的主要特性或功能。但它並不是一種具體的測量方法,如每分鐘心跳數,而是一種描述從外圍裝置可以得到的與心臟相關的測量方法的分類。

依照 蘋果 的說法:

服務是一個資料和相關行為的集合,用於實現裝置(或裝置的一部分)的功能或特性。比如,心率檢測器的一項服務可能是公開來自監測器的心率感測器的心率資料。

具體定義一個藍芽“服務”,我們應該看看 Bluetooth SIG 的 “GATT Services(服務)” 列表,這裡 GATT 代表 “Generic Attributes(通用屬性)”

向下滾動服務 列表,直到你在 Name(名字) 列中看到 “Heart Rate”。注意, Uniform Type Identifier (統一型別識別符號) 對應的是 “org.bluetooth.service.heart_rate”,Assigned Number(指定編碼) 則是 0x180D。請注意在後面的程式碼中我們將使用 0x180D 這個值。

點選 “Heart Rate(心率)”,你將開啟一個網頁,上面用粗體字寫著 Name: Heart Rate。請注意 Summary(摘要) ,“HEART RATE Service(心率服務)公開心率和其他與心率感測器相關的資料,用於健身應用。”向下滾動頁面就會發現 Heart Rate service 本身並不會提供每分鐘跳動的實際心率。這個服務是一個其他資料片段的集合,它們被稱為 characteristics(特徵)。最後,你會得到一個特徵來提供重要資料:心率。

Core Bluetooth 視角

來自 蘋果的文件

CBService 和它的子類 CBMutableService 代表一個外圍裝置的服務 - 為實現裝置(或裝置的一部分)的功能或特性而收集的資料和相關行為。CBService 物件特指遠端外圍裝置(使用 CBPeripheral 物件來表示)的服務。服務組可能是主要的,也有可能是次要的,可能會包含一個特徵組的程式碼,也有可能會包含一個服務組(代表其他的服務組)。

外圍裝置服務的特徵

外圍裝置的服務常常被分解成更細化但相關的資訊。特徵通常是我們找到重要資訊、真實資料的地方。再次檢視 蘋果 的說明:

服務本身是由特徵或包含的服務(這裡指別的服務)組成。特徵更詳細的提供了外圍裝置的服務資訊。例如,剛才描述的心率服務,可能包含一個描述裝置的心率感測器所在目標身體位置的特徵和另一個傳遞心率測量資料的特徵。

讓我們繼續使用 HRM 作為例子。請返回那個用粗體字寫著 Name: Heart Rate(名字:心率)介面。向下滾動直到你看到 Service Characteristics(服務特徵)。那是一個包含大量後設資料(關於資訊的資料)的大表格。請找到 Heart Rate Measurement(心率測量) 並點選 org.bluetooth.characteristic.heart_rate_measurement 然後審查。稍後我會對這個介面進行解釋。

Core Bluetooth 視角
來自 蘋果的文件

CBCharacteristic 和它的子類 CBMutableCharacteristic 代表關於外圍裝置服務的詳細資訊。CBCharacteristic 物件特指遠端外圍裝置(遠端外圍裝置使用 CBPeripheral 物件表示)服務的特徵。一個特徵包含一個單一的值以及任意個描述符來描述這個值。特徵的屬性描述瞭如何使用這個特徵的值以及如何訪問這些描述符。

GATT 規範

當你使用 Core Bluetooth 開發一款需要與藍芽外圍裝置互動的應用程式時,你首先應該前往 Bluetooth SIG 的首頁。

讓我們一起回顧我曾經的經歷,那會我在開發一個應用程式,用 HRM 做了各種各樣非常好玩的功能。檢視 GATT Specifications(GATT 技術指標) 部分,然後在 GATT Services(GATT 服務) 下面找到你需要的外圍裝置服務。

在本文介紹的 HRM 示例中,首先在 GATT Services(GATT 服務) 介面的 Name(名字) 列中找到 “Heart Rate”(也就是一個超連結)項。點選 “Heart Rate(心率)” 連結並且檢視完整的網站。請記住 Assigned Number(分配符)(0x180D)然後滑動到底部的 Service Characteristics(服務特徵) 表。仔細的檢視錶格並且找到有興趣的特徵。

在這個例子中,閱讀 Heart Rate Measurement(心率測量)Body Sensor Location(感測器所在身體部位) 分割槽,然後點選各自的詳細連結,org.bluetooth.characteristic.heart_rate_measurementorg.bluetooth.characteristic.body_sensor_location

Heart Rate Measurement(心率測量) 以及 Body Sensor Location(感測器所在身體部位) 介面中,分別記住它們的 Assigned Number(分配符),(0x2A37)和(0x2A38),然後檢視介面中的所有資訊,以便了解將被髮送到該 HRM 應用程式中的藍芽編碼資料結構該如何解譯。編寫程式碼時必須把藍芽編碼資料轉換成人類可讀的格式。

隨著本教程的深入,我將向你介紹更多細節,特別是當我向你展示,我用來與 BLE HRM 通訊的應用程式程式碼。

如果你 加入 Bluetooth SIG ,你可以獲得更多關於使用服務和特徵進行程式設計的詳細資訊。

編寫 Core Bluetooth 程式碼

在這次討論中,我將假設你瞭解 iOS 應用程式開發的基礎知識,包括 Swift 程式語言和 Xcode Single View App(單檢視應用程式)模板。測試應用程式的使用者介面(UI),包括 Auto Layout(自動佈局),程式碼如下所示,非常簡單。

我將用一系列步驟來描述程式碼 — 這些步驟在下面的程式碼中同樣會被解釋。因此,在閱讀本節中的步驟時,請參閱下面程式碼中對應的步驟。整個過程基本上是線性的。請記住,其中一些步驟表示回撥 — 正在呼叫的委託方法。

在編寫應用程式時,我會將 Core Bluetooth 元件分解成協議或類 — 例如,將核心功能從 UI 中分離出來。但這段程式碼的目的是向您展示 Core Bluetooth 如何在最少的干擾下工作。我的註釋很簡單,而且有實際意義。在一個頁面中你只會看到重要部分。

示例應用程式樣式

針對這篇文章我所開發的應用程式 UI 極其簡單。當應用程式被啟動,它開始掃描並嘗試匹配一個 HRM。掃描的過程通過 UIActivityIndicatorView 類在螢幕上顯示並旋轉來表明。當沒有匹配上任一 HRM 時,通過一個紅色正方形的 UIView 來表明。一旦發現一個 HRM 並初步連結,UIActivityIndicatorView 停止旋轉並隱藏,並且紅色 UIView 轉變為綠色。當 HRM 完全連結並被訪問,我會顯示 HRM 的品牌型號和穿戴者放置在身體上的預定位置。此時我會開始讀取並且顯示穿戴者每分鐘的心率,大約每秒更新。大多數 HRM 都是每秒傳送一次每分鐘心率值。我人為地設計了一個心率數字的脈衝動畫讓應用看起來更有吸引力,但是你看到的是我真實的心率。當 HRM 斷開連結,我清空所有的資訊文字,將正方形 UIView 轉變為紅色,顯示 UIActivityIndicatorView 並開始旋轉,同時再次開始掃描 HRM。

以下是我的應用程式在與三個不同品牌的 HRM 匹配執行時的樣式 — Scosche Rhythm+,Wahoo TICKR,還有Polar H7:

在 iOS 11 中使用 Core Bluetooth

Rhythm+ 使用紅外光“看”我的靜脈以確定心率。TICKR 和 H7 使用電極檢測告訴我心跳的電脈衝。

逐步瞭解我的程式碼

你可以在下一段找到完整的原始碼。在這裡,我將向你介紹實現步驟。

Step 0.00 : 我必須匯入 CoreBluetooth 框架。

Step 0.0 : 指定 GATT 中的 Assigned Numbers(分配符) 為常量。我這樣做讓藍芽規範的識別符號更具可讀性和可維護性,針對 “心率” 服務,其 “心率測量” 特徵,還有其 “身體感測器位置” 特徵。

Step 0.1 : 建立一個 UIViewController 的子類 HeartRateMonitorViewController。使 HeartRateMonitorViewController 遵守 **CBCentralManagerDelegate****CBPeripheralDelegate** 協議。我使用協議和委託的設計模式,正如我在 AppCoda 文章中 這裡 還有 這裡 分別描述的那樣。我們將實現來自兩個協議的方法。我們將呼叫一些 Core Bluetooth 的方法,一些方法將由 Core Bluetooth 為我們呼叫,以響應我們自己的呼叫。

Step 0.2 : 我們在 HeartRateMonitorViewController 類中定義例項變數,它們代表 CBCentralManagerCBPeripheral 類,所以它們在應用程式的生命週期內都是持續存在的。

Step 1 : 我們為程式在後臺建立一個併發佇列。我希望 Core Bluetooth 的執行發生在後臺。我希望 UI 保持響應。說不定,在一個更復雜的應用程式中,HRM 可能會執行數小時,為使用者收集心率資料。使用者可能希望使用其他應用程式特性,例如,修改應用程式設定,或者,如果使用者正在跑步,並且希望使用 Core Location 來跟蹤跑步的路線。因此,在心率資料正在收集和顯示的同時,使用者可以收集和/或檢視他們的地理位置。

Step 2 : 建立用於掃描、連線、管理和從外圍裝置收集資料的控制中心。這是必要的一步。缺少了控制中心 Core Bluetooth 將無法工作。另一個必要的:由於 HeartRateMonitorViewController 採用了 CBCentralManagerDelegate,我們將 centralManager 的委託屬性設定成 HeartRateMonitorViewControllerself)。同時我們還為控制中心指定了 DispatchQueue

Step 3.1 : centralManagerDidUpdateState 方法的呼叫基於裝置的藍芽狀態。理想情況下,我們應該考慮一個場景,在該場景中,使用者無意(或故意)在 Settings(設定) 應用程式中關閉藍芽。我們只能在藍芽為 .poweredOn 狀態時才能掃描外圍裝置。

Step 3.2 : 控制中心應該掃描感興趣的外圍裝置,但前提是裝置(如iPhone)開啟了藍芽。還記得上面標題為“通過廣播找到外圍裝置”的部分嗎?我們就是這樣處理這個呼叫的。我們的監聽針對正在廣播 心率 服務(0x180D)的 HRM。我們可以通過新增特定服務的 CBUUIDsserviceUUIDs 陣列引數(標記為 withServices),從而達到監聽並且連線更多外圍裝置的目的。例如,在一些健康相關的應用程式中,我們可以監聽並連線到 HRM 血壓監測器或者 BPM(儘管我們需要再建立一個 CBPeripheral 類的例項變數)。注意,如果我們做了這個呼叫:

centralManager?.scanForPeripherals(withServices: nil)
複製程式碼

我們可以監聽範圍內所有藍芽裝置的廣播。在一些藍芽功能類的應用程式中它可能有用。

Step 4.1 : 找到這個應用程式可以連線哪些感興趣的外圍裝置(HRM)。這個 didDiscover 方法告訴我們,在掃描時,控制中心已經發現了正在廣播的 HRM。

Step 4.2 : 我們必須在類的例項變數中儲存剛剛發現的外圍裝置的引用,它將持續存在。如果我們僅僅只是使用了一個區域性變數,我們會倒黴的。

Step 4.3 : 因為 HeartRateMonitorViewController 採用了 CBPeripheralDelegate 協議,所以 peripheralHeartRateMonitor 物件必須將它的 delegate 屬性設定為 HeartRateMonitorViewControllerself)。

Step 5 : 我們在 didDiscover 中告訴控制中心停止掃描以便保護電池壽命。當已經連線的 HRM 或外圍裝置斷開連線時,我們可以再次開啟掃描。

Step 6 : 此時還在 didDiscover 中,我們連線到被發現的感興趣的外圍裝置,一個 HRM。

Step 7 : didConnect 方法僅僅“當成功與一個外圍裝置連線時呼叫。”請注意“成功”這個詞。如果你發現一個外圍裝置但不能連線,那麼你需要進行一些除錯。請注意我更新了 UI 用來顯示我連線了那個外圍裝置,並表明我已經停止掃描,以及其他一些事情。

Step 8 : 此時還在 didConnect 方法中,我們在外圍裝置上尋找感興趣的服務。具體來說,我們希望找到 Heart Rate(心率)(0x180D)服務。

Step 9 :didDiscoverServices 方法被呼叫的時候,說明在我們所連線的外圍裝置上發現了 “Heart Rate(心率)” 服務。請記住我們需要尋找感興趣的特徵。這裡我對 Heart Rate(心率) 服務的所有特徵進行了一次迴圈以找到我接下來要用的那個。如果你前往 Bluetooth SIG 網頁中 “Heart Rate(心率)” 服務對應的頁面,滾動到下面標記為 Service Characteristics(服務特徵) 的分割槽,就可以檢視那三個可用的特徵。

Step 10 : didDiscoverCharacteristicsFor service 方法證明我們已經發現了感興趣的服務中所有的特徵。

Step 11 : 首先,我訂閱了一個通知 - “read” - 關於感興趣的 Body Sensor Location(感測器所在身體部位) 特徵。前往 “Heart Rate(心率)” 服務的頁面,你會發現這個特徵被標記為“Read Mandatory。”呼叫 peripheral.readValue 將會引起 peripheral:didUpdateValueForCharacteristic:error: 方法稍後被呼叫,所以我可以將這個特徵解析成人類語言。其次,我訂閱了一個定期通知 — “notify” — 關於感興趣的 Heart Rate Measurement(心率測量) 特徵。前往 “Heart Rate(心率)” 服務的頁面,你會發現這個特徵被標記為“Notify Mandatory。”呼叫 peripheral.setNotifyValue 將會引起 peripheral:didUpdateValueForCharacteristic:error: 方法稍後被呼叫,並且是幾乎每一秒鐘觸發一次,所以我可以將這個特徵解析成人類語言。

Step 12 : 因為我對特徵 Body Sensor Location(感測器所在身體部位) (0x2A38)訂閱了讀取值,並且對特徵 Heart Rate Measurement(心率測量) (0x2A37)訂閱了定期獲取通知,所以如果它們傳送值或者定期更新,我將分別獲得這兩個二進位制值。

Step 13 : 將 BLE Heart Rate Measurement(心率測量) 的資料解譯成人們可讀的格式。前往 GATT 規範的 頁面 找到這個特徵。第一個位元組是關於其餘資料的後設資料 (標記)。規範告訴我看第一個位元組的最低有效位,Heart Rate Value Format bit(心率值的標識位)。如果是 0(zero),每分鐘的心跳數將以 UINT8 格式在第二位元組。我從來沒有遇到過一個 HRM 使用第二個位元組以外的任何位元組,我在這裡演示的三個 HRM 也不例外。這就是為什麼我忽略了 Heart Rate Value Format bit(心率值的標識位) 值為 1(one)的用例。我看過所有被提到的實現,但從來沒有能夠測試這些實現。對於我無法重現的情況,我不會發表任何看法。

Step 14 : 將 BLE Body Sensor Location(感測器所在身體部位) 的資料解譯成人們可讀的格式。前往 GATT 規範的 頁面 找到這個特徵。這個特徵非常簡單。將值 1、2、3、4、5、6 或 7 儲存在 8 位中。形成的文字字串與這些值以解譯為目的的展示是一樣的。

Step 15 : 當一個外圍裝置從控制中心斷開時,採取適當的行動。我更新我的 UI 以及……

Step 16 : 開始掃描,為了發現一個正在廣播 Heart Rate(心率) 服務(0x180D)的外圍裝置。

我的原始碼

這裡是對於我們剛剛所討論的實現,完整的原始碼:

import UIKit

// STEP 0.00: 必須匯入 CoreBluetooth framework
import CoreBluetooth

// STEP 0.0: 指定 GATT 中的 "Assigned Numbers" 為常量,這樣它們會擁有更好的可讀性和可維護性

// MARK: - Core Bluetooth 服務 ID
let BLE_Heart_Rate_Service_CBUUID = CBUUID(string: "0x180D")

// MARK: - Core Bluetooth 特徵 ID
let BLE_Heart_Rate_Measurement_Characteristic_CBUUID = CBUUID(string: "0x2A37")
let BLE_Body_Sensor_Location_Characteristic_CBUUID = CBUUID(string: "0x2A38")

// STEP 0.1: 這個類同時採用了控制中心和外圍裝置的委託協議,所以必須遵守這些協議的要求
class HeartRateMonitorViewController: UIViewController, CBCentralManagerDelegate, CBPeripheralDelegate {
    
    // MARK: - Core Bluetooth 類的成員變數
    
    // STEP 0.2: 分別建立 CBCentralManager 和 CBPeripheral 的例項變數
    // 所以它們在應用程式的生命週期裡持續存在
    var centralManager: CBCentralManager?
    var peripheralHeartRateMonitor: CBPeripheral?
    
    // MARK: - UI outlets / 成員變數
    
    @IBOutlet weak var connectingActivityIndicator: UIActivityIndicatorView!
    @IBOutlet weak var connectionStatusView: UIView!
    @IBOutlet weak var brandNameTextField: UITextField!
    @IBOutlet weak var sensorLocationTextField: UITextField!
    @IBOutlet weak var beatsPerMinuteLabel: UILabel!
    @IBOutlet weak var bluetoothOffLabel: UILabel!
    
    // 設定 HealthKit 
    let healthKitInterface = HealthKitInterface()
    
    // MARK: - UIViewController delegate
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 在檢視載入完成以後,通常是通過一個 nib,做所有附加的設定。
        
        // 最初,我們在進行掃描並且沒有產生連線
        connectingActivityIndicator.backgroundColor = UIColor.white
        connectingActivityIndicator.startAnimating()
        connectionStatusView.backgroundColor = UIColor.red
        brandNameTextField.text = "----"
        sensorLocationTextField.text = "----"
        beatsPerMinuteLabel.text = "---"
        // 以防 Bluetooth 被關閉
        bluetoothOffLabel.alpha = 0.0
        
        // STEP 1: 為控制中心在後臺建立一個併發佇列
        let centralQueue: DispatchQueue = DispatchQueue(label: "com.iosbrain.centralQueueName", attributes: .concurrent)
        // STEP 2: 建立用於掃描、連線、管理和從外圍裝置收集資料的控制中心。
        centralManager = CBCentralManager(delegate: self, queue: centralQueue)
        
        // 從 HKHealthStore 讀取心率資料
        // healthKitInterface.readHeartRateData()
        
        // 從 HKHealthStore 讀取性別型別
        // healthKitInterface.readGenderType()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // 處理任何可以重新建立的資源
    }
    
    // MARK: - CBCentralManagerDelegate methods

    // STEP 3.1: 這個方法的呼叫基於裝置的藍芽狀態; 
    // 僅在 Bluetooth 為 .poweredOn 時才可以掃描外圍裝置
    func centralManagerDidUpdateState(_ central: CBCentralManager) {
        
        switch central.state {
        
        case .unknown:
            print("Bluetooth status is UNKNOWN")
            bluetoothOffLabel.alpha = 1.0
        case .resetting:
            print("Bluetooth status is RESETTING")
            bluetoothOffLabel.alpha = 1.0
        case .unsupported:
            print("Bluetooth status is UNSUPPORTED")
            bluetoothOffLabel.alpha = 1.0
        case .unauthorized:
            print("Bluetooth status is UNAUTHORIZED")
            bluetoothOffLabel.alpha = 1.0
        case .poweredOff:
            print("Bluetooth status is POWERED OFF")
            bluetoothOffLabel.alpha = 1.0
        case .poweredOn:
            print("Bluetooth status is POWERED ON")
            
            DispatchQueue.main.async { () -> Void in
                self.bluetoothOffLabel.alpha = 0.0
                self.connectingActivityIndicator.startAnimating()
            }
            
            // STEP 3.2: 掃描我們感興趣的外圍裝置
            centralManager?.scanForPeripherals(withServices: [BLE_Heart_Rate_Service_CBUUID])
            
        } // END switch
        
    } // END func centralManagerDidUpdateState
    
    // STEP 4.1: 找到這個應用程式可以連線哪些感興趣的外圍裝置
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        
        print(peripheral.name!)
        decodePeripheralState(peripheralState: peripheral.state)
        // STEP 4.2: 必須儲存一個外圍裝置的引用到類的例項變數中
        peripheralHeartRateMonitor = peripheral
        // STEP 4.3: 因為 HeartRateMonitorViewController 採用了 CBPeripheralDelegate 協議,
        // 所以 peripheralHeartRateMonitor 必須設定他的 
        // delegate 屬性為 HeartRateMonitorViewController (self)
        peripheralHeartRateMonitor?.delegate = self
        
        // STEP 5: 停止掃描以保護電池的壽命;當斷開連結的時候再次掃描。
        centralManager?.stopScan()
        
        // STEP 6: 與已經發現的,感興趣的外圍裝置建立連線
        centralManager?.connect(peripheralHeartRateMonitor!)
        
    } // END func centralManager(... didDiscover peripheral
    
    // STEP 7: “當一個與外圍裝置的連線被成功建立時呼叫。”
    // 只有當我們知道與外圍裝置的連線建立成功之後才能前往下一步
    func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        
        DispatchQueue.main.async { () -> Void in
            
            self.brandNameTextField.text = peripheral.name!
            self.connectionStatusView.backgroundColor = UIColor.green
            self.beatsPerMinuteLabel.text = "---"
            self.sensorLocationTextField.text = "----"
            self.connectingActivityIndicator.stopAnimating()
            
        }
        
        // STEP 8: 在外圍裝置上尋找感興趣的服務
        peripheralHeartRateMonitor?.discoverServices([BLE_Heart_Rate_Service_CBUUID])

    } // END func centralManager(... didConnect peripheral
    
    // STEP 15: 當一個外圍裝置斷開連線,使用適當的方法
    func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
        
        // print("Disconnected!")
        
        DispatchQueue.main.async { () -> Void in
            
            self.brandNameTextField.text = "----"
            self.connectionStatusView.backgroundColor = UIColor.red
            self.beatsPerMinuteLabel.text = "---"
            self.sensorLocationTextField.text = "----"
            self.connectingActivityIndicator.startAnimating()
            
        }
        
        // STEP 16: 在這個用例中,開始掃描相同或其他的外設,只要它們是 HRM,就可以重新聯機
        centralManager?.scanForPeripherals(withServices: [BLE_Heart_Rate_Service_CBUUID])
        
    } // END func centralManager(... didDisconnectPeripheral peripheral

    // MARK: - CBPeripheralDelegate methods
    
    func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        
        for service in peripheral.services! {
            
            if service.uuid == BLE_Heart_Rate_Service_CBUUID {
                
                print("Service: \(service)")
                
                // STEP 9: 在感興趣的服務中尋找感興趣的特徵
                peripheral.discoverCharacteristics(nil, for: service)
                
            }
            
        }
        
    } // END func peripheral(... didDiscoverServices
    
    // STEP 10: 從感興趣的服務中,確認我們所發現感興趣的特徵
    func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        
        for characteristic in service.characteristics! {
            print(characteristic)
            
            if characteristic.uuid == BLE_Body_Sensor_Location_Characteristic_CBUUID {
                
                // STEP 11: 訂閱關於感興趣特徵的單次通知;
                // “當你使用這個方法去讀取特徵的值時,外圍裝置將會呼叫…… 
                // peripheral:didUpdateValueForCharacteristic:error:”
                //
                // Read    Mandatory
                //
                peripheral.readValue(for: characteristic)
                
            }

            if characteristic.uuid == BLE_Heart_Rate_Measurement_Characteristic_CBUUID {

                // STEP 11: 訂閱關於感興趣特徵的持續通知;
                // “當你啟用特徵值的通知時,外圍裝置呼叫……
                // peripheral(_:didUpdateValueFor:error:)” 
                //
                // Notify    Mandatory
                //
                peripheral.setNotifyValue(true, for: characteristic)
                
            }
            
        } // END for
        
    } // END func peripheral(... didDiscoverCharacteristicsFor service
    
    // STEP 12: 每當一個特徵值定期更新或者釋出一次時,我們都會收到通知;
    // 閱讀並解譯我們訂閱的特徵值
    func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
        
        if characteristic.uuid == BLE_Heart_Rate_Measurement_Characteristic_CBUUID {
            
            // STEP 13: 通常我們需要將 BLE 的資料解析成人類可讀的格式
            let heartRate = deriveBeatsPerMinute(using: characteristic)
            
            DispatchQueue.main.async { () -> Void in
                
                UIView.animate(withDuration: 1.0, animations: {
                    self.beatsPerMinuteLabel.alpha = 1.0
                    self.beatsPerMinuteLabel.text = String(heartRate)
                }, completion: { (true) in
                    self.beatsPerMinuteLabel.alpha = 0.0
                })
                
            } // END DispatchQueue.main.async...

        } // END if characteristic.uuid ==...
        
        if characteristic.uuid == BLE_Body_Sensor_Location_Characteristic_CBUUID {
            
            // STEP 14: 通常我們需要將 BLE 的資料解析成人類可讀的格式
            let sensorLocation = readSensorLocation(using: characteristic)

            DispatchQueue.main.async { () -> Void in
                self.sensorLocationTextField.text = sensorLocation
            }
        } // END if characteristic.uuid ==...
        
    } // END func peripheral(... didUpdateValueFor characteristic
    
    // MARK: - Utilities
    
    func deriveBeatsPerMinute(using heartRateMeasurementCharacteristic: CBCharacteristic) -> Int {
        
        let heartRateValue = heartRateMeasurementCharacteristic.value!
        // 轉換為無符號 8 位整數陣列
        let buffer = [UInt8](heartRateValue)

        // UInt8: “一個 8 位無符號整數型別。”
        
        // 在緩衝區的第一個位元組(8 位)是標記(後設資料,用於管理包中其餘部分);
        // 如果最低有效位(LSB)是 0,心率(bpm)則是 UInt8 格式,
        // 如果 LSB 是 1,BPM 則是 UInt16
        if ((buffer[0] & 0x01) == 0) {
            // 第二個位元組:“心率的格式被設定為 UINT8”
            print("BPM is UInt8")
            // 將心率寫入 HKHealthStore
            // healthKitInterface.writeHeartRateData(heartRate: Int(buffer[1]))
            return Int(buffer[1])
        } else { // 我從來沒有看到過這個用例,所以我把它留給理論學家去爭論
            // 第二個和第三個位元組:“心率的格式被設定為 UINT16”
            print("BPM is UInt16")
            return -1
        }
        
    } // END func deriveBeatsPerMinute
    
    func readSensorLocation(using sensorLocationCharacteristic: CBCharacteristic) -> String {
        
        let sensorLocationValue = sensorLocationCharacteristic.value!
        //  轉換為無符號 8 位整數陣列
        let buffer = [UInt8](sensorLocationValue)
        var sensorLocation = ""
        
        // 只看 8 位
        if buffer[0] == 1
        {
            sensorLocation = "Chest"
        }
        else if buffer[0] == 2
        {
            sensorLocation = "Wrist"
        }
        else
        {
            sensorLocation = "N/A"
        }
        
        return sensorLocation
        
    } // END func readSensorLocation
    
    func decodePeripheralState(peripheralState: CBPeripheralState) {
        
        switch peripheralState {
            case .disconnected:
                print("Peripheral state: disconnected")
            case .connected:
                print("Peripheral state: connected")
            case .connecting:
                print("Peripheral state: connecting")
            case .disconnecting:
                print("Peripheral state: disconnecting")
        }
        
    } // END func decodePeripheralState(peripheralState

} // END class HeartRateMonitorViewController
複製程式碼

總結

我希望你喜歡這篇教程。買或者借一個 BLE 裝置,然後使用我的程式碼或自己編寫程式碼來連線它。遵循教程中所有我提供的超連結並且閱讀它們。查閱 Bluetooth SIG 的 網頁 以及蘋果的 Core Bluetooth這裡 也可以看到)框架文件,你一定可以對藍芽技術有一個概覽。

感謝閱讀。記得享受你的工作。不要忘記,當你的簡歷上面有藍芽的經驗將是你的職業生涯的一大亮點。

作為參考,你可以 在 GitHub 上面檢視完整的原始碼

本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 swift.gg

相關文章