Skip to content

Commit 8cb8cf2

Browse files
committed
feat(nova-cli): adopt to nova cli usage together with robotpad
1 parent 6b10bee commit 8cb8cf2

File tree

12 files changed

+370
-177
lines changed

12 files changed

+370
-177
lines changed

.dockerignore

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# General
2+
.DS_Store
3+
.git
4+
.gitignore
5+
.dockerignore
6+
__pycache__/
7+
*.pyc
8+
*.pyo
9+
*.pyd
10+
.Python
11+
env/
12+
venv/
13+
.venv/
14+
pip-log.txt
15+
pip-delete-this-directory.txt
16+
17+
# Project-specific
18+
README.md
19+
docs/
20+
tests/
21+
*.yml
22+
Dockerfile
23+
*.dockerfile
24+
*.env
25+
.idea/
26+
.vscode/
27+
*.log

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.env

Dockerfile

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# https://medium.com/@albertazzir/blazing-fast-python-docker-builds-with-poetry-a78a66f5aed0
2+
FROM python:3.9-buster as builder
3+
4+
RUN pip install --upgrade pip \
5+
&& pip install poetry==1.8.3
6+
7+
ENV POETRY_NO_INTERACTION=1 \
8+
POETRY_VIRTUALENVS_IN_PROJECT=1 \
9+
POETRY_VIRTUALENVS_CREATE=1 \
10+
POETRY_CACHE_DIR=/tmp/poetry_cache
11+
12+
WORKDIR /app
13+
14+
COPY pyproject.toml poetry.lock ./
15+
16+
RUN --mount=type=cache,target=$POETRY_CACHE_DIR poetry install --without dev --no-root
17+
18+
FROM python:3.9-slim-buster as runtime
19+
20+
ENV VIRTUAL_ENV=/app/.venv \
21+
PATH="/app/.venv/bin:$PATH"
22+
23+
COPY --from=builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
24+
WORKDIR /app
25+
COPY . .
26+
27+
ENTRYPOINT ["python", "-m", "gamepad_example"]

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
Small python project to demonstrate the usage of a gamepad
44

5-
## Usage
5+
## Local Usage
66
```
77
poetry install
88
poetry shell
9-
python src/move_robot.py
9+
python gamepad_example/move_robot.py
1010
```

gamepad_example/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import uvicorn
2+
from loguru import logger
3+
import os
4+
5+
from gamepad_example.app import app
6+
7+
8+
def main(host: str = "0.0.0.0", port: int = 3000):
9+
log_level = os.getenv('LOG_LEVEL', 'info')
10+
logger.info("Starting Service...")
11+
12+
# base path is injected/set when running within the wandelbots environment
13+
base_path = os.getenv('BASE_PATH', '')
14+
if len(base_path) > 0:
15+
logger.info("serving with base path '{}'", base_path)
16+
uvicorn.run(app, host=host, port=port, reload=False, log_level=log_level, proxy_headers=True, forwarded_allow_ips='*')

gamepad_example/__main__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from gamepad_example import main
2+
3+
if __name__ == "__main__":
4+
main()

gamepad_example/app.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import asyncio
2+
import copy
3+
from typing import List
4+
5+
import wandelbots_api_client as wb
6+
from decouple import config
7+
from fastapi import FastAPI, HTTPException, BackgroundTasks
8+
from fastapi.middleware.cors import CORSMiddleware
9+
from fastapi.responses import FileResponse
10+
from fastapi.responses import RedirectResponse
11+
from loguru import logger
12+
13+
from gamepad_example.move_robot import move_robot, stop_movement
14+
from gamepad_example.utils import CELL_ID
15+
from gamepad_example.utils import get_api_client
16+
from inputs import get_gamepad
17+
18+
BASE_PATH = config("BASE_PATH", default="", cast=str)
19+
app = FastAPI(title="gamepad_example", root_path=BASE_PATH)
20+
21+
app.add_middleware(
22+
CORSMiddleware,
23+
allow_origins=["*"],
24+
allow_credentials=True,
25+
allow_methods=["*"],
26+
allow_headers=["*"],
27+
)
28+
29+
30+
@app.get("/", summary="Redirects to the swagger docs page")
31+
async def root():
32+
# One could serve a nice UI here as well. For simplicity, we just redirect to the swagger docs page.
33+
return RedirectResponse(url=BASE_PATH + "/docs")
34+
35+
36+
@app.get("/app_icon.png", summary="Services the app icon for the homescreen")
37+
async def get_app_icon():
38+
try:
39+
return FileResponse(path="static/app_icon.png", media_type='image/png')
40+
except FileNotFoundError:
41+
raise HTTPException(status_code=404, detail="Icon not found")
42+
43+
44+
@app.post(
45+
"/activate_jogging",
46+
status_code=201,
47+
summary="Moves the robot via jogging with Gamepad input",
48+
description="oves the robot via jogging with Gamepad input")
49+
async def activate_jogging(background_tasks: BackgroundTasks):
50+
logger.info("creating api clients...")
51+
api_client = get_api_client()
52+
motion_group_api = wb.MotionGroupApi(api_client)
53+
controller_api = wb.ControllerApi(api_client)
54+
55+
# get the motion group id
56+
controllers = await controller_api.list_controllers(cell=CELL_ID)
57+
58+
if len(controllers.instances) != 1:
59+
raise HTTPException(
60+
status_code=400,
61+
detail="No or more than one controllers found. Example just works with one controllers. "
62+
"Go to settings app and create one or delete all except one.")
63+
controller_id = controllers.instances[0].controller
64+
logger.info("using controller {}", controller_id)
65+
66+
logger.info("selecting motion group...")
67+
motion_groups = await motion_group_api.list_motion_groups(cell=CELL_ID)
68+
if len(motion_groups.instances) != 1:
69+
raise HTTPException(
70+
status_code=400,
71+
detail="No or more than one motion group found. Example just works with one motion group. "
72+
"Go to settings app and create one or delete all except one.")
73+
motion_group_id = motion_groups.instances[0].motion_group
74+
logger.info("using motion group {}", motion_group_id)
75+
76+
logger.info("activating motion groups...")
77+
await motion_group_api.activate_motion_group(
78+
cell=CELL_ID,
79+
motion_group=motion_group_id)
80+
await controller_api.set_default_mode(cell=CELL_ID, controller=controller_id, mode="MODE_CONTROL")
81+
82+
background_tasks.add_task(move_robot, motion_group_id)
83+
api_client.close()
84+
return {"status": "jogging activated"}
85+
86+
87+
@app.post(
88+
"/deactivate_jogging",
89+
status_code=201,
90+
summary="Stops the robot jogging",
91+
description="stops the robot jogging")
92+
async def deactivate_jogging():
93+
stop_movement()
94+
return {"status": "jogging deactivated"}

gamepad_example/move_robot.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import asyncio
2+
from inputs import get_gamepad
3+
from loguru import logger
4+
5+
import wandelbots_api_client as wb
6+
7+
from gamepad_example.utils import CELL_ID
8+
from gamepad_example.utils import get_api_client
9+
10+
shall_run = False
11+
max_position_velocity = 100
12+
max_rotation_velocity = 0.5
13+
14+
gamepad_position_direction = wb.models.Vector3d(x=0, y=0, z=0)
15+
gamepad_rotation_direction = wb.models.Vector3d(x=0, y=0, z=0)
16+
gamepad_position_velocity = 0
17+
gamepad_rotation_velocity = 0
18+
gamepad_updated = False
19+
20+
21+
async def read_gamepad():
22+
global gamepad_updated
23+
global gamepad_position_direction, gamepad_position_velocity, max_position_velocity
24+
global gamepad_rotation_direction, gamepad_rotation_velocity, max_rotation_velocity
25+
global shall_run
26+
27+
while True:
28+
if not shall_run:
29+
return
30+
latest_gamepad_events = get_gamepad()
31+
for event in latest_gamepad_events:
32+
if event.ev_type == "Absolute" and event.code == "ABS_X":
33+
gamepad_position_direction.x = event.state / 32767
34+
elif event.ev_type == "Absolute" and event.code == "ABS_Y":
35+
gamepad_position_direction.y = event.state / 32767
36+
elif event.ev_type == "Absolute" and event.code == "ABS_Z":
37+
gamepad_position_direction.z = - event.state / 255
38+
elif event.ev_type == "Absolute" and event.code == "ABS_RZ":
39+
gamepad_position_direction.z = event.state / 255
40+
41+
elif event.ev_type == "Absolute" and event.code == "ABS_RX":
42+
gamepad_rotation_direction.y = event.state / 32767
43+
elif event.ev_type == "Absolute" and event.code == "ABS_RY":
44+
gamepad_rotation_direction.x = -event.state / 32767
45+
elif event.code == "BTN_TL":
46+
gamepad_rotation_direction.z = -event.state
47+
elif event.code == "BTN_TR":
48+
gamepad_rotation_direction.z = event.state
49+
logger.info("read event...")
50+
gamepad_updated = True
51+
52+
gamepad_position_velocity = (
53+
gamepad_position_direction.x ** 2 + gamepad_position_direction.y ** 2 + gamepad_position_direction.z ** 2) ** 0.5
54+
gamepad_position_velocity = gamepad_position_velocity / 1.732 * max_position_velocity
55+
gamepad_rotation_velocity = (
56+
gamepad_rotation_direction.x ** 2 + gamepad_rotation_direction.y ** 2 + gamepad_rotation_direction.z ** 2) ** 0.5
57+
gamepad_rotation_velocity = gamepad_rotation_velocity / 1.732 * max_rotation_velocity
58+
59+
await asyncio.sleep(0) # yield control to other tasks
60+
61+
62+
async def jogging_direction_generator(motion_group_id, responses_from_robot):
63+
async def read_responses(responses_from_robot):
64+
async for response in responses_from_robot:
65+
pass
66+
# print(response)
67+
68+
asyncio.create_task(read_responses(responses_from_robot))
69+
70+
global gamepad_position_direction, gamepad_position_velocity
71+
global gamepad_rotation_direction, gamepad_rotation_velocity
72+
global gamepad_updated
73+
global shall_run
74+
direction_request = wb.models.DirectionJoggingRequest(
75+
motion_group=motion_group_id,
76+
position_direction=wb.models.Vector3d(x=0, y=0, z=0),
77+
rotation_direction=wb.models.Vector3d(x=0, y=0, z=0),
78+
position_velocity=0,
79+
rotation_velocity=0,
80+
response_rate=1000
81+
)
82+
83+
yield direction_request
84+
while True:
85+
if not shall_run:
86+
return
87+
88+
if gamepad_updated:
89+
direction_request.position_direction = gamepad_position_direction
90+
direction_request.rotation_direction = gamepad_rotation_direction
91+
direction_request.position_velocity = gamepad_position_velocity
92+
direction_request.rotation_velocity = gamepad_rotation_velocity
93+
gamepad_updated = False
94+
yield direction_request
95+
await asyncio.sleep(0) # yield control to other tasks
96+
97+
98+
async def move_robot(motion_group_id):
99+
global shall_run
100+
101+
shall_run = True
102+
api_client = get_api_client()
103+
jogging_api = wb.MotionGroupJoggingApi(api_client)
104+
105+
# start the reading from gamepad
106+
asyncio.create_task(read_gamepad())
107+
108+
# start the robot interaction
109+
await jogging_api.direction_jogging(
110+
cell=CELL_ID,
111+
client_request_generator=lambda response_stream: jogging_direction_generator(
112+
motion_group_id, response_stream
113+
))
114+
api_client.close()
115+
116+
117+
def stop_movement():
118+
global shall_run
119+
shall_run = False

gamepad_example/utils.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import wandelbots_api_client as wb
2+
import requests
3+
from decouple import config
4+
from requests.auth import HTTPBasicAuth
5+
6+
CELL_ID = config("CELL_ID", default="cell", cast=str)
7+
8+
9+
def get_api_client() -> wb.ApiClient:
10+
"""Creates a new API client for the wandelbots API.
11+
"""
12+
# if there is basic auth required (e.g. when running this service locally)
13+
basic_user = config("NOVA_USERNAME", default=None, cast=str)
14+
basic_pwd = config("NOVA_PASSWORD", default=None, cast=str)
15+
16+
base_url = get_base_url(basic_user, basic_pwd)
17+
18+
client_config = wb.Configuration(host=base_url)
19+
client_config.verify_ssl = False
20+
21+
if basic_user is not None and basic_pwd is not None:
22+
client_config.username = basic_user
23+
client_config.password = basic_pwd
24+
25+
return wb.ApiClient(client_config)
26+
27+
28+
def get_base_url(basic_user, basic_pwd) -> str:
29+
# in-cluster it is the api-gateway service
30+
# when working with a remote instance one needs to provide the host via env variable
31+
api_host = config("WANDELAPI_BASE_URL", default="api-gateway:8080", cast=str)
32+
api_host = api_host.strip()
33+
api_host = api_host.replace("http://", "")
34+
api_host = api_host.replace("https://", "")
35+
api_host = api_host.rstrip("/")
36+
api_base_path = "/api/v1"
37+
protocol = get_protocol(api_host, basic_user, basic_pwd)
38+
if protocol is None:
39+
msg = f"Could not determine protocol for host {api_host}. Make sure the host is reachable."
40+
raise Exception(msg)
41+
return f"{protocol}{api_host}{api_base_path}"
42+
43+
44+
# get the protocol of the host (http or https)
45+
def get_protocol(host, basic_user, basic_pwd) -> str:
46+
api = f"/api/v1/cells/{CELL_ID}/controllers"
47+
auth = None
48+
if basic_user is not None and basic_pwd is not None:
49+
auth = HTTPBasicAuth(basic_user, basic_pwd)
50+
51+
try:
52+
response = requests.get(f"https://{host}{api}", auth=auth, timeout=5)
53+
if response.status_code == 200:
54+
return "https://"
55+
except requests.RequestException:
56+
pass
57+
58+
try:
59+
response = requests.get(f"http://{host}{api}", auth=auth, timeout=5)
60+
if response.status_code == 200:
61+
return "http://"
62+
except requests.RequestException:
63+
pass
64+
65+
return None

0 commit comments

Comments
 (0)