SpaceMouse - key mapping python code

Created this code to allow me to use my SpaceMouse Enterprise with Nomad on an Android simulator (Waydroid) in linux (xubuntu 24.10)

Fairly simple as it maps movement / buttons to keyboard keys.
Python is not something I do much of (pascal for me), so improvements to be made.

Let me know if any questions / suggestions

import pyspacemouse
import time
import subprocess
import sys

β€œβ€"
Based on code supplied by:
Jakub Andrysek :github.com/JakubAndrysek/pySpaceMouse
Brandon Lopez :github.com/bglopez/python-easyhid

thank you for blocks on which I build :slight_smile:

simple program to capture analog readings from SpaceMouse (and friends) and convert
positional data to keystrokes
knob MOVE left β†’ KP_LEFT
knob TILT left β†’ CTRL + KP_LEFT
knob LIFT β†’ PAGEUP

buttons on device mapped to top row ok qwerty keypad
btn1 β†’ CTRL+1 / btn10 β†’ CTRL+0 / btn11 β†’ CTRLΒ±
btn13 β†’ SHIFT+CTRL+1 / btn23 β†’ SHIFT+CTRLΒ±

Install (debian system example):
sudo apt-get install libhidapi-dev pipx
pipx install pyspacemouse
git clone GitHub - ahtn/python-easyhid: A simple interface to the HIDAPI library.
cd python-easyhid/
sudo python3 ./setup.py install

Usage:
start process:
sudo spacemouse-nomadsculpt.py &

in NomadSculpt:
place key binding in edit mode, move mouse knob or press enter key sequence on keyboard

Notes:
needs to by run as superuser (access of system device) - fix this if possible
needs refactoring to use events/callbacks rather than sleep
STATE_HI/LO can be altered to change sensitivity of key detection
need to figure how to call xdotool once for multiple keys - Ctrl+Shift+1
cleanup at exit needs β€˜cleaning up’ - fairly basic assumptions made

β€œβ€"

switch activate/deactivate levels

STATE_HI = 0.7 # 0 β†’ 1 - higher means less sensitive
STATE_LO = -0.7 # -1 β†’ 0 - lower means less sensitive

keyboard keycodes

KEY_ALT_R = β€œ108”
KEY_CTRL_R = β€œ105”
KEY_SHIFT_R = β€œ62”

KEY_MOVE_UP = β€œ111”
KEY_MOVE_DN = β€œ116”
KEY_MOVE_LT = β€œ113”
KEY_MOVE_RT = β€œ114”

KEY_PAGE_UP = β€œ112”
KEY_PAGE_DN = β€œ117”

keypress active - only allowing 1 action at a time

so cant move up and roll up at same time

key_act = 0

tracking mouse axis states

mouse_move_lt = 0
mouse_move_rt = 0
mouse_move_up = 0
mouse_move_dn = 0

mouse_tilt_lt = 0
mouse_tilt_rt = 0
mouse_tilt_up = 0
mouse_tilt_dn = 0

mouse_press_dn = 0
mouse_lift_up = 0

-----------------------------------------------------------------------------------

mouse_lt = keytest(mouse_lt, STATE_LO, KEY_LT)

def key_test( iCurrState, iAxisPos, iCutPt, sKeyMod, sKeyOut ):

global key_act

# axis left / down
if iCutPt == STATE_LO:

	if key_act == 0 and iCurrState == 0 and iAxisPos <= iCutPt:
		key_act = 1
		iCurrState = 1
		if sKeyMod == KEY_CTRL_R: subprocess.run(["xdotool", "keydown", KEY_CTRL_R])
		subprocess.run(["xdotool", "keydown", sKeyOut])

	if iCurrState == 1 and iAxisPos >= iCutPt:
		key_act = 0
		iCurrState = 0
		subprocess.run(["xdotool", "keyup",   sKeyOut])
		if sKeyMod == KEY_CTRL_R: subprocess.run(["xdotool", "keyup", KEY_CTRL_R])

# axis right / up
else:

	if key_act == 0 and iCurrState == 0 and iAxisPos >= iCutPt:
		key_act = 1
		iCurrState = 1
		if sKeyMod == KEY_CTRL_R: subprocess.run(["xdotool", "keydown", KEY_CTRL_R])
		subprocess.run(["xdotool", "keydown", sKeyOut])

	if iCurrState == 1 and iAxisPos <= iCutPt:
		key_act = 0
		iCurrState = 0
		subprocess.run(["xdotool", "keyup",   sKeyOut])
		if sKeyMod == KEY_CTRL_R: subprocess.run(["xdotool", "keyup", KEY_CTRL_R])

