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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wandelbots-nova
3
- Version: 0.2.2
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
- | `NOVA_HOST` | The base URL or hostname of the NOVA server instance. | Yes | None | `https://nova.example.com` |
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
- | `NOVA_HOST` | The base URL or hostname of the NOVA server instance. | Yes | None | `https://nova.example.com` |
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
- return await self._api_gateway.motion_group_infos_api.get_optimizer_configuration(
105
- cell=self._cell, motion_group=self._motion_group_id, tcp=tcp
106
- )
107
-
108
- async def plan(self, actions: list[Action], tcp: str) -> wb.models.JointTrajectory:
109
- current_joints = await self.joints(tcp=tcp)
110
- robot_setup = await self._get_optimizer_setup(tcp=tcp)
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
- controller_api = self._api_gateway.controller_api
35
- controller_list = await controller_api.list_controllers(cell=self._cell_id)
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("NOVA_HOST")
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"http://{host}/api/{version}",
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))
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "wandelbots-nova"
3
- version = "0.2.2"
3
+ version = "0.3.0"
4
4
  description = "Official Python SDK for the Wandelbots"
5
5
  authors = [
6
6
  "Wandelbots GmbH",
File without changes