Skip to content

symysak/ntrip-caster

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ntrip-caster

NTRIP (Networked Transport of RTCM via Internet Protocol) caster written in Go.

Features

  • NTRIP v1 and v2 for both clients (rovers) and servers (base stations).
    • Client pull: GET /<mountpoint> (ICY 200 OK for v1, chunked gnss/data for v2).
    • Server push: SOURCE <pw> /<mp> (v1) and POST /<mp> with Basic auth (v2).
    • Sourcetable on GET / or for unknown/offline mountpoints.
  • Handover endpoints. A client connects to a virtual endpoint and streams its NMEA GGA; the caster subscribes it to the nearest online member base station and re-switches as the client moves. Members are grouped manually in config so each endpoint serves one RTCM flavor (e.g. RTCM 3.0, 3.2 MSM4/5/7).
  • Username/password authentication for both clients and servers (passwords stored in plaintext, per the chosen configuration model). Individual mountpoints/handover endpoints can opt into anonymous client access with open: true (server push auth is unaffected).
  • Hot reload via systemctl reload (SIGHUP): client users, mountpoint definitions, and base-station metadata/handover groups apply without dropping live connections. Changing the listen address requires a restart.

Build

go build -o ntrip-caster ./cmd/ntrip-caster

Run

cp config.example.yaml config.yaml   # edit credentials and mountpoints
./ntrip-caster -config config.yaml
./ntrip-caster -config config.yaml -check   # validate config and exit

Architecture

Package Responsibility
internal/config YAML load, validation, hot-reload snapshot.
internal/caster Runtime hub: mountpoints, source fan-out, subscribers.
internal/server NTRIP v1/v2 wire protocol, auth, dispatch, handover.
internal/handover Nearest-base selection (haversine) from a GGA fix.
internal/nmea NMEA GGA parsing.
internal/sourcetable Sourcetable (CAS/STR) rendering.

Data flow: an NTRIP server attaches as the single source of a mountpoint; its bytes are copied and broadcast to every subscribed client. Slow clients whose queue overflows are disconnected rather than served a corrupted stream.

Data model

The central concept is the mountpoint: a named relay hub that ties together one Source (the base station currently pushing) and N Subscribers (the clients currently reading). A Subscriber is one reader's mailbox — a buffered chan []byte — and what it is subscribed to is a mountpoint. The Manager owns every mountpoint and the live config snapshot.

flowchart LR
    subgraph MGR["caster.Manager (all mountpoints + config)"]
        subgraph MP["Mountpoint &quot;TOKYO&quot;"]
            SRC["Source<br/>(1 base station, push)"]
            SUBS["Subscribers<br/>(N readers, each a chan []byte)"]
            SRC -->|"Broadcast(chunk)"| SUBS
        end
    end
    SERVER["NTRIP server (push)"] -->|"SOURCE / POST"| SRC
    SUBS -->|"streamToClient"| C1["client #1"]
    SUBS -->|"streamToClient"| C2["client #2"]
Loading

Role mapping:

Type Role in one sentence
caster.Manager Holds all mountpoints and the current config snapshot.
caster.Mountpoint One relay hub = one Source + N Subscribers.
caster.Source The single base station currently pushing into a mountpoint.
caster.Subscriber One reading client's receive channel + done signal.
server.* Speaks the NTRIP wire protocol and wires connections to the above.
server.handoverSession A client whose subscription is moved between mountpoints by position.

Handover then reads simply: it moves one subscriber's mountpoint from, say, TOKYO to OSAKA as the client moves — the connection and channel stay the same.

Processing flow

Connection dispatch (server.handleConn)

Every connection is one raw TCP socket. handleConn peeks the first bytes and routes by request kind; versionOf then picks v1/v2 framing from the Ntrip-Version header.

flowchart TD
    A["handleConn<br/>peek first bytes"] -->|"SOURCE …"| B["handleSourceV1<br/>(v1 push)"]
    A -->|"HTTP POST"| C["handleSourceV2<br/>(v2 push, Basic auth)"]
    A -->|"HTTP GET"| D["handleClient"]

    B --> E["runSource"]
    C --> E
    E --> F{"AttachSource<br/>already has a source?"}
    F -->|yes| G["409 / ERROR — reject"]
    F -->|no| H["ack, then read loop:<br/>copy chunk → Broadcast"]

    D --> I{"lookup path"}
    I -->|"root / unknown / offline"| J["writeSourcetable"]
    I -->|"auth fails"| K["401 Unauthorized"]
    I -->|"mountpoint"| L["handleMountpoint"]
    I -->|"handover group"| M["handleHandover"]
Loading

Source → client fan-out (caster)

One source feeds many clients. Broadcast non-blocking-sends each chunk to every subscriber's buffered channel; a subscriber that falls behind is dropped.

flowchart LR
    SRV["NTRIP server"] -->|"RTCM bytes"| RS["runSource<br/>read loop"]
    RS -->|"copy chunk"| BC["Mountpoint.Broadcast"]
    BC --> Q1["(sub ch #1)"]
    BC --> Q2["(sub ch #2)"]
    Q1 --> W1["streamToClient → rover #1"]
    Q2 --> W2["streamToClient → rover #2"]
    BC -. "buffer full" .-> X["drop slow client"]
Loading

Handover switching (server.handoverSession + handover.Selector)

handleHandover builds a handoverSession (in handover_session.go) whose single control loop owns the client connection. The active member changes as GGA fixes arrive or the current source drops, so the client connection survives switches; it is closed only when no member is online.

A per-group switch_margin_km applies hysteresis to avoid flapping at a boundary: once attached, a client only switches to a base that is more than the margin closer than its current one. A source disconnect bypasses the margin (immediate failover to the nearest remaining member).

sequenceDiagram
    participant R as Rover
    participant H as handleHandover
    participant Sel as handover.Selector
    participant MP as Mountpoints
    R->>H: GET /AUTO_… (v1/v2)
    alt no member online
        H-->>R: SOURCETABLE (fail fast)
    else at least one online
        H-->>R: stream OK header
        loop control loop
            R->>H: $GxGGA (lat, lon)
            H->>Sel: Nearest(group, lat, lon)
            Sel-->>H: nearest online member ("" if none)
            alt nearest == ""
                H-->>R: disconnect
            else member changed
                H->>MP: Subscribe(new) / drop old
            end
            MP-->>R: RTCM from nearest base
            Note over H,MP: active source drops → re-select<br/>(next-nearest) using last fix
        end
    end
Loading

Configuration

See config.example.yaml. Reloadable keys: client_users, mountpoints, handover.

systemd

See systemd/ntrip-caster.service.

sudo cp ntrip-caster /usr/local/bin/
sudo install -d /etc/ntrip-caster && sudo cp config.yaml /etc/ntrip-caster/
sudo cp systemd/ntrip-caster.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now ntrip-caster
# After editing the config:
sudo systemctl reload ntrip-caster

Limitations / notes

  • A handover connection fails immediately (sourcetable response) when no member base station is online. If the active base drops mid-stream, the client is automatically re-routed to the next-nearest online member using its last known position; the connection is closed only when no member remains online.
  • Passwords are plaintext by design of the current config model.
  • NTRIP v2 over TLS is not yet implemented (terminate TLS with a reverse proxy if required).

About

NTRIP v1/v2 caster in Go with nearest-base handover endpoints, per-mountpoint auth, and zero-downtime config reload.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages