NTRIP-Analyser

Analyses NTRIP RTCM 3.x streams from GNSS base stations, and answers whether a station is fit to serve RTK.

View the Project on GitHub pe1mew/NTRIP-Analyser

NTRIP-Analyser CLI manual

ntrip-analyser is the command-line application for connecting to NTRIP casters, retrieving mountpoint tables, and analyzing or decoding RTCM 3.x streams. It is designed for GNSS enthusiasts who need to inspect or debug NTRIP data streams.


Getting Started

0. General operation description

The program will emulate a rover using preconfigured (static) coordinates. At connection the program will authenticate and either request the mountpoint list of a NTRIP stream. When a NTRIP stream is opened, the program will send an GGA NMEA sentense with the configured coordinates at a 1 second interval to the NTRIP caster.

1. Configuration File (config.json)

A ready-to-edit example ships as bin/exampleConfig.json (and beside the binaries in each release). It points at the Dutch Kadaster open caster, including its ephemeris mountpoint for the sky plot. Its username and password are empty because those streams are served anonymously — it runs as shipped. --generate writes a template too. For a caster that does need credentials, they are yours to obtain; the analyser supplies none of its own.

Before using the program, you need a configuration file: it holds the connection and authentication details, and the coordinates of the emulated rover. The program will not work without one.

config.json is a default, not a requirement. It is simply the name the program looks for when -c is not given. -c takes any path, so name files after the stations they describe — ntrip-analyser -c rfsee.json --check — and keep a directory of them. Nothing requires the word “config”, and a file outside the working directory is often the better choice: credentials are stored in the clear, so /etc/ntrip/ with chmod 600 beats a file beside the binary.

Generating a default config.json with dummy values is done by using the -g argument when running the program:

ntripanalysis.exe -g

Example config.json:

{
    "mountpoints": [
        {
            "caster": "your.caster.example.com",
            "port": 2101,
            "tls": false,
            "mountpoint": "MOUNTPOINT",
            "username": "your_username",
            "password": "your_password",
            "send_gga": false,
            "latitude": 0.0,
            "longitude": 0.0
        }
    ]
}

Set the various parameters

The file is a list even when it holds one connection, because it is the same file the monitoring daemon and the Android app use. The CLI analyses one stream at a time, so it takes the first entry and says so when there are more:

[CONFIG] config.json lists 3 connections; using the first (RFSEE01) and ignoring the other 2.

An optional eph_caster / eph_port / eph_mountpoint block adds a second connection for ephemerides, for stations that do not broadcast their own on the observation stream. --sky opens it always; -t, -s, -d and --check open it only under --report, for the self-position. Files written by earlier releases, with NTRIP_CASTER and friends at the top level, are still read. The full description of the format is in jsonConfigs.md.


2. Command-Line Arguments

Run the program from the command line:

ntrip-analyser [options]

Options

Short Long Argument Description
-c –config [file] Specify config file (default: config.json)
-m –mounts   Show mountpoint (sourcetable) list and exit
-d –decode [types] Decode RTCM stream, optionally filter by comma-separated message numbers
-s –sat [seconds] count satellites received for N seconds (default: 60)
-t –types [seconds] Analyze message types for N seconds (default: 60)
-v –verbose   Print configuration and action details before running
  –check   Station acceptance test: eight KPIs, ~90 s (exit 0/6/1)
  –check-vrs   As –check plus the network-RTK assertions and GGA gate test
-g –generate   Geerate default config.json with dummy values and exit.
  –latitude value Override latitude in config
  –longitude value Override longitude in config
  –lat value Same as --latitude
  –lon value Same as --longitude
-h –help   Help information
-i –info   Information about the program, repository, an author
  –capture path Write every CRC-valid frame to a .rtcm3 file (see below)
  –capture-max MB Close the capture at this size and keep streaming; default no limit
  –reconnect   Reconnect with backoff if the stream drops
  –thresholds file Judge by this JSON policy instead of the built-in thresholds (see below)
  –thresholds-print   Print every threshold in force and where it came from, then exit

2ab. When a check ends without a verdict

A verdict is what held for sixty seconds, or a failure, which is conclusive at once. A run can end before either: the 300-second ceiling, the stream closing, or — under --check-vrs — the gate test answering before the eight checks have held their window. The report then says so rather than printing the live roll-up as though it were a conclusion:

