左手デバイス「Orbital2」の秘めたる力

バーチャルSNS「cluster」主催のGAMEJAMに応募した作品が、デジタルハリウッド大学賞を受賞しました。
デジタルハリウッド大学様のご厚意により「[Orbital2](https://brainmagicproduct.com/?from=c3d)」を賞品として頂いたので、感想を書き連ねていこうと思います。

#目次
- [Orbital2とは](#orbital2とは)
- [Orbital2の感想](#orbital2の感想)
- [Orbital2への要望](#orbital2への要望)
- [Orbital2の振る舞いを観察する](#orbital2の振る舞いを観察する)
 - [ハードウェアを確認する](#ハードウェアを確認する)
 - [シリアル通信を読む](#シリアル通信を読む)
- [Pythonで制御する](#pythonで制御する)
 - [キーボードとマウスを操る](#キーボードとマウスを操る)
 - [BlenderのShiftキーに対応する](#blenderのshiftキーに対応する)
 - [アプリごとに自動でプロファイルを切り替える](#アプリごとに自動でプロファイルを切り替える)
 - [ジョイスティックの読み取り](#ジョイスティックの読み取り)
 - [リストメニュー](#リストメニュー)
 - [グロウリングについて](#グロウリングについて)
 - [バイブレーションについて](#バイブレーションについて)
- [Blenderで活用する](#blenderで活用する)
 - [BlenderにOrbital2を召喚する](#blenderにorbital2を召喚する)
- [おわりに](#おわりに)


#Orbital2とは
Orbital2は、株式会社BRAIN MAGIC (Blackmagicじゃないよ) が開発した左手デバイスです。
詳細は[公式サイト](https://brainmagicproduct.com/?from=c3d)まで。

>![o2_lt.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2018473/f7cfe091-c5cc-b26f-e9f9-8b6bd1c07bb2.png)
出典:「[クリエイターを科学するミライの左手デバイス「Orbital2（オービタルツー）」](https://brainmagicproduct.com/)」

フラットリングと呼ばれる8つのスイッチや、ジョイスティックを倒す・回す・押す動作を組み合わせて色々できるみたいですね。

1658万色に輝くグロウリングも、ゲーマーにとっては嬉しいポイントです。

画像には書かれていませんが、振動によるフィードバック機能もあります。

#Orbital2の感想
Orbital2の使い方・導入方法は公式サイトに掲載されているので省略します。
[Orbital2の使い方・導入 for Windows](https://brainmagicportal.com/views/ja/howtouse/win/)
[Orbital2の使い方・導入 for Mac](https://brainmagicportal.com/views/ja/howtouse/mac/)

まずは、作業の様子を御覧ください
![operate.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2018473/ad4cf5dc-2ec2-a106-6251-15d882e00b1c.gif)
画面だけでは何をやっているのかわかりにくいですね。

ジョイスティックを傾けるとパイメニューが出てくるので、そのままダイヤルを回転させてショートカットを実行しています。

物理的にダイヤルを増やすデバイスが多い中、ジョイスティックを傾けてダイヤルの動作を選択する仕組みは洗練されていて気に入りました。

ショートカット名は画面にオーバーレイ表示されるので、どこに何の機能を登録したか一目瞭然です。

ポップアップとして、リスト形式のメニューを使用することも出来ます。

![listmenu_lt.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2018473/a57e00bc-4f08-31ad-5fbd-bf4c75622860.png)

##質感について

つや消し加工のさらさらした質感は、HTC VIVEを彷彿とさせます。

ダイヤルにはクリックタイプのロータリーエンコーダが採用されており[^1]、コトコトとした静かな回し心地です。

[^1]: https://twitter.com/Brain_Magi/status/1382650407936266241

![o2andvive.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2018473/29d88374-e733-5d35-387e-fee68850ce0b.jpeg)

<br>


#Orbital2への要望

Orbital2の開発チームは、要望、バグ報告を[Twitter](https://twitter.com/Brain_Magi)や[GitHub](https://github.com/brainmagi/Orbital2Core/issues)で受け付けています。

私も以前、Orbital2 Coreソフトウェアの誤字を報告したのですが、わずか一週間ほどで修正ビルドが上がってきました[^2]。
この対応の速さは素晴らしいです。

[^2]: https://github.com/brainmagi/Orbital2Core/issues/3

しかし、当たり前ではありますが、すべての要望が対応されるわけではありません。
この章では、2021年10月21日現在の、個人的なOrbital2への要望を挙げていきます。

##プロファイルを自動で切り替えたい

Orbital2 Coreには、アクティブなアプリケーションを検出して自動でプロファイルを切り替える機能がありません。

「ひとつのアプリにつき複数のプロファイルを使用することを想定している[^3]」とのことですが、オプションで欲しい機能ではあります。

[^3]: https://twitter.com/Brain_Magi/status/1211376954387001349


##BlenderのShiftキーについて

標準のBlenderのビューポートは、Shiftキーを押すことで視点を操作します。

ところが、Orbital2のボタンにShiftキーを割り当てると、Blenderの視点操作に想定外の挙動がみられます。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【お知らせ】BlenderにてOrbital2をご利用の際に、「Shiftキー」の長押しが解除されない不具合が発生しています。<br>現在改修を行っておりますので、ご不便をおかけし誠に申し訳ございませんが、改修完了まで今しばらくお待ちくださいませ。</p>&mdash; BRAINMAGIC(ブレインマジック)公式 #Orbital2 (@Brain_Magi) <a href="https://twitter.com/Brain_Magi/status/1179688808859590657?ref_src=twsrc%5Etfw">October 3, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

この問題は、2年以上前から指摘されていますが、未だ修正されずにいます。


##Linuxで使いたい
個人的に、作業をUbuntuで行うことが多いので、Linuxがサポートされると嬉しいです。ただ、ディスプレイサーバやディストーションは星の数ほどありますので、対応の手間を考えるとサポートが難しいのは理解できます。

#Orbital2の振る舞いを観察する
##ハードウェアを確認する
前置きが長くなりましたが、前述の問題を解決すべくOrbital2の振る舞いを観察していきます。
まずはじめに、Orbital2をLinuxに接続し、dmesgコマンドでデバイスがどのように扱われたかを確認します。

```
$ dmesg
[ 4040.701637] usb 5-2: USB disconnect, device number 6
[ 4040.701837] cp210x ttyUSB0: cp210x converter now disconnected from ttyUSB0
[ 4040.701853] cp210x 5-2:1.0: device disconnected
[ 4043.033322] usb 5-2: new full-speed USB device number 8 using xhci_hcd
[ 4043.197246] usb 5-2: New USB device found, idVendor=10c4, idProduct=ea60, bcdDevice = 1.00
[ 4043.197253] usb 5-2: New USB device strings: Mfr=1, Product=2, SerialNumber=3
[ 4043.197255] usb 5-2: Product: CP2103 USB to UART Bridge Controller
[ 4043.197257] usb 5-2: Manufacturer: Silicon Labs
[ 4043.197258] usb 5-2: SerialNumber: 0005
[ 4043.202251] cp210x 5-2:1.0: cp210x converter detected
[ 4043.205256] usb 5-2: cp210x converter now attached to ttyUSB0
```

どうやらSilicon Labs製のUSBシリアル「[CP2103](https://www.silabs.com/documents/public/data-sheets/CP2103.pdf)」が使われているようです。
また、コンバータがttyUSB0に割り当てられていることも確認できました。

これからttyUSB0の通信を眺めていきます。

##シリアル通信を読む

シリアル通信を解析するツールはたくさんありますが、私はArduino IDEを使いました。
ポートがbusyになることを防ぐため、モニタリングする前にOrbital2 Coreの接続を切りましょう。

:::note warn
WSL2はシリアルやUSBデバイスがサポートされていないので注意
:::

シリアルモニタを開いて、適当にOrbital2を操作してみます。

```

J S X <8b> Y ﾆ ; J S X <8b> Y ﾃ ; J S X <87> Y ﾃ ; S W 8 = 1 ; J S X <87> Y ｾ ; J S X <83> Y ｾ ; J S X <80> Y ｻ ; R E - R ; J S X { Y ｷ ; J S X x Y ｲ ; J S X u Y ｮ ; S W 6 = 1 ; J S X u Y ｫ ; J S X r Y ｫ ; J S X r Y ｯ ; J S X u Y ｴ ; J S X u Y ｸ ; J S X u Y ｽ ; J S X x Y ﾁ ; S W 6 = 0 ; J S X x Y ﾅ ; J S X ~ Y ﾅ ; J S X <81> Y ﾅ ; S W 6 = 0 ; J S X <85> Y ﾅ ; J S X <89> Y ﾊ ; S W 6 = 0 ; S W 6 = 0 ;

```

改行されていないため、非常に見辛いですね。ボタンを一つずつ押して確認していきます。

####ダイヤル右回転

```:ダイヤル右回転
R E - R ;
```

####ダイヤル左回転

```:ダイヤル左回転
R E - L ;
```

####フラットリングの手前のボタン (押した後離さない)

```:フラットリングの手前のボタン(押した後離さない)
S W 7 = 1 ;
```
`S W 7 = 1 ;`が1回だけ流れてきました。
連続して1が流れてくるわけではないみたいです。

####フラットリングの手前のボタン (クリック)

```:フラットリングの手前のボタン(クリック)
S W 7 = 1 ; S W 7 = 0 ; S W 7 = 0 ; S W 7 = 0 ; S W 7 = 0 ;
```

クリックするたびに、1,0,0,0,0 のセットが流れてきます。

####フラットリングの手前のボタン (長押し)

```:フラットリングの手前のボタン(長押し)
S W 7 = 1 ; S W 7 = 0 ; S W 7 = 0 ; S W 7 = 0 ; S W 7 = 0 ;
```

ボタンを押し込むと`S W 7 = 1 ;`が1回流れ、離すと`S W 7 = 0 ;`が4回流れます。

####オービタルエンジンの頂点押し込み

```:オービタルエンジンの頂点押し込み
R C 4 = 1 ; R C 4 = 0 ; R C 4 = 0 ; R C 4 = 0 ; R C 4 = 0 ;
```

オービタルエンジンの頂点押し込みも、フラットリングのボタンと同じ挙動です。

####ジョイスティック

```:ジョイスティック(テキスト変換)
J S X { Y <81> ; J S X x Y <81> ; J S X u Y <81> ; J S X u Y <84> ; J S X r Y <84> ; J S X r Y <87> ; J S X n Y <87> ; J S X n Y <8a> ; J S X n Y <8d> ; J S X k Y <8d> ; J S X k Y <90> ; J S X h Y <90> ; J S X h Y <93> ; J S X h Y <96> ; J S X e Y <96> ; J S X e Y <99> ; J S X b Y <99> ; J S X b Y <9e> ; J S X _ Y <9e> ; J S X _ Y ｢ ; J S X \ Y ｢ ; J S X \ Y ･ ; J S X \ Y ｩ ; J S X Y Y ｭ ; J S X Y Y ｰ ; J S X Y Y ｳ ; J S X Y Y ｷ ; J S X Y Y ｺ ; J S X Y Y ｿ ; J S X Y Y ﾂ ; J S X \ Y ﾅ ; J S X _ Y ﾅ ; J S X b Y ﾅ ; J S X b Y ﾈ ; J S X e Y ﾈ ; J S X h Y ﾈ ; J S X l Y ﾈ ; J S X l Y ﾋ ; J S X o Y ﾋ ; J S X r Y ﾎ ; J S X v Y ﾑ ; J S X z Y ﾑ ; J S X z Y ﾔ ; J S X ~ Y ﾔ ; J S X ~ Y ﾐ ; J S X <83> Y ﾐ ; J S X <87> Y ﾍ ; ...
```

`JS`,`X`,`Y`などの文字が見えます。16進数でも確かめてみましょう。

```:ジョイスティック(16進数)
4a 53 58 7b 59 81 3b 4a 53 58 78 59 81 3b 4a 53 58 75 59 81 3b 4a 53 58 75 59 84 3b 4a 53 58 72 59 84 3b 4a 53 58 72 59 87 3b 4a 53 58 6e 59 87 3b 4a 53 58 6e 59 8a 3b 4a 53 58 6e 59 8d 3b 4a 53 58 6b 59 8d 3b 4a 53 58 6b 59 90 3b 4a 53 58 68 59 90 3b 4a 53 58 68 59 93 3b 4a 53 58 68 59 96 3b 4a 53 58 65 59 96 3b 4a 53 58 65 59 99 3b 4a 53 58 62 59 99 3b 4a 53 58 62 59 9e 3b 4a 53 58 5f 59 9e 3b 4a 53 58 5f 59 a2 3b 4a 53 58 5c 59 a2 3b 4a 53 58 5c 59 a5 3b 4a 53 58 5c 59 a9 3b 4a 53 58 59 59 ad 3b 4a 53 58 59 59 b0 3b ...
```

以下の図は、`4a 53`を`JS`,  `58`を`X`,  `59`を`Y`,  `3b`を`;`へ置き換えた様子です。

![16_lt.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2018473/70f3a674-2e5e-62f9-5c1c-daa226e09757.png)

ジョイスティックの読み取りはチームメンバーの[おにさわ](https://twitter.com/onisawa_v)さんからご助言いただきました。ありがとうございます。

#Pythonで制御する
ざっくりとシリアル通信が読めたので、Pythonを使ってOrbital2 Coreの代替スクリプトを書いていきましょう。(`Python 3.10.0`で検証)
Pythonでシリアル通信するために、[pySerial](https://pythonhosted.org/pyserial/)というライブラリを使います。

```
$ pip install pyserial
```

```test.py
import serial
import time

ser = serial.Serial('接続しているポート番号','9600',timeout=0.05)
time.sleep(1)

while True:
    SR = (ser.readline())
    SRstr = str(SR)

    if "RC4=1" in SRstr:
        print('exit')
        ser.close()
        exit()

    elif "SW7=1" in SRstr:
        print('Key7が押されました')
```

フラットリングの手前のボタンを押すと、コンソールに「Key7が押されました」と表示されるはずです。
オービタルエンジンの頂点を押すことで、スクリプトの実行を終了します。

:::note warn
import serialが通らない場合は、serialとpySerialが競合していないか確認しましょう 
:::

:::note warn
シリアル通信をするスクリプトだからといって、スクリプト名を「serial.py」にするのはやめましょう 
:::

##キーボードとマウスを操る

Pythonでキーボードとマウスを操るには、[PyAutoGUI](https://pypi.org/project/PyAutoGUI/)というライブラリが便利です。

```
$ pip install pyautogui
```

```python
import pyautogui

pyautogui.press('space')
```

キーボードのSpaceを押すだけのスクリプトです。先程書いたシリアル通信のスクリプトといい感じに組み合わせてみてください。
長押ししたい場合は
`SW7=1`のときに`pyautogui.keyDown('space')`して、
`SW7=0`のときに`pyautogui.keyUp('space')`すると良いでしょう。

PyAutoGUIの詳しい使い方は[ドキュメント](https://pyautogui.readthedocs.io/en/latest/quickstart.html)をご確認ください。画像認識もできるので、ショートカットキーの登録ができないアプリケーションも力技でなんとかすることができます。


##BlenderのShiftキーに対応する

PyAutoGUIが送信する仮想キーコードでは、Shiftキーによる視点操作ができません。
対応策として、[PyDirectInput](https://pypi.org/project/PyDirectInput/)を使用します。

```
$ pip install pydirectinput
```

```python
import pyautogui
import pydirectinput

pydirectinput.keyDown('shift')
pydirectinput.keyUp('shift')
```

これで、BlenderのShiftキーでの視点操作が正しく機能するはずです。


##アプリごとに自動でプロファイルを切り替える

アプリごとに自動でプロファイルを切り替えるには、アクティブウィンドウの情報を取得する必要があります。

詳しい方法は、きむら しのぶ (@mix_dvd) さんの記事に記載されています。
[Pythonを使ってWindowsやMacを監視し、操作中のアプリの情報を収集してみる](https://qiita.com/mix_dvd/items/00d177097e8b3dea4db4)

```:取得したログの例
Firefox Developer Edition
TweetDeck at DuckDuckGo — Firefox Developer Edition
TweetDeck — Firefox Developer Edition
Spotify Premium
山口百恵 - 横須賀ストーリー
Opening - Excel
Microsoft Excel
サンプル.xlsx - Excel
sample.py - Visual Studio Code
```

アプリ名だけでなく、様々な情報が取得できました。この情報をもとに条件分岐させましょう。

##ジョイスティックの読み取り

[前章](#ジョイスティック)でジョイスティックの信号を読み取りました。

![16_lt.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2018473/70f3a674-2e5e-62f9-5c1c-daa226e09757.png)

PythonでジョイスティックのXY座標を取得するには、以下のコードを記述します。

```python
if "JS" in SRstr:
    x = ord(SR[3:4])-128
    y = ord(SR[5:6])-128
    print("X",x,"Y",y)
```


##リストメニュー

Orbital2 Coreのリストメニューが便利だったので、似たようなものを作ります。
(長くなるので割愛)
GUIのフレームワークは何でも良いと思いますが、生成するウィンドウのlocationにはPyAutoGUIで取得したマウス座標を指定するのが良いでしょう。

```python
window = PySimpleGUI.Window('ListMenu', layout, location=pyautogui.position())
```

![tw.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2018473/966b1ea7-8cd8-e299-3035-95d80ea4dc3b.png)


##グロウリングについて

左手デバイスには、実用器としての価値と、鑑賞品の両面があるということを知らねばなりません。

大衆実用器として生まれたものには、往々、極めて低級な美を盛るに過ぎない大量的なものが多く、芸術的価値に富んで世間に騒がれているものは、その数こそ少ないですが、眼高の士の心をゆさぶるもの、即ち思想的個性の発露になるものが多いのでありまして、この両者を明確に区別してかかることが肝要であります。

実際、優れた左手デバイスに接しますと、名巌のような、松樹のような、琅玕竹のような、梅花のような、その美しさに打たれるものであって、それがどんな素材で出来ているとか、どういうふうにして作られたかというようなことは、第一義的には念頭に浮んで来ないのであって、立派な絵や建築を見る場合と少しも違わないのであります。第二義的には機能に渉り様々吟味もしますが……。

その中でもOrbital2などは日常自分でも使用して、あるいは愛玩したり、なんとなく親しみも生じて手にとって見たり、ダイヤルを廻してみたり、様々に変化のあるグロウリングの具合など賞めそやしたりして鑑賞の対象となり、嬉しさを感ずるのであります。

(改変元 : [陶器鑑賞について](https://www.aozora.gr.jp/cards/001403/card55080.html) 北大路魯山人)





<br>

かようにして、独自のスクリプトからグロウリングのLEDの色を操ることは、刻下の急務であろうと考えられます。

正規のOrbital2 Coreソフトウェアからグロウリングへ向かう信号を解析することも考えましたが、生憎なことに、Orbital2 Coreソフトウェアの利用規約第4条は、プロトコルの解析を禁じているのです。

それならばLEDの操作は諦めるべきであろうか。どうしたものであろうか……なぞと、思案に暮れておりましたところへ、BRAIN MAGICの方から
「できる範囲ではありますが、Orbital2について何か提供などご協力ができればと存じます」と言う旨の連絡が来ました。

このような機会以外に、私がグロウリングのLEDを操る術を得ることは、将来、滅多に来ないような気がしましたから、「グロウリングのシリアル通信について教えてほしい」と言う意味のメールを出しましたところ、一週間程経ったあとに、「LEDの色を変更するには`0x54, 0x00, red, 0x00, green, 0x00, blue, 0x00, 0x3b, 0x00`(red, green, blueは0x00〜0xFFまでのRGB)を送信すればよい」との返事を受け取りました。

正直のところを云うと「お答えできかねます」と追い返されるのが関の山だろうと考えておりましたので、その返事の予想外なのに、また寛容であることに驚いたものでありました。



```python:LEDを水色(#58b6d2)に
ser.write(b"T,\x58,\xb6,\xd2,; ")
```

```python:LED消灯
ser.write(b"T,\x00,\x00,\x00,; ")
```

![music.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2018473/6f223858-9dec-c5c9-ff04-afeeb7c96aa1.gif)

これで、特定の曲が流れたときに色が変わるような芸当ができますね。

[psutil](https://pypi.org/project/psutil/)でCPUやメモリの使用率を取得し、それをもとに色変更するのも楽しそうです。


##バイブレーションについて

バイブレーションを動かすためのコマンドついては聞きそびれてしまいましたが、LEDのコマンドからある程度推測出来ます。

総当りしてみたところ、`0x4d, 0xff, 0x3b, 0x00`というコマンドを送信することで、Orbital2が振動することがわかりました。

```python:バイブレーション
ser.write(b"M,\xff,; ")
```


#Blenderで活用する

Blender内蔵のPythonでも、Orbital2とシリアル通信することができます。

詳しくは、[Checkered Bugによるチュートリアル](https://youtu.be/CypV9pPTCXo)や、[しましまさんの記事](https://shima2cgstudio.hatenablog.com/)にて解説されています。

##BlenderにOrbital2を召喚する

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">BlenderにOrbital2召喚できた。 <a href="https://t.co/PJCuHgAVcn">pic.twitter.com/PJCuHgAVcn</a></p>&mdash; ْ (@kame404) <a href="https://twitter.com/kame404/status/1438108088012210178?ref_src=twsrc%5Etfw">September 15, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

このツイートは、

① 現実世界のジョイスティックのXY座標を、Blender上のEmptyのXY座標へ割り当て
② Emptyの動きに合わせて、Blender上のジョイスティックオブジェクトが傾くよう、[ドライバで紐付ける](https://docs.blender.org/manual/en/latest/animation/drivers/workflow_examples.html)

ことにより実現しています。

![driver_lt.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2018473/0617809f-cf57-32e6-b099-add4d2d08854.png)




```python:Emptyを動かす
import bpy
import serial

ser = serial.Serial('COM3','9600',timeout=0.0)
obj = bpy.data.objects["Empty"]

while True:

    SR = (ser.readline())
    SRstr = str(SR)
    if "RC4=1" in SRstr:
        obj.location = (0,0,0)
        ser.close()

    elif "JS" in SRstr:
        x = (ord(SR[3:4])-128) / 100
        y = (ord(SR[5:6])-128) / 100
        print("X",x,"Y",y)
        obj.location.x = x
        obj.location.y = y
        bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)
```





#おわりに

改めまして、GAMEJAMを企画し開催して下さったcluster様、
スポンサー賞に「田んぼのある生活」を選んで下さったデジタルハリウッド大学様、
当記事の公開を許可して下さったBRAIN MAGIC様に厚く御礼申し上げます。


<br>

*チーム「おにさわランド」の「田んぼのある生活」をどうぞよろしくお願いします！*

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">こちら、【デジタルハリウッド大学賞】を頂きました！<br>「田んぼのある生活」です。ありがとうございます！<a href="https://t.co/WYOpSfxqEU">https://t.co/WYOpSfxqEU</a><a href="https://twitter.com/hashtag/ClusterGAMEJAM?src=hash&amp;ref_src=twsrc%5Etfw">#ClusterGAMEJAM</a> <a href="https://t.co/VcagZVYyGl">https://t.co/VcagZVYyGl</a></p>&mdash; ْ (@kame404) <a href="https://twitter.com/kame404/status/1433074678814285825?ref_src=twsrc%5Etfw">September 1, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>