Skip to content

Commit

Permalink
feat: handle https, add some convenience methods
Browse files Browse the repository at this point in the history
  • Loading branch information
cbiering committed Dec 20, 2024
1 parent 022245c commit e13d600
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 57 deletions.
7 changes: 5 additions & 2 deletions .env.template
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# The endpoint where the API is reachable from the container serving the Application.
NOVA_API="http://api-gateway.wandelbots.svc.cluster.local:8080"
# The name of the cell to be used for the Application.
CELL_NAME="cell"

# Using username/password authentication
NOVA_HOST="https://nova.example.com"
NOVA_USERNAME="my_username"
NOVA_PASSWORD="my_password"
# NOVA_ACCESS_TOKEN="" # Leave empty if not using token-based authentication

# Using token-based authentication (comment out username/password lines)
# NOVA_HOST="https://nova.example.com"
# NOVA_ACCESS_TOKEN="eyJhbGciOi..."
4 changes: 2 additions & 2 deletions .github/workflows/ci-dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ jobs:
poetry install
mkdir envs
touch envs/.env.tests
echo "NOVA_HOST=test.instance.mock.io" > envs/.env.tests
echo "CELL_ID=cell" >> envs/.env.tests
echo "NOVA_API=test.instance.mock.io" > envs/.env.tests
echo "CELL_NAME=cell" >> envs/.env.tests
echo "MOTION_GROUP=virtual-robot" >> envs/.env.tests
echo "TCP=Flange" >> envs/.env.tests
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ poetry install

| Variable | Description | Required | Default | Example |
|---------------------|---------------------------------------------------------------------------|----------|---------|----------------------------|
| `NOVA_HOST` | The base URL or hostname of the NOVA server instance. | Yes | None | `https://nova.example.com` |
| `NOVA_API` | The base URL or hostname of the NOVA server instance. | Yes | None | `https://nova.example.com` |
| `NOVA_USERNAME` | The username credential used for authentication with the NOVA service. | Yes* | None | `my_username` |
| `NOVA_PASSWORD` | The password credential used in conjunction with `NOVA_USERNAME`. | Yes* | None | `my_password` |
| `NOVA_ACCESS_TOKEN` | A pre-obtained access token for NOVA if using token-based authentication. | Yes* | None | `eyJhbGciOi...` |
Expand Down
6 changes: 3 additions & 3 deletions examples/01_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@


async def main():
nova = Nova()
nova = Nova(host="172.30.0.135")
cell = nova.cell()
controller = await cell.controller("ur")

controllers = await cell.controllers()
controller = controllers[0]
motion_group = controller[0]

# Current motion group state
Expand Down
3 changes: 2 additions & 1 deletion examples/02_plan_and_execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
async def main():
nova = Nova()
cell = nova.cell()
controller = await cell.controller("ur")
controllers = await cell.controllers()
controller = controllers[0]

# Define a home position
home_joints = (0, -pi / 4, -pi / 4, -pi / 4, pi / 4, 0)
Expand Down
3 changes: 2 additions & 1 deletion examples/03_move_and_set_ios.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
async def main():
nova = Nova()
cell = nova.cell()
controller = await cell.controller("ur")
controllers = await cell.controllers()
controller = controllers[0]

# Define a home position
home_joints = (0, -pi / 4, -pi / 4, -pi / 4, pi / 4, 0)
Expand Down
92 changes: 52 additions & 40 deletions nova/core/motion_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,32 @@ def current_motion(self) -> str:
# raise ValueError("No MotionId attached. There is no planned motion available.")
return self._current_motion

async def plan(self, actions: list[Action], tcp: str) -> wb.models.JointTrajectory:
current_joints = await self.joints(tcp=tcp)
robot_setup = await self._get_optimizer_setup(tcp=tcp)
motion_commands = CombinedActions(items=actions).to_motion_command()

request = wb.models.PlanTrajectoryRequest(
robot_setup=robot_setup,
motion_group=self.motion_group_id,
start_joint_position=current_joints.joints,
motion_commands=motion_commands,
tcp=tcp,
)

motion_api_client = self._api_gateway.motion_api
plan_response = await motion_api_client.plan_trajectory(
cell=self._cell, plan_trajectory_request=request
)

if isinstance(
plan_response.response.actual_instance, wb.models.PlanTrajectoryFailedResponse
):
failed_response = plan_response.response.actual_instance
raise PlanTrajectoryFailed(failed_response)

return plan_response.response.actual_instance

