macOS × mbed OS でBLE通信する方法 

# 背景

以前に[mbed × BLE × iOSでとりあえず通信したい人のための記事](https://qiita.com/Kyomesuke3/items/a9bb6e0f53bcf4c1b327)を投稿した者です。やはりMacともBLE通信したいなぁと思って、そのまま移植したら全然うまく動かなくて大変だったので、備忘録として手法をまとめます。

## 環境

> PC: MacBook Pro, macOS High Sierra，<font color="red">セントラル</font>
> IDE: Xcode9.2, Swift4
> mbed: TY51822r3, mbed-os 5，<font color="red">ペリフェラル</fonr>

# 主な躓きどころ

## iOSでは出なかったエラー`XPC connection invalid`が出る

macOS(OSX)にはサンドボックスと呼ばれる仕組みがあり、iOSとは勝手が少々異なる。

1. target -> Capabilities -> App Sandbox -> Hardware -> Bluetoothにチェックをいれる
![sandbox.png](https://qiita-image-store.s3.amazonaws.com/0/67153/f36960f8-e397-950c-0046-06eb3b6c5d7a.png)

2. `info.plist`に`Privacy - Bluetooth Peripheral Usage Description`を追加して何かしらの文章を登録する
![info.png](https://qiita-image-store.s3.amazonaws.com/0/67153/44bc2411-8c06-94b2-5877-96d293ee536e.png)

## レイテンシー（待ち時間、反応速度）がとにかく遅い．凄まじい遅延を感じる

これはペリフェラル側の設定が甘いことによる問題だと思われる。
今回の場合、mbedをペリフェラルとしており、その`Gap::ConnectionParams_t`の持つパラメータ`.slaveLatency`を0にすることで遅延をなくすことができた。

macOS側をペリフェラルにする場合は，`CBPeripheralManager.setDesiredConnectionLatency(, for: )`を使うことでレイテンシーを変更できるらしい。

# ソースコード例

ペリフェラルであるmbedにつけたセンサの値をReadしてセントラルのMacで100Hzにて取得するサンプル

## macOS：セントラル側

```swift:BLEManager.swift
import Foundation
import CoreBluetooth

protocol BLEManagerDelegate {
	func updateData(data: NSData)
}

class BLEManager: NSObject, CBCentralManagerDelegate, CBPeripheralDelegate {

	private var centralManager: CBCentralManager!
	private var centralFlag: Bool = false
	private var peripheral: CBPeripheral! = nil
	private var characteristic: CBCharacteristic!
	private var timer: Timer?
	public var delegate: BLEManagerDelegate?

	override init() {
		super.init()
		centralManager = CBCentralManager(delegate: self, queue: nil)
	}

	func centralManagerDidUpdateState(_ central: CBCentralManager) {
		centralFlag = (central.state == CBManagerState.poweredOn)
	}

	func start() {
		if centralFlag {
			//Device Information 0x180A
			centralManager.scanForPeripherals(withServices: [CBUUID(string: "180A")], options: nil)
		}
	}

	func stop() {
		if peripheral != nil {
			timer?.invalidate()
			centralManager.cancelPeripheralConnection(peripheral)
		}
	}

	func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
		if peripheral.name == "DEVICE_NAME" {
			self.peripheral = peripheral
			self.centralManager.stopScan()
			self.centralManager.connect(self.peripheral, options: nil)
		}
	}

	func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
		peripheral.delegate = self
		peripheral.discoverServices([CBUUID(string: "180A")])
	}

	func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
		Swift.print("Connect Failed")
	}

	func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
		if let services = peripheral.services {
			for service in services {
				//UUIDは環境による
				peripheral.discoverCharacteristics([CBUUID(string: "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF")], for: service)
			}
		}
	}

	func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
		if let characteristics = service.characteristics {
			for characteristic in characteristics {
				if (UInt8(characteristic.properties.rawValue) & UInt8(CBCharacteristicProperties.read.rawValue)) != 0 {
					self.characteristic = characteristic
					timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true, block: { (t) in
						peripheral.readValue(for: self.characteristic)
					})
				}
			}
		}
	}

	func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
		if error != nil {
			Swift.print("No Data")
			return
		}
		delegate?.updateData(data: characteristic.value! as NSData)
	}
}
```

## mbed：ペリフェラル側

```cpp:main.cpp
#include "mbed.h"
#include "BLE.h"

#if 0
Serial pc(USBTX, USBRX);
#define DEBUG(...) { pc.printf(__VA_ARGS__); }
#else
#define DEBUG(...)
#endif

#define DEVICE_NAME "DEVICE_NAME"

DigitalIn dIn(p8);
DigitalOut led(LED4);
int btn = 0;
Ticker ticker;
void tickerCallback(void);

BLE ble;
// GattService::UUID_DEVICE_INFORMATION_SERVIC
uint16_t UUID_SERVICE[] = {GattService::UUID_DEVICE_INFORMATION_SERVICE};
uint8_t UUID_CHAR_DATA[] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
uint8_t data[1] = {0};
GattCharacteristic customCharastic(UUID_CHAR_DATA, data, sizeof(data), sizeof(data), GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_READ);
GattCharacteristic *customChars[] = {&customCharastic};
GattService customService(GattService::UUID_DEVICE_INFORMATION_SERVICE, customChars, sizeof(customChars) / sizeof(GattCharacteristic *));
static Gap::ConnectionParams_t fast;

void connectionCallback(const Gap::ConnectionCallbackParams_t *params) {
    DEBUG("Connected!\r\n");
    led = 1;
    ble.gap().stopAdvertising();
    ble.updateConnectionParams(params->handle, &fast);
    ticker.attach(&tickerCallback, 0.01);
}

void disconnectionCallback(const Gap::DisconnectionCallbackParams_t *params) {
    DEBUG("Disconnected!\r\n");
    led = 0;
    ticker.detach();
    ble.gap().startAdvertising();
}

void connectTimeoutCallback(Gap::TimeoutSource_t source) {
    DEBUG("Timeout");
}

void tickerCallback() {
    DEBUG("update\r\n");
    uint8_t btn_ = (uint8_t)((dIn.read()+1)%2);
    data[0] = (btn_ == 1 && btn == 0)?(uint8_t)(1):(uint8_t)(0);
    btn = btn_;
    ble.updateCharacteristicValue(customCharastic.getValueAttribute().getHandle(), (uint8_t *)&data, sizeof(data));
}

int main(void) {
    dIn.mode(PullUp);
    DEBUG("start\r\n");
    
    DEBUG("Initialise\r\n");
    ble.init();

    DEBUG("Setup the event handlers\r\n");
    ble.gap().onConnection(connectionCallback);
    ble.gap().onDisconnection(disconnectionCallback);
    ble.gap().onTimeout(connectTimeoutCallback);
    
    DEBUG("Setup latency\r\n"); //ここが最重要
    ble.getPreferredConnectionParams(&fast);
    fast.minConnectionInterval = 7.5;
    fast.maxConnectionInterval = 10;
    fast.slaveLatency = 0;
    ble.setPreferredConnectionParams(&fast);

    DEBUG("Advertising payload\r\n");
    ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::BREDR_NOT_SUPPORTED | GapAdvertisingData::LE_GENERAL_DISCOVERABLE);
    ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LIST_16BIT_SERVICE_IDS, (uint8_t *)UUID_SERVICE, sizeof(UUID_SERVICE));
    ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LOCAL_NAME, (const uint8_t *)DEVICE_NAME, strlen(DEVICE_NAME));
    ble.gap().setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);
    ble.gap().setAdvertisingInterval(160); //100ms; in multiples of 0.625ms.
    ble.gap().startAdvertising();

    DEBUG("Add service\r\n");
    ble.gattServer().addService(customService);

    while (true) {
        ble.waitForEvent();
    }
}
```

# 余談

macOS(OSX) でCoreBluetooth使ってる例とか参考記事が本当に少ない...BLE開発本当に難しいです。ちょっとでも心が折れそうな人の支えになれば嬉しいです。