mirror of
https://github.com/hexbabe/python-appimage-module.git
synced 2025-07-20 20:42:05 +02:00
Add boilerplate derived from OAK-D module
This commit is contained in:
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal 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
27
Makefile
Normal 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
|
@@ -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
13
meta.json
Normal 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"
|
||||
}
|
32
packaging/AppImageBuilder.yml
Normal file
32
packaging/AppImageBuilder.yml
Normal 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
21
packaging/Dockerfile
Executable 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
BIN
packaging/viam-server.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@@ -0,0 +1 @@
|
||||
viam-sdk
|
43
run.sh
Executable file
43
run.sh
Executable 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
14
src/__init__.py
Normal 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
22
src/main.py
Normal 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
122
src/module.py
Normal 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
|
||||
"""
|
||||
...
|
Reference in New Issue
Block a user