macOS × Adafruit Feather 32u4でBLE GATTやる【決定版】

# 背景
Arduino互換機でBLE通信楽してやりたい！皆もそうだよね？
Arduino IDEで書ける互換機マイコン`Adafruit Feather 32u4 Bluefruit LE`をご存知でしょうか？　こいつはBLEモジュールが載ったワンボードマイコンです（他にもBLE NanoとかESP32とかあるよ）．
今回はMacとこのAdafruit Feather 32u4でGATTやる方法についての備忘録．
`Custom Service`を登録してRead専門の`Custom Characteristic`を追加する例を載せます．

## 環境
> PC: MacBook Pro, macOS Mojave，<font color="red">セントラル</font>
> IDE: Xcode10.1, Swift4

> マイコン: [Adafruit Feather 32u4 Bluefruit LE](https://www.switch-science.com/catalog/2723/)，<font color="red">ペリフェラル</font>
> IDE: Arduino IDE

<font color="red">現状，macOS Catalinaでは動かないらしいです！</font>

## 仮の設定
| Item           | UUID                                 |
|:---------------|:-------------------------------------|
| Service        | FF02AC5B-32A0-0CDD-DB39-5D3AB4336C6D |
| Characteristic | FF0234A7-32A0-0CDD-DB39-5D3AB4336C6D |

# Adafruit Feather 32u4サイドの実装
## Arduino IDEの設定
1. Arduino IDEの環境設定を開いて，`追加のボードマネージャのURL`に`https://adafruit.github.io/arduino-board-index/package_adafruit_index.json`を追加．

![ボードマネージャ.png](https://qiita-image-store.s3.amazonaws.com/0/67153/04bf1f76-146e-b260-3d7b-c39396a78181.png)

2.　[ツール]→[ボード]→[ボードマネージャ]で`Adafruit AVR Boards`をインストール
3.　[ツール]→[ボード]で`Adafruit Feather 32u4`を追加
4.　Macに`Adafruit Feather 32u4`を繋いでシリアルポートを選択
5.　[Adafruit_BluefruitLE_nRF51](https://github.com/adafruit/Adafruit_BluefruitLE_nRF51)から最新版のライブラリを丸ごとダウンロードしてきて，Macの`/Users/user_name/Documents/Arduino/libraries/`内に配置

## ソースコード例

定数宣言のヘッダファイル

```cpp:BlueFruitConfig.h
#define BUFSIZE                        128   // Size of the read buffer for incoming data
#define VERBOSE_MODE                   true  // If set to 'true' enables debug output

#define BLUEFRUIT_SWUART_RXD_PIN       9    // Required for software serial!
#define BLUEFRUIT_SWUART_TXD_PIN       10   // Required for software serial!
#define BLUEFRUIT_UART_CTS_PIN         11   // Required for software serial!
#define BLUEFRUIT_UART_RTS_PIN         8    // Optional, set to -1 if unused

#ifdef Serial1    // this makes it not complain on compilation if there's no Serial1
  #define BLUEFRUIT_HWSERIAL_NAME      Serial1
#endif

#define BLUEFRUIT_UART_MODE_PIN        12    // Set to -1 if unused

#define BLUEFRUIT_SPI_CS               8
#define BLUEFRUIT_SPI_IRQ              7
#define BLUEFRUIT_SPI_RST              4    // Optional but recommended, set to -1 if unused

#define BLUEFRUIT_SPI_SCK              13
#define BLUEFRUIT_SPI_MISO             12
#define BLUEFRUIT_SPI_MOSI             11
```

メインのソースコード

```cpp:custom_service.ino
#include <Arduino.h>
#include <SPI.h>
#include "Adafruit_BLE.h"
#include "Adafruit_BluefruitLE_SPI.h"
#include "Adafruit_BluefruitLE_UART.h"
#include "Adafruit_BLEGatt.h"

#include "BluefruitConfig.h"

#if SOFTWARE_SERIAL_AVAILABLE
  #include <SoftwareSerial.h>
#endif

Adafruit_BluefruitLE_SPI ble(BLUEFRUIT_SPI_CS, BLUEFRUIT_SPI_IRQ, BLUEFRUIT_SPI_RST);

Adafruit_BLEGatt gatt(ble);

void error(const __FlashStringHelper*err) {
  Serial.println(err);
  while (1);
}

int32_t serviceId;
int32_t characterId;

void setup(void) {
  randomSeed(micros());

  while (!Serial); // required for Flora & Micro
  delay(500);

  Serial.begin(115200);
  Serial.println(F("Adafruit Bluefruit Device Information Service (DIS) Example"));
  Serial.println(F("---------------------------------------------------"));

  Serial.print(F("Initialising the Bluefruit LE module: "));

  if (!ble.begin(VERBOSE_MODE)) {
    error(F("Couldn't find Bluefruit, make sure it's in Command mode & check wiring?"));
  }
  Serial.println(F("OK!"));

  Serial.println(F("Performing a factory reset: "));
  if (!ble.factoryReset()){
       error(F("Couldn't factory reset"));
  }

  ble.echo(false);

  Serial.println(F("Setting device name to 'Bluefruit': "));
  if (!ble.sendCommandCheckOK(F("AT+GAPDEVNAME=Bluefruit"))) {
    error(F("Could not set device name?"));
  }

  // Serial.println(F("Adding the Device Information Service definition (UUID = FF02AC5B-32A0-0CDD-DB39-5D3AB4336C6D): "));
  // uint8_t service_uuid128[] = {0xFF,0x02,0xAC,0x5B,0x32,0xA0,0x0C,0xDD,0xDB,0x39,0x5D,0x3A,0xB4,0x33,0x6C,0x6D};
  // serviceId = gatt.addService(service_uuid128);
  // if (serviceId == 0) {
  //   error(F("Could not add Device Information Service"));
  // }
  //
  // Serial.println(F("Adding the Custom Characteristic (UUID = 0x0002): "));
  // // uint8_t character_uuid128[] = {0xFF,0x02,0x34,0xA7,0x32,0xA0,0x0C,0xDD,0xDB,0x39,0x5D,0x3A,0xB4,0x33,0x6C,0x6D};
  // characterId = gatt.addCharacteristic(0x0002, GATT_CHARS_PROPERTIES_READ, 1, 20, BLE_DATATYPE_BYTEARRAY);
  // if (characterId == 0) {
  //   error(F("Could not add Custom Characteristic"));
  // }
  //
  //
  boolean success;
  Serial.println(F("Adding the Custom Service: "));
  success = ble.sendCommandWithIntReply(F("AT+GATTADDSERVICE=UUID128=FF-02-AC-5B-32-A0-0C-DD-DB-39-5D-3A-B4-33-6C-6D"), &serviceId);
  if (!success) {
    error(F("Could not add Custom Service"));
  }

  // 0x02 : Read
  // 0x04 : Write without a response
  // 0x08 : Write with a response
  // 0x10 : Notifications
  // 0x20 : ??
  Serial.println(F("Adding the Custom Characteristic: "));
  success = ble.sendCommandWithIntReply(F("AT+GATTADDCHAR=UUID128=FF-02-34-A7-32-A0-0C-DD-DB-39-5D-3A-B4-33-6C-6D,PROPERTIES=0x02,MIN_LEN=5, MAX_LEN=5, VALUE=00-00-00-00-00"), &characterId);
  if (!success) {
    error(F("Could not add Custom Characteristic"));
  }

  Serial.print(F("Adding Device Information Service UUID to the advertising payload: "));
  uint8_t advdata[] = {0x02, 0x01, 0x06, 0x05, 0x02, 0x09, 0x18, 0x0a, 0x18};
  ble.setAdvData(advdata, sizeof(advdata));
  ble.sendCommandCheckOK(F("AT+GAPSETADVDATA=02-01-06-05-02-0d-18-0a-18"));

  Serial.print(F("Performing a SW reset (service changes require a reset): "));
  ble.reset();

  Serial.println();
}

void loop(void) {
  uint8_t values[5] = {0,0,0,0,0};
  for (int j=0; j<5; j++)  {
    values[j] = (uint8_t)random(1, 255);
  }
  gatt.setChar(characterId, values, 5);
  delay(1000);
}
```

↑Serial通信周りはデバッグ時以外は必要ありません．

# Macサイドの実装
## プロジェクトの設定

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)

## ソースコード例

BLE周りの実装

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

let service_uuid: String = "FF02AC5B-32A0-0CDD-DB39-5D3AB4336C6D"
let character_uuid: String = "FF0234A7-32A0-0CDD-DB39-5D3AB4336C6D"

protocol BLEManagerDelegate {
    func updateData(_ values: [Int])
}

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)
    }
    
    //BLEのスキャン開始
    func start() {
        if centralFlag {
            Swift.print("start")
            centralManager.scanForPeripherals(withServices: nil, options: nil)
        }
    }
    //BLE通信終了
    func stop() {
        if peripheral != nil {
            Swift.print("stop")
            timer?.invalidate()
            centralManager.cancelPeripheralConnection(peripheral)
        }
    }
    
    func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        //ペリフェラルの名前でデバイスを特定
        if peripheral.name == "Bluefruit" {
            Swift.print("catch")
            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: service_uuid)])
    }
    
    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 {
                peripheral.discoverCharacteristics([CBUUID(string: character_uuid)], 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
                    //データのRead周期は1秒とした
                    timer = Timer.scheduledTimer(withTimeInterval: 1.0, 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
        }
        //データはIntの配列でゲット
        let data: NSData = characteristic.value! as NSData
        var buffer = [UInt8](repeating: 0, count: data.length)
        data.getBytes(&buffer, length: data.length)
        let values: [Int] = buffer.map { (value) -> Int in
            return Int(value)
        }
        delegate?.updateData(values)
    }
    
}
```

AppDelegateの実装

```swift:AppDelegate.swift
import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, BLEManagerDelegate {

    private let ble = BLEManager()
    private var mainVC: ViewController? = nil
    
    static var sharedInstance: AppDelegate {
        return NSApplication.shared.delegate as! AppDelegate
    }

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        ble.delegate = self
    }

    func applicationWillTerminate(_ aNotification: Notification) {
        ble.stop()
    }
    
    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
        return true
    }
    
    public func startBLE() {
        ble.start()
    }
    
    public func stopBLE() {
        ble.stop()
    }
    
    func updateData(_ values: [Int]) {
        Swift.print(values)
    }

}
```

UI周りの実装

```swift:ViewController.swift
import Cocoa

class ViewController: NSViewController {

    let appDelegate = AppDelegate.sharedInstance
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override var representedObject: Any? {
        didSet {
        }
    }

    //ボタンといい感じに紐付け
    @IBAction func push(_ sender: NSButton) {
        if sender.tag == 0 {
            appDelegate.startBLE()
        } else {
            appDelegate.stopBLE()
        }
    }
    
}
```

# 動作チェック
とりあえず有線でいいので繋いだ状態で電源供給をし，両方Runして接続してみました．


```:Xcodeのコンソール
2019-01-06 03:00:52.177745+0900 Adafruit_Feather_32u4[88802:12599203] [default] Unable to load Info.plist exceptions (eGPUOverrides)
start
catch
[131, 99, 224, 182, 43]
[61, 248, 213, 67, 73]
[221, 128, 12, 180, 139]
[133, 143, 6, 216, 93]
[114, 187, 216, 193, 33]
[8, 35, 231, 83, 7]
[8, 110, 243, 19, 58]
[121, 206, 83, 1, 60]
[74, 23, 28, 217, 46]
[199, 138, 15, 2, 130]
[49, 234, 160, 78, 116]
```

しっかり5つの乱数が1秒ごとに取得できてるっぽいです．