== NO VERDICT ==  the 300 s limit was reached after 301 s  exit=6
The checks above are the last reading, not a conclusion: the verdict had not held for 60 s.

The rows above it are still worth reading — they are what was true when the run ended. They are simply not a verdict, and exit 6 says the same thing to a script.

2a. The stability report (--report)

The eight checks ask whether a station is fit now and answer in about ninety seconds. --report asks a different question — has it been fit, and is it staying that way — which no ninety-second window can answer at any price.

ntrip-analyser -t 3600 --report

Six measurements over the run: availability (reconnects per hour), frame integrity, signal level, satellites held, ionosphere and delivery rate. Each row carries its value, the limit it was judged against, and the evidence behind it, so a verdict can be argued with rather than merely read. The report ends with the worst finding stated with its evidence, rather than a bare word:

== DEGRADED over 6.0 h -- Frame integrity: worst 99.860 % of frames passed CRC in 10 min ==

Four things about it are deliberate:

Two numbers are measured as changes rather than levels, which is what makes them useful over hours. Signal level reports how far the mean C/N0 fell from the best the window saw — a station that drops 7 dB to 41 dB-Hz is flagged, though 41 is a perfectly good level, because something changed. Frame integrity reports the worst CRC rate observed, not the average, because an average hides a bad ten minutes inside a good six hours.

The window is measured in stream time, not by this computer’s clock. The report counts seconds from the observation epochs in the stream itself, which has three consequences worth knowing:

A stream carrying no observations at all — station and antenna messages only — has no clock to measure with, and the report says so rather than guessing.

Self-position is printed below the table and carries no verdict. The analyser solves the station’s own position from the observations it streams, and reports the offset from the reference position the station advertises, the scatter about that offset, the worst code residual and the median apparent motion.

Self-position (no threshold yet -- figures only), 3410 epoch(s) solved
    offset from reference   E +1.842  N -0.317  U +2.015 m
    scatter about that      0.934 m
    worst code residual     1.903 m RMS
    median apparent motion  32.5 mm/s

The motion is the median over the window, not the maximum. A velocity from broadcast orbits is noisy epoch by epoch: on bases bolted to pillars the maximum settled at 15–80 mm/s, set by whichever epoch happened to be noisiest, while the median sits steadily in the low tens of mm/s and a handful of wild epochs cannot move it. It is read from a histogram at 1 mm/s resolution, so it costs fixed memory however long the run.

None of it is graded, deliberately: what counts as abnormal scatter for a station’s own position is not yet known from evidence, and a threshold invented to fill the column would be worse than no column at all.

GPS only, deliberately. Each constellation keeps its own system time and reaches the receiver down its own hardware path, so each needs a clock unknown of its own; one unknown for all of them forces a compromise across the lot, and BeiDou adds a 14-second time-scale offset on top. Measured against a station streaming six systems, 25 satellites under a single clock unknown put the station 1.8 km from where it is. Eight to twelve dual-frequency GPS satellites answer the question this asks. Multi-GNSS returns when the solve has a clock unknown per system.

Read the offset as an offset. Most of it is the dual-frequency code solution’s own bias — metres of it — and it says nothing about the station. The scatter is the half that measures something, and the apparent motion is the figure that would reveal an antenna that moved.

Some receivers send the phase-range rate backwards — every satellite the negative of what its orbit predicts. The solver works out which convention the station uses from the data itself, so the motion is right either way, and a reversed station is reported in the JSON as selfpos_rate_sign: -1. Orbits come from the same three places as for the sky map. A station that broadcasts its own ephemerides needs nothing more. One that does not — RFSEE01 streams MSM7 and nothing else — needs an outside source, or the line reads Self-position: nothing solved -- observations without orbits however long the run:

A station streaming one frequency cannot be solved at all; the line then reads Self-position: nothing solved -- single-frequency station: not computable, which is a statement about the stream rather than a fault.

2b. Capturing the stream to a file

--capture writes the stream to disk so that a converter can read it hours later, or so the analyser can replay it offline. It works with any mode that opens an observation stream — -d, -t, -s, -S/--sky, --check, --check-vrs — and is refused, rather than ignored, by the modes that do not.

ntrip-analyser -t 86400 --reconnect --capture /var/spool/gnss/ -q

