# `Sidereon.GNSS.RTCM`
[🔗](https://github.com/neilberkman/sidereon-ex/blob/main/lib/sidereon/gnss/rtcm.ex#L1)

RTCM 3.x differential-GNSS stream decoding.

RTCM 10403.x is the dominant wire format for real-time GNSS correction and
observation streams. This module is a thin wrapper over the `sidereon-core`
`rtcm` sans-I/O decoder: a forgiving frame layer that syncs on the `0xD3`
preamble and verifies the CRC-24Q, and a canonical message decoder.

`decode_messages/1` scans a whole byte buffer and returns every CRC-valid
message; `decode_frame/1` decodes the single frame at the start of a buffer.

## Message format

Each decoded message is a `{type, fields}` pair where `type` is one of
`:station_coordinates` (1005/1006), `:antenna_descriptor` (1007/1008/1033),
`:gps_ephemeris` (1019), `:glonass_ephemeris` (1020), `:beidou_ephemeris`
(1042), `:qzss_ephemeris` (1044), `:galileo_fnav_ephemeris` (1045),
`:galileo_inav_ephemeris` (1046), `:msm` (MSM4/MSM7 observations), or
`:unsupported` (any other number, preserved verbatim). The
`fields` map carries the raw transmitted integer fields; station coordinates
additionally carry the scaled `:x_m` / `:y_m` / `:z_m` / `:antenna_height_m`
values.

# `message`

```elixir
@type message() :: {message_type(), map()}
```

# `message_type`

```elixir
@type message_type() ::
  :station_coordinates
  | :antenna_descriptor
  | :gps_ephemeris
  | :glonass_ephemeris
  | :beidou_ephemeris
  | :qzss_ephemeris
  | :galileo_fnav_ephemeris
  | :galileo_inav_ephemeris
  | :msm
  | :unsupported
```

# `decode`

```elixir
@spec decode(binary()) :: {:ok, [message()]} | {:error, term()}
```

Decode every CRC-valid RTCM 3 frame in a byte buffer.

# `decode_frame`

```elixir
@spec decode_frame(binary()) ::
  {:ok, %{message_number: integer(), frame_len: integer(), body: binary()}}
  | {:error, term()}
```

Decode the single RTCM 3 frame that begins at the start of `bytes`.

Verifies the preamble and the CRC-24Q. Returns
`{:ok, %{message_number: n, frame_len: bytes, body: binary}}` or
`{:error, reason}` for a missing preamble, a truncated buffer, or a CRC
mismatch. The body can be fed back through `decode_messages/1` after re-framing.

# `decode_message`

```elixir
@spec decode_message(binary()) :: {:ok, message()} | {:error, term()}
```

Decode one RTCM message body.

# `decode_messages`

```elixir
@spec decode_messages(binary()) :: {:ok, [message()]} | {:error, term()}
```

Decode every CRC-valid RTCM 3 frame in a byte buffer.

Frames whose CRC fails or whose body cannot be decoded are skipped, and the
scan resynchronizes on the next preamble (the forgiving stream contract for a
noisy feed). Returns `{:ok, [{type, fields}, ...]}` in stream order, or
`{:error, reason}`.

# `decode_stream`

```elixir
@spec decode_stream(binary()) ::
  {:ok,
   %{
     messages: [message()],
     diagnostics: %{resync_bytes: non_neg_integer(), skipped_frames: [map()]}
   }}
  | {:error, term()}
```

Decode every CRC-valid RTCM 3 frame and return messages plus stream diagnostics.

The message list matches `decode_messages/1`. Diagnostics include skipped
resynchronization bytes and CRC-valid frames whose typed body decode failed.

# `derive_lli`

```elixir
@spec derive_lli(integer() | nil, integer() | nil, integer() | nil, boolean()) ::
  non_neg_integer()
```

Derive the RINEX LLI digit for one signal cell.

Pass `elapsed_ms` as `nil` for the first observation. When `elapsed_ms` is an
integer, `previous_min_lock_time_ms` may be `nil` to represent a previous
reserved indicator.

# `encode`

```elixir
@spec encode(message()) :: {:ok, binary()} | {:error, term()}
```

Construct a supported RTCM 3 message and return its message body.

# `encode_frame`

```elixir
@spec encode_frame(message() | binary()) :: {:ok, binary()} | {:error, term()}
```

Construct a supported RTCM 3 message and return its complete frame.

# `encode_message`

```elixir
@spec encode_message(message()) :: {:ok, binary()} | {:error, term()}
```

Construct a supported RTCM 3 message from a `{type, fields}` pair and encode it
into a complete transport frame (preamble, length, body, CRC-24Q).

`type` is one of the supported `message_type/0` atoms (`:unsupported` cannot be
constructed). `fields` is a map of the raw transmitted fields, the same shape
`decode_messages/1` produces for that type (the derived `:x_m`/`:y_m`/`:z_m`
station outputs are ignored when present, so a decoded map round-trips
directly). Returns `{:ok, frame_binary}` or `{:error, reason}`.

The output frame feeds back through `decode_messages/1`, so
`construct -> encode -> decode` reproduces the same message fields.

# `lli_bits`

```elixir
@spec lli_bits() :: %{loss_of_lock: 1, half_cycle: 2}
```

Return the RINEX LLI bit constants used by the RTCM MSM derivation helpers.

# `message_number`

```elixir
@spec message_number(binary()) :: {:ok, integer()} | {:error, term()}
```

Return the message number from one RTCM message body.

# `minimum_lock_time_ms`

```elixir
@spec minimum_lock_time_ms(String.t(), integer()) ::
  {:ok, non_neg_integer() | nil} | {:error, term()}
```

Minimum continuous-lock time in milliseconds for an MSM lock indicator.

`kind` is `"msm4"` or `"msm7"`.

# `msm_epoch_dt_ms`

```elixir
@spec msm_epoch_dt_ms(String.t(), integer(), integer()) ::
  {:ok, non_neg_integer()} | {:error, term()}
```

Elapsed milliseconds between two raw MSM epoch-time fields for one system.

# `msm_lli`

```elixir
@spec msm_lli([map()]) :: {:ok, [[map()]]} | {:error, term()}
```

Derive per-cell LLI rows by running a core lock-time tracker over MSM maps.

The input is a list of decoded MSM field maps, in stream order. The result is
one list of `%{satellite_id, signal_id, lli, min_lock_time_ms}` maps per input
message.

# `msm_signal_rinex_code`

```elixir
@spec msm_signal_rinex_code(String.t(), integer()) ::
  {:ok, String.t() | nil} | {:error, term()}
```

RINEX observation-code suffix for an MSM signal id, or `nil` if reserved.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
