From f3f63a7633701326632da8dbbfd47d8297be6d99 Mon Sep 17 00:00:00 2001 From: hexbabe Date: Thu, 30 Nov 2023 17:15:08 -0500 Subject: [PATCH] Add boilerplate derived from OAK-D module --- .gitignore | 13 ++++ Makefile | 27 ++++++++ README.md | 4 +- meta.json | 13 ++++ packaging/AppImageBuilder.yml | 32 +++++++++ packaging/Dockerfile | 21 ++++++ packaging/viam-server.png | Bin 0 -> 3811 bytes requirements.txt | 1 + run.sh | 43 ++++++++++++ src/__init__.py | 14 ++++ src/main.py | 22 ++++++ src/module.py | 122 ++++++++++++++++++++++++++++++++++ 12 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 meta.json create mode 100644 packaging/AppImageBuilder.yml create mode 100755 packaging/Dockerfile create mode 100644 packaging/viam-server.png create mode 100644 requirements.txt create mode 100755 run.sh create mode 100644 src/__init__.py create mode 100644 src/main.py create mode 100644 src/module.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7b0ac1b --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# top level +.DS_Store +.pytest_cache +.venv +module.tar.gz +viam-module-env +*.AppImage + +# src +src/__pycache__ + +# unit tests +tests/__pycache__ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..53088e1 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +# Makefile +IMAGE_NAME = appimage-builder-viam-python-example +CONTAINER_NAME = appimage-builder-viam-python-example +AARCH64_APPIMAGE_NAME = python-appimage-example--aarch64.AppImage + +# Developing +default: + @echo No make target specified. + +# Packaging +build: appimage-aarch64 + +non-appimage: clean # builds tarball from source that runs using venv + tar -czf module.tar.gz run.sh requirements.txt src + +appimage-aarch64: clean + docker build -f packaging/Dockerfile -t $(IMAGE_NAME) . + docker run --name $(CONTAINER_NAME) $(IMAGE_NAME) + docker cp $(CONTAINER_NAME):/app/$(AARCH64_APPIMAGE_NAME) ./$(AARCH64_APPIMAGE_NAME) + chmod +x ${AARCH64_APPIMAGE_NAME} + tar -czf module.tar.gz run.sh $(AARCH64_APPIMAGE_NAME) + +clean: + rm -f $(AARCH64_APPIMAGE_NAME) + rm -f module.tar.gz + docker container stop $(CONTAINER_NAME) || true + docker container rm $(CONTAINER_NAME) || true diff --git a/README.md b/README.md index e10b949..d8e6315 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# python-appimage-module -Example repo for bundling a Viam module using AppImage and AppImageBuilder +# python-appimage-example +Example repo for bundling a Viam module as an AppImage with AppImageBuilder diff --git a/meta.json b/meta.json new file mode 100644 index 0000000..2f26593 --- /dev/null +++ b/meta.json @@ -0,0 +1,13 @@ +{ + "module_id": "viam:python-appimage-example", + "visibility": "public", + "url": "https://github.com/viamrobotics/python-appimage-example", + "description": "Example of deploying a Python module with AppImageBuilder.", + "models": [ + { + "api": "rdk:component:camera", + "model": "viam:camera:oak-d" + } + ], + "entrypoint": "run.sh" +} \ No newline at end of file diff --git a/packaging/AppImageBuilder.yml b/packaging/AppImageBuilder.yml new file mode 100644 index 0000000..97b298d --- /dev/null +++ b/packaging/AppImageBuilder.yml @@ -0,0 +1,32 @@ +version: 1 +script: + - python3 -m pip install -r requirements.txt # install dependency packages as modules in root (modules in this context means Python modules) + - mkdir -p AppDir/usr/lib/python3.10 && cp -r /usr/local/lib/python3.10/site-packages AppDir/usr/lib/python3.10 # cp from root into appdir; site-packages contains all our modules including dependencies + - cp -r src AppDir/usr/lib/python3.10/site-packages # add source code dir to site-packages so the OAK-D module can be discovered in PYTHONPATH and be run as a Python module + - mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps/ && cp viam-server.png AppDir/usr/share/icons/hicolor/256x256/apps/ # icon is required + +AppDir: + path: ./AppDir + app_info: + id: com.example-org.python-appimage-example # replace with your own id and name + name: python-appimage-example # affects the outputted .AppImage name + version: "" # affects the outputted .AppImage name + icon: viam-server + exec: usr/bin/python3 + exec_args: "-m src.main $@" + apt: + arch: arm64 + allow_unauthenticated: true + sources: + - sourceline: 'deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports jammy main restricted universe multiverse' + include: + - python3.10 + - python3-pkg-resources # necessary for package metadata and paths + + runtime: + env: + PYTHONHOME: '${APPDIR}/usr' # https://docs.python.org/3/using/cmdline.html#environment-variables + PYTHONPATH: '${APPDIR}/usr/lib/python3.10/site-packages' + +AppImage: + arch: aarch64 # affects the outputted .AppImage name diff --git a/packaging/Dockerfile b/packaging/Dockerfile new file mode 100755 index 0000000..da16a26 --- /dev/null +++ b/packaging/Dockerfile @@ -0,0 +1,21 @@ +FROM python:3.10-bookworm + +RUN apt-get update && \ + apt-get install -y \ + build-essential \ + python3-pip \ + libgtk-3-bin \ + squashfs-tools \ + libglib2.0-bin \ + fakeroot + +# Credit to perrito666 for the ubuntu fix +RUN python3 -m pip install git+https://github.com/hexbabe/appimage-builder.git + +WORKDIR /app + +COPY . /app + +COPY packaging/. /app + +CMD ["appimage-builder", "--recipe", "packaging/AppImageBuilder.yml"] diff --git a/packaging/viam-server.png b/packaging/viam-server.png new file mode 100644 index 0000000000000000000000000000000000000000..d502e3063917fbc1b09b76d7fcd7313e57d42367 GIT binary patch literal 3811 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 Z9@&IPj3v>I{wgm&e*l$%`3wL6 literal 0 HcmV?d00001 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1cfcabf --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +viam-sdk \ No newline at end of file diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..b51acbc --- /dev/null +++ b/run.sh @@ -0,0 +1,43 @@ +#!/bin/sh +cd "$(dirname "$0")" +LOG_PREFIX="[Viam REPLACE_WITH_MODULE_NAME module setup]" + +echo "$LOG_PREFIX Starting the module." + +os=$(uname -s) +arch=$(uname -m) +appimage_path="./python-appimage-example--aarch64.AppImage" +# Run appimage if Linux aarch64 +if [ "$os" = "Linux" ] && [ "$arch" = "aarch64" ] && [ -f "$appimage_path" ]; then + echo "$LOG_PREFIX Detected system Linux AArch64 and appimage. Attempting to start appimage." + chmod +x "$appimage_path" + exec "$appimage_path" "$@" +else + echo "$LOG_PREFIX No usable appimage was found." +fi + +# Else, try running with a virtual environment and source +VENV_NAME="viam-module-env" +PYTHON="$VENV_NAME/bin/python" + +echo "$LOG_PREFIX Running the module using virtual environment. This requires Python >=3.8.1, pip3, and venv to be installed." + +if ! python3 -m venv "$VENV_NAME" >/dev/null 2>&1; then + echo "$LOG_PREFIX Error: failed to create venv. Please use your system package manager to install python3-venv." >&2 + exit 1 +else + echo "$LOG_PREFIX Created/found venv." +fi + +# Remove -U if viam-sdk should not be upgraded whenever possible +# -qq suppresses extraneous output from pip +echo "$LOG_PREFIX Installing/upgrading Python packages." +if ! "$PYTHON" -m pip install -r requirements.txt -Uqq; then + echo "$LOG_PREFIX Error: pip failed to install requirements.txt. Please use your system package manager to install python3-pip." >&2 + exit 1 +fi + +# Be sure to use `exec` so that termination signals reach the python process, +# or handle forwarding termination signals manually +echo "$LOG_PREFIX Starting module." +exec "$PYTHON" -m src.main "$@" diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..c28a2e9 --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,14 @@ +""" +This file registers the model with the Python SDK. +""" + +from viam.components.camera import Camera +from viam.resource.registry import Registry, ResourceCreatorRegistration + +from src.module import MyModule + +Registry.register_resource_creator( + Camera.SUBTYPE, + MyModule.MODEL, + ResourceCreatorRegistration(MyModule.new, MyModule.validate), +) diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..171ab13 --- /dev/null +++ b/src/main.py @@ -0,0 +1,22 @@ +import asyncio + +from viam.components.camera import Camera +from viam.logging import getLogger +from viam.module.module import Module +from src.module import MyModule + + +LOGGER = getLogger(__name__) + + +async def main(): + """This function creates and starts a new module, after adding all desired resources. + Resources must be pre-registered. For an example, see the `__init__.py` file. + """ + module = Module.from_args() + module.add_model_from_registry(Camera.SUBTYPE, MyModule.MODEL) + await module.start() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/module.py b/src/module.py new file mode 100644 index 0000000..a2253b5 --- /dev/null +++ b/src/module.py @@ -0,0 +1,122 @@ +from typing import Any, ClassVar, Dict, List, Mapping, NamedTuple, Optional, Tuple, Union +from typing_extensions import Self + +from PIL.Image import Image + +from viam.components.camera import Camera, DistortionParameters, IntrinsicParameters, RawImage +from viam.logging import getLogger +from viam.media.video import NamedImage +from viam.module.types import Reconfigurable +from viam.proto.app.robot import ComponentConfig +from viam.proto.common import ResourceName, ResponseMetadata +from viam.resource.base import ResourceBase +from viam.resource.types import Model, ModelFamily + + +LOGGER = getLogger(__name__) + + +class MyModule(Camera, Reconfigurable): # use a better name than this + """ + Camera represents any physical hardware that can capture frames. + """ + class Properties(NamedTuple): + """The camera's supported features and settings""" + supports_pcd: bool + """Whether the camera has a valid implementation of ``get_point_cloud``""" + intrinsic_parameters: IntrinsicParameters + """The properties of the camera""" + distortion_parameters: DistortionParameters + """The distortion parameters of the camera""" + + + MODEL: ClassVar[Model] = Model(ModelFamily("viam", "camera"), "oak-d") # make sure this matches the model in meta.json + + # create any class parameters here, 'some_pin' is used as an example (change/add as needed) + some_pin: int + + # Constructor + @classmethod + def new(cls, config: ComponentConfig, dependencies: Mapping[ResourceName, ResourceBase]) -> Self: + my_class = cls(config.name) + my_class.reconfigure(config, dependencies) + return my_class + + # Validates JSON Configuration + @classmethod + def validate(cls, config: ComponentConfig): + # here we validate config, the following is just an example and should be updated as needed + some_pin = config.attributes.fields["some_pin"].number_value + if some_pin == "": + raise Exception("A some_pin must be defined") + return + + # Handles attribute reconfiguration + def reconfigure(self, config: ComponentConfig, dependencies: Mapping[ResourceName, ResourceBase]): + # here we initialize the resource instance, the following is just an example and should be updated as needed + self.some_pin = int(config.attributes.fields["some_pin"].number_value) + LOGGER.info("Configuration success!") + return + + """ Implement the methods the Viam RDK defines for the Camera API (rdk:component:camera) """ + async def get_image( + self, mime_type: str = "", *, extra: Optional[Dict[str, Any]] = None, timeout: Optional[float] = None, **kwargs + ) -> Union[Image, RawImage]: + """Get the next image from the camera as an Image or RawImage. + Be sure to close the image when finished. + NOTE: If the mime type is ``image/vnd.viam.dep`` you can use :func:`viam.media.video.RawImage.bytes_to_depth_array` + to convert the data to a standard representation. + Args: + mime_type (str): The desired mime type of the image. This does not guarantee output type + Returns: + Image | RawImage: The frame + """ + ... + + + async def get_images(self, *, timeout: Optional[float] = None, **kwargs) -> Tuple[List[NamedImage], ResponseMetadata]: + """Get simultaneous images from different imagers, along with associated metadata. + This should not be used for getting a time series of images from the same imager. + Returns: + Tuple[List[NamedImage], ResponseMetadata]: + - List[NamedImage]: + The list of images returned from the camera system. + - ResponseMetadata: + The metadata associated with this response + """ + ... + + + async def get_point_cloud( + self, *, extra: Optional[Dict[str, Any]] = None, timeout: Optional[float] = None, **kwargs + ) -> Tuple[bytes, str]: + """ + Get the next point cloud from the camera. This will be + returned as bytes with a mimetype describing + the structure of the data. The consumer of this call + should encode the bytes into the formatted suggested + by the mimetype. + To deserialize the returned information into a numpy array, use the Open3D library. + :: + import numpy as np + import open3d as o3d + data, _ = await camera.get_point_cloud() + # write the point cloud into a temporary file + with open("/tmp/pointcloud_data.pcd", "wb") as f: + f.write(data) + pcd = o3d.io.read_point_cloud("/tmp/pointcloud_data.pcd") + points = np.asarray(pcd.points) + Returns: + bytes: The pointcloud data. + str: The mimetype of the pointcloud (e.g. PCD). + """ + ... + + + async def get_properties(self, *, timeout: Optional[float] = None, **kwargs) -> Properties: + """ + Get the camera intrinsic parameters and camera distortion parameters + Returns: + Properties: The properties of the camera + """ + ... \ No newline at end of file