Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
### CameraNodeConfig

# NODE_STATUS_UPDATE_INTERVAL=2.0
# NODE_STATE_UPDATE_INTERVAL=2.0
# NODE_NODE_NAME=null
# NODE_NODE_ID=null
# NODE_NODE_TYPE=null
# NODE_MODULE_NAME=null
# NODE_MODULE_VERSION=null
# NODE_URL="http://127.0.0.1:2000/"
# NODE_UVICORN_KWARGS={"limit_concurrency":10}
# NODE_ENABLE_RATE_LIMITING=true
# NODE_RATE_LIMIT_REQUESTS=100
# NODE_RATE_LIMIT_WINDOW=60
# NODE_RATE_LIMIT_SHORT_REQUESTS=50
# NODE_RATE_LIMIT_SHORT_WINDOW=1
# NODE_RATE_LIMIT_CLEANUP_INTERVAL=300
# NODE_CAMERA_ADDRESS=0
9 changes: 9 additions & 0 deletions .envrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash

# Automatically sets up your devbox environment whenever you cd into this
# directory via our direnv integration:

eval "$(devbox generate direnv --print-envrc)"

# check out https://www.jetify.com/docs/devbox/ide_configuration/direnv/
# for more details
14 changes: 9 additions & 5 deletions .github/workflows/pre-commit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,15 @@ jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
name: Checkout code
- uses: actions/setup-python@v4
name: Setup Python
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v7
- name: Setup PDM
uses: pdm-project/setup-pdm@v4
with:
python-version: 3.9
python-version: "3.10"
- name: Install dependencies
env:
PDM_USE_UV: "True"
run: pdm install
- uses: pre-commit/action@v3.0.1
name: Run Pre-Commit Checks
7 changes: 7 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,10 @@ repos:
rev: v1.0.0
hooks:
- id: check-json5
- repo: https://github.com/jag-k/pydantic-settings-export
rev: v1.0.3
hooks:
- id: pydantic-settings-export
entry: pdm run pydantic-settings-export
language: system
files: ^src/camera_rest_node\.py$
63 changes: 63 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# AGENTS.md/CLAUDE.md

This file provides guidance to AI coding agents. Note that AGENTS.md and CLAUDE.md are symlinked.

## Overview

`camera_module` is a standalone MADSci node module that exposes a USB/video camera as a REST service. It uses `opencv-python-headless` for capture and `pyzbar` for barcode decoding. The node registers with MADSci, manages a resource template ("capture deck slot"), and exposes two actions: `take_picture` and `read_barcode`.

## Source Layout

```
src/
camera_rest_node.py # CameraNode (RestNode subclass) — MADSci integration, actions, state/startup handlers
camera_interface.py # CameraInterface — pure OpenCV/pyzbar logic, no MADSci dependency
```

All source files are directly in `src/` (flat layout, not a package). The entry point is `python -m camera_rest_node`.

## Key Architecture Points

- **`CameraNode`** inherits from `madsci.node_module.rest_node_module.RestNode`. It owns `startup_handler` (called on init/re-init) and `state_handler` (polled periodically to update node status).
- **`CameraInterface`** holds a `threading.Lock` to serialize camera access. The camera is opened/closed per operation (not kept open). `pyzbar` import is wrapped in a try/except — if the native `libzbar0` library is missing, barcode reading raises `ImportError` at call time rather than at import.
- **Resource template**: On startup, the node registers a `Slot` resource template (`camera_capture_deck_slot`) via `resource_client`, then instantiates it per node name. This is required for MADSci resource tracking.
- **Focus control**: `_adjust_focus_settings_unlocked` must only be called while `camera_lock` is held. After a focus change it discards 30 frames to stabilize; otherwise 5 frames for startup.
- **`camera_address`**: Accepts `int` (device index, Windows) or `str` (device path, Linux/Mac, e.g. `/dev/video0`). A field validator on `CameraNodeConfig` coerces numeric strings to int.

## Development Commands

Uses `just` + `pdm` (with `uv` as backend). Devbox provides a reproducible shell (Python 3.12, pdm, uv, ruff, just).

```bash
just init # pdm install -G:all + pre-commit install
just checks # ruff lint + format via pre-commit (runs twice to verify fixes applied)
just dcb # docker compose build
just test # pytest (no automated tests currently — only a notebook in tests/)
```

Direct run (after `pip install .` or `just init`):
```bash
python -m camera_rest_node --host 127.0.0.1 --port 2000
```

Node configuration is driven by `settings.yaml` (MADSci settings discovery walks up from the working directory).

## Linting

Ruff with a broad rule set (see `ruff.toml`). Notable ignores: `E501` (line length), `ANN401` (Any types), `COM812`. Tests may use `S101` (assert). Line length is 88, double quotes, spaces.

## Docker

```bash
docker compose up # runs using ghcr.io/ad-sdl/camera_module
docker compose build # or: just dcb
```

The container runs `privileged: true` with `network_mode: host` so it can access `/dev/video*`. User in the container is added to the `video` group. `USER_ID`/`GROUP_ID` build args control file ownership. The `settings.yaml` and `.madsci/` directory are volume-mounted.

Base image: `ghcr.io/ad-sdl/madsci`. Package is installed with `uv pip install -e` inside the MADSci venv (`$MADSCI_VENV`).

## Platform Notes

