-
Notifications
You must be signed in to change notification settings - Fork 0
Usage
🛰 Sun-Moon Transit Predictor wiki — Home · Setup · Usage · Advanced · Reference · ↩ README
- Web UI
- E-paper display (optional, v0.31.0)
- Pushover setup & test push
- Satellite transits — ISS, Hubble & Tiangong
- Sky-target passes — satellite × deep-sky object
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 forimminent/candidateso urgent rows draw the eye. Polls/api/stateevery 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.
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 1° — 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).
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 plaintext —
GET /api/configreturns 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.
-
Tracker —
horizonS,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 triggerbutton 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.serviceunits only listeddata/underReadWritePaths, so saving from the Settings panel fails withEROFS: 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.serviceHot-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.
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.
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 + aircraft — RECENT 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.
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.
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.
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.GPIOdoes 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-displayThen 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 -fFull wiring table, the two-Pi (remote source) setup, the --dry-run
hardware-free preview and troubleshooting are in
display/README.md.
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.
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 --overwriteOr 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 1° — 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.
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=….
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 payloadExpected 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.
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'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.3scripts/install-pi5.shdoes an initial fetch and installs a dailystp-tle.timer(05:40 ± 20 min, Persistent) — so on a normal Pi install the satellite info just appears and stays fresh. Re-runinstall-pi5.shonce 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 theiss.satellitesarray — each{ name, tag, catnr, tlePath, typeDesc, enabled }; setenabled: false(or drop the entry) to switch one off, add your own by NORAD catalogue number. Set the whole block's"enabled": falseto 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 …(orHST/CSS). Disable per the usualpushoversettings. -
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.
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.
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 is1/500rad ≈ 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 onlydisc_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:
- Freshest possible TLE (hours old) → corridor known to ~1 km.
- 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.
- Frame just wide enough to absorb the residual error, then crop — fewer pixels on target, but a guaranteed catch.
- 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).
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.notifyWithinMsinconfig/service.json. Reliable horizon for sub-disc accuracy is roughly ≤ 48–72 h; keep the TLE fresh (the dailystp-tle.timer).
-
latitudeDeg/longitudeDegare 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-seconds →52 + 17/60 + 13.7/3600= 52.2871° decimal. -
52.1714is not decimal degrees — it is the packed aviation/NMEA "degrees + decimal-minutes" form (52°17.14') ≈ 52.2857°. Putting that intolatitudeDeglands 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.
-
-
elevationMis 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), never0. -
geoidUndulationMis 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;0is tolerable.
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; set0to 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) |
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 targets → Enabled. The Sun/Moon transit prediction for ISS/HST/Tiangong always runs regardless; this adds the deep-sky-field scan.
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.
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 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.
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.
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.