mirror of
https://github.com/lynchaos/ashvale-station.git
synced 2026-09-12 12:47:49 +00:00
Prebuilt Pi image and a one-line installer
Two routes onto a Pi. The image for a blank card, the script for a board that already works, which is most of them. deploy/install.sh installs apt dependencies, clones to /opt/ashvale, builds a venv with --system-site-packages, enables I2C and installs a systemd unit. Tested end to end on the real Zero 2 W by installing a second instance on port 8099 alongside the live station: both served, and the live station was untouched throughout. That test earned its keep twice. The installer first declared success while the service was crash-looping on a port clash, because systemd reports active for the instant between exec and the first failed bind; it now polls the HTTP endpoint instead, which is the only check that means anything. And my first attempt to verify that fix was itself worthless, because curl on 127.0.0.1:8000 was answered by the live station rather than the instance under test. deploy/pi-image is a pi-gen stage on Raspberry Pi OS Lite, Trixie, arm64, which is exactly what the board runs. Built by .github/workflows/image.yml against a pinned pi-gen commit, so the artifact does not move when an upstream branch does, and published to Releases where the 2 GB asset limit comfortably fits a Lite image. The image ships no password, no WiFi and no SSH host keys. Baked host keys would give every person who flashed it the same identity and make them trivially impersonable on their own network. Coordinates default to Greenwich at 0 m, wrong for everybody on purpose, because a plausible wrong altitude quietly biases the sea-level reduction on every row. The source is copied through a .gitignore filter rather than a hand-written exclude list, and that is a security property rather than tidiness: the hand-written list I wrote first missed HANDOVER.md, which is gitignored precisely because it contains LAN addresses and SSH details. Verified: 69 files, no state, no local notes, all essentials present.
This commit is contained in:
Executable
+147
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env bash
|
||||
# Install Ashvale Station onto a Raspberry Pi that is already running.
|
||||
#
|
||||
# Most people already have a working Pi and should not have to reflash a card to
|
||||
# try this. The prebuilt image exists for a fresh board; this exists for
|
||||
# everything else, and it is the same install the image performs.
|
||||
#
|
||||
# curl -fsSL https://raw.githubusercontent.com/lynchaos/ashvale-station/main/deploy/install.sh | bash
|
||||
#
|
||||
# Idempotent: safe to re-run to upgrade. It never touches data/ or config.yaml
|
||||
# on a machine that already has them, because those are your history and your
|
||||
# coordinates and neither can be regenerated.
|
||||
set -euo pipefail
|
||||
|
||||
REPO="${ASHVALE_REPO:-https://github.com/lynchaos/ashvale-station.git}"
|
||||
DEST="${ASHVALE_DEST:-/opt/ashvale}"
|
||||
BRANCH="${ASHVALE_BRANCH:-main}"
|
||||
SERVICE="${ASHVALE_SERVICE:-ashvale}"
|
||||
# Overridable so a second instance can be installed alongside a live one,
|
||||
# which is also the only way to test this script without stopping the real
|
||||
# station. Empty means "whatever config.yaml says".
|
||||
PORT_OVERRIDE="${ASHVALE_PORT:-}" # overridable so the installer can be tested without clobbering a live unit
|
||||
|
||||
say() { printf '\n\033[1;36m==>\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33m !\033[0m %s\n' "$*"; }
|
||||
die() { printf '\033[1;31m x\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "run with sudo: curl ... | sudo bash"
|
||||
|
||||
if ! grep -qi raspberry /proc/device-tree/model 2>/dev/null; then
|
||||
warn "this does not look like a Raspberry Pi; continuing anyway"
|
||||
fi
|
||||
|
||||
RUN_USER="${SUDO_USER:-$(getent passwd 1000 | cut -d: -f1)}"
|
||||
[ -n "$RUN_USER" ] || die "could not determine a non-root user to run the service as"
|
||||
|
||||
say "Installing system packages"
|
||||
# Hardware libraries come from apt, never pip. sense-hat pulls in RTIMULib, and
|
||||
# building that inside a clean venv on ARM is a genuine ordeal.
|
||||
apt-get update -qq
|
||||
apt-get install -y --no-install-recommends \
|
||||
git python3-venv python3-numpy python3-smbus2 sense-hat sqlite3
|
||||
|
||||
say "Fetching the application into $DEST"
|
||||
if [ -d "$DEST/.git" ]; then
|
||||
git -C "$DEST" fetch --depth 1 origin "$BRANCH"
|
||||
git -C "$DEST" reset --hard "origin/$BRANCH"
|
||||
else
|
||||
install -d "$DEST"
|
||||
git clone --depth 1 --branch "$BRANCH" "$REPO" "$DEST"
|
||||
fi
|
||||
|
||||
say "Creating the virtual environment"
|
||||
# --system-site-packages so numpy and the Sense HAT stack come from apt rather
|
||||
# than being compiled here. Plain uvicorn, never uvicorn[standard]: that extra
|
||||
# drags in watchfiles and uvloop, which compile Rust and C from source on ARM
|
||||
# for features this does not use.
|
||||
[ -d "$DEST/.venv" ] || python3 -m venv --system-site-packages "$DEST/.venv"
|
||||
"$DEST/.venv/bin/pip" install -q --upgrade pip
|
||||
"$DEST/.venv/bin/pip" install -q --no-cache-dir -r "$DEST/requirements.txt"
|
||||
|
||||
install -d -o "$RUN_USER" -g "$RUN_USER" "$DEST/data" "$DEST/data/state"
|
||||
chown -R "$RUN_USER":"$RUN_USER" "$DEST"
|
||||
|
||||
say "Enabling I2C for the Sense HAT"
|
||||
CFG=/boot/firmware/config.txt
|
||||
[ -f "$CFG" ] || CFG=/boot/config.txt
|
||||
if [ -f "$CFG" ] && ! grep -q '^dtparam=i2c_arm=on' "$CFG"; then
|
||||
printf '\n# --- Ashvale Station ---\ndtparam=i2c_arm=on\n' >> "$CFG"
|
||||
warn "I2C enabled: reboot before the Sense HAT is detected"
|
||||
fi
|
||||
|
||||
say "Installing the service"
|
||||
cat > "/etc/systemd/system/$SERVICE.service" <<UNIT
|
||||
[Unit]
|
||||
Description=Ashvale Station forecast service
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$RUN_USER
|
||||
WorkingDirectory=$DEST
|
||||
${PORT_OVERRIDE:+Environment=ASHVALE_SERVER__PORT=$PORT_OVERRIDE}
|
||||
ExecStart=$DEST/.venv/bin/python run.py
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
# A Zero 2 W has 512 MB. Cap the service so a runaway allocation takes the
|
||||
# service down instead of the whole board.
|
||||
MemoryMax=280M
|
||||
CPUWeight=70
|
||||
Nice=5
|
||||
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=ashvale
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNIT
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable "$SERVICE" >/dev/null
|
||||
systemctl restart "$SERVICE"
|
||||
|
||||
# Poll the endpoint, not systemd. "active" is true for the instant between
|
||||
# exec and the first failed bind, so a unit that is crash-looping on a port
|
||||
# clash reports healthy and the installer congratulates you on a broken install.
|
||||
# Asking the thing whether it answers is the only check that means anything.
|
||||
PORT="$PORT_OVERRIDE"
|
||||
[ -n "$PORT" ] || PORT="$(sed -n 's/^ *port: *\([0-9]\+\).*/\1/p' "$DEST/config.yaml" | head -1)"
|
||||
PORT="${PORT:-8000}"
|
||||
IP=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
for _ in $(seq 1 30); do
|
||||
if curl -fsS -o /dev/null --max-time 2 "http://127.0.0.1:${PORT}/api/status"; then
|
||||
say "Running: http://${IP:-<this-pi>}:${PORT}"
|
||||
HEALTHY=1; break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
if [ "${HEALTHY:-0}" != 1 ]; then
|
||||
systemctl is-active --quiet "$SERVICE" \
|
||||
&& warn "unit is up but nothing is answering on port ${PORT}; is it already in use?" \
|
||||
|| warn "unit is not running"
|
||||
die "install finished but the station is not serving: journalctl -u $SERVICE -n 40"
|
||||
fi
|
||||
|
||||
cat <<NOTE
|
||||
|
||||
Three things worth doing, in order of how much they matter:
|
||||
|
||||
1. Settings tab: set your latitude, longitude and altitude. Altitude feeds
|
||||
the sea-level pressure reduction on every row, and pressure tendency is
|
||||
what drives the precipitation forecast.
|
||||
2. Models and Calibration tab: put a thermometer next to the board and enter
|
||||
the reading. The Sense HAT sits above a SoC running 20 C hotter than the
|
||||
room; one reading fixes the bias on every forecast that follows.
|
||||
3. Settings tab: say whether it is indoors, and tell it when you open a
|
||||
window or turn the heating on. Those are regime changes and the models
|
||||
carry about 55 hours of memory.
|
||||
|
||||
Forecasts need about 10 hours of history before the first training pass.
|
||||
|
||||
No authentication and no TLS: trusted LAN only, do not port-forward it.
|
||||
|
||||
NOTE
|
||||
Reference in New Issue
Block a user