The 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 is RTKNTRIPService 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:
1

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.
2

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).
3

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.
4

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).
5

Start streaming

The RTCM callback is set to _handle_rtcm (which forwards to inject_rtcm), start_receiving() spawns the RTCM thread, and the GGA sender thread is started. The main loop then idles until a SIGINT/SIGTERM clears the run flag.

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 than 180 × 6 = 1080 bytes are dropped.
  • The flags byte 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 len field carries the real byte count.
Because the NTRIP parser hands over one RTCM3 frame at a time (typically well under 360 bytes), most frames become one or two GPS_RTCM_DATA messages. The injected bytes travel udpout:14560 → mavp2p → serial → FCU, and ArduPilot applies them to the connected RTK GPS.
mavp2p is a full-mesh router: because RTK connects to it, the injected RTCM reaches the FCU and RTK receives GLOBAL_POSITION_INT / GPS_RAW_INT back over the same 14560 endpoint. If RTK corrections stop working, first confirm mavproxy is up and that its command line still lists udps:0.0.0.0:14560 (docker-compose.yml:139). Removing that endpoint silently kills RTK.

GGA feedback (FCU → caster)

The GGA sender thread (main.py:147) runs once per second:
  1. update_gps_position() reads the latest GLOBAL_POSITION_INT (lat/lon/alt) and GPS_RAW_INT (fix_type) from the FCU.
  2. NTRIPClient.send_gga() builds an NMEA $GPGGA sentence from that position with an XOR checksum, but only actually transmits it when there is a fix (gps_fix > 0) and at least GGA_INTERVAL seconds (default 10) have elapsed since the last send.
This periodic position report is what lets a network-RTK/VRS caster compute a base solution for the drone’s location. Without it, the caster may drop the connection or serve stale corrections.

Configuration

Most configuration is read once at boot into RTKConfig (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.
VariableDefaultPurpose
NTRIP_HOSTeu.l1l5.skylark.swiftnav.comNTRIP caster hostname (Swift Skylark)
NTRIP_PORT2101Caster TCP port
NTRIP_MOUNTPOINTRTK-MSM5Correction stream (MSM5 = highest precision, multi-constellation)
NTRIP_USERNAMEsee caveat belowCaster username
NTRIP_PASSWORDsee caveat belowCaster password
RTK_MAVLINK_CONNECTIONudpout:127.0.0.1:14560MAVLink client endpoint into mavp2p
GGA_INTERVAL10Seconds between GGA position reports
SOCKET_TIMEOUT30NTRIP socket timeout (s)
GPS_FIX_TIMEOUT60Max wait for an initial GPS fix (s)
STARTDELAY (from RTK_STARTDELAY)10Boot delay before the client starts
LOG_DIR/home/skycore/skyhub_core/logsHost 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
Credentials-in-code caveat. src/shared/config.py:16-17 ships real Skylark caster credentials as fallback defaults — a hardcoded username (idrobots+idrobots) and a live password (redacted here as <redacted>). In a normal Compose deployment these are overridden by the .env values (which ship as your_username / your_password placeholders), but running main.py directly with no env — including in tests or ad-hoc debugging — will authenticate with the committed production credentials. Treat these as a secret to rotate/scrub. Do not print them in logs or issues, and do not paste the working curl -u ... line from the service README into shared channels.
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_typeMeaningTypical accuracy
0 / 1No GPS / no fix
22D fix
33D fix (no corrections)2–5 m horizontal
4DGPS / SBAS~1 m
5RTK Float0.5–1 m (30–60 s after corrections start)
6RTK Fixed1–3 cm (2–5 min, clear sky view)
The same 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

# from the repo root on the drone
docker compose build rtk-ntrip
docker compose up -d rtk-ntrip     # requires the mavproxy container running
docker compose logs -f rtk-ntrip
# healthy output progresses: heartbeat -> GPS fix -> "ICY 200 OK"
# -> "Sent GGA: $GPGGA,..." -> "RTCM data flowing: N bytes, M frames injected"
docker compose restart rtk-ntrip
Quick triage when RTK will not converge:
  • 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 OK never appears — bad caster host/port/mountpoint or credentials; verify network egress from the drone to NTRIP_HOST:NTRIP_PORT.
  • GPS_RTCM_DATA flowing 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 mavp2p still exposes udps:0.0.0.0:14560 and that RTK_MAVLINK_CONNECTION points at it.

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.