That is the unattended form: a day of stream, drops ridden out, into a directory. A directory argument gets the name the GUI proposes, YYYYMMDDHHmmss_<mountpoint>.rtcm3, so a folder of captures sorts by capture time and cron needs no unique name invented for it. A file path is used as given.

What lands on disk is frames only: no handshake, nothing that failed its CRC, none of the bytes between frames. So a capture is clean input to RTKLIB’s convbin by construction, and one written by the CLI is byte-for-byte the same as one written by the GUI.

Four behaviours worth knowing before leaving one running overnight:

Running it unattended, over SSH

A day-long capture outlives the SSH session that starts it. screen or tmux will do, but systemd is better here for one reason: stopping a unit sends SIGTERM, and the capture catches SIGTERM while capturing, so the file is closed and flushed rather than abandoned. One command, no unit file:

sudo systemd-run --unit=ntrip-capture \
  --property=StandardOutput=null --property=StandardError=journal \
  ntrip-analyser -c /etc/ntrip/config.json \
  -t 86400 --reconnect --capture /var/spool/gnss/ -q

Then journalctl -u ntrip-capture -f to look in on it, and sudo systemctl stop ntrip-capture to end it early with the file intact. -t 86400 stops by itself after a day in any case.

StandardOutput=null is not decoration. In -t mode the message-type stream goes to stdout and is not silenced by -q — some five tokens a second, which over a day fills the journal with 1077 1087 1097 1127 to no purpose. The capture summary and every error are on stderr, so nothing is lost.

StandardError=journal is what keeps that true. systemd’s StandardError= defaults to inherit, which means whatever StandardOutput is — so setting the one to null discards the other too. A six-hour capture run without it completed perfectly and reported nothing at all: no frame count, no byte count, no reconnect count, and no error had there been one. The two properties belong together.

Two figures for planning a long run: roughly 180 MB a day from a four-constellation MSM7 station at 1 Hz, and one reconnect per drop, each of which is a gap in the file rather than a truncation.

The summary prints even under -q, because the file is the result of the run rather than chatter about it:

[INFO] Capture: 159 frames, 62503 bytes -> /var/spool/gnss/20260815131024_RFSEE01.rtcm3

To turn one into a RINEX observation file — for a base-station declaration, say — see Declaring a base station.

2bb. Judging by your own thresholds

Every verdict rests on a number someone chose, and thresholds.md says which of them are well founded and which are starting points. --thresholds lets you disagree:

ntrip-analyser --thresholds-print                  # what am I judging by?
ntrip-analyser --thresholds survey.json --check    # judge by that instead

The policy is JSON and partial by design — it carries only what you change, and every key it omits keeps its built-in value, so the file does not rot as thresholds are added:

{
  "name": "hobby base",
  "tier1": { "min_cnr_median": 36.0, "expect_sats": { "gps": 6 } },
  "tier2": { "sats_warn": 20, "sats_bad": 12 }
}

bin/exampleThresholds.json is a worked example. Three behaviours are worth knowing:

The GUI and the monitoring service still use the built-in values.

2c. Reading a capture back

--rtcm-stdin reads the stream from standard input instead of opening a socket, so a capture goes back through the same framing, CRC, statistics and report as the live run that recorded it. It works with -d, -t, -s and -S/--sky.

ntrip-analyser -t 3600 --report --rtcm-stdin < 20260815131024_RFSEE01.rtcm3

Two things make that worth doing rather than merely possible:

Availability reads n/a. A file holds no arrival times and never drops, so a reconnection count taken from one would be an invention.

Where a replay and the live run disagree, the difference is the network — and that is information rather than error. In one 120-second run against a healthy station the live report recorded 29 satellites at its worst while the replay of that same session recorded 38, because the delivery stalled for seven seconds and the analyser could not see what had not yet arrived. The capture’s epochs are consecutive: the station never lost a satellite. A live run answers what am I being given; a replay answers what did the station send.

Modes that cannot honour the flag reject it rather than ignore it — --check among them, because tier 1 is a live acceptance test and a capture cannot answer questions about delivery.


3. Usage Examples


Using NTRIP analyser

1. Using -v (Verbose)

