11import argparse
22import logging
33from time import time , ctime
4- from dataclasses import dataclass
54from collections import deque # for storing x, y time series
6- import numpy as np # for smoothing moving average
7- import ctypes # for windows mouse POINT struct
85import socket # udp networking
96import struct # binary unpacking
10- # import keyboard # keyboard shortcuts
7+ from pynput import keyboard # for hotkeys
118
12- from pynput import keyboard
13-
14-
15-
16-
17- # Collect events until released
18- # with keyboard.Listener(
19- # on_press=on_press,
20- # on_release=on_release) as listener:
21- # listener.join()
22-
23- # ...or, in a non-blocking fashion:
24-
25-
26- enabled = True
27-
28- def toggle ():
29- global enabled
30- logging .info ("\n Toggled PhilNav on/off\n " )
31- enabled = not enabled
32-
33- # https://pynput.readthedocs.io/en/latest/keyboard.html#global-hotkeys
34-
35- def for_canonical (f ):
36- return lambda k : f (listener .canonical (k ))
37-
38- hotkey = keyboard .HotKey (
39- [keyboard .Key .shift , keyboard .Key .f7 .value ],
40- toggle )
41-
42- listener = keyboard .Listener (
43- on_press = for_canonical (hotkey .press ),
44- on_release = for_canonical (hotkey .release ))
45-
46- listener .start ()
47-
48- # keyboard.add_hotkey('space', toggle)
49- # keyboard.add_hotkey('space', toggle)
50-
51- # keyboard.write('The quick brown fox jumps over the lazy dog.')
52- # keyboard.add_hotkey('space', lambda: print('space was pressed!'))
53-
54- # returned from ctypes.windll.user32.GetCursorPos
55- # simple point.x, point.y
56- class POINT (ctypes .Structure ):
57- _fields_ = [("x" , ctypes .c_long ), ("y" , ctypes .c_long )]
9+ print ("\n \n CLIENT: Starting PhilNav\n " )
5810
5911import platform
60- # Done: Windows
61- # TODO: Mac, Linux
6212match platform .system ():
6313 case "Darwin" : # macOS
6414 from mouse_mac import getCursorPos , setCursorPos
15+ case "Windows" :
16+ from mouse_win import getCursorPos , setCursorPos
17+ case "Linux" :
18+ from mouse_nix import getCursorPos , setCursorPos
19+ case _:
20+ raise RuntimeError (f"Platform { platform .system ()} not supported (not Win, Mac, or Nix)" )
6521
6622
67- print ("\n \n CLIENT: Starting PhilNav\n " )
68-
6923# parse command line arguments
7024parser = argparse .ArgumentParser ()
7125parser .add_argument (
72- "-v" , "--verbose" , action = "store_true" , help = "provide verbose logging"
26+ "-v" , "--verbose" , action = "store_true" , help = "enable verbose logging"
27+ )
28+ parser .add_argument (
29+ "-s" , "--speed" , type = int , default = 25 , help = "mouse speed, default 25"
30+ )
31+ parser .add_argument (
32+ "-S" , "--smooth" , type = int , default = 3 , help = "averages mouse movements to smooth out jittering, default 3"
7333)
7434parser .add_argument (
7535 "-H" ,
@@ -81,22 +41,40 @@ class POINT(ctypes.Structure):
8141parser .add_argument (
8242 "-p" , "--port" , type = int , default = 4245 , help = "bind to port, default 4245"
8343)
84- parser .add_argument (
85- "-s" , "--speed" , type = int , default = 25 , help = "mouse speed, default 25"
86- )
87- parser .add_argument (
88- "-S" , "--smooth" , type = int , default = 3 , help = "averages mouse movements to smooth out jittering, default 3"
89- )
44+
9045args = parser .parse_args ()
46+
9147if args .smooth < 1 :
9248 args .smooth = 1
9349
94-
9550if args .verbose :
9651 logging .getLogger ().setLevel (logging .DEBUG )
97- logging .info (" Logging verbosely\n " )
52+ logging .info ("\n Logging verbosely\n " )
53+
54+
55+ # Hotkey to pause/resume moving the mouse
56+ enabled = True
9857
58+ def toggle ():
59+ global enabled
60+ enabled = not enabled
61+ logging .info ("\n Toggled PhilNav on/off\n " )
62+
63+ # Shift-F7 hard-coded for now
64+ hotkey_toggle = keyboard .HotKey (
65+ [keyboard .Key .shift , keyboard .Key .f7 .value ],
66+ toggle )
67+
68+ # https://pynput.readthedocs.io/en/latest/keyboard.html#global-hotkeys
69+ def for_canonical (f ):
70+ global listener
71+ return lambda k : f (listener .canonical (k ))
72+
73+ listener = keyboard .Listener (
74+ on_press = for_canonical (hotkey_toggle .press ),
75+ on_release = for_canonical (hotkey_toggle .release ))
9976
77+ listener .start ()
10078
10179
10280# initialize networking
@@ -106,41 +84,37 @@ class POINT(ctypes.Structure):
10684sock .settimeout (1 )
10785sock .bind ((args .host , args .port )) # Register our socket
10886
109- # How to get local IP address?
110- # Doesn't work for me: socket.gethostbyname(socket.gethostname())
111- # For now, just manually go to settings -> Wi-fi and look up your *local* IP
112- # address. It will be something like 192.x.x.x or 10.x.x.x This is not your
113- # public Internet address. This is your local area network address. On Windows,
114- # make sure it's set to a private network to allow discovery. You'd think public
115- # would allow discovery, but when you are *in public* - like at a coffee shop -
116- # you don't want strangers to access your PC.
87+ # How to get local IP address in python?
11788text_listening = (
11889 f"Listening on { sock .getsockname ()} for mouse data from Raspberry Pi server..."
11990)
12091print (ctime () + " - " + text_listening )
121- print ("\n Press Ctrl-C to exit\n " )
92+ print ("\n Press Ctrl-C to exit, press Shift-F7 to pause/resume \n " )
12293
12394
12495# Stats for debugging & performance. The goal is 60 frames per second, or
12596# 16.67ms per frame. That leads to a very smooth mouse cursor. (SmartNav was 100
12697# fps) A standard non-gaming monitor is also 60Hz. (TV is 30 fps)
127- @dataclass
128- class PhilNav :
129- time_start = time ()
130- time_debug = time ()
131- debug_num = 0
132- msg_time_start = time ()
133- msg_time_total = 0
134- msg_num = 0
135- x_q = deque ([], args .smooth )
136- y_q = deque ([], args .smooth )
137- x_q_long = deque ([], args .smooth * 3 )
138- y_q_long = deque ([], args .smooth * 3 )
139-
140-
98+ phil = {}
99+ phil .time_start = time ()
100+ phil .time_debug = time ()
101+ phil .debug_num = 0
102+
103+ # mouse (x_diff, y_diff) smoothing running averages
104+ smooth_long = args .smooth * 3 + 1
105+ phil .x_q = deque ([], args .smooth )
106+ phil .x_q_smooth = 0
107+ phil .x_q_long = deque ([], smooth_long )
108+ phil .x_q_long_smooth = 0
109+ phil .y_q = deque ([], args .smooth )
110+ phil .y_q_smooth = 0
111+ phil .y_q_long = deque ([], smooth_long )
112+ phil .y_q_long_smooth = 0
113+
114+
115+ # simple moving average to reduce mouse jitter
141116def smooth (q ):
142- sum = np .sum (q )
143- avg = sum / len (q )
117+ avg = sum (q ) / len (q )
144118 return avg
145119
146120
@@ -158,59 +132,62 @@ def smooth(q):
158132 # https://github.com/opentrack/opentrack/issues/747
159133 data , addr = sock .recvfrom (48 )
160134 except TimeoutError :
161- if int (time () - PhilNav .time_start ) % 5 == 0 :
162- logging .info (f" { ctime ()} - { text_listening } " )
135+ if int (time () - phil .time_start ) % 5 == 0 :
136+ logging .info (f"{ ctime ()} - { text_listening } " )
163137 continue
164138 else :
165- # measure time
166- PhilNav .msg_time_start = time ()
167- PhilNav .msg_num += 1
139+ if not enabled :
140+ continue
168141
169142 # Using OpenTrack protocol, but PhilNav uses:
170- # x_diff, y_diff, n/a, n/a, n/a, camera capture time
171- x , y , z , pitch , yaw , roll = struct .unpack ("dddddd" , data )
172-
173- # Simple moving average to smooth out jitters
174- PhilNav .x_q .append (x )
175- PhilNav .y_q .append (y )
176- PhilNav .x_q_long .append (x )
177- PhilNav .y_q_long .append (y )
178- if x ** 2 + y ** 2 < 0.2 :
179- x_smooth = smooth (PhilNav .x_q_long )
180- y_smooth = smooth (PhilNav .y_q_long )
181- elif x ** 2 + y ** 2 < 0.5 :
182- x_smooth = smooth (PhilNav .x_q )
183- y_smooth = smooth (PhilNav .y_q )
184- else :
185- x_smooth = x
186- y_smooth = y
187-
188- # The Magic Happens Now! eg. move mouse cursor =P
143+ # x_diff, y_diff, n/a, n/a, camera capture time, OpenCV processing time
144+ x_diff , y_diff , a , b , time_cam , ms_opencv = struct .unpack ("dddddd" , data )
145+
146+ # store recent mouse movements
147+ phil .x_q .append (x_diff )
148+ phil .y_q .append (y_diff )
149+ phil .x_q_long .append (x_diff )
150+ phil .y_q_long .append (y_diff )
151+
152+ # Perform more smoothing the *slower* the mouse is moving.
153+ # A slow-moving cursor means the user is trying to precisely
154+ # point at something.
155+ if x_diff ** 2 + y_diff ** 2 < 0.2 : # more smoothing
156+ x_smooth = smooth (phil .x_q_long )
157+ y_smooth = smooth (phil .y_q_long )
158+ elif x_diff ** 2 + y_diff ** 2 < 0.5 : # less smoothing
159+ x_smooth = smooth (phil .x_q )
160+ y_smooth = smooth (phil .y_q )
161+ else : # moving fast, no smoothing
162+ x_smooth = x_diff
163+ y_smooth = y_diff
164+
165+ # The Magic Happens Now!
189166 x_cur , y_cur = getCursorPos ()
190- # get current mouse position by reference (C++ thing)
191167 # I'm moving the Y axis slightly faster because looking left and right
192168 # is easier than nodding up and down. Also, monitors are wider than they
193169 # are tall.
194170 x_new = round (x_cur + x_smooth * args .speed )
195171 y_new = round (y_cur + y_smooth * args .speed * 1.25 )
196- if enabled :
197- setCursorPos (x_new , y_new ) # move mouse cursor
172+ setCursorPos (x_new , y_new ) # move mouse cursor
198173
199174 # I'm trying to measure the total time from capturing the frame on the
200175 # camera to moving the mouse cursor on my PC. This isn't super accurate.
201176 # It's sometimes negative (TIME TRAVEL!!!). The clock difference between
202177 # the Raspberry Pi and my PC seems to be around 10-20ms?
203- time_diff_ms = int ((time () - roll ) * 1000 )
178+ now = time ()
179+ now_str = ctime ()
180+ ms_time_diff = int ((now - time_cam ) * 1000 )
204181
205182 # it's 60 FPS, so only debug once per second
206- if time () - PhilNav .time_debug > 1 :
207- PhilNav .time_debug = time ()
208- PhilNav .debug_num += 1
183+ if now - phil .time_debug > 1 :
184+ phil .time_debug = now
185+ phil .debug_num += 1
209186 # display legend every 5 seconds
210- if PhilNav .debug_num % 5 == 1 :
187+ if phil .debug_num % 5 == 1 :
211188 logging .info (
212- f" { ctime () } - Received: ({ 'x_diff' :>8} ,{ 'y_diff' :>8} ,{ 'n/a' :>8} ,{ 'n/a' :>8} ,{ 'loc ns ' :>8} ,{ 'net ms ' :>8} ) "
189+ f"{ now_str } - Received: ({ 'x_diff' :>8} ,{ 'y_diff' :>8} ) ,{ 'n/a' :>8} ,{ 'n/a' :>8} ,{ 'time ms ' :>8} ,{ 'time cv ' :>8} "
213190 )
214191 logging .info (
215- f" { ctime () } - Received: ({ x :> 8.2f} ,{ y :> 8.2f} , { z :> 8.2f} ,{ pitch :> 8.2f} ,{ ( time () - PhilNav . msg_time_start ) * 1000 :> 8.2f } ,{ time_diff_ms :> 8 } ) "
192+ f"{ now_str } - Received: ({ x_diff :> 8.2f} ,{ y_diff :> 8.2f} ), { a :> 8.2f} ,{ b :> 8.2f} ,{ ms_time_diff :> 8} ,{ ms_opencv :>8 } "
216193 )
0 commit comments