spacenav-ws 0.1.1__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spacenav-ws
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: SpaceNav WebSocket Bridge for using a 3dConnexion spacemouse with onshape
5
5
  Author-email: RmStorm <roaldstorm@gmail.com>
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "spacenav-ws"
3
- version = "0.1.1"
3
+ version = "0.1.2"
4
4
  description = "SpaceNav WebSocket Bridge for using a 3dConnexion spacemouse with onshape"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -7,8 +7,8 @@ from typing import Any
7
7
  import numpy as np
8
8
  from scipy.spatial import transform
9
9
 
10
- import spacenav_ws.spacenav
11
- import spacenav_ws.wamp
10
+ from spacenav_ws.spacenav import MotionEvent, ButtonEvent, from_message
11
+ from spacenav_ws.wamp import WampSession, Prefix, Call, Subscribe, CallResult
12
12
 
13
13
 
14
14
  class Mouse3d:
@@ -44,8 +44,9 @@ class Controller:
44
44
  def __post_init__(self):
45
45
  self.affine = np.asarray(self.affine).reshape([4, 4])
46
46
 
47
- def __init__(self, reader: asyncio.StreamReader, mouse: Mouse3d, wamp_state_handler: spacenav_ws.wamp.WampSession):
47
+ def __init__(self, reader: asyncio.StreamReader, mouse: Mouse3d, wamp_state_handler: WampSession, client_metadata: dict):
48
48
  self.id = "controller0"
49
+ self.client_metadata = client_metadata
49
50
  self.reader = reader
50
51
  self._mouse = mouse
51
52
  self.wamp_state_handler = wamp_state_handler
@@ -57,9 +58,9 @@ class Controller:
57
58
  self.coordinate_system = None
58
59
  self.subscribed = False
59
60
 
60
- async def subscribe(self):
61
+ async def subscribe(self, msg: Subscribe):
61
62
  """When a subscription request for self.controller_uri comes in we start broadcasting!"""
62
- # await self.initialize()
63
+ logging.info("handling subscribe %s", msg)
63
64
  self.subscribed = True
64
65
 
65
66
  async def client_update(self, controller_id: str, args: dict[str, Any]):
@@ -82,20 +83,25 @@ class Controller:
82
83
  while True:
83
84
  mouse_event = await self.reader.read(32)
84
85
  nums = struct.unpack("iiiiiiii", mouse_event)
85
- event = spacenav_ws.spacenav.from_message(list(nums))
86
- if isinstance(event, spacenav_ws.spacenav.ButtonEvent):
86
+ event = from_message(list(nums))
87
+ if isinstance(event, ButtonEvent):
87
88
  logging.warning("Button presses are discarded for now! %s", event)
88
- elif isinstance(event, spacenav_ws.spacenav.MotionEvent):
89
+ elif isinstance(event, MotionEvent):
89
90
  if self.subscribed:
90
- await self.send_mouse_event_to_client(event)
91
-
92
- async def send_mouse_event_to_client(self, event: spacenav_ws.spacenav.MotionEvent):
91
+ if self.client_metadata["name"] == "Onshape":
92
+ await self.update_onshape_client(event)
93
+ elif self.client_metadata["name"] == "WebThreeJS Sample":
94
+ await self.update_3dconnexion_client(event)
95
+ else:
96
+ logging.warning("Unknown client! Cannot send mouse events, client_metadata:%s", self.client_metadata)
97
+
98
+ async def update_onshape_client(self, event: MotionEvent):
93
99
  # 1) pull down the current extents and model matrix
94
100
  extents = await self.remote_read("view.extents")
95
101
  flat = await self.remote_read("view.affine")
96
102
  curr_affine = np.asarray(flat, dtype=np.float32).reshape(4, 4)
97
103
 
98
- # TODO:
104
+ # TODO: This is not correct
99
105
  # 2) Handle rotation
100
106
  angles = np.array([event.pitch, event.yaw, -event.roll], dtype=np.float32) * 0.008
101
107
  rot_cam = transform.Rotation.from_euler("xyz", angles, degrees=True).as_matrix()
