Skip to content

Instantly share code, notes, and snippets.

@arpruss
Last active January 3, 2020 03:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save arpruss/f38a838df7a48973beab59dd26ec7daa to your computer and use it in GitHub Desktop.
Save arpruss/f38a838df7a48973beab59dd26ec7daa to your computer and use it in GitHub Desktop.
Saitek x45 mouse emulation
import sys
import time
import win32gui, win32con, win32api, win32file, win32event, win32gui_struct, winnt
import threading
from pywinusb import hid
RIGHT_CLICK = 1<<(5-1)
LEFT_CLICK = 1<<(6-1)
ACTIVE = 1<<(11-1)
UP = 1<<(23-1)
RIGHT = 1<<(24-1)
DOWN = 1<<(25-1)
LEFT = 1<<(26-1)
GUID_DEVINTERFACE_USB_DEVICE = "{A5DCBF10-6530-11D2-901F-00C04FB951ED}"
TICK = 0.025
deviceThread = None
movementThread = None
prevClick = False
prevRClick = False
dx = 0
dy = 0
speed = 0
BASE_SPEED = 0.08
def movement():
global movementThread
speed = BASE_SPEED
startTime = time.time()
prevTime = startTime-TICK
errx = 0.
erry = 0.
while movementThread is not None:
height = win32api.GetSystemMetrics(1)
t = time.time()
dt = t-prevTime
prevTime = t
movex = dx*speed*height*dt+errx
movey = dy*speed*height*dt+erry
imovex = int(round(movex))
imovey = int(round(movey))
errx = movex-imovex
erry = movey-imovey
win32api.mouse_event(win32con.MOUSEEVENTF_MOVE, imovex, imovey)
elapsed = t-startTime
speed = BASE_SPEED * (1 + 4.5*min(max(elapsed-0.25,0), 12))
time.sleep(TICK)
def handler(data):
global prevClick,prevRClick,dx,dy,movementThread
if len(data) == 12 and data[0] == 0:
buttons = data[8] | (data[9]<<8) | (data[10]<<16) | (data[11]<<24)
if buttons & ACTIVE == 0:
dx = 0
dy = 0
click = False
rclick = False
else:
click = buttons & LEFT_CLICK != 0
rclick = buttons & RIGHT_CLICK != 0
if buttons & LEFT:
dx = -1
elif buttons & RIGHT:
dx = 1
else:
dx = 0
if buttons & UP:
dy = -1
elif buttons & DOWN:
dy = 1
else:
dy = 0
if dx or dy:
if movementThread is None:
movementThread = threading.Thread(target = movement)
movementThread.daemon = True
movementThread.start()
elif movementThread is not None:
movementThread = None
if click != prevClick or rclick != prevRClick:
m = (2 if click else 4) | (8 if rclick else 16)
win32api.mouse_event(m, 0, 0, 0, 0)
prevClick = click
prevRClick = rclick
def findDevice():
try:
return hid.HidDeviceFilter(vendor_id = 0x06A3, product_id = 0x053C).get_devices()[0]
except:
return None
def identifyDevice(info):
return 'VID_06A3&PID_053C' in info.name
def saitekMouse():
global deviceThread
while deviceThread is not None:
time.sleep(1)
try:
device = findDevice()
if not device:
continue
device.open()
except:
print("Problem finding")
continue
print(device)
device.set_raw_data_handler(handler)
while deviceThread is not None and device.is_plugged():
try:
time.sleep(1)
except:
pass
print("bye")
device.close()
deviceThread = None
def killThreads():
global prevClick, prevRClick, dx, dy, deviceThread, movementThread, speed
# TODO: watch out for race conditions
deviceThread = None
prevClick = False
prevRClick = False
dx = 0
dy = 0
speed = 0
movementThread = None
def OnDeviceChange(hwnd, msg, wp, lp):
global deviceThread
try:
info = win32gui_struct.UnpackDEV_BROADCAST(lp)
except:
return False
# if wp==win32con.DBT_DEVICEREMOVECOMPLETE:
# print("remove",str(info))
# if identifyDevice(info):
# print("no Saitek x45")
# deviceThread = None
if wp==win32con.DBT_DEVICEARRIVAL:
print("arrive",str(info))
if identifyDevice(info):
print("Saitek x45")
if not deviceThread:
deviceThread = threading.Thread(target = saitekMouse)
deviceThread.daemon = True
deviceThread.start()
return True
wc = win32gui.WNDCLASS()
wc.lpszClassName = 'insertionnotify'
wc.lpfnWndProc={win32con.WM_DEVICECHANGE:OnDeviceChange}
win32gui.RegisterClass(wc)
hwnd = win32gui.CreateWindow(wc.lpszClassName,"insertionnotify",0,0,0,0,0,win32con.HWND_MESSAGE,None,None,None)
filter = win32gui_struct.PackDEV_BROADCAST_DEVICEINTERFACE(GUID_DEVINTERFACE_USB_DEVICE)
hdev = win32gui.RegisterDeviceNotification(hwnd, filter, win32con.DEVICE_NOTIFY_WINDOW_HANDLE)
def exitHandler(a,b=None):
sys.exit(0)
win32api.SetConsoleCtrlHandler(exitHandler, True)
print("Press ctrl+break to stop...")
if findDevice():
deviceThread = threading.Thread(target = saitekMouse)
deviceThread.daemon = True
deviceThread.start()
win32gui.PumpMessages()
while True:
win32gui.PumpWaitingMessages()
time.sleep(1)
win32gui.UnregisterDeviceNotification(hdev)
win32gui.DestroyWindow(hwnd)
win32gui.UnregisterClass(wc.lpszClassName, None)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment