Skip to content

Commit

Permalink
feat(RPS-1004): handle https, add some convenience methods (#15)
Browse files Browse the repository at this point in the history
Co-authored-by: cbiering <[email protected]>
  • Loading branch information
biering and cbiering authored Dec 23, 2024
1 parent 022245c commit e46cc90
Show file tree
Hide file tree
Showing 12 changed files with 440 additions and 378 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
16 changes: 10 additions & 6 deletions examples/01_basic.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
import asyncio

from nova import Nova


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

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

tcp_names = await motion_group.tcp_names()
print(tcp_names)

tcp = tcp_names[0]

# Current motion group state
state = await motion_group.get_state("Flange")
state = await motion_group.get_state(tcp)
print(state)

# Current joints positions
joints = await motion_group.joints("Flange")
joints = await motion_group.joints()
print(joints)

# Current TCP pose
tcp_pose = await motion_group.tcp_pose("Flange")
tcp_pose = await motion_group.tcp_pose(tcp)
print(tcp_pose)


Expand Down
12 changes: 8 additions & 4 deletions examples/02_plan_and_execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,19 @@
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)

# Connect to the controller and activate motion groups
async with controller[0] as mg:
async with controller[0] as motion_group:
tcp_names = await motion_group.tcp_names()
tcp = tcp_names[0]

# Get current TCP pose and offset it slightly along the x-axis
current_pose = await mg.tcp_pose("Flange")
current_pose = await motion_group.tcp_pose(tcp)
target_pose = current_pose @ Pose((1, 0, 0, 0, 0, 0))

actions = [
Expand All @@ -35,7 +39,7 @@ async def main():
jnt(home_joints),
]

await mg.run(actions, tcp="Flange", movement_controller=move_forward)
await motion_group.run(actions, tcp=tcp, movement_controller=move_forward)


if __name__ == "__main__":
Expand Down
15 changes: 8 additions & 7 deletions examples/03_move_and_set_ios.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,18 @@
async def main():
nova = Nova()
cell = nova.cell()
controller = await cell.controller("ur")
controllers = await cell.controllers()

# Define a home position
home_joints = (0, -pi / 4, -pi / 4, -pi / 4, pi / 4, 0)

# Connect to the controller and activate motion groups
async with controller:
motion_group = controller.motion_group()
async with controllers[0] as motion_group:
tcp_names = await motion_group.tcp_names()
tcp = tcp_names[0]

