fajr-watch/scripts/provision/first-boot.sh
Aric Camarata b62a361c9a Initial scaffold: turnkey Raspberry Pi twilight observation station
Complete project structure for an automated Fajr/Isha observation appliance:

Hardware:
- BOM for 3 build tiers ($270, $465, $1000)
- 20-unit bulk order spec (~$9,900 for fleet)
- Solar power, weatherproof enclosure, GPS timing

Software:
- Detection engine: multi-channel color analysis + temporal derivative
  tracking on horizon ROI, no fixed brightness threshold
- Capture scheduler: computes twilight windows from coordinates,
  captures every 10s during window, runs detection, uploads results
- Solar position via PyEphem for depression angle at each frame
- Upload sync: hourly cron to central server + CSV export for offline

Provisioning:
- first-boot.sh: one-shot setup script installs all deps, configures
  systemd service, sets up WiFi from station.yaml on boot partition
- Flash SD card, edit station.yaml, plug in, forget

Docs:
- Volunteer host guide: 10-minute installation, plug-and-play
- Hardware BOM with sourcing links
2026-03-23 05:58:38 -04:00

190 lines
5.3 KiB
Bash
Executable file

#!/bin/bash
# fajr-watch first-boot provisioning script
#
# This runs once on first boot of a fresh Raspberry Pi OS image.
# It installs all dependencies, configures the system, and enables
# the fajr-watch service.
#
# Usage: placed in /boot/fajr-watch/first-boot.sh by the image builder,
# triggered by a systemd oneshot service on first boot.
set -euo pipefail
LOG="/var/log/fajr-watch-provision.log"
exec &> >(tee -a "$LOG")
echo "=== fajr-watch first-boot provisioning ==="
echo "Date: $(date -u)"
# ── System setup ──
echo "[1/8] Updating system packages..."
apt-get update -qq
apt-get upgrade -y -qq
echo "[2/8] Installing system dependencies..."
apt-get install -y -qq \
python3-pip python3-venv python3-numpy python3-opencv \
python3-yaml python3-scipy python3-astropy \
gpsd gpsd-clients libgps-dev \
libusb-1.0-0-dev libudev-dev \
ntp ntpdate \
jq curl
# ── GPS setup ──
echo "[3/8] Configuring GPS..."
if [ -e /dev/ttyUSB0 ] || [ -e /dev/ttyACM0 ]; then
systemctl enable gpsd
systemctl start gpsd
echo "GPS device detected"
else
echo "No GPS device found. Using NTP for time sync."
fi
# ── Python environment ──
echo "[4/8] Setting up Python environment..."
VENV="/opt/fajr-watch/venv"
python3 -m venv --system-site-packages "$VENV"
source "$VENV/bin/activate"
pip install --quiet \
ephem \
pyyaml \
requests \
Pillow
# Install ZWO ASI SDK if a ZWO camera is configured
STATION_CONFIG="/boot/fajr-watch/station.yaml"
if [ -f "$STATION_CONFIG" ]; then
CAMERA_TYPE=$(python3 -c "
import yaml
with open('$STATION_CONFIG') as f:
c = yaml.safe_load(f)
print(c.get('camera', {}).get('type', ''))
")
if [[ "$CAMERA_TYPE" == zwo_* ]]; then
echo "ZWO camera configured. Installing ASI SDK..."
pip install --quiet zwoasi
# Download ZWO SDK library
if [ ! -f /usr/lib/libASICamera2.so ]; then
ARCH=$(uname -m)
if [ "$ARCH" = "aarch64" ]; then
SDK_URL="https://github.com/stevemarple/python-zwoasi/raw/master/lib/armv8/libASICamera2.so"
else
SDK_URL="https://github.com/stevemarple/python-zwoasi/raw/master/lib/armv7/libASICamera2.so"
fi
curl -sL "$SDK_URL" -o /usr/lib/libASICamera2.so
chmod 644 /usr/lib/libASICamera2.so
ldconfig
fi
fi
if [[ "$CAMERA_TYPE" == pi_* ]]; then
echo "Pi camera configured. Installing picamera2..."
pip install --quiet picamera2
fi
fi
# ── Install fajr-watch ──
echo "[5/8] Installing fajr-watch software..."
INSTALL_DIR="/opt/fajr-watch/app"
if [ -d /boot/fajr-watch/src ]; then
cp -r /boot/fajr-watch/src "$INSTALL_DIR/src"
cp -r /boot/fajr-watch/config "$INSTALL_DIR/config"
else
# Clone from GitHub if not bundled on the SD card
git clone --depth 1 https://github.com/acamarata/fajr-watch.git "$INSTALL_DIR"
fi
# ── Data directories ──
echo "[6/8] Creating data directories..."
mkdir -p /var/lib/fajr-watch/data/{captures,results,upload-queue}
chown -R pi:pi /var/lib/fajr-watch
# ── Copy station config ──
if [ -f "$STATION_CONFIG" ]; then
cp "$STATION_CONFIG" "$INSTALL_DIR/config/station.yaml"
echo "Station config loaded from boot partition"
fi
# ── WiFi setup from station config ──
if [ -f "$STATION_CONFIG" ]; then
WIFI_SSID=$(python3 -c "
import yaml
with open('$STATION_CONFIG') as f:
c = yaml.safe_load(f)
print(c.get('network', {}).get('wifi_ssid', ''))
")
WIFI_PASS=$(python3 -c "
import yaml
with open('$STATION_CONFIG') as f:
c = yaml.safe_load(f)
print(c.get('network', {}).get('wifi_password', ''))
")
if [ -n "$WIFI_SSID" ] && [ -n "$WIFI_PASS" ]; then
echo "[6b] Configuring WiFi: $WIFI_SSID"
nmcli device wifi connect "$WIFI_SSID" password "$WIFI_PASS" || true
fi
fi
# ── Systemd service ──
echo "[7/8] Installing systemd service..."
cat > /etc/systemd/system/fajr-watch.service << 'UNIT'
[Unit]
Description=fajr-watch twilight observation station
After=network-online.target gpsd.service
Wants=network-online.target
[Service]
Type=simple
User=pi
Group=pi
Environment=PYTHONPATH=/opt/fajr-watch/app
ExecStart=/opt/fajr-watch/venv/bin/python -m src.capture.scheduler
WorkingDirectory=/opt/fajr-watch/app
Restart=always
RestartSec=60
StandardOutput=append:/var/log/fajr-watch.log
StandardError=append:/var/log/fajr-watch.log
[Install]
WantedBy=multi-user.target
UNIT
systemctl daemon-reload
systemctl enable fajr-watch.service
systemctl start fajr-watch.service
# ── Upload cron ──
echo "[8/8] Setting up data upload cron..."
cat > /etc/cron.d/fajr-watch-upload << 'CRON'
# Upload completed detection results every hour
0 * * * * pi /opt/fajr-watch/venv/bin/python -m src.upload.sync 2>> /var/log/fajr-watch-upload.log
CRON
# ── Disable first-boot trigger ──
rm -f /boot/fajr-watch/first-boot-trigger
systemctl disable fajr-watch-provision.service 2>/dev/null || true
echo ""
echo "=== fajr-watch provisioning complete ==="
echo "Station ID: $(python3 -c "
import yaml
with open('$STATION_CONFIG') as f:
c = yaml.safe_load(f)
print(c.get('station', {}).get('id', 'unknown'))
" 2>/dev/null || echo 'unknown')"
echo "Service status:"
systemctl status fajr-watch.service --no-pager || true
echo ""
echo "Logs: journalctl -u fajr-watch -f"
echo "Data: /var/lib/fajr-watch/data/results/"