Add transjakarta-live-data research topic

Verified TransJakarta realtime stack: anonymous MQTT bus feed (wss://mqtt.tj.co.id:8084/mqtt), guest REST API (tijeapi.transjakarta.co.id), official GTFS. Cross-capture verification vs third-party tracker, connection recipes, snapshot tool.
This commit is contained in:
hermes 2026-10-12 09:34:23 +00:00
commit 3a518be39f
7 changed files with 306 additions and 0 deletions

View file

@ -0,0 +1,49 @@
# Analysis — architecture and verification evidence
## How the stack works
1. Buses carry GPS devices; vehicles are identified by fleet body numbers (`MYS-…`, `TJ-…`, `KWK-…`, `DMR-…`, `PRM-…`; the prefix appears to be operator/depot).
2. TransJakarta's backend ingests positions and publishes each bus to its MQTT broker; REST state backs the official app.
3. Client flow: login (guest works) → `POST /v1/bus` with your location → you receive nearby buses **plus MQTT topic wildcards for the map cells around you** → subscribe those on the broker → live positions stream in. REST covers routes/schedules; GTFS covers static geometry.
4. The upstream path from vehicle hardware into the broker is inferred — it is not observable from outside.
## The MQTT feed
- Broker: `wss://mqtt.tj.co.id:8084/mqtt` — MQTT 3.1.1 over WebSocket Secure, **anonymous** (no username/password; verified with independent clients).
- Topic schema: `/mobile_armada/<grid-cell>/<route-code>/<trip-id>/<bus-body-no>`
- Real captured example: `/mobile_armada/0021-0014/JAK.49/JAK.49-L03/KWK-1518`
- `<grid-cell>` = `{lat-index}-{lng-index}`, a ~0.01° (~1.1 km) map cell. Verified by probing the discovery API with offset coordinates: ±0.1° of latitude/longitude shifts the matching index by ±10 (index grows southward and eastward).
- Retained messages: subscribing delivers ~2,000+ last-known states within seconds, then live updates continue. Per-bus refresh observed at ~8–75 s (some active vehicles faster; many ~25–37 s); delivery is bursty.
- Fleet size observed live: 3,300–3,722 buses.
### Payload schema (complete key list, captured verbatim)
`latitude`, `longitude`, `snap_latitude`, `snap_longitude`, `bearing`, `estimated_time_next_stops`, `estimated_distance_next_stops`, `curr_stops`, `next_stops`, `prev_stops`, `passengers_status`, `route_code`, `route_long_name`, `route_color`, `route_text_color`, `trip_short_name`, `trip_headsign`, `bus_type`, `type`, `trip_id`, `bus_body_no`, `stops[]`, `timestamp`, `last_update`, `eta_by_parent_stop_id`, `next_parent_gate`, `next_parent_gate_eta`, `next_gate`, `next_gate_eta`, `direction`, `eta`.
- `stops[]` items: `name`, `stop_id`, `latitude`, `longitude`, `stop_sequence`, `time_from_prev`, `time_wait`, `headsign`, `parent_stop_id`, `parent_stop_name`, `eta`.
- `timestamp` is epoch milliseconds (e.g. `1791797549000`). Stop IDs (`B00436P`-style) match GTFS.
- Some fields are null for some vehicles (e.g. gate fields, `passengers_status`, `last_update`).
## The REST API
- `POST /v1/auth/login/guest` → `token` + `refresh_token` + `user{email: <random>@guest.tije.com, is_guest: true}`. Token lifetime ~24 h. Headers used: `User-Agent: okhttp/4.12.0`, `X-App-OS: android`, `X-App-Version: 2.10.2`, `X-Device-ID: <string>`.
- `POST /v1/bus` body `{latitude, longitude, radius, event: "change_loc"}` → `data { buses[], polyline, session_id, stops[], topics[] }`. `topics` = the MQTT wildcards for nearby cells (e.g. `/mobile_armada/0018-0022/#` …). This is the app's discovery mechanism.
- `GET /v1/bus?latitude=&longitude=&radius=` → array of nearby bus records (~15 at radius=1 near Monas; radius units unverified).
## Verification case: oyenza serves this same data
Simultaneous captures (node WS client on oyenza's stream + node MQTT client on TJ's broker):
- 1,794 buses common to both (of 1,823 seen on the broker in the same window).
- 100% agreement on `trip_id` and `route_code`; 99.7% on headsign.
- Position deltas: median 8.9 m, p90 42 m, 1,783/1,794 ≤ 100 m — consistent with reading the same moving buses a few seconds apart; no other explanation fits.
- Fleet overlap 97.8% over a 75 s window (2,869/2,933); remainder = capture-window skew.
- Category taxonomy maps 1:1, including the edge case: the app's whole "other" bucket (2 buses) = the only two untyped buses in the feed.
oyenza's own layer (what it adds): fleet-state backend; browser protocol `hello/feed/snap/delta` with positions packed as integers ×10⁵; deltas every 3.00 s (~1,000 position records each); full snapshot 3,722 buses; its own REST extras; a replay mode. Static layer from the official GTFS (240 routes, 698 shapes, 7,493 stops at capture). Its About panel states: "Posisi bus — feed langsung TransJakarta" and "Tidak berafiliasi".
## Confidence
- **Established (verified by direct capture):** guest token; both bus endpoints; anonymous MQTT access; topic schema + grid decoding; payload schema; retained behavior; GTFS file; Traccar-based bustrack; oyenza data identity.
- **Community-documented (not independently re-verified):** ~105 REST endpoint count; Tara chatbot WS; Kong gateway; refresh-token flow.
- **Inferred:** vehicle→broker ingest path; oyenza's exact subscription method (whole-tree vs per-cell — data identity holds either way); `radius` units.