# Get current TCP pose and offset it slightly along the x-axis
current_pose = await motion_group.tcp_pose("Flange")
current_pose = await motion_group.tcp_pose(tcp)
target_pose = current_pose @ Pose((100, 0, 0, 0, 0, 0))
actions = [
jnt(home_joints),
Expand All @@ -38,9 +39,9 @@ async def main():
def print_motion(motion):
print(motion)

await motion_group.run(actions, tcp="Flange", initial_movement_consumer=print_motion)
await motion_group.run(actions, tcp="Flange")
await motion_group.run(ptp(target_pose), tcp="Flange")
await motion_group.run(actions, tcp=tcp, initial_movement_consumer=print_motion)
await motion_group.run(actions, tcp=tcp)
await motion_group.run(ptp(target_pose), tcp=tcp)


if __name__ == "__main__":
Expand Down
9 changes: 6 additions & 3 deletions examples/04_move_multiple_robots.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@
async def move_robot(controller: Controller):
home_joints = (0, -pi / 4, -pi / 4, -pi / 4, pi / 4, 0)

async with controller[0] as mg:
current_pose = await mg.tcp_pose("Flange")
async with controller[0] as motion_group:
tcp_names = await motion_group.tcp_names()
tcp = tcp_names[0]

current_pose = await motion_group.tcp_pose(tcp)
target_pose = current_pose @ (100, 0, 0, 0, 0, 0)
actions = [jnt(home_joints), ptp(target_pose), jnt(home_joints)]

await mg.run(actions, tcp="Flange", movement_controller=speed_up_movement_controller)
await motion_group.run(actions, tcp=tcp, movement_controller=speed_up_movement_controller)


async def main():
Expand Down
11 changes: 6 additions & 5 deletions examples/05_selection_motion_group_activation.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import asyncio


async def move_robot(motion_group: MotionGroup):
async def move_robot(motion_group: MotionGroup, tcp: str):
home_pose = Pose((200, 200, 600, 0, pi, 0))
target_pose = home_pose @ (100, 0, 0, 0, 0, 0)
actions = [
Expand All @@ -27,16 +27,17 @@ async def move_robot(motion_group: MotionGroup):
ptp(home_pose),
]

await motion_group.run(actions, tcp="Flange")
await motion_group.run(actions, tcp=tcp)


async def main():
nova = Nova()
cell = nova.cell()
ur = await cell.controller("ur")
kuka = await cell.controller("kuka")
tcp = "Flange"

flange_state = await ur[0].get_state("Flange")
flange_state = await ur[0].get_state(tcp=tcp)
print(flange_state)

# activate all motion groups
Expand All @@ -53,13 +54,13 @@ async def main():

# activate motion group 0 from two different controllers
async with ur[0] as ur_0_mg, kuka[0] as kuka_0_mg:
await asyncio.gather(move_robot(ur_0_mg), move_robot(kuka_0_mg))
await asyncio.gather(move_robot(ur_0_mg, tcp), move_robot(kuka_0_mg, tcp))

# activate motion group 0 from two different controllers
mg_0 = ur.motion_group(0)
mg_1 = kuka.motion_group(0)
async with mg_0, mg_1:
await asyncio.gather(move_robot(mg_0), move_robot(mg_1))
await asyncio.gather(move_robot(mg_0, tcp), move_robot(mg_1, tcp))


if __name__ == "__main__":
Expand Down
110 changes: 67 additions & 43 deletions nova/core/motion_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ def __init__(
self._cell = cell
self._motion_group_id = motion_group_id
self._current_motion: str | None = None
self._optimizer_setup: wb.models.OptimizerSetup | None = None
self.is_activated = is_activated

async def __aenter__(self):
Expand All @@ -44,6 +45,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,56 +107,20 @@ 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
)
return len(spec.mechanical_joint_limits)

async def _get_optimizer_setup(self, tcp: str) -> wb.models.OptimizerSetup:
return await self._api_gateway.motion_group_infos_api.get_optimizer_configuration(
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
if self._optimizer_setup is None:
self._optimizer_setup = (
await self._api_gateway.motion_group_infos_api.get_optimizer_configuration(
cell=self._cell, motion_group=self._motion_group_id, tcp=tcp
)
)
return self._optimizer_setup

async def _load_planned_motion(
self, joint_trajectory: wb.models.JointTrajectory, tcp: str
Expand Down Expand Up @@ -176,3 +167,36 @@ 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 = 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) -> wb.models.Joints:
"""Get the current joint positions"""
state = await self.get_state()
return state.state.joint_position

async def tcp_pose(self, tcp: str | None = None) -> Pose:
"""Get the current TCP pose"""
state = await self.get_state(tcp=tcp)
return Pose(state.state.tcp_pose)

async def tcps(self) -> list[wb.models.RobotTcp]:
"""Get the available tool center points (TCPs)"""
response = await self._api_gateway.motion_group_infos_api.list_tcps(
cell=self._cell, motion_group=self.motion_group_id
)
return response.tcps

async def tcp_names(self) -> list[str]:
"""Get the names of the available tool center points (TCPs)"""
return [tcp.id for tcp in await self.tcps()]
22 changes: 16 additions & 6 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", default="cell")) -> "Cell":
return Cell(self._api_client, cell_id)


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

async def _get_controllers(self) -> list[wb.models.ControllerInstance]:
response = await self._api_gateway.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":
controller_api = self._api_gateway.controller_api
controller_list = await controller_api.list_controllers(cell=self._cell_id)
found_controller = next(
(c for c in controller_list.instances if c.host == controller_host), None
)
controllers = await self._get_controllers()
found_controller = next((c for c in controllers if c.host == controller_host), None)

if found_controller is None:
raise ControllerNotFoundException(controller=controller_host)
Expand Down
Loading

0 comments on commit e46cc90

Please sign in to comment.