Raspberry Pi で toio コアキューブをコントロールする（２回目）～ Notifyの処理+おまけ

(※) タイトル変更, リンク追加(2019/06/30)


[前回　Raspberry Pi で toio コアキューブをコントロールする（１回目）](https://qiita.com/FiveOne/items/505f369d1f77fa846a8b)の続き。

今回は、toio コアキューブから `Notify`を受け取って処理してみる。
[toio 通信概要](https://toio.github.io/toio-spec/docs/ble_communication_overview#%E3%82%AD%E3%83%A5%E3%83%BC%E3%83%96%E3%81%AE%E6%A9%9F%E8%83%BD%E3%81%AE%E5%88%A9%E7%94%A8)から、

* ID Information / 読み取りセンサー
* Sensor Information / モーションセンサー
* Button Information / ボタン

の３つの`Notify`を受け取ってみる。（最後に2台のキューブ同時制御のおまけあり）

# 前準備

必要な環境は、[前回](https://qiita.com/FiveOne/items/505f369d1f77fa846a8b)のまま。

# CoreCube クラス

コアキューブとのもろもろの通信部分を、簡単にクラス化した。エラー処理とか、全然していないので、参考程度ということで。ここに置くと大きいので、githubに置いたので、ダウンロードして、サンプルコードと同じフォルダに入れておくこと。

* [CoreCubeクラス](https://github.com/fiveone51/toioCoreCube/blob/master/coreCube.py)

# サンプルコード

以下のような動作をするソースコード

* コアキューブのボタンを押すと sound id = 2 が再生される。
* コアキューブをたたくと、sound id = 6 が再生される。
* マット、カードによって、X,Y座標、角度や、カード番号などが表示される。
* 何も Notifyが返ってこない時間が 10秒になると、処理終了

````sample_notify.py
from coreCube import CoreCube
import time
import bluepy
import sys
import struct

class MyDelegate(bluepy.btle.DefaultDelegate):
    def __init__(self, params, ptoio):             # コンストラクタで対応するtoioを指定する
        bluepy.btle.DefaultDelegate.__init__(self)
        self.ctoio = ptoio

    # notify callback: cHandle で何のNotifyかを見分けて処理分岐
    def handleNotification(self, cHandle, data):
        # ------------- ボタン
        if cHandle == CoreCube.HANDLE_TOIO_BTN:
          id, stat = struct.unpack('BB', data[0:2])
          if stat == 0x80:
            self.ctoio.soundId(2)
        # ------------- モーションセンサー
        if cHandle == CoreCube.HANDLE_TOIO_SEN:
          id, horizon, collision = struct.unpack('BBB', data[0:3])
          print("SENSOR:   HORIZON={:02x}, COLLISION={:02x}".format(horizon, collision))
          if collision:
            self.ctoio.soundId(6)
        # ------------- IDセンサー
        if cHandle == CoreCube.HANDLE_TOIO_ID:
          id = struct.unpack('b', data[0:1])[0]
          if id == 0x01:
            x, y, dir = struct.unpack('hhh', data[1:7])
            print("X,Y,dir = (%d,%d), %d" % (x,y,dir))
          elif id == 0x02:
            stdid = struct.unpack('i', data[1:5])[0]
            dir = struct.unpack('h', data[5:7])[0]
            print("ID = %d,  dir = %d" % (stdid,dir))

if __name__ == "__main__":

  # --- コアキューブへの接続（自動接続を行うには、rootで実行する必要あり）
  toio = CoreCube()
  if len(sys.argv) == 1:
    toio_addr = toio.cubeFinder()
    print("Connect to " + toio_addr)
  else:
    toio_addr = sys.argv[1]
  toio.connect(toio_addr, bluepy.btle.ADDR_TYPE_RANDOM)

  time.sleep(1)

  # --- Notifyを受け取るクラスの設定
  toio.withDelegate(MyDelegate(bluepy.btle.DefaultDelegate, toio))

  # --- Notifyを要求
  toio.writeCharacteristic(CoreCube.HANDLE_TOIO_ID  + 1, b'\x01\x00', True)
  toio.writeCharacteristic(CoreCube.HANDLE_TOIO_SEN + 1, b'\x01\x00', True)
  toio.writeCharacteristic(CoreCube.HANDLE_TOIO_BTN + 1, b'\x01\x00', True)

  # --- Notify待ち関数を実行させる。 10秒Notifyがなければ終了
  while True:
    if toio.waitForNotifications(10.0):
      pass
    else:
      break

  toio.disconnect()
````

考え方としては、`bluepy` の `Peripheralクラス`(CoreCubeクラスの親クラス)に、`withDelegateメソッド`で、`Notify`が返ってきたときの実行されるクラスを定義してあげればよい。

````.py
  # --- Notifyを受け取るクラスの設定
  toio.withDelegate(MyDelegate(bluepy.btle.DefaultDelegate, toio))
````

次に、コアキューブにNotifyを要求する必要がある。以下のように、Notifyが必要な Handleに +1 した値に対して、01(b'\x01\x00') を書き込むと Nofity を要求することができる（Notify を無効にする場合は、00 を書き込む）。
BLEの用語的には、CCCDにNotify通知の許可フラグを設定する、ということらしい。ほとんどの特性において、Handle に +1 したものが、CCCDになるようなので、深く考えず設定している。

````.py
  # --- Notifyを要求
  toio.writeCharacteristic(CoreCube.HANDLE_TOIO_ID  + 1, b'\x01\x00', True)
  toio.writeCharacteristic(CoreCube.HANDLE_TOIO_SEN + 1, b'\x01\x00', True)
  toio.writeCharacteristic(CoreCube.HANDLE_TOIO_BTN + 1, b'\x01\x00', True)
````

Notifyを処理するクラス（ここでは、MyDelegateクラス）には、2つのメソッドが定義されていて、一つはコンストラクタ。もう一つは Notify が返ってきたときに実行されるメソッド（handleNotification）。
Notify が返ってきたときに、このメソッドに、Notify 通知元のHandle(cHandle)と、実際のデータ(data)が渡される。このHandleで何のNotifyかを見分けて処理分岐を行えばよい。


````.py
    def handleNotification(self, cHandle, data):
        # ------------- ボタン
        if cHandle == CoreCube.HANDLE_TOIO_BTN:
            #（ボタンが押されたときの処理）
        # ------------- モーションセンサー
        if cHandle == CoreCube.HANDLE_TOIO_SEN:
            #（モーションサンサーの情報が変化したとき）
        # ------------- IDセンサー
        if cHandle == CoreCube.HANDLE_TOIO_ID:
            #（読み取りセンサーの読み取る情報が変化したとき）
````

（せっかく、CoreCobeクラスとして、BLE通信処理を隠したのに、MyDelegate クラスにBLE通信処理が出てきてしまっているのが、気持ち悪い・・・。改善の余地あり。）

# サンプルの実行

前回、いちいちコアキューブのアドレスを指定するのが面倒だったので、scanして、自動で接続するようなメソッド(cubeFinder）を追加した。ただし、scan は root で実行する必要があるので、要注意。上記のサンプルでは、2種類の実行方法があるので、お好きな方で。

````
$ python3 sample.py **:**:**:**:**:**                    # コアキューブのアドレスを指定
  または
$ sudo python3 sample.py                                 # 一番近くのコアキューブに自動接続
````

実際に実行し、コアキューブをマットやカードの上に置くと、びっくりするぐらい（1秒間に何十個）XY座標や角度の情報が返ってくるのが分かる。[技術仕様](https://toio.github.io/toio-spec/docs/ble_id#%E9%80%9A%E7%9F%A5)には、センサーの情報が変化するとNotifyが返るとなっているが、そんなに変わるものなのか？と思いよく見ると、微妙に角度や座標が変化しているようだ。それだけ、細かい情報を読んでいるということか。

````
$ sudo python3 sample_notify.py
Connect to **:**:**:**:**:**
SENSOR:   HORIZON=00, COLLISION=00
SENSOR:   HORIZON=01, COLLISION=00
SENSOR:   HORIZON=00, COLLISION=01
SENSOR:   HORIZON=00, COLLISION=01
SENSOR:   HORIZON=01, COLLISION=00
SENSOR:   HORIZON=01, COLLISION=00
SENSOR:   HORIZON=01, COLLISION=01
X,Y,dir = (323,340), 270
X,Y,dir = (323,341), 270
SENSOR:   HORIZON=01, COLLISION=01
X,Y,dir = (323,15957), 256
X,Y,dir = (323,341), 268
X,Y,dir = (323,341), 270
X,Y,dir = (323,342), 271
X,Y,dir = (323,341), 270
X,Y,dir = (323,342), 271
X,Y,dir = (323,341), 270
X,Y,dir = (323,341), 270
X,Y,dir = (323,342), 271
X,Y,dir = (323,341), 270
X,Y,dir = (323,342), 271
X,Y,dir = (323,342), 271
X,Y,dir = (323,341), 270
X,Y,dir = (323,342), 271
X,Y,dir = (323,342), 271
X,Y,dir = (323,341), 270
X,Y,dir = (323,342), 271
X,Y,dir = (323,341), 270
X,Y,dir = (323,342), 271
X,Y,dir = (323,342), 271
X,Y,dir = (323,341), 270
X,Y,dir = (323,342), 271
X,Y,dir = (323,341), 270
X,Y,dir = (323,342), 271
  :
````

# おまけ（２つのキューブの同時制御）

せっかく、今回 CoreCube クラスを定義したし、toio に２つのキューブが入っているので、2台を同時に接続して、輪唱させてみた。
「同時制御」なんて仰々しいように書いているが、単に、インスタンスを二つ作るだけ。

````sample_flog.py
from coreCube import CoreCube
import time
import bluepy

toio1_addr   = '**:**:**:**:**:**'      # 実際には、コアキューブのアドレス
toio2_addr   = '**:**:**:**:**:**'

toio1 = CoreCube()
toio1.connect(toio1_addr, bluepy.btle.ADDR_TYPE_RANDOM)

toio2 = CoreCube()
toio2.connect(toio2_addr, bluepy.btle.ADDR_TYPE_RANDOM)

MELODY_LENGTH = 0.5
MELODY_FLOG = [60, 62, 64, 65, 64, 62, 60, 128, 64, 65, 67, 69, 67, 65, 64, 128, 60, 128, 60, 128,
 60, 128, 60, 128, 60, 62, 64, 65, 64, 62, 60, 128, 128, 128, 128, 128, 128, 128, 128, 128]

time.sleep(1)

for i in range(len(MELODY_FLOG)):
  toio1.soundMono(0xFF, MELODY_FLOG[i])
  if i >=8:
    toio2.soundMono(0xFF, MELODY_FLOG[i-8])
  time.sleep(MELODY_LENGTH)

toio1.soundStop()
toio1.disconnect()
toio2.soundStop()
toio2.disconnect()
````

「カエルの歌」を2台のコアキューブで輪唱させてみた。:grin:
せっかくだから、2台ではなく、4台ぐらいでやってみたいけど、コアキューブがない・・・。

# 今後
技術仕様で公開されているコマンドでは、単に車輪の速さを指定することができるだけ。しかし、[toio ビジュアルプログラミング](https://toio.io/programming/visual-programming.html)で提供されている SCRATCH のコマンドには、「(X座標, Y座標) = (xx,yy)に動かす」とか、「xx度に向ける」というものがある。[ソースコードが公開](https://github.com/toio/toio-visual-programming/)されているので、ちょっと中を覗いて、CoreCubeクラスに追加できるか考えてみたい。


# 続き
[Raspberry Pi で toio コアキューブをコントロールする（３回目）～ ID(位置情報)の処理](https://qiita.com/FiveOne/items/176f5ad73d8e383b3505)


# 参考サイト

* [Raspberry Pi で toio コアキューブをコントロールする（１回目）](https://qiita.com/FiveOne/items/505f369d1f77fa846a8b)
* [CoreCubeクラス（自作）](https://github.com/fiveone51/toioCoreCube/blob/master/coreCube.py)
* [(公式) toio コアキューブ　技術仕様](https://toio.github.io/toio-spec/)
* [(公式) toio ビジュアルプログラミング](https://toio.io/programming/visual-programming.html)　[(ソースコード)](https://github.com/toio/toio-visual-programming/)