U.F.O. SAをPythonistaで操作する方法

U.F.O. SAでbluetoothによって音声同期できると聞いて購入したものの専用のアダプタをPCに挿さなくては使えないらしい。

アダプタを買うために送料を払うのもためらわれるし、わざわざ音声作品を聞くためにパソコンを起動するのも面倒くさい。

そこで、iPhoneから操作すれば、無料だし（無料とはいってない）、寝る前にすぐに使えて便利ということで、iPhoneにおいて最強のPython統合開発環境、Pythonistaを用いてU.F.O. SAを操作できるようにした。

#### 準備するもの
- U.F.O. SA
- iPhone or iPad
- Pythonista （有料アプリ）

#### 参考にしたサイト

[騎士の物語](https://yihuishou.github.io/2020/10/10/2/)
中国語のサイトです。Google翻訳を駆使してください。

[Pythonista cb module](https://omz-software.com/pythonista/docs/ios/cb.html)
Pythonistaの公式のcb moduleのチュートリアル。


# U.F.O. SAを操作する仕組みを考える

U.F.O. SAはBLE (Bluetooth Low  Energy)によって操作されているらしい。
消費電力の少ない通信規格ということしかわからないけども、個体識別番号(UUID)を識別して接続し、データを送信すれば動くのでまあ良し。

今回は、SERVISE_UUIDとCHAR_UUIDという2つのUUIDを取得することで、接続が可能になる。
SERVISE_UUIDとCHAR_UUIDはそれぞれ、個体識別番号におけるフォルダとファイルのような関係になる。

基本的な流れは、

1. デバイス名で検索
1. SERVISE_UUIDとCHAR_UUIDを用いてBLE接続
1. 特定のコマンドを送信

この手順を踏むことでU.F.O. SAを動かすことができる。

## U.F.O. SAのデバイス名とUUID
U.F.O. SAのデバイス名は`UFOSA`のようだ。

UUIDはU.F.O. SAやサイクロンSAなどVORZE製品で共通で以下の通り。

```python:
SERVISE_UUID = '40ee1111-63ec-4b7f-8ce7-712efd55b90e'
CHAR_UUID = '40ee2222-63ec-4b7f-8ce7-712efd55b90e'
```

## 命令バイトについて考える

命令はバイト文字列で渡し、`デバイスIDバイト　予約バイト　命令バイト`の順に命令を送ります。 

デバイスIDバイトはサイクロンSAで0x01, U.F.O. SAで0x02, ピストンSAで0x03です。
予約バイトはVORZE製品共通で0x01です。

命令バイトは方向と回転の情報を8ビットで与えます。
方向はバイトの最初のビットで指定します。0は時計回りの回転、1は反時計回りの回転を意味します。
速度は、残りの7ビットによって指定され、0~100の値をとります。

例えば、反時計回りに速度50にしたいのであれば、`0x01 << 7 | 0x32`と回転の命令を7 bitずらし、速度の情報を加算すれば、良いです。

つまり、U.F.O. SAで反時計回りに速度50の命令を出すならば、`0x02 0x01 (0x01 << 7 | 0x32)`と3バイトのバイト文字列を送信すれば、よいことになります。

サイクロンSAであれば、`0x01 0x01 (0x01 << 7 | 0x32)`です。

# Pythonistaでコードを考える

Pythonistaには標準でBLE通信ができる[cb (Core Bluetooth)モジュール](http://omz-software.com/pythonista/docs/ios/cb.html)という素敵なモジュールが搭載されている。
基本的にPythonista公式のcbモジュールのチュートリアルを書き換えれば、操作できます。

まず、cbモジュールの要請に合ったクラスを用意する。
このクラスの詳細は、公式のチュートリアルに任せるとして、チュートリアルのHeartRateManagerクラスから、変更がある点だけ、抜粋する。

## 1. デバイス名で検索
デバイス名で接続する機器を指定します。`did_discover_peripheral`メソッドを次のようにする。

```python:
def did_discover_peripheral(self, p):
    print(p.name)
    if p.name and 'UFOSA' in p.name and not self.peripheral:
        self.peripheral = p
        print('Connecting UFO SA...')
        cb.connect_peripheral(p)
```

これで、デバイス名の`UFOSA`が見つかった場合に接続を開始します。

## 2. SERVISE_UUIDとCHAR_UUIDを用いてBLE接続

これは、初めに以下のように定義すれば、良いです。

```python:
SERVISE_UUID = '40ee1111-63ec-4b7f-8ce7-712efd55b90e'
CHAR_UUID = '40ee2222-63ec-4b7f-8ce7-712efd55b90e'
```

## 3. 特定のコマンドを送信

`did_discover_characteristics`メソッドの中身を書き換えます。

```python:
def did_discover_characteristics(self, s, error):
    print('Did discover characteristics...')
    for c in s.characteristics:
        print(c.uuid)
        if c.uuid == CHAR_SSID:
            self.peripheral.set_notify_value(c, True)
            self.send_rotation_commands(c)
```

`send_rotation_commands`メソッドの中身をたとえば、以下のようにすると、U.F.O. SAで反時計回りに速度50の命令を出します。

```python:
def send_rotation_commands(self, c):
    b = b'\x02\x01' + ((0x01) << 7 | 0x32).to_bytes(1, byteorder='big')
    self.peripheral.write_characteristic_value(c, b, True)
```

ランダムな回転方向に強さ5, 10, 15, 20の4段階で、5–10秒の間隔で回転を変更させるのであれば、このように書き換えます。
速度の変化や回転の向きが予測できず、これだけで、実用できるレベルになることかと思います。

```python:
def send_rotation_commands(self, c):
    import time
    import random
    for i in range(100):
        time.sleep(random.randint(5, 10))
        direction = [0x00, 0x01][random.randint(0, 1)] << 7
        intensity = 5 * random.randint(1, 4)
        b = b'\x02\x01' + (direction | intensity).to_bytes(1, byteorder='big')
        self.peripheral.write_characteristic_value(c, b, True)
```

# おわりに

今回は、命令の出し方までを説明しました。次回は、csvファイルに合わせた命令の出し方を考えていこうと思います。