wandelbots-nova 0.2.2__tar.gz → 0.3.0__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.2.2 → wandelbots_nova-0.3.0}/PKG-INFO +2 -2
- {wandelbots_nova-0.2.2 → wandelbots_nova-0.3.0}/README.md +1 -1
- {wandelbots_nova-0.2.2 → wandelbots_nova-0.3.0}/nova/core/motion_group.py +67 -43
- {wandelbots_nova-0.2.2 → wandelbots_nova-0.3.0}/nova/core/nova.py +16 -6
- {wandelbots_nova-0.2.2 → wandelbots_nova-0.3.0}/nova/gateway.py +15 -3
- {wandelbots_nova-0.2.2 → wandelbots_nova-0.3.0}/pyproject.toml +1 -1
- {wandelbots_nova-0.2.2 → wandelbots_nova-0.3.0}/LICENSE +0 -0
- {wandelbots_nova-0.2.2 → wandelbots_nova-0.3.0}/nova/__init__.py +0 -0
- {wandelbots_nova-0.2.2 → wandelbots_nova-0.3.0}/nova/actions.py +0 -0
- {wandelbots_nova-0.2.2 → wandelbots_nova-0.3.0}/nova/core/__init__.py +0 -0
- {wandelbots_nova-0.2.2 → wandelbots_nova-0.3.0}/nova/core/controller.py +0 -0
- {wandelbots_nova-0.2.2 → wandelbots_nova-0.3.0}/nova/core/exceptions.py +0 -0
- {wandelbots_nova-0.2.2 → wandelbots_nova-0.3.0}/nova/core/movement_controller.py +0 -0
- {wandelbots_nova-0.2.2 → wandelbots_nova-0.3.0}/nova/types/__init__.py +0 -0
- {wandelbots_nova-0.2.2 → wandelbots_nova-0.3.0}/nova/types/collision_scene.py +0 -0
- {wandelbots_nova-0.2.2 → wandelbots_nova-0.3.0}/nova/types/pose.py +0 -0
- {wandelbots_nova-0.2.2 → wandelbots_nova-0.3.0}/nova/types/state.py +0 -0
- {wandelbots_nova-0.2.2 → wandelbots_nova-0.3.0}/nova/types/vector3d.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: wandelbots-nova
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Official Python SDK for the Wandelbots
|
|
5
5
|
Author: Wandelbots GmbH
|
|
6
6
|
Requires-Python: >=3.10,<4.0
|
|
@@ -100,7 +100,7 @@ poetry install
|
|
|
100
100
|
|
|
101
101
|
| Variable | Description | Required | Default | Example |
|
|
102
102
|
|---------------------|---------------------------------------------------------------------------|----------|---------|----------------------------|
|
|
103
|
-
| `
|
|
103
|
+
| `NOVA_API` | The base URL or hostname of the NOVA server instance. | Yes | None | `https://nova.example.com` |
|
|
104
104
|
| `NOVA_USERNAME` | The username credential used for authentication with the NOVA service. | Yes* | None | `my_username` |
|
|
105
105
|
| `NOVA_PASSWORD` | The password credential used in conjunction with `NOVA_USERNAME`. | Yes* | None | `my_password` |
|
|
106
106
|
| `NOVA_ACCESS_TOKEN` | A pre-obtained access token for NOVA if using token-based authentication. | Yes* | None | `eyJhbGciOi...` |
|
|
@@ -79,7 +79,7 @@ poetry install
|
|
|
79
79
|
|
|
80
80
|
| Variable | Description | Required | Default | Example |
|
|
81
81
|
|---------------------|---------------------------------------------------------------------------|----------|---------|----------------------------|
|
|
82
|
-
| `
|
|
82
|
+
| `NOVA_API` | The base URL or hostname of the NOVA server instance. | Yes | None | `https://nova.example.com` |
|
|
83
83
|
| `NOVA_USERNAME` | The username credential used for authentication with the NOVA service. | Yes* | None | `my_username` |
|
|
84
84
|
| `NOVA_PASSWORD` | The password credential used in conjunction with `NOVA_USERNAME`. | Yes* | None | `my_password` |
|
|
85
85
|
| `NOVA_ACCESS_TOKEN` | A pre-obtained access token for NOVA if using token-based authentication. | Yes* | None | `eyJhbGciOi...` |
|
|
@@ -21,6 +21,7 @@ class MotionGroup:
|
|
|
21
21
|
self._cell = cell
|
|
22
22
|
self._motion_group_id = motion_group_id
|
|
23
23
|
self._current_motion: str | None = None
|
|
24
|
+
self._optimizer_setup: wb.models.OptimizerSetup | None = None
|
|
24
25
|
self.is_activated = is_activated
|
|
25
26
|
|
|
26
27
|
async def __aenter__(self):
|
|
@@ -44,6 +45,32 @@ class MotionGroup:
|
|
|
44
45
|
# raise ValueError("No MotionId attached. There is no planned motion available.")
|
|
45
46
|
return self._current_motion
|
|
46
47
|
|
|
48
|
+
async def plan(self, actions: list[Action], tcp: str) -> wb.models.JointTrajectory:
|
|
49
|
+
current_joints = await self.joints(tcp=tcp)
|
|
50
|
+
robot_setup = await self._get_optimizer_setup(tcp=tcp)
|
|
51
|
+
motion_commands = CombinedActions(items=actions).to_motion_command()
|
|
52
|
+
|
|
53
|
+
request = wb.models.PlanTrajectoryRequest(
|
|
54
|
+
robot_setup=robot_setup,
|
|
55
|
+
motion_group=self.motion_group_id,
|
|
56
|
+
start_joint_position=current_joints.joints,
|
|
57
|
+
motion_commands=motion_commands,
|
|
58
|
+
tcp=tcp,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
motion_api_client = self._api_gateway.motion_api
|
|
62
|
+
plan_response = await motion_api_client.plan_trajectory(
|
|
63
|
+
cell=self._cell, plan_trajectory_request=request
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if isinstance(
|
|
67
|
+
plan_response.response.actual_instance, wb.models.PlanTrajectoryFailedResponse
|
|
68
|
+
):
|
|
69
|
+
failed_response = plan_response.response.actual_instance
|
|
70
|
+
raise PlanTrajectoryFailed(failed_response)
|
|
71
|
+
|
|
72
|
+
return plan_response.response.actual_instance
|
|
73
|
+
|
|
47
74
|
async def run(
|
|
48
75
|
self,
|
|
49
76
|
actions: list[Action] | Action,
|
|
@@ -80,20 +107,6 @@ class MotionGroup:
|
|
|
80
107
|
_movement_controller = movement_controller(movement_controller_context)
|
|
81
108
|
await self._api_gateway.motion_api.execute_trajectory(self._cell, _movement_controller)
|
|
82
109
|
|
|
83
|
-
async def get_state(self, tcp: str) -> wb.models.MotionGroupStateResponse:
|
|
84
|
-
response = await self._api_gateway.motion_group_infos_api.get_current_motion_group_state(
|
|
85
|
-
cell=self._cell, motion_group=self.motion_group_id, tcp=tcp
|
|
86
|
-
)
|
|
87
|
-
return response
|
|
88
|
-
|
|
89
|
-
async def joints(self, tcp: str) -> wb.models.Joints:
|
|
90
|
-
state = await self.get_state(tcp=tcp)
|
|
91
|
-
return state.state.joint_position
|
|
92
|
-
|
|
93
|
-
async def tcp_pose(self, tcp: str) -> Pose:
|
|
94
|
-
state = await self.get_state(tcp=tcp)
|
|
95
|
-
return Pose(state.state.tcp_pose)
|
|
96
|
-
|
|
97
110
|
async def _get_number_of_joints(self) -> int:
|
|
98
111
|
spec = await self._api_gateway.motion_group_infos_api.get_motion_group_specification(
|
|
99
112
|
cell=self._cell, motion_group=self.motion_group_id
|
|
@@ -101,35 +114,13 @@ class MotionGroup:
|
|
|
101
114
|
return len(spec.mechanical_joint_limits)
|
|
102
115
|
|
|
103
116
|
async def _get_optimizer_setup(self, tcp: str) -> wb.models.OptimizerSetup:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
motion_commands = CombinedActions(items=actions).to_motion_command()
|
|
112
|
-
|
|
113
|
-
request = wb.models.PlanTrajectoryRequest(
|
|
114
|
-
robot_setup=robot_setup,
|
|
115
|
-
motion_group=self.motion_group_id,
|
|
116
|
-
start_joint_position=current_joints.joints,
|
|
117
|
-
motion_commands=motion_commands,
|
|
118
|
-
tcp=tcp,
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
motion_api_client = self._api_gateway.motion_api
|
|
122
|
-
plan_response = await motion_api_client.plan_trajectory(
|
|
123
|
-
cell=self._cell, plan_trajectory_request=request
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
if isinstance(
|
|
127
|
-
plan_response.response.actual_instance, wb.models.PlanTrajectoryFailedResponse
|
|
128
|
-
):
|
|
129
|
-
failed_response = plan_response.response.actual_instance
|
|
130
|
-
raise PlanTrajectoryFailed(failed_response)
|
|
131
|
-
|
|
132
|
-
return plan_response.response.actual_instance
|
|
117
|
+
if self._optimizer_setup is None:
|
|
118
|
+
self._optimizer_setup = (
|
|
119
|
+
await self._api_gateway.motion_group_infos_api.get_optimizer_configuration(
|
|
120
|
+
cell=self._cell, motion_group=self._motion_group_id, tcp=tcp
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
return self._optimizer_setup
|
|
133
124
|
|
|
134
125
|
async def _load_planned_motion(
|
|
135
126
|
self, joint_trajectory: wb.models.JointTrajectory, tcp: str
|
|
@@ -176,3 +167,36 @@ class MotionGroup:
|
|
|
176
167
|
logger.debug(f"Motion {self.current_motion} stopped.")
|
|
177
168
|
except ValueError as e:
|
|
178
169
|
logger.debug(f"No motion to stop for {self}: {e}")
|
|
170
|
+
|
|
171
|
+
async def get_state(self, tcp: str | None = None) -> wb.models.MotionGroupStateResponse:
|
|
172
|
+
"""Get the current state of the motion group
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
tcp (str): The identifier of the tool center point (TCP) to be used for tcp_pose in response. If not set,
|
|
176
|
+
the flange pose is returned as tcp_pose.
|
|
177
|
+
"""
|
|
178
|
+
response = await self._api_gateway.motion_group_infos_api.get_current_motion_group_state(
|
|
179
|
+
cell=self._cell, motion_group=self.motion_group_id, tcp=tcp
|
|
180
|
+
)
|
|
181
|
+
return response
|
|
182
|
+
|
|
183
|
+
async def joints(self) -> wb.models.Joints:
|
|
184
|
+
"""Get the current joint positions"""
|
|
185
|
+
state = await self.get_state()
|
|
186
|
+
return state.state.joint_position
|
|
187
|
+
|
|
188
|
+
async def tcp_pose(self, tcp: str | None = None) -> Pose:
|
|
189
|
+
"""Get the current TCP pose"""
|
|
190
|
+
state = await self.get_state(tcp=tcp)
|
|
191
|
+
return Pose(state.state.tcp_pose)
|
|
192
|
+
|
|
193
|
+
async def tcps(self) -> list[wb.models.RobotTcp]:
|
|
194
|
+
"""Get the available tool center points (TCPs)"""
|
|
195
|
+
response = await self._api_gateway.motion_group_infos_api.list_tcps(
|
|
196
|
+
cell=self._cell, motion_group=self.motion_group_id
|
|
197
|
+
)
|
|
198
|
+
return response.tcps
|
|
199
|
+
|
|
200
|
+
async def tcp_names(self) -> list[str]:
|
|
201
|
+
"""Get the names of the available tool center points (TCPs)"""
|
|
202
|
+
return [tcp.id for tcp in await self.tcps()]
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from nova.core.controller import Controller
|
|
2
2
|
from nova.core.exceptions import ControllerNotFoundException
|
|
3
3
|
from nova.gateway import ApiGateway
|
|
4
|
+
import wandelbots_api_client as wb
|
|
5
|
+
from decouple import config
|
|
4
6
|
|
|
5
7
|
|
|
6
8
|
class Nova:
|
|
@@ -21,7 +23,7 @@ class Nova:
|
|
|
21
23
|
version=version,
|
|
22
24
|
)
|
|
23
25
|
|
|
24
|
-
def cell(self, cell_id: str = "cell") -> "Cell":
|
|
26
|
+
def cell(self, cell_id: str = config("CELL_NAME", default="cell")) -> "Cell":
|
|
25
27
|
return Cell(self._api_client, cell_id)
|
|
26
28
|
|
|
27
29
|
|
|
@@ -30,12 +32,20 @@ class Cell:
|
|
|
30
32
|
self._api_gateway = api_gateway
|
|
31
33
|
self._cell_id = cell_id
|
|
32
34
|
|
|
35
|
+
async def _get_controllers(self) -> list[wb.models.ControllerInstance]:
|
|
36
|
+
response = await self._api_gateway.controller_api.list_controllers(cell=self._cell_id)
|
|
37
|
+
return response.instances
|
|
38
|
+
|
|
39
|
+
async def controllers(self) -> list["Controller"]:
|
|
40
|
+
controllers = await self._get_controllers()
|
|
41
|
+
return [
|
|
42
|
+
Controller(api_gateway=self._api_gateway, cell=self._cell_id, controller_host=c.host)
|
|
43
|
+
for c in controllers
|
|
44
|
+
]
|
|
45
|
+
|
|
33
46
|
async def controller(self, controller_host: str = None) -> "Controller":
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
found_controller = next(
|
|
37
|
-
(c for c in controller_list.instances if c.host == controller_host), None
|
|
38
|
-
)
|
|
47
|
+
controllers = await self._get_controllers()
|
|
48
|
+
found_controller = next((c for c in controllers if c.host == controller_host), None)
|
|
39
49
|
|
|
40
50
|
if found_controller is None:
|
|
41
51
|
raise ControllerNotFoundException(controller=controller_host)
|
|
@@ -9,6 +9,8 @@ from decouple import config
|
|
|
9
9
|
|
|
10
10
|
T = TypeVar("T")
|
|
11
11
|
|
|
12
|
+
INTERNAL_CLUSTER_NOVA_API = "http://api-gateway.wandelbots.svc.cluster.local:8080"
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
def intercept(api_instance: T) -> T:
|
|
14
16
|
class Interceptor:
|
|
@@ -65,7 +67,7 @@ class ApiGateway:
|
|
|
65
67
|
version: str = "v1",
|
|
66
68
|
):
|
|
67
69
|
if host is None:
|
|
68
|
-
host = config("
|
|
70
|
+
host = config("NOVA_API", default=INTERNAL_CLUSTER_NOVA_API)
|
|
69
71
|
|
|
70
72
|
if username is None:
|
|
71
73
|
username = config("NOVA_USERNAME", default=None)
|
|
@@ -76,15 +78,25 @@ class ApiGateway:
|
|
|
76
78
|
if access_token is None:
|
|
77
79
|
access_token = config("NOVA_ACCESS_TOKEN", default=None)
|
|
78
80
|
|
|
81
|
+
if (username is None or password is None) and access_token is None:
|
|
82
|
+
raise ValueError("Please provide either username and password or an access token")
|
|
83
|
+
|
|
84
|
+
# Access token has more prio than username and password if both are provided at the same time, set username and
|
|
85
|
+
# password to None
|
|
86
|
+
if access_token is not None:
|
|
87
|
+
username = None
|
|
88
|
+
password = None
|
|
89
|
+
|
|
90
|
+
stripped_host = host.rstrip("/")
|
|
79
91
|
api_client_config = wb.Configuration(
|
|
80
|
-
host=f"
|
|
92
|
+
host=f"{stripped_host}/api/{version}",
|
|
81
93
|
username=username,
|
|
82
94
|
password=password,
|
|
83
95
|
access_token=access_token,
|
|
84
|
-
ssl_ca_cert=False,
|
|
85
96
|
)
|
|
86
97
|
|
|
87
98
|
self._api_client = wb.ApiClient(api_client_config)
|
|
99
|
+
self._host = host
|
|
88
100
|
|
|
89
101
|
# Use the intercept function to wrap each API client
|
|
90
102
|
self.controller_api = intercept(wb.ControllerApi(api_client=self._api_client))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|