Add boilerplate derived from OAK-D module

This commit is contained in:
hexbabe
2023-11-30 17:15:08 -05:00
parent 1d780570f4
commit f3f63a7633
12 changed files with 310 additions and 2 deletions

13
.gitignore vendored Normal file
View File

@@ -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__

27
Makefile Normal file
View File

@@ -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

View File

@@ -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

13
meta.json Normal file
View File

@@ -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"
}

View File

@@ -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

21
packaging/Dockerfile Executable file
View File

@@ -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"]

BIN
packaging/viam-server.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
viam-sdk

43
run.sh Executable file
View File

@@ -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 "$@"

14
src/__init__.py Normal file
View File

@@ -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),
)

22
src/main.py Normal file
View File

@@ -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())

122
src/module.py Normal file
View File

@@ -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
"""
...