Repository navigation
Expand file tree
/
Copy pathSpaceMouseAPI.py
More file actions
439 lines (371 loc) · 15.5 KB
/
Copy pathSpaceMouseAPI.py
File metadata and controls
439 lines (371 loc) · 15.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
# Pyhton class to interface the space mouse with enabled ProgMode
import serial
import time
import logging
from enum import Enum
# the following two enums shall be equivalent to: spacemouse-keys/parameterMenu.h
class ProgmodeError(Enum):
PE_OK = 10000
PE_INVALID_PARAM = 10001
PE_INVALID_VALUE = 10002
PE_VALUE_FAULT = 10003
PE_CMD_FAULT = 10004
class ParamType(Enum):
BOOL = 1
INT = 2
FLOAT = 3
class SpaceMouseAPI:
def __init__(self):
"""Initialize the SpaceMouseAPI instance."""
self.ser = None
self.magicNumber = None
self.numberParameters = None
self.actParamNo = None
def connect(self, port: str = None, baudrate: int = 115200, timeout: float = 1.0):
"""
Connect to the SpaceMouse device.
If a port is specified, use it directly.
If not, call the `search()` function (to be implemented later).
"""
if port is None:
port = self.search() # Placeholder for your own implementation
try:
self.ser = serial.Serial(port, baudrate=baudrate, timeout=timeout)
time.sleep(0.1) # Give the serial interface a short time to initialize
logging.info(f"Connected to SpaceMouse on port {port}")
self.quitAndFlush()
except serial.SerialException as e:
logging.warning(f"Failed to connect to SpaceMouse on port {port}: {e}")
self.ser = None
def close(self):
"""Close the serial connection if open."""
if self.ser and self.ser.is_open:
self.ser.close()
logging.info("Serial connection closed.")
else:
logging.info("No open serial connection to close.")
def quitAndFlush(self):
# send three q's to leave all menus
for i in range(0,3):
self.send_and_receive("q")
time.sleep(0.1)
time.sleep(0.5)
self.ser.reset_input_buffer()
def send_and_receive(self, message: str) -> str:
"""
Send a string to the device and return the received response line.
Args:
message (str): The string to send (newline added automatically if missing).
Returns:
str: The response received from the device, stripped of newline characters.
"""
if not self.ser or not self.ser.is_open:
raise ConnectionError("Serial connection not established.")
# Ensure the message ends with newline
if not message.endswith("\r\n"):
message += "\r\n"
logging.debug('Sending {0}'.format(message))
# Send the message
self.ser.write(message.encode("utf-8"))
# Read a response line
response = self.ser.readline().decode("utf-8", errors="ignore").strip()
logging.debug('Received: {0}'.format(response))
return response
def search(self):
"""
Placeholder search method.
To be implemented later by the user.
Should return a serial port string.
"""
#ports = [p.device for p in list(serial.tools.list_ports.comports())]
#logging.info("Ports found: %s",ports)
#for port in [(i, self.comboBox.itemText(i)) for i in range(self.comboBox.count())]:
#mouse_settings = get_mouse_settings('/dev/ttyACM0')
#if self.mouse_settings != 0:
# self.comboBox.setCurrentIndex(port[0])
# break
raise NotImplementedError("search() method not implemented yet.")
def sendCommand(self, cmd, value=None):
if value is None:
valueStr = ""
elif isinstance(value, (int, float)):
valueStr = str(value)
elif isinstance(value, str):
valueStr = value
else:
raise TypeError(f"Unsupported type for value: {type(value)}")
received = self.send_and_receive('>'+cmd+valueStr)
receivedCmd = received[1:2]
if len(received) >2:
payload = received[2:len(received)]
else:
payload = None
#logging.debug('cmd: {0}'.format(cmd))
#logging.debug('Received data raw: {0}'.format(received))
#logging.debug('Received cmd: {0}'.format(receivedCmd))
#logging.debug('Received length: {0}'.format(len(received)))
#logging.debug('Received data: {0}'.format(returnValue))
if receivedCmd == cmd:
logging.debug('{0} acknowledged'.format(cmd))
return self.interpret_serial_value(payload)
else:
logging.error('{0} not acknowledged'.format(cmd))
time.sleep(1)
logging.debug(self.ser.read_all())
return None
def parse_progmode_error(self, value: int):
"""
Compare an incoming numeric value with the ProgmodeError enum.
Return the matching enum object if found, otherwise None.
Includes a debug print for visibility.
"""
try:
error_enum = ProgmodeError(value)
logging.debug(f"Matched value {value} → {error_enum.name}")
return error_enum
except ValueError:
logging.warning(f"Unknown ProgmodeError value: {value}")
return None
def interpret_serial_value(self, value_str: str):
"""
Interpret a string received over the serial interface as:
- Boolean (0 or 1)
- Integer (-9999 ... +9999)
- Float (xx.yyy)
- ProgmodeError (values >= 10000)
Returns the interpreted Python type (bool, int, or float),
or None if parsing fails.
Includes a debug print.
"""
value_str = value_str.strip()
# Try boolean (strict 0 or 1)
if value_str in ("0", "1"):
result = bool(int(value_str))
logging.debug(f"Interpreted '{value_str}' as BOOL → {result}")
return result
# Try integer parsing (could be INT or Error Code)
try:
int_val = int(value_str)
# Check for ProgmodeError
if int_val >= 10000:
try:
error_enum = ProgmodeError(int_val)
logging.debug(f"Interpreted '{value_str}' as PROGMODE_ERROR → {error_enum.name}")
return error_enum
except ValueError:
# this might be given, when e.g. the magic number is queried
logging.debug(f"Unknown ProgmodeError value: {int_val}")
return int_val # Return as plain int if not in enum
# Normal integer range check
if -9999 <= int_val <= 9999:
logging.debug(f"Interpreted '{value_str}' as INT → {int_val}")
return int_val
except ValueError:
pass
# Try float (xx.yyy format)
try:
float_val = float(value_str)
# Ensure it has a decimal point in the original string
if "." in value_str:
logging.debug(f"Interpreted '{value_str}' as FLOAT → {float_val}")
return float_val
except ValueError:
pass
# if everything else was not plausible, than it is a string
logging.debug(f"Interpreting as a string:'{value_str}'")
return value_str
def getMagicNumber(self):
ret = self.sendCommand(cmd='m')
if isinstance(ret, int) and not isinstance(ret, bool):
self.magicNumber = ret
return self.magicNumber
else:
logging.error(f"Magic number not found. Error:{0}".format(ret))
return None
def getNumberParameters(self):
ret = self.sendCommand(cmd='n')
if isinstance(ret, int) and not isinstance(ret, bool):
self.numberParameters = ret
if self.numberParameters == 0:
logging.error("Number of Parameters is zero! The EEPROM is probably cleared!")
return self.numberParameters
else:
logging.error(f"Number of parameters not found. Error:{0}".format(ret))
return None
def setParameterNumber(self, paramNo):
if paramNo is None:
logging.error('No Param No. provided')
return False
if self.numberParameters is None:
self.getNumberParameters()
if paramNo<1 or paramNo>self.numberParameters:
#number 0 is not used!
logging.error(f"Parameter Number {0} not in range 0 to {1}".format(paramNo, self.numberParameters))
ret = self.sendCommand(cmd='p', value=paramNo)
if ret == ProgmodeError.PE_OK:
logging.debug(f"Set Parameter Number to {paramNo}")
self.actParamNo = paramNo
return True
elif ret == ProgmodeError.PE_INVALID_PARAM:
logging.warning(f"Unkown parameter number {0}".format(paramNo))
self.actParamNo = None
return False
else:
logging.warning(f"Unkown return type: {0}".format(ret))
self.actParamNo = None
return False
def getParameterType(self, paramNo=None):
# use new paramNo if provided
if paramNo is not None:
self.setParameterNumber(paramNo)
# check if a new or stored paramNo is present
if self.actParamNo is None:
logging.error('No Param No. selected')
return False
ret = self.sendCommand(cmd='t')
#logging.debug("ret is of type{0}".format(type(ret)))
try:
paramType = ParamType(ret)
logging.debug(f"Interpreted '{ret}' as ParamType → {paramType.name}")
return paramType
except ValueError:
logging.debug(f"Unknown ParamType value: {ret}")
return ret # Return as plain int if not in enum
def getParameterDescription(self, paramNo=None):
# use new paramNo if provided
if paramNo is not None:
self.setParameterNumber(paramNo)
# check if a new or stored paramNo is present
if self.actParamNo is None:
logging.error('No Param No. selected')
return False
ret = self.sendCommand(cmd='d')
if isinstance(ret,str):
#yes, it is the description
logging.debug(f"Description: '{ret}'")
return ret # Return as string
else:
logging.warning(f"Unknown return description: {ret}")
return None
def readValue(self, paramNo=None):
# use new paramNo if provided
if paramNo is not None:
self.setParameterNumber(paramNo)
# check if a new or stored paramNo is present
if self.actParamNo is None:
logging.error('No Param No. selected')
return False
#type = self.getParameterType_()
ret = self.sendCommand(cmd='r')
return ret
def writeValue(self, value, paramNo=None):
# use new paramNo if provided
if paramNo is not None:
self.setParameterNumber(paramNo)
else:
self.setParameterNumber(self.actParamNo)
# check if a new or stored paramNo is present
if self.actParamNo is None:
logging.error('No Param No. selected')
return False
paramType = self.getParameterType()
logging.debug("write value {0} to paramNo {1}".format(value, paramNo))
if (paramType == ParamType.BOOL) and isinstance(value, bool):
value_str = 1 if value==True else 0
logging.debug("Write Value {0}".format(value_str))
elif (paramType == ParamType.BOOL) and isinstance(value, (int,float)):
value_str = 1 if value==1 else 0
logging.debug("Write Value {0}".format(value_str))
elif (paramType == ParamType.INT) and isinstance(value, (int, float)):
value_str = "{0:d}".format(value)
logging.debug("Write Value {0}".format(value_str))
elif (paramType == ParamType.FLOAT) and isinstance(value, (int, float)):
value_str = "{0:.4f}".format(value)
logging.debug("Write Value {0}".format(value_str))
else:
logging.error("value is of type{0}, but param is type {1}".format(paramType(value), paramType))
return False
return self.sendCommand(cmd='w',value=value_str)
def printAllParams(self):
for i in range(1,sm.getNumberParameters()):
print(f"#{i}: {sm.getParameterDescription(i)} <{sm.getParameterType(i).name}> = {sm.readValue(i)} ")
def printParam(self, paramNo):
print(f"#{paramNo}: {sm.getParameterDescription(paramNo)} <{sm.getParameterType(paramNo).name}> = {sm.readValue(paramNo)} ")
def loadParamsFromEEPROM(self):
ret = self.sendCommand(cmd='l')
if ret == ProgmodeError.PE_OK:
logging.debug("Loaded Params from EEPROM")
return True
else:
logging.warning(f"Params not loaded:{0}".format(ret))
return False
def saveParamsToEEPROM(self):
ret = self.sendCommand(cmd='s')
if ret == ProgmodeError.PE_OK:
logging.debug("Saved Params to EEPROM")
return True
else:
logging.warning(f"Params not saved:{0}".format(ret))
return False
def invalidateMagicNumber(self):
ret = self.sendCommand(cmd='i')
if ret == ProgmodeError.PE_OK:
logging.debug("Invalidated Magic Number")
return True
else:
logging.warning(f"magic number not invalidated:{0}".format(ret))
return False
def clearEEPROM(self):
ret = self.sendCommand(cmd='c')
if ret == ProgmodeError.PE_OK:
logging.debug("Cleared EEPROM")
return True
else:
logging.warning(f"EEPROM not cleared{0}".format(ret))
return False
if __name__ == '__main__':
# change the debug logging here from logging.CRITICAL to logging.DEBUG
logging.basicConfig(level=logging.CRITICAL, format='[%(asctime)s] [%(levelname)s] [%(funcName)s] %(message)s')
sm = SpaceMouseAPI()
sm.connect('/dev/ttyACM0') # or sm.connect() if you implement search()
# this commands can be enabled and tested one after the other
#response = sm.send_and_receive(">m")
#print(response)
#sm.sendCommand(cmd='m')
#sm.sendCommand(cmd='z')
#sm.parse_progmode_error(10003)
# sm.interpret_serial_value("0")
# sm.interpret_serial_value("1")
# sm.interpret_serial_value("-5465")
# sm.interpret_serial_value("+555")
# sm.interpret_serial_value("5+fdsf")
# sm.interpret_serial_value("58.9878795")
# sm.interpret_serial_value("10004")
#print(f"Magic Number {sm.getMagicNumber()}")
#print(f"Number of Parameters {sm.getNumberParameters()}")
#if sm.setParameterNumber(5):
# print(f"Set Parameter Number to 5")
#print(f"{sm.getParameterType()}")
#print(f"{sm.getParameterDescription()}")
#sm.setParameterNumber(55)
#print(f"{sm.readValue(5)}")
#Print all values
sm.loadParamsFromEEPROM()
#sm.printAllParams()
#sm.printParam(32)
#sm.printParam(2)
#sm.printParam(3)
#sm.printParam(18)
#sm.writeValue(paramNo=32, value=201)
#sm.writeValue(paramNo=32, value=-150)
#sm.writeValue(paramNo=2, value=0.2)
#sm.writeValue(paramNo=3, value=-1.23)
#sm.writeValue(paramNo=18, value=True)
#sm.printParam(32)
#sm.printParam(2)
#sm.printParam(3)
#sm.printParam(18)
sm.printAllParams()
#sm.saveParamsToEEPROM()
#sm.clearEEPROM()
sm.close()