@@ -125,29 +131,59 @@ class Controller:
125
131
  await self.remote_write("view.affine", new_affine.reshape(-1).tolist())
126
132
  await self.remote_write("view.extents", new_extents)
127
133
 
134
+ async def update_3dconnexion_client(self, event: MotionEvent):
135
+ # 1) pull down the current extents and model matrix
136
+ flat = await self.remote_read("view.affine")
137
+ curr_affine = np.asarray(flat, dtype=np.float32).reshape(4, 4)
138
+
139
+ # 2) Handle rotation
140
+ # Rotate the model in the cameras perspective
141
+ # angles = np.array([event.pitch, event.yaw, -event.roll], dtype=np.float32) * 0.008
142
+ # rot_cam = transform.Rotation.from_euler("xyz", angles, degrees=True).as_matrix()
143
+ # rot_delta = np.eye(4, dtype=np.float32)
144
+ # rot_delta[:3, :3] = rot_cam
145
+ # rotated = rot_delta @ curr_affine
146
+
147
+ # Rotate the model from _its_ perspective
148
+ angles = np.array([event.pitch, event.yaw, -event.roll], dtype=np.float32) * 0.008
149
+ rot_cam = transform.Rotation.from_euler("xyz", angles, degrees=True).as_matrix()
150
+ rot_delta = np.eye(4, dtype=np.float32)
151
+ rot_delta[:3, :3] = rot_cam
152
+ rotated = curr_affine @ rot_delta
153
+
154
+ # 3) Handle translations
155
+ trans_delta = np.eye(4, dtype=np.float32)
156
+ # Probably event.y doesn't do anything at all here!
157
+ trans_delta[3, :3] = np.array([-event.x, -event.z, event.y], dtype=np.float32) * 0.001
158
+ new_affine = trans_delta @ rotated
159
+
160
+ # Write back changes
161
+ await self.remote_write("motion", True)
162
+ await self.remote_write("view.affine", new_affine.reshape(-1).tolist())
163
+
128
164
 
129
- async def create_mouse_controller(wamp_state_handler: spacenav_ws.wamp.WampSession, spacenav_reader: asyncio.StreamReader):
165
+ async def create_mouse_controller(wamp_state_handler: WampSession, spacenav_reader: asyncio.StreamReader):
130
166
  await wamp_state_handler.wamp.begin()
131
167
  # The first three messages are typically prefix setters!
132
168
  msg = await wamp_state_handler.wamp.next_message()
133
- while isinstance(msg, spacenav_ws.wamp.Prefix):
169
+ while isinstance(msg, Prefix):
134
170
  await wamp_state_handler.wamp.run_message_handler(msg)
135
171
  msg = await wamp_state_handler.wamp.next_message()
136
172
 
137
173
  # The first call after the prefixes must be 'create mouse'
138
- assert isinstance(msg, spacenav_ws.wamp.Call)
174
+ assert isinstance(msg, Call)
139
175
  assert msg.proc_uri == "3dx_rpc:create" and msg.args[0] == "3dconnexion:3dmouse"
140
176
  mouse = Mouse3d()
141
177
  logging.info(f'Created 3d mouse "{mouse.id}" for version {msg.args[1]}')
142
- await wamp_state_handler.wamp.send_message(spacenav_ws.wamp.CallResult(msg.call_id, {"connexion": mouse.id}))
178
+ await wamp_state_handler.wamp.send_message(CallResult(msg.call_id, {"connexion": mouse.id}))
143
179
 
144
180
  # And the second call after the prefixes must be 'create controller'
145
181
  msg = await wamp_state_handler.wamp.next_message()
146
- assert isinstance(msg, spacenav_ws.wamp.Call)
182
+ assert isinstance(msg, Call)
147
183
  assert msg.proc_uri == "3dx_rpc:create" and msg.args[0] == "3dconnexion:3dcontroller" and msg.args[1] == mouse.id
148
184
  metadata = msg.args[2]
149
- ctrl = Controller(spacenav_reader, mouse, wamp_state_handler)
185
+ ctrl = Controller(spacenav_reader, mouse, wamp_state_handler, metadata)
150
186
  logging.info(f'Created controller "{ctrl.id}" for mouse "{mouse.id}", for client "{metadata["name"]}", version "{metadata["version"]}"')
