Skip to content
joergsflow edited this page Jun 11, 2026 · 2 revisions

🛰 Sun-Moon Transit Predictor wiki — Home · Setup · Usage · Advanced · Reference · ↩ README

🚀 Setup — install & run

Contents


Quick start (Raspberry Pi)

Catch an aircraft — or the ISS — crossing the Sun or Moon, automatically. A little Raspberry Pi listens to every plane your antenna can hear, runs the geometry for your exact spot on Earth, and pings your phone (and optionally fires your camera) in the seconds before one slides across the disc. Set it up once; it then runs unattended for months, browser-administered, drawing ~5 W.

You need two cheap parcels and about 15 minutes:

  1. An ADS-B receiver — an RTL-SDR USB stick + a 1090 MHz antenna (~€30). Example bundle
  2. A Raspberry Pi (Pi 4 or 5) + microSD card + USB-C power (~€80). Example kit

Then three steps:

# 1. Flash "Raspberry Pi OS Lite (Legacy, 64-bit)" with Raspberry Pi Imager.
#    In "Edit Settings" set your Wi-Fi + enable SSH, then boot the Pi & SSH in.

# 2. Plug the SDR stick into a USB-2 port (antenna attached), then run:
curl -fsSL https://raw.githubusercontent.com/joergs-git/sun-moon-transit-predictor/main/scripts/bootstrap-pi5.sh | bash

# 3. Reboot once (so the SDR driver takes hold), then open in any browser:
#    http://<your-pi-ip>:8081/

That single line installs everything: the ADS-B decoder (dump1090-fa) and its RTL-SDR driver, Node.js, the predictor service, and a nightly auto-updater. After that, everything else lives in your browser — your location, your telescope optics, your phone alerts, your capture rigs.

Already have an ADS-B feed? (an existing dump1090, a PiAware box, a network aircraft.json) Skip the receiver install with ... bootstrap-pi5.sh | bash -s -- --no-dump1090, then point adsb.url at your feed.

Want zero prompts (cloud-init / Ansible)? Pass coordinates as env vars — see Install on the Pi.

Everything below is the long-form version: the full shopping list, every install option, how the prediction works, and the complete reference.

Hardware + software bill of materials

Shopping list — the short answer

If you only want the bottom-line "what do I order to make this work", it is essentially two parcels:

A. A tiny 1090 MHz ADS-B receiver — USB stick + antenna. A short, telescopic 1090 MHz antenna plus an RTL-SDR-based USB dongle. A more compact antenna also works: shorter 1090 MHz antenna (depending on your coax/antenna connectors you may need female–female SMA adapters). Plugs into the Pi's USB port; the antenna sits anywhere with a clear view of the sky (a windowsill is usually enough for 100-200 km range). Example kit, just to anchor the picture in your head: RTL-SDR + 1090 MHz antenna bundle. Any RTL-SDR (RTL2832U + R820T2 tuner) clone works as long as it decodes 1090 MHz Mode S — the RTL-SDR Blog v3 is the gold standard if you want one purchase that you never have to reconsider.

B. A small Raspberry Pi with a microSD card. The matchbox-sized computer that the receiver plugs into. Anything from a Pi 4 upwards is fine; a Pi 5 (4 GB) is the validated host. Needs a microSD card (16 GB+, endurance grade like SanDisk High Endurance), a USB-C power supply, and an Ethernet cable or your Wi-Fi credentials. Example: Raspberry Pi 5 starter kit.

C. (Optional) A piezo buzzer for audible alerts. A tiny passive piezo that beeps a transit countdown straight from the Pi's GPIO — no monitor needed. Two jumpers across GPIO13 ↔ GND, no extra parts. Example: piezo buzzer. Wiring + signals are in the 3D-Prints and display docs.

That's it. Plug the dongle into the Pi, antenna into the dongle, Pi into your network, follow the one-liner installer below, and from then on everything else lives in your browser — your observer location, your telescope optics, your Pushover notification target, your SharpCap capture rigs. After setup the Pi sits in a corner, draws maybe 5 W, and runs unattended for months at a time.

Why does it have to be local on a Pi at home?

In principle one could host this as an internet service, but two constraints make the Pi-at-home form factor the obvious choice:

  1. You only care about the sky above YOUR observatory. The maths has to run for your exact WGS84 coordinates — an aircraft only crosses a 0.5° solar/lunar disc when the geometry from a specific ground point lines up. There is no sensible way to time-share that computation across multiple users in different cities.
  2. The receiver has to be near the antenna. 1090 MHz line-of-sight covers maybe 200-400 km from a rooftop; the things you can photograph from your garden are inside that radius anyway. A Hamburg-based ADS-B feed cannot tell you what is crossing the Sun above Munich.
  3. Latency is real. The pipeline runs every 2 seconds end-to-end, from ADS-B fix to "fire SharpCap NOW". Every hop you add to that path eats into the < 1-second window when an aircraft is actually on disc. Local TCP between a Pi and a Windows capture machine is < 5 ms; a public cloud round-trip is 30-100 ms even on a good day.

Hence: small Pi + small dongle + small antenna, on your LAN, near your telescope. Once installed it is browser-administered and effectively maintenance-free.

The detailed bills of materials below add the optional + situational items (active LNA, PoE HAT, external SSD, SharpCap integration, etc.) for the cases where you want to push the setup further.

Required hardware

Item Notes
Raspberry Pi 5 (4 GB or 8 GB) The host. Earlier Pi models work too but the v0.7+ tracker tick + browser UI was profiled on the Pi 5.
microSD card (≥ 16 GB, A1/A2 endurance) Boot media. SanDisk High Endurance / Samsung PRO Endurance recommended — the SQLite history and lifecycle snapshot write small batches continuously.
USB-C power supply (5 V / 5 A) Official Raspberry Pi 5 PSU or equivalent. Skip if you go the PoE route below.
RTL-SDR USB stick (RTL2832U + R820T2 tuner) The 1090 MHz ADS-B receiver. The RTL-SDR Blog v3 is the de-facto standard — clean clock, metal case, bias-T for active antennas. Any clone works as long as it decodes 1090 MHz Mode S.
1090 MHz ADS-B antenna A FlightAware 1090 MHz outdoor antenna or any λ/4 mag-mount tuned for 1090 MHz. Sky view = range. A compact option: shorter 1090 MHz antenna.
Coax + adapters SMA male ↔ whatever your antenna terminates in. Short and shielded — every dB lost on the cable is range lost. Female–female SMA adapters are often needed to mate the coax to the antenna/dongle.
Network Ethernet or Wi-Fi to a router that can reach the Pi from your browser. The HTTP API is unauthenticated; keep the Pi on a trusted LAN or front it with a reverse proxy.

Optional / situational

