Skip to content
joergsflow edited this page Jun 12, 2026 · 3 revisions

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

🛰 Usage — alerts, web UI, display

Contents


Web UI

http://<host>:8081/ ships a single-page UI with two panels:

  • Sky now — current Sun/Moon Az/El with the observability flag.
  • Tracking — the unified lifecycle list (see Candidate lifecycle above). One row per (icao, body) or (flight, body), sorted by status urgency then ETA. Status pill on the left with the icon (📅 📡 ✈️ 🎯 ❌); whole-row tint for imminent / candidate so urgent rows draw the eye. Polls /api/state every 2 s — rows transition status in real time as the tracker sees them appear, converge, and (sometimes) drop.
  • History — paginated list backed by /api/history, showing every persisted notification (radio + candidate + imminent stages) with Transit time, callsign, IATA flight, origin / destination, body, minimum separation, altitude and speed.

FOV preview pane (v0.7.1+)

Top right of the page, beside Sky now, sits a permanent FOV preview pane (originally a click-to-open modal, v0.6.0). It auto-shows the most recently spotted live candidate whose minimum angular separation is under — i.e. visually close enough to actually intersect or graze the body — and refreshes on every 2 s state poll. Clicking a row in Tracking or History pins that entry into the pane (an orange bar marks the pinned row); the pin is released as soon as a newer qualifying live candidate (sep < 1°) arrives. Press Escape at any time to drop the pin and resume auto-tracking.

The sketch itself shows:

  • FOV rectangle sized to the optical setup configured in Settings (default 500 mm + ZWO ASI174MM → FOV ≈ 1.30° × 0.82°); changes take effect on the next poll, no reload needed.
  • Sun / Moon disc centred at the body's apparent diameter (Sun 0.53°, Moon 0.52°).
  • Aircraft silhouette scaled by line-of-sight distance using a generic ~36 m airliner footprint — at 10 km this works out to ~0.2°, roughly a third of the Sun's diameter.
  • Apparent transit line (dashed) connecting five samples of the aircraft–body relative position at ±60, ±30 and 0 s around closest approach, with an arrowhead in the direction of motion. Body drift is subtracted per sample, so the line shows the path as seen through a tracking mount keeping the disc centred.

The sketch is built client-side from a small transitPath array that the tracker attaches to every TransitCandidate. History rows written before v0.6.0 can still be pinned, but without the motion line (the disc + aircraft anchor point are derived from the existing payload_json).

Settings panel (v0.7.0+)

The header now exposes a ⚙ Settings button that opens an in-browser form for the three configuration areas you actually touch in the field:

  • Observer — name, latitude, longitude, elevation, plus optional temperature and pressure for the refraction model.
  • Pushover — app token, user key, device, master enable + minimum stage. Token and user key are stored on the Pi but never echoed back in plaintextGET /api/config returns them as ••••<last4> so a page reload (or a forgotten browser tab) cannot leak the secret. Leaving the masked value untouched on save keeps the existing credentials.
  • Telescope & sensor — focal length, sensor width/height in mm, pixel count, and a free-text sensor name. The FOV preview pane picks the new optics up on the next state poll, no reload needed.
  • TrackerhorizonS, thresholdDeg, looseThresholdDeg, minAltitudeM, minBodyElevationDeg (v0.30.37+). All hot-reload.
  • AirNav Radar API — bearer token (masked after save).
  • SharpCap capture trigger — toggle, host/port for the main rig, per-rig list (targets[]) with body / pre-buffer / post-buffer / maxSepDeg / minElevationDeg overrides. Hot-reload + Test trigger button per rig.

