mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-08-17 14:45:59 +08:00
![google-labs-jules[bot]](/assets/img/avatar_default.png)
This change introduces support for 3Dconnexion SpaceMouse devices on Linux within the 3DConnexion plugin. Key changes: - Added `LinuxSpacenavClient.py`, which uses `ctypes` to interface with the system-provided `libspnav.so.0` library. This client handles opening a connection to the `spacenavd` daemon and polling for motion and button events. - Modified `NavlibClient.py` to include platform detection. On Linux, it now uses `LinuxSpacenavClient` for device input. On Windows and macOS, it continues to use the existing `pynavlib`. - Updated the plugin initialization in `plugins/3DConnexion/__init__.py` to gracefully handle cases where `libspnav.so.0` might be missing or `spacenavd` is not accessible on Linux, disabling the plugin in such scenarios. - The core camera manipulation logic in `NavlibClient.py` has been adapted to accept transformation matrices from either `pynavlib` or the new Linux client, aiming for consistent behavior. - Placeholder adaptations for some `pynavlib`-specific methods have been added for the Linux path, returning `None` or basic Python types where `pynav.*` types were previously used. This implementation relies on you having `spacenavd` (version 0.6 or newer recommended) installed and running, along with `libspnav0` (or equivalent). Testing for this feature is currently manual, involving checking device response for camera manipulation (pan, zoom, rotate) within Cura on a Linux environment with a configured SpaceMouse. Output:
192 lines
7.1 KiB
Python
192 lines
7.1 KiB
Python
import ctypes
|
|
import logging
|
|
|
|
# Configure logging
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
|
|
# Constants from spnav.h
|
|
SPNAV_EVENT_MOTION = 1
|
|
SPNAV_EVENT_BUTTON = 2
|
|
SPNAV_EVENT_ANY = SPNAV_EVENT_MOTION | SPNAV_EVENT_BUTTON
|
|
|
|
# Structures and Union based on spnav.h
|
|
class SpnavMotionEvent(ctypes.Structure):
|
|
_fields_ = [
|
|
("type", ctypes.c_int),
|
|
("x", ctypes.c_int),
|
|
("y", ctypes.c_int),
|
|
("z", ctypes.c_int),
|
|
("rx", ctypes.c_int),
|
|
("ry", ctypes.c_int),
|
|
("rz", ctypes.c_int),
|
|
("period", ctypes.c_ushort),
|
|
]
|
|
|
|
class SpnavButtonEvent(ctypes.Structure):
|
|
_fields_ = [
|
|
("type", ctypes.c_int),
|
|
("press", ctypes.c_int),
|
|
("bnum", ctypes.c_int),
|
|
]
|
|
|
|
class SpnavEvent(ctypes.Union):
|
|
_fields_ = [
|
|
("type", ctypes.c_int),
|
|
("motion", SpnavMotionEvent),
|
|
("button", SpnavButtonEvent),
|
|
]
|
|
|
|
class LinuxSpacenavClient:
|
|
def __init__(self):
|
|
self.lib = None
|
|
self.available = False
|
|
try:
|
|
self.lib = ctypes.CDLL("libspnav.so.0")
|
|
self.available = True
|
|
except OSError:
|
|
try:
|
|
self.lib = ctypes.CDLL("libspnav.so")
|
|
self.available = True
|
|
except OSError:
|
|
logging.warning("libspnav.so.0 or libspnav.so not found. Spacenav functionality will be unavailable.")
|
|
return
|
|
|
|
if self.available:
|
|
logging.info("Successfully loaded libspnav.")
|
|
# Define function prototypes
|
|
try:
|
|
self.spnav_open = self.lib.spnav_open
|
|
self.spnav_open.restype = ctypes.c_int
|
|
self.spnav_open.argtypes = []
|
|
|
|
self.spnav_close = self.lib.spnav_close
|
|
self.spnav_close.restype = ctypes.c_int
|
|
self.spnav_close.argtypes = []
|
|
|
|
self.spnav_fd = self.lib.spnav_fd
|
|
self.spnav_fd.restype = ctypes.c_int
|
|
self.spnav_fd.argtypes = []
|
|
|
|
self.spnav_poll_event = self.lib.spnav_poll_event
|
|
self.spnav_poll_event.restype = ctypes.c_int
|
|
self.spnav_poll_event.argtypes = [ctypes.POINTER(SpnavEvent)]
|
|
|
|
self.spnav_remove_events = self.lib.spnav_remove_events
|
|
self.spnav_remove_events.restype = ctypes.c_int
|
|
self.spnav_remove_events.argtypes = [ctypes.c_int]
|
|
|
|
logging.info("Function prototypes defined successfully.")
|
|
|
|
except AttributeError as e:
|
|
logging.error(f"Error setting up function prototypes: {e}")
|
|
self.available = False
|
|
|
|
|
|
def open(self) -> bool:
|
|
if not self.available:
|
|
logging.warning("spnav_open called but library not available.")
|
|
return False
|
|
ret = self.spnav_open()
|
|
if ret == 0:
|
|
logging.info("Successfully opened connection to spacenavd (native protocol).")
|
|
return True
|
|
else:
|
|
# spnav_open returns -1 on error and sets errno.
|
|
# However, ctypes doesn't automatically pick up errno from C.
|
|
# For now, just log a generic error.
|
|
logging.error(f"spnav_open failed with return code {ret}.")
|
|
return False
|
|
|
|
def close(self) -> None:
|
|
if not self.available:
|
|
logging.warning("spnav_close called but library not available.")
|
|
return
|
|
ret = self.spnav_close()
|
|
if ret == 0:
|
|
logging.info("Successfully closed connection to spacenavd.")
|
|
else:
|
|
logging.error(f"spnav_close failed with return code {ret}.")
|
|
|
|
|
|
def poll_event(self) -> SpnavEvent | None:
|
|
if not self.available:
|
|
logging.warning("spnav_poll_event called but library not available.")
|
|
return None
|
|
|
|
event = SpnavEvent()
|
|
ret = self.spnav_poll_event(ctypes.byref(event))
|
|
if ret > 0:
|
|
# logging.debug(f"spnav_poll_event returned event type: {event.type}") # Too verbose for INFO
|
|
return event
|
|
elif ret == 0:
|
|
# No event pending
|
|
return None
|
|
else:
|
|
# Error
|
|
logging.error(f"spnav_poll_event failed with return code {ret}.")
|
|
return None
|
|
|
|
def get_fd(self) -> int:
|
|
if not self.available:
|
|
logging.warning("spnav_fd called but library not available.")
|
|
return -1
|
|
fd = self.spnav_fd()
|
|
if fd == -1:
|
|
logging.error("spnav_fd failed.")
|
|
else:
|
|
logging.info(f"spnav_fd returned file descriptor: {fd}")
|
|
return fd
|
|
|
|
def remove_events(self, event_type: int) -> int:
|
|
if not self.available:
|
|
logging.warning("spnav_remove_events called but library not available.")
|
|
return -1 # Or some other error indicator
|
|
|
|
ret = self.spnav_remove_events(event_type)
|
|
if ret < 0:
|
|
# spnav_remove_events returns number of events removed, or -1 on error
|
|
logging.error(f"spnav_remove_events failed with return code {ret} for event_type {event_type}.")
|
|
else:
|
|
logging.info(f"spnav_remove_events successfully removed {ret} events of type {event_type}.")
|
|
return ret
|
|
|
|
if __name__ == '__main__':
|
|
logging.info("Attempting to initialize LinuxSpacenavClient for testing...")
|
|
client = LinuxSpacenavClient()
|
|
|
|
if client.available:
|
|
logging.info("LinuxSpacenavClient available.")
|
|
if client.open():
|
|
logging.info("Spacenav opened successfully. You can try moving the device.")
|
|
|
|
# Example of polling for a few events
|
|
for _ in range(5): # Try to read 5 events
|
|
event = client.poll_event()
|
|
if event:
|
|
if event.type == SPNAV_EVENT_MOTION:
|
|
logging.info(f"Motion Event: x={event.motion.x}, y={event.motion.y}, z={event.motion.z}, rx={event.motion.rx}, ry={event.motion.ry}, rz={event.motion.rz}")
|
|
elif event.type == SPNAV_EVENT_BUTTON:
|
|
logging.info(f"Button Event: press={event.button.press}, bnum={event.button.bnum}")
|
|
else:
|
|
logging.info("No event polled.")
|
|
# break # if no event, might not be more immediately
|
|
|
|
# Example of getting file descriptor
|
|
fd = client.get_fd()
|
|
logging.info(f"File descriptor: {fd}")
|
|
|
|
# Example of removing pending events
|
|
# Note: This might clear events that your application wants. Use carefully.
|
|
# Usually, you'd call this if the event queue is full or if you want to ignore old events.
|
|
# client.remove_events(SPNAV_EVENT_ANY)
|
|
# logging.info("Attempted to remove any pending events.")
|
|
|
|
client.close()
|
|
logging.info("Spacenav closed.")
|
|
else:
|
|
logging.error("Failed to open spacenav.")
|
|
else:
|
|
logging.warning("LinuxSpacenavClient not available. Cannot run tests.")
|
|
|
|
logging.info("LinuxSpacenavClient.py basic test finished.")
|