refactor parser
This commit is contained in:
parent
7f4cc74348
commit
58454923ab
15 changed files with 712 additions and 664 deletions
104
lib/parser.ex
104
lib/parser.ex
|
|
@ -271,12 +271,13 @@ defmodule Parser do
|
|||
def parse_data(:query, _destination, data), do: parse_query(data)
|
||||
def parse_data(:user_defined, _destination, data), do: parse_user_defined(data)
|
||||
def parse_data(:third_party_traffic, _destination, data), do: parse_third_party_traffic(data)
|
||||
def parse_data(:peet_logging, _destination, data), do: Parser.Helpers.parse_peet_logging(data)
|
||||
|
||||
def parse_data(:invalid_test_data, _destination, data), do: Parser.Helpers.parse_invalid_test_data(data)
|
||||
def parse_data(:peet_logging, _destination, data), do: Parser.SpecialDataHelpers.parse_peet_logging(data)
|
||||
|
||||
def parse_data(:invalid_test_data, _destination, data), do: Parser.SpecialDataHelpers.parse_invalid_test_data(data)
|
||||
|
||||
def parse_data(:raw_gps_ultimeter, _destination, data) do
|
||||
case Parser.Helpers.parse_nmea_sentence(data) do
|
||||
case Parser.NMEAHelpers.parse_nmea_sentence(data) do
|
||||
{:error, error} ->
|
||||
%{
|
||||
data_type: :raw_gps_ultimeter,
|
||||
|
|
@ -294,10 +295,10 @@ defmodule Parser do
|
|||
<<"DFS", s, h, g, d, rest::binary>> = data
|
||||
|
||||
%{
|
||||
df_strength: Parser.Helpers.parse_df_strength(s),
|
||||
height: Parser.Helpers.parse_phg_height(h),
|
||||
gain: Parser.Helpers.parse_phg_gain(g),
|
||||
directivity: Parser.Helpers.parse_phg_directivity(d),
|
||||
df_strength: Parser.PHGHelpers.parse_df_strength(s),
|
||||
height: Parser.PHGHelpers.parse_phg_height(h),
|
||||
gain: Parser.PHGHelpers.parse_phg_gain(g),
|
||||
directivity: Parser.PHGHelpers.parse_phg_directivity(d),
|
||||
comment: rest,
|
||||
data_type: :df_report
|
||||
}
|
||||
|
|
@ -359,8 +360,8 @@ defmodule Parser do
|
|||
<<"/", latitude::binary-size(4), longitude::binary-size(4), symbol_code::binary-size(1), _cs::binary-size(2),
|
||||
_compression_type::binary-size(2), _rest::binary>>
|
||||
) do
|
||||
lat = Parser.Helpers.convert_to_base91(latitude)
|
||||
lon = Parser.Helpers.convert_to_base91(longitude)
|
||||
lat = Parser.CompressedPositionHelpers.convert_to_base91(latitude)
|
||||
lon = Parser.CompressedPositionHelpers.convert_to_base91(longitude)
|
||||
|
||||
%{
|
||||
latitude: lat,
|
||||
|
|
@ -414,7 +415,7 @@ defmodule Parser do
|
|||
|
||||
defp parse_position_uncompressed(latitude, sym_table_id, longitude, symbol_code, comment) do
|
||||
%{latitude: lat, longitude: lon} = parse_aprs_position(latitude, longitude)
|
||||
ambiguity = Parser.Helpers.calculate_position_ambiguity(latitude, longitude)
|
||||
ambiguity = Parser.UtilityHelpers.calculate_position_ambiguity(latitude, longitude)
|
||||
dao_data = parse_dao_extension(comment)
|
||||
{course, speed} = extract_course_and_speed(comment)
|
||||
|
||||
|
|
@ -448,7 +449,7 @@ defmodule Parser do
|
|||
|
||||
defp parse_position_short_uncompressed(latitude, sym_table_id, longitude) do
|
||||
%{latitude: lat, longitude: lon} = parse_aprs_position(latitude, longitude)
|
||||
ambiguity = Parser.Helpers.calculate_position_ambiguity(latitude, longitude)
|
||||
ambiguity = Parser.UtilityHelpers.calculate_position_ambiguity(latitude, longitude)
|
||||
|
||||
has_position =
|
||||
(is_number(lat) or is_struct(lat, Decimal)) and (is_number(lon) or is_struct(lon, Decimal))
|
||||
|
|
@ -471,10 +472,10 @@ defmodule Parser do
|
|||
end
|
||||
|
||||
defp parse_position_compressed(latitude_compressed, longitude_compressed, symbol_code, cs, compression_type, comment) do
|
||||
converted_lat = Parser.Helpers.convert_compressed_lat(latitude_compressed)
|
||||
converted_lon = Parser.Helpers.convert_compressed_lon(longitude_compressed)
|
||||
compressed_cs = Parser.Helpers.convert_compressed_cs(cs)
|
||||
ambiguity = Parser.Helpers.calculate_compressed_ambiguity(compression_type)
|
||||
converted_lat = Parser.CompressedPositionHelpers.convert_compressed_lat(latitude_compressed)
|
||||
converted_lon = Parser.CompressedPositionHelpers.convert_compressed_lon(longitude_compressed)
|
||||
compressed_cs = Parser.CompressedPositionHelpers.convert_compressed_cs(cs)
|
||||
ambiguity = Parser.CompressedPositionHelpers.calculate_compressed_ambiguity(compression_type)
|
||||
|
||||
has_position =
|
||||
(is_number(converted_lat) or is_struct(converted_lat, Decimal)) and
|
||||
|
|
@ -508,7 +509,7 @@ defmodule Parser do
|
|||
compression_type: compression_type,
|
||||
data_type: :position,
|
||||
compressed?: true,
|
||||
position_ambiguity: Parser.Helpers.calculate_compressed_ambiguity(compression_type),
|
||||
position_ambiguity: Parser.CompressedPositionHelpers.calculate_compressed_ambiguity(compression_type),
|
||||
dao: nil,
|
||||
course: nil,
|
||||
speed: nil,
|
||||
|
|
@ -549,7 +550,7 @@ defmodule Parser do
|
|||
symbol_code::binary-size(1), comment::binary>>,
|
||||
_data_type
|
||||
) do
|
||||
case Parser.Helpers.validate_position_data(latitude, longitude) do
|
||||
case Parser.UtilityHelpers.validate_position_data(latitude, longitude) do
|
||||
{:ok, {lat, lon}} ->
|
||||
position = parse_aprs_position(latitude, longitude)
|
||||
{course, speed} = extract_course_and_speed(comment)
|
||||
|
|
@ -558,7 +559,7 @@ defmodule Parser do
|
|||
latitude: lat,
|
||||
longitude: lon,
|
||||
position: position,
|
||||
time: Parser.Helpers.validate_timestamp(time),
|
||||
time: Parser.UtilityHelpers.validate_timestamp(time),
|
||||
symbol_table_id: sym_table_id,
|
||||
symbol_code: symbol_code,
|
||||
comment: comment,
|
||||
|
|
@ -726,11 +727,14 @@ defmodule Parser do
|
|||
<<"/", latitude_compressed::binary-size(4), longitude_compressed::binary-size(4), symbol_code::binary-size(1),
|
||||
cs::binary-size(2), compression_type::binary-size(1), comment::binary>> ->
|
||||
try do
|
||||
converted_lat = Parser.Helpers.convert_compressed_lat(latitude_compressed)
|
||||
converted_lon = Parser.Helpers.convert_compressed_lon(longitude_compressed)
|
||||
converted_lat =
|
||||
Parser.CompressedPositionHelpers.convert_compressed_lat(latitude_compressed)
|
||||
|
||||
converted_lon =
|
||||
Parser.CompressedPositionHelpers.convert_compressed_lon(longitude_compressed)
|
||||
|
||||
if is_number(converted_lat) and is_number(converted_lon) do
|
||||
compressed_cs = Parser.Helpers.convert_compressed_cs(cs)
|
||||
compressed_cs = Parser.CompressedPositionHelpers.convert_compressed_cs(cs)
|
||||
|
||||
base_data = %{
|
||||
latitude: converted_lat,
|
||||
|
|
@ -826,9 +830,13 @@ defmodule Parser do
|
|||
# Compressed position format
|
||||
<<"/", latitude_compressed::binary-size(4), longitude_compressed::binary-size(4), symbol_code::binary-size(1),
|
||||
cs::binary-size(2), compression_type::binary-size(1), comment::binary>> ->
|
||||
converted_lat = Parser.Helpers.convert_compressed_lat(latitude_compressed)
|
||||
converted_lon = Parser.Helpers.convert_compressed_lon(longitude_compressed)
|
||||
compressed_cs = Parser.Helpers.convert_compressed_cs(cs)
|
||||
converted_lat =
|
||||
Parser.CompressedPositionHelpers.convert_compressed_lat(latitude_compressed)
|
||||
|
||||
converted_lon =
|
||||
Parser.CompressedPositionHelpers.convert_compressed_lon(longitude_compressed)
|
||||
|
||||
compressed_cs = Parser.CompressedPositionHelpers.convert_compressed_cs(cs)
|
||||
|
||||
base_data = %{
|
||||
latitude: converted_lat,
|
||||
|
|
@ -878,22 +886,22 @@ defmodule Parser do
|
|||
# Comprehensive weather data parsing
|
||||
defp parse_weather_data(weather_data) do
|
||||
# Extract timestamp if present
|
||||
timestamp = Parser.Helpers.extract_timestamp(weather_data)
|
||||
weather_data = Parser.Helpers.remove_timestamp(weather_data)
|
||||
timestamp = Parser.WeatherHelpers.extract_timestamp(weather_data)
|
||||
weather_data = Parser.WeatherHelpers.remove_timestamp(weather_data)
|
||||
|
||||
# Parse each weather component
|
||||
weather_values = %{
|
||||
wind_direction: Parser.Helpers.parse_wind_direction(weather_data),
|
||||
wind_speed: Parser.Helpers.parse_wind_speed(weather_data),
|
||||
wind_gust: Parser.Helpers.parse_wind_gust(weather_data),
|
||||
temperature: Parser.Helpers.parse_temperature(weather_data),
|
||||
rain_1h: Parser.Helpers.parse_rainfall_1h(weather_data),
|
||||
rain_24h: Parser.Helpers.parse_rainfall_24h(weather_data),
|
||||
rain_since_midnight: Parser.Helpers.parse_rainfall_since_midnight(weather_data),
|
||||
humidity: Parser.Helpers.parse_humidity(weather_data),
|
||||
pressure: Parser.Helpers.parse_pressure(weather_data),
|
||||
luminosity: Parser.Helpers.parse_luminosity(weather_data),
|
||||
snow: Parser.Helpers.parse_snow(weather_data)
|
||||
wind_direction: Parser.WeatherHelpers.parse_wind_direction(weather_data),
|
||||
wind_speed: Parser.WeatherHelpers.parse_wind_speed(weather_data),
|
||||
wind_gust: Parser.WeatherHelpers.parse_wind_gust(weather_data),
|
||||
temperature: Parser.WeatherHelpers.parse_temperature(weather_data),
|
||||
rain_1h: Parser.WeatherHelpers.parse_rainfall_1h(weather_data),
|
||||
rain_24h: Parser.WeatherHelpers.parse_rainfall_24h(weather_data),
|
||||
rain_since_midnight: Parser.WeatherHelpers.parse_rainfall_since_midnight(weather_data),
|
||||
humidity: Parser.WeatherHelpers.parse_humidity(weather_data),
|
||||
pressure: Parser.WeatherHelpers.parse_pressure(weather_data),
|
||||
luminosity: Parser.WeatherHelpers.parse_luminosity(weather_data),
|
||||
snow: Parser.WeatherHelpers.parse_snow(weather_data)
|
||||
}
|
||||
|
||||
# Build base result map
|
||||
|
|
@ -917,9 +925,9 @@ defmodule Parser do
|
|||
digital_values = values |> Enum.drop(5) |> Enum.take(8)
|
||||
|
||||
%{
|
||||
sequence_number: Parser.Helpers.parse_telemetry_sequence(seq),
|
||||
analog_values: Parser.Helpers.parse_analog_values(analog_values),
|
||||
digital_values: Parser.Helpers.parse_digital_values(digital_values),
|
||||
sequence_number: Parser.TelemetryHelpers.parse_telemetry_sequence(seq),
|
||||
analog_values: Parser.TelemetryHelpers.parse_analog_values(analog_values),
|
||||
digital_values: Parser.TelemetryHelpers.parse_digital_values(digital_values),
|
||||
data_type: :telemetry,
|
||||
raw_data: rest
|
||||
}
|
||||
|
|
@ -949,9 +957,9 @@ defmodule Parser do
|
|||
|> Enum.chunk_every(3)
|
||||
|> Enum.map(fn [a, b, c] ->
|
||||
%{
|
||||
a: Parser.Helpers.parse_coefficient(a),
|
||||
b: Parser.Helpers.parse_coefficient(b),
|
||||
c: Parser.Helpers.parse_coefficient(c)
|
||||
a: Parser.TelemetryHelpers.parse_coefficient(a),
|
||||
b: Parser.TelemetryHelpers.parse_coefficient(b),
|
||||
c: Parser.TelemetryHelpers.parse_coefficient(c)
|
||||
}
|
||||
end)
|
||||
|
||||
|
|
@ -1065,7 +1073,7 @@ defmodule Parser do
|
|||
|
||||
# Third Party Traffic parsing
|
||||
def parse_third_party_traffic(packet) do
|
||||
if Parser.Helpers.count_leading_braces(packet) + 1 > 3 do
|
||||
if Parser.UtilityHelpers.count_leading_braces(packet) + 1 > 3 do
|
||||
%{
|
||||
error: "Maximum tunnel depth exceeded"
|
||||
}
|
||||
|
|
@ -1244,10 +1252,10 @@ defmodule Parser do
|
|||
|
||||
def parse_phg_data(<<"PHG", p, h, g, d, rest::binary>>) do
|
||||
%{
|
||||
power: Parser.Helpers.parse_phg_power(p),
|
||||
height: Parser.Helpers.parse_phg_height(h),
|
||||
gain: Parser.Helpers.parse_phg_gain(g),
|
||||
directivity: Parser.Helpers.parse_phg_directivity(d),
|
||||
power: Parser.PHGHelpers.parse_phg_power(p),
|
||||
height: Parser.PHGHelpers.parse_phg_height(h),
|
||||
gain: Parser.PHGHelpers.parse_phg_gain(g),
|
||||
directivity: Parser.PHGHelpers.parse_phg_directivity(d),
|
||||
comment: rest,
|
||||
data_type: :phg_data
|
||||
}
|
||||
|
|
|
|||
57
lib/parser/compressed_position_helpers.ex
Normal file
57
lib/parser/compressed_position_helpers.ex
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
defmodule Parser.CompressedPositionHelpers do
|
||||
@moduledoc """
|
||||
Compressed position helpers for APRS.
|
||||
"""
|
||||
|
||||
@spec convert_compressed_lat(binary()) :: {:ok, float()} | {:error, String.t()}
|
||||
def convert_compressed_lat(lat) when is_binary(lat) and byte_size(lat) == 4 do
|
||||
[l1, l2, l3, l4] = to_charlist(lat)
|
||||
{:ok, 90 - ((l1 - 33) * 91 ** 3 + (l2 - 33) * 91 ** 2 + (l3 - 33) * 91 + l4 - 33) / 380_926}
|
||||
end
|
||||
|
||||
def convert_compressed_lat(_), do: {:error, "Invalid compressed latitude"}
|
||||
|
||||
@spec convert_compressed_lon(binary()) :: {:ok, float()} | {:error, String.t()}
|
||||
def convert_compressed_lon(lon) when is_binary(lon) and byte_size(lon) == 4 do
|
||||
[l1, l2, l3, l4] = to_charlist(lon)
|
||||
{:ok, -180 + ((l1 - 33) * 91 ** 3 + (l2 - 33) * 91 ** 2 + (l3 - 33) * 91 + l4 - 33) / 190_463}
|
||||
end
|
||||
|
||||
def convert_compressed_lon(_), do: {:error, "Invalid compressed longitude"}
|
||||
|
||||
@spec calculate_compressed_ambiguity(String.t()) :: integer()
|
||||
def calculate_compressed_ambiguity(compression_type) do
|
||||
case compression_type do
|
||||
" " -> 0
|
||||
"!" -> 1
|
||||
"\"" -> 2
|
||||
"#" -> 3
|
||||
"$" -> 4
|
||||
_ -> 0
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def convert_to_base91(<<value::binary-size(4)>>) do
|
||||
[v1, v2, v3, v4] = to_charlist(value)
|
||||
(v1 - 33) * 91 * 91 * 91 + (v2 - 33) * 91 * 91 + (v3 - 33) * 91 + v4
|
||||
end
|
||||
|
||||
@spec convert_compressed_cs(binary()) :: map()
|
||||
def convert_compressed_cs(cs) do
|
||||
[c, s] = to_charlist(cs)
|
||||
c = c - 33
|
||||
s = s - 33
|
||||
|
||||
case c do
|
||||
x when x in ?!..?z ->
|
||||
%{course: s * 4, speed: Aprs.Convert.speed(1.08 ** s - 1, :knots, :mph)}
|
||||
|
||||
?Z ->
|
||||
%{range: 2 * 1.08 ** s}
|
||||
|
||||
_ ->
|
||||
%{}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,492 +1,14 @@
|
|||
defmodule Parser.Helpers do
|
||||
@moduledoc """
|
||||
Public helper functions for APRS parsing (NMEA, PHG/DF, compressed position, etc).
|
||||
This module has been split into domain-specific helpers:
|
||||
- Parser.NMEAHelpers
|
||||
- Parser.PHGHelpers
|
||||
- Parser.CompressedPositionHelpers
|
||||
- Parser.KISSHelpers
|
||||
- Parser.TelemetryHelpers
|
||||
- Parser.WeatherHelpers
|
||||
- Parser.UtilityHelpers
|
||||
Update your code to use the new modules directly.
|
||||
"""
|
||||
|
||||
import Decimal, only: [new: 1, add: 2, negate: 1]
|
||||
|
||||
@type phg_power :: {integer() | nil, String.t()}
|
||||
@type phg_height :: {integer() | nil, String.t()}
|
||||
@type phg_gain :: {integer() | nil, String.t()}
|
||||
@type phg_directivity :: {integer() | nil, String.t()}
|
||||
@type df_strength :: {integer() | nil, String.t()}
|
||||
|
||||
# NMEA helpers
|
||||
@spec parse_nmea_coordinate(String.t(), String.t()) :: {:ok, float()} | {:error, String.t()}
|
||||
def parse_nmea_coordinate(value, direction) when is_binary(value) and is_binary(direction) do
|
||||
case Float.parse(value) do
|
||||
{coord, _} ->
|
||||
coord = coord / 100.0
|
||||
coord = apply_nmea_direction(coord, direction)
|
||||
handle_coordinate_result(coord)
|
||||
|
||||
:error ->
|
||||
{:error, "Invalid coordinate value"}
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_nmea_coordinate(any(), any()) :: {:error, String.t()}
|
||||
def parse_nmea_coordinate(_, _), do: {:error, "Invalid coordinate format"}
|
||||
|
||||
defp handle_coordinate_result(coord) when is_tuple(coord), do: coord
|
||||
defp handle_coordinate_result(coord), do: {:ok, coord}
|
||||
|
||||
defp apply_nmea_direction(coord, direction) do
|
||||
case direction do
|
||||
"N" -> coord
|
||||
"S" -> -coord
|
||||
"E" -> coord
|
||||
"W" -> -coord
|
||||
_ -> {:error, "Invalid coordinate direction"}
|
||||
end
|
||||
end
|
||||
|
||||
# PHG/DF helpers
|
||||
@spec parse_phg_power(integer()) :: phg_power
|
||||
def parse_phg_power(?0), do: {1, "1 watt"}
|
||||
def parse_phg_power(?1), do: {4, "4 watts"}
|
||||
def parse_phg_power(?2), do: {9, "9 watts"}
|
||||
def parse_phg_power(?3), do: {16, "16 watts"}
|
||||
def parse_phg_power(?4), do: {25, "25 watts"}
|
||||
def parse_phg_power(?5), do: {36, "36 watts"}
|
||||
def parse_phg_power(?6), do: {49, "49 watts"}
|
||||
def parse_phg_power(?7), do: {64, "64 watts"}
|
||||
def parse_phg_power(?8), do: {81, "81 watts"}
|
||||
def parse_phg_power(?9), do: {81, "81 watts"}
|
||||
def parse_phg_power(p), do: {nil, "Unknown power: #{<<p>>}"}
|
||||
|
||||
@spec parse_phg_height(integer()) :: phg_height
|
||||
def parse_phg_height(?0), do: {10, "10 feet"}
|
||||
def parse_phg_height(?1), do: {20, "20 feet"}
|
||||
def parse_phg_height(?2), do: {40, "40 feet"}
|
||||
def parse_phg_height(?3), do: {80, "80 feet"}
|
||||
def parse_phg_height(?4), do: {160, "160 feet"}
|
||||
def parse_phg_height(?5), do: {320, "320 feet"}
|
||||
def parse_phg_height(?6), do: {640, "640 feet"}
|
||||
def parse_phg_height(?7), do: {1280, "1280 feet"}
|
||||
def parse_phg_height(?8), do: {2560, "2560 feet"}
|
||||
def parse_phg_height(?9), do: {5120, "5120 feet"}
|
||||
def parse_phg_height(h), do: {nil, "Unknown height: #{<<h>>}"}
|
||||
|
||||
@spec parse_phg_gain(integer()) :: phg_gain
|
||||
def parse_phg_gain(?0), do: {0, "0 dB"}
|
||||
def parse_phg_gain(?1), do: {1, "1 dB"}
|
||||
def parse_phg_gain(?2), do: {2, "2 dB"}
|
||||
def parse_phg_gain(?3), do: {3, "3 dB"}
|
||||
def parse_phg_gain(?4), do: {4, "4 dB"}
|
||||
def parse_phg_gain(?5), do: {5, "5 dB"}
|
||||
def parse_phg_gain(?6), do: {6, "6 dB"}
|
||||
def parse_phg_gain(?7), do: {7, "7 dB"}
|
||||
def parse_phg_gain(?8), do: {8, "8 dB"}
|
||||
def parse_phg_gain(?9), do: {9, "9 dB"}
|
||||
def parse_phg_gain(g), do: {nil, "Unknown gain: #{<<g>>}"}
|
||||
|
||||
@spec parse_phg_directivity(integer()) :: phg_directivity
|
||||
def parse_phg_directivity(?0), do: {360, "Omni"}
|
||||
def parse_phg_directivity(?1), do: {45, "45° NE"}
|
||||
def parse_phg_directivity(?2), do: {90, "90° E"}
|
||||
def parse_phg_directivity(?3), do: {135, "135° SE"}
|
||||
def parse_phg_directivity(?4), do: {180, "180° S"}
|
||||
def parse_phg_directivity(?5), do: {225, "225° SW"}
|
||||
def parse_phg_directivity(?6), do: {270, "270° W"}
|
||||
def parse_phg_directivity(?7), do: {315, "315° NW"}
|
||||
def parse_phg_directivity(?8), do: {360, "360° N"}
|
||||
def parse_phg_directivity(?9), do: {nil, "Undefined"}
|
||||
def parse_phg_directivity(d), do: {nil, "Unknown directivity: #{<<d>>}"}
|
||||
|
||||
@spec parse_df_strength(integer()) :: df_strength
|
||||
def parse_df_strength(?0), do: {0, "0 dB"}
|
||||
def parse_df_strength(?1), do: {1, "3 dB above S0"}
|
||||
def parse_df_strength(?2), do: {2, "6 dB above S0"}
|
||||
def parse_df_strength(?3), do: {3, "9 dB above S0"}
|
||||
def parse_df_strength(?4), do: {4, "12 dB above S0"}
|
||||
def parse_df_strength(?5), do: {5, "15 dB above S0"}
|
||||
def parse_df_strength(?6), do: {6, "18 dB above S0"}
|
||||
def parse_df_strength(?7), do: {7, "21 dB above S0"}
|
||||
def parse_df_strength(?8), do: {8, "24 dB above S0"}
|
||||
def parse_df_strength(?9), do: {9, "27 dB above S0"}
|
||||
def parse_df_strength(s), do: {nil, "Unknown strength: #{<<s>>}"}
|
||||
|
||||
# Compressed position helpers
|
||||
@spec convert_compressed_lat(binary()) :: {:ok, float()} | {:error, String.t()}
|
||||
def convert_compressed_lat(lat) when is_binary(lat) and byte_size(lat) == 4 do
|
||||
[l1, l2, l3, l4] = to_charlist(lat)
|
||||
{:ok, 90 - ((l1 - 33) * 91 ** 3 + (l2 - 33) * 91 ** 2 + (l3 - 33) * 91 + l4 - 33) / 380_926}
|
||||
end
|
||||
|
||||
def convert_compressed_lat(_), do: {:error, "Invalid compressed latitude"}
|
||||
|
||||
@spec convert_compressed_lon(binary()) :: {:ok, float()} | {:error, String.t()}
|
||||
def convert_compressed_lon(lon) when is_binary(lon) and byte_size(lon) == 4 do
|
||||
[l1, l2, l3, l4] = to_charlist(lon)
|
||||
{:ok, -180 + ((l1 - 33) * 91 ** 3 + (l2 - 33) * 91 ** 2 + (l3 - 33) * 91 + l4 - 33) / 190_463}
|
||||
end
|
||||
|
||||
def convert_compressed_lon(_), do: {:error, "Invalid compressed longitude"}
|
||||
|
||||
# PEET Logging parsing
|
||||
@spec parse_peet_logging(binary()) :: map()
|
||||
def parse_peet_logging(<<"*", peet_data::binary>>) do
|
||||
%{
|
||||
peet_data: peet_data,
|
||||
data_type: :peet_logging
|
||||
}
|
||||
end
|
||||
|
||||
@spec parse_peet_logging(any()) :: map()
|
||||
def parse_peet_logging(data) do
|
||||
%{
|
||||
peet_data: data,
|
||||
data_type: :peet_logging
|
||||
}
|
||||
end
|
||||
|
||||
# Invalid/Test Data parsing
|
||||
@spec parse_invalid_test_data(binary()) :: map()
|
||||
def parse_invalid_test_data(<<",", test_data::binary>>) do
|
||||
%{
|
||||
test_data: test_data,
|
||||
data_type: :invalid_test_data
|
||||
}
|
||||
end
|
||||
|
||||
@spec parse_invalid_test_data(any()) :: map()
|
||||
def parse_invalid_test_data(data) do
|
||||
%{
|
||||
test_data: data,
|
||||
data_type: :invalid_test_data
|
||||
}
|
||||
end
|
||||
|
||||
# NMEA sentence parsing (minimal public wrapper)
|
||||
@spec parse_nmea_sentence(any()) :: {:error, String.t()}
|
||||
def parse_nmea_sentence(_sentence) do
|
||||
# This is a minimal wrapper; full NMEA parsing logic can be implemented as needed
|
||||
{:error, "NMEA parsing not implemented"}
|
||||
end
|
||||
|
||||
# KISS to TNC2 conversion
|
||||
@spec kiss_to_tnc2(binary()) :: binary()
|
||||
def kiss_to_tnc2(<<0xC0, 0x00, rest::binary>>) do
|
||||
# Remove KISS framing and control byte
|
||||
tnc2 =
|
||||
rest
|
||||
|> String.trim_trailing(<<0xC0>>)
|
||||
|> String.replace(<<0xDB, 0xDC>>, <<0xC0>>)
|
||||
|> String.replace(<<0xDB, 0xDD>>, <<0xDB>>)
|
||||
|
||||
tnc2
|
||||
end
|
||||
|
||||
@spec kiss_to_tnc2(any()) :: map()
|
||||
def kiss_to_tnc2(_), do: %{error_code: :packet_invalid, error_message: "Unknown error"}
|
||||
|
||||
# TNC2 to KISS conversion
|
||||
@spec tnc2_to_kiss(binary()) :: binary()
|
||||
def tnc2_to_kiss(tnc2) do
|
||||
# Add KISS framing and escape special bytes
|
||||
escaped =
|
||||
tnc2
|
||||
|> String.replace(<<0xDB>>, <<0xDB, 0xDD>>)
|
||||
|> String.replace(<<0xC0>>, <<0xDB, 0xDC>>)
|
||||
|
||||
<<0xC0, 0x00>> <> escaped <> <<0xC0>>
|
||||
end
|
||||
|
||||
# Telemetry helpers
|
||||
@spec parse_telemetry_sequence(String.t()) :: integer() | nil
|
||||
def parse_telemetry_sequence(seq) do
|
||||
case Integer.parse(seq) do
|
||||
{num, _} -> num
|
||||
:error -> nil
|
||||
end
|
||||
end
|
||||
|
||||
def parse_analog_values(values) do
|
||||
Enum.map(values, &parse_analog_value/1)
|
||||
end
|
||||
|
||||
defp parse_analog_value("") do
|
||||
nil
|
||||
end
|
||||
|
||||
defp parse_analog_value(value) do
|
||||
case Float.parse(value) do
|
||||
{float_val, _} -> float_val
|
||||
:error -> nil
|
||||
end
|
||||
end
|
||||
|
||||
def parse_digital_values(values) do
|
||||
Enum.flat_map(values, &parse_digital_value/1)
|
||||
end
|
||||
|
||||
defp parse_digital_value("1"), do: [true]
|
||||
|
||||
defp parse_digital_value(value) when is_binary(value) do
|
||||
value
|
||||
|> String.graphemes()
|
||||
|> Enum.map(fn
|
||||
"1" -> true
|
||||
"0" -> false
|
||||
_ -> nil
|
||||
end)
|
||||
end
|
||||
|
||||
defp parse_digital_value(_), do: [nil]
|
||||
|
||||
def parse_coefficient(coeff) do
|
||||
case Float.parse(coeff) do
|
||||
{float_val, _} ->
|
||||
float_val
|
||||
|
||||
:error ->
|
||||
case Integer.parse(coeff) do
|
||||
{int_val, _} -> int_val
|
||||
:error -> coeff
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Weather helpers
|
||||
@spec extract_timestamp(String.t()) :: String.t() | nil
|
||||
def extract_timestamp(weather_data) do
|
||||
case Regex.run(~r/^(\d{6}[hz\/])/, weather_data) do
|
||||
[_, timestamp] -> timestamp
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec remove_timestamp(String.t()) :: String.t()
|
||||
def remove_timestamp(weather_data) do
|
||||
case Regex.run(~r/^\d{6}[hz\/]/, weather_data) do
|
||||
[timestamp] -> String.replace(weather_data, timestamp, "")
|
||||
nil -> weather_data
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_wind_direction(String.t()) :: integer() | nil
|
||||
def parse_wind_direction(weather_data) do
|
||||
case Regex.run(~r/(\d{3})\//, weather_data) do
|
||||
[_, direction] -> String.to_integer(direction)
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_wind_speed(String.t()) :: integer() | nil
|
||||
def parse_wind_speed(weather_data) do
|
||||
case Regex.run(~r/\/(\d{3})/, weather_data) do
|
||||
[_, speed] -> String.to_integer(speed)
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_wind_gust(String.t()) :: integer() | nil
|
||||
def parse_wind_gust(weather_data) do
|
||||
case Regex.run(~r/g(\d{3})/, weather_data) do
|
||||
[_, gust] -> String.to_integer(gust)
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_temperature(String.t()) :: integer() | nil
|
||||
def parse_temperature(weather_data) do
|
||||
case Regex.run(~r/t(-?\d{3})/, weather_data) do
|
||||
[_, temp] -> String.to_integer(temp)
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_rainfall_1h(String.t()) :: integer() | nil
|
||||
def parse_rainfall_1h(weather_data) do
|
||||
case Regex.run(~r/r(\d{3})/, weather_data) do
|
||||
[_, rain] -> String.to_integer(rain)
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_rainfall_24h(String.t()) :: integer() | nil
|
||||
def parse_rainfall_24h(weather_data) do
|
||||
case Regex.run(~r/p(\d{3})/, weather_data) do
|
||||
[_, rain] -> String.to_integer(rain)
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_rainfall_since_midnight(String.t()) :: integer() | nil
|
||||
def parse_rainfall_since_midnight(weather_data) do
|
||||
case Regex.run(~r/P(\d{3})/, weather_data) do
|
||||
[_, rain] -> String.to_integer(rain)
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_humidity(String.t()) :: integer() | nil
|
||||
def parse_humidity(weather_data) do
|
||||
case Regex.run(~r/h(\d{2})/, weather_data) do
|
||||
[_, humidity] ->
|
||||
val = String.to_integer(humidity)
|
||||
normalize_humidity(val)
|
||||
|
||||
nil ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_humidity(0), do: 100
|
||||
defp normalize_humidity(val), do: val
|
||||
|
||||
@spec parse_pressure(String.t()) :: float() | nil
|
||||
def parse_pressure(weather_data) do
|
||||
case Regex.run(~r/b(\d{5})/, weather_data) do
|
||||
[_, pressure] -> String.to_integer(pressure) / 10.0
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_luminosity(String.t()) :: integer() | nil
|
||||
def parse_luminosity(weather_data) do
|
||||
case Regex.run(~r/[lL](\d{3})/, weather_data) do
|
||||
[_, luminosity] -> String.to_integer(luminosity)
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_snow(String.t()) :: integer() | nil
|
||||
def parse_snow(weather_data) do
|
||||
case Regex.run(~r/s(\d{3})/, weather_data) do
|
||||
[_, snow] -> String.to_integer(snow)
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
# Ambiguity and utility helpers
|
||||
@doc false
|
||||
def count_spaces(str) do
|
||||
str
|
||||
|> String.graphemes()
|
||||
|> Enum.count(fn c -> c == " " end)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def count_leading_braces(packet), do: count_leading_braces(packet, 0)
|
||||
|
||||
@doc false
|
||||
def count_leading_braces(<<"}", rest::binary>>, count), do: count_leading_braces(rest, count + 1)
|
||||
|
||||
@doc false
|
||||
def count_leading_braces(_packet, count), do: count
|
||||
|
||||
@doc false
|
||||
def calculate_position_ambiguity(latitude, longitude) do
|
||||
lat_spaces = count_spaces(latitude)
|
||||
lon_spaces = count_spaces(longitude)
|
||||
|
||||
Map.get(
|
||||
%{{0, 0} => 0, {1, 1} => 1, {2, 2} => 2, {3, 3} => 3, {4, 4} => 4},
|
||||
{lat_spaces, lon_spaces},
|
||||
0
|
||||
)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def calculate_compressed_ambiguity(compression_type) do
|
||||
case compression_type do
|
||||
" " -> 0
|
||||
"!" -> 1
|
||||
"\"" -> 2
|
||||
"#" -> 3
|
||||
"$" -> 4
|
||||
_ -> 0
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def find_matches(regex, text) do
|
||||
case Regex.names(regex) do
|
||||
[] ->
|
||||
matches = Regex.run(regex, text)
|
||||
|
||||
Enum.reduce(Enum.with_index(matches), %{}, fn {match, index}, acc ->
|
||||
Map.put(acc, index, match)
|
||||
end)
|
||||
|
||||
_ ->
|
||||
Regex.named_captures(regex, text)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def parse_manufacturer(symbols) do
|
||||
Aprs.DeviceIdentification.identify_device(symbols)
|
||||
end
|
||||
|
||||
@doc false
|
||||
defp apply_latitude_direction(lat_val, "S"), do: negate(lat_val)
|
||||
defp apply_latitude_direction(lat_val, _), do: lat_val
|
||||
|
||||
@doc false
|
||||
defp apply_longitude_direction(lon_val, "W"), do: negate(lon_val)
|
||||
defp apply_longitude_direction(lon_val, _), do: lon_val
|
||||
|
||||
@doc false
|
||||
def convert_to_base91(<<value::binary-size(4)>>) do
|
||||
[v1, v2, v3, v4] = to_charlist(value)
|
||||
(v1 - 33) * 91 * 91 * 91 + (v2 - 33) * 91 * 91 + (v3 - 33) * 91 + v4
|
||||
end
|
||||
|
||||
@spec convert_compressed_cs(binary()) :: map()
|
||||
def convert_compressed_cs(cs) do
|
||||
[c, s] = to_charlist(cs)
|
||||
c = c - 33
|
||||
s = s - 33
|
||||
|
||||
case c do
|
||||
x when x in ?!..?z ->
|
||||
%{
|
||||
course: s * 4,
|
||||
speed: Aprs.Convert.speed(1.08 ** s - 1, :knots, :mph)
|
||||
}
|
||||
|
||||
?Z ->
|
||||
%{
|
||||
range: 2 * 1.08 ** s
|
||||
}
|
||||
|
||||
_ ->
|
||||
%{}
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def validate_position_data(latitude, longitude) do
|
||||
lat =
|
||||
case Regex.run(~r/^(\d{2})(\d{2}\.\d+)([NS])$/, latitude) do
|
||||
[_, degrees, minutes, direction] ->
|
||||
lat_val = add(new(degrees), Decimal.div(new(minutes), new("60")))
|
||||
apply_latitude_direction(lat_val, direction)
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
|
||||
lon =
|
||||
case Regex.run(~r/^(\d{3})(\d{2}\.\d+)([EW])$/, longitude) do
|
||||
[_, degrees, minutes, direction] ->
|
||||
lon_val = add(new(degrees), Decimal.div(new(minutes), new("60")))
|
||||
apply_longitude_direction(lon_val, direction)
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
|
||||
if is_struct(lat, Decimal) and is_struct(lon, Decimal) do
|
||||
{:ok, {lat, lon}}
|
||||
else
|
||||
{:error, :invalid_position}
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def validate_timestamp(_time), do: nil
|
||||
@compile {:no_warn_undefined, __MODULE__}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -61,9 +61,13 @@ defmodule Parser.Item do
|
|||
|
||||
<<"/", latitude_compressed::binary-size(4), longitude_compressed::binary-size(4), symbol_code::binary-size(1),
|
||||
cs::binary-size(2), compression_type::binary-size(1), comment::binary>> ->
|
||||
converted_lat = Parser.Helpers.convert_compressed_lat(latitude_compressed)
|
||||
converted_lon = Parser.Helpers.convert_compressed_lon(longitude_compressed)
|
||||
compressed_cs = Parser.Helpers.convert_compressed_cs(cs)
|
||||
converted_lat =
|
||||
Parser.CompressedPositionHelpers.convert_compressed_lat(latitude_compressed)
|
||||
|
||||
converted_lon =
|
||||
Parser.CompressedPositionHelpers.convert_compressed_lon(longitude_compressed)
|
||||
|
||||
compressed_cs = Parser.CompressedPositionHelpers.convert_compressed_cs(cs)
|
||||
|
||||
base_data = %{
|
||||
latitude: converted_lat,
|
||||
|
|
|
|||
28
lib/parser/kiss_helpers.ex
Normal file
28
lib/parser/kiss_helpers.ex
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
defmodule Parser.KISSHelpers do
|
||||
@moduledoc """
|
||||
KISS/TNC2 conversion helpers for APRS.
|
||||
"""
|
||||
|
||||
@spec kiss_to_tnc2(binary()) :: binary() | map()
|
||||
def kiss_to_tnc2(<<0xC0, 0x00, rest::binary>>) do
|
||||
tnc2 =
|
||||
rest
|
||||
|> String.trim_trailing(<<0xC0>>)
|
||||
|> String.replace(<<0xDB, 0xDC>>, <<0xC0>>)
|
||||
|> String.replace(<<0xDB, 0xDD>>, <<0xDB>>)
|
||||
|
||||
tnc2
|
||||
end
|
||||
|
||||
def kiss_to_tnc2(_), do: %{error_code: :packet_invalid, error_message: "Unknown error"}
|
||||
|
||||
@spec tnc2_to_kiss(binary()) :: binary()
|
||||
def tnc2_to_kiss(tnc2) do
|
||||
escaped =
|
||||
tnc2
|
||||
|> String.replace(<<0xDB>>, <<0xDB, 0xDD>>)
|
||||
|> String.replace(<<0xC0>>, <<0xDB, 0xDC>>)
|
||||
|
||||
<<0xC0, 0x00>> <> escaped <> <<0xC0>>
|
||||
end
|
||||
end
|
||||
39
lib/parser/nmea_helpers.ex
Normal file
39
lib/parser/nmea_helpers.ex
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
defmodule Parser.NMEAHelpers do
|
||||
@moduledoc """
|
||||
NMEA coordinate and sentence parsing helpers for APRS.
|
||||
"""
|
||||
|
||||
@spec parse_nmea_coordinate(String.t(), String.t()) :: {:ok, float()} | {:error, String.t()}
|
||||
def parse_nmea_coordinate(value, direction) when is_binary(value) and is_binary(direction) do
|
||||
case Float.parse(value) do
|
||||
{coord, _} ->
|
||||
coord = coord / 100.0
|
||||
coord = apply_nmea_direction(coord, direction)
|
||||
handle_coordinate_result(coord)
|
||||
|
||||
:error ->
|
||||
{:error, "Invalid coordinate value"}
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_nmea_coordinate(any(), any()) :: {:error, String.t()}
|
||||
def parse_nmea_coordinate(_, _), do: {:error, "Invalid coordinate format"}
|
||||
|
||||
defp handle_coordinate_result(coord) when is_tuple(coord), do: coord
|
||||
defp handle_coordinate_result(coord), do: {:ok, coord}
|
||||
|
||||
defp apply_nmea_direction(coord, direction) do
|
||||
case direction do
|
||||
"N" -> coord
|
||||
"S" -> -coord
|
||||
"E" -> coord
|
||||
"W" -> -coord
|
||||
_ -> {:error, "Invalid coordinate direction"}
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_nmea_sentence(any()) :: {:error, String.t()}
|
||||
def parse_nmea_sentence(_sentence) do
|
||||
{:error, "NMEA parsing not implemented"}
|
||||
end
|
||||
end
|
||||
|
|
@ -14,9 +14,13 @@ defmodule Parser.Object do
|
|||
<<"/", latitude_compressed::binary-size(4), longitude_compressed::binary-size(4), symbol_code::binary-size(1),
|
||||
cs::binary-size(2), compression_type::binary-size(1), comment::binary>> ->
|
||||
try do
|
||||
converted_lat = Parser.Helpers.convert_compressed_lat(latitude_compressed)
|
||||
converted_lon = Parser.Helpers.convert_compressed_lon(longitude_compressed)
|
||||
compressed_cs = Parser.Helpers.convert_compressed_cs(cs)
|
||||
converted_lat =
|
||||
Parser.CompressedPositionHelpers.convert_compressed_lat(latitude_compressed)
|
||||
|
||||
converted_lon =
|
||||
Parser.CompressedPositionHelpers.convert_compressed_lon(longitude_compressed)
|
||||
|
||||
compressed_cs = Parser.CompressedPositionHelpers.convert_compressed_cs(cs)
|
||||
|
||||
base_data = %{
|
||||
latitude: converted_lat,
|
||||
|
|
|
|||
76
lib/parser/phg_helpers.ex
Normal file
76
lib/parser/phg_helpers.ex
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
defmodule Parser.PHGHelpers do
|
||||
@moduledoc """
|
||||
PHG/DF helper functions for APRS.
|
||||
"""
|
||||
|
||||
@type phg_power :: {integer() | nil, String.t()}
|
||||
@type phg_height :: {integer() | nil, String.t()}
|
||||
@type phg_gain :: {integer() | nil, String.t()}
|
||||
@type phg_directivity :: {integer() | nil, String.t()}
|
||||
@type df_strength :: {integer() | nil, String.t()}
|
||||
|
||||
@spec parse_phg_power(integer()) :: phg_power
|
||||
def parse_phg_power(?0), do: {1, "1 watt"}
|
||||
def parse_phg_power(?1), do: {4, "4 watts"}
|
||||
def parse_phg_power(?2), do: {9, "9 watts"}
|
||||
def parse_phg_power(?3), do: {16, "16 watts"}
|
||||
def parse_phg_power(?4), do: {25, "25 watts"}
|
||||
def parse_phg_power(?5), do: {36, "36 watts"}
|
||||
def parse_phg_power(?6), do: {49, "49 watts"}
|
||||
def parse_phg_power(?7), do: {64, "64 watts"}
|
||||
def parse_phg_power(?8), do: {81, "81 watts"}
|
||||
def parse_phg_power(?9), do: {81, "81 watts"}
|
||||
def parse_phg_power(p), do: {nil, "Unknown power: #{<<p>>}"}
|
||||
|
||||
@spec parse_phg_height(integer()) :: phg_height
|
||||
def parse_phg_height(?0), do: {10, "10 feet"}
|
||||
def parse_phg_height(?1), do: {20, "20 feet"}
|
||||
def parse_phg_height(?2), do: {40, "40 feet"}
|
||||
def parse_phg_height(?3), do: {80, "80 feet"}
|
||||
def parse_phg_height(?4), do: {160, "160 feet"}
|
||||
def parse_phg_height(?5), do: {320, "320 feet"}
|
||||
def parse_phg_height(?6), do: {640, "640 feet"}
|
||||
def parse_phg_height(?7), do: {1280, "1280 feet"}
|
||||
def parse_phg_height(?8), do: {2560, "2560 feet"}
|
||||
def parse_phg_height(?9), do: {5120, "5120 feet"}
|
||||
def parse_phg_height(h), do: {nil, "Unknown height: #{<<h>>}"}
|
||||
|
||||
@spec parse_phg_gain(integer()) :: phg_gain
|
||||
def parse_phg_gain(?0), do: {0, "0 dB"}
|
||||
def parse_phg_gain(?1), do: {1, "1 dB"}
|
||||
def parse_phg_gain(?2), do: {2, "2 dB"}
|
||||
def parse_phg_gain(?3), do: {3, "3 dB"}
|
||||
def parse_phg_gain(?4), do: {4, "4 dB"}
|
||||
def parse_phg_gain(?5), do: {5, "5 dB"}
|
||||
def parse_phg_gain(?6), do: {6, "6 dB"}
|
||||
def parse_phg_gain(?7), do: {7, "7 dB"}
|
||||
def parse_phg_gain(?8), do: {8, "8 dB"}
|
||||
def parse_phg_gain(?9), do: {9, "9 dB"}
|
||||
def parse_phg_gain(g), do: {nil, "Unknown gain: #{<<g>>}"}
|
||||
|
||||
@spec parse_phg_directivity(integer()) :: phg_directivity
|
||||
def parse_phg_directivity(?0), do: {360, "Omni"}
|
||||
def parse_phg_directivity(?1), do: {45, "45° NE"}
|
||||
def parse_phg_directivity(?2), do: {90, "90° E"}
|
||||
def parse_phg_directivity(?3), do: {135, "135° SE"}
|
||||
def parse_phg_directivity(?4), do: {180, "180° S"}
|
||||
def parse_phg_directivity(?5), do: {225, "225° SW"}
|
||||
def parse_phg_directivity(?6), do: {270, "270° W"}
|
||||
def parse_phg_directivity(?7), do: {315, "315° NW"}
|
||||
def parse_phg_directivity(?8), do: {360, "360° N"}
|
||||
def parse_phg_directivity(?9), do: {nil, "Undefined"}
|
||||
def parse_phg_directivity(d), do: {nil, "Unknown directivity: #{<<d>>}"}
|
||||
|
||||
@spec parse_df_strength(integer()) :: df_strength
|
||||
def parse_df_strength(?0), do: {0, "0 dB"}
|
||||
def parse_df_strength(?1), do: {1, "3 dB above S0"}
|
||||
def parse_df_strength(?2), do: {2, "6 dB above S0"}
|
||||
def parse_df_strength(?3), do: {3, "9 dB above S0"}
|
||||
def parse_df_strength(?4), do: {4, "12 dB above S0"}
|
||||
def parse_df_strength(?5), do: {5, "15 dB above S0"}
|
||||
def parse_df_strength(?6), do: {6, "18 dB above S0"}
|
||||
def parse_df_strength(?7), do: {7, "21 dB above S0"}
|
||||
def parse_df_strength(?8), do: {8, "24 dB above S0"}
|
||||
def parse_df_strength(?9), do: {9, "27 dB above S0"}
|
||||
def parse_df_strength(s), do: {nil, "Unknown strength: #{<<s>>}"}
|
||||
end
|
||||
33
lib/parser/special_data_helpers.ex
Normal file
33
lib/parser/special_data_helpers.ex
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
defmodule Parser.SpecialDataHelpers do
|
||||
@moduledoc """
|
||||
Helpers for special APRS data types: PEET logging and invalid/test data.
|
||||
"""
|
||||
|
||||
def parse_peet_logging(<<"*", peet_data::binary>>) do
|
||||
%{
|
||||
peet_data: peet_data,
|
||||
data_type: :peet_logging
|
||||
}
|
||||
end
|
||||
|
||||
def parse_peet_logging(data) do
|
||||
%{
|
||||
peet_data: data,
|
||||
data_type: :peet_logging
|
||||
}
|
||||
end
|
||||
|
||||
def parse_invalid_test_data(<<",", test_data::binary>>) do
|
||||
%{
|
||||
test_data: test_data,
|
||||
data_type: :invalid_test_data
|
||||
}
|
||||
end
|
||||
|
||||
def parse_invalid_test_data(data) do
|
||||
%{
|
||||
test_data: data,
|
||||
data_type: :invalid_test_data
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -14,9 +14,9 @@ defmodule Parser.Telemetry do
|
|||
digital_values = values |> Enum.drop(5) |> Enum.take(8)
|
||||
|
||||
%{
|
||||
sequence_number: Parser.Helpers.parse_telemetry_sequence(seq),
|
||||
analog_values: Parser.Helpers.parse_analog_values(analog_values),
|
||||
digital_values: Parser.Helpers.parse_digital_values(digital_values),
|
||||
sequence_number: Parser.TelemetryHelpers.parse_telemetry_sequence(seq),
|
||||
analog_values: Parser.TelemetryHelpers.parse_analog_values(analog_values),
|
||||
digital_values: Parser.TelemetryHelpers.parse_digital_values(digital_values),
|
||||
data_type: :telemetry,
|
||||
raw_data: rest
|
||||
}
|
||||
|
|
@ -44,9 +44,9 @@ defmodule Parser.Telemetry do
|
|||
|> Enum.chunk_every(3)
|
||||
|> Enum.map(fn [a, b, c] ->
|
||||
%{
|
||||
a: Parser.Helpers.parse_coefficient(a),
|
||||
b: Parser.Helpers.parse_coefficient(b),
|
||||
c: Parser.Helpers.parse_coefficient(c)
|
||||
a: Parser.TelemetryHelpers.parse_coefficient(a),
|
||||
b: Parser.TelemetryHelpers.parse_coefficient(b),
|
||||
c: Parser.TelemetryHelpers.parse_coefficient(c)
|
||||
}
|
||||
end)
|
||||
|
||||
|
|
|
|||
59
lib/parser/telemetry_helpers.ex
Normal file
59
lib/parser/telemetry_helpers.ex
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
defmodule Parser.TelemetryHelpers do
|
||||
@moduledoc """
|
||||
Telemetry helpers for APRS.
|
||||
"""
|
||||
|
||||
@spec parse_telemetry_sequence(String.t()) :: integer() | nil
|
||||
def parse_telemetry_sequence(seq) do
|
||||
case Integer.parse(seq) do
|
||||
{num, _} -> num
|
||||
:error -> nil
|
||||
end
|
||||
end
|
||||
|
||||
def parse_analog_values(values) do
|
||||
Enum.map(values, &parse_analog_value/1)
|
||||
end
|
||||
|
||||
defp parse_analog_value("") do
|
||||
nil
|
||||
end
|
||||
|
||||
defp parse_analog_value(value) do
|
||||
case Float.parse(value) do
|
||||
{float_val, _} -> float_val
|
||||
:error -> nil
|
||||
end
|
||||
end
|
||||
|
||||
def parse_digital_values(values) do
|
||||
Enum.flat_map(values, &parse_digital_value/1)
|
||||
end
|
||||
|
||||
defp parse_digital_value("1"), do: [true]
|
||||
|
||||
defp parse_digital_value(value) when is_binary(value) do
|
||||
value
|
||||
|> String.graphemes()
|
||||
|> Enum.map(fn
|
||||
"1" -> true
|
||||
"0" -> false
|
||||
_ -> nil
|
||||
end)
|
||||
end
|
||||
|
||||
defp parse_digital_value(_), do: [nil]
|
||||
|
||||
def parse_coefficient(coeff) do
|
||||
case Float.parse(coeff) do
|
||||
{float_val, _} ->
|
||||
float_val
|
||||
|
||||
:error ->
|
||||
case Integer.parse(coeff) do
|
||||
{int_val, _} -> int_val
|
||||
:error -> coeff
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
72
lib/parser/utility_helpers.ex
Normal file
72
lib/parser/utility_helpers.ex
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
defmodule Parser.UtilityHelpers do
|
||||
@moduledoc """
|
||||
Utility and ambiguity helpers for APRS.
|
||||
"""
|
||||
|
||||
def count_spaces(str) do
|
||||
str |> String.graphemes() |> Enum.count(fn c -> c == " " end)
|
||||
end
|
||||
|
||||
def count_leading_braces(packet), do: count_leading_braces(packet, 0)
|
||||
|
||||
def count_leading_braces(<<"}", rest::binary>>, count), do: count_leading_braces(rest, count + 1)
|
||||
|
||||
def count_leading_braces(_packet, count), do: count
|
||||
|
||||
def calculate_position_ambiguity(latitude, longitude) do
|
||||
lat_spaces = count_spaces(latitude)
|
||||
lon_spaces = count_spaces(longitude)
|
||||
|
||||
Map.get(
|
||||
%{{0, 0} => 0, {1, 1} => 1, {2, 2} => 2, {3, 3} => 3, {4, 4} => 4},
|
||||
{lat_spaces, lon_spaces},
|
||||
0
|
||||
)
|
||||
end
|
||||
|
||||
def find_matches(regex, text) do
|
||||
case Regex.names(regex) do
|
||||
[] ->
|
||||
matches = Regex.run(regex, text)
|
||||
|
||||
Enum.reduce(Enum.with_index(matches), %{}, fn {match, index}, acc ->
|
||||
Map.put(acc, index, match)
|
||||
end)
|
||||
|
||||
_ ->
|
||||
Regex.named_captures(regex, text)
|
||||
end
|
||||
end
|
||||
|
||||
def validate_position_data(latitude, longitude) do
|
||||
import Decimal, only: [new: 1, add: 2, negate: 1]
|
||||
|
||||
lat =
|
||||
case Regex.run(~r/^(\d{2})(\d{2}\.\d+)([NS])$/, latitude) do
|
||||
[_, degrees, minutes, direction] ->
|
||||
lat_val = add(new(degrees), Decimal.div(new(minutes), new("60")))
|
||||
if direction == "S", do: negate(lat_val), else: lat_val
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
|
||||
lon =
|
||||
case Regex.run(~r/^(\d{3})(\d{2}\.\d+)([EW])$/, longitude) do
|
||||
[_, degrees, minutes, direction] ->
|
||||
lon_val = add(new(degrees), Decimal.div(new(minutes), new("60")))
|
||||
if direction == "W", do: negate(lon_val), else: lon_val
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
|
||||
if is_struct(lat, Decimal) and is_struct(lon, Decimal) do
|
||||
{:ok, {lat, lon}}
|
||||
else
|
||||
{:error, :invalid_position}
|
||||
end
|
||||
end
|
||||
|
||||
def validate_timestamp(_time), do: nil
|
||||
end
|
||||
|
|
@ -22,21 +22,21 @@ defmodule Parser.Weather do
|
|||
"""
|
||||
@spec parse_weather_data(String.t()) :: map()
|
||||
def parse_weather_data(weather_data) do
|
||||
timestamp = Parser.Helpers.extract_timestamp(weather_data)
|
||||
weather_data = Parser.Helpers.remove_timestamp(weather_data)
|
||||
timestamp = Parser.WeatherHelpers.extract_timestamp(weather_data)
|
||||
weather_data = Parser.WeatherHelpers.remove_timestamp(weather_data)
|
||||
|
||||
weather_values = %{
|
||||
wind_direction: Parser.Helpers.parse_wind_direction(weather_data),
|
||||
wind_speed: Parser.Helpers.parse_wind_speed(weather_data),
|
||||
wind_gust: Parser.Helpers.parse_wind_gust(weather_data),
|
||||
temperature: Parser.Helpers.parse_temperature(weather_data),
|
||||
rain_1h: Parser.Helpers.parse_rainfall_1h(weather_data),
|
||||
rain_24h: Parser.Helpers.parse_rainfall_24h(weather_data),
|
||||
rain_since_midnight: Parser.Helpers.parse_rainfall_since_midnight(weather_data),
|
||||
humidity: Parser.Helpers.parse_humidity(weather_data),
|
||||
pressure: Parser.Helpers.parse_pressure(weather_data),
|
||||
luminosity: Parser.Helpers.parse_luminosity(weather_data),
|
||||
snow: Parser.Helpers.parse_snow(weather_data)
|
||||
wind_direction: Parser.WeatherHelpers.parse_wind_direction(weather_data),
|
||||
wind_speed: Parser.WeatherHelpers.parse_wind_speed(weather_data),
|
||||
wind_gust: Parser.WeatherHelpers.parse_wind_gust(weather_data),
|
||||
temperature: Parser.WeatherHelpers.parse_temperature(weather_data),
|
||||
rain_1h: Parser.WeatherHelpers.parse_rainfall_1h(weather_data),
|
||||
rain_24h: Parser.WeatherHelpers.parse_rainfall_24h(weather_data),
|
||||
rain_since_midnight: Parser.WeatherHelpers.parse_rainfall_since_midnight(weather_data),
|
||||
humidity: Parser.WeatherHelpers.parse_humidity(weather_data),
|
||||
pressure: Parser.WeatherHelpers.parse_pressure(weather_data),
|
||||
luminosity: Parser.WeatherHelpers.parse_luminosity(weather_data),
|
||||
snow: Parser.WeatherHelpers.parse_snow(weather_data)
|
||||
}
|
||||
|
||||
result = %{timestamp: timestamp, data_type: :weather, raw_weather_data: weather_data}
|
||||
|
|
|
|||
116
lib/parser/weather_helpers.ex
Normal file
116
lib/parser/weather_helpers.ex
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
defmodule Parser.WeatherHelpers do
|
||||
@moduledoc """
|
||||
Weather field extraction helpers for APRS.
|
||||
"""
|
||||
|
||||
@spec extract_timestamp(String.t()) :: String.t() | nil
|
||||
def extract_timestamp(weather_data) do
|
||||
case Regex.run(~r/^(\d{6}[hz\/])/, weather_data) do
|
||||
[_, timestamp] -> timestamp
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec remove_timestamp(String.t()) :: String.t()
|
||||
def remove_timestamp(weather_data) do
|
||||
case Regex.run(~r/^\d{6}[hz\/]/, weather_data) do
|
||||
[timestamp] -> String.replace(weather_data, timestamp, "")
|
||||
nil -> weather_data
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_wind_direction(String.t()) :: integer() | nil
|
||||
def parse_wind_direction(weather_data) do
|
||||
case Regex.run(~r/(\d{3})\//, weather_data) do
|
||||
[_, direction] -> String.to_integer(direction)
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_wind_speed(String.t()) :: integer() | nil
|
||||
def parse_wind_speed(weather_data) do
|
||||
case Regex.run(~r/\/(\d{3})/, weather_data) do
|
||||
[_, speed] -> String.to_integer(speed)
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_wind_gust(String.t()) :: integer() | nil
|
||||
def parse_wind_gust(weather_data) do
|
||||
case Regex.run(~r/g(\d{3})/, weather_data) do
|
||||
[_, gust] -> String.to_integer(gust)
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_temperature(String.t()) :: integer() | nil
|
||||
def parse_temperature(weather_data) do
|
||||
case Regex.run(~r/t(-?\d{3})/, weather_data) do
|
||||
[_, temp] -> String.to_integer(temp)
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_rainfall_1h(String.t()) :: integer() | nil
|
||||
def parse_rainfall_1h(weather_data) do
|
||||
case Regex.run(~r/r(\d{3})/, weather_data) do
|
||||
[_, rain] -> String.to_integer(rain)
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_rainfall_24h(String.t()) :: integer() | nil
|
||||
def parse_rainfall_24h(weather_data) do
|
||||
case Regex.run(~r/p(\d{3})/, weather_data) do
|
||||
[_, rain] -> String.to_integer(rain)
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_rainfall_since_midnight(String.t()) :: integer() | nil
|
||||
def parse_rainfall_since_midnight(weather_data) do
|
||||
case Regex.run(~r/P(\d{3})/, weather_data) do
|
||||
[_, rain] -> String.to_integer(rain)
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_humidity(String.t()) :: integer() | nil
|
||||
def parse_humidity(weather_data) do
|
||||
case Regex.run(~r/h(\d{2})/, weather_data) do
|
||||
[_, humidity] ->
|
||||
val = String.to_integer(humidity)
|
||||
normalize_humidity(val)
|
||||
|
||||
nil ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_humidity(0), do: 100
|
||||
defp normalize_humidity(val), do: val
|
||||
|
||||
@spec parse_pressure(String.t()) :: float() | nil
|
||||
def parse_pressure(weather_data) do
|
||||
case Regex.run(~r/b(\d{5})/, weather_data) do
|
||||
[_, pressure] -> String.to_integer(pressure) / 10.0
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_luminosity(String.t()) :: integer() | nil
|
||||
def parse_luminosity(weather_data) do
|
||||
case Regex.run(~r/[lL](\d{3})/, weather_data) do
|
||||
[_, luminosity] -> String.to_integer(luminosity)
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec parse_snow(String.t()) :: integer() | nil
|
||||
def parse_snow(weather_data) do
|
||||
case Regex.run(~r/s(\d{3})/, weather_data) do
|
||||
[_, snow] -> String.to_integer(snow)
|
||||
nil -> nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,65 +1,66 @@
|
|||
defmodule Parser.HelpersTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Parser.Helpers
|
||||
|
||||
describe "NMEA helpers" do
|
||||
test "parse_nmea_coordinate parses valid coordinates" do
|
||||
assert Helpers.parse_nmea_coordinate("4916.45", "N") == {:ok, 49.1645}
|
||||
{:ok, val} = Helpers.parse_nmea_coordinate("12311.12", "W")
|
||||
assert Parser.NMEAHelpers.parse_nmea_coordinate("4916.45", "N") == {:ok, 49.1645}
|
||||
{:ok, val} = Parser.NMEAHelpers.parse_nmea_coordinate("12311.12", "W")
|
||||
assert_in_delta val, -123.1112, 1.0e-6
|
||||
end
|
||||
|
||||
test "parse_nmea_coordinate errors on invalid input" do
|
||||
assert Helpers.parse_nmea_coordinate("bad", "N") == {:error, "Invalid coordinate value"}
|
||||
assert Parser.NMEAHelpers.parse_nmea_coordinate("bad", "N") ==
|
||||
{:error, "Invalid coordinate value"}
|
||||
|
||||
assert Helpers.parse_nmea_coordinate("4916.45", "Q") ==
|
||||
assert Parser.NMEAHelpers.parse_nmea_coordinate("4916.45", "Q") ==
|
||||
{:error, "Invalid coordinate direction"}
|
||||
|
||||
assert Helpers.parse_nmea_coordinate(nil, nil) == {:error, "Invalid coordinate format"}
|
||||
assert Parser.NMEAHelpers.parse_nmea_coordinate(nil, nil) ==
|
||||
{:error, "Invalid coordinate format"}
|
||||
end
|
||||
|
||||
test "parse_nmea_sentence returns not implemented" do
|
||||
assert Helpers.parse_nmea_sentence("anything") == {:error, "NMEA parsing not implemented"}
|
||||
assert Parser.NMEAHelpers.parse_nmea_sentence("anything") ==
|
||||
{:error, "NMEA parsing not implemented"}
|
||||
end
|
||||
end
|
||||
|
||||
describe "PHG/DF helpers" do
|
||||
test "parse_phg_power returns correct values" do
|
||||
assert Helpers.parse_phg_power(?0) == {1, "1 watt"}
|
||||
assert Helpers.parse_phg_power(?9) == {81, "81 watts"}
|
||||
assert Helpers.parse_phg_power(?X) == {nil, "Unknown power: X"}
|
||||
assert Parser.PHGHelpers.parse_phg_power(?0) == {1, "1 watt"}
|
||||
assert Parser.PHGHelpers.parse_phg_power(?9) == {81, "81 watts"}
|
||||
assert Parser.PHGHelpers.parse_phg_power(?X) == {nil, "Unknown power: X"}
|
||||
end
|
||||
|
||||
test "parse_phg_height returns correct values" do
|
||||
assert Helpers.parse_phg_height(?0) == {10, "10 feet"}
|
||||
assert Helpers.parse_phg_height(?9) == {5120, "5120 feet"}
|
||||
assert Helpers.parse_phg_height(?X) == {nil, "Unknown height: X"}
|
||||
assert Parser.PHGHelpers.parse_phg_height(?0) == {10, "10 feet"}
|
||||
assert Parser.PHGHelpers.parse_phg_height(?9) == {5120, "5120 feet"}
|
||||
assert Parser.PHGHelpers.parse_phg_height(?X) == {nil, "Unknown height: X"}
|
||||
end
|
||||
|
||||
test "parse_phg_gain returns correct values" do
|
||||
assert Helpers.parse_phg_gain(?0) == {0, "0 dB"}
|
||||
assert Helpers.parse_phg_gain(?9) == {9, "9 dB"}
|
||||
assert Helpers.parse_phg_gain(?X) == {nil, "Unknown gain: X"}
|
||||
assert Parser.PHGHelpers.parse_phg_gain(?0) == {0, "0 dB"}
|
||||
assert Parser.PHGHelpers.parse_phg_gain(?9) == {9, "9 dB"}
|
||||
assert Parser.PHGHelpers.parse_phg_gain(?X) == {nil, "Unknown gain: X"}
|
||||
end
|
||||
|
||||
test "parse_phg_directivity returns correct values" do
|
||||
assert Helpers.parse_phg_directivity(?0) == {360, "Omni"}
|
||||
assert Helpers.parse_phg_directivity(?9) == {nil, "Undefined"}
|
||||
assert Helpers.parse_phg_directivity(?X) == {nil, "Unknown directivity: X"}
|
||||
assert Parser.PHGHelpers.parse_phg_directivity(?0) == {360, "Omni"}
|
||||
assert Parser.PHGHelpers.parse_phg_directivity(?9) == {nil, "Undefined"}
|
||||
assert Parser.PHGHelpers.parse_phg_directivity(?X) == {nil, "Unknown directivity: X"}
|
||||
end
|
||||
|
||||
test "parse_df_strength returns correct values" do
|
||||
assert Helpers.parse_df_strength(?0) == {0, "0 dB"}
|
||||
assert Helpers.parse_df_strength(?9) == {9, "27 dB above S0"}
|
||||
assert Helpers.parse_df_strength(?X) == {nil, "Unknown strength: X"}
|
||||
assert Parser.PHGHelpers.parse_df_strength(?0) == {0, "0 dB"}
|
||||
assert Parser.PHGHelpers.parse_df_strength(?9) == {9, "27 dB above S0"}
|
||||
assert Parser.PHGHelpers.parse_df_strength(?X) == {nil, "Unknown strength: X"}
|
||||
end
|
||||
end
|
||||
|
||||
describe "compressed position helpers" do
|
||||
test "convert_compressed_lat and lon handle valid input" do
|
||||
{:ok, lat} = Helpers.convert_compressed_lat("9RZV")
|
||||
{:ok, lon} = Helpers.convert_compressed_lon("9RZV")
|
||||
{:ok, lat} = Parser.CompressedPositionHelpers.convert_compressed_lat("9RZV")
|
||||
{:ok, lon} = Parser.CompressedPositionHelpers.convert_compressed_lon("9RZV")
|
||||
assert is_float(lat)
|
||||
assert is_float(lon)
|
||||
assert lat >= -90 and lat <= 90
|
||||
|
|
@ -67,24 +68,31 @@ defmodule Parser.HelpersTest do
|
|||
end
|
||||
|
||||
test "convert_compressed_lat and lon return error for invalid input" do
|
||||
assert Helpers.convert_compressed_lat("abcde") == {:error, "Invalid compressed latitude"}
|
||||
assert Helpers.convert_compressed_lon("abcde") == {:error, "Invalid compressed longitude"}
|
||||
assert Helpers.convert_compressed_lat(123) == {:error, "Invalid compressed latitude"}
|
||||
assert Helpers.convert_compressed_lon(123) == {:error, "Invalid compressed longitude"}
|
||||
assert Parser.CompressedPositionHelpers.convert_compressed_lat("abcde") ==
|
||||
{:error, "Invalid compressed latitude"}
|
||||
|
||||
assert Parser.CompressedPositionHelpers.convert_compressed_lon("abcde") ==
|
||||
{:error, "Invalid compressed longitude"}
|
||||
|
||||
assert Parser.CompressedPositionHelpers.convert_compressed_lat(123) ==
|
||||
{:error, "Invalid compressed latitude"}
|
||||
|
||||
assert Parser.CompressedPositionHelpers.convert_compressed_lon(123) ==
|
||||
{:error, "Invalid compressed longitude"}
|
||||
end
|
||||
end
|
||||
|
||||
describe "KISS/TNC2 utilities" do
|
||||
test "kiss_to_tnc2 decodes KISS frames" do
|
||||
frame = <<0xC0, 0x00, 65, 66, 67, 0xC0>>
|
||||
assert Helpers.kiss_to_tnc2(frame) == "ABC"
|
||||
assert Parser.KISSHelpers.kiss_to_tnc2(frame) == "ABC"
|
||||
|
||||
frame = <<0xC0, 0x00, 0x41, 0xDB, 0xDC, 0x42, 0xDB, 0xDD, 0x43, 0xC0>>
|
||||
assert Helpers.kiss_to_tnc2(frame) == "A\xC0B\xDB" <> "C"
|
||||
assert Parser.KISSHelpers.kiss_to_tnc2(frame) == "A\xC0B\xDB" <> "C"
|
||||
end
|
||||
|
||||
test "kiss_to_tnc2 returns error for invalid frame" do
|
||||
assert Helpers.kiss_to_tnc2("notkiss") == %{
|
||||
assert Parser.KISSHelpers.kiss_to_tnc2("notkiss") == %{
|
||||
error_code: :packet_invalid,
|
||||
error_message: "Unknown error"
|
||||
}
|
||||
|
|
@ -92,7 +100,7 @@ defmodule Parser.HelpersTest do
|
|||
|
||||
test "tnc2_to_kiss encodes TNC2 to KISS" do
|
||||
tnc2 = "A\xDBB\xC0C"
|
||||
kiss = Helpers.tnc2_to_kiss(tnc2)
|
||||
kiss = Parser.KISSHelpers.tnc2_to_kiss(tnc2)
|
||||
assert :binary.part(kiss, 0, 1) == <<0xC0>>
|
||||
assert :binary.part(kiss, byte_size(kiss) - 1, 1) == <<0xC0>>
|
||||
end
|
||||
|
|
@ -100,117 +108,129 @@ defmodule Parser.HelpersTest do
|
|||
|
||||
describe "telemetry helpers" do
|
||||
test "parse_telemetry_sequence parses valid values" do
|
||||
assert Helpers.parse_telemetry_sequence("123") == 123
|
||||
assert Helpers.parse_telemetry_sequence("abc") == nil
|
||||
assert Parser.TelemetryHelpers.parse_telemetry_sequence("123") == 123
|
||||
assert Parser.TelemetryHelpers.parse_telemetry_sequence("abc") == nil
|
||||
end
|
||||
|
||||
test "parse_analog_values parses valid values" do
|
||||
assert Helpers.parse_analog_values(["1.23", "4.56", ""]) == [1.23, 4.56, nil]
|
||||
assert Helpers.parse_analog_values(["abc", "1.23"]) == [nil, 1.23]
|
||||
assert Parser.TelemetryHelpers.parse_analog_values(["1.23", "4.56", ""]) == [
|
||||
1.23,
|
||||
4.56,
|
||||
nil
|
||||
]
|
||||
|
||||
assert Parser.TelemetryHelpers.parse_analog_values(["abc", "1.23"]) == [nil, 1.23]
|
||||
end
|
||||
|
||||
test "parse_digital_values parses valid values" do
|
||||
assert Helpers.parse_digital_values(["101"]) == [true, false, true]
|
||||
assert Helpers.parse_digital_values(["1", "0"]) == [true, false]
|
||||
assert Helpers.parse_digital_values(["abc"]) == [nil, nil, nil]
|
||||
assert Helpers.parse_digital_values([123]) == [nil]
|
||||
assert Parser.TelemetryHelpers.parse_digital_values(["101"]) == [true, false, true]
|
||||
assert Parser.TelemetryHelpers.parse_digital_values(["1", "0"]) == [true, false]
|
||||
assert Parser.TelemetryHelpers.parse_digital_values(["abc"]) == [nil, nil, nil]
|
||||
assert Parser.TelemetryHelpers.parse_digital_values([123]) == [nil]
|
||||
end
|
||||
|
||||
test "parse_coefficient parses valid values" do
|
||||
assert Helpers.parse_coefficient("1.23") == 1.23
|
||||
assert Helpers.parse_coefficient("123") == 123
|
||||
assert Helpers.parse_coefficient("abc") == "abc"
|
||||
assert Parser.TelemetryHelpers.parse_coefficient("1.23") == 1.23
|
||||
assert Parser.TelemetryHelpers.parse_coefficient("123") == 123
|
||||
assert Parser.TelemetryHelpers.parse_coefficient("abc") == "abc"
|
||||
end
|
||||
end
|
||||
|
||||
describe "weather helpers" do
|
||||
test "extract_timestamp handles valid formats" do
|
||||
assert Helpers.extract_timestamp("092345z123") == "092345z"
|
||||
assert Helpers.extract_timestamp("092345h123") == "092345h"
|
||||
assert Helpers.extract_timestamp("092345/123") == "092345/"
|
||||
assert Helpers.extract_timestamp("12345") == nil
|
||||
assert Parser.WeatherHelpers.extract_timestamp("092345z123") == "092345z"
|
||||
assert Parser.WeatherHelpers.extract_timestamp("092345h123") == "092345h"
|
||||
assert Parser.WeatherHelpers.extract_timestamp("092345/123") == "092345/"
|
||||
assert Parser.WeatherHelpers.extract_timestamp("12345") == nil
|
||||
end
|
||||
|
||||
test "remove_timestamp removes timestamp when present" do
|
||||
assert Helpers.remove_timestamp("092345z123") == "123"
|
||||
assert Helpers.remove_timestamp("092345h456") == "456"
|
||||
assert Helpers.remove_timestamp("092345/789") == "789"
|
||||
assert Helpers.remove_timestamp("12345") == "12345"
|
||||
assert Parser.WeatherHelpers.remove_timestamp("092345z123") == "123"
|
||||
assert Parser.WeatherHelpers.remove_timestamp("092345h456") == "456"
|
||||
assert Parser.WeatherHelpers.remove_timestamp("092345/789") == "789"
|
||||
assert Parser.WeatherHelpers.remove_timestamp("12345") == "12345"
|
||||
end
|
||||
|
||||
test "parse_wind_direction extracts direction" do
|
||||
assert Helpers.parse_wind_direction("123/045") == 123
|
||||
assert Helpers.parse_wind_direction("invalid") == nil
|
||||
assert Parser.WeatherHelpers.parse_wind_direction("123/045") == 123
|
||||
assert Parser.WeatherHelpers.parse_wind_direction("invalid") == nil
|
||||
end
|
||||
|
||||
test "parse_wind_speed extracts speed" do
|
||||
assert Helpers.parse_wind_speed("123/045") == 45
|
||||
assert Helpers.parse_wind_speed("invalid") == nil
|
||||
assert Parser.WeatherHelpers.parse_wind_speed("123/045") == 45
|
||||
assert Parser.WeatherHelpers.parse_wind_speed("invalid") == nil
|
||||
end
|
||||
|
||||
test "parse_wind_gust extracts gust speed" do
|
||||
assert Helpers.parse_wind_gust("g045") == 45
|
||||
assert Helpers.parse_wind_gust("invalid") == nil
|
||||
assert Parser.WeatherHelpers.parse_wind_gust("g045") == 45
|
||||
assert Parser.WeatherHelpers.parse_wind_gust("invalid") == nil
|
||||
end
|
||||
|
||||
test "parse_temperature extracts temperature" do
|
||||
assert Helpers.parse_temperature("t072") == 72
|
||||
assert Helpers.parse_temperature("invalid") == nil
|
||||
assert Parser.WeatherHelpers.parse_temperature("t072") == 72
|
||||
assert Parser.WeatherHelpers.parse_temperature("invalid") == nil
|
||||
end
|
||||
|
||||
test "parse_rainfall_1h extracts rainfall" do
|
||||
assert Helpers.parse_rainfall_1h("r010") == 10
|
||||
assert Helpers.parse_rainfall_1h("invalid") == nil
|
||||
assert Parser.WeatherHelpers.parse_rainfall_1h("r010") == 10
|
||||
assert Parser.WeatherHelpers.parse_rainfall_1h("invalid") == nil
|
||||
end
|
||||
|
||||
test "parse_rainfall_24h extracts rainfall" do
|
||||
assert Helpers.parse_rainfall_24h("p024") == 24
|
||||
assert Helpers.parse_rainfall_24h("invalid") == nil
|
||||
assert Parser.WeatherHelpers.parse_rainfall_24h("p024") == 24
|
||||
assert Parser.WeatherHelpers.parse_rainfall_24h("invalid") == nil
|
||||
end
|
||||
|
||||
test "parse_rainfall_since_midnight extracts rainfall" do
|
||||
assert Helpers.parse_rainfall_since_midnight("P036") == 36
|
||||
assert Helpers.parse_rainfall_since_midnight("invalid") == nil
|
||||
assert Parser.WeatherHelpers.parse_rainfall_since_midnight("P036") == 36
|
||||
assert Parser.WeatherHelpers.parse_rainfall_since_midnight("invalid") == nil
|
||||
end
|
||||
|
||||
test "parse_humidity extracts humidity" do
|
||||
assert Helpers.parse_humidity("h00") == 100
|
||||
assert Helpers.parse_humidity("h75") == 75
|
||||
assert Helpers.parse_humidity("invalid") == nil
|
||||
assert Parser.WeatherHelpers.parse_humidity("h00") == 100
|
||||
assert Parser.WeatherHelpers.parse_humidity("h75") == 75
|
||||
assert Parser.WeatherHelpers.parse_humidity("invalid") == nil
|
||||
end
|
||||
|
||||
test "parse_pressure extracts pressure" do
|
||||
assert Helpers.parse_pressure("b10150") == 1015.0
|
||||
assert Helpers.parse_pressure("invalid") == nil
|
||||
assert Parser.WeatherHelpers.parse_pressure("b10150") == 1015.0
|
||||
assert Parser.WeatherHelpers.parse_pressure("invalid") == nil
|
||||
end
|
||||
|
||||
test "parse_luminosity extracts luminosity" do
|
||||
assert Helpers.parse_luminosity("l123") == 123
|
||||
assert Helpers.parse_luminosity("L456") == 456
|
||||
assert Helpers.parse_luminosity("invalid") == nil
|
||||
assert Parser.WeatherHelpers.parse_luminosity("l123") == 123
|
||||
assert Parser.WeatherHelpers.parse_luminosity("L456") == 456
|
||||
assert Parser.WeatherHelpers.parse_luminosity("invalid") == nil
|
||||
end
|
||||
|
||||
test "parse_snow extracts snow depth" do
|
||||
assert Helpers.parse_snow("s012") == 12
|
||||
assert Helpers.parse_snow("invalid") == nil
|
||||
assert Parser.WeatherHelpers.parse_snow("s012") == 12
|
||||
assert Parser.WeatherHelpers.parse_snow("invalid") == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "PEET logging helpers" do
|
||||
test "parse_peet_logging handles data with and without prefix" do
|
||||
assert Helpers.parse_peet_logging("*1234") == %{peet_data: "1234", data_type: :peet_logging}
|
||||
assert Helpers.parse_peet_logging("1234") == %{peet_data: "1234", data_type: :peet_logging}
|
||||
assert Parser.SpecialDataHelpers.parse_peet_logging("*1234") == %{
|
||||
peet_data: "1234",
|
||||
data_type: :peet_logging
|
||||
}
|
||||
|
||||
assert Parser.SpecialDataHelpers.parse_peet_logging("1234") == %{
|
||||
peet_data: "1234",
|
||||
data_type: :peet_logging
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
describe "invalid/test data helpers" do
|
||||
test "parse_invalid_test_data handles data with and without comma" do
|
||||
assert Helpers.parse_invalid_test_data(",1234") == %{
|
||||
assert Parser.SpecialDataHelpers.parse_invalid_test_data(",1234") == %{
|
||||
test_data: "1234",
|
||||
data_type: :invalid_test_data
|
||||
}
|
||||
|
||||
assert Helpers.parse_invalid_test_data("1234") == %{
|
||||
assert Parser.SpecialDataHelpers.parse_invalid_test_data("1234") == %{
|
||||
test_data: "1234",
|
||||
data_type: :invalid_test_data
|
||||
}
|
||||
|
|
@ -219,53 +239,63 @@ defmodule Parser.HelpersTest do
|
|||
|
||||
describe "utility helpers" do
|
||||
test "count_spaces counts spaces correctly" do
|
||||
assert Helpers.count_spaces("a b c") == 2
|
||||
assert Helpers.count_spaces("abc") == 0
|
||||
assert Helpers.count_spaces(" ") == 1
|
||||
assert Parser.UtilityHelpers.count_spaces("a b c") == 2
|
||||
assert Parser.UtilityHelpers.count_spaces("abc") == 0
|
||||
assert Parser.UtilityHelpers.count_spaces(" ") == 1
|
||||
end
|
||||
|
||||
test "count_leading_braces counts braces correctly" do
|
||||
assert Helpers.count_leading_braces("}}abc") == 2
|
||||
assert Helpers.count_leading_braces("abc") == 0
|
||||
assert Helpers.count_leading_braces("}a}bc") == 1
|
||||
assert Parser.UtilityHelpers.count_leading_braces("}}abc") == 2
|
||||
assert Parser.UtilityHelpers.count_leading_braces("abc") == 0
|
||||
assert Parser.UtilityHelpers.count_leading_braces("}a}bc") == 1
|
||||
end
|
||||
|
||||
test "calculate_position_ambiguity determines ambiguity level" do
|
||||
assert Helpers.calculate_position_ambiguity("12 34.56N", "123 45.67W") == 1
|
||||
assert Helpers.calculate_position_ambiguity("12 34.56N", "123 45.67W") == 2
|
||||
assert Helpers.calculate_position_ambiguity("12 34.56N", "123 45.67W") == 3
|
||||
assert Helpers.calculate_position_ambiguity("12 34.56N", "123 45.67W") == 4
|
||||
assert Helpers.calculate_position_ambiguity("1234.56N", "12345.67W") == 0
|
||||
assert Parser.UtilityHelpers.calculate_position_ambiguity("12 34.56N", "123 45.67W") == 1
|
||||
assert Parser.UtilityHelpers.calculate_position_ambiguity("12 34.56N", "123 45.67W") == 2
|
||||
|
||||
assert Parser.UtilityHelpers.calculate_position_ambiguity("12 34.56N", "123 45.67W") ==
|
||||
3
|
||||
|
||||
assert Parser.UtilityHelpers.calculate_position_ambiguity("12 34.56N", "123 45.67W") ==
|
||||
4
|
||||
|
||||
assert Parser.UtilityHelpers.calculate_position_ambiguity("1234.56N", "12345.67W") == 0
|
||||
end
|
||||
|
||||
test "calculate_compressed_ambiguity determines compressed ambiguity" do
|
||||
assert Helpers.calculate_compressed_ambiguity(" ") == 0
|
||||
assert Helpers.calculate_compressed_ambiguity("!") == 1
|
||||
assert Helpers.calculate_compressed_ambiguity("\"") == 2
|
||||
assert Helpers.calculate_compressed_ambiguity("#") == 3
|
||||
assert Helpers.calculate_compressed_ambiguity("$") == 4
|
||||
assert Helpers.calculate_compressed_ambiguity("X") == 0
|
||||
assert Parser.CompressedPositionHelpers.calculate_compressed_ambiguity(" ") == 0
|
||||
assert Parser.CompressedPositionHelpers.calculate_compressed_ambiguity("!") == 1
|
||||
assert Parser.CompressedPositionHelpers.calculate_compressed_ambiguity("\"") == 2
|
||||
assert Parser.CompressedPositionHelpers.calculate_compressed_ambiguity("#") == 3
|
||||
assert Parser.CompressedPositionHelpers.calculate_compressed_ambiguity("$") == 4
|
||||
assert Parser.CompressedPositionHelpers.calculate_compressed_ambiguity("X") == 0
|
||||
end
|
||||
|
||||
test "validate_position_data validates positions" do
|
||||
assert {:ok, {lat, lon}} = Helpers.validate_position_data("4916.45N", "12311.12W")
|
||||
assert {:ok, {lat, lon}} =
|
||||
Parser.UtilityHelpers.validate_position_data("4916.45N", "12311.12W")
|
||||
|
||||
assert_in_delta Decimal.to_float(lat), 49.2742, 0.0001
|
||||
assert_in_delta Decimal.to_float(lon), -123.1853, 0.0001
|
||||
|
||||
assert Helpers.validate_position_data("invalid", "12311.12W") == {:error, :invalid_position}
|
||||
assert Helpers.validate_position_data("4916.45N", "invalid") == {:error, :invalid_position}
|
||||
assert Parser.UtilityHelpers.validate_position_data("invalid", "12311.12W") ==
|
||||
{:error, :invalid_position}
|
||||
|
||||
assert Parser.UtilityHelpers.validate_position_data("4916.45N", "invalid") ==
|
||||
{:error, :invalid_position}
|
||||
end
|
||||
|
||||
test "validate_timestamp always returns nil" do
|
||||
assert Helpers.validate_timestamp("any") == nil
|
||||
assert Parser.UtilityHelpers.validate_timestamp("any") == nil
|
||||
end
|
||||
|
||||
test "find_matches handles named and unnamed captures" do
|
||||
regex = ~r/(?<val>\d+)/
|
||||
assert Helpers.find_matches(regex, "abc123def") == %{"val" => "123"}
|
||||
assert Parser.UtilityHelpers.find_matches(regex, "abc123def") == %{"val" => "123"}
|
||||
|
||||
regex = ~r/(\d+)/
|
||||
assert Helpers.find_matches(regex, "abc123def") == %{0 => "123", 1 => "123"}
|
||||
assert Parser.UtilityHelpers.find_matches(regex, "abc123def") == %{0 => "123", 1 => "123"}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue