├── docker ├── BUILD ├── Dockerfile.base └── Dockerfile ├── .bazelrc ├── .gitignore ├── docs └── resources │ ├── MD.png │ ├── teleop.png │ ├── complex_mission.png │ ├── mission_orders.png │ ├── simple_mission.png │ ├── create_and_delete_robot.gif │ └── create_and_query_mission.gif ├── bzl ├── requirements_linting.txt ├── requirements.txt ├── BUILD ├── pylint.py ├── pytype.py ├── python.bzl └── pylintrc ├── packages ├── utils │ ├── BUILD │ ├── test_utils │ │ ├── __init__.py │ │ ├── mosquitto.sh │ │ ├── network.py │ │ ├── BUILD │ │ └── docker.py │ ├── telemetry_sender.py │ └── metrics.py ├── controllers │ └── mission │ │ ├── vda5050_types │ │ ├── __init__.py │ │ └── BUILD │ │ ├── BUILD │ │ ├── tests │ │ ├── retrieve_factsheet.py │ │ ├── start_order.py │ │ ├── delete_robot.py │ │ ├── BUILD │ │ ├── fail_robot.py │ │ ├── server.py │ │ ├── update_mission.py │ │ ├── mission.py │ │ ├── robot.py │ │ ├── test_context.py │ │ └── cancel_mission.py │ │ ├── main.py │ │ └── behavior_tree.py └── database │ ├── tests │ ├── BUILD │ └── postgres.py │ ├── BUILD │ └── client.py ├── charts ├── templates │ ├── NOTES.txt │ ├── mission-dispatch.yaml │ ├── _helpers.tpl │ ├── mosquitto.yaml │ └── mission-database.yaml ├── Chart.yaml ├── values.yaml └── README.md ├── .gitattributes ├── scripts ├── BUILD └── run_dev.sh ├── BUILD ├── cloud_common └── objects │ ├── __init__.py │ ├── common.py │ ├── object.py │ ├── detection_results.py │ └── robot.py ├── docker_compose ├── .env ├── vda5050-adapter-examples.yaml └── mission_dispatch_services.yaml ├── deps.bzl ├── WORKSPACE └── SECURITY.md /docker/BUILD: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.bazelrc: -------------------------------------------------------------------------------- 1 | test --test_output=errors -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bazel-* 2 | __pycache__ 3 | .vscode 4 | -------------------------------------------------------------------------------- /docs/resources/MD.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:aa2b896bf6a4faeef942b9f21edc3fc7ed4298fd46b984ce8e24978bcd744335 3 | size 366577 4 | -------------------------------------------------------------------------------- /docs/resources/teleop.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:8402a74567a81ac1f09c52e3b7310bf9cd47416677fa0ee5b0ba2b59e500b9ba 3 | size 107328 4 | -------------------------------------------------------------------------------- /docs/resources/complex_mission.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:6ea9cf532a6bacf6a6934aae2d0408c29ae386a3c9446e192e61f019f89623cf 3 | size 196295 4 | -------------------------------------------------------------------------------- /docs/resources/mission_orders.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:59f85ea8359d1dd7263183e190dcbef1f768cd1b6a6bcb5d7c3e1839625d0080 3 | size 398602 4 | -------------------------------------------------------------------------------- /docs/resources/simple_mission.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d64b18f10ccf44dba740163820e0a87a4e80be2b8aaa45debe8892d2a6931559 3 | size 49177 4 | -------------------------------------------------------------------------------- /docs/resources/create_and_delete_robot.gif: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:74bb0533965639e5fc9351e9a419de18597516f2a06a77ae27ca0219fcca9535 3 | size 9170165 4 | -------------------------------------------------------------------------------- /docs/resources/create_and_query_mission.gif: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:2035bdc569ab556a178d60c14f54f36c112d1127a7432d8595dc702988355553 3 | size 16441414 4 | -------------------------------------------------------------------------------- /bzl/requirements_linting.txt: -------------------------------------------------------------------------------- 1 | # Top-level dependencies 2 | mypy==1.8.0 3 | pylint==2.17.7 4 | 5 | # Sub dependencies 6 | astroid==2.15.8 7 | dill==0.3.8 8 | isort==5.13.2 9 | lazy-object-proxy==1.10.0 10 | mccabe==0.7.0 11 | mypy-extensions==1.0.0 12 | platformdirs==4.0.0 # this breaks on higher versions as of 5/10/2024 13 | tomli==2.0.1 14 | tomlkit==0.12.5 15 | wrapt==1.16.0 16 | typing-extensions==4.12.2 17 | -------------------------------------------------------------------------------- /packages/utils/BUILD: -------------------------------------------------------------------------------- 1 | load("@com_nvidia_isaac_mission_dispatch//bzl:python.bzl", "mission_dispatch_py_library") 2 | load("@python_third_party//:requirements.bzl", "requirement") 3 | 4 | mission_dispatch_py_library( 5 | name = "metrics", 6 | srcs = ["metrics.py"], 7 | visibility = ["//visibility:public"] 8 | ) 9 | 10 | mission_dispatch_py_library( 11 | name = "telemetry_sender", 12 | srcs = ["telemetry_sender.py"], 13 | visibility = ["//visibility:public"] 14 | ) 15 | -------------------------------------------------------------------------------- /charts/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | Make sure the nvcr secret is created at: 2 | kubectl -n {{ .Values.namespace.name }} create secret docker-registry nvcr-secret \ 3 | --docker-server=nvcr.io --docker-username=\$oauthtoken \ 4 | --docker-password= 5 | 6 | Make sure the Postgres database secret is created: 7 | kubectl -n {{ .Values.namespace.name }} create secret generic postgres-secret \ 8 | --from-literal=db_username= \ 9 | --from-literal=db_password= 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Images 2 | *.gif filter=lfs diff=lfs merge=lfs -text 3 | *.jpg filter=lfs diff=lfs merge=lfs -text 4 | *.png filter=lfs diff=lfs merge=lfs -text 5 | *.psd filter=lfs diff=lfs merge=lfs -text 6 | 7 | # Archives 8 | *.gz filter=lfs diff=lfs merge=lfs -text 9 | *.tar filter=lfs diff=lfs merge=lfs -text 10 | *.zip filter=lfs diff=lfs merge=lfs -text 11 | 12 | # Documents 13 | *.pdf filter=lfs diff=lfs merge=lfs -text 14 | 15 | # Shared libraries 16 | *.so filter=lfs diff=lfs merge=lfs -text 17 | *.so.* filter=lfs diff=lfs merge=lfs -text 18 | -------------------------------------------------------------------------------- /bzl/requirements.txt: -------------------------------------------------------------------------------- 1 | # Top-level dependencies 2 | pydantic==1.9.0 3 | paho-mqtt==1.6.1 4 | Pillow==10.2.0 5 | psycopg[binary,pool]==3.0.15 6 | psycopg-binary==3.0.15 7 | psycopg-pool==3.2.2 8 | pyyaml==6.0 9 | fastapi==0.109.1 10 | uvicorn==0.17.6 11 | charset-normalizer==3.3.2 12 | requests==2.32.3 13 | py_trees==2.1.6 14 | websockets==12.0 15 | opencv-python==4.10.0.84 16 | numpy==1.24.3 17 | 18 | # Sub dependencies 19 | anyio==4.3.0 20 | asgiref==3.8.1 21 | certifi==2024.2.2 22 | chardet==4.0.0 23 | click==8.1.7 24 | exceptiongroup==1.2.1 25 | h11==0.14.0 26 | idna==2.10 27 | pydot==2.0.0 28 | pyparsing==3.1.2 29 | sniffio==1.3.1 30 | starlette==0.36.2 31 | typing_extensions==4.12.2 32 | urllib3==1.26.19 33 | setuptools==70.0.0 34 | -------------------------------------------------------------------------------- /packages/controllers/mission/vda5050_types/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | from packages.controllers.mission.vda5050_types.vda5050_types import * 20 | -------------------------------------------------------------------------------- /packages/utils/test_utils/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | from packages.utils.test_utils.docker import run_docker_target 20 | from packages.utils.test_utils.network import check_port_open, wait_for_port 21 | -------------------------------------------------------------------------------- /bzl/BUILD: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | load("@io_bazel_rules_docker//container:container.bzl", "container_image") 20 | 21 | exports_files(["pylintrc", "pytype.py", "pylint.py"]) 22 | 23 | container_image( 24 | name = "python_base", 25 | base = "@loaded_base_docker_image//image", 26 | visibility = ["//visibility:public"], 27 | ) 28 | -------------------------------------------------------------------------------- /bzl/pylint.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | import os 20 | import subprocess 21 | import sys 22 | 23 | 24 | def main(): 25 | # Run pylint in a subprocess 26 | result = subprocess.run([sys.executable, "-m", "pylint"] + sys.argv[1:], 27 | env={"PYTHONPATH": os.environ["PYTHONPATH"]}) 28 | sys.exit(result.returncode) 29 | 30 | 31 | if __name__ == "__main__": 32 | main() 33 | -------------------------------------------------------------------------------- /docker/Dockerfile.base: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 2 | # Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # SPDX-License-Identifier: Apache-2.0 17 | 18 | # Install python and dependencies 19 | FROM nvcr.io/nvidia/base/ubuntu:22.04_20240212 20 | ARG DEBIAN_FRONTEND=noninteractive 21 | 22 | RUN apt-get update && apt-get install python3 python3-pip -y 23 | RUN ln -s /usr/bin/python3 /usr/bin/python 24 | RUN apt-get update && apt-get install libgl1-mesa-glx ffmpeg libsm6 libxext6 python3-tk -y 25 | RUN apt-get clean 26 | -------------------------------------------------------------------------------- /packages/controllers/mission/vda5050_types/BUILD: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | load("//bzl:python.bzl", "mission_dispatch_py_library") 20 | load("@python_third_party//:requirements.bzl", "requirement") 21 | 22 | mission_dispatch_py_library( 23 | name="vda5050_types", 24 | srcs = glob(["*.py"]), 25 | deps = [ 26 | "//:cloud_common_objects", 27 | requirement("pydantic") 28 | ], 29 | visibility = ["//visibility:public"] 30 | ) -------------------------------------------------------------------------------- /scripts/BUILD: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | load("//bzl:python.bzl", "mission_dispatch_py_binary") 20 | load("@python_third_party//:requirements.bzl", "requirement") 21 | 22 | 23 | mission_dispatch_py_binary( 24 | name = "pick_and_place_ui", 25 | main = "pick_and_place_ui.py", 26 | srcs = glob(["*.py"]), 27 | deps = [ 28 | requirement("Pillow"), 29 | requirement("requests"), 30 | requirement("websockets"), 31 | requirement("opencv-python"), 32 | requirement("numpy"), 33 | ], 34 | visibility = ["//visibility:public"] 35 | ) 36 | -------------------------------------------------------------------------------- /packages/utils/test_utils/mosquitto.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | # Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # SPDX-License-Identifier: Apache-2.0 18 | 19 | # Shell script to launch inside the mosquitto-broker container 20 | # to set the port and address 21 | 22 | CONFIG_FILE=/mosquitto.conf 23 | 24 | if [ $# != 2 ] ; then 25 | echo "usage: $0 " 26 | exit 1 27 | fi 28 | PORT=$1 29 | PORT_WEBSOCKET=$2 30 | 31 | echo "allow_anonymous true" > $CONFIG_FILE 32 | echo "listener $PORT 0.0.0.0" >> $CONFIG_FILE 33 | echo "listener $PORT_WEBSOCKET" >> $CONFIG_FILE 34 | echo "protocol websockets" >> $CONFIG_FILE 35 | mosquitto -c $CONFIG_FILE 36 | -------------------------------------------------------------------------------- /scripts/run_dev.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. 3 | # 4 | # NVIDIA CORPORATION and its licensors retain all intellectual property 5 | # and proprietary rights in and to this software, related documentation 6 | # and any modifications thereto. Any use, reproduction, disclosure or 7 | # distribution of this software and related documentation without an express 8 | # license agreement from NVIDIA CORPORATION is strictly prohibited. 9 | 10 | set -e 11 | 12 | ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )"/.. >/dev/null 2>&1 && pwd )" 13 | docker build --network host -t isaac-mission-dispatch "${ROOT}/docker" 14 | 15 | #Create folder $HOME/.cache/bazel if it does not already exist 16 | if [ ! -d "$HOME/.cache/bazel" ]; then 17 | mkdir -p "$HOME/.cache/bazel" 18 | fi 19 | 20 | docker run -it --rm \ 21 | --network host \ 22 | --workdir "${ROOT}" \ 23 | -e USER="$(id -u)" \ 24 | -e DISPLAY \ 25 | -v "${ROOT}:${ROOT}" \ 26 | -v /etc/passwd:/etc/passwd:ro \ 27 | -v /etc/group:/etc/group:ro \ 28 | -v "$HOME/.docker:$HOME/.docker:ro" \ 29 | -v "$HOME/.docker/buildx:$HOME/.docker/buildx" \ 30 | -v "/etc/timezone:/etc/timezone:ro" \ 31 | -v "$HOME/.cache/bazel:$HOME/.cache/bazel" \ 32 | -v /var/run/docker.sock:/var/run/docker.sock \ 33 | -u $(id -u) \ 34 | --group-add $(getent group docker | cut -d: -f3) \ 35 | isaac-mission-dispatch /bin/bash 36 | -------------------------------------------------------------------------------- /BUILD: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | load("@python_third_party//:requirements.bzl", "requirement") 20 | load("//bzl:python.bzl", "mission_dispatch_py_library") 21 | 22 | 23 | mission_dispatch_py_library( 24 | name = "cloud_common_objects", 25 | srcs = ["cloud_common/objects/common.py", 26 | "cloud_common/objects/mission.py", 27 | "cloud_common/objects/object.py", 28 | "cloud_common/objects/robot.py", 29 | "cloud_common/objects/detection_results.py"], 30 | data = ["cloud_common/objects/__init__.py"], 31 | deps = [ 32 | requirement("fastapi"), 33 | requirement("pydantic"), 34 | requirement("psycopg") 35 | ], 36 | visibility = ["//visibility:public"] 37 | ) 38 | -------------------------------------------------------------------------------- /packages/utils/telemetry_sender.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | 20 | from typing import Dict 21 | import logging 22 | 23 | class TelemetrySender: 24 | """ Telemetry Ingestion 25 | """ 26 | 27 | def __init__(self, telemetry_env: str = "DEV") -> None: 28 | self.logger = logging.getLogger("Isaac Mission Dispatch") 29 | self.logger.debug("telemetry env: %s", telemetry_env) 30 | 31 | def send_telemetry(self, metrics: Dict, 32 | service_name: str = "DISPATCH"): 33 | """ 34 | Send telemetry data 35 | 36 | Args: 37 | metrics: metric dictionary to send 38 | index: the index to send the info to 39 | """ 40 | self.logger.debug("Send telemetry data.") 41 | -------------------------------------------------------------------------------- /cloud_common/objects/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | from typing import Dict, List, Type 20 | 21 | from cloud_common.objects.mission import MissionObjectV1 22 | from cloud_common.objects.object import ApiObject, ApiObjectMethod, ObjectLifecycleV1 23 | from cloud_common.objects.robot import RobotObjectV1 24 | from cloud_common.objects.detection_results import DetectionResultsObjectV1 25 | 26 | ALL_OBJECTS: List[Type[ApiObject]] = [ 27 | RobotObjectV1, MissionObjectV1, DetectionResultsObjectV1] 28 | OBJECT_DICT: Dict[str, Type[ApiObject]] = { 29 | obj.get_alias(): obj for obj in ALL_OBJECTS} 30 | 31 | USER_API_OBJECT_DICT: Dict[str, Type[ApiObject]] = { 32 | obj.get_alias(): obj for obj in ALL_OBJECTS if obj is not DetectionResultsObjectV1} 33 | 34 | ApiObjectType = Type[ApiObject] 35 | -------------------------------------------------------------------------------- /packages/controllers/mission/BUILD: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | load("//bzl:python.bzl", "mission_dispatch_py_binary") 20 | load("@python_third_party//:requirements.bzl", "requirement") 21 | 22 | telemetry_deps = [ 23 | "//packages/utils:telemetry_sender" 24 | ] 25 | 26 | mission_dispatch_py_binary( 27 | name = "mission", 28 | main = "main.py", 29 | srcs = glob(["*.py"]), 30 | deps = [ 31 | "//:cloud_common_objects", 32 | "//packages/database:client", 33 | "//packages/controllers/mission/vda5050_types", 34 | "//packages/utils:metrics", 35 | requirement("pydantic"), 36 | requirement("paho-mqtt"), 37 | requirement("PyYAML"), 38 | requirement("py_trees"), 39 | ] + telemetry_deps, 40 | visibility = ["//visibility:public"] 41 | ) 42 | -------------------------------------------------------------------------------- /packages/utils/test_utils/network.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | import contextlib 20 | import socket 21 | import time 22 | 23 | # How often to poll to see if a port is open 24 | PORT_CHECK_PERIOD = 0.1 25 | 26 | 27 | def check_port_open(port: int, host: str = "localhost") -> bool: 28 | with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as test_socket: 29 | return test_socket.connect_ex((host, port)) == 0 30 | 31 | 32 | def wait_for_port(port: int, timeout: float = float("inf"), host: str = "localhost"): 33 | end_time = time.time() + timeout 34 | while time.time() < end_time: 35 | if check_port_open(host=host, port=port): 36 | return 37 | time.sleep(PORT_CHECK_PERIOD) 38 | raise ValueError(f"Port {host}:{port} did not open in time") 39 | -------------------------------------------------------------------------------- /charts/templates/mission-dispatch.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 2 | # Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 3 | 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # SPDX-License-Identifier: Apache-2.0 17 | 18 | apiVersion: apps/v1 19 | kind: Deployment 20 | metadata: 21 | name: mission-dispatch 22 | namespace: {{ .Values.namespace.name }} 23 | labels: 24 | app: mission-dispatch 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: mission-dispatch 30 | template: 31 | metadata: 32 | labels: 33 | app: mission-dispatch 34 | spec: 35 | imagePullSecrets: 36 | - name: {{ .Values.images.nvcrSecret }} 37 | containers: 38 | - name: mission-dispatch 39 | image: {{ .Values.images.missionDispatch }} 40 | args: ["--database_url", "http://mission-dispatch-database-internal", "--mqtt_host", "mqtt-broker", 41 | "--mqtt_transport", "websockets"] 42 | imagePullPolicy: Always 43 | -------------------------------------------------------------------------------- /packages/database/tests/BUILD: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | load("//bzl:python.bzl", "mission_dispatch_py_library") 20 | 21 | mission_dispatch_py_library( 22 | name="test_base", 23 | srcs=[ 24 | "test_base.py" 25 | ], 26 | deps=[ 27 | "//packages/utils/test_utils", 28 | "//:cloud_common_objects", 29 | ], 30 | visibility = ["//visibility:public"] 31 | ) 32 | 33 | py_test( 34 | name="postgres", 35 | srcs=[ 36 | "postgres.py" 37 | ], 38 | deps=[ 39 | "//packages/database:client", 40 | "//packages/utils/test_utils", 41 | "//packages/database/tests:test_base", 42 | ], 43 | data = [ 44 | "//packages/database:postgres-img-bundle", 45 | "//packages/utils/test_utils:postgres-database-img-bundle" 46 | ], 47 | tags = [ 48 | "exclusive" 49 | ], 50 | ) 51 | -------------------------------------------------------------------------------- /charts/Chart.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 2 | # Copyright (c) 2021-2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 3 | 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # SPDX-License-Identifier: Apache-2.0 17 | 18 | apiVersion: v2 19 | name: mission-dispatch 20 | description: A Helm chart for Kubernetes 21 | 22 | # A chart can be either an 'application' or a 'library' chart. 23 | # 24 | # Application charts are a collection of templates that can be packaged into versioned archives 25 | # to be deployed. 26 | # 27 | # Library charts provide useful utilities or functions for the chart developer. They're included as 28 | # a dependency of application charts to inject those utilities and functions into the rendering 29 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 30 | type: application 31 | 32 | # This is the chart version. This version number should be incremented each time you make changes 33 | # to the chart and its templates, including the app version. 34 | version: 0.1.1 35 | 36 | # This is the version number of the application being deployed. This version number should be 37 | # incremented each time you make changes to the application. 38 | appVersion: 1.16.0 39 | -------------------------------------------------------------------------------- /docker_compose/.env: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 2 | # Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # SPDX-License-Identifier: Apache-2.0 17 | 18 | # Docker images 19 | MISSION_DATABASE_IMAGE=nvcr.io/nvidia/isaac/mission-database:3.0.0 20 | MISSION_DISPATCH_IMAGE=nvcr.io/nvidia/isaac/mission-dispatch:3.0.0 21 | 22 | # MQTT 23 | # The TCP port for the MQTT broker to listen on 24 | MQTT_PORT_TCP=1883 25 | # The WEBSOCKET port for the MQTT broker to listen on 26 | MQTT_PORT_WEBSOCKET=9001 27 | # The transport mechanism("websockets", "tcp") for MQTT 28 | MQTT_TRANSPORT=websockets 29 | 30 | # Mission database 31 | # Port for mission database to host the REST API on 32 | DATABASE_API_PORT=5000 33 | # Port used for internal communication between mission dispatch and mission database 34 | DATABASE_CONTROLLER_PORT=5001 35 | # The name of database to connect to in postgres 36 | POSTGRES_DATABASE_NAME=mission 37 | # The postgres username to use 38 | POSTGRES_DATABASE_USERNAME=postgres 39 | # The postgres password to use 40 | POSTGRES_DATABASE_PASSWORD=postgres 41 | # The hostname of the postgres server 42 | POSTGRES_DATABASE_HOST=localhost 43 | # Starting PostgreSQL Db on this port 44 | POSTGRES_DATABASE_PORT=5432 45 | 46 | -------------------------------------------------------------------------------- /packages/utils/test_utils/BUILD: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | load("@python_third_party//:requirements.bzl", "requirement") 20 | load("//bzl:python.bzl", "mission_dispatch_py_library") 21 | load("@io_bazel_rules_docker//container:container.bzl", "container_image", "container_bundle") 22 | 23 | mission_dispatch_py_library( 24 | name = "test_utils", 25 | srcs = [ 26 | "__init__.py", 27 | "docker.py", 28 | "network.py", 29 | ], 30 | visibility = ["//visibility:public"] 31 | ) 32 | 33 | container_image( 34 | name = "mosquitto-img", 35 | base = "@mosquitto_base//image", 36 | files = [ 37 | "mosquitto.sh" 38 | ], 39 | entrypoint = ["sh", "mosquitto.sh"] 40 | ) 41 | 42 | container_bundle( 43 | name = "mosquitto-img-bundle", 44 | images = { 45 | "bazel-image": ":mosquitto-img" 46 | }, 47 | visibility = ["//visibility:public"] 48 | ) 49 | 50 | container_bundle( 51 | name = "postgres-database-img-bundle", 52 | images = { 53 | "bazel-image": "@postgres_database_base//image" 54 | }, 55 | visibility = ["//visibility:public"] 56 | ) 57 | -------------------------------------------------------------------------------- /packages/database/BUILD: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | load("@python_third_party//:requirements.bzl", "requirement") 20 | load("//bzl:python.bzl", "mission_dispatch_py_binary", "mission_dispatch_py_library") 21 | 22 | mission_dispatch_py_library( 23 | name = "common", 24 | srcs = [ 25 | "common.py" 26 | ], 27 | deps = [ 28 | "//:cloud_common_objects", 29 | requirement("fastapi"), 30 | requirement("pydantic"), 31 | requirement("uvicorn") 32 | ] 33 | ) 34 | 35 | mission_dispatch_py_binary( 36 | name = "postgres", 37 | main = "postgres.py", 38 | srcs = [ 39 | "postgres.py", 40 | ], 41 | deps = [ 42 | ":common", 43 | "//:cloud_common_objects", 44 | requirement("fastapi"), 45 | requirement("psycopg"), 46 | ], 47 | visibility = ["//visibility:public"], 48 | ) 49 | 50 | mission_dispatch_py_library( 51 | name = "client", 52 | srcs = ["client.py"], 53 | deps = [ 54 | "//:cloud_common_objects", 55 | requirement("pydantic"), 56 | requirement("requests"), 57 | ], 58 | visibility = ["//visibility:public"] 59 | ) 60 | -------------------------------------------------------------------------------- /bzl/pytype.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | import os 20 | import subprocess 21 | import sys 22 | 23 | 24 | def shadowed_module(path: str) -> bool: 25 | """ Whether a path indicates a module that shadows a dist-package and should be excluded from 26 | mypy """ 27 | shadowed_modules = [ 28 | "typing_extensions", "mypy_extensions" 29 | ] 30 | return any(module in path for module in shadowed_modules) 31 | 32 | 33 | def main(): 34 | # Determine the module include paths that should be used by mypy 35 | paths = os.environ["PYTHONPATH"] 36 | fixed_paths = ":".join(path for path in paths.split(":") if not shadowed_module(path)) 37 | env = { 38 | "PYTHONPATH": paths, 39 | "MYPYPATH": fixed_paths 40 | } 41 | 42 | # Run mypy in a subprocess 43 | result = subprocess.run([sys.executable, "-m", "mypy", 44 | "--explicit-package-bases", "--namespace-packages", 45 | "--follow-imports", "silent", "--check-untyped-defs"] + sys.argv[1:], 46 | env=env) 47 | sys.exit(result.returncode) 48 | 49 | 50 | if __name__ == "__main__": 51 | main() 52 | -------------------------------------------------------------------------------- /charts/values.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 2 | # Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 3 | 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # SPDX-License-Identifier: Apache-2.0 17 | 18 | images: 19 | # The name of the kubernetes secret which contains a key for pull images from nvcr.io 20 | nvcrSecret: nvcr-secret 21 | missionDispatch: nvcr.io/nvidia/isaac/mission-dispatch:3.2.0 22 | missionDatabase: nvcr.io/nvidia/isaac/mission-database:3.2.0 23 | 24 | # The url to host mission dispatch at for external users 25 | missionUrl: /mission-dispatch 26 | mqttUrl: /mqtt 27 | 28 | # Postgres database hostname that Mission Dispatch will connect to 29 | dbHostName: postgres-db-postgresql 30 | # The port of the Postgres database that Mission Dispatch will connect to 31 | dbPort: 5432 32 | # The hostname of mission dispatch server 33 | hostDomainName: www.example.com 34 | 35 | # Default Nginx Ingress Annotations 36 | # Nginx Ingress Controller Github: https://github.com/kubernetes/ingress-nginx 37 | ingressAnnotations: 38 | kubernetes.io/ingress.class: nginx 39 | nginx.ingress.kubernetes.io/rewrite-target: /$2 40 | nginx.ingress.kubernetes.io/backend-protocol: "HTTP" 41 | 42 | # The namespace to start the pods and services in 43 | namespace: 44 | # The name of the namespace to create all resources in 45 | name: default 46 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 2 | # Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # SPDX-License-Identifier: Apache-2.0 17 | 18 | # Install python and dependencies 19 | FROM nvcr.io/nvidia/base/ubuntu:22.04_20240212 20 | ARG DEBIAN_FRONTEND=noninteractive 21 | 22 | # Install python 23 | RUN apt-get update && apt-get install python3 python3-pip -y 24 | RUN ln -s /usr/bin/python3 /usr/bin/python 25 | 26 | # Install bazel 27 | RUN apt-get install wget -y 28 | RUN wget --progress=dot:mega https://github.com/bazelbuild/bazel/releases/download/6.5.0/bazel-6.5.0-linux-x86_64 -O /usr/bin/bazel 29 | RUN chmod +x /usr/bin/bazel 30 | 31 | # Install docker 32 | RUN apt-get update 33 | RUN apt-get install -y \ 34 | ca-certificates \ 35 | curl \ 36 | gnupg \ 37 | lsb-release \ 38 | git 39 | RUN git config --global --add safe.directory '*' 40 | RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg 41 | RUN echo \ 42 | "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \ 43 | $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null 44 | RUN apt-get update 45 | RUN apt-get install -y docker-ce-cli 46 | 47 | RUN apt-get update && apt-get install libgl1-mesa-glx ffmpeg libsm6 libxext6 python3-tk -y -------------------------------------------------------------------------------- /charts/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "mission-dispatch.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "mission-dispatch.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "mission-dispatch.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "mission-dispatch.labels" -}} 38 | helm.sh/chart: {{ include "mission-dispatch.chart" . }} 39 | {{ include "mission-dispatch.selectorLabels" . }} 40 | {{- if .Chart.AppVersion }} 41 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 42 | {{- end }} 43 | app.kubernetes.io/managed-by: {{ .Release.Service }} 44 | {{- end -}} 45 | 46 | {{/* 47 | Selector labels 48 | */}} 49 | {{- define "mission-dispatch.selectorLabels" -}} 50 | app.kubernetes.io/name: {{ include "mission-dispatch.name" . }} 51 | app.kubernetes.io/instance: {{ .Release.Name }} 52 | {{- end -}} 53 | 54 | {{/* 55 | Create the name of the service account to use 56 | */}} 57 | {{- define "mission-dispatch.serviceAccountName" -}} 58 | {{- if .Values.serviceAccount.create -}} 59 | {{ default (include "mission-dispatch.fullname" .) .Values.serviceAccount.name }} 60 | {{- else -}} 61 | {{ default "default" .Values.serviceAccount.name }} 62 | {{- end -}} 63 | {{- end -}} 64 | -------------------------------------------------------------------------------- /deps.bzl: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | load("@io_bazel_rules_docker//repositories:deps.bzl", container_deps = "deps") 20 | load("@io_bazel_rules_docker//container:container.bzl", "container_pull") 21 | load("@io_bazel_rules_docker//python3:image.bzl", _py3_image_repos = "repositories") 22 | load("@io_bazel_rules_docker//contrib:dockerfile_build.bzl", "dockerfile_image") 23 | load("@io_bazel_rules_docker//container:load.bzl", "container_load") 24 | 25 | def mission_dispatch_workspace(): 26 | # Pull dependencies needed for docker containers 27 | container_deps() 28 | 29 | container_pull( 30 | name = "mosquitto_base", 31 | registry = "dockerhub.nvidia.com", 32 | repository = "eclipse-mosquitto", 33 | tag = "latest", 34 | digest = "sha256:8bb31a44178c8ffecc530f33f40cccac2493ad9b32a98aa50ddbaef56d21cf55" 35 | ) 36 | 37 | container_pull( 38 | name = "postgres_database_base", 39 | registry = "docker.io/library", 40 | repository = "postgres", 41 | tag = "14.5", 42 | digest = "sha256:db3825afa034c78d03e301c48c1e8ed581f70e4b1c0d9dd944e3639a9d4b8b75" 43 | ) 44 | 45 | # Enable python3 based images 46 | _py3_image_repos() 47 | 48 | # Load dockerfile_image 49 | dockerfile_image( 50 | name = "base_docker_image", 51 | dockerfile = "@com_nvidia_isaac_mission_dispatch//docker:Dockerfile.base", 52 | visibility = ["//visibility:public"], 53 | ) 54 | 55 | # Load the image tarball. 56 | container_load( 57 | name = "loaded_base_docker_image", 58 | file = "@base_docker_image//image:dockerfile_image.tar", 59 | visibility = ["//visibility:public"], 60 | ) 61 | -------------------------------------------------------------------------------- /charts/README.md: -------------------------------------------------------------------------------- 1 | ## Installing Kubernetes 2 | 3 | To install kubernetes on a control-plane or worker node, run the following commands: 4 | 5 | ``` 6 | # Install docker 7 | sudo apt-get install -y \ 8 | apt-transport-https \ 9 | ca-certificates \ 10 | curl \ 11 | gnupg \ 12 | lsb-release 13 | echo \ 14 | "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \ 15 | $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null 16 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg 17 | sudo apt-get update 18 | sudo apt-get install -y docker-ce docker-ce-cli containerd.io 19 | 20 | # Update /etc/docker/daemon.json 21 | echo '{ 22 | "exec-opts": ["native.cgroupdriver=systemd"], 23 | "bip": "192.168.222.1/24", 24 | "dns": ["172.20.232.252","172.20.192.252", "1.1.1.1", "1.0.0.1"], 25 | "default-address-pools": [ 26 | { 27 | "base": "10.210.200.0/16", 28 | "size": 24 29 | } 30 | ] 31 | }' | sudo tee /etc/docker/daemon.json 32 | sudo service docker restart 33 | 34 | # Install kubernetes 35 | sudo apt-get install -y apt-transport-https ca-certificates curl 36 | sudo curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg 37 | echo "deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list 38 | sudo apt-get update 39 | sudo apt-get install -y kubelet=1.22.1-00 kubeadm=1.22.1-00 kubectl=1.22.1-00 40 | sudo apt-mark hold kubelet kubeadm kubectl 41 | 42 | # Disable swap 43 | sudo swapoff -a 44 | ``` 45 | 46 | ## Setup Control-plane node 47 | 48 | Create a new cluster on the control-plane node: 49 | 50 | ``` 51 | # Start kubernetes 52 | sudo kubeadm init --pod-network-cidr=10.244.0.0/16 53 | 54 | # Setup kubeconfig to allow kubectl access to cluster 55 | mkdir -p $HOME/.kube 56 | sudo rm -f $HOME/.kube/config 57 | sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config 58 | sudo chown $(id -u):$(id -g) $HOME/.kube/config 59 | 60 | # Install flannel 61 | kubectl apply -f charts/kube-flannel.yml 62 | 63 | # Install gpu-operator 64 | helm repo add nvidia https://helm.ngc.nvidia.com/nvidia && helm repo update 65 | helm install --create-namespace -n gpu-operator gpu-operator nvidia/gpu-operator 66 | ``` -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | workspace(name="com_nvidia_isaac_mission_dispatch") 20 | 21 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 22 | 23 | # Include rules needed for pip 24 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 25 | 26 | http_archive( 27 | name="rules_python", 28 | sha256="778aaeab3e6cfd56d681c89f5c10d7ad6bf8d2f1a72de9de55b23081b2d31618", 29 | strip_prefix="rules_python-0.34.0", 30 | url="https://github.com/bazelbuild/rules_python/releases/download/0.34.0/rules_python-0.34.0.tar.gz", 31 | ) 32 | 33 | load("@rules_python//python:repositories.bzl", "py_repositories") 34 | 35 | py_repositories() 36 | 37 | # Include rules 38 | http_archive( 39 | name="io_bazel_rules_docker", 40 | sha256="b1e80761a8a8243d03ebca8845e9cc1ba6c82ce7c5179ce2b295cd36f7e394bf", 41 | urls=["https://github.com/bazelbuild/rules_docker/releases/download/v0.25.0/rules_docker-v0.25.0.tar.gz"], 42 | ) 43 | load("@io_bazel_rules_docker//repositories:repositories.bzl", 44 | container_repositories="repositories") 45 | container_repositories() 46 | 47 | 48 | # Setup workspace for mission dispatch 49 | load("//:deps.bzl", "mission_dispatch_workspace") 50 | load("@rules_python//python:pip.bzl", "pip_parse") 51 | # Install python dependencies from pip 52 | pip_parse( 53 | name="python_third_party", 54 | requirements_lock="@com_nvidia_isaac_mission_dispatch//bzl:requirements.txt" 55 | ) 56 | 57 | load("@python_third_party//:requirements.bzl", "install_deps") 58 | install_deps() 59 | 60 | # Install linting dependencies from pip 61 | pip_parse( 62 | name="python_third_party_linting", 63 | requirements_lock="@com_nvidia_isaac_mission_dispatch//bzl:requirements_linting.txt" 64 | ) 65 | 66 | load("@python_third_party_linting//:requirements.bzl", "install_deps") 67 | install_deps() 68 | 69 | mission_dispatch_workspace() 70 | -------------------------------------------------------------------------------- /packages/controllers/mission/tests/retrieve_factsheet.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | import time 20 | import unittest 21 | 22 | from cloud_common import objects as api_objects 23 | from packages.controllers.mission.tests import client as simulator 24 | from cloud_common.objects import mission as mission_object 25 | from cloud_common.objects import robot as robot_object 26 | from cloud_common.objects import common 27 | 28 | from packages.controllers.mission.tests import test_context 29 | 30 | 31 | class TestRetrieveFactsheet(unittest.TestCase): 32 | 33 | def test_retrieve_factsheet(self): 34 | """ Test if factsheet retrieval is functional """ 35 | 36 | robot_arm = simulator.RobotInit("test01", 0, 0, 0, robot_type="arm") 37 | robot_amr = simulator.RobotInit("test02", 0, 0, 0, robot_type="amr") 38 | with test_context.TestContext([robot_arm, robot_amr], tick_period=1.0) as ctx: 39 | 40 | ctx.db_client.create( 41 | api_objects.RobotObjectV1(name="test01", status={})) 42 | time.sleep(0.25) 43 | self.assertGreater( 44 | len(ctx.db_client.list(api_objects.RobotObjectV1)), 0) 45 | 46 | time.sleep(2) 47 | factsheet = ctx.db_client.get(robot_object.RobotObjectV1, "test01").status.factsheet 48 | 49 | assert (factsheet.agv_class == "FORKLIFT") 50 | 51 | ctx.db_client.create( 52 | api_objects.RobotObjectV1(name="test02", status={})) 53 | time.sleep(0.25) 54 | self.assertGreater( 55 | len(ctx.db_client.list(api_objects.RobotObjectV1)), 1) 56 | 57 | time.sleep(2) 58 | factsheet = ctx.db_client.get(robot_object.RobotObjectV1, "test02").status.factsheet 59 | 60 | assert (factsheet.agv_class == "CARRIER") 61 | 62 | 63 | if __name__ == "__main__": 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /docker_compose/vda5050-adapter-examples.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 2 | # Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # SPDX-License-Identifier: Apache-2.0 17 | 18 | services: 19 | # Create Postgres database 20 | postgres: 21 | image: postgres:14.5 22 | environment: 23 | - POSTGRES_USER=${POSTGRES_DATABASE_USERNAME} 24 | - POSTGRES_PASSWORD=${POSTGRES_DATABASE_PASSWORD} 25 | - POSTGRES_DB=${POSTGRES_DATABASE_NAME} 26 | - POSTGRES_INITDB_ARGS=--auth-host=scram-sha-256 --auth-local=scram-sha-256 27 | ports: 28 | - '${POSTGRES_DATABASE_PORT}:${POSTGRES_DATABASE_PORT}' 29 | healthcheck: 30 | test: pg_isready -U ${POSTGRES_DATABASE_USERNAME} 31 | interval: 3s 32 | timeout: 10s 33 | retries: 5 34 | networks: 35 | - vda5050-adapter 36 | 37 | # Create an instance of mission database 38 | mission-database: 39 | image: ${MISSION_DATABASE_IMAGE} 40 | command: ["--port", "${DATABASE_API_PORT}", 41 | "--controller_port", "${DATABASE_CONTROLLER_PORT}", 42 | "--db_port", "${POSTGRES_DATABASE_PORT}", 43 | "--db_name", "${POSTGRES_DATABASE_NAME}", 44 | "--db_username", "${POSTGRES_DATABASE_USERNAME}", 45 | "--db_password", "${POSTGRES_DATABASE_PASSWORD}", 46 | "--db_host", postgres, 47 | "--address", mission-database] 48 | depends_on: 49 | postgres: 50 | condition: service_healthy 51 | networks: 52 | - vda5050-adapter 53 | 54 | # Create an instance of mission dispatch 55 | mission-dispatch: 56 | image: ${MISSION_DISPATCH_IMAGE} 57 | depends_on: 58 | - mission-database 59 | command: ["--mqtt_host", deployment-mosquitto-1, 60 | "--mqtt_port", "${MQTT_PORT_TCP}", 61 | "--database_url", "http://mission-database:${DATABASE_CONTROLLER_PORT}", 62 | "--mqtt_prefix", uagv/v2/OSRF] 63 | networks: 64 | - vda5050-adapter 65 | 66 | networks: 67 | vda5050-adapter: 68 | name: deployment_vda5050-adapter-examples 69 | external: true 70 | -------------------------------------------------------------------------------- /docker_compose/mission_dispatch_services.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 2 | # Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | # SPDX-License-Identifier: Apache-2.0 17 | 18 | version: "3.3" 19 | services: 20 | # Create an MQTT broker 21 | mosquitto: 22 | image: eclipse-mosquitto:latest 23 | command: 24 | - /bin/sh 25 | - -c 26 | - | 27 | sh mosquitto.sh "${MQTT_PORT_TCP}" "${MQTT_PORT_WEBSOCKET}" 28 | network_mode: "host" 29 | volumes: 30 | - type: bind 31 | source: ../packages/utils/test_utils/mosquitto.sh 32 | target: /mosquitto.sh 33 | 34 | # Create Postgres database 35 | postgres: 36 | image: postgres:14.5 37 | environment: 38 | - POSTGRES_USER=${POSTGRES_DATABASE_USERNAME} 39 | - POSTGRES_PASSWORD=${POSTGRES_DATABASE_PASSWORD} 40 | - POSTGRES_DB=${POSTGRES_DATABASE_NAME} 41 | - POSTGRES_INITDB_ARGS=--auth-host=scram-sha-256 --auth-local=scram-sha-256 42 | ports: 43 | - '${POSTGRES_DATABASE_PORT}:${POSTGRES_DATABASE_PORT}' 44 | network_mode: "host" 45 | healthcheck: 46 | test: pg_isready -U ${POSTGRES_DATABASE_USERNAME} 47 | interval: 3s 48 | timeout: 10s 49 | retries: 5 50 | 51 | # Create an instance of mission database 52 | mission-database: 53 | image: ${MISSION_DATABASE_IMAGE} 54 | command: ["--port", "${DATABASE_API_PORT}", 55 | "--controller_port", "${DATABASE_CONTROLLER_PORT}", 56 | "--db_port", "${POSTGRES_DATABASE_PORT}", 57 | "--db_name", "${POSTGRES_DATABASE_NAME}", 58 | "--db_username", "${POSTGRES_DATABASE_USERNAME}", 59 | "--db_password", "${POSTGRES_DATABASE_PASSWORD}"] 60 | network_mode: "host" 61 | depends_on: 62 | postgres: 63 | condition: service_healthy 64 | 65 | # Create an instance of mission dispatch 66 | mission-dispatch: 67 | image: ${MISSION_DISPATCH_IMAGE} 68 | command: ["--mqtt_port", "${MQTT_PORT_WEBSOCKET}", 69 | "--mqtt_transport", "${MQTT_TRANSPORT}", 70 | "--database_url", "http://localhost:${DATABASE_CONTROLLER_PORT}"] 71 | network_mode: "host" 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /cloud_common/objects/common.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | import enum 20 | 21 | import pydantic 22 | 23 | # Tell pylint to ignore the invalid names. We must use fields that are specified 24 | # by VDA5050. 25 | # pylint: disable=invalid-name 26 | 27 | 28 | class ICSError(Exception): 29 | """ 30 | Base class for exceptions in this module. 31 | If unexpected Error occurs user will be shown this error. 32 | """ 33 | error_code: str = "ICS_ERROR" 34 | 35 | def __init__(self, message: str): 36 | super().__init__(message) 37 | self.message = message 38 | 39 | def __repr__(self): 40 | return f"{self.__class__.__name__}: {self.message}" 41 | 42 | def __str__(self): 43 | return self.message 44 | 45 | 46 | class ICSUsageError(ICSError): 47 | """ Exception raised for errors to notify users with appropriate message. """ 48 | error_code: str = "USAGE" 49 | 50 | 51 | class ICSServerError(ICSError): 52 | """ Exception raised for errors in the server. """ 53 | error_code: str = "SERVER" 54 | 55 | 56 | class TaskType(enum.Enum): 57 | MISSION = "MISSION" 58 | MAP_UPDATE = "MAP_UPDATE" 59 | 60 | 61 | class Pose2D(pydantic.BaseModel): 62 | """Specifies a pose to be traveled to by the robot""" 63 | x: float = pydantic.Field( 64 | description="The x coordinate of the pose in meters", default=0.0) 65 | y: float = pydantic.Field( 66 | description="The y coordinate of the pose in meters", default=0.0) 67 | theta: float = pydantic.Field( 68 | description="The rotation of the pose in radians", default=0.0) 69 | map_id: str = pydantic.Field( 70 | description="The ID of the map this pose is associated with", default="") 71 | allowedDeviationXY: float = pydantic.Field( 72 | description="Allowed coordinate deviation radius", 73 | default=0.1) 74 | allowedDeviationTheta: float = pydantic.Field( 75 | description="Allowed theta deviation radians", 76 | default=0.0) 77 | 78 | 79 | def handle_response(response): 80 | if response.status_code >= 400 and response.status_code < 500: 81 | raise ICSUsageError(response.text) 82 | if response.status_code >= 500: 83 | raise ICSServerError(response.text) 84 | -------------------------------------------------------------------------------- /cloud_common/objects/object.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | import abc 20 | import base64 21 | import enum 22 | import uuid 23 | from typing import Any, Callable, Dict, List, NamedTuple, Optional, Type 24 | 25 | import pydantic 26 | 27 | # The number of characters to include in the short object ID 28 | SHORT_ID_LENGTH = 8 29 | 30 | 31 | class ObjectLifecycleV1(str, enum.Enum): 32 | ALIVE = "ALIVE" 33 | PENDING_DELETE = "PENDING_DELETE" 34 | DELETED = "DELETED" 35 | 36 | 37 | class ApiObjectMethod(NamedTuple): 38 | name: str 39 | description: str 40 | function: Callable 41 | params: Optional[Type] = None 42 | returns: Optional[Type] = None 43 | 44 | 45 | class ApiObject(pydantic.BaseModel, metaclass=abc.ABCMeta): 46 | """Represents an api object with a specification and a state""" 47 | 48 | # Every API object has a unique name 49 | name: str 50 | status: Any = None 51 | lifecycle: ObjectLifecycleV1 = ObjectLifecycleV1.ALIVE 52 | 53 | def __init__(self, *args, **kwargs): 54 | if kwargs.get("name") is None: 55 | kwargs["name"] = self.get_uuid() 56 | super().__init__(*args, **kwargs) 57 | 58 | @property 59 | def spec(self) -> Any: 60 | return self.get_spec_class()(**self.dict()) 61 | 62 | @classmethod 63 | @abc.abstractmethod 64 | def get_alias(cls) -> str: 65 | pass 66 | 67 | @classmethod 68 | @abc.abstractmethod 69 | def get_spec_class(cls) -> Any: 70 | pass 71 | 72 | @classmethod 73 | @abc.abstractmethod 74 | def get_status_class(cls) -> Any: 75 | pass 76 | 77 | @classmethod 78 | @abc.abstractmethod 79 | def get_query_params(cls) -> Any: 80 | pass 81 | 82 | @staticmethod 83 | def get_query_map() -> Dict: 84 | return {} 85 | 86 | @classmethod 87 | def get_methods(cls) -> List[ApiObjectMethod]: 88 | return [] 89 | 90 | @classmethod 91 | def supports_spec_update(cls) -> bool: 92 | return True 93 | 94 | @classmethod 95 | def table_name(cls): 96 | return cls.__name__.lower() 97 | 98 | @classmethod 99 | def get_uuid(cls) -> str: 100 | return base64.b32encode(uuid.uuid4().bytes).lower().decode("utf-8").replace("=", "") 101 | 102 | @classmethod 103 | def default_spec(cls): 104 | pass 105 | -------------------------------------------------------------------------------- /packages/utils/metrics.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | import enum 20 | from typing import Union 21 | 22 | 23 | class Timeframe(enum.Enum): 24 | RUNTIME = "RUNTIME" 25 | DAILY = "DAILY" 26 | MISSION = "MISSION" 27 | ROBOT = "ROBOT" 28 | 29 | 30 | class Telemetry: 31 | """ Collect telemetry data 32 | """ 33 | 34 | def __init__(self): 35 | """ 36 | Initialize the Telemetry object. 37 | """ 38 | self.data = {} 39 | 40 | def add_kpi(self, name: str, value: Union[float, dict, str], frequency: Timeframe): 41 | """ 42 | Add a scalar KPI to telemetry data. 43 | 44 | Args: 45 | name (str): The name of the KPI. 46 | value (Union[float, dict, str]): The value of the KPI. 47 | frequency (Timeframe): The frequency at which the KPI should be recorded. 48 | """ 49 | if frequency.value not in self.data: 50 | self.data[frequency.value] = {} 51 | 52 | self.data[frequency.value][name] = value 53 | 54 | def aggregate_scalar_kpi(self, name: str, value: float, frequency: Timeframe): 55 | """ 56 | Calculate statistics or aggregate values for a specific KPI. 57 | 58 | Args: 59 | name (str): The name of the KPI. 60 | value (str): The string value of the KPI. 61 | frequency (Timeframe): The frequency at which the KPI should be recorded. 62 | """ 63 | if frequency.value not in self.data: 64 | self.data[frequency.value] = {} 65 | self.data[frequency.value][name] += value 66 | 67 | def get_kpis_by_frequency(self, frequency: Timeframe): 68 | """ 69 | Retrieve KPIs for a specific frequency. 70 | 71 | Args: 72 | frequency (Timeframe): The frequency for which KPIs should be retrieved. 73 | 74 | Returns: 75 | dict: A dictionary containing the KPIs for the specified frequency. 76 | """ 77 | result = [{k: v} for k, v in self.data.items() if k == frequency.value] 78 | return result[0] if result else {} 79 | 80 | def clear_frequency(self, frequency: Timeframe): 81 | """ 82 | Clear all KPIs for a specific frequency. 83 | 84 | Args: 85 | frequency (Timeframe): The frequency for which to clear all KPIs. 86 | """ 87 | if frequency.value in self.data: 88 | self.data[frequency.value] = {} 89 | -------------------------------------------------------------------------------- /charts/templates/mosquitto.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 2 | # Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 3 | 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # SPDX-License-Identifier: Apache-2.0 17 | 18 | apiVersion: v1 19 | kind: ConfigMap 20 | metadata: 21 | name: mosquitto-config-file 22 | labels: 23 | app: mqtt_broker 24 | data: 25 | mosquitto.conf: |- 26 | listener 1883 0.0.0.0 27 | protocol websockets 28 | allow_anonymous true 29 | 30 | --- 31 | 32 | apiVersion: apps/v1 33 | kind: Deployment 34 | metadata: 35 | name: mqtt-broker 36 | namespace: {{ .Values.namespace.name }} 37 | labels: 38 | app: mqtt-broker 39 | spec: 40 | replicas: 1 41 | selector: 42 | matchLabels: 43 | app: mqtt-broker 44 | template: 45 | metadata: 46 | labels: 47 | app: mqtt-broker 48 | spec: 49 | volumes: 50 | - name: mosquitto-conf 51 | configMap: 52 | name: mosquitto-config-file 53 | items: 54 | - key: mosquitto.conf 55 | path: mosquitto.conf 56 | imagePullSecrets: 57 | - name: {{ .Values.images.nvcrSecret }} 58 | containers: 59 | - name: mqtt-broker 60 | image: eclipse-mosquitto 61 | volumeMounts: 62 | - name: mosquitto-conf 63 | mountPath: /mosquitto.conf 64 | subPath: mosquitto.conf 65 | args: ["mosquitto", "-c", "/mosquitto.conf"] 66 | imagePullPolicy: Always 67 | ports: 68 | - name: mqtt 69 | containerPort: 1883 70 | protocol: TCP 71 | 72 | --- 73 | 74 | apiVersion: v1 75 | kind: Service 76 | metadata: 77 | name: mqtt-broker 78 | namespace: {{ .Values.namespace.name }} 79 | labels: 80 | app: mqtt-broker 81 | spec: 82 | ports: 83 | - port: 1883 84 | targetPort: mqtt 85 | protocol: TCP 86 | name: mqtt 87 | selector: 88 | app: mqtt-broker 89 | 90 | --- 91 | 92 | apiVersion: networking.k8s.io/v1 93 | kind: Ingress 94 | metadata: 95 | name: mqtt-broker 96 | namespace: {{ .Values.namespace.name }} 97 | {{- with .Values.ingressAnnotations }} 98 | annotations: 99 | {{- toYaml . | nindent 4 }} 100 | {{- end }} 101 | spec: 102 | rules: 103 | - host: {{ .Values.hostDomainName }} 104 | http: 105 | paths: 106 | - path: {{ .Values.mqttUrl }}(/|$)(.*) 107 | pathType: Prefix 108 | backend: 109 | service: 110 | name: mqtt-broker 111 | port: 112 | number: 1883 113 | -------------------------------------------------------------------------------- /bzl/python.bzl: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2022 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | load("@python_third_party_linting//:requirements.bzl", "requirement") 20 | load("@io_bazel_rules_docker//python3:image.bzl", "py3_image") 21 | load("@io_bazel_rules_docker//container:container.bzl", "container_bundle", "container_push") 22 | 23 | def py_type_test(name, srcs, deps): 24 | native.py_test( 25 | name = name, 26 | main = "@com_nvidia_isaac_mission_dispatch//bzl:pytype.py", 27 | srcs = ["@com_nvidia_isaac_mission_dispatch//bzl:pytype.py"] + srcs, 28 | deps = deps + [requirement("mypy")], 29 | args = ["$(location {})".format(src) for src in srcs], 30 | tags = ["lint"], 31 | ) 32 | 33 | def py_lint_test(name, srcs): 34 | native.py_test( 35 | name = name, 36 | main = "@com_nvidia_isaac_mission_dispatch//bzl:pylint.py", 37 | srcs = ["@com_nvidia_isaac_mission_dispatch//bzl:pylint.py"] + srcs, 38 | deps = [requirement("pylint")], 39 | data = ["@com_nvidia_isaac_mission_dispatch//bzl:pylintrc"], 40 | args = ["--rcfile=$(location @com_nvidia_isaac_mission_dispatch//bzl:pylintrc)"] + 41 | ["$(location {})".format(src) for src in srcs], 42 | tags = ["lint"], 43 | ) 44 | 45 | def mission_dispatch_py_library(**kwargs): 46 | native.py_library(**kwargs) 47 | py_type_test( 48 | name = kwargs["name"] + "-type-test", 49 | srcs = kwargs.get("srcs", []), 50 | deps = kwargs.get("deps", []), 51 | ) 52 | py_lint_test( 53 | name = kwargs["name"] + "-lint-test", 54 | srcs = kwargs.get("srcs", []), 55 | ) 56 | 57 | def mission_dispatch_py_binary(**kwargs): 58 | native.py_binary(**kwargs) 59 | py_type_test( 60 | name = kwargs["name"] + "-type-test", 61 | srcs = kwargs.get("srcs", []), 62 | deps = kwargs.get("deps", []), 63 | ) 64 | py_lint_test( 65 | name = kwargs["name"] + "-lint-test", 66 | srcs = kwargs.get("srcs", []), 67 | ) 68 | 69 | image_kwargs = dict(**kwargs) 70 | if "main" not in image_kwargs: 71 | image_kwargs["main"] = image_kwargs["name"] + ".py" 72 | image_kwargs["name"] += "-img" 73 | 74 | py3_image( 75 | base = "@com_nvidia_isaac_mission_dispatch//bzl:python_base", 76 | **image_kwargs 77 | ) 78 | 79 | container_bundle( 80 | name = image_kwargs["name"] + "-bundle", 81 | images = { 82 | "bazel_image": image_kwargs["name"] 83 | }, 84 | visibility = ["//visibility:public"] 85 | ) 86 | -------------------------------------------------------------------------------- /packages/controllers/mission/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | import argparse 20 | import logging 21 | import sys 22 | 23 | from packages.controllers.mission import server as mission_server 24 | 25 | LOGGING_LEVELS = ["DEBUG", "INFO", "WARNING", "ERROR"] 26 | 27 | 28 | if __name__ == "__main__": 29 | parser = argparse.ArgumentParser() 30 | parser.add_argument("--mqtt_host", default="localhost", 31 | help="The hostname of the mqtt server to connect to") 32 | parser.add_argument("--mqtt_port", default=1883, type=int, 33 | help="The port of the mqtt server to connect to") 34 | parser.add_argument("--mqtt_transport", default="tcp", choices=("tcp", "websockets"), 35 | help="Set transport mechanism as WebSockets or raw TCP") 36 | parser.add_argument("--mqtt_ws_path", default=None, 37 | help="The path for the websocket if mqtt_transport is websockets") 38 | parser.add_argument("--mqtt_prefix", default="uagv/v2/RobotCompany", 39 | help="The prefix to add to all VDA5050 mqtt topics") 40 | parser.add_argument("--mqtt_username", default=None, 41 | help="The Username to authenticate to MQTT broker") 42 | parser.add_argument("--mqtt_password", default=None, 43 | help="The password to authenticate to MQTT broker") 44 | parser.add_argument("--database_url", default="http://localhost:5001", 45 | help="The url where the database REST API is hosted") 46 | parser.add_argument("--mission_ctrl_url", default=None, 47 | help="The url where the mission control REST API is hosted") 48 | parser.add_argument("--log_level", default="INFO", choices=LOGGING_LEVELS, 49 | help="The minimum level of log messages to print") 50 | parser.add_argument("--push_telemetry", action="store_true", help="Enable pushing telemetry") 51 | parser.add_argument("--telemetry_env", default="DEV", 52 | help="Environment to push telemetry to (DEV | TEST | PROD)") 53 | parser.add_argument("--disable_request_factsheet", action="store_true", 54 | help="Disable factsheet pulling") 55 | 56 | args = parser.parse_known_args()[0] 57 | logger = logging.getLogger("Isaac Mission Dispatch") 58 | logger.setLevel(args.log_level) 59 | logger.addHandler(logging.StreamHandler(sys.stderr)) 60 | del args.log_level 61 | server = mission_server.RobotServer(**vars(args)) 62 | server.run() 63 | -------------------------------------------------------------------------------- /packages/database/tests/postgres.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | import signal 20 | import unittest 21 | 22 | from packages.database import client as db_client 23 | from packages.utils import test_utils 24 | from packages.database.tests import test_base 25 | 26 | # The TCP port for the database to listen on 27 | DATABASE_PORT = 5021 28 | DATABASE_CONTROLLER_PORT = 5022 29 | # The TCP port for the postgres db to connect on 30 | POSTGRES_DATABASE_PORT = 5432 31 | 32 | 33 | class TestPostgresDatabase(test_base.TestDatabase): 34 | @classmethod 35 | def setUpClass(cls): 36 | if cls.has_process_crashed: 37 | raise ValueError("Can't run test due to previous failure") 38 | 39 | # Register signal handler 40 | signal.signal(signal.SIGUSR1, cls.catch_signal) 41 | 42 | # Create the database and wait some time for it to start up 43 | cls.postgres_database, postgres_address = \ 44 | cls.run_docker(cls, image="//packages/utils/test_utils:postgres-database-img-bundle", 45 | docker_args=["-e", "POSTGRES_PASSWORD=postgres", 46 | "-e", "POSTGRES_DB=mission", 47 | "--publish", F"{str(POSTGRES_DATABASE_PORT) 48 | }:{str(POSTGRES_DATABASE_PORT)}", 49 | "-e", "POSTGRES_INITDB_ARGS=--auth-host=scram-sha-256 --auth-local=scram-sha-256"], 50 | args=['postgres']) 51 | test_utils.wait_for_port(host=postgres_address, port=POSTGRES_DATABASE_PORT, timeout=120) 52 | print(f"Database setup done on {postgres_address}:{POSTGRES_DATABASE_PORT}") 53 | # Startup server API's 54 | cls.database, address = cls.run_docker(cls, image="//packages/database:postgres-img-bundle", 55 | args=["--port", str(DATABASE_PORT), 56 | "--controller_port", str( 57 | DATABASE_CONTROLLER_PORT), 58 | "--db_host", postgres_address, 59 | "--address", "0.0.0.0"]) 60 | cls.client = db_client.DatabaseClient(f"http://{address}:{DATABASE_PORT}") 61 | cls.controller_client = db_client.DatabaseClient( 62 | f"http://{address}:{DATABASE_CONTROLLER_PORT}") 63 | test_utils.wait_for_port(host=address, port=DATABASE_PORT, timeout=140) 64 | test_utils.wait_for_port(host=address, port=DATABASE_CONTROLLER_PORT, timeout=140) 65 | 66 | @classmethod 67 | def tearDownClass(cls): 68 | cls.close(cls, processes=[cls.database, cls.postgres_database]) 69 | 70 | 71 | if __name__ == "__main__": 72 | unittest.main() 73 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Isaac Mission Dispatch Security considerations 2 | Isaac provides a reference copy of Isaac Cloud Services which allow for API based submission of missions to enable Edge/Cloud control of robots. The reference is intended to work on a limited access workstation to demonstrate functionality and should not be considered secure. The following details outline security parameters to safely deploy locally and understand exercises needed for a secure production deployment. 3 | 4 | ## Containerized services 5 | All local services are provided as Docker Containers. Through the included docker compose files, you can understand and limit network access to your robotics fleet. 6 | 7 | ## Postgres 8 | Postgres is used by Mission Dispatch and Mission Database to store the state of a mission, state of robots, and facilitate state management for VDA5050. The default implementation provided is to use username/password. A production environment should access via encrypted channel, encrypt data at rest, restrict user access via eg (OIDC), among other standard postgres security practices. Please evaluate the network access parameters provided in the tutorial docker compose to ensure they are compatible with your organization's security policies. 9 | 10 | ## Docker and FastAPI access 11 | The Mission Dispatch API when launched by container is by default exposed via FastAPI. This API is provided unencrypted to allow you to understand and use the robot primitives. A production installation should involve securing this endpoint and adding both authentication and encryption. This will ensure only authorized users are controlling your robots. 12 | 13 | ## MQTT 14 | Mission Dispatch by default connects to Isaac Mission Client over MQTT and the vda5050 protocol. The default implementation is not secure and should be operated in a trusted network. Securing MQTT involves securing both the broker and the client. The default broker used is mosquitto and the default client is paho_mqtt. 15 | * Many MQTT security weaknesses are context-specific and cannot be enumerated, isolated from the deployed environment, without a proper risk assessment of this context. Such an analysis is recommended to any team that is utilizing MQTT. 16 | * The chosen broker should be inspected to minimally verify RELRO, Stack Canary, NX, PIE, and Fortify protections were enabled at build time. 17 | * Users should verify the latest implementation of paho_mqtt for vulnerabilities and update the requirements.txt to include any relevant patches. 18 | * Users should follow a guide to securing MQTT which includes enabling TLS/SSL encryption, a trusted CA certificate, mutual verification, etc. Brokers also include additional features you may require like Access Control Lists, token based authentication, rate limiting, and more. 19 | 20 | 21 | ## Report a Security Vulnerability 22 | 23 | To report a potential security vulnerability in any NVIDIA product, please use either: 24 | * This web form: [Security Vulnerability Submission Form](https://www.nvidia.com/object/submit-security-vulnerability.html), or 25 | * Send email to: [NVIDIA PSIRT](mailto:psirt@nvidia.com) 26 | 27 | **OEM Partners should contact their NVIDIA Customer Program Manager** 28 | 29 | If reporting a potential vulnerability via email, please encrypt it using NVIDIA’s public PGP key ([see PGP Key page](https://www.nvidia.com/en-us/security/pgp-key/)) and include the following information: 30 | * Product/Driver name and version/branch that contains the vulnerability 31 | * Type of vulnerability (code execution, denial of service, buffer overflow, etc.) 32 | * Instructions to reproduce the vulnerability 33 | * Proof-of-concept or exploit code 34 | * Potential impact of the vulnerability, including how an attacker could exploit the vulnerability 35 | 36 | See https://www.nvidia.com/en-us/security/ for past NVIDIA Security Bulletins and Notices. 37 | -------------------------------------------------------------------------------- /charts/templates/mission-database.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 2 | # Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 3 | 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | # SPDX-License-Identifier: Apache-2.0 17 | 18 | apiVersion: apps/v1 19 | kind: Deployment 20 | metadata: 21 | name: mission-dispatch-database 22 | namespace: {{ .Values.namespace.name }} 23 | labels: 24 | app: mission-dispatch-database 25 | spec: 26 | replicas: 1 27 | selector: 28 | matchLabels: 29 | app: mission-dispatch-database 30 | template: 31 | metadata: 32 | labels: 33 | app: mission-dispatch-database 34 | spec: 35 | imagePullSecrets: 36 | - name: {{ .Values.images.nvcrSecret }} 37 | containers: 38 | - name: mission-dispatch-database 39 | image: {{ .Values.images.missionDatabase }} 40 | command: ["sh"] 41 | args: 42 | - -c 43 | - | 44 | /app/packages/database/postgres-img.binary --root_path {{ .Values.missionUrl }} \ 45 | --address 0.0.0.0 --port 5000 --controller_port 5001 --db_host {{ .Values.dbHostName }} \ 46 | --db_port {{ .Values.dbPort }} --db_username $DB_USERNAME --db_password $DB_PASSWORD 47 | imagePullPolicy: Always 48 | env: 49 | - name: "DB_USERNAME" 50 | valueFrom: 51 | secretKeyRef: 52 | key: db_username 53 | name: postgres-secret 54 | - name: "DB_PASSWORD" 55 | valueFrom: 56 | secretKeyRef: 57 | key: db_password 58 | name: postgres-secret 59 | ports: 60 | - name: http 61 | containerPort: 5000 62 | protocol: TCP 63 | - name: http-internal 64 | containerPort: 5001 65 | protocol: TCP 66 | 67 | --- 68 | 69 | apiVersion: v1 70 | kind: Service 71 | metadata: 72 | name: mission-dispatch-database 73 | namespace: {{ .Values.namespace.name }} 74 | labels: 75 | app: mission-dispatch-database 76 | spec: 77 | ports: 78 | - port: 80 79 | targetPort: http 80 | protocol: TCP 81 | name: http 82 | selector: 83 | app: mission-dispatch-database 84 | 85 | --- 86 | 87 | apiVersion: v1 88 | kind: Service 89 | metadata: 90 | name: mission-dispatch-database-internal 91 | namespace: {{ .Values.namespace.name }} 92 | labels: 93 | app: mission-dispatch-database 94 | spec: 95 | ports: 96 | - port: 80 97 | targetPort: http-internal 98 | protocol: TCP 99 | name: http 100 | selector: 101 | app: mission-dispatch-database 102 | 103 | --- 104 | 105 | apiVersion: networking.k8s.io/v1 106 | kind: Ingress 107 | metadata: 108 | name: mission-dispatch-database 109 | namespace: {{ .Values.namespace.name }} 110 | {{- with .Values.ingressAnnotations }} 111 | annotations: 112 | {{- toYaml . | nindent 4 }} 113 | {{- end }} 114 | spec: 115 | rules: 116 | - host: {{ .Values.hostDomainName }} 117 | http: 118 | paths: 119 | - path: {{ .Values.missionUrl }}(/|$)(.*) 120 | pathType: Prefix 121 | backend: 122 | service: 123 | name: mission-dispatch-database 124 | port: 125 | number: 80 126 | -------------------------------------------------------------------------------- /packages/controllers/mission/tests/start_order.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | import time 20 | import unittest 21 | 22 | from cloud_common import objects as api_objects 23 | from packages.controllers.mission.tests import client as simulator 24 | from cloud_common.objects import mission as mission_object 25 | from cloud_common.objects import robot as robot_object 26 | 27 | from packages.controllers.mission.tests import test_context 28 | 29 | 30 | class TestMissions(unittest.TestCase): 31 | def run_single_mission(self, ctx: test_context.TestContext): 32 | """ Helper function to run a simple mission on a single robot """ 33 | 34 | # Waypoint for a scenario that will be reused for different test cases 35 | MISSION_WAYPOINT_X = 30.0 36 | MISSION_WAYPOINT_Y = 30.0 37 | 38 | # Create the robot and then the mission 39 | ctx.db_client.create( 40 | api_objects.RobotObjectV1(name="test01", status={})) 41 | time.sleep(0.25) 42 | ctx.db_client.create(test_context.mission_from_waypoint( 43 | "test01", MISSION_WAYPOINT_X, MISSION_WAYPOINT_Y)) 44 | time.sleep(0.25) 45 | 46 | # Make sure the mission is done. 47 | # The result can be either completed or failed based on state of robot client 48 | completed = False 49 | for update in ctx.db_client.watch(api_objects.MissionObjectV1): 50 | if update.status.state.done: 51 | completed = True 52 | break 53 | self.assertTrue(completed) 54 | 55 | def test_mission_dispatch_slow(self): 56 | """ Test the case where the mission dispatch starts last """ 57 | robot = simulator.RobotInit("test01", 0, 0, 0) 58 | delay = test_context.Delay(mission_dispatch=10) 59 | with test_context.TestContext([robot], delay=delay, enforce_start_order=False) as ctx: 60 | ctx.wait_for_database() 61 | self.run_single_mission(ctx) 62 | 63 | def test_mission_simulator_slow(self): 64 | """ Test the case where the mission simulator starts last """ 65 | robot = simulator.RobotInit("test01", 0, 0, 0) 66 | delay = test_context.Delay(mission_simulator=10) 67 | with test_context.TestContext([robot], delay=delay, enforce_start_order=False) as ctx: 68 | ctx.wait_for_database() 69 | self.run_single_mission(ctx) 70 | 71 | def test_mqtt_broker_slow(self): 72 | """ Test the case where the mqtt broker starts last """ 73 | robot = simulator.RobotInit("test01", 0, 0, 0) 74 | delay = test_context.Delay(mqtt_broker=10) 75 | with test_context.TestContext([robot], delay=delay, enforce_start_order=False) as ctx: 76 | ctx.wait_for_database() 77 | self.run_single_mission(ctx) 78 | 79 | def test_mission_database_slow(self): 80 | """ Test the case where the mission database starts last """ 81 | robot = simulator.RobotInit("test01", 0, 0, 0) 82 | delay = test_context.Delay(mission_database=10) 83 | with test_context.TestContext([robot], delay=delay, enforce_start_order=False) as ctx: 84 | ctx.wait_for_database() 85 | self.run_single_mission(ctx) 86 | 87 | 88 | if __name__ == "__main__": 89 | unittest.main() 90 | -------------------------------------------------------------------------------- /cloud_common/objects/detection_results.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2023-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | 20 | from typing import Any, Dict, List, Optional 21 | 22 | import pydantic 23 | 24 | from cloud_common.objects import object 25 | 26 | 27 | class Point3D(pydantic.BaseModel): 28 | x: float = 0 29 | y: float = 0 30 | z: float = 0 31 | 32 | 33 | class Quaternion(pydantic.BaseModel): 34 | w: float = 0 35 | x: float = 0 36 | y: float = 0 37 | z: float = 0 38 | 39 | 40 | class Pose3D(pydantic.BaseModel): 41 | position: Point3D 42 | orientation: Quaternion 43 | 44 | 45 | class DetectedObjectCenter2D(pydantic.BaseModel): 46 | x: float = 0 47 | y: float = 0 48 | theta: float = 0 49 | 50 | 51 | class DetectedObjectBoundingBox2D(pydantic.BaseModel): 52 | center: Optional[DetectedObjectCenter2D] = None 53 | size_x: float = 0 54 | size_y: float = 0 55 | 56 | 57 | class DetectedObjectBoundingBox3D(pydantic.BaseModel): 58 | center: Optional[Pose3D] = None 59 | size_x: float = 0 60 | size_y: float = 0 61 | size_z: float = 0 62 | 63 | 64 | class DetectedObject(pydantic.BaseModel): 65 | """Represents the detected object from mission client""" 66 | bbox2d: Optional[DetectedObjectBoundingBox2D] = None 67 | bbox3d: Optional[DetectedObjectBoundingBox3D] = None 68 | object_id: int = 0 69 | class_id: str = '' 70 | 71 | @pydantic.root_validator 72 | def check_f1_f2(cls, values): 73 | bbox_2d = values.get('bbox2d') 74 | bbox_3d = values.get('bbox3d') 75 | if bbox_2d is None and bbox_3d is None: 76 | raise ValueError('Either bbox2d or bbox3d must be provided.') 77 | return values 78 | 79 | 80 | class DetectionResultsStatusV1(pydantic.BaseModel): 81 | """Represents the status of the robot's object detector.""" 82 | # A string containing JSON information about all detected 83 | # objects associated with the paired robot 84 | 85 | # The information will include bounding box information, class, 86 | # and ID. 87 | 88 | detected_objects: List[DetectedObject] = [] 89 | 90 | 91 | class DetectionResultsSpecV1(pydantic.BaseModel): 92 | """Specifies constant properties about the object detector, such as its name.""" 93 | pass 94 | 95 | 96 | class DetectionResultsQueryParamsV1(pydantic.BaseModel): 97 | """Specifies the supported query parameters allowed for obj detectors""" 98 | pass 99 | 100 | 101 | class DetectionResultsObjectV1(DetectionResultsSpecV1, object.ApiObject): 102 | """Represents an object detector.""" 103 | status: DetectionResultsStatusV1 = DetectionResultsStatusV1() 104 | 105 | @classmethod 106 | def get_alias(cls) -> str: 107 | return 'detection_results' 108 | 109 | @classmethod 110 | def get_spec_class(cls) -> Any: 111 | return DetectionResultsSpecV1 112 | 113 | @classmethod 114 | def get_status_class(cls) -> Any: 115 | return DetectionResultsStatusV1 116 | 117 | @classmethod 118 | def default_spec(cls) -> Dict: 119 | return DetectionResultsSpecV1().dict() # type: ignore 120 | 121 | @classmethod 122 | def get_query_params(cls) -> Any: 123 | return DetectionResultsQueryParamsV1 124 | 125 | @staticmethod 126 | def get_query_map() -> Dict: 127 | return { 128 | 129 | } 130 | 131 | @classmethod 132 | def supports_spec_update(cls) -> bool: 133 | return False 134 | -------------------------------------------------------------------------------- /packages/controllers/mission/tests/delete_robot.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | import time 20 | import unittest 21 | 22 | from cloud_common import objects as api_objects 23 | from packages.controllers.mission.tests import client as simulator 24 | from cloud_common.objects import mission as mission_object 25 | from cloud_common.objects import robot as robot_object 26 | 27 | from packages.controllers.mission.tests import test_context 28 | 29 | 30 | class TestDeleteRobot(unittest.TestCase): 31 | def test_delete_idle_robot(self): 32 | """ Test if an idle robot is correctly deleted """ 33 | robot = simulator.RobotInit("test01", 0, 0, 0) 34 | with test_context.TestContext([robot]) as ctx: 35 | # Create the robot 36 | ctx.db_client.create( 37 | api_objects.RobotObjectV1(name="test01", status={})) 38 | time.sleep(0.25) 39 | # Check that the robot has been populated in the database 40 | self.assertGreater( 41 | len(ctx.db_client.list(api_objects.RobotObjectV1)), 0) 42 | 43 | # Delete robot 44 | ctx.db_client.delete(api_objects.RobotObjectV1, "test01") 45 | time.sleep(10) 46 | 47 | # Check to see if the robot is gone from the database 48 | self.assertEqual( 49 | len(ctx.db_client.list(api_objects.RobotObjectV1)), 0) 50 | 51 | def test_delete_on_task_robot(self): 52 | """ Test if the server kills the robot correctly when the robot is executing a mission """ 53 | MISSION_DEFAULT_X = 50 54 | MISSION_DEFAULT_Y = 50 55 | robot = simulator.RobotInit("test01", 0, 0, 0) 56 | with test_context.TestContext([robot], tick_period=1.0) as ctx: 57 | # Create the robot 58 | ctx.db_client.create( 59 | api_objects.RobotObjectV1(name="test01", status={})) 60 | time.sleep(0.25) 61 | self.assertGreater( 62 | len(ctx.db_client.list(api_objects.RobotObjectV1)), 0) 63 | mission = test_context.mission_from_waypoint( 64 | "test01", MISSION_DEFAULT_X, MISSION_DEFAULT_Y) 65 | ctx.db_client.create(mission) 66 | 67 | # Watch, and break when robot is officially ON_TASK / mission is RUNNING 68 | for update in ctx.db_client.watch(api_objects.RobotObjectV1): 69 | if update.status.state == robot_object.RobotStateV1.ON_TASK: 70 | break 71 | 72 | ctx.db_client.delete(api_objects.RobotObjectV1, "test01") 73 | robot_objects = ctx.db_client.list(api_objects.RobotObjectV1) 74 | 75 | # Robot should not be deleted yet 76 | self.assertGreater(len(robot_objects), 0) 77 | self.assertEqual( 78 | robot_objects[0].lifecycle, api_objects.object.ObjectLifecycleV1.PENDING_DELETE) 79 | for update in ctx.db_client.watch(api_objects.MissionObjectV1): 80 | # mission should still be set to failed once the robot is pending delete 81 | if update.status.state.done: 82 | self.assertEqual(update.status.state, 83 | mission_object.MissionStateV1.FAILED) 84 | break 85 | time.sleep(1) 86 | 87 | # The mission is finished, so the robot should be deleted 88 | robot_objects = ctx.db_client.list(api_objects.RobotObjectV1) 89 | self.assertEqual(len(robot_objects), 0) 90 | 91 | 92 | if __name__ == "__main__": 93 | unittest.main() 94 | -------------------------------------------------------------------------------- /packages/controllers/mission/tests/BUILD: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | load("//bzl:python.bzl", "mission_dispatch_py_binary") 20 | load("//bzl:python.bzl", "mission_dispatch_py_library") 21 | load("@python_third_party//:requirements.bzl", "requirement") 22 | 23 | mission_dispatch_py_binary( 24 | name = "client", 25 | srcs = ["client.py"], 26 | deps = [ 27 | "//packages/controllers/mission/vda5050_types", 28 | requirement("paho-mqtt"), 29 | requirement("pydantic") 30 | ], 31 | visibility = ["//visibility:public"] 32 | ) 33 | 34 | py_library( 35 | name = "test_context", 36 | srcs = [ 37 | "test_context.py" 38 | ], 39 | deps = [ 40 | "//packages/database:client", 41 | "//packages/utils/test_utils", 42 | ":client", 43 | requirement("requests") 44 | ], 45 | data = [ 46 | "//packages/controllers/mission:mission-img-bundle", 47 | "//packages/controllers/mission/tests:client-img-bundle", 48 | "//packages/database:postgres-img-bundle", 49 | "//packages/utils/test_utils:postgres-database-img-bundle", 50 | "//packages/utils/test_utils:mosquitto-img-bundle" 51 | ], 52 | ) 53 | 54 | mission_dispatch_py_library( 55 | name = "mission_examples", 56 | srcs = [ 57 | "mission_examples.py" 58 | ], 59 | ) 60 | 61 | py_test( 62 | name = "cancel_mission", 63 | srcs = [ 64 | "cancel_mission.py" 65 | ], 66 | deps = [ 67 | ":test_context", 68 | ], 69 | tags = [ 70 | "exclusive" 71 | ], 72 | size = "large" 73 | ) 74 | 75 | py_test( 76 | name = "delete_robot", 77 | srcs = [ 78 | "delete_robot.py" 79 | ], 80 | deps = [ 81 | ":test_context", 82 | ], 83 | tags = [ 84 | "exclusive" 85 | ], 86 | size = "large" 87 | ) 88 | 89 | py_test( 90 | name = "fail_robot", 91 | srcs = [ 92 | "fail_robot.py" 93 | ], 94 | deps = [ 95 | ":test_context", 96 | ], 97 | tags = [ 98 | "exclusive" 99 | ], 100 | size = "large" 101 | ) 102 | 103 | py_test( 104 | name = "mission", 105 | srcs = [ 106 | "mission.py" 107 | ], 108 | deps = [ 109 | ":test_context", 110 | ":mission_examples", 111 | ], 112 | tags = [ 113 | "exclusive" 114 | ], 115 | size = "large" 116 | ) 117 | 118 | py_test( 119 | name = "robot", 120 | srcs = [ 121 | "robot.py" 122 | ], 123 | deps = [ 124 | ":test_context" 125 | ], 126 | tags = [ 127 | "exclusive" 128 | ], 129 | size = "large" 130 | ) 131 | 132 | py_test( 133 | name = "start_order", 134 | srcs = [ 135 | "start_order.py" 136 | ], 137 | deps = [ 138 | ":test_context", 139 | ], 140 | tags = [ 141 | "exclusive" 142 | ], 143 | size = "large" 144 | ) 145 | 146 | py_test( 147 | name = "mission_tree", 148 | srcs = [ 149 | "mission_tree.py" 150 | ], 151 | deps = [ 152 | ":test_context", 153 | ], 154 | tags = [ 155 | "exclusive" 156 | ], 157 | size = "large" 158 | ) 159 | 160 | py_test( 161 | name = "server", 162 | srcs = [ 163 | "server.py" 164 | ], 165 | deps = [ 166 | ":test_context", 167 | ], 168 | tags = [ 169 | "exclusive" 170 | ], 171 | size = "large" 172 | ) 173 | 174 | py_test( 175 | name = "update_mission", 176 | srcs = [ 177 | "update_mission.py" 178 | ], 179 | deps = [ 180 | ":test_context", 181 | ], 182 | tags = [ 183 | "exclusive" 184 | ], 185 | size = "large" 186 | ) 187 | 188 | py_test( 189 | name = "retrieve_factsheet", 190 | srcs = [ 191 | "retrieve_factsheet.py" 192 | ], 193 | deps = [ 194 | ":test_context", 195 | ], 196 | tags = [ 197 | "exclusive" 198 | ], 199 | size = "large" 200 | ) -------------------------------------------------------------------------------- /packages/utils/test_utils/docker.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | import re 20 | import subprocess 21 | from typing import List, Tuple, Union 22 | import uuid 23 | import time 24 | 25 | # Top level bash script to run as init process (PID 1) in each docker container to make sure that 26 | # the docker container exits when the calling python process exits 27 | SH_TEMPLATE = """ 28 | EXIT_CODE_FILE=$(mktemp) 29 | cleanup() { 30 | EXIT_CODE=$(cat $EXIT_CODE_FILE) 31 | exit $EXIT_CODE 32 | } 33 | trap cleanup INT 34 | ( COMMAND ; echo $? > $EXIT_CODE_FILE ; kill -s INT $$ ) & 35 | read _ 36 | """ 37 | 38 | # How often to poll to see if a container is running 39 | CONTAINER_CHECK_PERIOD = 0.1 40 | 41 | 42 | def check_container_running(name: str) -> bool: 43 | result = subprocess.run(["docker", "container", "inspect", name], # pylint: disable=subprocess-run-check 44 | stdout=subprocess.PIPE, 45 | stderr=subprocess.PIPE) 46 | return result.returncode == 0 47 | 48 | def wait_for_container(name: str, timeout: float = float("inf")): 49 | end_time = time.time() + timeout 50 | while time.time() < end_time: 51 | if check_container_running(name): 52 | return 53 | time.sleep(CONTAINER_CHECK_PERIOD) 54 | raise ValueError("Container did not start in time") 55 | 56 | def get_container_ip(name: str) -> str: 57 | process = subprocess.run(["docker", "inspect", "-f", 58 | "{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}", name], 59 | stdout=subprocess.PIPE, check=True) 60 | return process.stdout.decode("utf-8") 61 | 62 | def run_docker_target(bazel_target: str, args: Union[List[str], None] = None, 63 | docker_args: Union[List[str], None] = None, 64 | start_timeout: float = 120, 65 | delay: int = 0) -> Tuple[subprocess.Popen, str]: 66 | # Set default arguments 67 | if args is None: 68 | args = [] 69 | 70 | # Get the path of the bazel image 71 | regex = r"//(.+):(.+)" 72 | match = re.match(regex, bazel_target) 73 | if not match: 74 | raise ValueError(f"bazel_target \"{bazel_target}\" does not match regex: \"{regex}\"") 75 | package, target = match.groups() 76 | bundle_script = f"{package}/{target}" 77 | 78 | # Run the bundle script to add the image to the docker daemon, and get the hash 79 | bundle_result = subprocess.run([bundle_script], stdout=subprocess.PIPE, check=True) 80 | image_hash_match = re.search(r"Tagging (.+) as", bundle_result.stdout.decode("utf-8")) 81 | if not image_hash_match: 82 | raise ValueError(f"Could not determine image hash for target {bazel_target}") 83 | image_hash = image_hash_match.groups()[0] 84 | 85 | # Get the entrypoint command 86 | result = subprocess.run(["docker", "inspect", "-f", "{{.Config.Entrypoint}}", image_hash], 87 | stdout=subprocess.PIPE, check=True).stdout.decode("utf-8") 88 | args = result[1:-2].split(" ") + args 89 | if delay != 0: 90 | args = ["sleep", str(delay), ";"] + args 91 | 92 | # Run a the container inside a special bash script that will exit if 93 | # the calling process dies, so the container will always exit 94 | name = f"bazel-test-{str(uuid.uuid4())}" 95 | script = SH_TEMPLATE.replace("COMMAND", " ".join(args)) 96 | docker_cmd = ["docker", "run", "-i", "--rm", "--entrypoint", "sh", "--name", name] 97 | if docker_args is not None: 98 | docker_cmd.extend(docker_args) 99 | docker_cmd.extend([image_hash, "-c", script]) 100 | print(" ".join(docker_cmd), flush=True) 101 | process = subprocess.Popen(docker_cmd, stdin=subprocess.PIPE) # pylint: disable=consider-using-with 102 | try: 103 | wait_for_container(name, timeout=start_timeout) 104 | address = get_container_ip(name).strip() 105 | except: 106 | process.kill() 107 | raise 108 | return process, address 109 | -------------------------------------------------------------------------------- /packages/controllers/mission/tests/fail_robot.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | import time 20 | import unittest 21 | import uuid 22 | 23 | from cloud_common import objects as api_objects 24 | from packages.controllers.mission.tests import client as simulator 25 | from cloud_common.objects import mission as mission_object 26 | from cloud_common.objects import robot as robot_object 27 | 28 | from packages.controllers.mission.tests import test_context 29 | 30 | # Waypoint for a mission that will be reused for many tests 31 | DEFAULT_MISSION_X = 10.0 32 | DEFAULT_MISSION_Y = 10.0 33 | 34 | # Definition for mission `SCENARIO1` with multiple waypoints 35 | SCENARIO1_WAYPOINTS = [ 36 | (1, 1), 37 | (10, 10), 38 | (5, 5), 39 | ] 40 | 41 | # Expected progression of mission state for the mission `SCENARIO1` 42 | SCENARIO1_EXPECTED_STATUSES = [ 43 | mission_object.MissionStatusV1(state="PENDING", current_node=0), 44 | mission_object.MissionStatusV1(state="RUNNING", current_node=0), 45 | mission_object.MissionStatusV1(state="RUNNING", current_node=1), 46 | mission_object.MissionStatusV1(state="RUNNING", current_node=2), 47 | mission_object.MissionStatusV1(state="COMPLETED", current_node=2), 48 | ] 49 | 50 | SCENARIO2_WAYPOINTS = [ 51 | (1, 1), 52 | (10, 10), 53 | (5, 5), 54 | ] 55 | 56 | SCENARIO2_EXPECTED_STATUSES = [ 57 | mission_object.MissionStatusV1(state="PENDING", current_node=0), 58 | mission_object.MissionStatusV1(state="RUNNING", current_node=0), 59 | mission_object.MissionStatusV1(state="FAILED", current_node=0), 60 | ] 61 | 62 | 63 | class TestMissions(unittest.TestCase): 64 | def test_warning_mission(self): 65 | """ Test sending a single mission to a single robot that always is a warning """ 66 | robot = simulator.RobotInit("warning_robot01", 0, 0, 0, "map", 1) 67 | with test_context.TestContext([robot], fail_as_warning=True) as ctx: 68 | # Create the robot and then the mission 69 | ctx.db_client.create(api_objects.RobotObjectV1( 70 | name="warning_robot01", status={})) 71 | time.sleep(0.25) 72 | ctx.db_client.create(test_context.mission_from_waypoints( 73 | "warning_robot01", SCENARIO1_WAYPOINTS)) 74 | 75 | # Make sure the mission is updated and completed 76 | for expected_state, update in zip(SCENARIO1_EXPECTED_STATUSES, 77 | ctx.db_client.watch(api_objects.MissionObjectV1)): 78 | self.assertEqual(update.status.state, expected_state.state) 79 | self.assertEqual(update.status.current_node, 80 | expected_state.current_node) 81 | 82 | # Make sure the robot is at the last position in the list of waypoints 83 | robot_status = ctx.db_client.get( 84 | api_objects.RobotObjectV1, "warning_robot01").status 85 | self.assertEqual(robot_status.pose.x, SCENARIO1_WAYPOINTS[-1][0]) 86 | self.assertEqual(robot_status.pose.y, SCENARIO1_WAYPOINTS[-1][0]) 87 | 88 | def test_fatal_mission(self): 89 | """ Test a single mission to a single robot that always is fatal """ 90 | robot = simulator.RobotInit("fatal_robot01", 0, 0, 0, "map", 1) 91 | with test_context.TestContext([robot]) as ctx: 92 | # Create the robot and then the mission 93 | ctx.db_client.create(api_objects.RobotObjectV1( 94 | name="fatal_robot01", status={})) 95 | time.sleep(0.25) 96 | ctx.db_client.create(test_context.mission_from_waypoints( 97 | "fatal_robot01", SCENARIO2_WAYPOINTS)) 98 | 99 | # Make sure the mission is updated and completed 100 | for expected_state, update in zip(SCENARIO2_EXPECTED_STATUSES, 101 | ctx.db_client.watch(api_objects.MissionObjectV1)): 102 | self.assertEqual(update.status.state, expected_state.state) 103 | self.assertEqual(update.status.current_node, 104 | expected_state.current_node) 105 | 106 | 107 | if __name__ == "__main__": 108 | unittest.main() 109 | -------------------------------------------------------------------------------- /packages/database/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | 20 | import json 21 | from typing import Any, List, Optional, Dict 22 | import uuid 23 | import requests 24 | import logging 25 | 26 | from cloud_common import objects 27 | from cloud_common.objects.mission import MissionObjectV1, MissionRouteNodeV1 28 | from cloud_common.objects.detection_results import DetectionResultsObjectV1 29 | from cloud_common.objects.robot import RobotObjectV1 30 | from cloud_common.objects import common 31 | 32 | 33 | class DatabaseClient: 34 | """A connection to the centralized database where all api objects are stored""" 35 | 36 | def __init__(self, url: str = "http://localhost:5000"): 37 | self._url = url 38 | self._publisher_id = str(uuid.uuid4()) 39 | self._logger = logging.getLogger("Isaac Mission Dispatch") 40 | 41 | def create(self, obj: objects.ApiObject): 42 | url = f"{self._url}/{obj.get_alias()}" 43 | fields = json.loads(obj.spec.json()) 44 | fields["name"] = obj.name 45 | response = requests.post(url, json=fields, params={ 46 | "publisher_id": self._publisher_id}) 47 | common.handle_response(response) 48 | 49 | def update_spec(self, obj: objects.ApiObject): 50 | url = f"{self._url}/{obj.get_alias()}/{obj.name}" 51 | response = requests.put(url, json=json.loads(obj.spec.json()), 52 | params={"publisher_id": self._publisher_id}) 53 | common.handle_response(response) 54 | 55 | def update_status(self, obj: objects.ApiObject): 56 | url = f"{self._url}/{obj.get_alias()}/{obj.name}" 57 | response = requests.put(url, json={"status": json.loads(obj.status.json())}, 58 | params={"publisher_id": self._publisher_id}) 59 | common.handle_response(response) 60 | 61 | def list(self, object_type: Any, params: Optional[Dict] = None) -> List[objects.ApiObject]: 62 | url = f"{self._url}/{object_type.get_alias()}" 63 | response = requests.get(url, params=params) 64 | common.handle_response(response) 65 | return [object_type(**obj) for obj in json.loads(response.text)] 66 | 67 | def get(self, object_type: Any, name: str) -> objects.ApiObject: 68 | url = f"{self._url}/{object_type.get_alias()}/{name}" 69 | response = requests.get(url) 70 | common.handle_response(response) 71 | return object_type(**json.loads(response.text)) 72 | 73 | def watch(self, object_type: Any): 74 | url = f"{self._url}/{object_type.get_alias()}/watch" 75 | response = requests.get(url, stream=True, params={ 76 | "publisher_id": self._publisher_id}) 77 | for i in response.iter_lines(): 78 | yield object_type(**json.loads(i)) 79 | 80 | def delete(self, object_type: Any, name: str): 81 | url = f"{self._url}/{object_type.get_alias()}/{name}" 82 | response = requests.delete(url) 83 | common.handle_response(response) 84 | if object_type == RobotObjectV1: 85 | try: 86 | self.get(DetectionResultsObjectV1, name) 87 | self._logger.info( 88 | "Deleting corresponding detection results object.") 89 | url = f"{self._url}/detection_results/{name}" 90 | response = requests.delete(url) 91 | common.handle_response(response) 92 | except objects.common.ICSUsageError as e: 93 | self._logger.info( 94 | "Caught error (deleting non-existent database object): %s", e) 95 | 96 | def cancel_mission(self, name: str): 97 | url = f"{self._url}/{MissionObjectV1.get_alias()}/{name}/cancel" 98 | response = requests.post(url) 99 | common.handle_response(response) 100 | 101 | def update_mission(self, name: str, update_nodes: Dict[str, MissionRouteNodeV1]): 102 | url = f"{self._url}/{MissionObjectV1.get_alias()}/{name}/update" 103 | response = requests.post(url, json=update_nodes, 104 | params={"publisher_id": self._publisher_id}) 105 | common.handle_response(response) 106 | 107 | def is_running(self, timeout: int = 5) -> bool: 108 | url = f"{self._url}/health" 109 | try: 110 | response = requests.get(url, timeout=timeout) 111 | if response.status_code == 200: 112 | return True 113 | except requests.ConnectionError: 114 | return False 115 | except requests.Timeout: 116 | return False 117 | return False 118 | -------------------------------------------------------------------------------- /packages/controllers/mission/tests/server.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | import time 20 | import unittest 21 | 22 | from cloud_common import objects as api_objects 23 | from packages.controllers.mission.tests import client as simulator 24 | from cloud_common.objects import mission as mission_object 25 | from packages.controllers.mission.tests import test_context 26 | 27 | # Definition for mission `SCENARIO1` with multiple waypoints 28 | SCENARIO1_WAYPOINTS = [ 29 | (1, 1), 30 | (5, 5), 31 | ] 32 | 33 | # Expected progression of mission state for the mission `SCENARIO1` 34 | SCENARIO1_EXPECTED_STATUSES = [ 35 | mission_object.MissionStatusV1(state="PENDING", current_node=0), 36 | mission_object.MissionStatusV1(state="RUNNING", current_node=0), 37 | mission_object.MissionStatusV1(state="RUNNING", current_node=1), 38 | mission_object.MissionStatusV1(state="COMPLETED", current_node=1), 39 | ] 40 | 41 | 42 | class TestMissionServer(unittest.TestCase): 43 | def test_client_update_freq(self): 44 | """ Test a mission with different update frequencies of the client simulator """ 45 | tick_periods = [1, 0.1, 0.01] 46 | for tick_period in tick_periods: 47 | robot = simulator.RobotInit("test01", 0, 0, 0) 48 | with test_context.TestContext([robot], tick_period=tick_period) as ctx: 49 | # Create the robot and then the mission 50 | ctx.db_client.create( 51 | api_objects.RobotObjectV1(name="test01", status={})) 52 | time.sleep(0.25) 53 | ctx.db_client.create(test_context.mission_from_waypoints("test01", 54 | SCENARIO1_WAYPOINTS)) 55 | 56 | # Make sure the mission is updated and completed 57 | for expected_state, update in zip(SCENARIO1_EXPECTED_STATUSES, 58 | ctx.db_client.watch(api_objects.MissionObjectV1)): 59 | self.assertEqual(update.status.state, expected_state.state) 60 | self.assertEqual(update.status.current_node, 61 | expected_state.current_node) 62 | 63 | def test_restart_from_database(self): 64 | """ Test if MD can restart from the database """ 65 | robot = simulator.RobotInit("test01", 0, 0, 0) 66 | restart_once = False 67 | with test_context.TestContext([robot]) as ctx: 68 | # Create the robot and then the mission 69 | ctx.db_client.create( 70 | api_objects.RobotObjectV1(name="test01", status={})) 71 | time.sleep(0.25) 72 | ctx.db_client.create(test_context.mission_from_waypoints( 73 | "test01", SCENARIO1_WAYPOINTS)) 74 | 75 | # Make sure the mission is updated and completed 76 | completed = False 77 | watcher = ctx.db_client.watch(api_objects.MissionObjectV1) 78 | for update in watcher: 79 | if not restart_once and update.status.state == "RUNNING": 80 | ctx.restart_mission_server() 81 | print("Restart mission server") 82 | restart_once = True 83 | continue 84 | if update.status.state == mission_object.MissionStateV1.COMPLETED: 85 | completed = True 86 | break 87 | self.assertTrue(completed) 88 | 89 | def test_mqtt_reconnection(self): 90 | """ Test if MD is able to handle MQTT reconnection """ 91 | robot = simulator.RobotInit("test01", 0, 0, 0) 92 | restart_once = False 93 | with test_context.TestContext([robot]) as ctx: 94 | # Create the robot and then the mission 95 | ctx.db_client.create( 96 | api_objects.RobotObjectV1(name="test01", status={})) 97 | time.sleep(0.25) 98 | ctx.db_client.create(test_context.mission_from_waypoints( 99 | "test01", SCENARIO1_WAYPOINTS)) 100 | 101 | # Make sure the mission is updated and completed 102 | completed = False 103 | watcher = ctx.db_client.watch(api_objects.MissionObjectV1) 104 | for update in watcher: 105 | if not restart_once and update.status.state == "RUNNING": 106 | ctx.restart_mqtt_server() 107 | print("Restart the Mosquitto broker") 108 | restart_once = True 109 | continue 110 | if update.status.state == mission_object.MissionStateV1.COMPLETED: 111 | completed = True 112 | break 113 | self.assertTrue(completed) 114 | 115 | 116 | if __name__ == "__main__": 117 | unittest.main() 118 | -------------------------------------------------------------------------------- /cloud_common/objects/robot.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | import datetime 20 | import enum 21 | from typing import Any, Dict, List, Optional 22 | 23 | import pydantic 24 | from fastapi import Query 25 | from pydantic import Field 26 | 27 | from cloud_common.objects import common, object 28 | 29 | 30 | class RobotStateV1(enum.Enum): 31 | """Robot state 32 | """ 33 | IDLE = "IDLE" 34 | ON_TASK = "ON_TASK" 35 | # TODO(danyu): Update robot state to charging once charging actions are ready 36 | CHARGING = "CHARGING" 37 | MAP_DEPLOYMENT = "MAP_DEPLOYMENT" 38 | TELEOP = "TELEOP" 39 | 40 | @property 41 | def running(self): 42 | return self in (RobotStateV1.ON_TASK, RobotStateV1.MAP_DEPLOYMENT, RobotStateV1.CHARGING) 43 | 44 | @property 45 | def can_switch_teleop(self): 46 | return self in (RobotStateV1.IDLE, RobotStateV1.ON_TASK, 47 | RobotStateV1.MAP_DEPLOYMENT, RobotStateV1.TELEOP) 48 | 49 | @property 50 | def can_deploy_map(self): 51 | return self in (RobotStateV1.IDLE, RobotStateV1.CHARGING) 52 | 53 | 54 | class RobotTeleopActionV1(enum.Enum): 55 | START = "START" 56 | STOP = "STOP" 57 | 58 | 59 | class RobotTypeV1(enum.Enum): 60 | ARM = "FORKLIFT" 61 | AMR = "CARRIER" 62 | 63 | 64 | class RobotSoftwareVersionV1(pydantic.BaseModel): 65 | os: str = "" 66 | app: str = "" 67 | 68 | 69 | class RobotHardwareVersionV1(pydantic.BaseModel): 70 | manufacturer: str = "" 71 | serial_number: str = "" 72 | 73 | 74 | class RobotTypeIdentifierV1(pydantic.BaseModel): 75 | agv_class: str = "" 76 | speed_max: float = -1 77 | 78 | 79 | class RobotBatterySpecV1(pydantic.BaseModel): 80 | """Represents the specs of the robot's battery.""" 81 | critical_level: float = 10.0 82 | recommended_minimum: Optional[float] = None 83 | recommended_maximum: Optional[float] = None 84 | 85 | 86 | class RobotStatusV1(pydantic.BaseModel): 87 | """Represents the status of the robot.""" 88 | pose: common.Pose2D = common.Pose2D() 89 | software_version: RobotSoftwareVersionV1 = RobotSoftwareVersionV1() 90 | hardware_version: RobotHardwareVersionV1 = RobotHardwareVersionV1() 91 | factsheet: RobotTypeIdentifierV1 = RobotTypeIdentifierV1() 92 | online: bool = False 93 | battery_level: float = 0.0 94 | state: RobotStateV1 = RobotStateV1.IDLE 95 | info_messages: Optional[Dict] = pydantic.Field( 96 | None, description="Data collected from the mission client.") 97 | errors: Dict = pydantic.Field( 98 | {}, description="Key value pairs to describe if something is wrong with the robot.") 99 | 100 | 101 | class RobotSpecV1(pydantic.BaseModel): 102 | """Specifies constant properties about the robot, such as its name.""" 103 | labels: List[str] = pydantic.Field( 104 | [], description="A list of labels to assign to the robot, used to identify certain groups \ 105 | of robots.") 106 | battery: RobotBatterySpecV1 = RobotBatterySpecV1() 107 | heartbeat_timeout: datetime.timedelta = pydantic.Field( 108 | datetime.timedelta(seconds=30), 109 | description="The window of time after the dispatch gets a message from a robot for a \ 110 | robot to be considered online") 111 | switch_teleop: bool = pydantic.Field( 112 | False, description="Toggle the mode of the robot to TELEOP." 113 | ) 114 | 115 | 116 | class RobotQueryParamsV1(pydantic.BaseModel): 117 | """Specifies the supported query parameters allowed for robots""" 118 | min_battery: Optional[float] 119 | max_battery: Optional[float] 120 | state: Optional[RobotStateV1] 121 | online: Optional[bool] 122 | names: Optional[List[str]] = Field(Query(None)) 123 | robot_type: Optional[RobotTypeV1] 124 | 125 | 126 | class RobotObjectV1(RobotSpecV1, object.ApiObject): 127 | """Represents a robot.""" 128 | status: RobotStatusV1 129 | 130 | @classmethod 131 | def get_alias(cls) -> str: 132 | return "robot" 133 | 134 | @classmethod 135 | def get_spec_class(cls) -> Any: 136 | return RobotSpecV1 137 | 138 | @classmethod 139 | def get_status_class(cls) -> Any: 140 | return RobotStatusV1 141 | 142 | @classmethod 143 | def default_spec(cls) -> Dict: 144 | return RobotSpecV1().dict() # type: ignore 145 | 146 | @classmethod 147 | def get_query_params(cls) -> Any: 148 | return RobotQueryParamsV1 149 | 150 | @staticmethod 151 | def get_query_map() -> Dict: 152 | return { 153 | "min_battery": "(status->'battery_level')::float >= {}", 154 | "max_battery": "(status->'battery_level')::float <= {}", 155 | "names": "name in {}", 156 | "state": "status->>'state' = '{}'", 157 | "online": "status->>'online' = '{}'", 158 | "robot_type": "(status->'factsheet'->>'agv_class')::text = '{}'" 159 | } 160 | 161 | @classmethod 162 | def get_methods(cls) -> List[object.ApiObjectMethod]: 163 | return [ 164 | object.ApiObjectMethod( 165 | name="teleop", description="This endpoint is to place the robot into teleop or \ 166 | to take the robot out of teleop.", 167 | function=cls.teleop, 168 | params=RobotTeleopActionV1) 169 | ] 170 | 171 | async def teleop(self, teleop: RobotTeleopActionV1): 172 | if not self.status.state.can_switch_teleop: 173 | raise common.ICSUsageError( 174 | f"Robot {self.name} is in {self.status.state} and request cannot be satisfied.") 175 | self.switch_teleop = (teleop == RobotTeleopActionV1.START) 176 | return teleop.value + " teleop action received." 177 | -------------------------------------------------------------------------------- /packages/controllers/mission/tests/update_mission.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2023 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | import time 20 | import unittest 21 | 22 | from cloud_common import objects as api_objects 23 | from packages.controllers.mission.tests import client as simulator 24 | from cloud_common.objects import mission as mission_object 25 | from cloud_common.objects import common 26 | 27 | from packages.controllers.mission.tests import test_context 28 | 29 | WAYPOINT_1 = (10, 10) 30 | WAYPOINT_2 = (5, 5) 31 | WAYPOINT_3 = (3, 3) 32 | 33 | MISSION_TREE = [ 34 | test_context.route_generator(), 35 | {"name": "selector_1", "selector": {}, "parent": "root"}, 36 | test_context.action_generator(params={"should_fail": 1, "time": 3}, parent="selector_1"), 37 | {"name": "sequence_1", "sequence": {}, "parent": "selector_1"}, 38 | test_context.route_generator(parent="sequence_1"), 39 | test_context.route_generator(parent="sequence_1"), 40 | test_context.route_generator() 41 | ] 42 | 43 | 44 | class TestUpdateMissions(unittest.TestCase): 45 | def test_update_pending_mission(self): 46 | """ Test if pending mission gets updated """ 47 | 48 | robot = simulator.RobotInit("test01", 0, 0, 0) 49 | with test_context.TestContext([robot]) as ctx: 50 | # Create the robot 51 | ctx.db_client.create( 52 | api_objects.RobotObjectV1(name="test01", status={})) 53 | time.sleep(0.25) 54 | self.assertGreater( 55 | len(ctx.db_client.list(api_objects.RobotObjectV1)), 0) 56 | 57 | # Create two missions 58 | mission_1 = test_context.mission_from_waypoint( 59 | "test01", WAYPOINT_1[0], WAYPOINT_1[1]) 60 | ctx.db_client.create(mission_1) 61 | time.sleep(0.25) 62 | 63 | # The second mission will be pending as the robot executes the first mission. 64 | mission_2 = test_context.mission_from_waypoint( 65 | "test01", WAYPOINT_2[0], WAYPOINT_2[1]) 66 | ctx.db_client.create(mission_2) 67 | 68 | missions = ctx.db_client.list(api_objects.MissionObjectV1) 69 | self.assertEqual(len(missions), 2) 70 | 71 | # Update the second mission 72 | update_nodes = {"0": {"waypoints": [ 73 | {"x": WAYPOINT_3[0], "y": WAYPOINT_3[1], "theta": 0}]}} 74 | ctx.db_client.update_mission(mission_2.name, update_nodes) 75 | 76 | # Wait till it's done 77 | for mission in ctx.db_client.watch(api_objects.MissionObjectV1): 78 | if mission.status.state.done and mission.name == mission_2.name: 79 | self.assertEqual(mission.status.state, 80 | mission_object.MissionStateV1.COMPLETED) 81 | break 82 | 83 | # Make sure the robot is at the updated position 84 | robot_status = ctx.db_client.get( 85 | api_objects.RobotObjectV1, "test01").status 86 | self.assertAlmostEqual(robot_status.pose.x, 87 | WAYPOINT_3[0], places=2) 88 | self.assertAlmostEqual(robot_status.pose.y, 89 | WAYPOINT_3[1], places=2) 90 | 91 | def test_update_running_mission(self): 92 | """ Test if running mission gets updated """ 93 | 94 | robot = simulator.RobotInit("test01", 0, 0, 0) 95 | with test_context.TestContext([robot]) as ctx: 96 | # Create the robot 97 | ctx.db_client.create( 98 | api_objects.RobotObjectV1(name="test01", status={})) 99 | time.sleep(0.25) 100 | self.assertGreater( 101 | len(ctx.db_client.list(api_objects.RobotObjectV1)), 0) 102 | 103 | # Create a mission 104 | mission_1 = test_context.mission_object_generator( 105 | "test01", MISSION_TREE) 106 | ctx.db_client.create(mission_1) 107 | time.sleep(0.25) 108 | 109 | # Update node 6 110 | update_nodes = {"6": {"waypoints": [ 111 | {"x": WAYPOINT_2[0], "y": WAYPOINT_2[1], "theta": 0}]}} 112 | ctx.db_client.update_mission(mission_1.name, update_nodes) 113 | 114 | # Wait till it's done 115 | for mission in ctx.db_client.watch(api_objects.MissionObjectV1): 116 | if mission.status.state.done and mission.name == mission_1.name: 117 | self.assertEqual(mission.status.state, 118 | mission_object.MissionStateV1.COMPLETED) 119 | break 120 | 121 | # Make sure the robot is at the updated position 122 | robot_status = ctx.db_client.get( 123 | api_objects.RobotObjectV1, "test01").status 124 | self.assertAlmostEqual(robot_status.pose.x, 125 | WAYPOINT_2[0], places=2) 126 | self.assertAlmostEqual(robot_status.pose.y, 127 | WAYPOINT_2[1], places=2) 128 | 129 | def test_update_completed_mission(self): 130 | """ Test if completed mission gets updated """ 131 | 132 | robot = simulator.RobotInit("test01", 0, 0, 0) 133 | with test_context.TestContext([robot]) as ctx: 134 | # Create the robot 135 | ctx.db_client.create( 136 | api_objects.RobotObjectV1(name="test01", status={})) 137 | time.sleep(0.25) 138 | self.assertGreater( 139 | len(ctx.db_client.list(api_objects.RobotObjectV1)), 0) 140 | 141 | # Create a mission 142 | mission_1 = test_context.mission_from_waypoint( 143 | "test01", WAYPOINT_3[0], WAYPOINT_3[1]) 144 | ctx.db_client.create(mission_1) 145 | time.sleep(0.25) 146 | 147 | # Wait till it's done 148 | for mission in ctx.db_client.watch(api_objects.MissionObjectV1): 149 | if mission.status.state.done and mission.name == mission_1.name: 150 | self.assertEqual(mission.status.state, 151 | mission_object.MissionStateV1.COMPLETED) 152 | break 153 | 154 | # Update a completed mission 155 | update_nodes = {"0": {"waypoints": [ 156 | {"x": WAYPOINT_1[0], "y": WAYPOINT_1[1], "theta": 0}]}} 157 | with self.assertRaises(common.ICSUsageError): 158 | ctx.db_client.update_mission(mission_1.name, update_nodes) 159 | 160 | 161 | if __name__ == "__main__": 162 | unittest.main() 163 | -------------------------------------------------------------------------------- /packages/controllers/mission/behavior_tree.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | import py_trees 20 | from typing import Any 21 | import cloud_common.objects.mission as mission_object 22 | 23 | def tree2mission_state(type: py_trees.common.Status) -> mission_object.MissionStateV1: 24 | if type == py_trees.common.Status.SUCCESS: 25 | return mission_object.MissionStateV1.COMPLETED 26 | elif type == py_trees.common.Status.FAILURE: 27 | return mission_object.MissionStateV1.FAILED 28 | elif type == py_trees.common.Status.RUNNING: 29 | return mission_object.MissionStateV1.RUNNING 30 | else: 31 | return mission_object.MissionStateV1.PENDING 32 | 33 | def mission2tree_state(type: mission_object.MissionStateV1) -> py_trees.common.Status: 34 | if type == mission_object.MissionStateV1.COMPLETED: 35 | return py_trees.common.Status.SUCCESS 36 | elif type == mission_object.MissionStateV1.RUNNING: 37 | return py_trees.common.Status.RUNNING 38 | elif type == mission_object.MissionStateV1.PENDING: 39 | return py_trees.common.Status.INVALID 40 | else: 41 | return py_trees.common.Status.FAILURE 42 | 43 | 44 | class ConstantBehaviorNode(py_trees.behaviour.Behaviour): 45 | """ 46 | Constant behavior tree node 47 | """ 48 | 49 | def __init__(self, name: str, idx: int, const_status=py_trees.common.Status.SUCCESS): 50 | print( 51 | f"Create a constant node for mission node {idx} with status {const_status}", flush=True) 52 | self.idx = idx 53 | self.name = name 54 | self.const_status = const_status 55 | super().__init__(self.name) 56 | 57 | @property 58 | def type(self) -> str: 59 | return "leaf" 60 | 61 | @property 62 | def is_order(self): 63 | """Whether this node involves sending VDA5050 orders to the robot""" 64 | return True 65 | 66 | def initialise(self): 67 | self.status = py_trees.common.Status.RUNNING 68 | 69 | def update(self) -> py_trees.common.Status: 70 | return self.const_status 71 | 72 | 73 | class MissionLeafNode(py_trees.behaviour.Behaviour): 74 | """ 75 | Route/action/notify behavior tree node 76 | """ 77 | def __init__(self, mission: mission_object.MissionObjectV1, idx: int, 78 | status=py_trees.common.Status.INVALID): 79 | self.mission = mission 80 | self.idx = idx 81 | self.name = str(self.mission.mission_tree[idx].name) 82 | self.status = status 83 | super(MissionLeafNode, self).__init__(self.name) # pylint: disable=super-with-arguments 84 | 85 | @property 86 | def type(self) -> str: 87 | return "leaf" 88 | 89 | @property 90 | def is_order(self): 91 | """Whether this node involves sending VDA5050 orders to the robot""" 92 | return False 93 | 94 | def initialise(self): 95 | self.status = py_trees.common.Status.RUNNING 96 | 97 | def update(self) -> py_trees.common.Status: 98 | # Update result based on order information feedback from server 99 | # Count PENDING orders as RUNNING since the robot might not have acknowledged the order yet 100 | if self.mission.status.node_status[self.name].state == \ 101 | mission_object.MissionStateV1.PENDING: 102 | return py_trees.common.Status.RUNNING 103 | else: 104 | return mission2tree_state(self.mission.status.node_status[self.name].state) 105 | 106 | 107 | class SequenceBehaviorNode(py_trees.composites.Sequence): 108 | """ 109 | Sequence behavior tree node 110 | """ 111 | def __init__(self, name: str, idx: int, status=py_trees.common.Status.INVALID): 112 | self.idx = idx 113 | self.name = name 114 | self.status = status 115 | super().__init__(self.name, memory=True) 116 | 117 | @property 118 | def type(self) -> str: 119 | return "control" 120 | 121 | @property 122 | def is_order(self): 123 | """Whether this node involves sending VDA5050 orders to the robot""" 124 | return True 125 | 126 | 127 | class SelectorBehaviorNode(py_trees.composites.Selector): 128 | """ 129 | Selector behavior tree node 130 | """ 131 | def __init__(self, name: str, idx: int, status=py_trees.common.Status.INVALID): 132 | self.idx = idx 133 | self.name = name 134 | self.status = status 135 | super().__init__(self.name) 136 | 137 | @property 138 | def type(self) -> str: 139 | return "control" 140 | 141 | @property 142 | def is_order(self): 143 | """Whether this node involves sending VDA5050 orders to the robot""" 144 | return True 145 | 146 | 147 | class MissionBehaviorTree(): 148 | """Mission behavior Tree 149 | """ 150 | def __init__(self, mission: mission_object.MissionObjectV1): 151 | # The behavior tree has an implicit sequence node as its root which is named “root” 152 | self.root = py_trees.composites.Sequence(name="root") 153 | self.mission = mission 154 | self.failure_reason = "" 155 | 156 | @property 157 | def current_node(self) -> Any: 158 | # Recursive function to extract the last running node of the tree 159 | return self.root.tip() 160 | 161 | @property 162 | def status(self) -> py_trees.common.Status: 163 | return self.root.status 164 | 165 | def create_behavior_tree(self): 166 | for i, mission_node in enumerate(self.mission.mission_tree): 167 | # Get parent node 168 | status = mission2tree_state( 169 | self.mission.status.node_status[str(mission_node.name)].state) 170 | parent = None 171 | for node in self.root.iterate(): 172 | if node.name == mission_node.parent: 173 | parent = node 174 | if parent is None: 175 | self.root.status = py_trees.common.Status.FAILURE 176 | self.failure_reason = f"Given parent {mission_node.parent} does not exist" 177 | return False 178 | 179 | # Check if this is a control node: selector or sequence 180 | if mission_node.type == mission_object.MissionNodeType.SELECTOR: 181 | parent.add_child(SelectorBehaviorNode(str(mission_node.name), i, status)) 182 | elif mission_node.type == mission_object.MissionNodeType.SEQUENCE: 183 | parent.add_child(SequenceBehaviorNode(str(mission_node.name), i, status)) 184 | # Check if this is a leaf node: route, action, or notify 185 | elif mission_node.type in (mission_object.MissionNodeType.ROUTE, 186 | mission_object.MissionNodeType.ACTION, 187 | mission_object.MissionNodeType.NOTIFY, 188 | mission_object.MissionNodeType.MOVE): 189 | leaf_node = MissionLeafNode(self.mission, i, status) 190 | parent.add_child(leaf_node) 191 | elif mission_node.type == mission_object.MissionNodeType.CONSTANT: 192 | if mission_node.constant is not None: 193 | if mission_node.constant.success: 194 | status = py_trees.common.Status.SUCCESS 195 | else: 196 | status = py_trees.common.Status.FAILURE 197 | parent.add_child(ConstantBehaviorNode(str(mission_node.name), i, status)) 198 | # Not supported mission node type 199 | else: 200 | self.info("Invalid mission node type") 201 | return True 202 | 203 | def update(self): 204 | self.root.tick_once() 205 | self.post_tick() 206 | 207 | def post_tick(self): 208 | # Update all the non-pending control node 209 | for node in self.root.iterate(): 210 | if node.name != "root" and node.is_order: 211 | self.mission.status.node_status[node.name].state = \ 212 | tree2mission_state(node.status) 213 | 214 | def info(self, message: str): 215 | print(f"[Isaac Mission Dispatch (Behavior Tree)] | : " 216 | f"[{self.mission.name}] {message}", flush=True) 217 | -------------------------------------------------------------------------------- /packages/controllers/mission/tests/mission.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | import time 20 | import unittest 21 | import math 22 | 23 | from cloud_common import objects as api_objects 24 | from packages.controllers.mission.tests import client as simulator 25 | from cloud_common.objects import mission as mission_object 26 | from cloud_common.objects import robot as robot_object 27 | 28 | from packages.controllers.mission.tests import test_context 29 | from packages.controllers.mission.tests import mission_examples 30 | 31 | # Waypoint for a mission that will be reused for many tests 32 | DEFAULT_MISSION_X = 10.0 33 | DEFAULT_MISSION_Y = 10.0 34 | 35 | # Definition for mission `SCENARIO1` with multiple waypoints 36 | SCENARIO1_WAYPOINTS = [ 37 | (1, 1), 38 | (10, 10), 39 | (5, 5), 40 | ] 41 | 42 | # Expected progression of mission state for the mission `SCENARIO1` 43 | SCENARIO1_EXPECTED_STATUSES = [ 44 | mission_object.MissionStatusV1(state="PENDING", current_node=0), 45 | mission_object.MissionStatusV1(state="RUNNING", current_node=0), 46 | mission_object.MissionStatusV1(state="RUNNING", current_node=1), 47 | mission_object.MissionStatusV1(state="RUNNING", current_node=2), 48 | mission_object.MissionStatusV1(state="COMPLETED", current_node=2), 49 | ] 50 | 51 | 52 | class TestMissions(unittest.TestCase): 53 | def test_long_mission(self): 54 | """ Test sending a very long mission to a single robot """ 55 | robot = simulator.RobotInit("test01", 0, 0, 0) 56 | with test_context.TestContext([robot]) as ctx: 57 | # Create the robot and then the mission 58 | ctx.db_client.create( 59 | api_objects.RobotObjectV1(name="test01", status={})) 60 | time.sleep(0.25) 61 | ctx.db_client.create( 62 | test_context.mission_object_generator("test01", mission_examples.MISSION_TREE_LONG)) 63 | 64 | # Make sure the mission is updated and completed 65 | for update in ctx.db_client.watch(api_objects.MissionObjectV1): 66 | if update.status.state == mission_object.MissionStateV1.COMPLETED: 67 | break 68 | 69 | # Make sure the robot is at the last position in the list of waypoints 70 | robot_status = ctx.db_client.get( 71 | api_objects.RobotObjectV1, "test01").status 72 | waypoint = mission_examples.MISSION_TREE_LONG[-1]["route"]["waypoints"][-1] 73 | self.assertAlmostEqual(robot_status.pose.x, 74 | waypoint["x"], places=2) 75 | self.assertAlmostEqual(robot_status.pose.y, 76 | waypoint["y"], places=2) 77 | 78 | def test_single_mission(self): 79 | """ Test sending a single mission to a single robot """ 80 | robot = simulator.RobotInit("test01", 0, 0, 0) 81 | with test_context.TestContext([robot]) as ctx: 82 | # Create the robot and then the mission 83 | ctx.db_client.create( 84 | api_objects.RobotObjectV1(name="test01", status={})) 85 | time.sleep(0.25) 86 | ctx.db_client.create(test_context.mission_from_waypoints( 87 | "test01", SCENARIO1_WAYPOINTS)) 88 | 89 | # Make sure the mission is updated and completed 90 | for expected_state, update in zip(SCENARIO1_EXPECTED_STATUSES, 91 | ctx.db_client.watch(api_objects.MissionObjectV1)): 92 | self.assertEqual(update.status.state, expected_state.state) 93 | self.assertEqual(update.status.current_node, 94 | expected_state.current_node) 95 | 96 | # Make sure the robot is at the last position in the list of waypoints 97 | robot_status = ctx.db_client.get( 98 | api_objects.RobotObjectV1, "test01").status 99 | self.assertEqual(robot_status.pose.x, SCENARIO1_WAYPOINTS[-1][0]) 100 | self.assertEqual(robot_status.pose.y, SCENARIO1_WAYPOINTS[-1][0]) 101 | 102 | def test_robot_object_second(self): 103 | """ Test creating a mission for a robot that doesnt exist, then creating the robot later """ 104 | robot = simulator.RobotInit("test01", 0, 0, 0) 105 | with test_context.TestContext([robot]) as ctx: 106 | # Create the robot and then the mission 107 | ctx.db_client.create(test_context.mission_from_waypoints( 108 | "test01", SCENARIO1_WAYPOINTS)) 109 | time.sleep(0.25) 110 | ctx.db_client.create( 111 | api_objects.RobotObjectV1(name="test01", status={})) 112 | 113 | # Make sure the mission is updated and completed 114 | for expected_state, update in zip(SCENARIO1_EXPECTED_STATUSES, 115 | ctx.db_client.watch(api_objects.MissionObjectV1)): 116 | self.assertEqual(update.status.state, expected_state.state) 117 | self.assertEqual(update.status.current_node, 118 | expected_state.current_node) 119 | 120 | # Make sure the robot is at the last position in the list of waypoints 121 | robot_status = ctx.db_client.get( 122 | api_objects.RobotObjectV1, "test01").status 123 | self.assertEqual(robot_status.pose.x, SCENARIO1_WAYPOINTS[-1][0]) 124 | self.assertEqual(robot_status.pose.y, SCENARIO1_WAYPOINTS[-1][0]) 125 | 126 | def test_mission_failure(self): 127 | """ Test a sequence of 4 missions PASS, FAIL, PASS, FAIL """ 128 | 129 | expected_states = [ 130 | # All 4 missions start out as PENDING 131 | mission_object.MissionStatusV1(state="PENDING", current_node=0), 132 | mission_object.MissionStatusV1(state="PENDING", current_node=0), 133 | mission_object.MissionStatusV1(state="PENDING", current_node=0), 134 | mission_object.MissionStatusV1(state="PENDING", current_node=0), 135 | # The first mission runs then completes 136 | mission_object.MissionStatusV1(state="RUNNING", current_node=0), 137 | mission_object.MissionStatusV1(state="COMPLETED", current_node=0), 138 | # The second mission fails 139 | mission_object.MissionStatusV1(state="RUNNING", current_node=0), 140 | mission_object.MissionStatusV1(state="FAILED", current_node=0, 141 | failure_reason="Failure period reached"), 142 | # The third mission runs then completes 143 | mission_object.MissionStatusV1(state="RUNNING", current_node=0), 144 | mission_object.MissionStatusV1(state="COMPLETED", current_node=0), 145 | # The fourth mission fails 146 | mission_object.MissionStatusV1(state="RUNNING", current_node=0), 147 | mission_object.MissionStatusV1(state="FAILED", current_node=0, 148 | failure_reason="Failure period reached"), 149 | ] 150 | 151 | robot = simulator.RobotInit("test01", 0, 0, 0, "map", 2) 152 | with test_context.TestContext([robot]) as ctx: 153 | # Create the robot and then the four missions 154 | watcher = ctx.db_client.watch(api_objects.MissionObjectV1) 155 | for i in range(0, 4): 156 | mission = test_context.mission_from_waypoint( 157 | "test01", i * 2 + 1, i * 2 + 1, "mission_" + str(i)) 158 | ctx.db_client.create(mission) 159 | 160 | # Sequence matters, otherwise we can't capture the first mission's pending state 161 | ctx.db_client.create( 162 | api_objects.RobotObjectV1(name="test01", status={})) 163 | 164 | # Make sure the mission is updated and completed 165 | for expected_state, update in zip(expected_states, watcher): 166 | self.assertEqual(update.status.state, expected_state.state) 167 | self.assertEqual(update.status.current_node, 168 | expected_state.current_node) 169 | 170 | def test_timeout(self): 171 | """ Test sending a mission that times out """ 172 | MISSION_WAYPOINT_X = 15 173 | MISSION_WAYPOINT_Y = 15 174 | expected_statuses = [ 175 | mission_object.MissionStatusV1(state="PENDING"), 176 | mission_object.MissionStatusV1(state="RUNNING"), 177 | mission_object.MissionStatusV1(state="FAILED", 178 | failure_reason="Mission timed out"), 179 | ] 180 | robot = simulator.RobotInit("test01", 0, 0, 0) 181 | with test_context.TestContext([robot]) as ctx: 182 | # Create the robot and then the mission 183 | ctx.db_client.create( 184 | api_objects.RobotObjectV1(name="test01", status={})) 185 | time.sleep(0.25) 186 | watcher = ctx.db_client.watch(api_objects.MissionObjectV1) 187 | mission = test_context.mission_from_waypoint( 188 | "test01", MISSION_WAYPOINT_X, MISSION_WAYPOINT_Y) 189 | mission.timeout = 1 190 | ctx.db_client.create(mission) 191 | 192 | # Make sure the mission is listed as FAILED 193 | for expected_status, update in zip(expected_statuses, watcher): 194 | self.assertEqual(update.status.state, expected_status.state) 195 | if update.status.state == mission_object.MissionStateV1.FAILED: 196 | self.assertEqual(update.status.failure_reason, 197 | expected_status.failure_reason) 198 | 199 | def test_mission_move_node(self): 200 | """ Test sending a mission with move nodes """ 201 | robot = simulator.RobotInit("test01", 1, 1, math.pi/4) 202 | move_mission = [test_context.move_generator(move={"distance": 1}), 203 | test_context.move_generator(move={"rotation": math.pi/4})] 204 | with test_context.TestContext([robot]) as ctx: 205 | # Create the robot and then the mission 206 | ctx.db_client.create( 207 | api_objects.RobotObjectV1(name="test01", status={})) 208 | time.sleep(3) 209 | ctx.db_client.create( 210 | test_context.mission_object_generator("test01", move_mission)) 211 | 212 | # Make sure the mission is updated and completed 213 | for update in ctx.db_client.watch(api_objects.MissionObjectV1): 214 | if update.status.state == mission_object.MissionStateV1.COMPLETED: 215 | break 216 | 217 | # Make sure the robot is at the last position in the list of waypoints 218 | updated_robot_status = ctx.db_client.get( 219 | api_objects.RobotObjectV1, "test01").status 220 | self.assertAlmostEqual(updated_robot_status.pose.x, 1.71, places=2) 221 | self.assertAlmostEqual(updated_robot_status.pose.y, 1.71, places=2) 222 | self.assertAlmostEqual(updated_robot_status.pose.theta, math.pi/2, places=2) 223 | 224 | 225 | if __name__ == "__main__": 226 | unittest.main() 227 | -------------------------------------------------------------------------------- /packages/controllers/mission/tests/robot.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | import datetime 20 | import time 21 | import unittest 22 | import paho.mqtt.client as mqtt_client 23 | import packages.controllers.mission.vda5050_types as types 24 | 25 | from cloud_common import objects as api_objects 26 | from packages.controllers.mission.tests import client as simulator 27 | from cloud_common.objects import mission as mission_object 28 | from cloud_common.objects import robot as robot_object 29 | from cloud_common.objects.robot import RobotStateV1 30 | 31 | from packages.controllers.mission.tests import test_context 32 | # Waypoint for a mission that will be reused for many tests 33 | DEFAULT_MISSION_X = 10.0 34 | DEFAULT_MISSION_Y = 10.0 35 | 36 | # Definition for mission `SCENARIO1` with multiple waypoints 37 | SCENARIO1_WAYPOINTS = [ 38 | (1, 1), 39 | (10, 10), 40 | (5, 5), 41 | ] 42 | 43 | MISSION_TREE_1 = [ 44 | test_context.route_generator(), 45 | test_context.action_generator( 46 | params={}, name="teleop", action_type="pause_order"), 47 | test_context.route_generator() 48 | ] 49 | 50 | 51 | class TestMissions(unittest.TestCase): 52 | 53 | def test_many_robots(self): 54 | """ Test sending a mission to 5 different robots at the same time """ 55 | sim_robots = [] 56 | robots = [] 57 | missions = [] 58 | num_robots = 5 59 | 60 | for i in range(0, num_robots): 61 | name = f"test{i:02d}" 62 | sim_robots.append(simulator.RobotInit(name, i, i)) 63 | robots.append(api_objects.RobotObjectV1(name=name, status={})) 64 | missions.append( 65 | test_context.mission_from_waypoint(name, i + 10, i + 5)) 66 | 67 | with test_context.TestContext(sim_robots) as ctx: 68 | for robot in robots: 69 | ctx.db_client.create(robot) 70 | time.sleep(0.25) 71 | for mission in missions: 72 | ctx.db_client.create(mission) 73 | time.sleep(0.25) 74 | 75 | # Wait for all missions to complete 76 | completed_missions = set() 77 | for mission in ctx.db_client.watch(api_objects.MissionObjectV1): 78 | if mission.status.state == mission_object.MissionStateV1.COMPLETED: 79 | completed_missions.add(mission.name) 80 | if len(completed_missions) == len(missions): 81 | break 82 | time.sleep(1) 83 | 84 | # Check the state of all missions and robots 85 | db_robots = ctx.db_client.list(api_objects.RobotObjectV1) 86 | db_missions = ctx.db_client.list(api_objects.MissionObjectV1) 87 | 88 | for mission in db_missions: 89 | self.assertEqual(mission.status.state, 90 | mission_object.MissionStateV1.COMPLETED) 91 | for robot in db_robots: 92 | id = int(robot.name.lstrip("test")) 93 | self.assertEqual(robot.status.pose.x, id + 10) 94 | self.assertEqual(robot.status.pose.y, id + 5) 95 | 96 | def test_robot_offline(self): 97 | """ Test that the server labels the robot as offline after not receiving messages """ 98 | robot = simulator.RobotInit("test01", 0, 0, 0) 99 | with test_context.TestContext([robot], tick_period=2.0, disable_request_factsheet=True) as ctx: 100 | # Create the robot and then the mission 101 | ctx.db_client.create(api_objects.RobotObjectV1( 102 | name="test01", heartbeat_timeout=1, status={})) 103 | 104 | # The simulator "tick_period" is smaller than the heartbeat_timeout, so the robot 105 | # will alternate between online and offline 106 | expected_online = [False, True, False, True] 107 | for online, update in zip(expected_online, 108 | ctx.db_client.watch(api_objects.RobotObjectV1)): 109 | self.assertEqual(update.status.online, online) 110 | 111 | def test_robot_task_state(self): 112 | """ Test if the robot task state is correctly updated """ 113 | robot = simulator.RobotInit("test01", 0, 0, 0) 114 | with test_context.TestContext([robot]) as ctx: 115 | # Create the robot 116 | ctx.db_client.create( 117 | api_objects.RobotObjectV1(name="test01", status={})) 118 | 119 | # Create a watcher so we can see how the state of the robot changes over time 120 | watcher = ctx.db_client.watch(api_objects.RobotObjectV1) 121 | 122 | # Grab the first state, the robot should be IDLE 123 | first_update = next(watcher) 124 | self.assertEqual(first_update.status.state, 125 | robot_object.RobotStateV1.IDLE) 126 | 127 | # Submit a mission to the robot 128 | ctx.db_client.create(test_context.mission_from_waypoint("test01", 129 | DEFAULT_MISSION_X, DEFAULT_MISSION_Y)) 130 | 131 | # Wait for the robot to be ON_TASK 132 | for update in watcher: 133 | if update.status.state == robot_object.RobotStateV1.ON_TASK: 134 | break 135 | 136 | # Wait for the robot to be IDLE and verify its in the right place 137 | for update in watcher: 138 | if update.status.state == robot_object.RobotStateV1.IDLE: 139 | self.assertEqual(update.status.pose.x, DEFAULT_MISSION_X) 140 | self.assertEqual(update.status.pose.y, DEFAULT_MISSION_Y) 141 | break 142 | 143 | def test_robot_hardware_version_update(self): 144 | """ Test robot hardware version update """ 145 | robot = simulator.RobotInit( 146 | "test01", 0, 0, 0, "map", 0, 0, "NV", "1NV023200CAR00010") 147 | with test_context.TestContext([robot]) as ctx: 148 | # Create the robot and then the mission 149 | ctx.db_client.create( 150 | api_objects.RobotObjectV1(name="test01", status={})) 151 | time.sleep(0.25) 152 | ctx.db_client.create(test_context.mission_from_waypoints( 153 | "test01", SCENARIO1_WAYPOINTS)) 154 | 155 | watcher = ctx.db_client.watch(api_objects.RobotObjectV1) 156 | for update in watcher: 157 | if update.status.online: 158 | break 159 | next_update = next(watcher) 160 | 161 | robot_hardware = next_update.status.hardware_version 162 | self.assertEqual(robot_hardware.manufacturer, "NV") 163 | self.assertEqual(robot_hardware.serial_number, "1NV023200CAR00010") 164 | 165 | def test_battery_level(self): 166 | """" Validate battery level """ 167 | robot = simulator.RobotInit("test01", 0, 0, battery=42) 168 | with test_context.TestContext([robot]) as ctx: 169 | ctx.db_client.create( 170 | api_objects.RobotObjectV1(name="test01", status={})) 171 | watcher = ctx.db_client.watch(api_objects.RobotObjectV1) 172 | for update in watcher: 173 | if update.status.battery_level == 42: 174 | break 175 | 176 | def test_charging_transition(self): 177 | """ Validate charging state transition """ 178 | robot = simulator.RobotInit("test01", 0, 0) 179 | # Create MQTT Client to simulate messages from robot 180 | client = mqtt_client.Client(transport=test_context.MQTT_TRANSPORT) 181 | client.ws_set_options(path=test_context.MQTT_WS_PATH) 182 | with test_context.TestContext([robot]) as ctx: 183 | client.connect(ctx.mqtt_address, test_context.MQTT_PORT) 184 | ctx.db_client.create( 185 | api_objects.RobotObjectV1(name="test01", status={})) 186 | 187 | # Initial state is IDLE 188 | watcher = ctx.db_client.watch(api_objects.RobotObjectV1) 189 | for update in watcher: 190 | if update.status.state == RobotStateV1.IDLE: 191 | break 192 | 193 | # Publish charging=True message 194 | # State should transition to CHARGING 195 | topic = f"{test_context.MQTT_PREFIX}/test01/state" 196 | message = types.VDA5050State( 197 | headerId=0, 198 | timestamp=datetime.datetime.now().isoformat(), 199 | manufacturer="", 200 | serialNumber="", 201 | orderId="", 202 | orderUpdateId=0, 203 | lastNodeId="", 204 | lastNodeSequenceId=0, 205 | nodeStates=[], 206 | edgeStates=[], 207 | actionStates=[], 208 | agvPosition={"x": 0, "y": 0, 209 | "theta": 0, "mapId": ""}, 210 | batteryState={"batteryCharge": 50, 211 | "charging": True}, 212 | safetyState=types.VDA5050SafetyStatus( 213 | eStop=types.VDA5050EStop.NONE, fieldViolation=False 214 | )) 215 | client.publish(topic, message.json()) 216 | time.sleep(0.5) 217 | for update in watcher: 218 | if update.status.state == RobotStateV1.CHARGING: 219 | break 220 | 221 | # Publish charging=False message 222 | # State should transition to IDLE 223 | message.batteryState.charging = False 224 | client.publish(topic, message.json()) 225 | time.sleep(0.5) 226 | for update in watcher: 227 | if update.status.state == RobotStateV1.IDLE: 228 | break 229 | 230 | def test_teleop_in_mission(self): 231 | """ Test mission with teleop node""" 232 | robot = simulator.RobotInit("test01", 0, 0, 0) 233 | with test_context.TestContext([robot]) as ctx: 234 | # Create the robot and then the mission 235 | ctx.db_client.create( 236 | api_objects.RobotObjectV1(name="test01", status={})) 237 | time.sleep(0.25) 238 | ctx.db_client.create( 239 | test_context.mission_object_generator("test01", MISSION_TREE_1)) 240 | 241 | # Make sure the robot is in teleop mode 242 | watcher = ctx.db_client.watch(api_objects.RobotObjectV1) 243 | for update in watcher: 244 | if update.status.state == robot_object.RobotStateV1.TELEOP: 245 | break 246 | # Simulate teleop 247 | time.sleep(5) 248 | # Stop teleop 249 | ctx.call_teleop_service( 250 | robot_name="test01", teleop=robot_object.RobotTeleopActionV1.STOP) 251 | for update in ctx.db_client.watch(api_objects.MissionObjectV1): 252 | if update.status.state == mission_object.MissionStateV1.COMPLETED: 253 | break 254 | 255 | # Make sure the robot is at the last position in the list of waypoints 256 | robot_status = ctx.db_client.get( 257 | api_objects.RobotObjectV1, "test01").status 258 | waypoint = MISSION_TREE_1[-1]["route"]["waypoints"][-1] 259 | self.assertAlmostEqual(robot_status.pose.x, 260 | waypoint["x"], places=2) 261 | self.assertAlmostEqual(robot_status.pose.y, 262 | waypoint["y"], places=2) 263 | 264 | def test_teleop_by_user_request(self): 265 | """ Test teleop by user request""" 266 | robot = simulator.RobotInit("test01", 0, 0, 0) 267 | with test_context.TestContext([robot]) as ctx: 268 | # Create the robot and then the mission 269 | ctx.db_client.create( 270 | api_objects.RobotObjectV1(name="test01", status={})) 271 | time.sleep(0.25) 272 | ctx.db_client.create(test_context.mission_from_waypoints( 273 | "test01", SCENARIO1_WAYPOINTS)) 274 | 275 | for mission in ctx.db_client.watch(api_objects.MissionObjectV1): 276 | if mission.status.state == mission_object.MissionStateV1.RUNNING: 277 | break 278 | # Simulate teleop 279 | watcher = ctx.db_client.watch(api_objects.RobotObjectV1) 280 | # Start teleop 281 | ctx.call_teleop_service( 282 | robot_name="test01", teleop=robot_object.RobotTeleopActionV1.START) 283 | time.sleep(5) 284 | for update in watcher: 285 | if update.status.state == robot_object.RobotStateV1.TELEOP: 286 | break 287 | # Stop teleop 288 | ctx.call_teleop_service( 289 | robot_name="test01", teleop=robot_object.RobotTeleopActionV1.STOP) 290 | for update in watcher: 291 | if update.status.state == robot_object.RobotStateV1.ON_TASK: 292 | break 293 | for update in ctx.db_client.watch(api_objects.MissionObjectV1): 294 | if update.status.state == mission_object.MissionStateV1.COMPLETED: 295 | break 296 | 297 | 298 | if __name__ == "__main__": 299 | unittest.main() 300 | -------------------------------------------------------------------------------- /packages/controllers/mission/tests/test_context.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | import os 20 | import multiprocessing 21 | import random 22 | import time 23 | import signal 24 | from typing import Dict, List, NamedTuple, Tuple, Optional 25 | 26 | from cloud_common import objects as api_objects 27 | from cloud_common.objects import robot as robot_object 28 | from packages.controllers.mission.tests import client as simulator 29 | from packages.database import client as db_client 30 | from packages.utils import test_utils 31 | import requests 32 | import logging 33 | 34 | # The TCP port for the api server to listen on 35 | DATABASE_PORT = 5003 36 | # The TCP port for the api server to listen for controller traffic 37 | DATABASE_CONTROLLER_PORT = 5004 38 | # The TCP port for the MQTT broker to listen on 39 | MQTT_PORT_TCP = 1885 40 | # The WEBSOCKET port for the MQTT broker to listen on 41 | MQTT_PORT_WEBSOCKET = 9001 42 | # The transport mechanism("websockets", "tcp") for MQTT 43 | MQTT_TRANSPORT = "websockets" 44 | # The path for the websocket if 'mqtt_transport' is 'websockets'" 45 | MQTT_WS_PATH = "/mqtt" 46 | # The port for the MQTT broker to listen on 47 | MQTT_PORT = MQTT_PORT_TCP if MQTT_TRANSPORT == "tcp" else MQTT_PORT_WEBSOCKET 48 | # How far the simulator should move the robots each second 49 | SIM_SPEED = 10 50 | # Starting PostgreSQL Db on this port 51 | POSTGRES_DATABASE_PORT = 5432 52 | # The MQTT topic prefix 53 | MQTT_PREFIX = "uagv/v2/RobotCompany" 54 | 55 | 56 | class Delay(NamedTuple): 57 | mqtt_broker: int = 0 58 | mission_dispatch: int = 0 59 | mission_database: int = 0 60 | mission_simulator: int = 0 61 | 62 | 63 | class TestContext: 64 | crashed_process = False 65 | 66 | def __init__(self, robots, name="test context", delay: Delay = Delay(), 67 | tick_period: float = 0.25, enforce_start_order: bool = True, fail_as_warning=False, disable_request_factsheet=False): 68 | logging.basicConfig(level=logging.INFO, 69 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 70 | self.logger = logging.getLogger("Isaac Mission Dispatch Test Context") 71 | if TestContext.crashed_process: 72 | raise ValueError("Can't run test due to previous failure") 73 | 74 | # Set random seed to get pseudo-random numbers for consistent testing result 75 | random.seed(0) 76 | 77 | self._robots = robots 78 | self._name = name 79 | 80 | fail_as_warning = fail_as_warning or any( 81 | robot.fail_as_warning for robot in robots) 82 | 83 | self.logger.info(f"Opening context: {self._name}") 84 | 85 | # Register signal handler 86 | signal.signal(signal.SIGUSR1, self.catch_signal) 87 | 88 | # Start postgreSQL db 89 | self._postgres_database, postgres_address = \ 90 | self.run_docker(image="//packages/utils/test_utils:postgres-database-img-bundle", 91 | docker_args=["-e", "POSTGRES_PASSWORD=postgres", 92 | "-e", "POSTGRES_DB=mission", 93 | "-e", "POSTGRES_INITDB_ARGS=--auth-host=scram-sha-256 --auth-local=scram-sha-256"], 94 | args=['postgres']) 95 | test_utils.wait_for_port( 96 | host=postgres_address, port=POSTGRES_DATABASE_PORT, timeout=120) 97 | 98 | # Start the database 99 | self._database_process, self.database_address = \ 100 | self.run_docker(image="//packages/database:postgres-img-bundle", 101 | args=["--port", str(DATABASE_PORT), 102 | "--controller_port", str( 103 | DATABASE_CONTROLLER_PORT), 104 | "--db_host", postgres_address, 105 | "--db_port", str(POSTGRES_DATABASE_PORT), 106 | "--address", "0.0.0.0"]) 107 | 108 | # Start the Mosquitto broker 109 | self._mqtt_process, self.mqtt_address = self.run_docker( 110 | "//packages/utils/test_utils:mosquitto-img-bundle", 111 | args=[str(MQTT_PORT_TCP), str(MQTT_PORT_WEBSOCKET)], 112 | delay=delay.mqtt_broker) 113 | 114 | # Wait for both broker and db to start 115 | if enforce_start_order: 116 | self.wait_for_mqtt() 117 | self.wait_for_database() 118 | 119 | dispatch_args = ["--mqtt_port", str(MQTT_PORT), 120 | "--mqtt_host", self.mqtt_address, 121 | "--mqtt_transport", str(MQTT_TRANSPORT), 122 | "--mqtt_ws_path", str(MQTT_WS_PATH), 123 | "--mqtt_prefix", str(MQTT_PREFIX), 124 | "--database_url", f"http://{self.database_address}:{DATABASE_CONTROLLER_PORT}", 125 | ] 126 | 127 | if disable_request_factsheet: 128 | dispatch_args.append("--disable_request_factsheet") 129 | 130 | # Start mission server 131 | self._server_process, _ = self.run_docker( 132 | "//packages/controllers/mission:mission-img-bundle", 133 | args=dispatch_args, 134 | delay=delay.mission_dispatch) 135 | 136 | # Start simulator 137 | sim_args = ["--robots", " ".join(str(robot) for robot in self._robots), 138 | "--speed", str(SIM_SPEED), 139 | "--mqtt_port", str(MQTT_PORT), 140 | "--mqtt_host", self.mqtt_address, 141 | "--mqtt_transport", str(MQTT_TRANSPORT), 142 | "--mqtt_ws_path", str(MQTT_WS_PATH), 143 | "--mqtt_prefix", str(MQTT_PREFIX), 144 | "--tick_period", str(tick_period), 145 | ] 146 | 147 | if fail_as_warning: 148 | sim_args.append("--fail_as_warning") 149 | 150 | self._sim_process, _ = self.run_docker("//packages/controllers/mission/tests:client-img-bundle", 151 | args=sim_args, 152 | delay=delay.mission_simulator) 153 | 154 | # Create db client 155 | self.md_url = f"http://{self.database_address}:{DATABASE_PORT}" 156 | self.db_client = db_client.DatabaseClient(self.md_url) 157 | self.md_ctrl_url = f"http://{self.database_address}:{DATABASE_CONTROLLER_PORT}" 158 | self.db_controller_client = db_client.DatabaseClient(self.md_ctrl_url) 159 | 160 | def wait_for_database(self): 161 | test_utils.wait_for_port( 162 | host=self.database_address, port=DATABASE_PORT, timeout=120) 163 | 164 | def wait_for_mqtt(self): 165 | test_utils.wait_for_port( 166 | host=self.mqtt_address, port=MQTT_PORT, timeout=120) 167 | 168 | def restart_mission_server(self): 169 | self.close([self._server_process]) 170 | time.sleep(1) 171 | self._server_process, _ = self.run_docker( 172 | "//packages/controllers/mission:mission-img-bundle", 173 | args=["--mqtt_port", str(MQTT_PORT), 174 | "--mqtt_host", self.mqtt_address, 175 | "--mqtt_transport", str(MQTT_TRANSPORT), 176 | "--mqtt_ws_path", str(MQTT_WS_PATH), 177 | "--mqtt_prefix", str(MQTT_PREFIX), 178 | "--database_url", f"http://{self.database_address}:{DATABASE_CONTROLLER_PORT}"]) 179 | 180 | def restart_mqtt_server(self): 181 | # Restart the Mosquitto broker 182 | self.close([self._mqtt_process]) 183 | time.sleep(1) 184 | self._mqtt_process, self.mqtt_address = self.run_docker( 185 | "//packages/utils/test_utils:mosquitto-img-bundle", 186 | args=[str(MQTT_PORT_TCP), str(MQTT_PORT_WEBSOCKET)]) 187 | self.wait_for_mqtt() 188 | 189 | def catch_signal(self, signal, frame): 190 | TestContext.crashed_process = True 191 | raise OSError("Child process crashed!") 192 | 193 | def run_docker(self, image: str, args: List[str], docker_args: List[str] = None, 194 | delay: float = 0.0) -> Tuple[multiprocessing.Process, str]: 195 | pid = os.getpid() 196 | queue = multiprocessing.Queue() 197 | 198 | def wrapper_process(): 199 | docker_process, address = \ 200 | test_utils.run_docker_target( 201 | image, args=args, docker_args=docker_args, delay=delay) 202 | queue.put(address) 203 | docker_process.wait() 204 | os.kill(pid, signal.SIGUSR1) 205 | 206 | process = multiprocessing.Process(target=wrapper_process, daemon=True) 207 | process.start() 208 | return process, queue.get() 209 | 210 | def close(self, processes): 211 | for process in processes: 212 | if process is not None: 213 | process.terminate() 214 | process.join() 215 | process.close() 216 | 217 | def call_teleop_service(self, robot_name: str, teleop: robot_object.RobotTeleopActionV1): 218 | endpoint = self.md_url + f"/robot/{robot_name}/teleop" 219 | response = requests.post(url=endpoint, params={"params": teleop.value}) 220 | if response.status_code == 200: 221 | self.logger.info(f"Teleop {teleop.value} request sent") 222 | else: 223 | self.logger.info(f"Teleop {teleop.value} failed") 224 | 225 | def __enter__(self): 226 | return self 227 | 228 | def __exit__(self, type, value, traceback): 229 | self.close([self._server_process, self._database_process, 230 | self._postgres_database, self._mqtt_process, self._sim_process]) 231 | self.logger.info(f"Context closed: {self._name}") 232 | 233 | 234 | def mission_from_waypoints(robot: str, waypoints, name: Optional[str] = None, timeout: int = 1000): 235 | """Converts a (x, y) coordinate into a mission object""" 236 | return api_objects.MissionObjectV1( 237 | name=name, 238 | robot=robot, 239 | mission_tree=[ 240 | {"route": {"waypoints": [{"x": x, "y": y, "theta": 0}]}} for x, y in waypoints 241 | ], 242 | status={}, 243 | timeout=timeout) 244 | 245 | 246 | def mission_from_waypoint(robot: str, x: float, y: float, name: Optional[str] = None): 247 | """Converts a (x, y) coordinate into a mission object""" 248 | return mission_from_waypoints(robot, [(x, y)], name) 249 | 250 | 251 | def pose1D_generator(pose_scale=3, min_dist=0.5): 252 | """Generate random 1D pose within certain range 253 | 254 | Args: 255 | pose_scale (int, optional): range from 0 to pose_scale. Defaults to 3. 256 | min_dist (float, optional): minimum value of the point. Defaults to 0.5. 257 | 258 | Returns: 259 | float: 1D pose 260 | """ 261 | return round(random.random() * pose_scale + min_dist, 1) 262 | 263 | 264 | def route_generator(parent: str = "root", name: str = None, waypoints_size: int = None): 265 | """ Generate route dict 266 | 267 | Args: 268 | parent: parent name 269 | name: node name 270 | 271 | Returns: 272 | Dict: route mission node 273 | """ 274 | if waypoints_size is None: 275 | waypoints_size = random.randint(1, 4) 276 | waypoints = {"waypoints": [{"x": pose1D_generator(), "y": pose1D_generator(), "theta": 0} 277 | for _ in range(waypoints_size)]} 278 | route_dict = {"route": waypoints, "parent": parent} 279 | if name is not None: 280 | route_dict.update({"name": name}) 281 | return route_dict 282 | 283 | 284 | def move_generator(parent: str = "root", name: str = None, move: dict = {}): 285 | """ Generate move dict 286 | 287 | Args: 288 | parent: parent name 289 | name: node name 290 | 291 | Returns: 292 | Dict: move mission node 293 | """ 294 | move_dict = {"move": move, "parent": parent} 295 | if name is not None: 296 | move_dict.update({"name": name}) 297 | return move_dict 298 | 299 | 300 | def action_generator(params: dict, parent: str = "root", 301 | name: str = None, action_type: str = "dummy_action") -> Dict: 302 | """ Generate action mission node 303 | 304 | Args: 305 | params: action parameters 306 | parent: parent name 307 | name: node name 308 | action_type: type of the action 309 | 310 | Returns: 311 | Dict: action mission node 312 | """ 313 | action_dict = {"parent": parent, 314 | "action": {"action_type": action_type, 315 | "action_parameters": params}} 316 | if name is not None: 317 | action_dict.update({"name": name}) 318 | return action_dict 319 | 320 | 321 | def notify_generator(url: str, json_data: Dict, 322 | parent: str = "root", name: str = None) -> Dict: 323 | """ Generate notify mission node 324 | 325 | Args: 326 | url (str): URL to make API call 327 | json_data (Dict): JSON payload to be included in API call. 328 | parent: parent name 329 | name: node name 330 | 331 | Returns: 332 | Dict: notify mission node 333 | """ 334 | notify_dict = {"parent": parent, 335 | "notify": { 336 | "url": url, 337 | "json_data": json_data 338 | }} 339 | if name is not None: 340 | notify_dict.update({"name": name}) 341 | return notify_dict 342 | 343 | 344 | def mission_object_generator(robot: str, mission_tree, timeout=1000): 345 | """Converts a mission tree into a mission object""" 346 | return api_objects.MissionObjectV1( 347 | robot=robot, 348 | mission_tree=mission_tree, 349 | status={}, timeout=timeout) 350 | -------------------------------------------------------------------------------- /packages/controllers/mission/tests/cancel_mission.py: -------------------------------------------------------------------------------- 1 | """ 2 | SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES 3 | Copyright (c) 2021-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | https://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | 17 | SPDX-License-Identifier: Apache-2.0 18 | """ 19 | import time 20 | import unittest 21 | 22 | from cloud_common import objects as api_objects 23 | from packages.controllers.mission.tests import client as simulator 24 | from cloud_common.objects import mission as mission_object 25 | from cloud_common.objects import robot as robot_object 26 | from cloud_common.objects import common 27 | 28 | from packages.controllers.mission.tests import test_context 29 | 30 | 31 | class TestCancelMissions(unittest.TestCase): 32 | def test_cancel_pending_mission(self): 33 | """ Test if pending mission gets canceled """ 34 | waypoints_1 = (10, 10) 35 | waypoints_2 = (3, 3) 36 | robot = simulator.RobotInit("test01", 0, 0, 0) 37 | with test_context.TestContext([robot], tick_period=1.0) as ctx: 38 | # Create the robot 39 | ctx.db_client.create( 40 | api_objects.RobotObjectV1(name="test01", status={})) 41 | time.sleep(0.25) 42 | self.assertGreater( 43 | len(ctx.db_client.list(api_objects.RobotObjectV1)), 0) 44 | 45 | # Create two missions 46 | mission_1 = test_context.mission_from_waypoint( 47 | "test01", waypoints_1[0], waypoints_1[1]) 48 | ctx.db_client.create(mission_1) 49 | time.sleep(0.25) 50 | 51 | # The second mission will be pending as the robot executes the first mission. 52 | # The test will demonstrate the cancelation of this pending mission. 53 | mission_2 = test_context.mission_from_waypoint( 54 | "test01", waypoints_2[0], waypoints_2[1]) 55 | ctx.db_client.create(mission_2) 56 | 57 | missions = ctx.db_client.list(api_objects.MissionObjectV1) 58 | self.assertEqual(len(missions), 2) 59 | 60 | # Cancel the mission 61 | ctx.db_client.cancel_mission(mission_2.name) 62 | # Wait till it's done 63 | for mission in ctx.db_client.watch(api_objects.MissionObjectV1): 64 | if mission.status.state.done and mission.name == mission_2.name: 65 | self.assertEqual(mission.status.state, 66 | mission_object.MissionStateV1.CANCELED) 67 | break 68 | 69 | def test_delete_pending_mission(self): 70 | """ Test if pending mission gets deleted """ 71 | waypoints_1 = (10, 10) 72 | waypoints_2 = (3, 3) 73 | robot = simulator.RobotInit("test01", 0, 0, 0) 74 | with test_context.TestContext([robot], tick_period=1.0) as ctx: 75 | # Create the robot 76 | ctx.db_client.create( 77 | api_objects.RobotObjectV1(name="test01", status={})) 78 | time.sleep(0.25) 79 | self.assertGreater( 80 | len(ctx.db_client.list(api_objects.RobotObjectV1)), 0) 81 | 82 | # Create two missions 83 | mission_1 = test_context.mission_from_waypoint( 84 | "test01", waypoints_1[0], waypoints_1[1]) 85 | ctx.db_client.create(mission_1) 86 | time.sleep(0.25) 87 | 88 | # The second mission will be pending as the robot executes the first mission. 89 | # The test will demonstrate the cancelation of this pending mission. 90 | mission_2 = test_context.mission_from_waypoint( 91 | "test01", waypoints_2[0], waypoints_2[1]) 92 | ctx.db_client.create(mission_2) 93 | 94 | missions = ctx.db_client.list(api_objects.MissionObjectV1) 95 | self.assertEqual(len(missions), 2) 96 | 97 | # Delete the mission 98 | ctx.db_client.delete(api_objects.MissionObjectV1, mission_2.name) 99 | time.sleep(10) 100 | 101 | # Check that the second mission has been deleted 102 | missions = ctx.db_client.list(api_objects.MissionObjectV1) 103 | self.assertEqual(len(missions), 1) 104 | 105 | def test_cancel_running_mission(self): 106 | """ Test if running mission gets canceled """ 107 | waypoint_x = 5 108 | waypoint_y = 5 109 | robot = simulator.RobotInit("test01", 0, 0, 0) 110 | with test_context.TestContext([robot], tick_period=1.0) as ctx: 111 | # Create the robot 112 | ctx.db_client.create( 113 | api_objects.RobotObjectV1(name="test01", status={})) 114 | time.sleep(0.25) 115 | self.assertGreater( 116 | len(ctx.db_client.list(api_objects.RobotObjectV1)), 0) 117 | 118 | # Create mission. This is a long mission so that the cancelation request is made 119 | # while the mission is still running. 120 | test_mission = test_context.mission_from_waypoint( 121 | "test01", waypoint_x, waypoint_y) 122 | ctx.db_client.create(test_mission) 123 | 124 | # Make sure the mission is running 125 | for mission in ctx.db_client.watch(api_objects.MissionObjectV1): 126 | if mission.status.state == mission_object.MissionStateV1.RUNNING: 127 | break 128 | 129 | # Cancel the mission 130 | ctx.db_client.cancel_mission(test_mission.name) 131 | 132 | # Wait till it's done 133 | for mission in ctx.db_client.watch(api_objects.MissionObjectV1): 134 | if mission.status.state.done: 135 | self.assertEqual(mission.status.state, 136 | mission_object.MissionStateV1.CANCELED) 137 | self.assertEqual( 138 | mission.status.node_status["0"].state, mission_object.MissionStateV1.CANCELED) 139 | self.assertEqual( 140 | len(ctx.db_client.list(api_objects.MissionObjectV1)), 1) 141 | break 142 | 143 | def test_delete_running_mission(self): 144 | """ Test if running mission gets deleted after completed """ 145 | waypoint_x = 5 146 | waypoint_y = 5 147 | robot = simulator.RobotInit("test01", 0, 0, 0) 148 | with test_context.TestContext([robot], tick_period=1.0) as ctx: 149 | # Create the robot 150 | ctx.db_client.create( 151 | api_objects.RobotObjectV1(name="test01", status={})) 152 | time.sleep(0.25) 153 | 154 | # Create mission. This is a long mission so that the cancelation request is made 155 | # while the mission is still running. 156 | test_mission = test_context.mission_from_waypoint( 157 | "test01", waypoint_x, waypoint_y) 158 | ctx.db_client.create(test_mission) 159 | 160 | # Make sure the mission is running 161 | for mission in ctx.db_client.watch(api_objects.MissionObjectV1): 162 | if mission.status.state == mission_object.MissionStateV1.RUNNING and mission.name == test_mission.name: 163 | break 164 | 165 | # Delete the mission 166 | ctx.db_client.delete( 167 | api_objects.MissionObjectV1, test_mission.name) 168 | time.sleep(0.25) 169 | fetched_mission = ctx.db_client.get( 170 | api_objects.MissionObjectV1, test_mission.name) 171 | self.assertEqual(fetched_mission.lifecycle, 172 | api_objects.object.ObjectLifecycleV1.PENDING_DELETE) 173 | self.assertEqual( 174 | len(ctx.db_client.list(api_objects.MissionObjectV1)), 1) 175 | 176 | # Wait the mission is completed 177 | for update in ctx.db_client.watch(api_objects.MissionObjectV1): 178 | if update.status.state.done: 179 | break 180 | 181 | # Check that the mission has been deleted 182 | time.sleep(0.25) 183 | self.assertEqual( 184 | len(ctx.db_client.list(api_objects.MissionObjectV1)), 0) 185 | 186 | def test_skip_canceled_mission(self): 187 | """ Test if a mission after a canceled mission gets properly executed """ 188 | waypoints = [(5, 5), (5, 10), (10, 5)] 189 | mission_names = ["m1", "m_cancel", "m3"] 190 | robot = simulator.RobotInit("test01", 0, 0, 0) 191 | with test_context.TestContext([robot]) as ctx: 192 | # Create the robot 193 | ctx.db_client.create( 194 | api_objects.RobotObjectV1(name="test01", status={})) 195 | time.sleep(0.25) 196 | self.assertGreater( 197 | len(ctx.db_client.list(api_objects.RobotObjectV1)), 0) 198 | 199 | for waypoint, name in zip(waypoints, mission_names): 200 | mission = test_context.mission_from_waypoint( 201 | "test01", waypoint[0], waypoint[1], name) 202 | ctx.db_client.create(mission) 203 | # In case the mission is done before cancel 204 | if name == "m_cancel": 205 | ctx.db_client.cancel_mission(name) 206 | 207 | # Cancel the second mission 208 | completed_mission = 0 209 | for mission in ctx.db_client.watch(api_objects.MissionObjectV1): 210 | if mission.status.state.done: 211 | completed_mission += 1 212 | if completed_mission == 3: 213 | break 214 | 215 | # Check that the second mission has been canceled, and the mission after is completed 216 | missions = ctx.db_client.list(api_objects.MissionObjectV1) 217 | self.assertEqual(len(missions), 3) 218 | for mission in missions: 219 | expected_state = mission_object.MissionStateV1.COMPLETED 220 | if mission.name == "m_cancel": 221 | expected_state = mission_object.MissionStateV1.CANCELED 222 | self.assertEqual(mission.status.state, expected_state) 223 | 224 | def test_cancel_running_mission_run_new_mission(self): 225 | """ Test if canceling a running mission will transition to running a new mission """ 226 | waypoints = [(10, 10), (3, 3)] 227 | mission_names = [] 228 | robot = simulator.RobotInit("test01", 0, 0, 0) 229 | with test_context.TestContext([robot], tick_period=0.5) as ctx: 230 | # Create the robot 231 | ctx.db_client.create( 232 | api_objects.RobotObjectV1(name="test01", status={})) 233 | time.sleep(0.25) 234 | self.assertGreater( 235 | len(ctx.db_client.list(api_objects.RobotObjectV1)), 0) 236 | 237 | # Create the missions 238 | for waypoint in waypoints: 239 | mission = test_context.mission_from_waypoint( 240 | "test01", waypoint[0], waypoint[1]) 241 | ctx.db_client.create(mission) 242 | mission_names.append(mission.name) 243 | time.sleep(0.25) 244 | 245 | # Make sure the mission is running 246 | for mission in ctx.db_client.watch(api_objects.MissionObjectV1): 247 | if mission.status.state == mission_object.MissionStateV1.RUNNING and \ 248 | mission.name == mission_names[0]: 249 | break 250 | 251 | # Cancel the first mission 252 | ctx.db_client.cancel_mission(mission_names[0]) 253 | finished_mission = 0 254 | for mission in ctx.db_client.watch(api_objects.MissionObjectV1): 255 | if mission.status.state.done: 256 | finished_mission += 1 257 | if finished_mission == 2: 258 | break 259 | 260 | # Check that the first mission has been canceled, and the mission after is completed 261 | missions = ctx.db_client.list(api_objects.MissionObjectV1) 262 | self.assertEqual(len(missions), 2) 263 | idx = 0 if missions[0].name == mission_names[0] else 1 264 | self.assertEqual(missions[idx].status.state, 265 | mission_object.MissionStateV1.CANCELED) 266 | self.assertEqual(missions[1 - idx].status.state, 267 | mission_object.MissionStateV1.COMPLETED) 268 | 269 | def test_delete_completed_mission(self): 270 | """ Test if a completed mission gets deleted """ 271 | waypoint_x = 1 272 | waypoint_y = 1 273 | robot = simulator.RobotInit("test01", 0, 0, 0) 274 | with test_context.TestContext([robot]) as ctx: 275 | # Create the robot 276 | ctx.db_client.create( 277 | api_objects.RobotObjectV1(name="test01", status={})) 278 | time.sleep(0.25) 279 | 280 | # Create mission. This is a long mission so that the cancelation request is made 281 | # while the mission is still running. 282 | test_mission = test_context.mission_from_waypoint( 283 | "test01", waypoint_x, waypoint_y) 284 | ctx.db_client.create(test_mission) 285 | 286 | # Make sure the mission is completed 287 | for mission in ctx.db_client.watch(api_objects.MissionObjectV1): 288 | if mission.status.state.done and mission.name == test_mission.name: 289 | break 290 | 291 | # Delete the mission 292 | ctx.db_client.delete( 293 | api_objects.MissionObjectV1, test_mission.name) 294 | # Check that the mission has been deleted 295 | time.sleep(0.25) 296 | self.assertEqual( 297 | len(ctx.db_client.list(api_objects.MissionObjectV1)), 0) 298 | 299 | def test_cancel_completed_mission(self): 300 | """ Test if a completed mission can be canceled """ 301 | waypoint_x = 1 302 | waypoint_y = 1 303 | robot = simulator.RobotInit("test01", 0, 0, 0) 304 | with test_context.TestContext([robot]) as ctx: 305 | # Create the robot 306 | ctx.db_client.create( 307 | api_objects.RobotObjectV1(name="test01", status={})) 308 | time.sleep(0.25) 309 | self.assertGreater( 310 | len(ctx.db_client.list(api_objects.RobotObjectV1)), 0) 311 | 312 | # Create mission 313 | test_mission = test_context.mission_from_waypoint( 314 | "test01", waypoint_x, waypoint_y) 315 | ctx.db_client.create(test_mission) 316 | 317 | # Make sure the mission is completed 318 | for mission in ctx.db_client.watch(api_objects.MissionObjectV1): 319 | if mission.status.state.done: 320 | break 321 | 322 | # Cancel the mission 323 | with self.assertRaises(common.ICSUsageError): 324 | ctx.db_client.cancel_mission(test_mission.name) 325 | 326 | 327 | if __name__ == "__main__": 328 | unittest.main() 329 | -------------------------------------------------------------------------------- /bzl/pylintrc: -------------------------------------------------------------------------------- 1 | # This Pylint rcfile contains a best-effort configuration to uphold the 2 | # best-practices and style described in the Google Python style guide: 3 | # https://google.github.io/styleguide/pyguide.html 4 | # 5 | # Its canonical open-source location is: 6 | # https://google.github.io/styleguide/pylintrc 7 | 8 | [MASTER] 9 | 10 | # Files or directories to be skipped. They should be base names, not paths. 11 | ignore=third_party 12 | 13 | # Files or directories matching the regex patterns are skipped. The regex 14 | # matches against base names, not paths. 15 | ignore-patterns= 16 | 17 | # Pickle collected data for later comparisons. 18 | persistent=no 19 | 20 | # List of plugins (as comma separated values of python modules names) to load, 21 | # usually to register additional checkers. 22 | load-plugins= 23 | 24 | # Use multiple processes to speed up Pylint. 25 | jobs=4 26 | 27 | # Allow loading of arbitrary C extensions. Extensions are imported into the 28 | # active Python interpreter and may run arbitrary code. 29 | unsafe-load-any-extension=no 30 | 31 | disable=missing-module-docstring, 32 | redefined-builtin, 33 | unsubscriptable-object, 34 | unused-argument, 35 | no-self-argument, 36 | missing-kwoa 37 | 38 | 39 | [MESSAGES CONTROL] 40 | 41 | # Only show warnings with the listed confidence levels. Leave empty to show 42 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED 43 | confidence= 44 | 45 | # Enable the message, report, category or checker with the given id(s). You can 46 | # either give multiple identifier separated by comma (,) or put this option 47 | # multiple time (only on the command line, not in the configuration file where 48 | # it should appear only once). See also the "--disable" option for examples. 49 | #enable= 50 | 51 | # Disable the message, report, category or checker with the given id(s). You 52 | # can either give multiple identifiers separated by comma (,) or put this 53 | # option multiple times (only on the command line, not in the configuration 54 | # file where it should appear only once).You can also use "--disable=all" to 55 | # disable everything first and then reenable specific checks. For example, if 56 | # you want to run only the similarities checker, you can use "--disable=all 57 | # --enable=similarities". If you want to run only the classes checker, but have 58 | # no Warning level messages displayed, use"--disable=all --enable=classes 59 | # --disable=W" 60 | disable=abstract-method, 61 | apply-builtin, 62 | arguments-differ, 63 | attribute-defined-outside-init, 64 | backtick, 65 | bad-option-value, 66 | basestring-builtin, 67 | buffer-builtin, 68 | c-extension-no-member, 69 | consider-using-enumerate, 70 | cmp-builtin, 71 | cmp-method, 72 | coerce-builtin, 73 | coerce-method, 74 | delslice-method, 75 | div-method, 76 | duplicate-code, 77 | eq-without-hash, 78 | execfile-builtin, 79 | file-builtin, 80 | filter-builtin-not-iterating, 81 | fixme, 82 | getslice-method, 83 | global-statement, 84 | hex-method, 85 | idiv-method, 86 | implicit-str-concat-in-sequence, 87 | import-error, 88 | import-self, 89 | import-star-module-level, 90 | inconsistent-return-statements, 91 | input-builtin, 92 | intern-builtin, 93 | invalid-str-codec, 94 | locally-disabled, 95 | long-builtin, 96 | long-suffix, 97 | map-builtin-not-iterating, 98 | misplaced-comparison-constant, 99 | missing-function-docstring, 100 | metaclass-assignment, 101 | next-method-called, 102 | next-method-defined, 103 | no-absolute-import, 104 | no-else-break, 105 | no-else-continue, 106 | no-else-raise, 107 | no-else-return, 108 | no-init, # added 109 | no-member, 110 | no-name-in-module, 111 | no-self-use, 112 | nonzero-method, 113 | oct-method, 114 | old-division, 115 | old-ne-operator, 116 | old-octal-literal, 117 | old-raise-syntax, 118 | parameter-unpacking, 119 | print-statement, 120 | raise-missing-from, # TODO: Re-enable 121 | raising-string, 122 | range-builtin-not-iterating, 123 | raw_input-builtin, 124 | rdiv-method, 125 | reduce-builtin, 126 | relative-import, 127 | reload-builtin, 128 | return-value, # TODO: Re-enable 129 | round-builtin, 130 | setslice-method, 131 | signature-differs, 132 | standarderror-builtin, 133 | superfluous-parens, 134 | suppressed-message, 135 | sys-max-int, 136 | too-few-public-methods, 137 | too-many-ancestors, 138 | too-many-arguments, 139 | too-many-boolean-expressions, 140 | too-many-branches, 141 | too-many-instance-attributes, 142 | too-many-locals, 143 | too-many-nested-blocks, 144 | too-many-public-methods, 145 | too-many-return-statements, 146 | too-many-statements, 147 | trailing-newlines, 148 | unichr-builtin, 149 | unicode-builtin, 150 | unnecessary-pass, 151 | unpacking-in-except, 152 | useless-else-on-loop, 153 | useless-object-inheritance, 154 | useless-suppression, 155 | using-cmp-argument, 156 | wrong-import-order, 157 | xrange-builtin, 158 | zip-builtin-not-iterating, 159 | 160 | 161 | [REPORTS] 162 | 163 | # Set the output format. Available formats are text, parseable, colorized, msvs 164 | # (visual studio) and html. You can also give a reporter class, eg 165 | # mypackage.mymodule.MyReporterClass. 166 | output-format=text 167 | 168 | # Put messages in a separate file for each module / package specified on the 169 | # command line instead of printing them on stdout. Reports (if any) will be 170 | # written in a file name "pylint_global.[txt|html]". This option is deprecated 171 | # and it will be removed in Pylint 2.0. 172 | #files-output=no 173 | 174 | # Tells whether to display a full report or only the messages 175 | reports=no 176 | 177 | # Python expression which should return a note less than 10 (10 is the highest 178 | # note). You have access to the variables errors warning, statement which 179 | # respectively contain the number of errors / warnings messages and the total 180 | # number of statements analyzed. This is used by the global evaluation report 181 | # (RP0004). 182 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 183 | 184 | # Template used to display messages. This is a python new-style format string 185 | # used to format the message information. See doc for all details 186 | #msg-template= 187 | 188 | 189 | [BASIC] 190 | 191 | # Good variable names which should always be accepted, separated by a comma 192 | good-names=main,_ 193 | 194 | # Bad variable names which should always be refused, separated by a comma 195 | bad-names= 196 | 197 | # Colon-delimited sets of names that determine each other's naming style when 198 | # the name regexes allow several styles. 199 | name-group= 200 | 201 | # Include a hint for the correct naming format with invalid-name 202 | include-naming-hint=no 203 | 204 | # List of decorators that produce properties, such as abc.abstractproperty. Add 205 | # to this list to register other decorators that produce valid properties. 206 | property-classes=abc.abstractproperty,cached_property.cached_property,cached_property.threaded_cached_property,cached_property.cached_property_with_ttl,cached_property.threaded_cached_property_with_ttl 207 | 208 | # Regular expression matching correct function names 209 | function-rgx=^(?:(?PsetUp|tearDown|setUpModule|tearDownModule)|(?P_?[A-Z][a-zA-Z0-9]*)|(?P_?[a-z][a-z0-9_]*))$ 210 | 211 | # Regular expression matching correct variable names 212 | variable-rgx=^[a-z][a-z0-9_]*$ 213 | 214 | # Regular expression matching correct constant names 215 | const-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ 216 | 217 | # Regular expression matching correct attribute names 218 | attr-rgx=^_{0,2}[a-z][a-z0-9_]*$ 219 | 220 | # Regular expression matching correct argument names 221 | argument-rgx=^[a-z][a-z0-9_]*$ 222 | 223 | # Regular expression matching correct class attribute names 224 | class-attribute-rgx=^(_?[A-Z][A-Z0-9_]*|__[a-z0-9_]+__|_?[a-z][a-z0-9_]*)$ 225 | 226 | # Regular expression matching correct inline iteration names 227 | inlinevar-rgx=^[a-z][a-z0-9_]*$ 228 | 229 | # Regular expression matching correct class names 230 | class-rgx=^_?[A-Z][a-zA-Z0-9]*$ 231 | 232 | # Regular expression matching correct module names 233 | module-rgx=^(_?[a-z][a-z0-9_]*|__init__)$ 234 | 235 | # Regular expression matching correct method names 236 | method-rgx=(?x)^(?:(?P_[a-z0-9_]+__|runTest|setUp|tearDown|setUpTestCase|tearDownTestCase|setupSelf|tearDownClass|setUpClass|(test|assert)_*[A-Z0-9][a-zA-Z0-9_]*|next)|(?P_{0,2}[A-Z][a-zA-Z0-9_]*)|(?P_{0,2}[a-z][a-z0-9_]*))$ 237 | 238 | # Regular expression which should only match function or class names that do 239 | # not require a docstring. 240 | no-docstring-rgx=(__.*__|main|test.*|.*test|.*Test)$ 241 | 242 | # Minimum line length for functions/classes that require docstrings, shorter 243 | # ones are exempt. 244 | docstring-min-length=10 245 | 246 | 247 | [TYPECHECK] 248 | 249 | # List of decorators that produce context managers, such as 250 | # contextlib.contextmanager. Add to this list to register other decorators that 251 | # produce valid context managers. 252 | contextmanager-decorators=contextlib.contextmanager,contextlib2.contextmanager 253 | 254 | # Tells whether missing members accessed in mixin class should be ignored. A 255 | # mixin class is detected if its name ends with "mixin" (case insensitive). 256 | ignore-mixin-members=yes 257 | 258 | # List of module names for which member attributes should not be checked 259 | # (useful for modules/projects where namespaces are manipulated during runtime 260 | # and thus existing member attributes cannot be deduced by static analysis. It 261 | # supports qualified module names, as well as Unix pattern matching. 262 | ignored-modules= 263 | 264 | # List of class names for which member attributes should not be checked (useful 265 | # for classes with dynamically set attributes). This supports the use of 266 | # qualified names. 267 | ignored-classes=optparse.Values,thread._local,_thread._local 268 | 269 | # List of members which are set dynamically and missed by pylint inference 270 | # system, and so shouldn't trigger E1101 when accessed. Python regular 271 | # expressions are accepted. 272 | generated-members= 273 | 274 | 275 | [FORMAT] 276 | 277 | # Maximum number of characters on a single line. 278 | max-line-length=100 279 | 280 | # Regexp for a line that is allowed to be longer than the limit. 281 | ignore-long-lines=(?x)( 282 | ^\s*(\#\ )??$| 283 | ^\s*(from\s+\S+\s+)?import\s+.+$) 284 | 285 | # Allow the body of an if to be on the same line as the test if there is no 286 | # else. 287 | single-line-if-stmt=yes 288 | 289 | # List of optional constructs for which whitespace checking is disabled. `dict- 290 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 291 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 292 | # `empty-line` allows space-only lines. 293 | #no-space-check= 294 | 295 | # Maximum number of lines in a module 296 | max-module-lines=99999 297 | 298 | # String used as indentation unit. 299 | indent-string=' ' 300 | 301 | # Number of spaces of indent required inside a hanging or continued line. 302 | indent-after-paren=4 303 | 304 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 305 | expected-line-ending-format= 306 | 307 | 308 | [MISCELLANEOUS] 309 | 310 | # List of note tags to take in consideration, separated by a comma. 311 | notes=TODO 312 | 313 | 314 | [STRING] 315 | 316 | # This flag controls whether inconsistent-quotes generates a warning when the 317 | # character used as a quote delimiter is used inconsistently within a module. 318 | check-quote-consistency=yes 319 | 320 | 321 | [VARIABLES] 322 | 323 | # Tells whether we should check for unused import in __init__ files. 324 | init-import=no 325 | 326 | # A regular expression matching the name of dummy variables (i.e. expectedly 327 | # not used). 328 | dummy-variables-rgx=^\*{0,2}(_$|unused_|dummy_) 329 | 330 | # List of additional names supposed to be defined in builtins. Remember that 331 | # you should avoid to define new builtins when possible. 332 | additional-builtins= 333 | 334 | # List of strings which can identify a callback function by name. A callback 335 | # name must start or end with one of those strings. 336 | callbacks=cb_,_cb 337 | 338 | # List of qualified module names which can have objects that can redefine 339 | # builtins. 340 | redefining-builtins-modules=six,six.moves,past.builtins,future.builtins,functools 341 | 342 | 343 | [LOGGING] 344 | 345 | # Logging modules to check that the string format arguments are in logging 346 | # function parameter format 347 | logging-modules=logging,absl.logging,tensorflow.io.logging 348 | 349 | 350 | [SIMILARITIES] 351 | 352 | # Minimum lines number of a similarity. 353 | min-similarity-lines=4 354 | 355 | # Ignore comments when computing similarities. 356 | ignore-comments=yes 357 | 358 | # Ignore docstrings when computing similarities. 359 | ignore-docstrings=yes 360 | 361 | # Ignore imports when computing similarities. 362 | ignore-imports=no 363 | 364 | 365 | [SPELLING] 366 | 367 | # Spelling dictionary name. Available dictionaries: none. To make it working 368 | # install python-enchant package. 369 | spelling-dict= 370 | 371 | # List of comma separated words that should not be checked. 372 | spelling-ignore-words= 373 | 374 | # A path to a file that contains private dictionary; one word per line. 375 | spelling-private-dict-file= 376 | 377 | # Tells whether to store unknown words to indicated private dictionary in 378 | # --spelling-private-dict-file option instead of raising a message. 379 | spelling-store-unknown-words=no 380 | 381 | 382 | [IMPORTS] 383 | 384 | # Deprecated modules which should not be used, separated by a comma 385 | deprecated-modules=regsub, 386 | TERMIOS, 387 | Bastion, 388 | rexec, 389 | sets 390 | 391 | # Create a graph of every (i.e. internal and external) dependencies in the 392 | # given file (report RP0402 must not be disabled) 393 | import-graph= 394 | 395 | # Create a graph of external dependencies in the given file (report RP0402 must 396 | # not be disabled) 397 | ext-import-graph= 398 | 399 | # Create a graph of internal dependencies in the given file (report RP0402 must 400 | # not be disabled) 401 | int-import-graph= 402 | 403 | # Force import order to recognize a module as part of the standard 404 | # compatibility libraries. 405 | known-standard-library= 406 | 407 | # Force import order to recognize a module as part of a third party library. 408 | known-third-party=enchant, absl 409 | 410 | # Analyse import fallback blocks. This can be used to support both Python 2 and 411 | # 3 compatible code, which means that the block might have code that exists 412 | # only in one or another interpreter, leading to false positives when analysed. 413 | analyse-fallback-blocks=no 414 | 415 | 416 | [CLASSES] 417 | 418 | # List of method names used to declare (i.e. assign) instance attributes. 419 | defining-attr-methods=__init__, 420 | __new__, 421 | setUp 422 | 423 | # List of member names, which should be excluded from the protected access 424 | # warning. 425 | exclude-protected=_asdict, 426 | _fields, 427 | _replace, 428 | _source, 429 | _make 430 | 431 | # List of valid names for the first argument in a class method. 432 | valid-classmethod-first-arg=cls, 433 | class_ 434 | 435 | # List of valid names for the first argument in a metaclass class method. 436 | valid-metaclass-classmethod-first-arg=mcs 437 | 438 | 439 | [EXCEPTIONS] 440 | 441 | # Exceptions that will emit a warning when being caught. Defaults to 442 | # "Exception" 443 | #overgeneral-exceptions=StandardError, 444 | # Exception, 445 | # BaseException 446 | --------------------------------------------------------------------------------