Metadata-Version: 2.4
Name: rpi-camera-ensemble
Version: 0.5.1
Summary: RPi Camera Ensemble: Central orchestration of video acquisition across multiple Raspberry Pi cameras.
Project-URL: Homepage, https://github.com/MurineShiftWork/rpi-camera-ensemble
Project-URL: Documentation, https://MurineShiftWork.github.io/rpi-camera-ensemble/
Project-URL: Issue Tracker, https://github.com/MurineShiftWork/rpi-camera-ensemble/issues
Author-email: "Lars B. Rollik" <lars@rollik.me>
License: Copyright (c) 2025-present Lars B. Rollik. All rights reserved.
        
        Permission is granted, free of charge, to use, copy, modify, and distribute
        this software and associated documentation files (the "Software") for
        non-commercial research or academic purposes only, subject to the following
        conditions:
        
        1. This copyright notice and permission notice must be included in all copies
           or substantial portions of the Software.
        
        2. Any publication, presentation, or product that uses or builds upon the
           Software must give clear attribution to the original work and its authors.
        
        3. Commercial use is prohibited without a separate written commercial licence
           agreement with the copyright holder. Commercial use means incorporation of
           the Software into anything for which fees or other compensation are charged
           or received, including commercial products and commercial services.
        
        4. No patent licence, express or implied, is granted under these terms. Any
           use that would require a patent licence from the copyright holder requires
           a separate written agreement.
        
        5. Redistribution, in source or binary form, is permitted only for
           non-commercial purposes and must retain this notice unmodified.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM,
        DAMAGES, OR OTHER LIABILITY ARISING FROM, OUT OF, OR IN CONNECTION WITH THE
        SOFTWARE OR THE USE OR DEALINGS IN THE SOFTWARE.
        
        For commercial or patent licensing enquiries contact: lars@rollik.me
License-File: LICENSE
Classifier: Development Status :: 4 - Beta
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Multimedia :: Video
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.10
Requires-Dist: numpy
Requires-Dist: pydantic>=2.0
Requires-Dist: pyyaml
Requires-Dist: typing-extensions; python_version < '3.11'
Provides-Extra: agent
Requires-Dist: fastapi; extra == 'agent'
Requires-Dist: uvicorn[standard]; extra == 'agent'
Provides-Extra: all
Requires-Dist: aiohttp; extra == 'all'
Requires-Dist: fastapi; extra == 'all'
Requires-Dist: lgpio; extra == 'all'
Requires-Dist: picamera2; (platform_machine == 'aarch64') and extra == 'all'
Requires-Dist: uvicorn[standard]; extra == 'all'
Provides-Extra: conductor
Requires-Dist: aiohttp; extra == 'conductor'
Provides-Extra: dev
Requires-Dist: commitizen; extra == 'dev'
Requires-Dist: httpx; extra == 'dev'
Requires-Dist: mypy; extra == 'dev'
Requires-Dist: pre-commit; extra == 'dev'
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: types-pyyaml; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material; extra == 'docs'
Requires-Dist: mkdocstrings[python]; extra == 'docs'
Provides-Extra: lgpio
Requires-Dist: lgpio; extra == 'lgpio'
Provides-Extra: pigpio
Requires-Dist: pigpio; extra == 'pigpio'
Provides-Extra: rpi
Requires-Dist: fastapi; extra == 'rpi'
Requires-Dist: lgpio; extra == 'rpi'
Requires-Dist: picamera2; (platform_machine == 'aarch64') and extra == 'rpi'
Requires-Dist: uvicorn[standard]; extra == 'rpi'
Provides-Extra: rpigpio
Requires-Dist: rpi-gpio; extra == 'rpigpio'
Description-Content-Type: text/markdown