151
187
 
152
- await wamp_state_handler.wamp.send_message(spacenav_ws.wamp.CallResult(msg.call_id, {"instance": ctrl.id}))
188
+ await wamp_state_handler.wamp.send_message(CallResult(msg.call_id, {"instance": ctrl.id}))
153
189
  return ctrl
@@ -22,18 +22,7 @@ ORIGINS = [
22
22
  "https://3dconnexion.com",
23
23
  "https://cad.onshape.com",
24
24
  ]
25
- # from importlib.resources import files, as_file
26
-
27
- # # Build a Traversable pointing to spacenav_ws/certs/ip.crt
28
- # resource = files(__package__).joinpath("certs", "ip.crt")
29
- # # as_file() ensures we have a real filesystem path even if inside a zip/wheel
30
- # with as_file(resource) as cert_path:
31
- # CERT_FILE = str(cert_path)
32
-
33
- # # Same for the key
34
- # key_res = files(__package__).joinpath("certs", "ip.key")
35
- # with as_file(key_res) as key_path:
36
- # KEY_FILE = str(key_path)
25
+
37
26
  CERT_FILE = Path(__file__).parent / "certs" / "ip.crt"
38
27
  KEY_FILE = Path(__file__).parent / "certs" / "ip.key"
39
28
 
@@ -114,7 +103,7 @@ async def read_mouse_stream():
114
103
  logging.info("Start moving your mouse!")
115
104
  async for event in get_mouse_event_generator():
116
105
  logging.info(event.strip())
117
-
106
+
118
107
 
119
108
  @cli.command()
120
109
  def read_mouse():
@@ -17,9 +17,9 @@ def get_sync_spacenav_socket():
17
17
  async def get_async_spacenav_socket_reader() -> tuple[asyncio.StreamReader, asyncio.StreamWriter]:
18
18
  try:
19
19
  return await asyncio.open_unix_connection(SPACENAV_SOCKET_PATH)
20
- except FileNotFoundError as e:
20
+ except (FileNotFoundError, ConnectionRefusedError):
21
21
  logging.exception("Space mouse not found!")
22
- raise RuntimeError from e
22
+ exit(1)
23
23
 
24
24
 
25
25
  @dataclass
@@ -5,7 +5,8 @@ import logging
5
5
  import random
6
6
  import string
7
7
  from enum import IntEnum
8
- from typing import Any, ClassVar, Dict, NamedTuple, Optional, Type
8
+ from types import CoroutineType
9
+ from typing import Any, ClassVar, Dict, NamedTuple, Optional, Type, Callable
9
10
 
10
11
  from fastapi import WebSocket
11
12
 
@@ -102,8 +103,8 @@ class WampProtocol:
102
103
  self._session_id = _rand_id(16)
103
104
 
104
105
  self.prefixes = {}
105
- self.call_handlers = {}
106
- self.subscribe_handlers = {}
106
+ self.call_handlers: dict[str, Callable[..., CoroutineType[Any, Any, None]]] = {}
107
+ self.subscribe_handlers: dict[str, Callable[[Subscribe], CoroutineType[Any, Any, None]]] = {}
107
108
 
108
109
  async def begin(self):
109
110
  await self._socket.accept(subprotocol="wamp")
@@ -148,7 +149,7 @@ class WampProtocol:
148
149
  logging.warning("Unknown subscribable: %s", topic)
149
150
  else:
150
151
  logging.debug(f"handle subscribe to '{topic}' by calling: {handler}")
151
- await handler()
152
+ await handler(msg)
152
153
 
153
154
  async def handle_callresult(self, msg: CallResult):
154
155
  logging.warning("No callresult handler for msg: %s", msg)
@@ -389,7 +389,7 @@ wheels = [
389
389
 
390
390
  [[package]]
391
391
  name = "spacenav-ws"
392
- version = "0.1.0"
392
+ version = "0.1.2"
393
393
  source = { editable = "." }
394
394
  dependencies = [
395
395
  { name = "fastapi" },
File without changes
File without changes
File without changes
File without changes