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
2 changes: 1 addition & 1 deletion docs/readthedocs/cad.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CAD Files
## [SAVIOUR 3D Printed Cases v1](https://grabcad.com/library/saviour-pi-5-cases-v1-0-0-2)

[![SAVIOUR 3D printed cases v1](images/saviour_cam_cad_sep_2025.jpg)](https://grabcad.com/library/saviour-pi-5-cases-v1-0-0-2)
[![SAVIOUR 3D printed cases v1](images/saviour_cam_cad_sep_2025.webp)](https://grabcad.com/library/saviour-pi-5-cases-v1-0-0-2)

These were the earliest deployed versions of the SAVIOUR cases. They come in two halves and screw together using standoffs to get the spacing right from the Pi assembly. They have since been replaced with snap-fit cases that do not require additional standoffs, but they are still functional and very robust.
17 changes: 16 additions & 1 deletion docs/readthedocs/faqs.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,19 @@ Many SAVIOUR users also capture electrophysiology data in their experiments. A T
Primarily behavioural neuroscience labs, though the same approach suits any experiment needing several sensors recording in sync.

***Why was SAVIOUR made?***<br>
SAVIOUR was developed in response to many one-off data gathering rigs being created that did similar things in a messy, expensive, and hard to reproduce way.
SAVIOUR was developed in response to many one-off data gathering rigs being created that did similar things in a messy, expensive, and hard to reproduce way.

***How do I know if PTP sync is good enough to start recording?***<br>
Click "Check Ready" on the Recording page - it checks ptp4l and phc2sys offset on every module against a threshold (50 µs by default) and reports which modules aren't ready. The System page also shows live per-module PTP offset if you want to watch it settle after a reboot.

***A camera was just rebooted - can I record straight away?***<br>
Give it 5–10 minutes first. phc2sys needs that long to converge its frequency estimate for that crystal; recording immediately after a reboot can leave a larger-than-usual (but still bounded) inter-camera phase offset.

***What happens to a recording if the NAS/export share goes down?***<br>
Files are staged locally on the module and export is retried with backoff once the share comes back - recordings aren't lost, but they won't appear on the share until the export queue catches up.

***Can I change which rig UI (basic / loom / apa / habitat / acoustic startle) a controller shows?***<br>
The frontend variant is selected by which App is imported in `src/controller/frontend/src/main.jsx`. Switching rigs means editing that import and rebuilding the frontend - it isn't a runtime setting yet.

***Do all modules need to be the same type?***<br>
No - a system is any mix of camera, microphone, TTL, RFID and rig-specific module types (loom camera, APA camera/arduino) all reporting to one controller. Add or remove modules freely; the controller discovers them automatically over mDNS.
52 changes: 51 additions & 1 deletion docs/readthedocs/getting_started.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,53 @@
# Getting Started

Coming soon!
## Before you start

This guide assumes:

- You have the hardware together: a PoE+ switch, one controller, and one or more modules, all on the same network. See [Hardware](hardware.md) for what to buy and how to size your switch.
- Every device already has Raspberry Pi OS flashed to its SD card (or NVMe, for the controller) and boots normally - a plain [Raspberry Pi Imager](https://www.raspberrypi.com/software/) install, nothing SAVIOUR-specific needed at that stage.
- SAVIOUR itself is installed on each device. If it isn't yet, see below.

### Installing SAVIOUR

On a fresh Raspberry Pi OS install, run:

```
curl -fsSL https://raw.githubusercontent.com/Kind-Wyllie-lab/saviour/main/install.sh | bash
```

This clones the repo to `/usr/local/src/saviour` and runs `setup.sh`, which installs everything SAVIOUR needs (PTP, ffmpeg, Picamera2, Samba, etc.). It doesn't assign a role by itself - that's the next step, below.

Doing this one device at a time works fine but is slow for a big rig - once you've got one device fully configured, it's usually faster to clone its SD card/NVMe image onto the rest instead (`scripts/multiclone.sh` in the repo).

## Assigning a device role

1. Boot the device on the PoE network (SAVIOUR should already be installed - see above).
2. Run `sudo saviour-config` on the device.
3. Choose Controller (one per system) or Module, then pick the module type (camera, microphone, TTL, RFID, ...).
4. The device reboots into its assigned role and appears automatically once discovered.

## Connecting modules to the controller

1. Power on the controller first - it acts as the PTP grandmaster and service discovery hub.
2. Power on modules; they register over mDNS and appear on the Dashboard within a few seconds.
3. Check the System page to confirm every module shows a recent heartbeat and a locked PTP offset before recording.

## Running a recording session

1. Configure each module on the Settings page (resolution, sample rate, etc.) before starting.
2. On the Recording page, click "Check Ready" to confirm PTP sync is within threshold on every module.
3. Start the session, and stop it (or let a scheduled window end it) once you're done.
4. Recordings export automatically to the controller's share once each module finishes.

## Exporting and retrieving data

1. Recordings land on the controller's Samba share, organised by session name and date.
2. Connect to the share from a lab workstation to copy files off, or point analysis tools at it directly.
3. Use `tools/analyse_framesync.py` and `tools/make_aligned_video.py` to check multi-camera timing and build aligned review videos.

## Troubleshooting a module

1. A module marked offline usually means its heartbeat timed out - check power, PoE link and network cabling first.
2. The System page shows per-module CPU, disk, temperature and PTP offset - a drifting PTP offset points to a clock sync problem, not a recording bug.
3. Reboot or shut down an individual module from its actions menu on the System page if it needs a clean restart.
55 changes: 54 additions & 1 deletion docs/readthedocs/how_it_works.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,56 @@
# How SAVIOUR Works

Coming soon!
## Overview

A SAVIOUR system is one **controller** talking to any number of **modules** over a PoE LAN. Every device is a Raspberry Pi 5 running the same codebase, with `/etc/saviour/config` telling it which of the two roles to boot into.

The controller is the brains of the operation: it's the PTP grandmaster (so every device's clock is disciplined to the same reference), it runs mDNS service discovery to find modules as they come online, and it hosts the web GUI you actually interact with (Flask + Socket.IO, port 5000 by default). Modules are dumber by design - each one just owns a single sensor or piece of equipment (camera, microphone, TTL I/O, etc.) and does what the controller tells it.

Commands flow controller -> module over a ZeroMQ ROUTER/DEALER socket pair (port 5555): the controller binds a ROUTER, each module connects a DEALER using its own module ID as identity, and sends a "hello" frame to register. Status and heartbeats flow the other way on a separate PUB/SUB pair (port 5556) - modules publish, the controller subscribes to everything. This is plain ZeroMQ, not MQTT or anything with brokers or persistence; if a module isn't connected when a command goes out, it doesn't get it, which is why heartbeats and reconnect logic matter as much as they do.

The recordings themselves follow a similar controller/module split. While recording, a module writes video/audio straight to its own local storage (its SD card, not the network) - the network is only carrying commands and status, never the recording itself, so a flaky wifi card or a busy switch can't drop frames. Once a segment (or the whole session) finishes, the module exports it across to the controller's NVMe drive over the Samba share described below. From there you can get at it two ways: download it straight from the GUI's Recording page (individual files, or the whole session as a zip), or connect to the controller's Samba share from your own PC and copy files off directly - the GUI download is disabled above a certain size and points you at the share instead, since a browser download isn't a sensible way to move tens of gigabytes.

None of this needs the internet. The whole point of SAVIOUR is that it works on a closed lab LAN with no cloud dependency - the sections below cover how that's actually wired together.

## Network

Every device connects to the same PoE+ switch, controller included. The controller's `eth0` gets a static IP, `10.0.0.1/16` by default (chosen in `saviour-config`, written into `/etc/saviour/config`). Modules don't get static IPs - the controller runs `dnsmasq` as a small DHCP server for the network, handing out addresses from the top half of its own /16 range to anything that plugs in.

The controller can also act as your gateway to the outside world if you want one (`GATEWAY_MODE=controller` in the config, NAT'd out through wifi or a second interface), or you can point it at an existing router, or run it fully offline. None of the recording/discovery/export machinery cares either way - it's LAN-only by design.

## Avahi (mDNS hostname resolution)

Avahi is the system mDNS daemon (the same one most Linux desktops use for network discovery), and on SAVIOUR its job is narrow: making `saviour.local` resolve to the controller's IP from any device on the same LAN, so you can type that into a browser instead of hunting for an IP address. `saviour-config` sets `host-name=saviour` in `/etc/avahi/avahi-daemon.conf` and restricts it to `eth0` only.

That's genuinely all avahi does here - it is *not* how modules and the controller find each other. That's a separate mechanism, covered next, and it's worth keeping the two straight because they're easy to conflate.

## Zeroconf (service discovery)

Module <-> controller discovery is handled entirely by `python-zeroconf`, a Python library implementing mDNS/DNS-SD directly (it doesn't touch avahi at all, even though they're both speaking the same underlying protocol). Both sides register their own service type and browse for the other's:

- The controller registers itself as `_controller._tcp.local.` and browses for `_module._tcp.local.`.
- Each module registers itself as `_module._tcp.local.` and browses for `_controller._tcp.local.`.

The advertised service carries properties in its TXT record - a module's registration includes its `type`, `id`, `name`, `group` and current software `version`, so the controller can build a full picture of what just appeared on the network (IP and port come from the mDNS packet itself) without a separate handshake. This is also why a module rebooting with new firmware shows its updated version on the Dashboard automatically once it re-announces - no polling required.

## Samba Shares

Exporting recordings off a module and onto the controller is done over Samba (CIFS), mounted by the module at `/mnt/export`. The controller runs `smbd` serving a `controller_share`, with three separate accounts: `saviour_module` (a machine account every module authenticates as, password regenerated automatically), and `saviour_user`/`saviour_admin` for a researcher connecting from their own laptop to grab files.

Getting a file onto the share safely, without ever losing a partially-copied recording, is the fiddly part. Each file is renamed locally to `PENDING_<filename>`, copied across to the share (also as `PENDING_<filename>`), `fsync`'d, then atomically renamed to its final name on both sides - only then is the local copy moved into an `exported/` folder. If anything fails partway through, the `PENDING_` rename is rolled back locally, so the file is always either safely on the controller or still sitting untouched on the module, never in a state where it looks exported but isn't.

## systemd

Everything runs under systemd. `saviour.service` is what actually launches the controller or module Python process - `saviour-config` writes its `WorkingDirectory`/`ExecStart` to point at the right entrypoint for whatever role and type you've configured, and it's set to restart automatically if the process dies.

PTP sync (the thing that makes every camera's timestamps comparable to the millisecond) runs as its own pair of services, `ptp4l.service` and `phc2sys.service`, independent of `saviour.service` - `ptp4l` disciplines a hardware clock, `phc2sys` disciplines the system clock from it, and they keep running even if you restart the SAVIOUR process itself.

One small logging detail worth knowing if you're ever reading logs directly rather than through the web UI: when running under systemd, log lines don't carry their own timestamp, because journald already timestamps every entry it receives (`journalctl` shows both). Run the same code manually outside systemd and you'll see timestamps reappear in the log lines themselves - it's not a bug, it's just avoiding printing the same information twice.

## Picamera2

Every camera-type module (`camera`, `apa_camera`, `loom_camera`) is built on Picamera2, the current Raspberry Pi camera stack, wrapped in a shared `CameraBase` class that owns the whole lifecycle: opening the sensor, configuring resolution/framerate/sensor mode, the MJPEG live-preview stream you see on the Dashboard, and segmented recording to disk.

Recording isn't one long file - it's split into segments (so a multi-hour session isn't one enormous, hard-to-recover video), and every segment gets its own timestamp CSV sidecar recording the real capture time of each frame. That per-frame timestamp is what makes multi-camera alignment possible later - frame *index* alone isn't reliable enough once you're comparing cameras that may drift by a frame or two over a long session.

`CameraBase` itself is relatively recent - the three camera module types each used to be their own ~1200-line, independently-maintained implementation of the same Picamera2 plumbing, which meant a bug fixed in one didn't get fixed in the other two. They're now thin subclasses (as little as 47 lines) of one shared base, which is also why cross-cutting camera features - FrameSync, rotation, sensor-mode switching - only need to be written once.
Binary file added docs/readthedocs/images/acquisition-board.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/readthedocs/images/complete_system.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/readthedocs/images/ephys-gui-ttls.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/readthedocs/images/in_out_label.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/readthedocs/images/io-board.png-2.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 16 additions & 2 deletions docs/readthedocs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ A SAVIOUR system consists of a "controller" device talking to one or more "modul

SAVIOUR was developed to meet the needs of behavioural neuroscience researchers (at the Simon's Initative for the Developing Brain, University of Edinburgh) who wanted an affordable, high throughput means of running their experiments with low barriers to entry.

To learn more about making recordings with SAVIOUR, visit [Getting Started](getting_started.md) and [FAQs](faqs.md).
To learn more about making recordings with SAVIOUR, visit [Getting Started](getting_started.md) and [FAQs](faqs.md).

To learn about developing modules, GUIs, and the process of contributing to SAVIOUR, start with [How it Works](how_it_works.md) and then visit [Contributing to SAVIOUR](about/contributing.md).
To learn about developing modules, GUIs, and the process of contributing to SAVIOUR, start with [How it Works](how_it_works.md) and then visit [Contributing to SAVIOUR](about/contributing.md).

## All pages

- [Getting Started](getting_started.md) - assigning device roles, connecting modules, running and exporting a recording session.
- [FAQs](faqs.md)
- [Using SAVIOUR with Ephys](open_ephys.md) - syncing and aligning SAVIOUR recordings with an Open Ephys rig.
- [How it Works](how_it_works.md) - architecture and internals, for contributors.
- [Hardware](hardware.md) - the parts list for a controller, and for each module type.
- [CAD/3D Prints](cad.md)
- [Contributing to SAVIOUR](about/contributing.md)
- [License](about/license.md)
- [Acknowledgements](about/acknowledgements.md)

These pages are also published in full at [saviour.readthedocs.io](https://saviour.readthedocs.io) - the same content shown here in the SAVIOUR GUI is the single source for both.
38 changes: 37 additions & 1 deletion docs/readthedocs/open_ephys.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,39 @@
# Using SAVIOUR with Open Ephys

Coming soon!
**IMPORTANT**: read this guide, and double check everything is working correctly before hitting record! Researchers have lost hours of data because a cable was plugged into the wrong port of their HDMI board.

![A complete SAVIOUR and Open Ephys rig: acquisition laptop, acquisition board, and the animal in the recording chamber](images/complete_system.webp)

The acquisition board itself, up close:

![The Open Ephys acquisition board](images/acquisition-board.webp)

On the back of it are four HDMI ports for digital I/O - SAVIOUR only ever uses **Digital In**.

![The four HDMI digital I/O ports on the acquisition board, labelled - Digital In is the one SAVIOUR uses](images/in_out_label.webp)

Each HDMI port carries 8 TTL channels, so you'll need an IO board to break the HDMI connector out into individual BNC connectors before you can wire a single channel to a TTL module. SAVIOUR only needs one of those 8 channels on Digital In - a pseudorandom pulse train for alignment, per the steps below. The other 7 channels on that same port are free for anything else you want logged alongside the ephys data - an experiment-start signal, a stimulus trigger, a lever press, whatever your experiment needs.

## Syncing SAVIOUR to an ephys acquisition system

1. Wire a shared TTL line between the ephys acquisition system and a TTL module input pin so both systems see the same sync pulses.
2. Recording an input pin logs each pulse edge with a PTP-disciplined timestamp, in the same clock domain as every camera/microphone/TTL module on the network.
3. On the ephys side, log the same pulses against its own acquisition clock - the shared pulses are your alignment reference between the two clocks.

Once wired up correctly, each channel's pulses show up directly in the Open Ephys GUI as coloured columns overlaid on the recording - one colour per input channel, so you can see exactly which channel fired, and when, right alongside the neural data it's aligning.

![Open Ephys GUI showing inbound TTL pulses overlaid on the recording as coloured columns, one colour per input channel](images/ephys-gui-ttls.webp)

## Sending sync pulses out to the ephys rig

1. Configure a TTL module output pin (fixed-interval or pseudorandom pulse train) and wire it into a spare digital/sync input on the acquisition system.
2. Start the pulse train before recording begins so there are reference edges throughout the whole session, not just at the start.
3. A pseudorandom (non-periodic) pulse train is easier to align unambiguously than a fixed-rate one if a few pulses are missed on either side.

## Aligning ephys data post-hoc

1. Export the TTL module's per-pulse timestamp CSV alongside the ephys recording.
2. Match pulse edges between the two logs to fit a clock offset (and drift, if the ephys clock isn't disciplined) between ephys time and SAVIOUR/PTP time.
3. Apply that mapping to bring spike times, video frames and any other module's timestamps into one common timeline.

For more information and tooling, see the dedicated [saviour-ephys-analysis](https://github.com/Kind-Wyllie-lab/saviour-ephys-analysis) repository.
Loading
Loading