FishCam is an autonomous underwater video recording system designed for long-term deployment in marine environments. It runs on a Raspberry Pi Zero 2W and captures video, IMU orientation data, and acoustic buzzer sequences for multi-unit synchronization.
Original FishCam code: https://github.com/xaviermouy/fishcam_2020_paper
FishCam/
scripts/ # All deployable scripts (run from this directory)
utils/ # Post-processing and diagnostic utilities
fishcam_config.yaml # Master configuration file
fishcamStartup.sh # Boot-time startup orchestration
wittypi_schedules/ # WittyPi on/off schedule files (.wpi)
POWER_SAVING_SETUP.md # Power saving hardware setup guide
fishcam_setup_steps.txt # Complete setup checklist
At runtime, data is written relative to the scripts directory:
../data/video/ # H.264 / MJPEG video files + per-video _metadata.json
../data/logs/ # Timestamped .log files, power_saving.log, imu_acquisition.log
../data/imu/ # BNO085 IMU CSV files
| Script | Description |
|---|---|
run_video.py |
Main video capture. Records continuously, producing H.264/MJPEG clips with per-frame metadata JSON files. Includes retry logic for transient camera errors. |
run_imu.py |
Reads Adafruit BNO085 IMU over I2C. Writes CSV rows (quaternion, Euler angles, accel, gyro, mag). Flushes every 50 samples. Auto-recovers from I2C crashes. |
run_buzzer.py |
Acoustic sequence controller. Fires at configured trigger times daily. Supports m-sequence (LFSR-based, recommended) and legacy beep modes. |
run_power_manager.py |
Monitors reed switch to toggle between power-saving and configuration modes. Controls WiFi, Bluetooth, HDMI, CPU frequency, services. |
run_network.py |
Connects to WiFi at boot via nmcli. Skipped when power saving mode manages WiFi state. |
run_api.py |
Flask HTTP API serving system status and the web dashboard on port 5000. |
| Script | Description |
|---|---|
fishcamStartup.sh |
Boot-time orchestration (via cron). Syncs RTC, creates directories, connects WiFi, then starts background processes (power manager, IMU, buzzer, API) and video capture in foreground. |
| Script | Description |
|---|---|
start_new_deployment.py |
Interactive 9-step pre-deployment wizard: verify identity, timezone, buzzer schedule, IMU calibration, WittyPi config, clear data/logs, optional reboot. |
configure_wittypi.py |
WittyPi setup: RTC sync, power schedule, voltage protection, daily on/off windows. |
sync_rtc.py |
Boot-time RTC sync from NTP (called by fishcamStartup.sh). |
delete_data.py |
Safe data deletion with confirmation prompt. |
verify_fishcam.sh |
System verification and auto-fix: packages, cron, repo, permissions, hostname, I2C. |
updateFishCamRepo.sh |
Fetches and resets to origin/main. |
clear_system_logs.sh |
Clears systemd journal (pre-deployment). |
clear_wittypi_logs.sh |
Clears WittyPi logs (pre-deployment). |
| Script | Description |
|---|---|
monitor_system.py |
Real-time curses dashboard: process status, log tails, hardware health, storage, buzzer schedule. |
monitor_imu.py |
Real-time IMU data display: orientation, accel, gyro, mag, calibration status. |
calibrate_imu.py |
Interactive IMU calibration with magnetometer coverage tracking. Auto-saves to BNO085 NVRAM. |
dashboard.html |
Web-based monitoring interface. Auto-discovers all fishcams on the network (fishcam01..20.local). |
system_status.py |
Shared status collection module used by both the CLI monitor and the web API. |
| Script | Description |
|---|---|
config.py |
Configuration singleton. Loads fishcam_config.yaml. FishCam ID derived from hostname. |
buzzer_utils.py |
Shared buzzer timing helpers (trigger parsing, schedule display). No hardware dependencies. |
wittypi_utils.py |
WittyPi hardware interface: RTC sync, WiFi/NTP checks, voltage reading. |
| Script | Description |
|---|---|
convertH264toMP4.py |
Convert H.264 video files to MP4. |
convertH264toMP4_ffmpeg.py |
Convert H.264 to MP4 using ffmpeg. |
parse_camera_logs.py |
Extract frame statistics, dropped frames, and buzzer events from logs. |
parse_wittypi_log.py |
Parse WittyPi log files. |
check_power_status.sh |
Report current power saving status. |
deployment_timezone: "America/New_York"The Pi always runs in UTC. This timezone is used to interpret all local times in the config (WittyPi deployment window, daily schedule, buzzer trigger times). Must be a valid IANA timezone name.
video:
duration: 900
resolution: [1920, 1080]
frameRate: 10
quality: 'medium'
format: 'h264'duration: Duration of each video clip in seconds.resolution: Video resolution[width, height]in pixels.frameRate: Frame rate in frames per second.quality: Encoding quality. Options:'very_low','low','medium','high','very_high'.format: Video format. Options:'h264'or'mjpeg'.
camera:
sharpness: 1.0
contrast: 1.0
brightness: 0.0
saturation: 1.0
AnalogueGain: 8.0
AeEnable: true
AeExposureMode: 0
AwbEnable: true
AwbMode: 0
vflip: false
hflip: falsesharpness: 0.0 to 16.0 (default: 1.0).contrast: 0.0 to 32.0 (default: 1.0).brightness: -1.0 to 1.0 (default: 0.0).saturation: 0.0 to 32.0 (default: 1.0).AnalogueGain: Analogue gain, 1.0 to 16.0 (replaces ISO).AeEnable: Auto exposure (true/false).AeExposureMode: 0=Normal, 1=Short, 2=Long, 3=Custom.AwbEnable: Auto white balance (true/false).AwbMode: 0=Auto, 1=Tungsten, 2=Fluorescent, 3=Indoor, 4=Daylight, 5=Cloudy, 6=Custom.vflip/hflip: Vertical/horizontal flip (true/false).
network:
wifi_auto_connect: true
wifi_ssid: "FishcamNetwork"
wifi_password: "your_password"wifi_auto_connect: Automatically connect to WiFi on boot / when entering config mode.wifi_ssid: WiFi network name.wifi_password: WiFi password (stored in plaintext -- avoid committing real credentials).
buzzer:
enabled: true
pin: 26
sequence_mode: 'msequence'
# M-sequence parameters
msequence_n: 6
chip_duration_sec: 0.1
# Legacy beep parameters
beep_count: 4
beep_duration_sec: 0.1
beep_gap_sec: 0.1
# Schedule
trigger_times:
- "00:00"
- "06:00"
- "12:00"
- "18:00"
number_sequences: 5
gap_between_sequences_sec: 5
missed_trigger_grace_sec: 60enabled: Enable/disable buzzer (true/false).pin: GPIO pin for buzzer (BCM numbering).sequence_mode:'msequence'(recommended) or'beep'(legacy).
Each unit plays a unique maximal-length LFSR sequence derived automatically from the unit number in the hostname (e.g., fishcam02 -> unit 2). Provides excellent autocorrelation properties for TDOA cross-correlation. No per-unit config edit needed.
msequence_n: LFSR shift register length. Sequence length = 2^n - 1. n=5 -> 31 chips, n=6 -> 63 chips.chip_duration_sec: Duration of each chip (on or off) in seconds.
Each unit plays a fixed number of beeps. Requires beep_count set uniquely per unit.
beep_count: Number of beeps per sequence (must be unique per fishcam).beep_duration_sec: Duration of each beep in seconds.beep_gap_sec: Silence between beeps within a sequence.
trigger_times: List of HH:MM times (local/deployment timezone) when the buzzer fires each day.number_sequences: Number of sequence repetitions per trigger.gap_between_sequences_sec: Gap between sequence repetitions.missed_trigger_grace_sec: Fire a late trigger if it was scheduled within this many seconds ago (0 = disabled).
power_saving:
enabled: true
reed_switch_pin: 24
led_pin: 23
check_interval: 2.0
cpu_freq_power_saving: 800
cpu_freq_config: 1000
components:
disable_wifi: true
disable_bluetooth: true
disable_hdmi: false
disable_usb: false
throttle_cpu: true
stop_services: true
disable_led_triggers: falseenabled: Enable/disable power saving mode (requires reed switch hardware).reed_switch_pin: GPIO pin for reed switch (BCM numbering).led_pin: GPIO pin for status LED (BCM numbering).check_interval: How often to check reed switch state (seconds).cpu_freq_power_saving/cpu_freq_config: CPU frequency in MHz for each mode (Pi Zero 2W range: 600-1000 MHz).
Component controls -- each can be individually toggled:
| Component | Savings | Description |
|---|---|---|
disable_wifi |
~40-50 mA | Disable WiFi via nmcli |
disable_bluetooth |
~10-15 mA | Disable Bluetooth |
disable_hdmi |
~20-30 mA | Disable HDMI output |
disable_usb |
~20-30 mA | USB autosuspend |
throttle_cpu |
~50-100 mA | Throttle CPU frequency |
stop_services |
~10-20 mA | Stop non-essential services |
disable_led_triggers |
minimal | Disable activity LED triggers |
See POWER_SAVING_SETUP.md for hardware wiring and setup.
imu:
enabled: true
i2c_address: 0x4A
sample_rate_hz: 1
reports:
accelerometer: false
gyroscope: false
magnetometer: false
rotation_vector: true
linear_acceleration: false
gravity: falseenabled: Enable/disable IMU acquisition.i2c_address: I2C address (0x4A default, 0x4B if ADDR pin is high).sample_rate_hz: Sampling rate in Hz (1-50 Hz, BNO085 hardware limited).reports: Enable/disable individual sensor reports. Disabling unused ones reduces file size (~47 MB/hour at 50 Hz with all enabled).
Wiring: VIN -> 3.3V (Pin 1), GND -> GND (Pin 6), SDA -> GPIO2 (Pin 3), SCL -> GPIO3 (Pin 5).
wittypi:
install_dir: '/home/fishcam/Desktop/wittypi'
i2c_address: 0x08
power_cut_delay_sec: 60
low_voltage_cutoff_v: 0
recovery_voltage_v: 0
voltage_log_interval_min: 10
auto_sync_rtc_from_internet: true
auto_sync_rtc_from_gps: false
rtc_sync_min_interval_min: 15
deployment:
start: "2026-08-01 06:00:00"
end: "2026-09-30 23:59:59"
daily_schedule:
anchor_time: "06:00"
windows:
- name: "daytime"
duration_hours: 14
on_min: 15
off_min: 45
- name: "nighttime"
duration_hours: 10
on_min: 5
off_min: 115power_cut_delay_sec: Seconds WittyPi waits after SIGTERM before hard power cut. Must allow clean shutdown (~15s) + margin.low_voltage_cutoff_v/recovery_voltage_v: Battery voltage protection thresholds (set both to 0 to disable).voltage_log_interval_min: How often to log input voltage to CSV.auto_sync_rtc_from_internet: Sync RTC from NTP at boot if WiFi is up.rtc_sync_min_interval_min: Minimum minutes between RTC syncs (prevents excessive I2C writes).deployment: Start and end dates (local timezone) for the deployment window.daily_schedule: Daily on/off cycling. Windows must sum to 24 hours.anchor_timeis the local time where the sequence restarts each day.
paths:
video_dir: '../data/video'
log_dir: '../data/logs'
imu_dir: '../data/imu'
gps_dir: '../data/gps'api:
port: 5000-
Install dependencies (on the Pi):
sudo apt install python3 cron git pip install pyyaml picamera2 lgpio flask pip install adafruit-blinka adafruit-circuitpython-bno08x
-
Edit configuration: Modify
fishcam_config.yamlwith your settings. -
Run the system:
cd /home/fishcam/Desktop/FishCam/FishCam/scripts python run_video.py -
Or run the full startup (all subsystems):
sh fishcamStartup.sh
python monitor_system.py
# Press r to refresh, q to quitShows process status, log tails, hardware health (camera, I2C, voltage, CPU), storage, and buzzer schedule.
python monitor_imu.py
# Press q to quitLive orientation, accelerometer, gyroscope, magnetometer readings. Safe to run anytime -- reads from I2C directly or from the live CSV when run_imu.py is active.
Open dashboard.html in a browser while on the same WiFi network. Auto-discovers all fishcams (fishcam01..20.local) and displays processes, acquisition status, hardware, and logs.
Each fishcam runs a local API on port 5000 (started automatically on boot). Access a single unit at: http://fishcam01.local:5000/
bash verify_fishcam.shAuto-fixes missing packages, cron job, outdated repo, permissions. Reports hostname format, I2C config, SSH status, filesystem expansion.
Location: ../data/video/
Naming Convention:
[iteration]_[hostname]_[timestamp]_[width]x[height]_awbm-[awb]_aem-[ae]_fr-[fps]_q-[quality]_sh-[sharp]_b-[bright]_c-[contrast]_ag-[gain]_sat-[sat].[format]
Example: 1_fishcam01_20250102T143052.123456Z_1920x1080_awbm-0_aem-0_fr-10_q-medium_sh-1.0_b-0.0_c-1.0_ag-8.0_sat-1.0.h264
Written alongside each video file with _metadata.json suffix.
{
"video_file": "...",
"start_time": "2025-01-02T14:30:52.123456",
"duration_sec": 300,
"expected_frame_rate": 10,
"resolution": [1920, 1080],
"total_frames": 2998,
"frames": [
{
"frame_sequence": 0,
"sensor_timestamp_us": 1234567890123,
"system_time": 1735831852.123,
"datetime": "2025-01-02T14:30:52.123456+0000",
"exposure_time": 33000,
"analogue_gain": 8.0
}
]
}frame_sequence: Hardware frame sequence number (gaps = dropped frames).sensor_timestamp_us: Hardware timestamp from camera sensor (microseconds).system_time: Unix timestamp (seconds since epoch).datetime: ISO 8601 timestamp with timezone.
Location: ../data/imu/
CSV rows with timestamp, quaternion (i, j, k, real), Euler angles (heading, pitch, roll), and enabled sensor reports. Written at the configured sample rate.
Location: ../data/logs/
| Log File | Contents |
|---|---|
YYYYMMDDTHHMMSS.log |
Video capture events, frame statistics, buzzer events |
power_saving.log |
Power mode transitions, WiFi/BT/CPU changes, reed switch state |
imu_acquisition.log |
IMU start/stop, I2C errors, recovery attempts |
buzzer.log |
Trigger events, sequence timing, missed triggers |
Default credentials:
- Username:
fishcam - Hostname:
fishcam0x(replace x with the unit number)
Important: The FishCam ID is derived from the hostname at runtime, not from the config file. Always set the hostname correctly.
-
Install required software:
sudo apt update && sudo apt upgrade sudo apt install python3 cron git pip install flask pip install adafruit-blinka adafruit-circuitpython-bno08x -
Download repository:
cd /home/fishcam/Desktop/ git clone https://github.com/xaviermouy/FishCam.git -
Configure Raspberry Pi (via raspi-config):
- Interfaces: Enable SSH, Enable I2C
- Localization: Set timezone to UTC
- System: Boot to CLI, change hostname, splash screen off
- Advanced Options: Persistent logging
-
Set up IMU (BNO085):
- Lower I2C baud rate to 100 kHz in
/boot/firmware/config.txt:dtparam=i2c_arm=on,i2c_arm_baudrate=100000 - Verify:
sudo i2cdetect -y 1(should show 0x4A)
- Lower I2C baud rate to 100 kHz in
-
Setup cron job for auto-start:
crontab -e # Add: @reboot sh /home/fishcam/Desktop/FishCam/FishCam/scripts/fishcamStartup.sh & -
Fix folder ownership:
sudo chown -R fishcam:fishcam /home/fishcam/Desktop/FishCam/
See fishcam_setup_steps.txt for the complete checklist.
-
Update the repo:
bash updateFishCamRepo.sh -
Install WittyPi (v4 mini):
cd /home/fishcam/Desktop wget http://www.uugear.com/repo/WittyPi4/install.sh sudo sh install.sh sudo usermod -aG i2c fishcam sudo chown -R fishcam:fishcam /home/fishcam/Desktop/wittypi/ -
Configure WittyPi:
python configure_wittypi.py -
Set hostname (this becomes the FishCam ID):
sudo raspi-config # System > Change hostname to fishcam0x -
Set buzzer beep count (legacy mode only): edit
buzzer.beep_countin config to a unique value per unit. -
Configure WiFi credentials in
fishcam_config.yaml. -
Expand filesystem:
sudo raspi-config> Advanced Options > Expand Filesystem -
Adjust lens focus and camera orientation:
rpicam-hello --timeout 0 --width 1920 --height 1080 --framerate 10
Set
vflip/hflipin config as needed. -
Configure power saving mode in
fishcam_config.yaml. See POWER_SAVING_SETUP.md for hardware wiring. -
Run pre-deployment wizard:
python start_new_deployment.py -
Physical checks: coin battery, camera cable, O-rings, seal, pressure test.
- Run
python start_new_deployment.py(covers most steps below) - Verify video recording:
python run_video.py(stop with Ctrl+C after one clip) - Test buzzer:
python run_buzzer.py - Check IMU:
python monitor_imu.py - Run system check:
bash verify_fishcam.sh - Verify SD card has sufficient space
- Verify battery is fully charged
- External label matches hostname
FishCam supports optional power saving mode for extended deployments. See POWER_SAVING_SETUP.md for hardware setup.
Control: Reed switch + magnet toggles between deployment (power saving) and configuration (full power) modes.
| Configuration | Power Draw | Battery Life (10,000 mAh)* |
|---|---|---|
| Video only (no power saving) | ~300-350 mA | ~28 hours |
| Video + High Endurance SD card | ~250-300 mA | ~33 hours |
| Video + Power Saving + High Endurance | ~150-200 mA | ~50-66 hours |
| Optimized (all features) | ~100-150 mA | ~66-100 hours |
*Assumes 80% usable capacity. Actual runtime varies with resolution, framerate, and conditions.
- Optimized for continuous recording (designed for dashcams/security cameras)
- Lower power consumption (~50-100 mA vs ~150-200 mA for standard cards)
- Better write endurance (up to 10,000 hours continuous recording)
- Temperature rated (-25C to 85C)
For deploying multiple fishcams with m-sequence buzzer mode (recommended):
- Use the same
fishcam_config.yamlon all units (samemsequence_n,chip_duration_sec,trigger_times). - Set each unit's hostname to a unique
fishcamXX(e.g., fishcam01, fishcam02, ...). The m-sequence is automatically derived from the unit number. - All fishcams play unique sequences at the same trigger times, enabling TDOA cross-correlation.
For legacy beep mode, set sequence_mode: 'beep' and assign a unique beep_count per unit.