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)

最新ブログ一覧