>ntrip-analyser.exe -t -v
=== NTRIP-Analyser Configuration ===
  Config file: config.json
  NTRIP_CASTER: somecaster.net
  NTRIP_PORT: 2101
  MOUNTPOINT: SOMEMOUNTPOINT
  USERNAME: SOMEUSERNAME
  PASSWORD: SOMEPASSWORD
  LATITUDE: 0.000
  LONGITUDE: 0.000
  Analysis time: 60
  Show mount table: no
  Decode stream: no
  Action: Analyze message types for 60 seconds
====================================

2. Test a NTRIP stream

>ntrip-analyser.exe -t
[INFO] Analyzing message types for 60 seconds...
1137 1077 1087 1097 1117 1127 1127 1137 1077 1087 1097 1117 1127 1127 1137 1077 1087 1097 
...
1127 1137 1077 1087 1097 1117 1127 1127 1137
[INFO] Message type analysis complete. Statistics:
+-------------+-------+---------------+---------------+---------------+
| MessageType | Count |  Min-DT (S)   |  Max-DT (S)   |  Avg-DT (S)   |
+-------------+-------+---------------+---------------+---------------+
| 1005        |     2 |        29.993 |        29.993 |        14.997 |
| 1077        |    60 |         0.985 |         1.017 |         0.983 |
| 1087        |    60 |         0.986 |         1.017 |         0.983 |
| 1097        |    60 |         0.986 |         1.016 |         0.983 |
| 1117        |    60 |         0.985 |         1.016 |         0.983 |
| 1127        |   120 |         0.001 |         0.988 |         0.496 |
| 1137        |    61 |         0.899 |         1.130 |         0.984 |
+-------------+-------+---------------+---------------+---------------+

Acceptance testing

--check answers one question: does this station meet the basic KPIs for RTK service? It watches the stream for about ninety seconds and prints an eight-row verdict.

ntrip-analyser --check
1 Connected and producing    PASS   1695.40  Authenticated, connected, data flowing
2 RTCM 3.x format            PASS    543.00  CRC-valid RTCM 3.x frames decoded
3 Reference position (ARP)   PASS      1.00  1005/1006 received with non-zero coordinates
4 Observations flowing       PASS      5.00  Every constellation streaming at 0.5 Hz or faster
5 Satellites in view         PASS     46.00  At or above what this station advertises
6 Median C/N0                PASS     45.02  Antenna and LNA chain healthy
7 Frame integrity (CRC)      PASS      0.00  Fewer than 1 error per 1000 frames
8 Advertised versus actual   WARN      1.00  Streaming a constellation the sourcetable omits

== CAUTION ==  exit=6

KPI 8 compares what the sourcetable promises against what arrives: message types, their rates, and the constellations in the NavSys field. Streaming something never advertised is an observation, not a fault — it warns rather than fails, because the data is right and only the metadata is wrong. Advertising a constellation that is not currently streamed is not even that: QZSS is advertised across Europe and visible from none of it.

The verdict, not each row, is what must hold: it has to stay unchanged for sixty continuous seconds before it is reported, so a station that flickers cannot pass by being briefly healthy at the right moment.

The exit code makes it scriptable — an installer’s sign-off, or a cron check:

Exit Meaning
0 STATION OK — all eight held for the full window
6 CAUTION — a marginal reading, or a soft KPI (satellite count, C/N0) failing
1 FAILED — a hard KPI failed: connectivity, format, ARP, multi-GNSS, or CRC

--check-vrs adds the network-RTK layer for mountpoints that expect the rover to upload its position. It sends a GGA every ten seconds from the config’s LATITUDE/LONGITUDE and adds five assertions: the caster accepts the GGA, corrections start within ten seconds of it, the broadcast reference position is near the rover, the stream holds at that cadence, and finally the gate test — the uplink stops and the caster’s reaction classifies the service:

V5  GGA gating   warn   91.00  Still streaming 90 s after GGA stopped: fixed base
== STATION OK ==  [service: not gated (fixed base?)]  exit=0

A physical base ignores GGA and keeps streaming, which is correct behaviour for what it is, so the gate result is reported as a classification rather than a failure.

The same verdict engine runs on every frontend (src/core/kpi.c, src/core/vrs_check.c), so a station cannot pass here and fail elsewhere.

Notes


License

The code is under the Apache License 2.0 with the Commons Clause; this documentation is under CC BY-NC 4.0. docs/licences.md sets out the full position, including what the project depends on and what it connects to.