首頁(yè) 資訊 Halo 正式開(kāi)源: 使用可穿戴設(shè)備進(jìn)行開(kāi)源健康追蹤

Halo 正式開(kāi)源: 使用可穿戴設(shè)備進(jìn)行開(kāi)源健康追蹤

來(lái)源:泰然健康網(wǎng) 時(shí)間:2024年11月28日 03:19

在飛速發(fā)展的可穿戴技術(shù)領(lǐng)域,我們正處于一個(gè)十字路口。市場(chǎng)上充斥著各式時(shí)尚、功能豐富的設(shè)備,聲稱(chēng)能夠徹底改變我們對(duì)健康和健身的方式。然而,在這些光鮮的外觀和營(yíng)銷(xiāo)宣傳背后,隱藏著一個(gè)令人擔(dān)憂的現(xiàn)實(shí):大多數(shù)這些設(shè)備是封閉系統(tǒng),其內(nèi)部運(yùn)行被專(zhuān)有代碼和封閉硬件所掩蓋。作為消費(fèi)者,我們對(duì)這些設(shè)備如何收集、處理及可能共享我們的健康數(shù)據(jù)一無(wú)所知。

這時(shí),Halo 出現(xiàn)了,它是一種旨在讓健康追蹤更加普惠化的開(kāi)源替代方案。通過(guò)這系列文章,我們將引導(dǎo)你從基礎(chǔ)入手,構(gòu)建并使用完全透明、可定制的可穿戴設(shè)備。

需要說(shuō)明的是,Halo 的目標(biāo)并不是在拋光度或功能完整性上與消費(fèi)級(jí)可穿戴設(shè)備競(jìng)爭(zhēng)。相反,它提供了一種獨(dú)特的、動(dòng)手實(shí)踐的方式來(lái)理解健康追蹤設(shè)備背后的技術(shù)。

我們將使用 Swift 5 來(lái)構(gòu)建對(duì)應(yīng)的 iOS 界面,以及 Python >= 3.10。由于此項(xiàng)目的代碼完全 開(kāi)源,你可以隨時(shí)提交合并請(qǐng)求,或者分叉項(xiàng)目以探索全新的方向。

你將需要:

獲取 COLMI R02 實(shí)際設(shè)備,價(jià)格在撰寫(xiě)時(shí)為 11 到 30 美金左右。 一個(gè)安裝了 Xcode 16 的開(kāi)發(fā)環(huán)境,以及可選的 Apple 開(kāi)發(fā)者計(jì)劃會(huì)員資格。 Python >= 3.10,并安裝了 pandas、numpy、torch 當(dāng)然還有 transformers。

致謝

此項(xiàng)目基于 Python 倉(cāng)庫(kù) 的代碼及我的學(xué)習(xí)成果構(gòu)建。

免責(zé)聲明

作為一名醫(yī)生,我有法律義務(wù)提醒你:你即將閱讀的內(nèi)容并不是醫(yī)學(xué)建議。現(xiàn)在,讓我們開(kāi)始讓一些可穿戴設(shè)備發(fā)出蜂鳴聲吧!

配對(duì)戒指

在進(jìn)入代碼之前,讓我們先了解藍(lán)牙低能耗(BLE)的關(guān)鍵規(guī)格。BLE 基于一個(gè)簡(jiǎn)單的客戶端-服務(wù)器模型,使用三個(gè)核心概念:中央設(shè)備(Centrals)、服務(wù)(Services) 和 特征(Characteristics)。以下是它們的具體介紹:

中央設(shè)備(例如你的 iPhone)負(fù)責(zé)啟動(dòng)和管理與外設(shè)(例如我們的 COLMI R02 戒指)的連接。戒指通過(guò)廣播自身信息等待手機(jī)連接,每次僅支持一臺(tái)手機(jī)連接。 服務(wù)是戒指上相關(guān)功能的集合,例如心率監(jiān)測(cè)服務(wù)或電池狀態(tài)服務(wù)。每個(gè)服務(wù)都有一個(gè)唯一標(biāo)識(shí)符(UUID),客戶端通過(guò)它來(lái)找到對(duì)應(yīng)服務(wù)。 特征是每個(gè)服務(wù)中的具體數(shù)據(jù)點(diǎn)或控制機(jī)制。例如,它們可能是只讀(獲取傳感器數(shù)據(jù))、只寫(xiě)(發(fā)送命令)或兩者兼有。有些特征還能在其值發(fā)生變化時(shí)自動(dòng)通知手機(jī),這對(duì)于實(shí)時(shí)健康監(jiān)測(cè)尤為重要。

當(dāng)手機(jī)連接到戒指時(shí),會(huì)定位所需的服務(wù),并與特定特征交互以發(fā)送命令或接收數(shù)據(jù)。這種結(jié)構(gòu)化的方法不僅確保了通信效率,還能延長(zhǎng)電池使用時(shí)間。了解了這些基礎(chǔ)知識(shí)后,讓我們開(kāi)始構(gòu)建吧!

設(shè)置 Xcode 項(xiàng)目

創(chuàng)建一個(gè)名為 Halo 的新項(xiàng)目,目標(biāo)平臺(tái)為 iOS。組織標(biāo)識(shí)符建議使用反向域名格式(如 com.example)。本項(xiàng)目中,我們使用 com.FirstNameLastName。

接下來(lái),為應(yīng)用啟用必要的功能。在 Xcode 中,打開(kāi) Signing & Capabilities 選項(xiàng)卡,啟用以下 后臺(tái)模式(Background Modes),以確保應(yīng)用在后臺(tái)運(yùn)行時(shí)能夠保持與戒指的連接并處理數(shù)據(jù)。

然后,我們將使用 Apple 提供的最新框架 AccessorySetupKit,用于將藍(lán)牙和 Wi-Fi 配件連接到 iOS 應(yīng)用。此框架自 iOS 18 推出,替代了傳統(tǒng)的廣泛藍(lán)牙權(quán)限請(qǐng)求方式,專(zhuān)注于為用戶明確批準(zhǔn)的特定設(shè)備提供訪問(wèn)權(quán)限。

當(dāng)用戶嘗試將 COLMI R02 戒指連接到應(yīng)用時(shí),AccessorySetupKit 會(huì)顯示一個(gè)系統(tǒng)界面,僅列出兼容的附近設(shè)備。用戶選擇設(shè)備后,應(yīng)用即可與戒指通信,而無(wú)需請(qǐng)求完整的藍(lán)牙權(quán)限。這大大提升了用戶隱私,同時(shí)簡(jiǎn)化了設(shè)備連接的管理流程。

打開(kāi) Info.plist 文件(可以在左側(cè)邊欄中找到,或通過(guò) Project Navigator (?1) > Your Target > Info 定位)。添加以下鍵值條目以支持與 COLMI R02 戒指的配對(duì):

添加 NSAccessorySetupKitSupports,類(lèi)型為 Array,并將 Bluetooth 作為第一個(gè)項(xiàng)目。 添加 NSAccessorySetupBluetoothServices,類(lèi)型為 Array,并將以下 UUID 作為 String 項(xiàng): 6E40FFF0-B5A3-F393-E0A9-E50E24DCCA9E 0000180A-0000-1000-8000-00805F9B34FB

至此,初步配置完成!

Ring Session Manager 類(lèi)

接下來(lái),我們將創(chuàng)建一個(gè) RingSessionManager 類(lèi),用于管理所有與戒指的通信。此類(lèi)的主要職責(zé)包括:

掃描附近的戒指 連接到戒指 發(fā)現(xiàn)服務(wù)和特征 實(shí)現(xiàn)數(shù)據(jù)讀寫(xiě)操作 第一步:創(chuàng)建 RingSessionManager

