Metadata-Version: 2.4
Name: rpi-camera-ensemble
Version: 0.4.3
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" <L.B.Rollik@protonmail.com>
License: BSD 3-Clause License
        
        Copyright (c) 2025, Lars B. Rollik
        All rights reserved.
        
        Redistribution and use in source and binary forms, with or without
        modification, are permitted provided that the following conditions are met:
        
        1. Redistributions of source code must retain the above copyright notice, this
           list of conditions and the following disclaimer.
        
        2. Redistributions in binary form must reproduce the above copyright notice,
           this list of conditions and the following disclaimer in the documentation
           and/or other materials provided with the distribution.
        
        3. Neither the name of the copyright holder nor the names of its
           contributors may be used to endorse or promote products derived from
           this software without specific prior written permission.
        
        THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
        AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
        IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
        DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
        FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
        DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
        SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
        CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
        OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
        OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
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: picamera2; 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; 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: 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'
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; 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).