return( iCurrState )

-----------------------------------------------------------------------------------

Main - open connection to device

success = pyspacemouse.open()

if success:

# allow cleanup at exit
try:

	while 1:

		# allow downtime for cpu
		# better to use callbacks, maybe later :)
		time.sleep(0.01)

		# get all axis / button states
		state = pyspacemouse.read()

		# print("Btns", state.buttons)
		# print("X:", f'{state.x:.3f}', "Y:", f'{state.y:.3f}', "Z:", f'{state.z:.3f}', "Pitch:", f'{state.pitch:.3f}', "Roll:", f'{state.roll:.3f}', "Yaw:", f'{state.yaw:.3f}')

		mouse_move_lt = key_test(mouse_move_lt, state.x, STATE_LO, "", KEY_MOVE_LT)
		mouse_move_rt = key_test(mouse_move_rt, state.x, STATE_HI, "", KEY_MOVE_RT)

		mouse_move_up = key_test(mouse_move_up, state.y, STATE_HI, "", KEY_MOVE_UP)
		mouse_move_dn = key_test(mouse_move_dn, state.y, STATE_LO, "", KEY_MOVE_DN)

		mouse_tilt_lt = key_test(mouse_tilt_lt, state.roll,  STATE_LO, KEY_CTRL_R, KEY_MOVE_LT)
		mouse_tilt_rt = key_test(mouse_tilt_rt, state.roll,  STATE_HI, KEY_CTRL_R, KEY_MOVE_RT)
		mouse_tilt_up = key_test(mouse_tilt_up, state.pitch, STATE_HI, KEY_CTRL_R, KEY_MOVE_UP)
		mouse_tilt_dn = key_test(mouse_tilt_dn, state.pitch, STATE_LO, KEY_CTRL_R, KEY_MOVE_DN)

		mouse_press_dn = key_test(mouse_press_dn, state.z, STATE_LO, "", KEY_PAGE_DN)
		mouse_lift_up  = key_test(mouse_lift_up,  state.z, STATE_HI, "", KEY_PAGE_UP)

		# for each button in array / on device (hopefully)
		# ndx is zero based, button cnt 1 based
		sKey = "0";

		for ndx, x in enumerate(state.buttons):

			# reset to first key in line at overflow - 1:10 twice
			if ndx == 10 or ndx == 20:
				sKey = "0"

			# inc key about to be active
			sKey = chr( ord(sKey)+1 )

			# specific assignment for 10th key
			if ndx == 9 or ndx == 19:
				sKey = "0"

			# button active
			if x:

				# print("key2:", sKey, "ndx:", ndx)

				subprocess.run(["xdotool", "keydown", KEY_CTRL_R])
				if ndx > 9: subprocess.run(["xdotool", "keydown", KEY_SHIFT_R])

				subprocess.run(["xdotool", "key", sKey])

				if ndx > 9: subprocess.run(["xdotool", "keyup", KEY_SHIFT_R])
				subprocess.run(["xdotool", "keyup", KEY_CTRL_R])


# capture exit calls
except KeyboardInterrupt:

	print("\nCleaning up and exiting...")

	# key active, ensure key release
	if key_act:

		if mouse_move_lt or mouse_tilt_lt:	subprocess.run(["xdotool", "keyup", KEY_MOVE_LT])
		if mouse_move_rt or mouse_tilt_lt:	subprocess.run(["xdotool", "keyup", KEY_MOVE_RT])
		if mouse_move_up or mouse_tilt_up:	subprocess.run(["xdotool", "keyup", KEY_MOVE_UP])
		if mouse_move_dn or mouse_tilt_dn:	subprocess.run(["xdotool", "keyup", KEY_MOVE_DN])

		if mouse_press_dn: 					subprocess.run(["xdotool", "keyup", KEY_PAGE_UP])
		if mouse_lift_up: 					subprocess.run(["xdotool", "keyup", KEY_PAGE_DN])

		# assume these need clearing, its easier
		subprocess.run(["xdotool", "keyup", KEY_ALT_R])
		subprocess.run(["xdotool", "keyup", KEY_CTRL_R])
		subprocess.run(["xdotool", "keyup", KEY_SHIFT_R])

	sys.exit(0)  # Exit gracefully with exit status 0

else:
print(β€œ\nNo connection to SpaceMouse found”)
sys.exit(0)

1 Like