Current section

Files

Jump to
sidereon lib sidereon gnss rtcm.ex
Raw

lib/sidereon/gnss/rtcm.ex

defmodule Sidereon.GNSS.RTCM do
@moduledoc """
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 shapes
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), `: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.
"""
alias Sidereon.NIF
@type message_type ::
:station_coordinates
| :antenna_descriptor
| :gps_ephemeris
| :glonass_ephemeris
| :msm
| :unsupported
@type message :: {message_type(), map()}
@doc """
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}`.
"""
@spec decode_messages(binary()) :: {:ok, [message()]} | {:error, term()}
def decode_messages(bytes) when is_binary(bytes) do
{:ok, NIF.rtcm_decode_messages(bytes)}
rescue
e in ErlangError -> {:error, e.original}
end
@doc """
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.
"""
@spec decode_stream(binary()) ::
{:ok,
%{
messages: [message()],
diagnostics: %{
resync_bytes: non_neg_integer(),
skipped_frames: [map()]
}
}}
| {:error, term()}
def decode_stream(bytes) when is_binary(bytes) do
case NIF.rtcm_decode_stream(bytes) do
{:ok, {messages, diagnostics}} ->
{:ok, %{messages: messages, diagnostics: diagnostics}}
{:error, reason} ->
{:error, reason}
end
rescue
e in ErlangError -> {:error, e.original}
end
@doc """
Decode every CRC-valid RTCM 3 frame in a byte buffer.
"""
@spec decode(binary()) :: {:ok, [message()]} | {:error, term()}
def decode(bytes), do: decode_messages(bytes)
@doc """
Decode one RTCM message body.
"""
@spec decode_message(binary()) :: {:ok, message()} | {:error, term()}
def decode_message(body) when is_binary(body) do
case NIF.rtcm_decode_message(body) do
{:ok, message} -> {:ok, message}
{:error, reason} -> {:error, reason}
end
rescue
e in ErlangError -> {:error, e.original}
end
@doc """
Return the message number from one RTCM message body.
"""
@spec message_number(binary()) :: {:ok, integer()} | {:error, term()}
def message_number(body) when is_binary(body) do
case NIF.rtcm_message_number(body) do
{:ok, number} -> {:ok, number}
{:error, reason} -> {:error, reason}
end
rescue
e in ErlangError -> {:error, e.original}
end
@doc """
Return the RINEX LLI bit constants used by the RTCM MSM derivation helpers.
"""
@spec lli_bits() :: %{loss_of_lock: 1, half_cycle: 2}
def lli_bits do
{loss_of_lock, half_cycle} = NIF.rtcm_lli_bits()
%{loss_of_lock: loss_of_lock, half_cycle: half_cycle}
end
@doc """
Minimum continuous-lock time in milliseconds for an MSM lock indicator.
`kind` is `"msm4"` or `"msm7"`.
"""
@spec minimum_lock_time_ms(String.t(), integer()) ::
{:ok, non_neg_integer() | nil} | {:error, term()}
def minimum_lock_time_ms(kind, indicator) when is_binary(kind) and is_integer(indicator) do
case NIF.rtcm_minimum_lock_time_ms(kind, indicator) do
{:ok, value} -> {:ok, value}
{:error, reason} -> {:error, reason}
end
rescue
e in ErlangError -> {:error, e.original}
end
@doc """
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.
"""
@spec derive_lli(integer() | nil, integer() | nil, integer() | nil, boolean()) ::
non_neg_integer()
def derive_lli(previous_min_lock_time_ms, elapsed_ms, current_min_lock_time_ms, half_cycle?)
when (is_integer(previous_min_lock_time_ms) or is_nil(previous_min_lock_time_ms)) and
(is_integer(elapsed_ms) or is_nil(elapsed_ms)) and
(is_integer(current_min_lock_time_ms) or is_nil(current_min_lock_time_ms)) and is_boolean(half_cycle?) do
NIF.rtcm_derive_lli(
previous_min_lock_time_ms,
elapsed_ms,
current_min_lock_time_ms,
half_cycle?
)
end
@doc """
Elapsed milliseconds between two raw MSM epoch-time fields for one system.
"""
@spec msm_epoch_dt_ms(String.t(), integer(), integer()) ::
{:ok, non_neg_integer()} | {:error, term()}
def msm_epoch_dt_ms(system, previous, current)
when is_binary(system) and is_integer(previous) and is_integer(current) do
case NIF.rtcm_msm_epoch_dt_ms(system, previous, current) do
{:ok, value} -> {:ok, value}
{:error, reason} -> {:error, reason}
end
rescue
e in ErlangError -> {:error, e.original}
end
@doc """
RINEX observation-code suffix for an MSM signal id, or `nil` if reserved.
"""
@spec msm_signal_rinex_code(String.t(), integer()) :: {:ok, String.t() | nil} | {:error, term()}
def msm_signal_rinex_code(system, signal_id) when is_binary(system) and is_integer(signal_id) do
case NIF.rtcm_msm_signal_rinex_code(system, signal_id) do
{:ok, value} -> {:ok, value}
{:error, reason} -> {:error, reason}
end
rescue
e in ErlangError -> {:error, e.original}
end
@doc """
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.
"""
@spec msm_lli([map()]) :: {:ok, [[map()]]} | {:error, term()}
def msm_lli(messages) when is_list(messages) do
{:ok, NIF.rtcm_msm_lli(messages)}
rescue
e in ErlangError -> {:error, e.original}
end
@doc """
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.
"""
@spec encode_message(message()) :: {:ok, binary()} | {:error, term()}
def encode_message(message), do: encode_frame(message)
@doc """
Construct a supported RTCM 3 message and return its message body.
"""
@spec encode(message()) :: {:ok, binary()} | {:error, term()}
def encode({type, fields}) when is_atom(type) and is_map(fields) do
encode_constructed_message(type, fields, &NIF.rtcm_encode/2)
end
@doc """
Construct a supported RTCM 3 message and return its complete frame.
"""
@spec encode_frame(message() | binary()) :: {:ok, binary()} | {:error, term()}
def encode_frame({type, fields}) when is_atom(type) and is_map(fields) do
encode_constructed_message(type, fields, &NIF.rtcm_encode_frame/2)
end
def encode_frame(body) when is_binary(body) do
case NIF.rtcm_encode_frame_body(body) do
{:ok, frame} -> {:ok, frame}
{:error, reason} -> {:error, reason}
end
rescue
e in ErlangError -> {:error, e.original}
end
@doc """
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.
"""
@spec decode_frame(binary()) ::
{:ok, %{message_number: integer(), frame_len: integer(), body: binary()}}
| {:error, term()}
def decode_frame(bytes) when is_binary(bytes) do
case NIF.rtcm_decode_frame(bytes) do
{:ok, %{body: body} = frame} -> {:ok, %{frame | body: :erlang.list_to_binary(body)}}
{:error, reason} -> {:error, reason}
end
rescue
e in ErlangError -> {:error, e.original}
end
defp encode_constructed_message(type, fields, encoder) do
case encoder.(Atom.to_string(type), fields) do
{:ok, bytes} -> {:ok, bytes}
{:error, reason} -> {:error, reason}
end
rescue
e in ArgumentError -> {:error, e.message}
e in ErlangError -> {:error, e.original}
end
end