Skip to content

feat(realtime): poll HTTP telemetry sources and publish vehicle positions - #61

Open
dotjae wants to merge 17 commits into
mainfrom
feat/fetch-telemetry
Open

feat(realtime): poll HTTP telemetry sources and publish vehicle positions#61
dotjae wants to merge 17 commits into
mainfrom
feat/fetch-telemetry

Conversation

@dotjae

@dotjae dotjae commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Add a pluggable telemetry source adapter registry (realtime_engine/sources/) with a generic HTTP+JSON adapter driven entirely by a Sensor's source_http_url + source_json_mapping — no per-provider code needed for feeds that fit the mapping schema.
  • Add fetch_positions Celery task: polls ACTIVE HTTP/both sensors every 10s, filters to in-service vehicles, and republishes positions onto transit/vehicle/<id>/position (the same MQTT topic/consumer used by direct MQTT telemetry).
  • Extend operations.Sensor with source_type/source_http_url/source_json_mapping fields to configure per-sensor HTTP sources.
  • Gate the in-service vehicle set on vehicle:<id>:current_run (same signal the MQTT consumer uses) rather than runs:in_progress, which deadlocked HTTP-only vehicles — a run only reaches IN_PROGRESS once telemetry proves movement, and that telemetry is exactly what this task delivers.
  • operations/migrations is now regenerated at container start like the repo's other apps (was previously force-committed, missing from the regen list).

Dependency note

This branch is stacked on the still-open #60 (feat/eta-stop-times), so the diff currently includes those commits too. Please merge #60 first — once it lands on main, this PR's diff will automatically collapse to just the fetch-telemetry commits.

Test plan

dotjae and others added 17 commits June 25, 2026 12:13
Vendored inference half of the ETA model lifecycle (canonical training source
lives in gtfs-django/eta_prediction): estimator, feature engineering, and the
model registry loader, with heavy deps slimmed (xgboost is an optional extra).

The registry resolves model/metadata paths relative to its directory, so it is
relocatable: bind-mountable, checked-in placeholders, or models written by an
external retraining suite all load regardless of MODEL_REGISTRY_DIR. Wired into
databus as an editable uv workspace member.

See backend/gtfs-eta/README.md for provenance and extraction intent.
Deterministic synthetic baseline (polyreg_distance, global) so the stop-time
producer runs end-to-end on a fresh checkout without a trained model. Kept as a
standalone commit so it is trivial to drop once the retraining suite supplies
real models. Regenerate with: python -m gtfs_eta.seed_baseline_model.
Replace the fake_stop_times placeholder with a real producer that calls
gtfs_eta.estimate_stop_times. Pure/impure split: compute_stop_time_updates
derives contract entries from run state + shape geometry; produce_stop_times
does the Redis I/O. Upcoming stops come from the monotonic shape geometry and
distances feed the estimator's precomputed-distance hook, fixing duplicate
stop_sequences and the non-decreasing upcoming count. Builder sorts/dedups
StopTimeUpdate entries defensively. New config: MODEL_REGISTRY_DIR,
ETA_MAX_STOPS, ETA_DEFAULT_UNCERTAINTY_S.
Cover the pure helper, both bug regressions (no duplicate stop_sequence;
non-increasing upcoming count), output-adapter edges, and the impure producer's
Redis read/skip/write guards.
Superseded by the real ETA producer. Removes the fabricated stop-time generator
and its static route_stops.csv (whose 0-based sequences surfaced an off-by-one).
Generates the pending schema for the Sensor model (moved provides_*
flags off Equipment/EquipmentLog) plus the Company.linked_agency
ManyToMany conversion and Vehicle.status SOLD removal already present
in models.py.
The telemetry publisher/consumer read these from the environment but
they were never declared in the env files.
Introduces realtime_engine/sources/ — a registry of source adapters that
fetch vehicle position data and normalize it into the shared position
telemetry contract:

- transforms.py: unit conversions (km/h->m/s, km->m) and Costa Rica local
  timestamp parsing ported from navsat-bridge, plus a dotted-path getter.
- base.py: SourceAdapter protocol and a string-keyed adapter registry.
- http_json.py: a generic HTTP+JSON adapter (kind="http") driven entirely
  by a sensor's source_http_url/source_json_mapping, so new HTTP feeds
  (e.g. NavSat) need no per-provider code.
- publisher.py: paho-mqtt v2 publishing of normalized positions to
  transit/vehicle/<id>/position, matching the existing ingestion consumer.
DB-free unit tests for realtime_engine/sources/: unit conversions and
timestamp parsing, the HTTP+JSON adapter (array and single-object bodies,
unit conversion, malformed-record skipping, vehicle_id resolution from the
mapped path vs. sensor fallback, and contract compliance with
position.validate_for_write), and the MQTT publisher (topic/payload shape,
paho v2 client setup, batch publish/disconnect, per-message error handling).
Sensors are faked with SimpleNamespace and requests.get/paho are
monkeypatched — no HTTP, MQTT, or database access.
Polls ACTIVE Sensor rows configured for HTTP position feeds (source_type
"http" or "both"), fetches readings via the pluggable adapter registry,
keeps only readings for vehicles currently running (per runs:in_progress),
and publishes the survivors on transit/vehicle/<id>/position. Per-sensor
failures are caught and logged so one bad source can't sink the poll.
Wires the new HTTP telemetry poll into beat alongside the other
realtime_engine periodic tasks.
fetch_position, fetch_and_publish, and update_gtfs_realtime were an early
sketch superseded by the pluggable HTTP source adapters and the new
realtime_engine.tasks.fetch_positions task. Removes the now-unused
requests/paho/Vehicle/chord/group imports along with them; the legitimate
builder tasks are untouched.
operations/migrations was force-committed against the repo's convention of
gitignoring migrations/ and regenerating them at container start, and it
was missing from that regen list. Untracks the committed migration (it stays
on disk, now gitignored like its siblings) and adds "operations" to
APPS_TO_MIGRATE in docker-entrypoint.sh.
Building the in-service set from runs:in_progress deadlocked HTTP-only
vehicles: a run only enters IN_PROGRESS once telemetry proves the vehicle
is moving, but that telemetry is exactly what fetch_positions delivers, and
it refused to publish until the run was already IN_PROGRESS. Gate instead on
vehicle:<id>:current_run presence -- the same signal the MQTT consumer uses
to accept telemetry -- so a CONFIRMED run bootstraps forward.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants