mirror of
https://github.com/acamarata/fajr-watch.git
synced 2026-06-30 18:54:27 +00:00
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
This commit is contained in:
parent
b62a361c9a
commit
01085cfcad
2 changed files with 419 additions and 0 deletions
151
.github/docs/hardware/SHIP-KIT.md
vendored
Normal file
151
.github/docs/hardware/SHIP-KIT.md
vendored
Normal file
|
|
@ -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
|
||||
268
scripts/provision/flash-kit.sh
Executable file
268
scripts/provision/flash-kit.sh
Executable file
|
|
@ -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
|
||||
Loading…
Reference in a new issue