bleno側の解説とブラッシュアップ: 1Keyboardみたいなアプリの続き 1

オリィ研究所（ http://orylab.com/ ） の 椎葉です。
前回 ( http://qiita.com/YoshifumiShiiba/items/4c32dbbd8f8bb38be6ad ) の続きで、より実用的にブラッシュアップする上でのbleno部分の改造と解説をやってみます。

# blenoとはなんぞや

前回も軽くは触れましたが、**bleno**( https://github.com/sandeepmistry/bleno ) はMac、Win、LinuxのマルチプラットフォームでBluetoothペリフェラル（操作される側）の実装が行えるNode.jsのライブラリです。

同作者による**nobel** ( https://github.com/sandeepmistry/noble ) と言うライブラリもあり、こちらはBLE Central(操作する側)の実装を行うライブラリです。ちなみに、今回のプロジェクトはこっちのほうがよかったんじゃないか説が浮上しております。
![introduction-to-bluetooth-low-energy-12-638.jpg](https://qiita-image-store.s3.amazonaws.com/0/9162/76d959a1-66a8-b0f0-8ee2-c7b127db74d1.jpeg)
*iOSはペリフェラルとしてもセントラルとして振る舞える。Macをペリフェラルにするよりは、iOSを探す側のセントラルである方が自然だし実装もクールに済んだはず。*

実行環境がとてもシビアなので、エンジニア以外にはアプリケーションとしてプロダクションにはしづらいです。うちの会社もそうですが、デバイスとしてOSや環境ごと提供するような領域のプロジェクトではうまく活用できると思います。

# 1 Keyboardもどきの現状の課題
![IMG_0024.JPG](https://qiita-image-store.s3.amazonaws.com/0/9162/23621b7d-ad2b-955c-9fd4-33d5bf4452c4.jpeg)

前回の記事でMacのコマンドラインからBLE経由でiOSに入力を行うことのできるSwift製のCustom Keyboardのプロトタイプを作りましたが、幾つかの課題を放置したままになっていました。

1. **バックスペースを入力することができない**
2. **改行を入力することができない**
3. **できたらカーソル移動も実装したい**

今回は解説を交えながらこれらの課題を解決したいと思います。

# 準備する

すでに済んでいる人はスキップして問題ありません。

前回 (http://qiita.com/YoshifumiShiiba/items/4c32dbbd8f8bb38be6ad) の記事を参考にNodejs側のプロジェクトを準備します。
また、package.json, index.js, lib/bleno.coffeeを同様に前回のコードを参考に設置します。

![Screen Shot 2017-01-27 at 4.23.12 PM.png](https://qiita-image-store.s3.amazonaws.com/0/9162/24ac69f9-7c64-b6aa-a8eb-e067e852f77f.png)

# コマンドラインでバックスペースやアローキーの入力を受ける

前回はNodejs標準のreadLineモジュールを使って、こんなコードでprocess.stdinからテキスト入力を拾っていました。

```coffeescript:bleno.coffee

  # コマンドライン入力を受け付けて送信するメソッド
  # blenoからコールバック関数を引数として受け取る
  startInput: (valueCallback)->
    @readLine = readline.createInterface
      input: process.stdin
      output: process.stdout

    # 入力受ける
    @readLine.question ">", (answer)=>
      if answer isnt ""
        data = Buffer.from answer, 'utf8'
        # 送信！
        valueCallback data
      @readLine.close()

      # stopフラグが立っていたら再帰せずに終了
      if @stop then return
      @startInput valueCallback
```

## keypressモジュール

このアプローチでテキストを取得している限り、どう頑張っても修飾キーの入力をとることができないので、keypress(https://www.npmjs.com/package/keypress) モジュールを使って、キーを押されたイベントを取得します。

```
# プロジェクトルートで
npm install keypress --save
```

上記のコマンドでインストールし、下記の実装でkeypressを初期化します。

```coffeescript:bleno
  # keypress初期化
  initKeyPress: ()->
    keypress(process.stdin)
    process.stdin.on 'keypress', (ch, key)=>
      # どんなオブジェクトがくるか確認したい時
      # console.log('got "keypress"', ch)
      # console.log('got "keypress"', key)

      # Ctrl+Cで終了(書いておかないと困る)
      if key and key.ctrl and key.name is 'c'
        console.log 'pause!'
        @quitByInput()
        return

      # STR作成メソッドコール
      str = @makeStrByKey ch, key
      # 送信メソッドコール
      if str then @send str

    # process.stdin起動
    process.stdin.setRawMode true
    process.stdin.resume()
```

stdinをkeypressに食わせることでstdinにイベントを追加するスタイルのようです。その後stdin.setRawMode(true)とresume()を叩いて生のキーボードイベントを取得します。

上記コードの補助メソッド群が下記です。結局コールバック関数は引き回さずプロパティにしました。

```coffeescript:bleno.coffee
  # chとkeyに応じて返すSTRを決める
  makeStrByKey: (ch, key)->
    str = ""
    if key and key.name is 'backspace'
      # backspaceやrightみたいなメタ文字は加工して投げる。
      str = "{{{backspace}}}"
    else if key and key.name is "return"
      str = "\n"
    else if key and key.name is "right"
      str = "{{{right}}}"
    else if key and key.name is "left"
      str = "{{{left}}}"
    else if ch
      str = ch
    console.log str
    return str

  # 文字をiOSに送信する
  send: (str)->
    if not @updateCallback then return
    data = Buffer.from str, 'utf8'
    # 送信！
    @updateCallback data

  # 正常な終了
  quitByInput: ()->
    process.stdin.setRawMode false
    process.stdin.pause()
    process.exit(1)

```

これでほぼほぼキー入力は正常に取得できそうです。

# bleno

ここでbleno側の実装について解説していきたいと思います。
下記が、Blenoクラスのコンストラクタです。

```coffeescript:bleno.coffee
bleno = null
class Bleno

  constructor: ()->
    try
      # blenoはrequireが例外を吐く
      bleno = require 'bleno'
    catch error
      # blenoが例外を吐いたら終了
      bleno = null
      console.log "This mac cannot use me, good bye."
      process.exit(0)
      return
    # 初期化メソッドコール
    @init()

```

コメントの通りなのですが、Blenoは現状プロセスにrequireした段階で環境が要求を満たしていない場合などに例外を吐いてしまいます。回避するためにrequireをtryで囲み、成功した場合にのみそれをつかって処理を続行するように実装します。

下記がデバイス名とサービスの定義部分です。

```coffeescript:bleno.coffee
  init: ()->
    # デバイス名定義
    name = 'BlenoKeyboard'
    serviceUuids = [ '1192d150-e46d-11e6-bf01-fe55135034f3' ]

    # Serviceの定義
    primaryService = new bleno.PrimaryService
      uuid: serviceUuids[0]
      characteristics: [

        # Characteristicの定義
        new bleno.Characteristic
          uuid: '1192d57e-e46d-11e6-bf01-fe55135034f3'
          properties: [
            'notify'
          ]

          # Notifyに登録された時
          onSubscribe: (maxValueSize, updateValueCallback)=>
            console.log 'start input'
            @updateCallback = updateValueCallback

          # Notifyが解除された時
          onUnsubscribe: ()=>
            console.log 'stop input'
            @updateCallback = null
      ]
```

ここの実装内容がセントラル側の実装にも大きく関わります、というかこれを叩きます。
前回はすぐに動かしたくて16bitのUUIDで定義しましたが、今回は真面目にジェネレータ（https://www.uuidgenerator.net ）などを使ってオリジナルのUUIDを作成します。

### Characteristic
このCharacteristicがBLEのサービスを実行する本体になります。
今回はペリフェラル側のデータをペリフェラル側からのイベントに応じてセントラルが受け取るパターンなのでNofityを使用しています。
他にも、'read', 'write', 'indicate'などのタイプを利用できます。オフィシャルのReadme（ https://github.com/sandeepmistry/bleno#characteristic )。

# bleno.coffee
というわけで、下記が出来上がったファイル本体になります。

```coffeescript:bleno.coffee
bleno = null
keypress = require 'keypress'

class Bleno

  updateCallback: null

  constructor: ()->
    try
      # blenoはrequireが例外を吐く
      bleno = require 'bleno'
    catch error
      # blenoが例外を吐いたら終了
      bleno = null
      console.log "This mac cannot use me, good bye."
      process.exit(0)
      return
    # 初期化メソッドコール
    @init()

  init: ()->
    # デバイス名定義
    name = 'BlenoKeyboard'
    serviceUuids = [ '1192d150-e46d-11e6-bf01-fe55135034f3' ]

    # Serviceの定義
    primaryService = new bleno.PrimaryService
      uuid: serviceUuids[0]
      characteristics: [

        # Characteristicの定義
        new bleno.Characteristic
          uuid: '1192d57e-e46d-11e6-bf01-fe55135034f3'
          properties: [
            'notify'
          ]

          # Notifyに登録された時
          onSubscribe: (maxValueSize, updateValueCallback)=>
            console.log 'start input'
            @updateCallback = updateValueCallback

          # Notifyが解除された時
          onUnsubscribe: ()=>
            console.log 'stop input'
            @updateCallback = null
      ]

    # bluetoothデバイスの状態変化イベント
    bleno.on 'stateChange', (state) ->
      console.log 'stateChange: ' + state
      if state == 'poweredOn'
        bleno.startAdvertising name, serviceUuids, (error) ->
          if error
            console.error error
          return
      else
        # startする
        bleno.stopAdvertising()
      return

    # advertising startイベント
    bleno.on 'advertisingStart', (error) ->
      if !error
        console.log 'start advertising...'
        # Service設置
        bleno.setServices [ primaryService ]
      else
        console.error error
      return

    @initKeyPress()


  # keypress初期化
  initKeyPress: ()->
    keypress(process.stdin)
    process.stdin.on 'keypress', (ch, key)=>
      # どんなオブジェクトがくるか確認したい時
      # console.log('got "keypress"', ch)
      # console.log('got "keypress"', key)

      # Ctrl+Cで終了(書いておかないと困る)
      if key and key.ctrl and key.name is 'c'
        console.log 'pause!'
        @quitByInput()
        return

      # STR作成メソッドコール
      str = @makeStrByKey ch, key
      # 送信メソッドコール
      if str then @send str

    # process.stdin起動
    process.stdin.setRawMode true
    process.stdin.resume()

  # chとkeyに応じて返すSTRを決める
  makeStrByKey: (ch, key)->
    str = ""
    if key and key.name is 'backspace'
      # backspaceやrightみたいなメタ文字は加工して投げる。
      str = "{{{backspace}}}"
    else if key and key.name is "return"
      str = "\n"
    else if key and key.name is "right"
      str = "{{{right}}}"
    else if key and key.name is "left"
      str = "{{{left}}}"
    # tabやShift-tabを投げられれば便利だと思ったけど、iOS側が対応していないらしい。
    # else if key and key.name is "tab" and key.shift
    #   str = "{{{tab}}}"
    #   console.log 'shift-tab'
    # else if key and key.name is "tab" and not key.shift
    #   str = "{{{shift-tab}}}"
    #   console.log 'tab'
    else if ch
      str = ch
    console.log str
    return str

  # 文字をiOSに送信する
  send: (str)->
    if not @updateCallback then return
    data = Buffer.from str, 'utf8'
    # 送信！
    @updateCallback data

  # 正常な終了
  quitByInput: ()->
    process.stdin.setRawMode false
    process.stdin.pause()
    process.exit(1)



module.exports = Bleno

```


# 動作確認
UUIDを変えたので前回のiOSアプリでは動作しません。

このようにBletoothのペリフェラルをテストする、動作を確認する際には、よくLightBlue Explorer（ https://itunes.apple.com/us/app/lightblue-explorer-bluetooth/id557428110?mt=8 ）のような汎用のBLE制御アプリを使います。

![Screen Shot 2017-01-27 at 6.02.19 PM.png](https://qiita-image-store.s3.amazonaws.com/0/9162/6c001ee5-8e1f-0a3d-d6f2-36981df3c138.png)

Mac側でNodejsアプリケーションを走らせます。

```
node index.js
```

次に、Light Blueでデバイスとサービスをたどり、作成したCharacteristicへ接続します。
データ形式を文字列にして、'Listen on notification'をタップして。。

![IMG_0026.JPG](https://qiita-image-store.s3.amazonaws.com/0/9162/e7d54701-a5e1-1147-b0af-5eee34a9a305.jpeg)
![IMG_0027.JPG](https://qiita-image-store.s3.amazonaws.com/0/9162/d6daf098-1a0c-bda9-21eb-b7b94397cb5a.jpeg)

動いた！
特殊文字もちゃんと制御できています。素晴らしい。さすが。

# まとめ
bleno超たのしい。
次はiOS側をこれに適応した形に調整しないといけません。それもまた解説していきたいと思います。