[//]: # (Badges)

[![CI](https://img.shields.io/github/actions/workflow/status/MurineShiftWork/rpi-camera-ensemble/ci.yml?branch=main&label=CI)](https://github.com/MurineShiftWork/rpi-camera-ensemble/actions)
[![Docs](https://img.shields.io/github/actions/workflow/status/MurineShiftWork/rpi-camera-ensemble/docs.yml?branch=main&label=docs)](https://MurineShiftWork.github.io/rpi-camera-ensemble/)
[![PyPI](https://img.shields.io/pypi/v/rpi-camera-ensemble)](https://pypi.org/project/rpi-camera-ensemble/)
[![License](https://img.shields.io/github/license/MurineShiftWork/rpi-camera-ensemble)](LICENSE)

# RPi Camera Ensemble

Central orchestration of synchronized video acquisition across multiple Raspberry Pi cameras.

Runs a **Conductor** on the acquisition machine that controls one or more **Agents**: each Agent is a FastAPI server running on an individual Raspberry Pi, managing local camera acquisition and GPIO-based TTL synchronization.

---

## Features

- **Multi-camera orchestration**: start/stop all cameras with a single call; UUID-guarded sessions prevent accidental stops
- **TTL synchronization**: per-frame GPIO pulses (emitter) and edge-detection timestamps (receiver) via `lgpio` (RPi 4 + RPi 5) or legacy `pigpio`
- **PiCamera2 backend**: uses the modern `libcamera` stack; preview and recording states
- **REST API on each agent**: `GET /status`, `POST /agent/acquisition/init`, `POST /agent/acquisition/record`, `POST /agent/acquisition/stop`
- **Heartbeat monitoring**: background health checks with configurable interval
- **Disk space check**: pre-acquisition guard based on estimated bitrate and free space
- **CLI**: `rce-agent run/config` entrypoints for systemd or docker-compose deployment

---

## Architecture

```
Conductor (acquisition machine)
    │  HTTP via aiohttp
    ├── AgentClient → Agent (RPi cam01)  →  Acquisition → PiCamera2
    ├── AgentClient → Agent (RPi cam02)  →  Acquisition → PiCamera2
    └── AgentClient → Agent (RPi cam03)  →  Acquisition → PiCamera2
                                                    │
                                              TTL out pin (lgpio)
                                              TTL in  pin (lgpio)
```

---

## Requirements

- Python ≥ 3.10
- **On each Raspberry Pi**: `picamera2` (system package), `lgpio` (`pip install lgpio`), `libgpiod2` (system package)
- **On the acquisition machine (Conductor)**: `aiohttp`

---

## Installation

```bash
# Acquisition machine (Conductor only)
pip install "rpi-camera-ensemble[conductor]"

# On each Raspberry Pi (Agent + TTL sync)
pip install "rpi-camera-ensemble[agent,lgpio]"
```

Install from source:

```bash
git clone https://github.com/MurineShiftWork/rpi-camera-ensemble.git
cd rpi-camera-ensemble
pip install -e ".[agent,lgpio,dev]"
```

---

## Quick start

### Agent (on each Raspberry Pi)

Create a config file (`agent.yaml`):

```yaml
instance_name: cam01
api_host: 0.0.0.0
api_port: 8000
data_dir: /data/recordings
log_dir: /data/logs
```

Start the agent:

```bash
rce-agent run --config agent.yaml
```

Generate a template config:

```bash
rce-agent config --to-template agent.template.yaml
```

### Conductor (on the acquisition machine)

```python
from rpi_camera_ensemble.conductor.conductor import Conductor
from rpi_camera_ensemble.config.acquisition import EnsembleAcquisitionConfig
from rpi_camera_ensemble.config.conductor import ConductorConfig

ensemble_cfg = EnsembleAcquisitionConfig.from_yaml("ensemble.yaml")
conductor_cfg = ConductorConfig(log_dir="/data/logs/conductor")

with Conductor(config=conductor_cfg, ensemble_config=ensemble_cfg) as conductor:
    conductor.setup_agents()
    conductor.initialize_acquisition(
        acquisition_path="/data/recordings/experiment_001",
        acquisition_name="experiment_001",
    )
    conductor.start_preview()
    conductor.start_recording()
    # ... recording ...
    conductor.stop_acquisition()
```

Ensemble config (`ensemble.yaml`):

```yaml
agents:
  cam01:
    api_url: http://192.168.1.10:8000
  cam02:
    api_url: http://192.168.1.11:8000
```

---

## TTL synchronization

TTL is configured per agent inside `ensemble.yaml` under `camera_config.ttl`. Default backend is `lgpio` (recommended):

```yaml
agents:
  cam01:
    api_url: http://192.168.1.10:8000
    camera_config:
      ttl:
        library: lgpio
        gpiochip: 0       # RPi 4: 0, RPi 5: 4
        out_pin: 25       # emits 1 ms pulse on each encoded frame
        in_pin: 23        # records edge timestamps from external trigger
        out_duration_us: 1000
```

`lgpio` requires `libgpiod2` on the Pi:

```bash
sudo apt install libgpiod2
pip install lgpio
```

> **RPi 5 note**: set `gpiochip: 4` (RP1 southbridge). RPi 4 uses `gpiochip: 0`.

---

## Output structure

Each agent writes files named `{basename}.rce.{instance}.{timestamp}.{type}.{ext}`:

```
/data/recordings/experiment_001/
├── session.rce.cam01.20260101_120000_000000.video.h264
├── session.rce.cam01.20260101_120000_000000.ttl_out.npz
├── session.rce.cam01.20260101_120000_000000.ttl_in.npz
├── session.rce.cam02.20260101_120000_000000.video.h264
├── session.rce.cam02.20260101_120000_000000.ttl_out.npz
└── session.rce.cam02.20260101_120000_000000.ttl_in.npz
```

Load and validate a session from the acquisition machine:

```python
from rpi_camera_ensemble.io import RCESession, validate_session

session = RCESession.from_directory("/data/recordings/experiment_001/")
qc = validate_session(session)
qc.print_report()
```

---

## Deployment

See [`deploy/`](https://github.com/MurineShiftWork/rpi-camera-ensemble/tree/main/deploy) for Ansible playbooks to provision Raspberry Pis (apt packages, Python venv, systemd service).

---

## Development

```bash
uv sync --extra dev
uv run pre-commit install
uv run pytest
```

---

## License

Copyright (c) 2025 Lars B. Rollik. All rights reserved. See [LICENSE](LICENSE).
