Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: handle https, add some convenience methods #15

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
biering marked this conversation as resolved.
Show resolved Hide resolved
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()
biering marked this conversation as resolved.
Show resolved Hide resolved
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
99 changes: 59 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be cached?

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,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
Loading