首先創(chuàng)建一個(gè)新的 Swift 文件(?N),命名為 RingSessionManager.swift。以下是類(lèi)的定義以及需要實(shí)現(xiàn)的關(guān)鍵屬性:

@Observableclass RingSessionManager: NSObject { // 追蹤連接狀態(tài) var peripheralConnected = false var pickerDismissed = true // 存儲(chǔ)當(dāng)前連接的戒指 var currentRing: ASAccessory? private var session = ASAccessorySession() // 核心藍(lán)牙對(duì)象 private var manager: CBCentralManager? private var peripheral: CBPeripheral?} 第二步:發(fā)現(xiàn)戒指

戒指通過(guò)特定的藍(lán)牙服務(wù) UUID 進(jìn)行廣播。為了找到它,我們需要?jiǎng)?chuàng)建一個(gè) ASDiscoveryDescriptor 對(duì)象,指定其藍(lán)牙服務(wù)的 UUID。以下代碼完成了這一功能:

private static let ring: ASPickerDisplayItem = { let descriptor = ASDiscoveryDescriptor() descriptor.bluetoothServiceUUID = CBUUID(string: "6E40FFF0-B5A3-F393-E0A9-E50E24DCCA9E") return ASPickerDisplayItem( name: "COLMI R02 Ring", productImage: UIImage(named: "colmi")!, descriptor: descriptor )}()

確保將戒指圖片添加到項(xiàng)目資源目錄中,或者用合適的占位符替換 UIImage(named: "colmi")!。

第三步:顯示戒指選擇器

為了讓用戶選擇戒指,我們調(diào)用系統(tǒng)內(nèi)置的設(shè)備選擇器界面:

func presentPicker() { session.showPicker(for: [Self.ring]) { error in if let error { print("Failed to show picker: (error.localizedDescription)") } }} 第四步:處理戒指選擇

當(dāng)用戶從選擇器中選定設(shè)備后,應(yīng)用需要處理連接和管理邏輯。以下代碼實(shí)現(xiàn)了事件處理:

private func handleSessionEvent(event: ASAccessoryEvent) { switch event.eventType { case .accessoryAdded: guard let ring = event.accessory else { return } saveRing(ring: ring) case .activated: // 重新連接已配對(duì)戒指 guard let ring = session.accessories.first else { return } saveRing(ring: ring) case .accessoryRemoved: currentRing = nil manager = nil }} 第五步:建立連接

完成選擇戒指后,我們需要與其建立藍(lán)牙連接:

func connect() { guard let manager, manager.state == .poweredOn, let peripheral else { return } let options: [String: Any] = [ CBConnectPeripheralOptionNotifyOnConnectionKey: true, CBConnectPeripheralOptionNotifyOnDisconnectionKey: true, CBConnectPeripheralOptionStartDelayKey: 1 ] manager.connect(peripheral, options: options)} 第六步:理解委托方法

在 RingSessionManager 中,我們實(shí)現(xiàn)了兩個(gè)關(guān)鍵的委托協(xié)議,用于管理藍(lán)牙通信過(guò)程。

中央管理器委托(CBCentralManagerDelegate)
此委托主要處理藍(lán)牙連接的整體狀態(tài)。

func centralManagerDidUpdateState(_ central: CBCentralManager) { print("Central manager state: (central.state)") switch central.state { case .poweredOn: if let peripheralUUID = currentRing?.bluetoothIdentifier { if let knownPeripheral = central.retrievePeripherals(withIdentifiers: [peripheralUUID]).first { print("Found previously connected peripheral") peripheral = knownPeripheral peripheral?.delegate = self connect() } else { print("Known peripheral not found, starting scan") } } default: peripheral = nil }}

當(dāng)藍(lán)牙開(kāi)啟時(shí),程序會(huì)檢查是否有已連接的戒指,并嘗試重新連接。
成功連接后:

func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { print("DEBUG: Connected to peripheral: (peripheral)") peripheral.delegate = self print("DEBUG: Discovering services...") peripheral.discoverServices([CBUUID(string: Self.ringServiceUUID)]) peripheralConnected = true}

斷開(kāi)連接時(shí):

func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: (any Error)?) { print("Disconnected from peripheral: (peripheral)") peripheralConnected = false characteristicsDiscovered = false}

外設(shè)委托(CBPeripheralDelegate)

此委托主要處理與戒指的具體通信。
首先發(fā)現(xiàn)戒指的服務(wù):

func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: (any Error)?) { print("DEBUG: Services discovery callback, error: (String(describing: error))") guard error == nil, let services = peripheral.services else { print("DEBUG: No services found or error occurred") return } print("DEBUG: Found (services.count) services") for service in services { if service.uuid == CBUUID(string: Self.ringServiceUUID) { print("DEBUG: Found ring service, discovering characteristics...") peripheral.discoverCharacteristics([ CBUUID(string: Self.uartRxCharacteristicUUID), CBUUID(string: Self.uartTxCharacteristicUUID) ], for: service) } }}

