wandelbots-nova 0.1.4__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.
- wandelbots_nova-0.1.4/PKG-INFO +58 -0
- wandelbots_nova-0.1.4/README.md +36 -0
- wandelbots_nova-0.1.4/nova/__init__.py +18 -0
- wandelbots_nova-0.1.4/nova/core/__init__.py +0 -0
- wandelbots_nova-0.1.4/nova/core/controller.py +60 -0
- wandelbots_nova-0.1.4/nova/core/exceptions.py +13 -0
- wandelbots_nova-0.1.4/nova/core/motion_group.py +287 -0
- wandelbots_nova-0.1.4/nova/core/nova.py +46 -0
- wandelbots_nova-0.1.4/nova/gateway.py +93 -0
- wandelbots_nova-0.1.4/nova/types/__init__.py +17 -0
- wandelbots_nova-0.1.4/nova/types/action.py +450 -0
- wandelbots_nova-0.1.4/nova/types/collision_scene.py +171 -0
- wandelbots_nova-0.1.4/nova/types/pose.py +180 -0
- wandelbots_nova-0.1.4/nova/types/state.py +16 -0
- wandelbots_nova-0.1.4/nova/types/vector3d.py +69 -0
- wandelbots_nova-0.1.4/pyproject.toml +130 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: wandelbots-nova
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Official Python SDK for the Wandelbots
|
|
5
|
+
Author: Wandelbots GmbH
|
|
6
|
+
Requires-Python: >=3.10,<4.0
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Requires-Dist: httpx (>=0.28.0,<0.29.0)
|
|
13
|
+
Requires-Dist: loguru (>=0.7.2,<0.8.0)
|
|
14
|
+
Requires-Dist: numpy (>=2.1.3,<3.0.0)
|
|
15
|
+
Requires-Dist: pydantic (>=2.10.3,<3.0.0)
|
|
16
|
+
Requires-Dist: python-decouple (>=3.8,<4.0)
|
|
17
|
+
Requires-Dist: scipy (>=1.14.1,<2.0.0)
|
|
18
|
+
Requires-Dist: wandelbots_api_client (==24.9.0rc3)
|
|
19
|
+
Requires-Dist: websockets (>=14.1.0,<15.0.0)
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# wandelbots-nova
|
|
23
|
+
|
|
24
|
+
This library provides an SDK for the Wandelbots NOVA API.
|
|
25
|
+
|
|
26
|
+
The SDK will help you to build your own apps and services on top of NOVA and makes programming a robot as easy as possible.
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
This library requires
|
|
31
|
+
* Python >=3.10
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
To use the library, first install it using the following command
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install wandelbots-nova
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Then import the library in your code
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from nova import Nova, MotionGroup
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Development
|
|
48
|
+
|
|
49
|
+
To install the development dependencies, run the following command
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
poetry install
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
Have a look at the [examples](/examples) directory to see how to use the library.
|
|
58
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# wandelbots-nova
|
|
2
|
+
|
|
3
|
+
This library provides an SDK for the Wandelbots NOVA API.
|
|
4
|
+
|
|
5
|
+
The SDK will help you to build your own apps and services on top of NOVA and makes programming a robot as easy as possible.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
This library requires
|
|
10
|
+
* Python >=3.10
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
To use the library, first install it using the following command
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install wandelbots-nova
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Then import the library in your code
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from nova import Nova, MotionGroup
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Development
|
|
27
|
+
|
|
28
|
+
To install the development dependencies, run the following command
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
poetry install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Usage
|
|
35
|
+
|
|
36
|
+
Have a look at the [examples](/examples) directory to see how to use the library.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from nova.core.nova import Nova, Cell
|
|
2
|
+
from nova.core.motion_group import MotionGroup
|
|
3
|
+
from nova.core.controller import Controller
|
|
4
|
+
from nova.types.pose import Pose
|
|
5
|
+
from nova.types.action import Action, lin, ptp, jnt, cir
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Nova",
|
|
9
|
+
"Cell",
|
|
10
|
+
"MotionGroup",
|
|
11
|
+
"Controller",
|
|
12
|
+
"lin",
|
|
13
|
+
"ptp",
|
|
14
|
+
"jnt",
|
|
15
|
+
"cir",
|
|
16
|
+
"Action",
|
|
17
|
+
"Pose",
|
|
18
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from typing import final
|
|
2
|
+
|
|
3
|
+
import wandelbots_api_client as wb
|
|
4
|
+
from nova.core.motion_group import MotionGroup
|
|
5
|
+
from loguru import logger
|
|
6
|
+
|
|
7
|
+
from nova.gateway import ApiGateway
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Controller:
|
|
11
|
+
def __init__(self, *, api_gateway: ApiGateway, cell: str, controller_host: str):
|
|
12
|
+
self._api_gateway = api_gateway
|
|
13
|
+
self._controller_api = api_gateway.controller_api
|
|
14
|
+
self._motion_group_api = api_gateway.motion_group_api
|
|
15
|
+
self._cell = cell
|
|
16
|
+
self._controller_host = controller_host
|
|
17
|
+
self._motion_groups: dict[str, MotionGroup] = {}
|
|
18
|
+
|
|
19
|
+
async def _get_controller(self, host: str) -> wb.models.ControllerInstance | None:
|
|
20
|
+
controller_list_response = await self._controller_api.list_controllers(cell=self._cell)
|
|
21
|
+
controller_list = list(controller_list_response.instances)
|
|
22
|
+
return next((c for c in controller_list if c.host == host), None)
|
|
23
|
+
|
|
24
|
+
@final
|
|
25
|
+
async def __aenter__(self):
|
|
26
|
+
activate_all_motion_groups_response = (
|
|
27
|
+
await self._motion_group_api.activate_all_motion_groups(
|
|
28
|
+
cell=self._cell, controller=self._controller_host
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
# TODO: should we store these states? I dont like storing state, then you have to manage them
|
|
32
|
+
# stateless looks simpler
|
|
33
|
+
# TODO: should we deactivate these motions groups? what does wandelscript does?
|
|
34
|
+
motion_groups = activate_all_motion_groups_response.instances
|
|
35
|
+
for mg in motion_groups:
|
|
36
|
+
logger.info(f"Found motion group {mg.motion_group}")
|
|
37
|
+
motion_group = MotionGroup(
|
|
38
|
+
api_gateway=self._api_gateway, cell=self._cell, motion_group_id=mg.motion_group
|
|
39
|
+
)
|
|
40
|
+
self._motion_groups[motion_group.motion_group_id] = motion_group
|
|
41
|
+
return self
|
|
42
|
+
|
|
43
|
+
@final
|
|
44
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
45
|
+
# TODO: should we deactivate these motions groups? what does wandelscript does?
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
def get_motion_groups(self) -> dict[str, MotionGroup]:
|
|
49
|
+
return self._motion_groups
|
|
50
|
+
|
|
51
|
+
def get_motion_group(self, motion_group_id: str = "0") -> MotionGroup:
|
|
52
|
+
# TODO: I know this doesnt looks good :)
|
|
53
|
+
# here are some considerations for a better implementation:
|
|
54
|
+
# If possible I would prefer stateless approach,
|
|
55
|
+
# so we dont return it from the internal state, but we fetch it from the API with the id
|
|
56
|
+
# in that case having str id is more fleixble than having an int id
|
|
57
|
+
return self._motion_groups[f"{motion_group_id}@{self._controller_host}"]
|
|
58
|
+
|
|
59
|
+
def __getitem__(self, item):
|
|
60
|
+
return self._motion_groups[f"{item}@{self._controller_host}"]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
import wandelbots_api_client as wb
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ControllerNotFoundException(Exception):
|
|
7
|
+
def __init__(self, controller: str = None):
|
|
8
|
+
super().__init__(f"Controller {controller} not found.")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PlanTrajectoryFailed(Exception):
|
|
12
|
+
def __init__(self, error: wb.models.PlanTrajectoryFailedResponse):
|
|
13
|
+
super().__init__(f"Plan trajectory failed: {json.dumps(error.to_dict(), indent=2)}")
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
from collections.abc import AsyncGenerator
|
|
2
|
+
|
|
3
|
+
from nova.core.exceptions import PlanTrajectoryFailed
|
|
4
|
+
from nova.gateway import ApiGateway
|
|
5
|
+
from nova.types.state import MotionState
|
|
6
|
+
from nova.types.action import Action, CombinedActions
|
|
7
|
+
from nova.types.pose import Pose
|
|
8
|
+
from nova.types.collision_scene import CollisionScene
|
|
9
|
+
from loguru import logger
|
|
10
|
+
import wandelbots_api_client as wb
|
|
11
|
+
|
|
12
|
+
MAX_JOINT_VELOCITY_PREPARE_MOVE = 0.2
|
|
13
|
+
START_LOCATION_OF_MOTION = 0.0
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MotionGroup:
|
|
17
|
+
def __init__(self, api_gateway: ApiGateway, cell: str, motion_group_id: str):
|
|
18
|
+
self._api_gateway = api_gateway
|
|
19
|
+
self._motion_api_client = api_gateway.motion_api
|
|
20
|
+
self._cell = cell
|
|
21
|
+
self._motion_group_id = motion_group_id
|
|
22
|
+
self._current_motion: str | None = None
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def motion_group_id(self) -> str:
|
|
26
|
+
return self._motion_group_id
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def current_motion(self) -> str:
|
|
30
|
+
# if not self._current_motion:
|
|
31
|
+
# raise ValueError("No MotionId attached. There is no planned motion available.")
|
|
32
|
+
return self._current_motion
|
|
33
|
+
|
|
34
|
+
async def stream_run(
|
|
35
|
+
self,
|
|
36
|
+
actions: list[Action] | Action,
|
|
37
|
+
tcp: str,
|
|
38
|
+
# collision_scene: dts.CollisionScene | None,
|
|
39
|
+
response_rate_in_ms: int = 200,
|
|
40
|
+
) -> AsyncGenerator[MotionState, None]:
|
|
41
|
+
"""An asynchronous generator that tracks and yields motion states along a given path.
|
|
42
|
+
|
|
43
|
+
This method iterates over the motion states of a given path while handling motion events
|
|
44
|
+
associated with specific path parameters. It tracks the motion states in the `_motion_recording`
|
|
45
|
+
attribute and yields each state as it progresses through the path. Additionally, if there are any
|
|
46
|
+
motion events defined in the path tags, they will be executed when the corresponding path parameter
|
|
47
|
+
is reached.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
actions: the motion defining the path
|
|
51
|
+
tcp (Optional[str]): The tool center point (TCP) to be used for the motion.
|
|
52
|
+
collision_scene (Optional[CollisionScene]): The collision scene to be used for collision detection.
|
|
53
|
+
response_rate_in_ms (int): The sample time in milliseconds to be used for the motion.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Context manager returns an iterator
|
|
57
|
+
Yields:
|
|
58
|
+
MotionState: The current motion state along the path.
|
|
59
|
+
Raises:
|
|
60
|
+
RobotMotionError: if the robot motion stop without reaching the target and stops motion then on exit
|
|
61
|
+
StopAsyncIteration: If the motion path iteration is completed.
|
|
62
|
+
|
|
63
|
+
"""
|
|
64
|
+
if not isinstance(actions, list):
|
|
65
|
+
actions = [actions]
|
|
66
|
+
|
|
67
|
+
# TODO get default tcp if tcp is not set
|
|
68
|
+
motion_iter = self._planned_motion_iter(
|
|
69
|
+
actions=actions, tcp=tcp, collision_scene=None, response_rate_in_ms=response_rate_in_ms
|
|
70
|
+
)
|
|
71
|
+
async for motion_state in motion_iter:
|
|
72
|
+
yield motion_state
|
|
73
|
+
|
|
74
|
+
async def run(self, actions: list[Action] | Action, tcp: str):
|
|
75
|
+
async for _ in self.stream_run(actions=actions, tcp=tcp):
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
async def get_state(self, tcp: str) -> wb.models.MotionGroupStateResponse:
|
|
79
|
+
response = await self._api_gateway.motion_group_infos_api.get_current_motion_group_state(
|
|
80
|
+
cell=self._cell, motion_group=self.motion_group_id, tcp=tcp
|
|
81
|
+
)
|
|
82
|
+
return response
|
|
83
|
+
|
|
84
|
+
async def joints(self, tcp: str) -> wb.models.Joints:
|
|
85
|
+
state = await self.get_state(tcp=tcp)
|
|
86
|
+
return state.state.joint_position
|
|
87
|
+
|
|
88
|
+
async def tcp_pose(self, tcp: str) -> Pose:
|
|
89
|
+
state = await self.get_state(tcp=tcp)
|
|
90
|
+
return Pose(state.state.tcp_pose)
|
|
91
|
+
|
|
92
|
+
async def _get_number_of_joints(self) -> int:
|
|
93
|
+
spec = await self._api_gateway.motion_group_infos_api.get_motion_group_specification(
|
|
94
|
+
cell=self._cell, motion_group=self.motion_group_id
|
|
95
|
+
)
|
|
96
|
+
return len(spec.mechanical_joint_limits)
|
|
97
|
+
|
|
98
|
+
async def _get_optimizer_setup(self, tcp: str) -> wb.models.OptimizerSetup:
|
|
99
|
+
return await self._api_gateway.motion_group_infos_api.get_optimizer_configuration(
|
|
100
|
+
cell=self._cell, motion_group=self._motion_group_id, tcp=tcp
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
async def plan(
|
|
104
|
+
self, actions: list[Action] | Action, tcp: str
|
|
105
|
+
) -> wb.models.PlanTrajectoryResponse:
|
|
106
|
+
if not isinstance(actions, list):
|
|
107
|
+
actions = [actions]
|
|
108
|
+
|
|
109
|
+
if len(actions) == 0:
|
|
110
|
+
raise ValueError("Actions are empty")
|
|
111
|
+
|
|
112
|
+
current_joints = await self.joints(tcp=tcp)
|
|
113
|
+
robot_setup = await self._get_optimizer_setup(tcp=tcp)
|
|
114
|
+
|
|
115
|
+
# TODO: paths = [wb.models.MotionCommandPath(**path.model_dump()) for path in path.motions]
|
|
116
|
+
combined_actions = CombinedActions(items=actions)
|
|
117
|
+
motions = [
|
|
118
|
+
wb.models.MotionCommandPath.from_dict(motion.model_dump())
|
|
119
|
+
for motion in combined_actions.motions
|
|
120
|
+
]
|
|
121
|
+
print(motions)
|
|
122
|
+
motion_commands = [wb.models.MotionCommand(path=motion) for motion in motions]
|
|
123
|
+
|
|
124
|
+
request = wb.models.PlanTrajectoryRequest(
|
|
125
|
+
robot_setup=robot_setup,
|
|
126
|
+
motion_group=self.motion_group_id,
|
|
127
|
+
start_joint_position=current_joints.joints,
|
|
128
|
+
motion_commands=motion_commands,
|
|
129
|
+
tcp=tcp,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
motion_api_client = self._api_gateway.motion_api
|
|
133
|
+
plan_response = await motion_api_client.plan_trajectory(
|
|
134
|
+
cell=self._cell, plan_trajectory_request=request
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if isinstance(
|
|
138
|
+
plan_response.response.actual_instance, wb.models.PlanTrajectoryFailedResponse
|
|
139
|
+
):
|
|
140
|
+
failed_response = plan_response.response.actual_instance
|
|
141
|
+
raise PlanTrajectoryFailed(failed_response)
|
|
142
|
+
|
|
143
|
+
return plan_response
|
|
144
|
+
|
|
145
|
+
async def _get_trajectory_sample(
|
|
146
|
+
self, location: float
|
|
147
|
+
) -> wb.models.GetTrajectorySampleResponse:
|
|
148
|
+
"""Call the RAE to get single sample of trajectory from a previously planned path
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
location: The path parameter along the trajectory to sample
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
The trajectory sample at the specified location
|
|
155
|
+
"""
|
|
156
|
+
if location < 0:
|
|
157
|
+
raise ValueError("location cannot be negative")
|
|
158
|
+
|
|
159
|
+
return await self._motion_api_client.get_motion_trajectory_sample(
|
|
160
|
+
cell=self._cell, motion=self.current_motion, location_on_trajectory=location
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
async def _planned_motion_iter(
|
|
164
|
+
self,
|
|
165
|
+
actions: list[Action],
|
|
166
|
+
tcp: str,
|
|
167
|
+
collision_scene: CollisionScene | None,
|
|
168
|
+
response_rate_in_ms: int,
|
|
169
|
+
) -> AsyncGenerator[MotionState]:
|
|
170
|
+
number_of_joints = await self._get_number_of_joints()
|
|
171
|
+
|
|
172
|
+
async def move_along_path(
|
|
173
|
+
motion_id: str,
|
|
174
|
+
joint_velocities: list[float] | None = None,
|
|
175
|
+
joint_trajectory: wb.models.JointTrajectory | None = None,
|
|
176
|
+
) -> AsyncGenerator[MotionState]:
|
|
177
|
+
motion_api = self._api_gateway.motion_api
|
|
178
|
+
load_plan_response = await motion_api.load_planned_motion(
|
|
179
|
+
cell=self._cell,
|
|
180
|
+
planned_motion=wb.models.PlannedMotion(
|
|
181
|
+
motion_group=self.motion_group_id,
|
|
182
|
+
times=joint_trajectory.times,
|
|
183
|
+
joint_positions=joint_trajectory.joint_positions,
|
|
184
|
+
locations=joint_trajectory.locations,
|
|
185
|
+
tcp="Flange",
|
|
186
|
+
),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
load_plan_response = load_plan_response.plan_successful_response
|
|
190
|
+
|
|
191
|
+
limit_override = wb.models.LimitsOverride()
|
|
192
|
+
if joint_velocities is not None:
|
|
193
|
+
limit_override.joint_velocity_limits = wb.models.Joints(joints=joint_velocities)
|
|
194
|
+
|
|
195
|
+
# Iterator that moves the robot to start of motion
|
|
196
|
+
move_to_trajectory_stream = motion_api.stream_move_to_trajectory_via_joint_ptp(
|
|
197
|
+
cell=self._cell, motion=load_plan_response.motion, location_on_trajectory=0
|
|
198
|
+
)
|
|
199
|
+
async for motion_state_move_to_trajectory in move_to_trajectory_stream:
|
|
200
|
+
yield motion_state_move_to_trajectory
|
|
201
|
+
|
|
202
|
+
responses = []
|
|
203
|
+
|
|
204
|
+
async def movement_controller(
|
|
205
|
+
response_stream: AsyncGenerator,
|
|
206
|
+
) -> (AsyncGenerator)[
|
|
207
|
+
wb.models.ExecuteTrajectoryRequest, wb.models.ExecuteTrajectoryResponse
|
|
208
|
+
]:
|
|
209
|
+
yield wb.models.InitializeMovementRequest(
|
|
210
|
+
trajectory=load_plan_response.motion, initial_location=0
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
combined_actions = CombinedActions(items=actions)
|
|
214
|
+
initialize_movement_response = await anext(response_stream)
|
|
215
|
+
print(f"initial move response {initialize_movement_response}")
|
|
216
|
+
set_io_list = [
|
|
217
|
+
wb.models.SetIO(io=action.model_dump(), location=action.path_parameter)
|
|
218
|
+
for action in combined_actions.actions
|
|
219
|
+
]
|
|
220
|
+
|
|
221
|
+
yield wb.models.StartMovementRequest(
|
|
222
|
+
set_ios=set_io_list, start_on_io=None, pause_on_io=None
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
async for execute_trajectory_response in response_stream:
|
|
226
|
+
response = execute_trajectory_response.actual_instance
|
|
227
|
+
print(f"execute_trajectory_response {response}")
|
|
228
|
+
responses.append(response)
|
|
229
|
+
|
|
230
|
+
# Terminate the generator
|
|
231
|
+
if isinstance(response, wb.models.Standstill):
|
|
232
|
+
if (
|
|
233
|
+
response.standstill.reason
|
|
234
|
+
== wb.models.StandstillReason.REASON_MOTION_ENDED
|
|
235
|
+
):
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
await motion_api.execute_trajectory(self._cell, movement_controller)
|
|
239
|
+
|
|
240
|
+
"""
|
|
241
|
+
if any(isinstance(motion, dts.UnresolvedMotion) for motion in path.motions) or collision_scene is not None:
|
|
242
|
+
current_joints = await self._get_current_joints()
|
|
243
|
+
optimizer_setup = await self.get_optimizer_setup(tcp)
|
|
244
|
+
path = await resolve_motions_and_check_collisions(
|
|
245
|
+
initial_joints=current_joints,
|
|
246
|
+
motion_trajectory=path,
|
|
247
|
+
collision_scene=collision_scene,
|
|
248
|
+
optimizer_setup=optimizer_setup,
|
|
249
|
+
moving_robot_identifier=self.identifier,
|
|
250
|
+
)
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
plan_response = await self.plan(actions, tcp)
|
|
254
|
+
|
|
255
|
+
# if not rae_pb_parser.plan_response.is_executable(plan_response):
|
|
256
|
+
# raise MotionException(plan_response, [], [])
|
|
257
|
+
|
|
258
|
+
# await self._get_trajectory_sample(response_rate_in_ms)
|
|
259
|
+
# TODO: take velocity override into account.
|
|
260
|
+
# if len(trajectory.sample) > 0 and not math.isnan(trajectory[-1].time):
|
|
261
|
+
# self._execution_duration += trajectory[-1].time
|
|
262
|
+
|
|
263
|
+
# self._current_motion = plan_response.response.actual_instance
|
|
264
|
+
logger.debug(f"Planned move session: {self.current_motion}")
|
|
265
|
+
|
|
266
|
+
# TODO refactor RAE commands vs. multiple motion chains
|
|
267
|
+
joints_velocities = [MAX_JOINT_VELOCITY_PREPARE_MOVE] * number_of_joints
|
|
268
|
+
move_iter = move_along_path(
|
|
269
|
+
self.current_motion,
|
|
270
|
+
joint_velocities=joints_velocities,
|
|
271
|
+
joint_trajectory=plan_response.response.actual_instance,
|
|
272
|
+
)
|
|
273
|
+
async for motion_state in move_iter:
|
|
274
|
+
yield motion_state
|
|
275
|
+
|
|
276
|
+
logger.debug(f"Move session '{self.current_motion}' was successfully executed.")
|
|
277
|
+
self._current_motion = None
|
|
278
|
+
|
|
279
|
+
async def stop(self):
|
|
280
|
+
logger.debug(f"Stopping motion of {self}...")
|
|
281
|
+
try:
|
|
282
|
+
await self._motion_api_client.stop_execution(
|
|
283
|
+
cell=self._cell, motion=self.current_motion
|
|
284
|
+
)
|
|
285
|
+
logger.debug(f"Motion {self.current_motion} stopped.")
|
|
286
|
+
except ValueError as e:
|
|
287
|
+
logger.debug(f"No motion to stop for {self}: {e}")
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from nova.core.controller import Controller
|
|
2
|
+
from nova.core.exceptions import ControllerNotFoundException
|
|
3
|
+
from nova.gateway import ApiGateway
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Nova:
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
*,
|
|
10
|
+
host: str | None = None,
|
|
11
|
+
username: str | None = None,
|
|
12
|
+
password: str | None = None,
|
|
13
|
+
access_token: str | None = None,
|
|
14
|
+
version: str = "v1",
|
|
15
|
+
):
|
|
16
|
+
self._api_client = ApiGateway(
|
|
17
|
+
host=host,
|
|
18
|
+
username=username,
|
|
19
|
+
password=password,
|
|
20
|
+
access_token=access_token,
|
|
21
|
+
version=version,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def cell(self, cell_id: str = "cell") -> "Cell":
|
|
25
|
+
# TODO check if the cell exists
|
|
26
|
+
return Cell(self._api_client, cell_id)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Cell:
|
|
30
|
+
def __init__(self, api_gateway: ApiGateway, cell_id: str):
|
|
31
|
+
self._api_gateway = api_gateway
|
|
32
|
+
self._cell_id = cell_id
|
|
33
|
+
|
|
34
|
+
async def controller(self, controller_host: str = None) -> "Controller":
|
|
35
|
+
controller_api = self._api_gateway.controller_api
|
|
36
|
+
controller_list = await controller_api.list_controllers(cell=self._cell_id)
|
|
37
|
+
found_controller = next(
|
|
38
|
+
(c for c in controller_list.instances if c.host == controller_host), None
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if found_controller is None:
|
|
42
|
+
raise ControllerNotFoundException(controller=controller_host)
|
|
43
|
+
|
|
44
|
+
return Controller(
|
|
45
|
+
api_gateway=self._api_gateway, cell=self._cell_id, controller_host=found_controller.host
|
|
46
|
+
)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import functools
|
|
3
|
+
import time
|
|
4
|
+
from typing import TypeVar
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
import wandelbots_api_client as wb
|
|
8
|
+
from decouple import config
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def intercept(api_instance: T) -> T:
|
|
14
|
+
class Interceptor:
|
|
15
|
+
def __init__(self, instance: T):
|
|
16
|
+
self._instance = instance
|
|
17
|
+
|
|
18
|
+
def __getattr__(self, name):
|
|
19
|
+
# Retrieve the original attribute
|
|
20
|
+
original_attr = getattr(self._instance, name)
|
|
21
|
+
|
|
22
|
+
# If it's not callable, return it as is
|
|
23
|
+
if not callable(original_attr):
|
|
24
|
+
return original_attr
|
|
25
|
+
|
|
26
|
+
# Wrap async callables
|
|
27
|
+
if asyncio.iscoroutinefunction(original_attr):
|
|
28
|
+
|
|
29
|
+
@functools.wraps(original_attr)
|
|
30
|
+
async def async_wrapper(*args, **kwargs):
|
|
31
|
+
logger.info(f"Calling {name} with args={args}, kwargs={kwargs}")
|
|
32
|
+
start = time.time()
|
|
33
|
+
try:
|
|
34
|
+
return await original_attr(*args, **kwargs)
|
|
35
|
+
finally:
|
|
36
|
+
duration = time.time() - start
|
|
37
|
+
logger.info(f"{name} took {duration:.2f} seconds")
|
|
38
|
+
|
|
39
|
+
return async_wrapper
|
|
40
|
+
|
|
41
|
+
# Wrap sync callables
|
|
42
|
+
@functools.wraps(original_attr)
|
|
43
|
+
def sync_wrapper(*args, **kwargs):
|
|
44
|
+
logger.debug(f"Calling {name} with args={args}, kwargs={kwargs}")
|
|
45
|
+
start = time.time()
|
|
46
|
+
try:
|
|
47
|
+
return original_attr(*args, **kwargs)
|
|
48
|
+
finally:
|
|
49
|
+
duration = time.time() - start
|
|
50
|
+
logger.debug(f"{name} took {duration:.2f} seconds")
|
|
51
|
+
|
|
52
|
+
return sync_wrapper
|
|
53
|
+
|
|
54
|
+
return Interceptor(api_instance)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ApiGateway:
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
*,
|
|
61
|
+
host: str | None = None,
|
|
62
|
+
username: str | None = None,
|
|
63
|
+
password: str | None = None,
|
|
64
|
+
access_token: str | None = None,
|
|
65
|
+
version: str = "v1",
|
|
66
|
+
):
|
|
67
|
+
if host is None:
|
|
68
|
+
host = config("NOVA_HOST")
|
|
69
|
+
|
|
70
|
+
if username is None:
|
|
71
|
+
username = config("NOVA_USERNAME", default=None)
|
|
72
|
+
|
|
73
|
+
if password is None:
|
|
74
|
+
password = config("NOVA_PASSWORD", default=None)
|
|
75
|
+
|
|
76
|
+
if access_token is None:
|
|
77
|
+
access_token = config("NOVA_ACCESS", default=None)
|
|
78
|
+
|
|
79
|
+
api_client_config = wb.Configuration(
|
|
80
|
+
host=f"http://{host}/api/{version}",
|
|
81
|
+
username=username,
|
|
82
|
+
password=password,
|
|
83
|
+
access_token=access_token,
|
|
84
|
+
ssl_ca_cert=False,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
self._api_client = wb.ApiClient(api_client_config)
|
|
88
|
+
|
|
89
|
+
# Use the intercept function to wrap each API client
|
|
90
|
+
self.controller_api = intercept(wb.ControllerApi(api_client=self._api_client))
|
|
91
|
+
self.motion_group_api = intercept(wb.MotionGroupApi(api_client=self._api_client))
|
|
92
|
+
self.motion_api = intercept(wb.MotionApi(api_client=self._api_client))
|
|
93
|
+
self.motion_group_infos_api = intercept(wb.MotionGroupInfosApi(api_client=self._api_client))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from wandelbots_api_client.models import * # noqa: F401, F403
|
|
2
|
+
from nova.types.pose import Pose
|
|
3
|
+
from nova.types.vector3d import Vector3d
|
|
4
|
+
from nova.types.action import Action, Motion, MotionSettings, lin, spl, ptp, cir, jnt
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Vector3d",
|
|
8
|
+
"Pose",
|
|
9
|
+
"Motion",
|
|
10
|
+
"MotionSettings",
|
|
11
|
+
"lin",
|
|
12
|
+
"spl",
|
|
13
|
+
"ptp",
|
|
14
|
+
"cir",
|
|
15
|
+
"jnt",
|
|
16
|
+
"Action",
|
|
17
|
+
]
|