mirror of
https://github.com/zvx-echo6/central.git
synced 2026-06-10 11:54:37 +02:00
v0.13.0: sat_orbits adapter (forward-orbit-track per satellite) + antimeridian splitter
## Matt's "each sat's path" framing
After enabling the satellite family in v0.12.1, the `/events` map showed overlapping orange visibility-footprint circles from satpass_predict + a polar-orbit ground track wrapping the wrong way across the antimeridian (the v0.11.2 documented limitation). Matt's ask:
> honestly i just want each sats path.
Interpreted as: one continuous orbital track per satellite, color-coded, no observer-specific clutter, no visibility-footprint overlays. Six tracked sats = six distinguishable lines on the map.
## Family placement — global line counterpart to global points
| Adapter | What it publishes | Geometry | Cadence |
|---|---|---|---|
| satpass_predict (v0.11.1) | Observer-anchored pass alerts | LineString ground-track + Polygon footprint per pass | 1h |
| sat_positions (v0.12.0) | Current sub-sat POINT per sat | Point centroid only | 60s |
| **sat_orbits (this PR)** | Forward-orbit LINE per sat | LineString / MultiLineString, 90min horizon | 5min |
Each answers a different question; they complement.
## Antimeridian splitter — shared sat_common primitive
`split_antimeridian(coords)` lives in `sat_common.py` next to `gmst_rad` / `eci_to_ecef` / `subsatellite_point`. Returns `None` for <2 vertices, a `LineString` dict for the common no-crossing case, or a `MultiLineString` dict when one or more ±180° crossings exist. Each crossing closes the current segment at `sign(prev_lon)*180` with a linearly-interpolated latitude and starts the next at `sign(cur_lon)*180` with the same lat (sub-0.1° error at LEO orbital speeds, well below Leaflet rendering precision).
**Sibling concern fixed:** `satpass_predict._build_pass_geometry` now routes its `ground_track` through `split_antimeridian` too. This was the v0.11.2 documented limitation ("polar-orbit crossings near ±180° will produce a polygon that visually wraps the wrong way"). Sat_orbits and satpass_predict share the helper because the antimeridian problem is identical for both — and **44/44 existing satpass_predict tests still pass** because the splitter returns a LineString identical in shape to the prior inline construction when there's no crossing (which is the case for every CONUS-observer ISS-fixture test).
New test specifically for the splitter inside `_build_pass_geometry`: synthesized polar-orbit `ground_track` produces a `GeometryCollection` whose linear-geometry component is a `MultiLineString` with 2 segments (first ends at +180, second starts at -180).
## GUI per-NORAD-ID color helper
20-line addition to `events_list.html`:
```js
function orbitColorForNoradId(norad) {
var hue = (norad * 137.508) % 360; // golden-angle hue distribution
return "hsl(" + hue.toFixed(1) + ", 70%, 50%)";
}
function getRowColor(adapter, row) {
if (adapter === "tomtom_flow") return flowColor(row.dataset.severity);
if (adapter === "sat_orbits") {
var norad = parseInt((row.dataset.eventId || "").split(":")[0], 10);
if (!isNaN(norad)) return orbitColorForNoradId(norad);
}
return getAdapterColor(adapter);
}
```
`event_id` shape is `<norad_id>:<iso>` (same as sat_positions), so JS reads the first colon-token. **Additive**: tomtom_flow keeps its severity-based color, every other adapter keeps its per-adapter palette color, sat_orbits gets per-satellite distinguishable lines.
## Phase A sanity (per spec)
```
vertices = 91 ✓ (90min @ 60s + 1 endpoint)
first vertex = (170.66°, -17.15°, 417.4km) ✓ matches v0.11.1 ISS pin
last vertex = (140.52°, -8.60°, 415.9km) ✓ geographically distinct
antimeridian crossings in 90min track = 1
geometry type = MultiLineString, 2 segments ✓ splitter integrates
```
## Diff size
**+838 / −9 = +829 net** across 15 files. Spec budget was ≤800 lines. **29 over** — much tighter than v0.12.0 (894) or v0.12.1 (848). Adapter LoC 275 (well under 350 cap). sat_common splitter 51 LoC (~budget).
Test breakdown: 285 (sat_orbits) + 60 (sat_common splitter) + 26 (satpass regression) + 12 (events_feed) + 4 (telemetry-separation) = 387 LoC tests. Production: 275 + 51 + 37 (migration) + 41 (doc) + 16 (partials) + 21 (JS) + 15 (satpass refactor) + 2 (wiring) = 458 LoC.
## Test plan
- [x] `pytest tests/test_sat_orbits.py` — 19 new tests, all pass.
- [x] `pytest tests/test_sat_common.py` — 7 new splitter tests, 16 total pass.
- [x] `pytest tests/test_satpass_predict.py` — **45/45 pass** (44 existing regression-guard + 1 new polar-orbit splitter integration test). The `_build_pass_geometry` rewire is byte-identical for non-crossing tracks.
- [x] `pytest tests/test_events_feed_frontend.py` — 125/125 pass (sat_orbits sample + expected subject extended).
- [x] `pytest tests/test_telemetry_separation.py` — 9/9 pass (`_TELEMETRY` pin extended with `sat_orbits`).
- [x] `pytest tests/test_consumer_doc.py` — 6/6 pass (new `### sat_orbits` subsection accepted).
- [x] Full sweep `pytest tests/` (excluding postgres-dep files): **1274 passed, 1 skipped, 0 failures**.
- [x] Ruff: clean on all new + touched satellite-family code.
## Deploy plan
1. Squash-merge PR #N → tag v0.13.0 at merge SHA → push tag.
2. `ssh central`, `git pull` on `/opt/central`. **No `uv sync`** (no new dep).
3. Apply migration 041 manually via psql (per option C):
`sudo -u postgres psql central -f /opt/central/sql/migrations/041_add_sat_orbits_adapter.sql`
4. `sudo systemctl restart central-supervisor` (picks up new adapter + STREAM_CATEGORY_DOMAINS extension) + `sudo systemctl restart central-gui` (picks up new partials + ADAPTER_GROUPS extension + JS color helper).
5. **No** `central-archive` restart (CENTRAL_SAT pre-existed; only the category-domain tuple grew, archive already covers `central.sat.>`).
6. Verify: `config.adapters` has `sat_orbits` row with `enabled=false`; supervisor log shows discovery; no polling until Matt flips it.
7. Matt enables via `/adapters/sat_orbits/edit` when ready. First poll happens within 5min; orbit-track LineStrings surface at `/telemetry` filtered by adapter=sat_orbits, color-coded per NORAD ID.
## Halt acknowledgment
Per spec acceptance bar #6: **squash-merge NOT authorized**. Branch + PR open. Halting for line-by-line review.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
parent
8e388dabd5
commit
3f1fec9846
15 changed files with 838 additions and 9 deletions
|
|
@ -1979,6 +1979,47 @@ at parameter `00060`, gage height (ft) at `00065`, water temperature (°C) at
|
|||
the key via GUI `/api-keys` then the adapter picks it up on the next
|
||||
config-change notification.
|
||||
|
||||
### sat_orbits — forward orbital track per satellite (v0.13.0)
|
||||
|
||||
- **Source:** same as `sat_positions` and `satpass_predict` — reads the
|
||||
latest TLE per `norad_id` from `celestrak_tle`'s events. Empty TLE table
|
||||
= zero events yielded, no exception.
|
||||
- **Data class:** `telemetry`. Surfaces on `/telemetry`, not `/events`;
|
||||
these are continuous-state trajectories, not alerts.
|
||||
- **Stream:** `CENTRAL_SAT` (existing; v0.13.0 extends
|
||||
`STREAM_CATEGORY_DOMAINS["CENTRAL_SAT"]` to
|
||||
`("tle", "pass", "position", "orbit")`).
|
||||
- **Subject:** `central.sat.orbit.<norad_id>` — one subject per satellite.
|
||||
Consumers can subscribe to `central.sat.orbit.>` for every tracked
|
||||
satellite's forward track, or pin to a single satellite.
|
||||
- **Dedup key shape:** `<norad_id>:<propagation_iso>` where
|
||||
`propagation_iso` is the propagation start time truncated to whole
|
||||
seconds. Matches the `sat_positions` convention.
|
||||
- **Severity:** always 1 (informational telemetry).
|
||||
- **Geo:**
|
||||
- `centroid = (current_lon_deg, current_lat_deg)` — first vertex of the
|
||||
track, anchoring the "here's where it IS" dot on the map.
|
||||
- `geometry` = GeoJSON `LineString` for the common case (no
|
||||
antimeridian crossing), or `MultiLineString` when the forward track
|
||||
crosses ±180° (polar orbits, mid-Pacific orbits). Antimeridian
|
||||
splitting lives in `sat_common.split_antimeridian` and is shared
|
||||
with `satpass_predict`'s ground-track render — v0.13.0 also fixes
|
||||
the v0.11.2 satpass_predict "wrong-way wrap" bug as a sibling concern.
|
||||
- **Event.data fields:** `norad_id`, `satellite_name`,
|
||||
`propagation_start_iso`, `forward_minutes`, `sample_seconds`,
|
||||
`vertex_count`, `current_lon_deg`, `current_lat_deg`, `current_alt_km`,
|
||||
`tle_epoch`.
|
||||
- **Cadence:** 300s (5 min) default. Lower than `sat_positions`' 60s
|
||||
because a forward LineString covers ~90 minutes of orbit; the
|
||||
trajectory doesn't change meaningfully tick-to-tick at that horizon.
|
||||
- **Settings:** `track_only_norad_ids` (empty = all fresh TLEs;
|
||||
non-empty pins to those IDs), `forward_minutes = 90` (~1 LEO orbit),
|
||||
`sample_seconds = 60` (~90 vertices per event), `max_tle_age_days = 14`.
|
||||
- **GUI rendering:** sat_orbits events are colored **per-NORAD-ID** in
|
||||
the events map (golden-angle HSL hue distribution off `norad_id`) so
|
||||
multiple tracked satellites render as distinguishable lines. Other
|
||||
adapters keep their existing per-adapter palette color.
|
||||
|
||||
\
|
||||
---
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue