Aprs (aprs v2.0.1)

View Source

Main APRS packet parsing library.

parse/1 accepts a TNC2-format packet (SRC>DST,PATH:information) and returns {:ok, map} or {:error, reason}.

Units

Every packet format reports the same units, so a caller does not have to know how a packet happened to encode a value:

  • speed - knots, float
  • altitude - feet, float
  • course - degrees, integer; 360 is due north, 0 (or absent) is unknown
  • posresolution - metres, float

Summary

Functions

Parse a TNC2-format packet (SRC>DST,PATH:information).

Split a callsign into [base_callsign, ssid], validating it as AX.25.

Parse an information field that has already been classified by parse_datatype/1.

Determine the data type of an information field.

parse_datatype/1 wrapped in an :ok tuple, for use in a with chain.

parse_position_without_timestamp/1 with the messaging flag set, for the = data type indicator.

Parse the body of a timestamped position report (/ or @), with the data type indicator already removed.

Parse the body of an untimestamped position report (! or =), compressed or uncompressed, with the data type indicator already removed.

Split a packet into [sender, path, information_field].

Split the path into [destination, digipeater_path].

Returns the version of the library, the same string mix.exs declares.

Types

coordinate()

@type coordinate() :: float() | nil

packet()

@type packet() :: %{
  :id => String.t(),
  :sender => String.t(),
  :path => String.t(),
  :destination => String.t(),
  :information_field => String.t(),
  :data_type => atom(),
  :base_callsign => String.t(),
  :ssid => String.t() | nil,
  :data_extended => map() | nil,
  :received_at => DateTime.t(),
  optional(atom()) => term()
}

parse_result()

@type parse_result() :: {:ok, packet()} | {:error, atom() | String.t()}

position_ambiguity()

@type position_ambiguity() :: 0..4

position_data()

@type position_data() :: %{
  :latitude => coordinate(),
  :longitude => coordinate(),
  optional(:timestamp) => String.t() | integer() | nil,
  optional(:symbol_table_id) => String.t() | nil,
  optional(:symbol_code) => String.t() | nil,
  optional(:comment) => String.t(),
  optional(:altitude) => float() | nil,
  optional(:phg) => String.t() | nil,
  optional(:aprs_messaging?) => boolean(),
  optional(:compressed?) => boolean(),
  optional(:position_ambiguity) => position_ambiguity(),
  optional(:dao) => map() | nil,
  optional(:course) => integer() | nil,
  optional(:speed) => float() | nil,
  optional(:has_position) => boolean(),
  optional(:data_type) => atom()
}

Functions

parse(message)

@spec parse(term()) :: parse_result()

Parse a TNC2-format packet (SRC>DST,PATH:information).

Returns {:ok, packet}, or {:error, reason} for a packet that is not parseable at all - a malformed header, an invalid callsign or path, or a body larger than 8192 bytes. A packet whose header is well formed but whose information field is not always parses; the trouble is reported in data_type (:unknown_datatype, :malformed_position, :mic_e_error, and so on) rather than as an error.

The returned map carries the packet envelope, the type-specific fields under data_extended, those same fields flattened into the top level, and the reference-parser field names (srccallsign, symboltable, posambiguity, ...). See the README for the full field list.

Examples

iex> {:ok, packet} = Aprs.parse("N0CALL>APRS,TCPIP*,qAC,T2TEST:=4903.50N/07201.75W-Hi")
iex> {packet.data_type, packet.latitude, packet.comment}
{:position_with_message, 49.05833333333333, "Hi"}

iex> Aprs.parse("not a packet")
{:error, :invalid_packet}

parse_callsign(callsign)

@spec parse_callsign(String.t()) ::
  {:ok, [String.t()]} | {:error, String.t() | atom()}

Split a callsign into [base_callsign, ssid], validating it as AX.25.

The SSID is "0" when the callsign carries none.

Examples

iex> Aprs.parse_callsign("N0CALL-9")
{:ok, ["N0CALL", "9"]}

iex> Aprs.parse_callsign("N0CALL")
{:ok, ["N0CALL", "0"]}

parse_data(arg1, destination, data)

@spec parse_data(atom(), String.t(), String.t()) :: map() | nil

Parse an information field that has already been classified by parse_datatype/1.

destination is the TOCALL, which Mic-E packets need for the latitude. The returned map always carries a data_type, which may be more specific than the one passed in (a :weather position, a :message_ack, a :mic_e_error). Returns nil when the field cannot be parsed at all.

parse_datatype(data)

@spec parse_datatype(String.t()) :: atom()

Determine the data type of an information field.

Returns the atom for the data type indicator, :empty for an empty field, or :unknown_datatype for an indicator that is not recognised and holds no ! within its first 40 bytes.

Examples

iex> Aprs.parse_datatype("=4903.50N/07201.75W-")
:position_with_message

iex> Aprs.parse_datatype("T#005,199,000,255,073,123,01101001")
:telemetry

iex> Aprs.parse_datatype("~nonsense")
:unknown_datatype

parse_datatype_safe(data)

@spec parse_datatype_safe(String.t()) :: {:ok, atom()}

parse_datatype/1 wrapped in an :ok tuple, for use in a with chain.

Examples

iex> Aprs.parse_datatype_safe("!4903.50N/07201.75W-")
{:ok, :position}

parse_position_with_message_without_timestamp(position_data)

@spec parse_position_with_message_without_timestamp(String.t()) :: map()

parse_position_without_timestamp/1 with the messaging flag set, for the = data type indicator.

parse_position_with_timestamp(aprs_messaging?, arg2, data_type)

@spec parse_position_with_timestamp(boolean(), binary(), atom()) :: map()

Parse the body of a timestamped position report (/ or @), with the data type indicator already removed.

The leading seven bytes are the timestamp; the rest is parsed as an untimestamped position. aprs_messaging? is true for @.

parse_position_without_timestamp(position_data)

@spec parse_position_without_timestamp(String.t()) :: map()

Parse the body of an untimestamped position report (! or =), compressed or uncompressed, with the data type indicator already removed.

Returns the position fields plus everything the comment carried: a data extension, altitude, telemetry, DAO and, on the weather symbol, a weather report. Coordinates that do not decode give data_type: :malformed_position and has_position: false rather than an error.

split_packet(message)

@spec split_packet(String.t()) :: {:ok, [String.t()]} | {:error, :invalid_packet}

Split a packet into [sender, path, information_field].

path is everything between > and the first :, destination included.

Examples

iex> Aprs.split_packet("N0CALL>APRS,TCPIP*:>Hello")
{:ok, ["N0CALL", "APRS,TCPIP*", ">Hello"]}

iex> Aprs.split_packet("N0CALL")
{:error, :invalid_packet}

split_path(path)

@spec split_path(String.t()) :: {:ok, [String.t()]}

Split the path into [destination, digipeater_path].

The digipeater path is "" when the packet has no digipeaters.

Examples

iex> Aprs.split_path("APRS,WIDE1-1,WIDE2-1")
{:ok, ["APRS", "WIDE1-1,WIDE2-1"]}

iex> Aprs.split_path("APRS")
{:ok, ["APRS", ""]}

version()

@spec version() :: String.t()

Returns the version of the library, the same string mix.exs declares.

Examples

iex> [major, minor, patch] = String.split(Aprs.version(), ".")
iex> Enum.all?([major, minor, patch], &match?({_, ""}, Integer.parse(&1)))
true