(The dump1090-status link in the header is hardcoded to http://<host>:8080/ since v0.15.2 — the configurable "External links" field that used to be here was removed in M46.)

Saved changes hot-reload the running service in place and are written back to config/observer.json + config/service.json so the next restart (including the nightly auto-update timer) keeps the new values.

Upgrading from a pre-v0.7.0 install: older stp.service units only listed data/ under ReadWritePaths, so saving from the Settings panel fails with EROFS: read-only file system. Refresh the systemd unit on the Pi with one of:

# Option A — re-run the installer (preserves existing configs):
bash scripts/install-pi5.sh --non-interactive

# Option B — drop-in override, no reinstall needed:
sudo systemctl edit stp.service        # opens an empty override
# Paste these three lines, save and exit:
#   [Service]
#   ReadWritePaths=
#   ReadWritePaths=/home/<user>/sun-moon-transit-predictor/data \
#                  /home/<user>/sun-moon-transit-predictor/config
sudo systemctl daemon-reload
sudo systemctl restart stp.service

Hot-reload of the in-memory state still works even when the disk write fails — the Settings panel just shows the actionable hint as a warning so you see exactly what to fix.

Tracking-list persistence across restarts (v0.7.0+)

The unified tracking panel is snapshotted to data/lifecycle.json every 30 s and on SIGTERM. On startup the file is read back so the panel does not appear empty after the auto-update timer restarts the service overnight. Entries whose predicted closest-approach time is already more than 10 minutes in the past are dropped on load to keep the panel meaningful; restored live entries are marked stale until the next tick reaffirms them.

What is persisted, what is not. The History panel reads from <repo>/data/history.db (SQLite, see src/store.js), which is written server-side every time the notifier dispatches a stage — both early and precise rows. Closing the browser does not lose anything; the next load (even days later) re-reads the same DB file. What is not written is the live "candidate" stream (/api/state.candidates) — those rows are recomputed in memory each tick and only graduate to the DB if they trip a notification. If you want every detected near-miss persisted, you would need to call store.recordEvent from the tracker tick rather than only from the notifier — happy to add that as a config switch if useful.

E-paper display (optional, v0.31.0)

A standalone client that drives a Waveshare 4.2" B/W SPI e-paper panel (400×300) on the Pi 5 for a browserless at-a-glance readout — no browser, no monitor needed:

A fixed three-paragraph layout with large, legible body text:

  • Header (two lines) — big bold clock + date on line 1; place + GPS on line 2; a compact Sky-now (Sun/Moon elevation) in the top-right
  • Nearest plane — the nearest tracked plane in detail, with ETA and SEP as the big bold headline figures and route/bearing/distance/altitude/ speed small underneath, plus a large FOV preview on the right
  • Recent + aircraftRECENT learned transits on the left (flight, how long ago, achieved SEP) and the live tracked aircraft with big SEP / ETA per plane (right); the heading carries a (candidates / total live) counter

Planes come from the unified live-tracking list, so the panel keeps showing nearby traffic even when nothing is a Real candidate.

The client lives in display/ and carries no logic of its own — it polls the predictor's /api/state, so it can render data from this Pi or a remote Pi on the LAN.

Configured entirely from the browser

Open ⚙ Settings → E-paper display and the client picks the changes up live (within a few seconds, no restart):

Setting Meaning
Enabled Master on/off. Off → the panel clears once and idles.
Data source URL Blank = this Pi (localhost). Set a LAN URL like http://192.168.1.50:8081 to drive a local panel from a remote ADS-B/Node host.
Quick refresh (s) Partial-refresh cadence — fast, flash-free text update. Default 2, floor 1.
Long refresh (s) Full-refresh cadence — the periodic brief flash that clears e-paper ghosting. Default 60, must be ≥ Quick.

The on-panel layout itself is fixed (the three paragraphs above) — there are no list-length or compact toggles.

Audio buzzer (optional)

A piezo buzzer wired between a GPIO pin (default GPIO13) and GND gives an audible transit countdown — driven by display/buzzer.py in the same client (no extra service), configured from ⚙ Settings → Audio / buzzer. The client always PWM-drives the pin, so it works for both passive (needs a frequency) and active (beeps on power) buzzers — no need to know which you have. Confirm yours and find the loudest tone with cd display && python3 epaper_client.py --test-buzzer (2000 Hz is the default). A Test signals button in Settings plays the whole sequence once on the Pi. Defaults: a rising 3-tone chord (3 × 0.1 s, +200 Hz/step) when a new Real candidate comes within 2 min, a falling chord (3 × 0.1 s, −200 Hz/step from 500 Hz) when one is lost/passes, an accelerating countdown for candidates within 0.3°2 × 0.05 s every 10 s from 60 s, 5 s from 15 s, 2 s from 8 s — then a rising chord at the transit itself. Beep length (down to 20 ms), count, frequency, frequency step, fade-out, intervals and windows are all adjustable in Settings. See display/README.md.

E-paper isn't a video display. A full refresh flashes for a couple of seconds; a partial refresh is quick (~0.3–0.5 s) but builds up ghosting. The two-cadence design (quick partial + periodic full clear) gives a lively yet clean panel; ~1–2 s is the practical floor for the quick refresh.

Quick install (Pi 5)

SPI, not I2C — the 4.2" panel plugs onto the 40-pin header and talks SPI. The Pi 5's new GPIO needs Waveshare's gpiozero/lgpio driver (the old RPi.GPIO does not work on the Pi 5).

Easiest — let the installer do everything (enables SPI, installs the Python panel libraries, adds the spi/gpio groups, installs + enables the service):

# fresh box, with the panel, in one line:
curl -fsSL https://raw.githubusercontent.com/joergs-git/sun-moon-transit-predictor/main/scripts/bootstrap-pi5.sh | bash -s -- --with-display

# already have the repo:
bash scripts/install-pi5.sh --with-display

Then reboot once (so SPI + group membership take effect) and turn the panel on in the web UI: ⚙ Settings → E-paper display → Enabled → Save.

Manual install (if you'd rather not use the flag)
# 1. Enable SPI, then reboot.
sudo raspi-config nonint do_spi 0 && sudo reboot

# 2. Python deps + group access for the panel.
pip3 install -r display/requirements.txt
sudo usermod -aG spi,gpio "$USER"

# 3. Install + start the service (fills in install dir + user).
sudo cp systemd/stp-display.service /etc/systemd/system/
sudo sed -i "s|__INSTALL_DIR__|$(pwd)|g; s|__USER__|$USER|g" /etc/systemd/system/stp-display.service
sudo systemctl daemon-reload
sudo systemctl enable --now stp-display.service

# 4. Turn it on in the web UI: ⚙ Settings → E-paper display → Enabled → Save
journalctl -u stp-display -f

Full wiring table, the two-Pi (remote source) setup, the --dry-run hardware-free preview and troubleshooting are in display/README.md.

Pushover setup & test push

A fresh checkout has no config/service.json — only config/service.example.json. Without a service config the Pushover client runs in disabled mode (enabled: false) and silently no-ops every send. That's safe for first-boot but means nothing will alert until you provide credentials.

1. Provide credentials

scripts/install-pi5.sh prompts for your Pushover application token and user key the first time it runs and writes them into config/service.json. To re-do it later:

bash scripts/install-pi5.sh --overwrite

Or edit config/service.json directly:

"pushover": {
  "token":   "azGD…<your app token>",
  "user":    "uQiR…<your user/group key>",
  "device":  "",
  "enabled": true,
  "minStage": "radio",
  "radioThresholdDeg": 1.0
}

device is optional — leave empty to fan out to every device on the account. minStage controls which stages dispatch at all (radio = all, imminent = only the ±30 s alert). radioThresholdDeg adds a tighter Pushover-only filter on top: the tracker still surfaces matches inside tracker.looseThresholdDeg (2° by default) to the tracking panel, but the phone only buzzes when the projected minimum separation is at or below this value (default — i.e. only flights likely to actually graze the body). Restart the service (sudo systemctl restart stp.service) after editing, or use the Settings panel in the web UI for hot-reload.

Alert learning

The UI surfaces a rolling 14-day stats panel showing how well the early-warning radio stage predicts the tight transits that actually matter. Each history row gets one of three outcome tags:

  • graduated — radio alert paid off: the flight later reached candidate or imminent.
  • faded — radio alert never tightened up: false positive of the early stage.
  • surprise — candidate or imminent fired with no prior radio warning. Useful to spot under-detected geometries.

Headline numbers in the panel:

  • hit rate = radioGraduated / radioFired — how often a radio alert was worth paying attention to.
  • surprise rate = surprises / (graduated + surprises) — how often we missed an early heads-up for a transit that actually fired.

Same data is available raw via GET /api/learning?windowDays=….

2. Send a test push

A small helper ships in scripts/test-push.js. It loads the live config/service.json and sends a single low-priority message via the same PushoverClient the notifier uses, so it verifies token, user key, network, and TLS in one shot.

node scripts/test-push.js
node scripts/test-push.js "custom message"      # optional payload

Expected output: pushover: sent (status=1, request=…). The push should land on every Pushover-equipped device within a couple of seconds. If the config is disabled or missing keys, the script prints pushover: disabled and exits 1 without contacting the API.

3. Verify in production

To confirm the live service can actually reach Pushover (not just the helper), tail the journal while temporarily lowering thresholdDeg in config/service.json to a wide value (e.g. 30) and restarting — the next overhead aircraft will then trip both an early and a precise notification. Restore the threshold afterwards:

sudo systemctl restart stp.service
journalctl -u stp.service -f | grep -iE 'push|notif'

Satellite transits — ISS, Hubble & Tiangong (v0.9.0; multi-sat v0.32.0)

Satellites are predicted alongside aircraft and shown in LIVE-TRACKING-SIGNALS, History and the FOV preview, in front of both the Sun and the Moon, with their own cyan highlight + 🛰 badge (and a small satellite glyph instead of an aircraft silhouette in the sketch).

Since v0.32.0 this is no longer ISS-only. Three satellites ship as defaults — the ISS, the Hubble Space Telescope (HST) and the Chinese Tiangong station (CSS / Tianhe core) — each running the identical prediction pipeline. The prediction is the easy part; whether you can actually photograph the result differs enormously between them (see Can you actually photograph it? below).

Target NORAD shows as apparent size (overhead) difficulty
ISS 25544 🛰 ISS ~50″ long the easy one — resolvable as a shape
Tiangong (CSS) 48274 🛰 CSS ~20″ next-easiest after the ISS
Hubble (HST) 20580 🛰 HST ~5″ × 1.6″ hard — see the numbers
  • Offline, dependency-free. Position comes from an embedded SGP4 propagator (src/sgp4.js, validated against the official Spacetrack #3 / Vallado 88888 verification vectors) applied to a local TLE file — the running service never touches the network for this.

  • The TLEs. Each satellite stays inactive until its TLE exists on disk (data/iss.tle, data/hst.tle, data/tiangong.tle). Since v0.10.3 scripts/install-pi5.sh does an initial fetch and installs a daily stp-tle.timer (05:40 ± 20 min, Persistent) — so on a normal Pi install the satellite info just appears and stays fresh. Re-run install-pi5.sh once if you upgraded from < v0.10.3.

    # see / force a refresh:
    systemctl list-timers | grep stp-tle
    node scripts/refresh-tle.js          # → data/{iss,hst,tiangong}.tle (Celestrak)
    node scripts/refresh-tle.js /path/iss.tle   # legacy: ISS only, explicit path
    systemctl start stp-tle.service      # same, via the timer's unit

    A TLE older than ~3 days noticeably degrades transit timing; the daily timer keeps it current. One satellite being momentarily unreachable does not fail the refresh of the others. No network at install time? The timer retries — or run the command above once you're online.

  • Tuning (config/service.json → iss): horizonMs (how far ahead to scan for the next Sun/Moon transit, default 14 days — these are weeks apart at a fixed site; raising it costs more CPU per recompute since the scan is O(horizon)), visibleHorizonMs (next-visible-pass cap, default 30 days; cheap — the scan returns at the first pass found), recomputeMs (scan cadence, default 10 min), thresholdDeg / looseThresholdDeg. The extra satellites live in the iss.satellites array — each { name, tag, catnr, tlePath, typeDesc, enabled }; set enabled: false (or drop the entry) to switch one off, add your own by NORAD catalogue number. Set the whole block's "enabled": false to switch satellites off entirely.

  • A satellite transit is written to History like any transit and feeds the Disc xing column (its angular rate is huge, so the full-disc crossing time is well under a second — see the numbers below).

  • Pushover (v0.10.0). Satellite transits ride the same notifier path as aircraft, so you get a heads-up the moment a Sun/Moon transit is predicted (and again ±30 s before). Titles read 🛰 ISS Sun transit predicted … (or HST / CSS). Disable per the usual pushover settings.

  • Next visible pass (v0.10.0, ISS only). The Sky-now panel shows the next naked-eye ISS pass for the site — station above 20°, sky dark (Sun below −6°, "after dusk") and the ISS sunlit (offline cylindrical Earth-shadow test). It is a visibility line, independent of any disc transit. (HST and Tiangong get transit prediction but not this naked-eye-pass widget.)

  • Alert-learning hit/surprise/graze rates are an ADS-B-traffic quality signal and therefore exclude satellite rows (a deliberately-hunted orbital event would otherwise skew them); the satellites still appear in the History table itself.

Can you actually photograph it? — the numbers

A satellite transit lasts well under a second and the target is tiny. Whether it is worth chasing comes down to three numbers: apparent size, your plate scale (arcsec per pixel) and the seeing. Here is the maths, worked out, so you can judge your own rig.

1 — Apparent size = physical size ÷ distance. An object of length L at slant range R subtends L / R radians (× 206 265 = arcsec):

Target length / diameter typical range overhead at 30° elevation
ISS 109 m / 73 m 420 km 54″ / 36″ 32″ / 21″
Tiangong 37 m 390 km ~20″ ~12″
Hubble 13.2 m / 4.2 m 535 km 5.1″ / 1.6″ 3.0″ / 1.0″
a typical 2 m satellite 2 m 600 km 0.7″ 0.4″

So Hubble is ~10× smaller than the ISS. That single fact is why the ISS resolves into a recognisable shape and HST barely does.

2 — Plate scale decides if the shape is resolved. Plate scale = 206 265 × pixel_size / focal_length (arcsec/px). For the project's reference camera (ASI174MM, 5.86 µm pixels) at the documented 714 mm effective focal length: 206265 × 0.00586 / 714 ≈ 1.69″/px. Hubble at 5.1″ is then ~3 px long, ~1 px wide — a smudge, not a telescope shape:

focal length arcsec/px ISS (54″) Tiangong (20″) HST (5.1″)
500 mm 2.42″ 22 px 8 px 2.1 px
714 mm (reference) 1.69″ 32 px 12 px 3.0 px
1500 mm 0.81″ 67 px 25 px 6.3 px
2800 mm 0.43″ 125 px 47 px 11.8 px

To see Hubble as a shape you want ~0.4–0.8″/px → 1500–2800 mm focal, where it becomes 6–12 px. But then the seeing (atmospheric blur, typically 1–3″) becomes the hard limit: there is no point sampling at 0.4″/px if the air smears everything to 2″. Excellent, steady seeing (≤ 2″) is mandatory for HST; the ISS shrugs the same seeing off because it is 50″ to begin with.

3 — The transit is brutally short. All three sweep the sky at roughly 0.7–0.8°/s, so they cross a 0.5° solar/lunar disc in about:

0.5° ÷ 0.75°/s ≈ 0.67 s — typically 0.5–1 s.

That means a high frame rate is non-negotiable: at 30 fps you capture ~20 frames across the whole disc and maybe 1–2 while the object is actually on it; at 100–200 fps you get a usable burst. A global-shutter mono camera (the ASI174MM is one) at full speed on a cropped ROI is the right tool.

How precisely is the transit point predicted?

This is the question that decides a long-focal-length, narrow-FOV attempt. Two independent error sources, from the SGP4 + TLE propagation:

  • Timing (along-track). A fresh (< 1 day) TLE has ~1 km of along-track error; at the satellite's ground speed that is only ~0.13 s of timing error — negligible against a 0.5–1 s transit. Fix: record a 2–4 s high-fps window centred on the prediction and the timing error vanishes inside the buffer.
  • Position on the disc (cross-track). This is the hard one. The angular error at your eyepiece = position_error / slant_range. At ~500 km range a 1 km cross-track TLE error is 1/500 rad ≈ 6.9′ ≈ 23 % of the 0.5° disc — so a fresh TLE tells you reliably whether it transits and roughly where on the disc, but not the exact chord. And the centre-line corridor on the ground (where you'd see a central transit) is only disc_radius × range ≈ 0.25° × 500 km ≈ 4–5 km wide.

Because that error grows 1–3 km/day, a prediction more than ~3 days out (3–9 km drift > corridor width) can flip transit ↔ miss with each TLE refresh. That is exactly why this tool only pushes/logs a transit inside iss.notifyWithinMs = 72 h (see the next section) — and it is physics, not a bug.

The fundamental trade-off. Longer focal length resolves the satellite (more px on target) but shrinks the FOV — at 2000 mm the ASI174MM frame is only ~0.32° × 0.20°, smaller than the disc: Moon-only, a limb section, and the object must thread that exact window. Widen the FOV to make the catch easier and the object shrinks back to a few pixels. You cannot beat this optically; the professionals beat it operationally:

  1. Freshest possible TLE (hours old) → corridor known to ~1 km.
  2. Be mobile — drive onto the predicted centre line (to a few hundred metres) for a central transit. A fixed rig essentially waits for a corridor to pass over its own spot.
  3. Frame just wide enough to absorb the residual error, then crop — fewer pixels on target, but a guaranteed catch.
  4. Or pick the easy targets first: ISS (~50″), then Tiangong (~20″), where pixels-on-target are generous and seeing is forgiving.

This tool gives you the when / which body / separation for your fixed location with the 72 h trust window — for all three satellites. It does not yet tell you how far the centre line misses you by (a possible future "centre-line offset" feature).

Good to know — satellite transit prediction is only reliable a few days out

SGP4 propagated from a TLE drifts roughly 1–3 km/day cross-track (more after an ISS reboost). The transit centre line is only ~4–5 km wide and the Sun/Moon disc is 0.5°, so a transit predicted > ~3 days ahead is essentially noise: it appears, then vanishes after the next daily TLE refresh (and a different phantom may appear). This is physics, not a bug, and applies to all three satellites equally.

Consequences in this tool (v0.10.9+):

  • A transit only fires Pushover and gets a History row once it is within iss.notifyWithinMs (default 3 days / 72 h) — close enough that SGP4+TLE is trustworthy. This stops phantom-transit alert spam and the "⚡ surprise" pollution it caused in the learning stats.
  • The Sky-now "Next ISS Sun/Moon transit" line still previews the soonest predicted transit even weeks out, but anything beyond the notify window is shown flagged "tentative — refines with each daily TLE". So Sky-now saying "none in the next N days" while an old, now-stale row sits in History is expected — they reflect different TLEs at different times, each correct for its own.
  • Visible passes (the other Sky-now line) are unaffected — they recur ~daily and the next one is near, so it stays accurate.
  • Want it sooner/later anyway? Tune iss.notifyWithinMs in config/service.json. Reliable horizon for sub-disc accuracy is roughly ≤ 48–72 h; keep the TLE fresh (the daily stp-tle.timer).

Good to know — observer coordinates & elevation

  • latitudeDeg / longitudeDeg are decimal degrees, WGS84 (e.g. 52.2870, 7.4223). There is no aviation-vs-astronomy datum difference — ADS-B, AirNav and this tool all use WGS84. The same point just has several notations; mixing them up is the usual confusion:
    • 52°17'13.7"N = degrees-minutes-seconds52 + 17/60 + 13.7/3600 = 52.2871° decimal.
    • 52.1714 is not decimal degrees — it is the packed aviation/NMEA "degrees + decimal-minutes" form (52°17.14') ≈ 52.2857°. Putting that into latitudeDeg lands you ~13 km off. Use the decimal form (your phone GPS / Google-Maps right-click at the antenna gives it directly), and use the same value for the rbfeeder /AirNav station so the feed and the predictor agree.
  • elevationM is the WGS84 ellipsoidal height of your site — in practice your local height above sea level is fine (the geometry is robust to a few tens of metres of observer height). It is not "height above ground" and not the antenna's height over the roof — just the site elevation (Rheine ≈ 40–50 m), never 0.
  • geoidUndulationM is a separate field: EGM2008 N at the site (Rheine ≈ +46 m). It only corrects aircraft barometric altitude (≈ MSL) to ellipsoidal before the geometric comparison — it is not applied to your own elevation. Set it (~46 for Rheine) for the best aircraft- altitude accuracy; 0 is tolerable.

Good to know — how far can you actually see an aircraft, and the elevation rule

There is no single hard distance. How far a plane stays usable depends on its size, how the Sun lights it (a sunlit fuselage or a contrail carries far further than a shaded belly), the ground visibility (aerosol / humidity haze) and the atmospheric seeing on the day.

Rule of thumb for clear North-German air, for good visual recognition of the airframe (wings, engines, type) — not mere detection:

  • 8–10× binoculars: the airframe shape is clearly recognizable out to roughly 30–40 km slant distance, and the type is still guessable to ~20–25 km. Mere detection of a jet or its contrail reaches much further — 60–80 km+.
  • Small telescope at ~40–80×: more geometric detail (~30–50 km), but it is turbulence-limited — at low elevation the unsteady air smears the image before haze ever does.

Why elevation dominates everything. Both the slant distance to a cruising airliner (~11 km altitude) and the amount of hazy, turbulent air you look through scale with 1 / sin(elevation). The slant range is simply R = h / sin(el). Below ~20° the line of sight grazes the worst — the haziest, most turbulent low air — and clouds often sit right on the horizon. ≥ 30° is the practical sweet spot; ≥ 45° is best.

This is why the predictor (v0.15.0):

  • shows a 3-state visibility traffic-light per row — red below 30°, amber 30–45°, green ≥ 45° (aircraft elevation at closest approach);
  • by default only sends Pushover notifications when the target is ≥ 30° elevation (configurable via pushover.minElevationDeg; set 0 to disable the gate). The ISS is exempt — it has its own 15° visibility gate. History and all statistics still record everything, regardless of the notify gate.
Elevation Slant range @ ~11 km Rel. air mass Visual usability
20° ~32 km ~2.9 usable but shimmery, weak contrast
30° ~22 km ~2.0 practical entry point — much steadier
45° ~15.5 km ~1.4 very good
60–90° 11–13 km ~1.0–1.15 optimal (also best for transit photos)

Sky-target passes — shoot a satellite crossing a deep-sky object (v0.34.0+)

A second, optional mode: instead of a satellite crossing the Sun or Moon, predict + auto-capture a satellite (ISS / HST / Tiangong) crossing the framed field of a deep-sky object, star or planet — then composite that frame with a separately-stacked background. The two layers are shot decoupled: the background is a normal long exposure / stack; the predictor only needs the satellite's path through the field and the exact trigger time.

Opt-in. It ships off — turn it on under ⚙ Settings → Sky targetsEnabled. The Sun/Moon transit prediction for ISS/HST/Tiangong always runs regardless; this adds the deep-sky-field scan.

The Sky-target plan ("Drehbuch")

When enabled, a Sky-target plan panel appears at the top of the page: a time-sorted timeline of every upcoming satellite × object pass.

Column Meaning
When predicted closest-approach time
Object the framed sky target
Sat 🛰 ISS / HST / CSS (Tiangong)
Type transit (through the object/disc) vs field (within the framed FOV)
El satellite elevation at closest approach
Miss closest approach to the field centre (arcmin / °)
In field how long the satellite stays in the frame
Conf. prediction confidence — see below

Confidence (🟢🟡🟠🔴) comes from the TLE age at the event: a far-future pass uses elements that will be days stale by then, so its cross-track position is uncertain to kilometres → arcminutes in a narrow field. The daily TLE refresh shrinks this as the event nears, so a 🟠 grob event days out becomes 🟢 sicher closer in. Thresholds: 🟢 < 1 d · 🟡 1–3 d · 🟠 3–6 d · 🔴 > 6 d (the ISS is capped at 🟡 beyond ~2 d because its monthly reboosts invalidate SGP4 abruptly). A ⚠ flags two events too close to catch with one scope; 🌑 marks a satellite in Earth's shadow (not sunlit → invisible).

Two views (toggle "next opportunity"):

  • Horizon (default) — every pass in the next plan horizon days.
  • Next opportunity — the soonest pass per object × satellite across the full scan horizon, so you can see when each object's chance first comes into reach even if it's still weeks out.

These events naturally cluster in twilight: the satellite must be sunlit and the sky dark to be a visible point, which only overlaps near dusk/dawn.

Pointing the scope — "🔭 Scope target"

A pulldown in the header (between the title and the clock, shown when the SharpCap trigger is enabled) sets what your telescope is pointed at:

  • Auto / Sun / Moon — the usual aircraft & ISS disc-transit capture.
  • A sky object (only those with an upcoming pass appear) — the SharpCap trigger then arms on that object's satellite passes instead, at the predicted T-0 with your pre/post-roll. Your selection is remembered across restarts.

The catalogue (Settings → Catalog)

The objects scanned live under ⚙ Settings → Catalog as an editable table — ID · Name · RA(h) · Dec(°) · Ø(°) · Planet — so you can see exactly what's computed and add/remove/disable entries. Ships with a curated ~44-object list: the 20 brightest stars + 20 largest/brightest DSO across both hemispheres (LMC/SMC, η Carinae, ω Cen, 47 Tuc, M31, M42, Pleiades, Double Cluster, M8/M13 …) + the bright planets. Coordinates are J2000 (the engine precesses them); the elevation gate hides whatever never rises for your latitude.

Plan-alerts (Pushover)

Optionally (Settings → Sky targets → Plan alerts) get a Pushover the moment a planned pass first reaches a confidence threshold ("this transit is firming up") — separate from the live transit alerts, edge-triggered (one push per event, restart-safe), with object · satellite · exact time · miss · lead · confidence.

Reality check — imaging difficulty

Prediction is the easy part. Resolving the satellite against a star field is hard: HST is only ~5″, so on a ~700 mm rig it's a ~3 px smudge; you want ~1500–2800 mm + sub-2″ seeing + ≥100 fps for the < 1 s crossing. The ISS (~50″) and Tiangong (~20″) are far more forgiving — start there. See the numbers above.

Clone this wiki locally