つい、カッとなって飛ばしてしまった。後悔はしていない。
apple watchの心拍数データとサイクルコンピュータ連携の現状
昔のサイクルコンピュータはant+のみに対応していたが、最近はbleにも対応しているため、apple watch(以下、watch)の心拍数データをサイクルコンピュータ(以下、サイコン)に連携できるのではないかと考えた。
しかし、watchのアプリを探してもサイコンに心拍数を送れるアプリはiphoneを経由して送るもののみ見つかり、watch単体で送れるアプリはない模様である。
調べて分かったこと
- ble機器はセントラル・ペリフェラルのどちらかの役割を担うこととなり、心拍計はペリフェラルの役割、サイコンはセントラルの役割を担っている。
- セントラルは複数のペリフェラルと同時に接続できるが、通常、ペリフェラルは一つのセントラルとのみ接続できる。
- iphoneは、セントラル・ペリフェラルのどちらかになることが出来るが、watchはセントラルのみにしかなることが出来ない。よって、watchはbleのペリフェラルとして心拍計代わりになることは出来ない。
- 出来ないのは、xcodeのフレームワークCoreBluetoothにおいて、watchがペリフェラル役になるためのクラス定義等がないためなので、c言語レベルから自作できちゃう人がいれば可能かも。
- 上記により、現状、iphone経由で心拍計をサイコンに送っているのは、下記経由であると想像できる。
- watch〜iphone間がbleでないと考えたのは、その場合、iphoneがペリフェラルとして2つの機器(watch、サイコン)に同時に接続することとなり、通常は不可能であるため。
サイコンとの連携方法の考察
- watchがペリフェラルになれない現状では、サイコンとの間にペリフェラル役を担う第3者が必要。
- iphoneにその役割をさせれば解決だけど、既に複数のアプリが存在するので、2番煎じをしても面白くない。
- よって、最近手を出したarduinoに第3者役をさせることとし、使い勝手がよいM5stickCPlus2(以下、M5stickCP2)を使うこととした。
具体案
- 当初案
- 当初案の落とし穴
- watchはsppプロファイルが使えない。
- arduinoではBluetoothシリアル(sppプロファイルを利用)での通信が簡単にできたので、bleの代わりにBTclassicの技術(spp)を使って繋ごうと思ったのに当てが外れた。
- なお、appleに申請して許可をもらえれば、sppを使った通信が可能らしいが、そこまでする気力はなかった。
- 代替案
- 代替案の説明
- Bluetoothシリアルが使えないため、watchからもbleで通信することとした。
- よって、watch(セントラル)からサイコン(セントラル)に心拍数を送るため、watchとサイコンを相手とするペリフェラル役がそれぞれ必要となった。
- 家にはM5stackの端末がいくつかあるが、最も小さいM5atomS3をwatch相手のペリフェラル役とした。
- M5atomS3とM5stickCP2は、有線(groveケーブル)でつなぎ通信すると共に、バッテリーを持たないM5atomS3をM5stickCP2からの電源供給で動かすこととした。
作ったコード(xcode編)
- watch (apple watch series9 45mm)
//メイン
import SwiftUI
@main
struct MyHR_Watch_AppApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
//ここまでメイン
//コンテントビュー
import SwiftUI
import HealthKit
struct ContentView: View {
private var healthStore = HKHealthStore()
let heartRateQuantity = HKUnit(from: "count/min")
@State private var value = 0
@ObservedObject var central = MyCentral()
@StateObject var extendedRuntimeSession = ExtendedRuntimeSession()
var body: some View {
HStack{
Text("\(value)")
.fontWeight(.regular)
.font(.system(size: 50))
Text("BPM")
.font(.headline)
.fontWeight(.bold)
.foregroundColor(Color.red)
.padding(.bottom, 28.0)
Spacer()
}
.onAppear(perform: start)
ScrollView {
VStack {
HStack {
Button("scan開始"){ self.central.startScan()
extendedRuntimeSession.sessionEndCompletion = end
extendedRuntimeSession.startSession()
}
Button("停止"){
self.central.stopScan()
self.end()
}
}
ForEach(0..<self.central.cddtNms.count, id: \.self) {
i in
Button(self.central.cddtNms[i]){
self.central.connect(index: i)
}
}
Text("状態:" + (self.central.cnct ? "接続" : "未接続"))
}
}
}
func start() {
autorizeHealthKit()
startHeartRateQuery(quantityTypeIdentifier: .heartRate)
}
private func end() {
extendedRuntimeSession.endSession()
WKInterfaceDevice.current().play(.stop)
}
func autorizeHealthKit() {
//取り扱いたいHRの型を取得
let healthKitTypes: Set = [
HKObjectType.quantityType(forIdentifier: HKQuantityTypeIdentifier.heartRate)!]
//HR型に対する、書込み・読取りする権限を得る
healthStore.requestAuthorization(toShare: healthKitTypes, read: healthKitTypes) { _, _ in }
}
private func startHeartRateQuery(quantityTypeIdentifier: HKQuantityTypeIdentifier) {
// 1 クエリ条件のインスタンス取得
let devicePredicate = HKQuery.predicateForObjects(from: [HKDevice.local()])
// 2 数値アップデート時のコールバック関数
let updateHandler: (HKAnchoredObjectQuery, [HKSample]?, [HKDeletedObject]?, HKQueryAnchor?, Error?) -> Void = {
query, samples, deletedObjects, queryAnchor, error in
// 3
guard let samples = samples as? [HKQuantitySample] else {
return
}
self.process(samples, type: quantityTypeIdentifier)
}
// 4 データベースに値が追加されるたびに実行されるクエリーを生成
let query = HKAnchoredObjectQuery(type: HKObjectType.quantityType(forIdentifier: quantityTypeIdentifier)!, predicate: devicePredicate, anchor: nil, limit: HKObjectQueryNoLimit, resultsHandler: updateHandler)
//
query.updateHandler = updateHandler //4と重複する?と思ったが、ないと心拍数が更新されなかった
// 5 クエリーを実行(1回だけでなく、更新のたびにupdateHandlerが呼ばれる)
healthStore.execute(query)
}
//updateHandlerに呼ばれる関数
private func process(_ samples: [HKQuantitySample], type: HKQuantityTypeIdentifier) {
var lastHeartRate = 0.0
for sample in samples {
if type == .heartRate {
lastHeartRate = sample.quantity.doubleValue(for: heartRateQuantity)
}
//心拍数表示に使うState変数を更新する
self.value = Int(lastHeartRate)
if(self.central.cnct){
self.central.write(hr:value)
}
}
}
}
//ここまでコンテントビュー
//BLE周り
import Foundation
import CoreBluetooth
class MyCentral: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeripheralDelegate {
//セントラルのおまじない
let cMngr = CBCentralManager()
var cddts: [CBPeripheral] = []
var pprl: CBPeripheral?
var serv: CBService?
var cha: CBCharacteristic?
var hr = 0
@Published var cddtNms: [String] = []
@Published var cnct = false
override init() {
super.init()
cMngr.delegate = self
}
func startScan() {
print("start scan")
cddtNms.removeAll()
cddts.removeAll()
cnct = false
if !cMngr.isScanning {
cMngr.scanForPeripherals(withServices: [Const.SERV_ID], options: nil)
}
}
func stopScan() {
print("stop scan")
if cMngr.isScanning {
cMngr.stopScan()
guard let pprl else {return}
cMngr.cancelPeripheralConnection(pprl) //disconnect大事!
cddtNms.removeAll()
cddts.removeAll()
cnct = false
}
}
// Centralの状態が変化したとき
func centralManagerDidUpdateState(_ ctrl: CBCentralManager) { //デリゲートのメソッド実装
switch ctrl.state {
case .poweredOff:
print("BLE PoweredOff")
break
case .poweredOn:
print("BLE poweredOn")
break
case .resetting:
print("BLE resetting")
break
case .unauthorized:
print("BLE unauthorized")
break
case .unknown:
print("BLE unknown")
break
case .unsupported:
print("BLE unsupported")
break
@unknown default:
print("BLE illegalstate \(ctrl.state)")
}
}
func connect(index: Int) {
print("BLE start connect")
self.pprl = cddts[index]
cMngr.connect(self.pprl!, options: nil)
}
func write(hr : Int) {
guard let pprl else {
print("peripheral is nil")
return
}
guard let cha else {
print("characteristic is nil")
return
}
print("書込み:\(hr)" )
let value = "hr_" + String(format: "%03d", hr)
pprl.writeValue(value.data(using: .utf8)!, for: cha, type: .withResponse)
}
// Peripheralを見つけたとき
func centralManager(_ ctrl: CBCentralManager, didDiscover pprl: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) { //デリゲートのメソッド実装
let name = pprl.name
let localName = advertisementData[CBAdvertisementDataLocalNameKey] as? String
if localName != nil && localName == "M5AtomS3_BLE" {
//初回のみ追加
if !cddtNms.contains("M5AtomS3_BLE"){
print("発見したペリフェラル名: \(String(describing: name)) アドバタイズ情報におけるlocalName:\(String(describing: localName))")
cddtNms.append(localName!)
cddts.append(pprl)
}
}
}
// ①セントラルがペリフェラルに接続したとき
func centralManager(_ ctrl: CBCentralManager, didConnect pprl: CBPeripheral) { //デリゲートのメソッド実装
print("①ペリフェラルに接続⇒サービスを検索")
pprl.delegate = self
//次はサービスを探す(みつかったら、②に進む)
pprl.discoverServices([Const.SERV_ID])
}
// セントラルがペリフェラルへの接続に失敗したとき
func centralManager(_ ctrl: CBCentralManager, didFailToConnect pprl: CBPeripheral, error: Error?) { //デリゲートのメソッド実装
print("BLE connect failed")
}
// セントラルとペリフェラルの接続が切れたとき
public func centralManager(_ ctrl: CBCentralManager, didDisconnectPeripheral pprl: CBPeripheral, error: Error?) {
print("centralManager:didDisconnectPeripheral: \(pprl)")
cddtNms.removeAll()
cddts.removeAll()
cnct = false
}
// ②接続したペリフェラルにServiceが見つかったとき
func peripheral(_ pprl: CBPeripheral, didDiscoverServices error: Error?) {
print("②サービスを発見⇒キャラクタリスティックを検索")
serv = pprl.services?.first
//次はキャラクタリスティックを探す(見つかったら、③に進む)
pprl.discoverCharacteristics([Const.CHA_ID], for: serv!)
}
// ③接続したペリフェラルのサービスにCharacteristicが見つかったとき
func peripheral(_ pprl: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
print("③キャラクタリスティックを発見")
for cha in service.characteristics! {
if cha.properties.contains(.read){
pprl.readValue(for: cha)
print("read:")
print(cha)
}
if cha.uuid == Const.CHA_ID {
self.cha = cha //書込のキャラクタリスティック
}
}
self.pprl = pprl //再度紐付ける
self.serv = service //再度紐付ける
cnct = true
}
// ④readしたとき、通知を受け取ったとき
func peripheral(_ pprl: CBPeripheral, didUpdateValueFor cha: CBCharacteristic, error: Error?) {
print("④read")
print("valueを含むキャラクタリスティック: \(cha)")
if let data = cha.value {
print("data: \(data)")
print("string: \(String(data: data, encoding: .utf8) ?? "")")
}
}
// ⑤writeValue を .withResponse で実行した場合に呼ばれる
public func peripheral(_ pprl: CBPeripheral, didWriteValueFor cha: CBCharacteristic, error: Error?) {
print("⑤書き込んだキャラクタリスティック: \(cha)")//writeしたvalueは反映しない。最初に読み込んだときのまま
if let error = error {
print("error: \(error)")
return
}
}
}
struct Const {
static let SERV_ID = CBUUID(string: "6773857d-bf2e-4747-82df-d6ff90f9f8f8") //サービス
static let CHA_ID = CBUUID(string: "d6c2fcf1-e6e6-4a68-9216-3e3edda0f638") //キャラクタリスティック
}
//ここまでBLE周り
//バックグラウンド動作用
import Foundation
import WatchKit
final class ExtendedRuntimeSession: NSObject, ObservableObject {
private var session: WKExtendedRuntimeSession!
var sessionEndCompletion: (() -> Void)?
func startSession() {
session = WKExtendedRuntimeSession()
session.start()
}
func endSession() {
session.invalidate()
}
}
extension ExtendedRuntimeSession: WKExtendedRuntimeSessionDelegate {
func extendedRuntimeSessionDidStart(_ extendedRuntimeSession: WKExtendedRuntimeSession) {}
func extendedRuntimeSessionWillExpire(_ extendedRuntimeSession: WKExtendedRuntimeSession) {
sessionEndCompletion?()
}
func extendedRuntimeSession(_ extendedRuntimeSession: WKExtendedRuntimeSession, didInvalidateWith reason: WKExtendedRuntimeSessionInvalidationReason, error: Error?) {
sessionEndCompletion?()
}
}
//ここまでバックグラウンド動作用
作ったコード(arduinoIDE編)
- M5atomS3
#include <M5Unified.h>
#include <BLEDevice.h>
#include <BLE2902.h> //CCCD(Client Characteristic Configuration Descriptor)のuuid:0x2902より
#define SERVICE_UUID "6773857d-bf2e-4747-82df-d6ff90f9f8f8" //適当に生成したUUID
#define CHARACTERISTIC_UUID "d6c2fcf1-e6e6-4a68-9216-3e3edda0f638" //適当に生成したUUID
//キャラクタリスティックのコールバック関数
class MyCharacteristicCallbacks : public BLECharacteristicCallbacks {
//セントラルから書込みされたとき(onWrite)の処理
void onWrite(BLECharacteristic *pCharacteristic) {
std::string value = pCharacteristic->getValue(); //書き込まれた文字列を取得
String text = value.c_str(); //String型に変換
if (text.length() > 0) { //文字列の長さがゼロより大きいときに文字列を表示
//watchから受け取った文字列をM5stickCP2(以下、CP2)に送る
Serial.println("from_watch: " + text); //シリアルモニタ表示
Serial1.println(text); //CP2へ送信
}
}
};
void startService(BLEServer *pServer) //setupの④で呼び出す関数
{
BLEService *pService = pServer->createService(SERVICE_UUID); //④−1 サービスの初期設定
BLECharacteristic *pCharacteristic = pService->createCharacteristic( //④−2 キャラクタリスティックの新設
CHARACTERISTIC_UUID, // キャラクタリスティックのUUID設定
BLECharacteristic::PROPERTY_WRITE); // writeを許可
pCharacteristic->addDescriptor(new BLE2902()); //④-3 キャラクタリスティックのCCCDを設定(省略してもエラーにならなかった)
pCharacteristic->setCallbacks(new MyCharacteristicCallbacks()); //④−4 キャラクタリスティックにコールバック関数を設定
pService->start(); //④-5 サービス開始
}
void startAdvertising() //setupの⑤で呼び出す関数
{
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising(); //⑤-1 アドバタイズのインスタンス入手
pAdvertising->addServiceUUID(SERVICE_UUID); //⑤-2 アドバタイズにサービスIDを埋め込み
pAdvertising->setScanResponse(true); //⑤-3 セントラルにスキャンされたときに反応するかどうかを設定
//trueにしないと、Advertising DataにService UUIDが含まれないらしい。
BLEDevice::startAdvertising(); //⑤-4 アドバタイズ実行
}
// Serverのコールバック関数で接続時、切断時の処理を設定する
class ServerCallbacks : public BLEServerCallbacks {
// 接続時に呼び出される
void onConnect(BLEServer *pServer) {
Serial.println("connected!"); //シリアルモニタ表示
Serial1.println("cnt000"); //CP2に接続を通知
// 接続されたらAdvertisingを停止する
BLEDevice::stopAdvertising();
}
// 切断時に呼び出される
void onDisconnect(BLEServer *pServer) {
Serial.println("disconnected!"); //シリアルモニタ表示
Serial1.println("dcn000"); //CP2に接続切断を通知
delay(1000);
// アドバタイズを再開する
BLEDevice::startAdvertising();
Serial.println("Now Advertising!"); //シリアルモニタ表示
Serial1.println("adv000"); //CP2にアドバタイズを通知
}
};
void setup() {
M5.begin();
Serial1.begin(9600, SERIAL_8N1, 1, 2); // Groveケーブルへ
BLEDevice::init("M5AtomS3_BLE"); //①BLEモジュールの初期化
BLEServer *pServer = BLEDevice::createServer(); //②BLEサーバーの設定
pServer->setCallbacks(new ServerCallbacks()); //③BLEサーバー用コールバック関数の追加
startService(pServer); //④サービス設定
startAdvertising(); //⑤アドバタイズ実行
delay(2000); //CP2のシリアル通信受け入れまで待機
Serial.println("Now Advertising!"); //シリアルモニタ表示
Serial1.println("adv000"); //アドバタイジング
}
void loop() {
//アドバタイズ再開やCP2への通知等は全てコールバック関数内で行うため、loop内の処理は不要
}
- M5stickCP2
#include <M5Unified.h>
#include <BLEDevice.h>
#include <BLE2902.h> //CCCD(Client Characteristic Configuration Descriptor)のuuid:0x2902より
#define SERVICE_UUID "180d"
//notify用のキャラクタリスティック
#define NOTIFY_CHARACTERISTIC_UUID "2a37"
BLECharacteristic *pNotifyCharacteristic;
//コネクト状態のフラグ追加
static bool connected = false;
static M5Canvas cv1_0(&M5.Lcd); // スプライト1_(メモリ描画領域)をcv1_0として準備
static M5Canvas cv1_1(&M5.Lcd); // スプライト1_1(メモリ描画領域)をcv1_1として準備
static M5Canvas cv1_2(&M5.Lcd); // スプライト1_2(メモリ描画領域)をcv1_2として準備
static M5Canvas cv1_3(&M5.Lcd); // スプライト1_3(メモリ描画領域)をcv1_3として準備
static M5Canvas cv1_4(&M5.Lcd); // スプライト1_4(メモリ描画領域)をcv1_4として準備
static M5Canvas cv2(&M5.Lcd); // スプライト2(メモリ描画領域)をcv2として準備
static M5Canvas cv3(&M5.Lcd); // スプライト3(メモリ描画領域)をcv3として準備
void startService(BLEServer *pServer) //setup④で呼び出す関数
{
BLEService *pService = pServer->createService(SERVICE_UUID); //④−1 サービスの初期設定
// 権限を最小にするためにNotify用のCharacteristicはReadWrite用とは別に定義
pNotifyCharacteristic = pService->createCharacteristic( //④-2 notifyキャラクタリスティックの新設
NOTIFY_CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_NOTIFY);
pNotifyCharacteristic->addDescriptor(new BLE2902()); //④-3 キャラクタリスティックのCCCDを設定
pService->start(); //④-4 サービス開始
}
void startAdvertising() //setup⑤で呼び出す関数
{
BLEAdvertising *pAdvertising = BLEDevice::getAdvertising(); //⑤-1 アドバタイズのインスタンスを入手
pAdvertising->addServiceUUID(SERVICE_UUID); //⑤-2 アドバタイズにサービスIDを埋め込み
pAdvertising->setScanResponse(true); //⑤-3 セントラルにスキャンされたときに反応するかどうかを設定
//trueにしないと、Advertising DataにService UUIDが含まれない。
BLEDevice::startAdvertising(); //⑤-4 アドバタイズ実行
}
// Serverのコールバックで接続に対する処理を行う
class ServerCallbacks : public BLEServerCallbacks {
// 接続時に呼び出される
void onConnect(BLEServer *pServer) {
cv1_1.fillScreen(BLACK);
cv1_1.setCursor(0, 0);
cv1_1.println("接続済み");
cv1_1.pushSprite(0, 20);
connected = true; //追加
// 接続されたらAdvertisingを停止する
BLEDevice::stopAdvertising();
}
// 切断された時に呼び出される
void onDisconnect(BLEServer *pServer) {
connected = false;
cv1_1.fillScreen(BLACK);
cv1_1.setCursor(0, 0);
cv1_1.println("接続切れ");
cv1_1.pushSprite(0, 20);
delay(500);
// 再接続のためにもう一度Advertisingする
BLEDevice::startAdvertising();
cv1_1.println("アドバタイズ");
cv1_1.pushSprite(0, 20);
}
};
void setup() {
auto cfg = M5.config();
M5.begin(cfg);
cv1_0.createSprite(135, 20); //M5stickCP2(以下、CP2)のタイトルを表示
cv1_1.createSprite(135, 40); //CP2の接続状態を表示
cv1_2.createSprite(135, 20); //CP2の画面輝度を表示
cv1_3.createSprite(135, 20); //CP2のバッテリーレベルを表示
cv1_4.createSprite(135, 20); //M5atomS3lite(以下、atom)のタイトルを表示
cv2.createSprite(135, 60); //atomの接続状態を表示
cv3.createSprite(135, 60); //サイコンへ発信する心拍数
cv1_0.setTextColor(ORANGE);
cv1_1.setTextColor(ORANGE);
cv1_2.setTextColor(ORANGE);
cv1_3.setTextColor(ORANGE);
cv1_4.setTextColor(CYAN);
cv2.setTextColor(CYAN);
cv1_0.setFont(&Font2); //16pt
cv1_1.setFont(&lgfxJapanMincho_20);
cv1_2.setFont(&lgfxJapanMincho_20);
cv1_3.setFont(&lgfxJapanMincho_20);
cv1_4.setFont(&Font2);
cv2.setFont(&lgfxJapanMincho_20);
BLEDevice::init("M5stickCP2_BLE"); //①BLEモジュールの初期化
BLEServer *pServer = BLEDevice::createServer(); //②BLEサーバーの設定
pServer->setCallbacks(new ServerCallbacks()); //③BLEサーバーのコールバック関数の埋め込み
startService(pServer); //④サービス設定
startAdvertising(); //⑤アドバタイズ実行
cv1_0.print(" <M5stickCP2>");
cv1_0.pushSprite(0, 0);
cv1_4.setCursor(0, 4);
cv1_4.print(" <M5atomS3>");
cv1_4.pushSprite(0, 100);
cv1_1.println("アドバタイズ");
cv1_1.pushSprite(0, 20);
Serial1.begin(9600, SERIAL_8N1, 32, 33); // Groveへ
}
int brt = -1; //輝度(200,100,0の3段階をループする。初期設定=-1により、最初のループで200に切り替わる)
float blv = 0; //バッテリーレベル
bool hb = true; //心拍表示の点滅用
void loop() {
M5.update();
//ボタンAを押して輝度を切り替える(200⇒100⇒0⇒200⇒...)
if (M5.BtnA.wasPressed() || brt < 0) {
brt += -100;
if (brt < 0) {
brt = 200;
}
M5.Lcd.setBrightness(brt);
cv1_2.fillScreen(BLACK);
cv1_2.setCursor(0, 0);
cv1_2.print("輝度:");
cv1_2.print(brt);
cv1_2.pushSprite(0, 60);
}
//CP2のバッテリー残量を表示する
float newblv = (float)(M5.Power.getBatteryLevel());
if (abs(newblv - blv) > 4) {
blv = newblv;
cv1_3.fillScreen(BLACK);
cv1_3.setCursor(0, 0);
cv1_3.printf("バッテリ:%3d%%", (int)(round(blv / 5) * 5));
cv1_3.pushSprite(0, 80);
}
// atomからの受信データ処理a
if (Serial1.available()) { // 受信があれば
if (int(Serial1.available()) > 0) { // 受信データがあれば
std::string str = Serial1.readStringUntil('\n').c_str();
//受信データを前3文字、後ろ3文字で切り分ける
std::string numstr = str.substr(3); //心拍数部分(後ろ3文字)
std::string cndstr = str.erase(3); //接続状況部分(前3文字)
int val = std::stoi(numstr);
//心拍データでない(後ろ3文字が000)ときの処理
if (val == 0) {
String mes = "";
if (cndstr == "adv") { //アドバタイズ中
mes += "アドバタイズ";
} else if (cndstr == "cnt") { //接続済み
cv2.fillScreen(BLACK);
cv2.setCursor(0, 0);
mes += "接続済み";
} else if (cndstr == "dcn") { //接続切れ
mes += "接続切れ";
}
cv2.println(mes);
cv2.pushSprite(0, 120);
return; //心拍数データじゃないので、以降の心拍数処理は省く
}
//以降は心拍数データのときの処理
cv3.fillScreen(BLACK);
//コネクト中のみnotify処理
if (connected) {
//notify処理
uint8_t value = (uint8_t)val;
char data[] = { 0, value };
std::string myStringForUnit16((char *)&data, 2);
pNotifyCharacteristic->setValue(myStringForUnit16);
pNotifyCharacteristic->notify();
//notify中表示(心拍数の左横で●を点滅させて、サイコンと接続中であることを示す)
String heart = "●";
if (hb) {
cv3.setTextColor(RED); // 文字色
} else {
cv3.setTextColor(WHITE); // 文字色
heart = "○";
}
hb = !hb;
cv3.drawCentreString(heart, 10, 0, &lgfxJapanMincho_20);
}
//サイコンと接続中かどうかを問わず、転送された心拍数を表示する。
cv3.setTextColor(GREEN); // 文字色
char hrstr[4];
sprintf(hrstr, "%03d", val);
cv3.drawCentreString((String)hrstr, 67, 0, &Font7);
cv3.pushSprite(0, 180);
}
}
}
コードの解説
- watch
- 最新のxcode15.3で作成した。
- CoreBluetoothとHealthKitをimportして利用するので、あまり悩まずに作れる。
- HealthKitはimport文だけでなく、フレームワークの追加も必要であることに注意。
- privacy設定でbluetoothとHealthの許可設定が必要。
- バックグラウンドで動作するように、バックグラウンドモードのSession Typeは「Physical therapy」(バックグランド最長1時間)を選んだ。
- 画面は下図のとおり、心拍数表示と、ble接続用のボタンが2つ。
- scan開始ボタンを押すと、アドバタイズ中のペリフェラルをscanし、名前が「M5AtomS3_BLE」のペリフェラルを発見すると、「状態:未接続」の上にボタンが追加される(ボタン名はペリフェラル名。つまり「M5AtomS3_BLE」)
- 出現したボタンを押すと、「状態:未接続」が「状態:接続」に変化する。
-
M5atom3(実際はM5atomS3lite[液晶画面なし]を使用)
- ペリフェラルとして、まずはアドバタイズし、watchから接続されたら、アドバタイズを停止して、watchからの書込みを受け付ける状態に推移する。
- watchから書込みを受けると、コールバック関数が呼び出され、M5stickCP2に対し、シリアル通信を行う。このときの通信する文字列は"hr_***"(***は心拍数)
- シリアル通信は、心拍数以外にも、通信状況に応じて、次の三つをおくる。
- アドバタイズ開始"adv000"、ble接続"cnt000"、ble切断"dcn000"
-
M5stickCP2
- 画面(横135,縦240)を下記領域に分割して利用
- 上から順に...
- cv1_0.createSprite(135, 20); //M5stickCP2(以下、CP2)のタイトルを表示
- cv1_1.createSprite(135, 40); //CP2の接続状態を表示
- cv1_2.createSprite(135, 20); //CP2の画面輝度を表示
- cv1_3.createSprite(135, 20); //CP2のバッテリーレベルを表示
- cv1_4.createSprite(135, 20); //M5atomS3lite(以下、atom)のタイトルを表示
- cv2.createSprite(135, 60); //atomの接続状態を表示
- cv3.createSprite(135, 60); //サイコンへ発信する心拍数
- なお、サイコンと未接続状態でも、watchから心拍数を受信していれば、下段に表示される。
- さらに、サイコンと接続状態になれば、心拍数の左側で●が点滅して、接続状態にあることを示す。
- 接続状態については、CP2、atomとも、「アドバタイズ」「接続済み」「接続切れ」をループする形となる。
- 画面輝度は、0〜255だが、ボタンAを押すごとに200⇒100⇒0をループする形とした。
実際の動作
なぜか、M5stickCP2の表示の方が、watchより早い気がする。
最後に
作ってはみたものの、実際のM5stickCP2は画面を消した状態で1時間ちょっとしかバッテリーが持たなかったし、watchのバックグラウンドも1時間までしか継続しない。なにより、appleの有料デベロッパー契約をしていないため、watchのアプリは1週間しか持たない(再インストールは可能)。
よって、あまり実用的ではないし、M5stickCP2とM5atomS3を持ち運ぶくらいなら、腕に巻くタイプの心拍計でもつけた方が確実。
でも、ちゃんと動作したのを見れただけでも良し!
おかげで、bleに詳しくなったしね。