Item When you want it
3D-printed stand / case A printable desk stand that holds the Pi 5, the 4.2" e-paper panel and the RTL-SDR dongle in one unit (plus separate Pi-5 and e-paper cases). STL files, a photo, and the buzzer wiring are in 3D-Prints/.
Piezo buzzer (any small passive piezo) Audible transit alerts — a rising chord when a candidate appears, a falling one when it's gone, an accelerating countdown, and an entry chord at the actual disc transit. Two jumpers, no extra parts: GPIO13 (pin 33) ↔ buzzer ↔ GND (pin 34). Example buzzer. Wiring + tuning: 3D-Prints/README.md and display/README.md.
Waveshare PoE HAT (or equivalent IEEE 802.3af/at HAT) If you want PoE-only operation — single Ethernet cable provides power and network, no USB-C PSU needed. Mounts on the Pi 5's 40-pin GPIO header. Verify the HAT's spec matches the Pi 5 power budget (≥ 5 V/5 A continuous including ADS-B-stick draw).
Active LNA (Uputronics / RTL-SDR Blog) at the antenna feedpoint Pulls weaker / further aircraft out of the noise; powered via the RTL-SDR's bias-T. Only worth it if you're seeing < 200 km range.
1090 MHz bandpass / SAW filter Cuts strong out-of-band signals (FM broadcast, cellular) that can desensitise the RTL. Often built into the LNAs above.
Active cooling case (Argon ONE V3, Pi 5 official cooler, etc.) The Pi 5 throttles under sustained load; the tracker tick is light but if you co-host other services you'll want active cooling.
External USB-C SSD Move data/history.db and data/lifecycle.json off the SD card by symlinking the data/ directory. Massively extends SD-card life for multi-year deployments.
Pushover account (pushover.net) Phone notifications for the three transit stages. The pipeline runs fine without it (pushover.enabled=false), but you'll only see transits in the web UI.
Windows PC running SharpCap + camera Only if you want the predictor to automatically start a capture the moment a transit goes imminent (too tight a window — often < 30 s — for SharpCap's own Sequencer). Runs a tiny stdlib-only Python listener inside SharpCap (4.x uses embedded CPython, not IronPython — the listener is portable across both); the Pi triggers it over TCP. Multi-rig: drive several scopes/PCs in parallel via sharpcap.targets[]. See SharpCap capture trigger. Off by default.
Waveshare 4.2" B/W e-paper panel (SPI, 400×300) A browserless physical readout — clock, location, live count and the soonest Real candidates (ETA, altitude, speed, distance, angle) plus Sky-now + an FOV preview. Plugs onto the Pi's 40-pin header (SPI, not I2C). Configured entirely from the web Settings panel; can also be driven from a remote predictor over the LAN. See E-paper display. Off by default.

Required software (installed by scripts/install-pi5.sh)

Item What it does
Raspberry Pi OS Lite, 64-bit — Legacy (Bullseye) Use the Legacy image. In Raspberry Pi Imager: Choose OS → Raspberry Pi OS (other) → "Raspberry Pi OS Lite (Legacy, 64-bit)". This is the known-good image — the current (Bookworm) Lite image caused dependency/version trouble with the ADS-B + Node stack during bring-up. Set hostname, SSH key and Wi-Fi in the Imager's "Edit Settings" before flashing for a zero-touch first boot.
dump1090-fa (FlightAware) The ADS-B decoder for the RTL-SDR / AirNav FlightStick — exposes aircraft.json on http://localhost:8080/data/aircraft.json (polled every 2 s). Not in the default repos — but the bootstrap-pi5.sh one-liner installs it by default (skip with --no-dump1090). Manual steps: ADS-B receiver setup below.
Node.js 22+ Runtime. Pulled from NodeSource by the installer if absent. Needs --experimental-sqlite on Node 22; stable on Node 24+.
git Not on a fresh Pi OS Lite image — sudo apt-get install -y git first (the bootstrap-pi5.sh one-liner does this for you).
This repo (sun-moon-transit-predictor) git clone https://github.com/joergs-git/sun-moon-transit-predictor.git — contains bin/stp.js, the systemd units in systemd/, the install + auto-update scripts in scripts/, the web UI in web/.

Optional external services

Item What you get
adsbdb.com (no account needed) IATA flight numbers, origin / destination airports, airline names attached to every candidate. Used live for the tracking panel and Pushover payload, cached for 1 h per callsign. Skip with routes.enabled=false.
OpenSky Network account (free) Optional schedule augmentation: backfills the predictor's watchlist with flights you may not have seen yourself yet. Configured via scripts/refresh-schedule.js. Off by default.
AirNav On-Demand API v2 (paid, token) Optional rich airframe + live route + photo for an aircraft. Paste the bearer token from airnavradar.com/api/dashboard into ⚙ Settings → AirNav Radar API (stored masked in service.json, server-side only — the browser uses our /api/acinfo proxy). Each upstream call is billed in credits, so it is fetched only on an explicit row click (FOV box) or a flight-number hover, and cached per airframe for the session (static data 6 h, live 60 s). Off until a token is set.

ADS-B receiver setup (dump1090-fa + AirNav FlightStick)

The Quick start one-liner already does all of this for youbootstrap-pi5.sh installs dump1090-fa + the RTL-SDR driver by default. This section is the manual reference: what those steps actually are, for troubleshooting or if you ran with --no-dump1090.

The AirNav RadarBox / AirNav ADS-B FlightStick is a standard RTL-SDR (RTL2832U + R820T2, built-in 1090 MHz SAW filter) — it needs no special driver, just dump1090-fa from FlightAware's apt repo. Plug the FlightStick into a USB-2 port (USB-3 ports are RF-noisy at 1090 MHz), antenna attached, then:

# 1. FlightAware apt repo + the decoder (pulls in rtl-sdr automatically).
#    NOTE: the repo-package version (here 1.3) bumps occasionally — if this
#    404s, check the directory listing at
#    https://www.flightaware.com/adsb/piaware/files/packages/pool/piaware/f/flightaware-apt-repository/
#    or use the official installer: flightaware.com/adsb/piaware/install
sudo apt-get update
wget -O /tmp/fa-repo.deb \
  https://www.flightaware.com/adsb/piaware/files/packages/pool/piaware/f/flightaware-apt-repository/flightaware-apt-repository_1.3_all.deb
sudo dpkg -i /tmp/fa-repo.deb && rm /tmp/fa-repo.deb
sudo apt-get update
sudo apt-get install -y dump1090-fa

# 2. Stop the DVB-T kernel driver grabbing the stick (idempotent; harmless
#    if dump1090-fa already did it). Then reboot so it takes effect.
echo 'blacklist dvb_usb_rtl28xxu' | sudo tee /etc/modprobe.d/blacklist-rtl.conf
sudo reboot

After the reboot it runs as a systemd service (dump1090-fa) on port 8080. Verify — do not start the app until this returns aircraft (needs sky view + planes overhead):

systemctl status dump1090-fa --no-pager
curl -s localhost:8080/data/aircraft.json | head -c 300   # JSON with "aircraft":[…]
# or open  http://<pi-ip>:8080/  (SkyAware map) in a browser

Gain: leave the dump1090-fa default — the FlightStick's built-in filter makes AGC work well out of the box. Only if range is poor, tune --gain in /etc/default/dump1090-fa then sudo systemctl restart dump1090-fa (no other experiments needed). The app's adsb.url default (http://localhost:8080/data/aircraft.json) already matches this — nothing to configure on the app side.

Sidenote — sharing to AirNav RadarBox (rbfeeder). Optional and fully independent of this predictor. rbfeeder + your AirNav sharing key runs alongside dump1090-fa (reads the same decoder) and uploads to airnavradar.com; this app only ever needs the local aircraft.json on :8080, so the two don't interfere. MLAT active = the feeder's multilateration client is up: for Mode-S aircraft that do not broadcast their own GPS position, several internet-connected stations jointly compute the position from the signal's time-difference-of-arrival. It needs a precise station location — use the same WGS84 decimal degrees you put in config/observer.json so the feed and the predictor agree.

Quick install on the Pi 5

The Quick start at the top is all most people need. This section is the full reference: every flag, the manual path, and the zero-touch first-boot recipe.

Recommended OS image: Raspberry Pi OS Lite (Legacy, 64-bit) — see the Required software note above; the Legacy image is the validated one. Set hostname, SSH key and Wi-Fi in the Imager's "Edit Settings" before flashing.

From a blank image (one-liner bootstrap)

On a fresh OS with nothing installed yet, scripts/bootstrap-pi5.sh installs the apt prerequisites (git, curl, ca-certificates), dump1090-fa + the RTL-SDR driver (the ADS-B data source — on by default), clones the repo, and hands off to install-pi5.sh (forwarding all remaining flags + STP_* env vars). Review it first — piping a remote script to a shell runs code as you:

curl -fsSL https://raw.githubusercontent.com/joergs-git/sun-moon-transit-predictor/main/scripts/bootstrap-pi5.sh | bash
# zero-touch (coordinates via env, no prompts):
curl -fsSL .../scripts/bootstrap-pi5.sh | STP_LAT=52.28 STP_LON=7.44 STP_ELEV=50 bash -s -- --non-interactive
# bring-your-own ADS-B feed (skip the dump1090-fa install):
curl -fsSL .../scripts/bootstrap-pi5.sh | bash -s -- --no-dump1090

dump1090-fa is the ADS-B data source — without it (and --no-dump1090 with no replacement feed) the predictor has no aircraft to track. A reboot after the bootstrap is recommended so the DVB-T blacklist takes effect.

Manual install (no bootstrap)

Raspberry Pi OS Lite has no git out of the box — install it first (the bootstrap one-liner above does this for you):

sudo apt-get update
sudo apt-get install -y git
git clone https://github.com/joergs-git/sun-moon-transit-predictor.git
cd sun-moon-transit-predictor
bash scripts/install-pi5.sh

install-pi5.sh (idempotent — safe to re-run after every git pull):

  1. installs Node.js 22 from NodeSource if it isn't already present,
  2. runs npm install --omit=dev,
  3. prompts for observer coordinates + Pushover credentials and writes config/observer.json + config/service.json with the current defaults (both gitignored so git pull / the auto-updater can never overwrite them),
  4. installs and starts the stp.service systemd unit (light sandboxing — ProtectSystem=strict, ReadWritePaths=…/data),
  5. unless --no-auto-update: installs stp-update.timer (nightly) + stp-update.path (version-badge click-to-update) + the narrow sudoers rule,
  6. always installs stp-tle.timer (daily ISS TLE refresh) and does one initial TLE fetch, so the ISS feature is active out of the box.

After it finishes, browse to http://<pi-ip>:8081/. Logs: journalctl -u stp.service -f.

Re-running the script keeps existing config files. Useful flags:

Flag Effect
--overwrite Re-prompt for everything; rewrite both config files.
--non-interactive Zero prompts; reads defaults from env vars (see below). Pairs well with cloud-init / Ansible / first-boot scripts.
--no-auto-update Skip the nightly stp-update.timer install.

Zero-touch first-boot install

For a true zero-interaction setup, drop credentials in env vars and let the installer write everything in one shot:

STP_LAT=52.2833 \
STP_LON=7.4406 \
STP_ELEV=50 \
STP_GEOID_M=46 \
STP_PUSHOVER_TOKEN=azGD…  \
STP_PUSHOVER_USER=uQiR… \
bash scripts/install-pi5.sh --non-interactive

The full env-var list is in the script's header (bash scripts/install-pi5.sh --help).

Service control (systemd)

The installer registers stp.service as a systemd unit and starts it. From then on it auto-restarts on failure and comes back after a reboot. The day-to-day commands:

# status / start / stop / restart
sudo systemctl status   stp.service
sudo systemctl start    stp.service
sudo systemctl stop     stp.service
sudo systemctl restart  stp.service

# enable / disable autostart on boot
sudo systemctl enable   stp.service
sudo systemctl disable  stp.service

# logs (live tail and last hour)
journalctl -u stp.service -f
journalctl -u stp.service --since "1 hour ago" --no-pager

After editing config/observer.json or config/service.json, restart the service so the changes are picked up:

sudo systemctl restart stp.service

To remove the unit (without uninstalling Node or the repo):

sudo systemctl disable --now stp.service
sudo rm /etc/systemd/system/stp.service
sudo systemctl daemon-reload

Updating the service

Auto-update is on by default

What triggers an update? Every commit pushed (or merged) to the main branch on github.com/joergs-git/sun-moon-transit-predictor. The Pi tracks origin/main directly — GitHub Releases / tags are not required and are ignored. Latency: up to 24 hours (the next 03:30 timer firing). To pull immediately, see Manual update below.

The installer drops scripts/auto-update.sh plus a systemd timer (stp-update.timer) that fires nightly at 03:30 ± 15 min. Each run:

  1. Backs up config/observer.json and config/service.json to a temp dir (defensive — even if upstream renames or .gitignores them, your per-site setup survives).
  2. git pull --ff-only (no merges, no force).
  3. Restores the configs if anything changed underneath them.
  4. Runs npm install --omit=dev only if package.json / lockfile moved.
  5. Restarts stp.service only if backend code (src/, bin/, package*.json, systemd/stp.service, config/service.example.json) changed. Frontend-only commits don't restart — the browser picks them up on the next refresh.

The restart is graceful (~5 s downtime; SIGTERM → flush SQLite → exit → systemd respawn). No interactive prompt, no SSH session needed, no manual intervention on the Pi.

Inspect / probe / disable:

# what's scheduled and when next?
systemctl list-timers | grep stp-update

# run an update right now (same code path the timer uses)
sudo systemctl start stp-update.service
journalctl -u stp-update.service -n 50 --no-pager

# turn the auto-updater off without touching the main service
sudo systemctl disable --now stp-update.timer

Click-to-update from the web UI (v0.8.1)

The small version badge next to the page title is clickable. Clicking it (after a confirm dialog) makes the service pull origin/main and restart — no SSH needed.

Security model — the unauthenticated LAN UI never gets a shell:

  • POST /api/update only drops a trigger file (data/update.request); it runs no git/systemctl. A confirmed JSON body is required, which also blocks naive cross-site drive-by triggering (the request needs a CORS preflight this server does not answer).
  • A privileged stp-update.path systemd unit watches that file and fires the same stp-update.service the nightly timer uses. The updater deletes the trigger on start, so a single click can't loop it.
  • update.debounceMs (default 30 s) swallows double-clicks / two clients.
  • Take it out entirely with "update": { "enabled": false } in config/service.json, or sudo systemctl disable --now stp-update.path.
# is the click-to-update watcher active?
systemctl status stp-update.path --no-pager

Troubleshooting: "I click the version, confirm, but nothing updates"

The endpoint only drops a trigger file — the actual git pull + restart is done by the privileged stp-update.pathstp-update.service units. Nothing happens if that watcher isn't running:

  • It's a no-op on non-systemd hosts (e.g. a macOS dev box). Click-to- update is a Pi/Linux feature; test it on the Pi, not the laptop.

  • stp-update.path not installed/enabled on the Pi. The unit was added in v0.8.1. auto-update.sh (nightly / code update) does not install systemd units, so a Pi set up before v0.8.1 and only code-updated never got it. One-time fix on the Pi:

    cd ~/sun-moon-transit-predictor
    bash scripts/install-pi5.sh           # idempotent; installs + enables stp-update.path
    systemctl is-active stp-update.path   # → active

Since v0.10.1 the UI no longer fails silently: after you confirm, the line under the title reports requested → consumed (restarting…), or, if no watcher consumes the trigger within ~12 s, "stuck — stp-update.path not installed/enabled (run scripts/install-pi5.sh)". The nightly updater also logs this warning to journalctl -u stp-update.service.

Manual update

The same script is safe to run on demand:

cd ~/sun-moon-transit-predictor
bash scripts/auto-update.sh

Or the long form, which is what auto-update.sh automates:

git pull --ff-only
npm install --omit=dev          # only if package.json changed
sudo systemctl restart stp.service
journalctl -u stp.service -n 30 --no-pager

Frontend-only updates

Files in web/ are served live from disk by the Node process — no build step, no bundling. After a pull, a hard browser refresh (Ctrl+Shift+R) is enough; systemctl restart is not needed for HTML / JS / CSS-only changes. The auto-updater detects this and skips the restart.

What is preserved across updates

config/observer.json and config/service.json are gitignored. They are written once by the installer and never overwritten by git pull, auto-update.sh, or a re-run of the installer (use --overwrite to force). The schema reference lives at config/observer.example.json and config/service.example.json — diff your real files against those when a release notes a new field.

One-time migration from v0.1.x → v0.2.0

Earlier versions tracked config/observer.json in git. Pulling v0.2.0+ on top of an older checkout will refuse with error: Your local changes ... would be overwritten by merge (which is git protecting your real coordinates). Run this exact sequence once, on each existing Pi, the first time you update:

cd ~/sun-moon-transit-predictor

# 1. Back up the real coords (still intact on disk)
cp config/observer.json /tmp/observer.json.bak

# 2. Reset the working-tree file to HEAD so the pull's deletion can apply
git checkout -- config/observer.json

# 3. Pull — succeeds now and removes the old tracked file
git pull --ff-only

# 4. Restore the real config; observer.json is now gitignored, so git
#    will never touch it again
cp /tmp/observer.json.bak config/observer.json
rm /tmp/observer.json.bak

# 5. Verify
cat config/observer.json

# 6. Re-run the installer. Your config is kept (no prompts) — the only
#    new artefact is the nightly auto-update timer + sudoers fragment.
bash scripts/install-pi5.sh

# 7. Sanity check
systemctl status stp.service
systemctl list-timers | grep stp-update
curl -s http://localhost:8081/api/health
node scripts/test-push.js          # optional: confirm Pushover end-to-end

After this one-time step, every subsequent push to main rolls onto the Pi automatically the next night via auto-update.sh, with the same backup/restore guard built in. You will never need to repeat steps 1–4.

Push-driven updates (GitHub webhook)

Webhooks require an inbound HTTPS endpoint, which a typical home Pi behind NAT does not expose. Workable patterns if you need near-real-time updates:

  • a public reverse tunnel (Cloudflare Tunnel, Tailscale Funnel, ngrok) pointing at a tiny webhook receiver on the Pi that runs auto-update.sh, or
  • a GitHub Actions job that opens an SSH tunnel via Tailscale and runs bash scripts/auto-update.sh on the Pi after each merge to main.

For a hobby setup the bundled nightly timer is almost always enough.