rtk-ntrip service is the on-drone RTK (Real-Time Kinematic) corrections client. It reproduces Mission Planner’s RTK-injection behavior autonomously on the Jetson companion computer: it connects to an NTRIP caster, streams down RTCM3 correction data, and injects it into the ArduPilot flight controller as MAVLink GPS_RTCM_DATA. In return it feeds the vehicle’s own position back to the caster as NMEA GGA sentences, which is how a network-RTK caster (like Swift Navigation’s Skylark) picks the nearest base-station solution. With corrections flowing, an RTK-capable GPS (e.g. CubePilot Here4) moves from a 2–5 m 3D fix to centimetre-level RTK Fixed.
It is one profile-gated container in the SkyCore stack (docker-compose.yml, profile rtk), running network_mode: host alongside mavproxy, core, and gamepad. It is entirely optional — a drone without RTK hardware simply omits the rtk profile — and it never runs under SITL.
The service does not talk to the FCU serial port directly. It is a plain MAVLink UDP client of the single
mavp2p router (see MAVLink Routing), which owns /dev/ttyACM0 and fans traffic out to every consumer. RTK owns the dedicated endpoint udps:0.0.0.0:14560.Data path
Two loops run concurrently: an RTCM receiver thread (caster → FCU) and a GGA sender thread (FCU → caster). The service coordinator isRTKNTRIPService in docker/rtk-ntrip/main.py, which wires two lifecycle-managed modules — MAVLinkModule (src/modules/mavlink/) and NTRIPModule (src/modules/ntrip/).
Startup sequence
RTKNTRIPService.start() (docker/rtk-ntrip/main.py:57) runs a strict ordered bring-up and exits non-zero if any step fails:
Boot delay
entrypoint.sh waits STARTDELAY seconds (mapped from RTK_STARTDELAY, default 10 in .env.example) before exec’ing python3 /app/main.py. This lets the GPS and mavproxy come up first.Connect MAVLink
MAVLinkHandler.connect() opens RTK_MAVLINK_CONNECTION (default udpout:127.0.0.1:14560), sends an initial MAV_TYPE_GCS heartbeat to punch through the mavp2p UDP endpoint, waits for a heartbeat back, then requests the MAV_DATA_STREAM_POSITION stream at 1 Hz (src/modules/mavlink/handler.py:44).Wait for a GPS fix
wait_for_gps_fix(GPS_FIX_TIMEOUT) polls GPS_RAW_INT for up to 60 s. A non-zero fix_type is enough to proceed; the service logs a warning but continues even if the timeout elapses.Connect the NTRIP caster
NTRIPClient.connect() opens a TCP socket and sends an NTRIP-over-HTTP/1.0 request: GET /<mountpoint> with Authorization: Basic <base64(user:pass)>. It succeeds only if the response contains 200 OK or ICY 200 OK (src/modules/ntrip/client.py:61).RTCM injection (caster → FCU)
The NTRIP socket delivers a continuous byte stream.NTRIPClient._parse_rtcm_frames() buffers it and splits it into complete RTCM3 frames by scanning for the 0xD3 preamble and reading the 10-bit payload length (total frame = 3-byte header + payload + 3-byte CRC-24Q). Each fully-formed frame is passed individually to the callback so it can be sized correctly for MAVLink.
MAVLinkHandler.inject_rtcm() (src/modules/mavlink/handler.py:134) then fragments each frame into GPS_RTCM_DATA messages, matching Mission Planner’s exact framing:
- Max 180 bytes of payload per message (
RTCM_MAX_PAYLOAD); frames larger than180 × 6 = 1080bytes are dropped. - The
flagsbyte packs: bit 0 = fragmented flag, bits 1–2 = fragment index (0–3), bits 3–7 = a 5-bit sequence id (0–31, incremented per frame and wrapped mod 32). - Each chunk is zero-padded to a full 180-byte payload; the
lenfield carries the real byte count.
GPS_RTCM_DATA messages. The injected bytes travel udpout:14560 → mavp2p → serial → FCU, and ArduPilot applies them to the connected RTK GPS.
GGA feedback (FCU → caster)
The GGA sender thread (main.py:147) runs once per second:
update_gps_position()reads the latestGLOBAL_POSITION_INT(lat/lon/alt) andGPS_RAW_INT(fix_type) from the FCU.NTRIPClient.send_gga()builds an NMEA$GPGGAsentence from that position with an XOR checksum, but only actually transmits it when there is a fix (gps_fix > 0) and at leastGGA_INTERVALseconds (default 10) have elapsed since the last send.
Configuration
Most configuration is read once at boot intoRTKConfig (src/shared/config.py) and passed through docker-compose.yml; env values in the compose file take precedence over the code defaults. The last two rows below are not RTKConfig fields — STARTDELAY is consumed by entrypoint.sh, and LOG_DIR is a Compose-level host path the Python code never reads.
| Variable | Default | Purpose |
|---|---|---|
NTRIP_HOST | eu.l1l5.skylark.swiftnav.com | NTRIP caster hostname (Swift Skylark) |
NTRIP_PORT | 2101 | Caster TCP port |
NTRIP_MOUNTPOINT | RTK-MSM5 | Correction stream (MSM5 = highest precision, multi-constellation) |
NTRIP_USERNAME | see caveat below | Caster username |
NTRIP_PASSWORD | see caveat below | Caster password |
RTK_MAVLINK_CONNECTION | udpout:127.0.0.1:14560 | MAVLink client endpoint into mavp2p |
GGA_INTERVAL | 10 | Seconds between GGA position reports |
SOCKET_TIMEOUT | 30 | NTRIP socket timeout (s) |
GPS_FIX_TIMEOUT | 60 | Max wait for an initial GPS fix (s) |
STARTDELAY (from RTK_STARTDELAY) | 10 | Boot delay before the client starts |
LOG_DIR | /home/skycore/skyhub_core/logs | Host log directory; ${LOG_DIR}/rtk-ntrip is mounted to /var/log/skyhub in the container (docker-compose.yml:167), where main.py writes timestamped rtk-ntrip_*.log files |
Stale README defaults.
docker/rtk-ntrip/README.md still documents MAVLINK_CONNECTION | udpin:127.0.0.1:14550 and STARTDELAY 25. The live code uses the env var RTK_MAVLINK_CONNECTION defaulting to udpout:127.0.0.1:14560, and Compose sets STARTDELAY from RTK_STARTDELAY (10). Trust src/shared/config.py and docker-compose.yml over the README table.Interpreting GPS fix types
fix_type comes straight from ArduPilot’s GPS_RAW_INT. The RTK convergence you care about is types 5 and 6:
fix_type | Meaning | Typical accuracy |
|---|---|---|
| 0 / 1 | No GPS / no fix | — |
| 2 | 2D fix | — |
| 3 | 3D fix (no corrections) | 2–5 m horizontal |
| 4 | DGPS / SBAS | ~1 m |
| 5 | RTK Float | 0.5–1 m (30–60 s after corrections start) |
| 6 | RTK Fixed | 1–3 cm (2–5 min, clear sky view) |
fix_type surfaces to operators through the Gateway’s telemetry streams (GPS is part of the dashboard stream — see Socket.IO Telemetry Streaming).
Operating the service
- No GPS fix — the service waits up to 60 s then proceeds anyway; check that the RTK GPS is wired to the FCU and reporting via
mavproxy. - NTRIP
200 OKnever appears — bad caster host/port/mountpoint or credentials; verify network egress from the drone toNTRIP_HOST:NTRIP_PORT. GPS_RTCM_DATAflowing but stuck at Float — expected during convergence; needs clear sky view and a few minutes. Confirm GGA is being sent (the caster needs your position to serve local corrections).- Nothing reaches the FCU — confirm
mavp2pstill exposesudps:0.0.0.0:14560and thatRTK_MAVLINK_CONNECTIONpoints at it.
Related
MAVLink Routing (mavp2p)
The single MAVLink hub and its port map (14560 is RTK’s endpoint).
Microservices & Container Profiles
Where rtk-ntrip sits among the SkyCore Compose services and profiles.
ArUco Precision Landing
A downstream consumer of accurate positioning for RTL/AUTO auto-docking.
Isaac Visual SLAM
The GPS-denied alternative when RTK/GPS is unavailable.

