From 01085cfcadb054eb90a09b26be58241e092714cf Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Mon, 23 Mar 2026 06:14:46 -0400 Subject: [PATCH] add shippable kit design and SD card flash script Ship Kit ($146/unit, $3,262 for 20): - Pi 4B + Pi Camera Module 3 Wide NoIR in IP65 junction box - 15ft weatherproof USB-C cable + wall adapter - Universal L-bracket + zip ties for mounting - Fits in USPS Medium Flat Rate Box ($17.10 shipping) flash-kit.sh: one-command SD card provisioning - Writes Pi OS + fajr-watch + station.yaml - Pre-configures WiFi, station ID, coordinates - Volunteer just plugs it in, no setup needed --- .github/docs/hardware/SHIP-KIT.md | 151 +++++++++++++++++ scripts/provision/flash-kit.sh | 268 ++++++++++++++++++++++++++++++ 2 files changed, 419 insertions(+) create mode 100644 .github/docs/hardware/SHIP-KIT.md create mode 100755 scripts/provision/flash-kit.sh diff --git a/.github/docs/hardware/SHIP-KIT.md b/.github/docs/hardware/SHIP-KIT.md new file mode 100644 index 0000000..78b68ed --- /dev/null +++ b/.github/docs/hardware/SHIP-KIT.md @@ -0,0 +1,151 @@ +# Shippable Kit Design + +Goal: 20 identical kits you can assemble, flash, box, and mail. Volunteer opens the box, mounts it pointing east, plugs in power, done. + +## Design Principles + +1. **Ships in a USPS Medium Flat Rate Box** ($17.10, 11" x 8.5" x 5.5"). No oversized packages. +2. **No pole included.** Volunteers mount it on whatever they have: fence post, deck railing, wall bracket, window sill, tripod. Include zip ties and a universal L-bracket. +3. **No soldering.** All connections are plug-and-play USB cables. +4. **Two power options:** outdoor AC outlet (included 15ft USB-C cable + wall adapter) or solar panel (optional add-on, ships separately if needed). +5. **WiFi pre-configured.** Ask the volunteer for their SSID and password before shipping. Pre-flash it on the SD card. Zero on-site configuration. +6. **One cable to plug in.** Power. That's it. Camera is internal, WiFi is pre-set, GPS is built in. + +## The Kit + +### What's Inside the Box + +| # | Item | Cost | Notes | +|---|---|---|---| +| 1 | Raspberry Pi 4B 2GB | $45 | Pre-flashed SD card inside | +| 2 | Raspberry Pi Camera Module 3 Wide NoIR | $35 | 120 FOV, no IR filter (better low-light), fixed lens (no alignment needed) | +| 3 | Weatherproof junction box (IP65) | $12 | Hammond 1554K or similar, 6.3" x 3.5" x 2.1" polycarbonate. Camera lens hole pre-drilled. | +| 4 | GPS module (U-blox NEO-6M) | $10 | Glued inside the box, antenna on outside | +| 5 | 64GB SD card (pre-flashed) | $10 | Station config pre-loaded | +| 6 | 15ft outdoor USB-C extension cable | $12 | Weatherproof, runs to nearest outlet | +| 7 | 5V/3A USB-C wall adapter | $8 | Standard phone charger | +| 8 | USB-C right-angle adapter | $3 | For clean cable entry into box | +| 9 | Weatherproof cable gland (PG9) | $2 | Seals the power cable entry hole | +| 10 | Silica gel packets (4x) | $2 | Moisture control inside box | +| 11 | Mounting kit: L-bracket + 4 zip ties + 2 screws | $5 | Universal mount | +| 12 | Quick-start card (laminated) | $2 | 5 steps with pictures | +| **Total per kit** | | **~$146** | | +| **20 kits** | | **~$2,920** | | +| **Shipping (20x USPS Medium Flat Rate)** | | **$342** | | +| **Grand total** | | **~$3,262** | | + +### Optional Solar Add-On ($80, ships separately) + +For volunteers with no outdoor outlet: +- 20W folding solar panel ($25) +- 10Ah USB power bank with pass-through charging ($35) +- 6ft USB-C cable ($5) +- Velcro strips for panel mounting ($3) +- Ships in a separate flat mailer ($12 shipping) + +## Why Pi Camera Module 3 Wide NoIR (Not ZWO) + +For a shippable kit, the Pi Camera Module 3 Wide wins over ZWO: + +| Factor | Pi Cam 3 Wide NoIR | ZWO ASI224MC | +|---|---|---| +| Cost | $35 | $180 | +| Setup | Ribbon cable, zero config | USB, SDK install, gain tuning | +| Lens | Built-in 120 FOV (no alignment) | Separate lens (must focus + align) | +| Low-light | Good (IMX708, 1.4um pixels) | Excellent (IMX224, 3.75um, 0.8e noise) | +| Size | Tiny (25mm x 24mm) | Larger, needs USB port | +| Power | ~0.25W via ribbon | ~1.75W via USB | +| Fragility | Ribbon cable (delicate but enclosed) | USB cable (robust) | + +The ZWO is scientifically better. But for 20 mail-out kits where the volunteer never touches the camera, the Pi Cam 3 Wide is simpler, cheaper, and good enough. The 120 FOV wide-angle captures the full eastern horizon band without a separate fisheye lens. + +We can always deploy ZWO units at the highest-priority dark-sky sites (your own stations, Cherry Springs, etc.) where you control the setup. + +## Assembly (Your Side, Per Kit) + +Time: ~15 minutes per unit once you have the process down. 20 units in an afternoon. + +### 1. Prepare the junction box + +- Drill a 12mm hole on one short end (camera lens) +- Drill a 16mm hole on the bottom (PG9 cable gland for power) +- Drill a 6mm hole on top (GPS antenna cable) + +### 2. Install components + +- Mount Pi 4B inside box using brass standoffs (pre-tapped holes in the junction box) +- Connect Pi Camera Module 3 via 150mm ribbon cable +- Position camera lens against the drilled hole, secure with hot glue +- Connect GPS module to Pi UART pins (4 wires: VCC, GND, TX, RX) +- Route GPS antenna wire through top hole, seal with silicone +- Install PG9 cable gland in bottom hole +- Place 4 silica gel packets inside +- Close and seal the box + +### 3. Flash the SD card + +```bash +# On your Mac, for each station: +./scripts/provision/flash-kit.sh \ + --station-id "station-017" \ + --lat 41.95 \ + --lng -80.55 \ + --elevation 175 \ + --wifi-ssid "VolunteerWiFi" \ + --wifi-pass "password123" \ + --host-name "Ahmed in Conneaut" \ + --environment suburban \ + --horizon lake +``` + +This writes Pi OS + fajr-watch software + station.yaml to the SD card in one command. + +### 4. Test + +- Insert SD card, connect power, wait 2 minutes +- LED should go green (connected) +- Check the web dashboard at fajr.watch/stations to see the unit reporting +- Capture a test frame to verify the camera works +- Power off, pack it up + +### 5. Pack and ship + +Contents of the USPS Medium Flat Rate Box: +- Junction box with electronics (wrapped in bubble wrap) +- USB-C wall adapter in a small bag +- 15ft USB-C cable (coiled) +- Mounting bracket + zip ties in a bag +- Laminated quick-start card on top + +## Quick-Start Card (What the Volunteer Sees) + +``` +FAJR-WATCH STATION — Quick Setup + +1. MOUNT the box pointing EAST (toward sunrise) + Use the bracket, zip ties, or set it on a flat surface. + The camera lens (small hole on the side) must face east + with a clear view of the sky above the horizon. + +2. PLUG the long USB cable into the box (bottom). + Run the other end to the nearest outdoor outlet. + Plug in the wall adapter. + +3. WAIT 2 minutes. The green light means it's working. + +4. DONE. The station runs itself. It observes dawn and dusk + each day and uploads the data over your WiFi. + +Questions? Text/email: fajr-watch@acamarata.com +Your station ID: _______________ +``` + +## Volunteer Onboarding Flow + +1. Volunteer signs up (Google Form or website) +2. They provide: name, email, address, WiFi SSID + password, description of their horizon (photo preferred) +3. You evaluate the site (is the eastern horizon clear enough?) +4. You assign a station ID, flash the SD card with their config +5. Assemble, test, pack, ship +6. They receive it, follow the 3-step card, done +7. You see their station appear on the dashboard within 24 hours diff --git a/scripts/provision/flash-kit.sh b/scripts/provision/flash-kit.sh new file mode 100755 index 0000000..1d99e72 --- /dev/null +++ b/scripts/provision/flash-kit.sh @@ -0,0 +1,268 @@ +#!/bin/bash +# flash-kit.sh — Flash a complete fajr-watch SD card for a volunteer kit. +# +# Writes Raspberry Pi OS Lite + fajr-watch software + station config +# to an SD card in one step. Run on your Mac/Linux workstation. +# +# Usage: +# ./scripts/provision/flash-kit.sh \ +# --station-id "conneaut-01" \ +# --lat 41.95 --lng -80.55 --elevation 175 \ +# --wifi-ssid "MyNetwork" --wifi-pass "password" \ +# --host-name "Aric Camarata" \ +# --environment suburban --horizon lake \ +# --device /dev/disk4 +# +# Requirements: +# - Raspberry Pi Imager CLI (rpi-imager) or dd +# - A downloaded Raspberry Pi OS Lite image (.img or .img.xz) +# - An SD card (32GB+) inserted + +set -euo pipefail + +# ── Parse arguments ── + +STATION_ID="" +LAT="" +LNG="" +ELEVATION="0" +WIFI_SSID="" +WIFI_PASS="" +HOST_NAME="" +ENVIRONMENT="unknown" +HORIZON="unknown" +CAMERA_TYPE="pi_cam3_wide" +DEVICE="" +PI_OS_IMAGE="${PI_OS_IMAGE:-}" + +while [[ $# -gt 0 ]]; do + case "$1" in + --station-id) STATION_ID="$2"; shift 2;; + --lat) LAT="$2"; shift 2;; + --lng) LNG="$2"; shift 2;; + --elevation) ELEVATION="$2"; shift 2;; + --wifi-ssid) WIFI_SSID="$2"; shift 2;; + --wifi-pass) WIFI_PASS="$2"; shift 2;; + --host-name) HOST_NAME="$2"; shift 2;; + --environment) ENVIRONMENT="$2"; shift 2;; + --horizon) HORIZON="$2"; shift 2;; + --camera) CAMERA_TYPE="$2"; shift 2;; + --device) DEVICE="$2"; shift 2;; + --image) PI_OS_IMAGE="$2"; shift 2;; + *) echo "Unknown argument: $1"; exit 1;; + esac +done + +# ── Validate ── + +if [ -z "$STATION_ID" ] || [ -z "$LAT" ] || [ -z "$LNG" ]; then + echo "Error: --station-id, --lat, and --lng are required." + echo "" + echo "Usage: $0 --station-id ID --lat LAT --lng LNG [options]" + exit 1 +fi + +if [ -z "$DEVICE" ]; then + echo "Available disks:" + if [ "$(uname)" = "Darwin" ]; then + diskutil list external physical + else + lsblk -d -o NAME,SIZE,MODEL | grep -v "^loop" + fi + echo "" + echo "Specify the SD card device with --device /dev/diskN" + exit 1 +fi + +echo "=== fajr-watch SD Card Flasher ===" +echo "Station: $STATION_ID" +echo "Location: $LAT, $LNG (elev ${ELEVATION}m)" +echo "WiFi: $WIFI_SSID" +echo "Device: $DEVICE" +echo "" + +# ── Confirm ── + +read -p "This will ERASE $DEVICE. Continue? [y/N] " confirm +if [ "$confirm" != "y" ] && [ "$confirm" != "Y" ]; then + echo "Aborted." + exit 0 +fi + +# ── Flash Pi OS ── + +if [ -z "$PI_OS_IMAGE" ]; then + # Try to find a downloaded image + PI_OS_IMAGE=$(ls -t ~/Downloads/*raspios*lite*arm64*.img* 2>/dev/null | head -1 || true) + if [ -z "$PI_OS_IMAGE" ]; then + echo "Error: No Raspberry Pi OS image found." + echo "Download from https://www.raspberrypi.com/software/operating-systems/" + echo "Then pass with --image /path/to/image.img" + exit 1 + fi +fi + +echo "Flashing Pi OS from: $PI_OS_IMAGE" + +if [ "$(uname)" = "Darwin" ]; then + RDISK="${DEVICE/disk/rdisk}" + diskutil unmountDisk "$DEVICE" + + if [[ "$PI_OS_IMAGE" == *.xz ]]; then + xz -dc "$PI_OS_IMAGE" | sudo dd of="$RDISK" bs=4m + else + sudo dd if="$PI_OS_IMAGE" of="$RDISK" bs=4m + fi + + sync + sleep 2 + + # Re-mount boot partition + diskutil mountDisk "$DEVICE" + BOOT_MOUNT=$(diskutil info "${DEVICE}s1" 2>/dev/null | grep "Mount Point" | awk '{print $3}') + if [ -z "$BOOT_MOUNT" ]; then + BOOT_MOUNT="/Volumes/bootfs" + fi +else + if [[ "$PI_OS_IMAGE" == *.xz ]]; then + xz -dc "$PI_OS_IMAGE" | sudo dd of="$DEVICE" bs=4M status=progress + else + sudo dd if="$PI_OS_IMAGE" of="$DEVICE" bs=4M status=progress + fi + sync + sleep 2 + # Mount boot partition + BOOT_MOUNT="/mnt/fajr-boot" + sudo mkdir -p "$BOOT_MOUNT" + sudo mount "${DEVICE}1" "$BOOT_MOUNT" +fi + +echo "Boot partition mounted at: $BOOT_MOUNT" + +# ── Enable SSH ── + +touch "$BOOT_MOUNT/ssh" + +# ── Configure WiFi (for Pi OS Bookworm, use NetworkManager via firstrun) ── + +if [ -n "$WIFI_SSID" ] && [ -n "$WIFI_PASS" ]; then + # Create a firstrun script that configures WiFi via nmcli + cat > "$BOOT_MOUNT/firstrun.sh" << FIRSTRUN +#!/bin/bash +set -e +nmcli device wifi connect "$WIFI_SSID" password "$WIFI_PASS" || true +FIRSTRUN + chmod +x "$BOOT_MOUNT/firstrun.sh" +fi + +# ── Copy fajr-watch software ── + +FAJR_DIR="$BOOT_MOUNT/fajr-watch" +mkdir -p "$FAJR_DIR" + +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +cp -r "$REPO_ROOT/src" "$FAJR_DIR/src" +cp -r "$REPO_ROOT/config" "$FAJR_DIR/config" +cp "$REPO_ROOT/requirements.txt" "$FAJR_DIR/" +cp "$REPO_ROOT/scripts/provision/first-boot.sh" "$FAJR_DIR/" + +# ── Write station config ── + +cat > "$FAJR_DIR/station.yaml" << YAML +station: + id: "$STATION_ID" + lat: $LAT + lng: $LNG + elevation_m: $ELEVATION + horizon: "$HORIZON" + environment: "$ENVIRONMENT" + host: "$HOST_NAME" + contact: "" + +camera: + type: "$CAMERA_TYPE" + lens_mm: 4.74 + orientation: "east" + azimuth_offset: 0 + +network: + wifi_ssid: "$WIFI_SSID" + wifi_password: "$WIFI_PASS" + upload_url: "https://api.fajr.watch/v1/upload" + api_key: "" + +capture: + interval_s: 10 + twilight_margin_deg: 5 + raw_format: true + max_exposure_s: 30 + target_brightness: 80 + +processing: + detect_on_device: true + min_confidence: 0.5 + keep_raw_frames: false + retention_days: 30 +YAML + +# ── Create first-boot trigger ── + +touch "$FAJR_DIR/first-boot-trigger" + +# ── Create first-boot systemd service (runs once) ── + +ROOTFS_MOUNT="" +if [ "$(uname)" = "Darwin" ]; then + # On macOS, we can't easily write to the ext4 rootfs partition. + # Instead, we'll use a cmdline.txt hook. + echo "Note: On macOS, first-boot service must be configured after first SSH login." + echo "Run: sudo bash /boot/fajr-watch/first-boot.sh" +else + ROOTFS_MOUNT="/mnt/fajr-rootfs" + sudo mkdir -p "$ROOTFS_MOUNT" + sudo mount "${DEVICE}2" "$ROOTFS_MOUNT" + + sudo cp "$FAJR_DIR/first-boot.sh" "$ROOTFS_MOUNT/usr/local/bin/fajr-watch-provision.sh" + sudo chmod +x "$ROOTFS_MOUNT/usr/local/bin/fajr-watch-provision.sh" + + sudo tee "$ROOTFS_MOUNT/etc/systemd/system/fajr-watch-provision.service" > /dev/null << SERVICE +[Unit] +Description=fajr-watch first-boot provisioning +After=network-online.target +Wants=network-online.target +ConditionPathExists=/boot/fajr-watch/first-boot-trigger + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/fajr-watch-provision.sh +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target +SERVICE + + sudo ln -sf /etc/systemd/system/fajr-watch-provision.service \ + "$ROOTFS_MOUNT/etc/systemd/system/multi-user.target.wants/fajr-watch-provision.service" + + sudo umount "$ROOTFS_MOUNT" +fi + +# ── Cleanup ── + +if [ "$(uname)" = "Darwin" ]; then + diskutil unmountDisk "$DEVICE" +else + sudo umount "$BOOT_MOUNT" +fi + +echo "" +echo "=== SD card ready ===" +echo "Station: $STATION_ID" +echo "Location: $LAT, $LNG" +echo "" +echo "Insert into Pi, connect camera, apply power." +if [ "$(uname)" = "Darwin" ]; then + echo "" + echo "NOTE (macOS): After first boot, SSH in and run:" + echo " sudo bash /boot/fajr-watch/first-boot.sh" +fi