- On Linux/Mac, install `libzbar0` (apt) or `zbar` (brew) for barcode support; the Docker image installs this automatically.
- Camera address on Linux is typically `/dev/video0` (string); on Windows use an integer index.
1 change: 1 addition & 0 deletions CLAUDE.md
6 changes: 5 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ LABEL org.opencontainers.image.licenses=MIT
# Module specific logic goes below here #
#########################################

ARG USER_ID=9999
ARG GROUP_ID=9999

RUN apt-get update && \
apt-get install -y libzbar0 && \
rm -rf /var/lib/apt/lists/* && rm -rf /var/cache/apt/archives/*
Expand All @@ -19,7 +22,8 @@ COPY ./README.md camera_module/README.md
COPY ./pyproject.toml camera_module/pyproject.toml

RUN --mount=type=cache,target=/root/.cache \
pip install -e ./camera_module
uv pip install --python ${MADSCI_VENV}/bin/python -e /home/madsci/camera_module && \
chown -R ${USER_ID}:${GROUP_ID} /home/madsci/camera_module

CMD ["python", "-m", "camera_rest_node"]

Expand Down
87 changes: 70 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,83 @@
# camera_module

Provides a simple MADSci node for integrating web cameras to capture images.
A MADSci node module that exposes a USB/video camera as a REST service. Supports image capture and barcode reading via [OpenCV](https://opencv.org/) and [pyzbar](https://pypi.org/project/pyzbar/).

See `definitions/camera_node_template.node.info.yaml` for details on the capabilities of this node, and `definitions/camera_node_template.node.yaml` as a template for your own Camera Node definition file.
## Configuration

## Installation and Usage
All configuration is done via environment variables (prefixed `NODE_`) or a `settings.yaml` file. See [docs/Configuration.md](docs/Configuration.md) for the full reference and [`.env.example`](.env.example) for a commented template.

### Python
The most common settings to override:

| Variable | Default | Purpose |
|---|---|---|
| `NODE_CAMERA_ADDRESS` | `0` | Camera index (int) or device path (e.g. `/dev/video0`) |
| `NODE_URL` | `http://127.0.0.1:2000/` | URL the node binds to and advertises |
| `NODE_NODE_NAME` | _(class name)_ | Human-readable name registered with MADSci |

## Getting Started

### Option 1 — Devbox (recommended)

[Devbox](https://www.jetify.com/devbox) provides a reproducible shell with Python 3.12, pdm, uv, ruff, and just pre-installed.

```bash
devbox shell # enter the reproducible environment
just init # install dependencies + pre-commit hooks
```

### Option 2 — PDM

```bash
pdm install -G:all # install all dependencies including dev extras
```

### Option 3 — pip / uv (minimal)

```bash
# Create a virtual environment named .venv
python -m venv .venv
# Activate the virtual environment on Linux or macOS
source .venv/bin/activate
# Alternatively, activate the virtual environment on Windows
# .venv\Scripts\activate
# Install the module and dependencies in the venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install .
# Run the environment
python -m camera_rest_node --host 127.0.0.1 --port 2000
```

Note: on Mac or Linux, you'll need to install the `zbar` dependencies manually (see https://pypi.org/project/pyzbar/ for details).
> **Linux/Mac:** `pyzbar` requires the native `zbar` library. Install it with:
> - Debian/Ubuntu: `sudo apt install libzbar0`
> - macOS: `brew install zbar`
>
> Without it, `take_picture` still works; `read_barcode` will raise `ImportError`.

## Running the Node

```bash
python -m camera_rest_node
```

### Docker
The host, port, and all other settings can be set via environment variables or `settings.yaml` (discovered by walking up the directory tree). See [docs/Configuration.md](docs/Configuration.md).

## Development

```bash
just checks # ruff lint + format (via pre-commit, auto-fixes then re-checks)
just test # pytest
just dcb # docker compose build
```

## Docker

A pre-built image is available at `ghcr.io/ad-sdl/camera_module`. To run with Docker Compose:

```bash
# Copy and edit the example env file
cp .env.example .env

docker compose up
```

The container runs with `privileged: true` and `network_mode: host` so it can access `/dev/video*`. The `madsci` user inside the container is added to the `video` group automatically.

Set `USER_ID` and `GROUP_ID` to match your host user to avoid file permission issues with volume-mounted paths:

```bash
USER_ID=$(id -u) GROUP_ID=$(id -g) docker compose up
```

- We provide a `Dockerfile` and example docker compose file (`compose.yaml`) to run this node dockerized.
- There is also a pre-built image available as `ghcr.io/ad-sdl/camera_module`.
- You can control the container user's id and group id by setting the `USER_ID` and `GROUP_ID`
The `settings.yaml` and `.madsci/` directory are bind-mounted from the project root into the container.
8 changes: 6 additions & 2 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ services:
camera_module:
container_name: camera_module
image: ghcr.io/ad-sdl/camera_module
env_file:
- path: .env
required: false
environment:
- USER_ID=${USER_ID:-1000}
- GROUP_ID=${GROUP_ID:-1000}
Expand All @@ -12,8 +15,9 @@ services:
tags:
- ghcr.io/ad-sdl/camera_module:latest
- ghcr.io/ad-sdl/camera_module:dev
command: python camera_module/src/camera_rest_node.py --node_definition definitions/camera_node_template.node.yaml
command: python camera_module/src/camera_rest_node.py
volumes:
- ./definitions:/home/madsci/definitions
- ./settings.yaml:/home/madsci/settings.yaml
- ./.madsci:/home/madsci/.madsci
privileged: true
network_mode: host
Loading
Loading