發(fā)現(xiàn)特征后:

func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { print("DEBUG: Characteristics discovery callback, error: (String(describing: error))") guard error == nil, let characteristics = service.characteristics else { print("DEBUG: No characteristics found or error occurred") return } print("DEBUG: Found (characteristics.count) characteristics") for characteristic in characteristics { switch characteristic.uuid { case CBUUID(string: Self.uartRxCharacteristicUUID): print("DEBUG: Found UART RX characteristic") self.uartRxCharacteristic = characteristic case CBUUID(string: Self.uartTxCharacteristicUUID): print("DEBUG: Found UART TX characteristic") self.uartTxCharacteristic = characteristic peripheral.setNotifyValue(true, for: characteristic) default: print("DEBUG: Found other characteristic: (characteristic.uuid)") } } characteristicsDiscovered = true}

接收數(shù)據(jù)時(shí):

func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { if characteristic.uuid == CBUUID(string: Self.uartTxCharacteristicUUID) { if let value = characteristic.value { print("Received value: (value)") } }}

發(fā)送命令后:

func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { if let error = error { print("Write to characteristic failed: (error.localizedDescription)") } else { print("Write to characteristic successful") }} 完整代碼

完整的 RingSessionManager 類(lèi)代碼如下:

import Foundationimport AccessorySetupKitimport CoreBluetoothimport SwiftUI @Observableclass RingSessionManager: NSObject { var peripheralConnected = false var pickerDismissed = true var currentRing: ASAccessory? private var session = ASAccessorySession() private var manager: CBCentralManager? private var peripheral: CBPeripheral? private var uartRxCharacteristic: CBCharacteristic? private var uartTxCharacteristic: CBCharacteristic? private static let ringServiceUUID = "6E40FFF0-B5A3-F393-E0A9-E50E24DCCA9E" private static let uartRxCharacteristicUUID = "6E400002-B5A3-F393-E0A9-E50E24DCCA9E" private static let uartTxCharacteristicUUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" private static let deviceInfoServiceUUID = "0000180A-0000-1000-8000-00805F9B34FB" private static let deviceHardwareUUID = "00002A27-0000-1000-8000-00805F9B34FB" private static let deviceFirmwareUUID = "00002A26-0000-1000-8000-00805F9B34FB" private static let ring: ASPickerDisplayItem = { let descriptor = ASDiscoveryDescriptor() descriptor.bluetoothServiceUUID = CBUUID(string: ringServiceUUID) return ASPickerDisplayItem( name: "COLMI R02 Ring", productImage: UIImage(named: "colmi")!, descriptor: descriptor ) }() private var characteristicsDiscovered = false override init() { super.init() self.session.activate(on: DispatchQueue.main, eventHandler: handleSessionEvent(event:)) } // MARK: - RingSessionManager actions func presentPicker() { session.showPicker(for: [Self.ring]) { error in if let error { print("Failed to show picker due to: (error.localizedDescription)") } } } func removeRing() { guard let currentRing else { return } if peripheralConnected { disconnect() } session.removeAccessory(currentRing) { _ in self.currentRing = nil self.manager = nil } } func connect() { guard let manager, manager.state == .poweredOn, let peripheral else { return } let options: [String: Any] = [ CBConnectPeripheralOptionNotifyOnConnectionKey: true, CBConnectPeripheralOptionNotifyOnDisconnectionKey: true, CBConnectPeripheralOptionStartDelayKey: 1 ] manager.connect(peripheral, options: options) } func disconnect() { guard let peripheral, let manager else { return } manager.cancelPeripheralConnection(peripheral) } // MARK: - ASAccessorySession functions private func saveRing(ring: ASAccessory) { currentRing = ring if manager == nil { manager = CBCentralManager(delegate: self, queue: nil) } } private func handleSessionEvent(event: ASAccessoryEvent) { switch event.eventType { case .accessoryAdded, .accessoryChanged: guard let ring = event.accessory else { return } saveRing(ring: ring) case .activated: guard let ring = session.accessories.first else { return } saveRing(ring: ring) case .accessoryRemoved: self.currentRing = nil self.manager = nil case .pickerDidPresent: pickerDismissed = false case .pickerDidDismiss: pickerDismissed = true default: print("Received event type (event.eventType)") } }} // MARK: - CBCentralManagerDelegateextension RingSessionManager: CBCentralManagerDelegate { func centralManagerDidUpdateState(_ central: CBCentralManager) { print("Central manager state: (central.state)") switch central.state { case .poweredOn: if let peripheralUUID = currentRing?.bluetoothIdentifier { if let knownPeripheral = central.retrievePeripherals(withIdentifiers: [peripheralUUID]).first { print("Found previously connected peripheral") peripheral = knownPeripheral peripheral?.delegate = self connect() } else { print("Known peripheral not found, starting scan") } } default: peripheral = nil } } func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) { print("DEBUG: Connected to peripheral: (peripheral)") peripheral.delegate = self print("DEBUG: Discovering services...") peripheral.discoverServices([CBUUID(string: Self.ringServiceUUID)]) peripheralConnected = true } func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: (any Error)?) { print("Disconnected from peripheral: (peripheral)") peripheralConnected = false characteristicsDiscovered = false } func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: (any Error)?) { print("Failed to connect to peripheral: (peripheral), error: (error.debugDescription)") }} // MARK: - CBPeripheralDelegateextension RingSessionManager: CBPeripheralDelegate { func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: (any Error)?) { print("DEBUG: Services discovery callback, error: (String(describing: error))") guard error == nil, let services = peripheral.services else { print("DEBUG: No services found or error occurred") return } print("DEBUG: Found (services.count) services") for service in services { if service.uuid == CBUUID(string: Self.ringServiceUUID) { print("DEBUG: Found ring service, discovering characteristics...") peripheral.discoverCharacteristics([ CBUUID(string: Self.uartRxCharacteristicUUID), CBUUID(string: Self.uartTxCharacteristicUUID) ], for: service) } } } func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) { print("DEBUG: Characteristics discovery callback, error: (String(describing: error))") guard error == nil, let characteristics = service.characteristics else { print("DEBUG: No characteristics found or error occurred") return } print("DEBUG: Found (characteristics.count) characteristics") for characteristic in characteristics { switch characteristic.uuid { case CBUUID(string: Self.uartRxCharacteristicUUID): print("DEBUG: Found UART RX characteristic") self.uartRxCharacteristic = characteristic case CBUUID(string: Self.uartTxCharacteristicUUID): print("DEBUG: Found UART TX characteristic") self.uartTxCharacteristic = characteristic peripheral.setNotifyValue(true, for: characteristic) default: print("DEBUG: Found other characteristic: (characteristic.uuid)") } } characteristicsDiscovered = true } func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) { if characteristic.uuid == CBUUID(string: Self.uartTxCharacteristicUUID) { if let value = characteristic.value { print("Received value: (value)") } } } func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) { if let error = error { print("Write to characteristic failed: (error.localizedDescription)") } else { print("Write to characteristic successful") } }}

最后一步:將其應(yīng)用到我們的應(yīng)用程序中

在 ContentView.swift 中粘貼以下代碼,作為主界面的一部分:

import SwiftUIimport AccessorySetupKit struct ContentView: View { @State var ringSessionManager = RingSessionManager() var body: some View { List { Section("MY DEVICE", content: { if ringSessionManager.pickerDismissed, let currentRing = ringSessionManager.currentRing { makeRingView(ring: currentRing) } else { Button { ringSessionManager.presentPicker() } label: { Text("Add Ring") .frame(maxWidth: .infinity) .font(Font.headline.weight(.semibold)) } } }) }.listStyle(.insetGrouped) } @ViewBuilder private func makeRingView(ring: ASAccessory) -> some View { HStack { Image("colmi") .resizable() .aspectRatio(contentMode: .fit) .frame(height: 70) VStack(alignment: .leading) { Text(ring.displayName) .font(Font.headline.weight(.semibold)) } } }} #Preview { ContentView()}

如果一切配置正確,你現(xiàn)在可以構(gòu)建并運(yùn)行應(yīng)用。當(dāng)點(diǎn)擊“Add Ring”按鈕時(shí),將彈出一個(gè)界面,顯示附近的兼容設(shè)備(包括 COLMI R02 戒指)。選擇設(shè)備后,應(yīng)用即可完成連接。

連接演示

在后續(xù)的文章中,我們將進(jìn)一步探索如何與戒指交互,包括讀取電池電量、獲取傳感器數(shù)據(jù)(如 PPG 和加速度計(jì)),并基于這些數(shù)據(jù)開(kāi)發(fā)實(shí)時(shí)心率監(jiān)測(cè)、活動(dòng)追蹤及睡眠檢測(cè)功能。敬請(qǐng)期待!

英文原文: https://hf.co/blog/cyrilzakka/halo-introduction

原文作者: Cyril, ML Researcher, Health AI Lead @ Hugging Face

譯者: Lu Cheng, Hugging Face Fellow

相關(guān)知識(shí)

智能可穿戴設(shè)備正在開(kāi)啟健康管理新時(shí)代
穿戴式醫(yī)療設(shè)備.pptx
可穿戴設(shè)備對(duì)醫(yī)療健康行業(yè)的影響
“焦慮之源”,還是“激勵(lì)之泉”?聽(tīng)她解析可穿戴設(shè)備數(shù)據(jù)如何助力健康管理
醫(yī)療健康與可穿戴設(shè)備
可穿戴技術(shù)進(jìn)一步滿足健身與健康需求
健康可穿戴設(shè)備是否可靠看了就知道
可穿戴設(shè)備與健康醫(yī)療數(shù)據(jù)采集技術(shù)
可穿戴智能設(shè)備都有些什么?
醫(yī)療可穿戴設(shè)備,全球市場(chǎng)總體規(guī)模,前20大廠商排名及市場(chǎng)份額?醫(yī)療可穿戴設(shè)備產(chǎn)品介紹醫(yī)療可穿戴設(shè)備是一類(lèi)設(shè)計(jì)用于穿戴或附著在身體上的電子設(shè)備,其主要目的是監(jiān)測(cè)和管理個(gè)人健康和福祉的各...

網(wǎng)址: Halo 正式開(kāi)源: 使用可穿戴設(shè)備進(jìn)行開(kāi)源健康追蹤 http://www.u1s5d6.cn/newsview143598.html

推薦資訊