着任したばかりですまないが
君が搭乗する機体は操縦桿が折れてしまった。幸い、toio core cubeで姿勢が取れるようになったらしい。そこで、君にはtoio core cubeでACECOMBAT™7を操縦してもらう。君の確かな腕が必要だ。頼んだぞ。
トイオ、お前はRaspberry Py zero Wとエレメントを組め
えー、本来、ACECOMBAT™7(戦闘機を操るフライトシューティングゲームです)は下図のようなフライトスティックやゲームパッドで遊ぶものですが、今回、toio core cubeの姿勢情報やボタン操作などを使って操縦してみます。なお、ACECOMBAT™7はSteam版(PC版)を使います。
toio core cubeの姿勢情報やボタン操作は Raspberry Pi Zero WのBLEで読み出します。そしてその情報をもとに、USB HIDデバイス(ゲームパッドやキーボードやマウスなど)の操作情報に変換し、PCに送り込んであげると、PC側ではあたかもUSBゲームパッドやUSBキーボードやマウスから操作されたかのようにふるまいます。
ハードウェア
写真中、PCとUSBケーブルが2本つながっているのは、一つはUSB HIDデバイス(キーボードとマウス)としての接続、もう一つはFTDIを介してRaspberry Pi Zero Wのシリアルポートにつないでいます。これはWindows PCからシリアルポートでログインしてRaspberry Pi Zero Wを操作するためです。(Raspberry Pi Zero WをローカルネットにWiFi接続しているなら、SSHでログインすればいいのでこの接続は不要)
ソフトウェア
Raspberry PiのUSBポートは通常は周辺機器をつなぐホストモードで動作しますが、Raspberry Pi Zeroのmicro BおよびRaspberry Pi 4BのType-CポートはUSB OTGの機能をもっており、ターゲットモード、つまりRaspberry Pi自身がUSB機器としてふるまうように設定することができます。
ターゲットモードにするにはUSB Gadget Driverを使うのですが、今回はその設定に https://github.com/roy-n-roy/raspi_web_console のシェルスクリプト init_usb.sh をそのまま使わせていただきました。このシェルスクリプトを(root権限で)Raspberry Pi Zero Wで動かすと以下のデバイスファイルが生成されます。ここにレポートデータというバイナリデータを書き込むと、キーボードやマウスを操作したのと同じUSB HIDパケットがUSBポートから送出されます。Raspberry Pi ZeroのUSBポートをPCにつないでおけば、PCはキーボードやマウスを操作されたのと同じ挙動をします。
/dev/hidg0 にレポートデータを書き込むとキーボード
/dev/hidg1 にレポートデータを書き込むとマウス/デジタイザ
それぞれのデバイスファイルに書き込むレポートデータの仕様は以下の通りです。
キーボード操作のレポートデータ 合計8バイト
| modifier key | scancode 1 | scancode 2 | scancode 3 | scancode 4 | scancode 5 |scancode 6 |
| ---- | ---- |---- | ---- | ---- | ---- | ---- | ---- |
| 2bytes | 1byte | 1byte | 1byte | 1byte | 1byte | 1byte |
マウス操作のレポートデータ 合計10バイト
| 0x01 |ボタン5bit|X signed 16bit|Y signed 16bit| wheel signed 16bit | AC pan signed 16bit |
| ---- | ---- |---- | ---- | ---- | ---- | ---- | ---- |
|1byte|1byte|2bytes|2bytes|2bytes|2bytes|
pythonライブラリtomotoioを使って、toio core cubeの姿勢情報やボタン操作、マットの座標などを読み取り、それをキーボード、マウスの操作情報に相当するレポートデータに変換し、/dev/hidg[01]に書き込むpythonスクリプトを書いて動かします。
本番だ トイオ、お前の腕を見せてもらうぞ
操作方法
本来のSteam版(PC版) ACECOMBAT™7のキーアサインはこんな感じです。
これに対してtoio core cubeの操作を操縦桿、スロットル、兵装切り替え、発射ボタンなどにわりあてるわけですが、いろいろ試行錯誤した結果、3つのtoio core cubeを使うことにしました。
- スロットルキューブ スロットルとヨー(垂直尾翼) 簡易プレイマットの上で動かします。
- 兵装選択キューブ 兵装切り替え キューブを置いた簡易カードのマスの位置で切り替えます。
- 操縦桿キューブ 天地をひっくり返して持ち、キューブの傾きで操縦桿。ボタンは発射ボタン。
「toio™コア キューブ」(単体)付属の簡易プレイマットを敷いた中央に、「toio™コア キューブ」(単体)付属の簡易カード(折りたたんで一部のみ)を置き、その上に兵装選択キューブを置きます。その左側にスロットルキューブを置きます。操縦桿キューブは右手で持って使うので右側に置いておきます。
これ以外の操作(チャフ散布、レーダーレンジ切り替え、視点切り替えなど)もあったりするのですが、必要最小限に絞りました。
今回作ったスクリプトは以下の通りです。動かすには、まずtomotoioライブラリのscan-cubes.shで、3つのtoio core cubeのBLE UUIDを採取しtoio-cubes.txtに保存します。(toio core cubeの割り当ては、順番に、操縦桿、スロットル、兵装選択になります。)
そしてこのスクリプトをtomotoioライブラリのexamples/ディレクトリに置いてpython3で動かします。(examples/utils.pyを使うので)
また、デバイスファイル/dev/hidg[01]にアクセスする都合上、root権限で動かす必要があります。
pythonスクリプトの内容はここをクリック(約200行)
import logging as log
from datetime import datetime
from functools import partial
from time import sleep
from utils import createCubes, releaseCubes, Cube
from utils import MagneticSenseType, PostureType, NotifyType
from utils import TiltEuler
from tomotoio.data import Light, Note, ToioIDType
def write_report_mouse(report):
with open('/dev/hidg1', 'rb+') as fd:
fd.write(report)
def write_report_keyboard(report):
with open('/dev/hidg0', 'rb+') as fd:
fd.write(report)
# print(report.hex())
def make_report_mouse(btn1, btn2, btn3, btn4, btn5, x, y, wheel, pan) -> bytes:
btnbit = 0x00
if(btn1 == True):
btnbit = btnbit | 0x01
if(btn2 == True):
btnbit = btnbit | 0x02
if(btn3 == True):
btnbit = btnbit | 0x04
if(btn4 == True):
btnbit = btnbit | 0x08
if(btn5 == True):
btnbit = btnbit | 0x10
return bytes([1, btnbit]) + x.to_bytes(2, byteorder='little', signed=True) + y.to_bytes(2, byteorder='little', signed=True) + wheel.to_bytes(2, byteorder='little', signed=True) + pan.to_bytes(2, byteorder='little', signed=True)
def clip2bytes(i:int) -> int:
if(i < -32767):
i = -32767
elif (i > 32767):
i = 32767
return int(i)
def make_report_keyboard(modifier, scancodes) -> bytes:
return bytes([modifier, 0]) + scancodes
cubes = createCubes()
roll = 0
pitch = 0
yaw = 0
cube_btn0 = False
cube_btn1 = False
cube_btn2 = False
THROTTLE_CENTER_Y = 270
THROTTLE_CENTER_ANGLE = 270
throttle_y = THROTTLE_CENTER_Y
throttle_angle = THROTTLE_CENTER_ANGLE
vulcan = False
sp_weapon = False
prev_sp_weapon = False
try:
def listener_motion(cube: Cube, notificationType: str, e):
# log.debug("%s, %s, %s, %s", datetime.now().isoformat(), cube.name, notificationType, e)
global roll, pitch, yaw
if(type(e) is TiltEuler):
roll = e.roll
pitch = e.pitch
yaw = e.yaw
# log.debug("%s, %s, %s, %s", datetime.now().isoformat(), roll, pitch, yaw)
def button_listener(cube: Cube, notificationType: str, e):
global cube_btn0, cube_btn1, cube_btn2
# log.debug("%s %s", notificationType, e)
if(notificationType == 'Button1'):
cube_btn1 = e
if(cube_btn1 == True):
cubes[1].setLight(0, 255, 0, 0)
else:
cubes[1].setLightOff()
elif(notificationType == 'Button2'):
cube_btn2 = e
else:
cube_btn0 = e
if(cube_btn0 == True):
cubes[0].setLight(255, 0, 0, 0)
else:
cubes[0].setLightOff()
def toioid_listener(cube: Cube, notificationType: str, e):
global throttle_y, throttle_angle, vulcan, sp_weapon
# log.debug("%s %s", notificationType, e)
if(notificationType == 'Throttle'):
if(e.type == ToioIDType.POSITION):
throttle_y = e.y
throttle_angle = e.angle
elif(notificationType == 'WeaponSelect'):
if(e.type == ToioIDType.STANDARD):
if(e.value == 3670366):
vulcan = False
sp_weapon = False
cubes[2].setLight(0,0, 255)
elif(e.value == 3670305):
vulcan = False
sp_weapon = True
cubes[2].setLight(0,255,0)
elif(e.value == 3670335):
vulcan = True
cubes[2].setLight(255,0,0)
cubes[0].motion.addListener(partial(listener_motion, cubes[0], 'motiontilt'))
cubes[0].motion.enableNotification()
cubes[0].setConfigHighPrecisionTiltSensor(PostureType.EULER, 0.1, NotifyType.PERIODIC)
cubes[0].button.addListener(partial(button_listener, cubes[0], 'Button0'))
cubes[0].button.enableNotification()
cubes[1].button.addListener(partial(button_listener, cubes[1], 'Button1'))
cubes[1].button.enableNotification()
cubes[1].toioID.addListener(partial(toioid_listener, cubes[1], 'Throttle'))
cubes[1].toioID.enableNotification()
cubes[2].button.addListener(partial(button_listener, cubes[2], 'Button2'))
cubes[2].button.enableNotification()
cubes[2].toioID.addListener(partial(toioid_listener, cubes[2], 'WeaponSelect'))
cubes[2].toioID.enableNotification()
prev_yaw = yaw
while True:
Ys = pitch * 32768 / 90
if(roll < 0) :
Xs = roll + 180
else:
Xs = roll - 180
Xs = Xs * 32768 / 90
Xs = -0.003 * Xs
Ys = -0.01 * Ys
if(abs(Xs) < 15) :
Xs = 0
if(abs(Ys) < 15) :
Ys = 0
# log.debug("Xs %s Ys %s cube_btn0 %s cube_btn1 %s",
# Xs, Ys, cube_btn0, cube_btn1)
# log.debug(" cube_btn0 %s cube_btn1 %s cube_btn2 %s", cube_btn0, cube_btn1, cube_btn2)
# log.debug(" vulcan %s sp_weapon %s ", vulcan ,sp_weapon)
if(vulcan):
mouse_btn1 = cube_btn0
mouse_btn2 = False
else:
mouse_btn1 = False
mouse_btn2 = cube_btn0
if(sp_weapon != prev_sp_weapon):
mouse_btn3 = True
prev_sp_weapon = sp_weapon
# log.debug("change")
else:
mouse_btn3 = False
# log.debug("mouse_btn %s %s %s", mouse_btn1, mouse_btn2, mouse_btn3)
report_mouse = make_report_mouse(mouse_btn1, mouse_btn2, 0, 0, 0, clip2bytes(Xs), clip2bytes(Ys), 0, 0)
write_report_mouse(report_mouse)
throttle = THROTTLE_CENTER_Y - throttle_y
plane_yaw = throttle_angle - THROTTLE_CENTER_ANGLE
# log.debug(" throttle %s plane_yaw %s ", throttle ,plane_yaw)
throttle_key = 0
yaw_key = 0
if(abs(throttle) > 10):
if(throttle > 0):
throttle_key = 26 # w
cubes[1].setLight(255,0,0)
else:
throttle_key = 22 # s
cubes[1].setLight(0,0,255)
else:
cubes[1].setLightOff()
if(abs(plane_yaw) > 8):
if(plane_yaw > 0) :
yaw_key = 8 # e
else:
yaw_key = 20 # q
scancodes = bytes()
if(throttle_key != 0):
scancodes = scancodes + bytes([throttle_key])
if(yaw_key != 0):
scancodes = scancodes + bytes([yaw_key])
if(cube_btn2 == True):
scancodes = scancodes + bytes([43]) # TAB change target
if(cube_btn1 == True):
scancodes = scancodes + bytes([3]) # 2 high G turn
if(mouse_btn3 == True): # change sp_weapon
scancodes = scancodes + bytes([6]) # c key
for i in range(6 - len(scancodes)):
scancodes = scancodes + bytes([0])
report_keyboard = make_report_keyboard(0, scancodes)
write_report_keyboard(report_keyboard)
#print(scancodes.hex())
sleep(0.02)
finally:
releaseCubes(cubes)
トイオ お前の腕なら十分やれる
実際の操作の様子
というわけで無事、任務を終えてRTBできました。(動画ではカットしていますが、ちゃんとMission 01をクリアして帰投しています。)
toio core cubeでも飛べます。
編隊長、トイオは使えますよ
まあ、toio core cubeの傾きをマウス操作とのチューニングがまだまだいまいちでMission 01の難易度Normalをクリアするのが精一杯だったりしますが。
(注) 章のタイトルはAceCombat™️7で出てくるセリフからの引用ですので、なんだかわかんなかった人はすいません。