Joy-conをジャイロマウスのように使うテスト
先日左手が腱鞘炎になり両手の使い方を考えなければならなくなったプログラマーです。
腱鞘炎の原因を調べてみるとSEにとっては切っても切れないお方、パソコンの使いすぎが現れます。
対処法としては姿勢を正すとか、マウスやキーボードの前にクッションを置くなどがあげられます。
では別のアプローチとして、手首に負荷のかからない(かかりにくい)マウスはないものか。
と検討したところ「空中マウス」の存在に行き当たりました。
ただし親指で操作していたら結果は同じどころか悪化する可能性もあります。
そんなわけでジャイロセンサーを搭載したマウスを簡単に作れないかと思っての実験です。
検討と結果
身近にある加速度/ジャイロセンサーを搭載したものを確認したところ、以下の候補がありました。
・スマートフォン
・PS4コントローラー
・Nintendo SwitchのJoy-con
スマートフォンはBluetooth接続できるのですが、
加速度/ジャイロセンサーの値だけをPCに飛ばすには専用のプログラムかアプリが必要です。
PS4コントローラーはBluetooth接続しGamePadとして利用することはできますが、
加速度/ジャイロセンサーはPS4専用のアプリケーションでないと動作しないとのこと。
残ったのはNintendo SwitchのJoy-conだけでした。
……………。Joy-conってBluetooth接続のHIDとして使えるんですね。
して、短時間で作成した結果として
・pythonで簡単作成
・それっぽい動作は可能
問題点は
・専用ツールでないので動作がガクガク
・取得した値を調整してないのでカーソルが飛ぶ
・センサーが敏感で常にブレる
といったところ。
作成物とプログラム:使用ライブラリ
ゲームコントローラーをpythonで使う場合はpygameを使うのが有名ですが
あちらはジャイロセンサーの値を取れないので、今回はHIDデバイスとして使います。
こちらのサイトを参考にJoy-conを認識します。
Joy-ConにPythonからBluetooth接続をして6軸センサーと入力情報を取得する
送信された値を元にpyautoguiを使ってマウスを動かします。
使うライブラリの2つはpipでインストールできます。
pip install hidapi pip install pyautogui
余談
別件で利用していたためpyautoguiを使っていますが、
こちらを参考にしても動かせるとは思います。
どちらがスムーズに動かせるかは要検証。
作成物とプログラム:プログラム全文
サッと書いた後に精査していないため無駄やゴミも多いですがとりあえずのテストとして。
Joy-conはRを使用しています。
Lだとボタンの番号が変わる可能性。
import hid
import pyautogui
import sys
import time
def write_output_report(joycon_device, packet_number, command, subcommand, argument, message):
joycon_device.write(command
+ packet_number.to_bytes(1, byteorder='big')
+ b'\x00\x01\x40\x40\x00\x01\x40\x40'
+ subcommand
+ argument)
print(message)
def to_int16le_from_2bytes(hbytebe, lbytebe):
uint16le = (lbytebe << 8) | hbytebe
int16le = uint16le if uint16le < 32768 else uint16le - 65536
return int16le
def get_nbit_from_input_report(input_report, offset_byte, offset_bit, nbit):
return (input_report[offset_byte] >> offset_bit) & ((1 << nbit) - 1)
def get_accel_x(input_report):
return (to_int16le_from_2bytes(input_report[13], input_report[14]))
def get_accel_y(input_report):
return (to_int16le_from_2bytes(input_report[15], input_report[16]))
def get_accel_z(input_report):
return (to_int16le_from_2bytes(input_report[17], input_report[18]))
def get_gyro_x(input_report):
return (to_int16le_from_2bytes(input_report[19], input_report[20]))
def get_gyro_y(input_report):
return (to_int16le_from_2bytes(input_report[21], input_report[22]))
def get_gyro_z(input_report):
return (to_int16le_from_2bytes(input_report[23], input_report[24]))
def stop_system(set_device):
write_output_report(set_device, 0, b'\x01', b'\x40', b'\x00', 'Sensor_OFF')
time.sleep(0.02)
write_output_report(set_device, 1, b'\x01', b'\x03', b'\x3F', 'Change_mode')
time.sleep(0.02)
VENDOR_ID = 0x057E
L_PRODUCT_ID = 0x2006
R_PRODUCT_ID = 0x2007
joycon_device = hid.device()
joycon_device.open(VENDOR_ID, R_PRODUCT_ID)
time.sleep(0.02)
sw,sh = pyautogui.size()
move_merge = 20
mode = 1
bef_pos = {'x':0, 'y':0,'click':False,'drag':False,}
x_limit = True
#ランプ消灯
count = 0
write_output_report(joycon_device, count, b'\x01', b'\x30', count.to_bytes(1, byteorder='big'), 'stop_lamp')
time.sleep(0.02)
stop_system(joycon_device)
while True:
"""
m1=1, m2=3: XYBA
1=A
2=X
4=B
8=Y
16=SL
32=SR
m1=2, m2=4
2=+ボタン
16=Homeボタン
64=R(m2=3)
128=ZR(m2=3)
m1=3: スティック
0=左
1=左上
2=上
3=右上
4=右
5=右下
6=下
7=左下
8=ニュートラル
"""
read_data = joycon_device.read(49)
if mode == 1:
if 0 <= read_data[3] <= 7:
nx,ny = pyautogui.position()
if read_data[3] == 0:
nx = nx - move_merge
if read_data[3] == 1:
nx = nx - (move_merge / 2)
ny = ny - (move_merge / 2)
if read_data[3] == 2:
ny = ny - move_merge
if read_data[3] == 3:
nx = nx + (move_merge / 2)
ny = ny - (move_merge / 2)
if read_data[3] == 4:
nx = nx + move_merge
if read_data[3] == 5:
nx = nx + (move_merge / 2)
ny = ny + (move_merge / 2)
if read_data[3] == 6:
ny = ny + move_merge
if read_data[3] == 7:
nx = nx - (move_merge / 2)
ny = ny + (move_merge / 2)
if nx < 0:
nx = 1
if ny < 0:
ny = 1
if sw < nx:
nx = sw -1
if sh < ny:
ny = sh -1
pyautogui.moveTo(nx,ny)
if read_data[2] == 128:
if bef_pos['click'] == False:
bef_pos['click'] = True
pyautogui.click()
else:
if bef_pos['drag'] == False:
pyautogui.mouseDown()
bef_pos['drag'] = True
else:
pyautogui.mouseUp()
bef_pos['click'] = False
bef_pos['drag'] = False
if read_data[1] == 32:
mode = 2
write_output_report(joycon_device, 0, b'\x01', b'\x40', b'\x01', 'Sensor_ON')
time.sleep(0.02)
write_output_report(joycon_device, 1, b'\x01', b'\x03', b'\x30', 'Change_mode')
print('mode_change 1s')
time.sleep(1)
print('GO')
if read_data[1] == 16:
stop_system(joycon_device)
break
if mode == 2:
if read_data[3] == 16:
mode = 1
stop_system(joycon_device)
print('mode_change 0.2s')
time.sleep(0.2)
print('GO')
if read_data[3] == 32:
stop_system(joycon_device)
break
if read_data[3] == 128:
if bef_pos['click'] == False:
bef_pos['click'] = True
pyautogui.click()
else:
if bef_pos['drag'] == False:
pyautogui.mouseDown()
bef_pos['drag'] = True
else:
pyautogui.mouseUp()
bef_pos['click'] = False
bef_pos['drag'] = False
merge_x = 1300
accel_x = get_accel_x(read_data)
if accel_x < (0 - merge_x):
ny = sh - 1
elif accel_x > merge_x:
ny = 1
else:
ny = int( (1 - ((accel_x + merge_x) / (merge_x * 2)) ) * sh )
accel_y = get_accel_y(read_data)
if accel_y < (0 - accel_y):
nx = sw - 1
elif accel_y > accel_y:
nx = 1
else:
nx = int( (1 - ((accel_y + merge_x) / (merge_x * 2)) ) * sw )
pyautogui.moveTo(nx,ny)
機能
初期モード
・スティック:マウスカーソル移動
・ZR:左クリック
・SR:モード変更
・SL:終了
ジャイロモード
・ジャイロ:マウスカーソル移動
・ZR:左クリック
・SR:モード変更
・SL:終了
※SL,SRはSwitch本体と接続する箇所にあるボタン
作ってみた感想
精度を考えなければ簡単にできるものですね。
おまけ:PS4コントローラー接続用の設定と値
VENDOR_ID = 0x54c
PRODUCT_ID = 0x9cc
1,2: Lスティック 1X軸:0=左 255=右 2Y軸:0=上 255=下 3,4: Rスティック 3X軸:0=左 255=右 4Y軸:0=上 255=下 5:ボタン 0=上 2=右 4=下 6=左 8=ニュートラル 24=□ 40=✕ 72=◯ 136=△ 6:L1,R1 1=L1 2=R1 7: 16=share 32=option 64=L3 128=R3 8:L2(0~255) 9:R2(0~255)Tweet