async def run(
self,
actions: list[Action] | Action,
Expand Down Expand Up @@ -80,20 +106,6 @@ async def run(
_movement_controller = movement_controller(movement_controller_context)
await self._api_gateway.motion_api.execute_trajectory(self._cell, _movement_controller)

async def get_state(self, tcp: str) -> wb.models.MotionGroupStateResponse:
response = await self._api_gateway.motion_group_infos_api.get_current_motion_group_state(
cell=self._cell, motion_group=self.motion_group_id, tcp=tcp
)
return response

async def joints(self, tcp: str) -> wb.models.Joints:
state = await self.get_state(tcp=tcp)
return state.state.joint_position

async def tcp_pose(self, tcp: str) -> Pose:
state = await self.get_state(tcp=tcp)
return Pose(state.state.tcp_pose)

async def _get_number_of_joints(self) -> int:
spec = await self._api_gateway.motion_group_infos_api.get_motion_group_specification(
cell=self._cell, motion_group=self.motion_group_id
Expand All @@ -105,32 +117,6 @@ async def _get_optimizer_setup(self, tcp: str) -> wb.models.OptimizerSetup:
cell=self._cell, motion_group=self._motion_group_id, tcp=tcp
)

async def plan(self, actions: list[Action], tcp: str) -> wb.models.JointTrajectory:
current_joints = await self.joints(tcp=tcp)
robot_setup = await self._get_optimizer_setup(tcp=tcp)
motion_commands = CombinedActions(items=actions).to_motion_command()

request = wb.models.PlanTrajectoryRequest(
robot_setup=robot_setup,
motion_group=self.motion_group_id,
start_joint_position=current_joints.joints,
motion_commands=motion_commands,
tcp=tcp,
)

motion_api_client = self._api_gateway.motion_api
plan_response = await motion_api_client.plan_trajectory(
cell=self._cell, plan_trajectory_request=request
)

if isinstance(
plan_response.response.actual_instance, wb.models.PlanTrajectoryFailedResponse
):
failed_response = plan_response.response.actual_instance
raise PlanTrajectoryFailed(failed_response)

return plan_response.response.actual_instance

async def _load_planned_motion(
self, joint_trajectory: wb.models.JointTrajectory, tcp: str
) -> wb.models.PlanSuccessfulResponse:
Expand Down Expand Up @@ -176,3 +162,29 @@ async def stop(self):
logger.debug(f"Motion {self.current_motion} stopped.")
except ValueError as e:
logger.debug(f"No motion to stop for {self}: {e}")

async def get_state(self, tcp: str | None) -> wb.models.MotionGroupStateResponse:
"""Get the current state of the motion group
Args:
tcp (str): The identifier of the tool center point (TCP) to be used for tcp_pose in response. If not set,
the flange pose is returned as tcp_pose.
"""
response = await self._api_gateway.motion_group_infos_api.get_current_motion_group_state(
cell=self._cell, motion_group=self.motion_group_id, tcp=tcp
)
return response

async def joints(self, tcp: str) -> wb.models.Joints:
state = await self.get_state(tcp=tcp)
return state.state.joint_position

async def tcp_pose(self, tcp: str) -> Pose:
state = await self.get_state(tcp=tcp)
return Pose(state.state.tcp_pose)

async def tcps(self) -> list[wb.models.RobotTcp]:
return (await self._api_gateway.motion_group_infos_api.list_tcps()).tcps

async def tcp_names(self) -> list[str]:
return [tcp.id for tcp in await self.tcps()]
21 changes: 17 additions & 4 deletions nova/core/nova.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from nova.core.controller import Controller
from nova.core.exceptions import ControllerNotFoundException
from nova.gateway import ApiGateway
import wandelbots_api_client as wb
from decouple import config


class Nova:
Expand All @@ -21,7 +23,7 @@ def __init__(
version=version,
)

def cell(self, cell_id: str = "cell") -> "Cell":
def cell(self, cell_id: str = config("CELL_NAME", "cell")) -> "Cell":
return Cell(self._api_client, cell_id)


Expand All @@ -30,11 +32,22 @@ def __init__(self, api_gateway: ApiGateway, cell_id: str):
self._api_gateway = api_gateway
self._cell_id = cell_id

async def controller(self, controller_host: str = None) -> "Controller":
async def _get_controllers(self) -> list[wb.models.ControllerInstance]:
controller_api = self._api_gateway.controller_api
controller_list = await controller_api.list_controllers(cell=self._cell_id)
response = await controller_api.list_controllers(cell=self._cell_id)
return response.instances

async def controllers(self) -> list["Controller"]:
controllers = await self._get_controllers()
return [
Controller(api_gateway=self._api_gateway, cell=self._cell_id, controller_host=c.host)
for c in controllers
]

async def controller(self, controller_host: str = None) -> "Controller":
controllers = await self._get_controllers()
found_controller = next(
(c for c in controller_list.instances if c.host == controller_host), None
(c for c in controllers if c.host == controller_host), None
)

if found_controller is None:
Expand Down
15 changes: 12 additions & 3 deletions nova/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,23 @@ def sync_wrapper(*args, **kwargs):
return Interceptor(api_instance)


def _validate_host(host: str) -> str:
"""Remove any trailing slashes and validate scheme"""
_url = host.rstrip("/")
return _url

class ApiGateway:
def __init__(
self,
*,
host: str | None = None,
host: str = "http://api-gateway.wandelbots.svc.cluster.local:8080",
username: str | None = None,
password: str | None = None,
access_token: str | None = None,
version: str = "v1",
):
if host is None:
host = config("NOVA_HOST")
host = config("NOVA_API")

if username is None:
username = config("NOVA_USERNAME", default=None)
Expand All @@ -76,8 +81,12 @@ def __init__(
if access_token is None:
access_token = config("NOVA_ACCESS_TOKEN", default=None)

if (username is None or password is None) and access_token is None:
raise ValueError("Please provide either username and password or an access token")

stripped_host = host.rstrip("/")
api_client_config = wb.Configuration(
host=f"http://{host}/api/{version}",
host=f"{stripped_host}/api/{version}",
username=username,
password=password,
access_token=access_token,
Expand Down

0 comments on commit e13d600

Please sign in to comment.