From 8ea697f39e04bd840a0102dc839d29b9ca2f2629 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sat, 21 Jun 2025 10:49:51 -0500 Subject: [PATCH] more parsing fixes --- lib/parser.ex | 183 +------------------------------------------ lib/parser/mic_e.ex | 185 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+), 180 deletions(-) create mode 100644 lib/parser/mic_e.ex diff --git a/lib/parser.ex b/lib/parser.ex index b17e25a..d4a747e 100644 --- a/lib/parser.ex +++ b/lib/parser.ex @@ -6,6 +6,7 @@ defmodule Parser do """ # import Bitwise alias Aprs.Convert + alias Parser.MicE require Logger @@ -221,8 +222,8 @@ defmodule Parser do def parse_datatype(_), do: :unknown_datatype @spec parse_data(atom(), String.t(), String.t()) :: map() | nil - def parse_data(:mic_e, destination, data), do: parse_mic_e(data, destination) - def parse_data(:mic_e_old, destination, data), do: parse_mic_e(data, destination) + def parse_data(:mic_e, destination, data), do: MicE.parse(data, destination) + def parse_data(:mic_e_old, destination, data), do: MicE.parse(data, destination) def parse_data(:position, destination, <<"!", rest::binary>>) do parse_data(:position, destination, rest) @@ -687,184 +688,6 @@ defmodule Parser do end end - @spec parse_mic_e(binary(), String.t()) :: map() - def parse_mic_e(data, destination \\ nil) do - case data do - <> -> - # Try to extract lat/lon from destination if provided - dest_pos = if destination, do: parse_mic_e_destination(destination), else: %{} - %{latitude: lat, longitude: lon} = parse_aprs_position(latitude, longitude) - - %{ - latitude: lat || Map.get(dest_pos, :latitude), - longitude: lon || Map.get(dest_pos, :longitude), - symbol_table_id: symbol_table_id, - symbol_code: symbol_code, - speed: speed, - course: course, - dti: dti, - comment: rest, - data_type: :mic_e - } - - _ -> - nil - end - end - - @spec parse_mic_e_digit(binary()) :: [integer() | atom() | nil] - def parse_mic_e_digit(<>) when c in ?0..?9, do: [c - ?0, 0, nil] - def parse_mic_e_digit(<>) when c in ?A..?J, do: [c - ?A, 1, :custom] - def parse_mic_e_digit(<>) when c in ?P..?Y, do: [c - ?P, 1, :standard] - def parse_mic_e_digit(<<"K">>), do: [0, 1, :custom] - def parse_mic_e_digit(<<"L">>), do: [0, 0, nil] - def parse_mic_e_digit(<<"Z">>), do: [0, 1, :standard] - def parse_mic_e_digit(_), do: [:unknown, :unknown, :unknown] - - @spec parse_mic_e_destination(String.t()) :: map() - def parse_mic_e_destination(destination_field) do - digits = - destination_field - |> String.codepoints() - |> Enum.map(&parse_mic_e_digit/1) - |> Enum.map(&hd/1) - - deg = digits |> Enum.slice(0..1) |> Enum.join() |> String.to_integer() - min = digits |> Enum.slice(2..3) |> Enum.join() |> String.to_integer() - fractional = digits |> Enum.slice(4..5) |> Enum.join() |> String.to_integer() - - [ns, lo, ew] = destination_field |> to_charlist() |> Enum.slice(3..5) - - north_south_indicator = - case ns do - x when x in ?0..?9 -> :south - x when x == ?L -> :south - x when x in ?P..?Z -> :north - _ -> :unknown - end - - east_west_indicator = - case ew do - x when x in ?0..?9 -> :east - x when x == ?L -> :east - x when x in ?P..?Z -> :west - _ -> :unknown - end - - longitude_offset = - case lo do - x when x in ?0..?9 -> 0 - x when x == ?L -> 0 - x when x in ?P..?Z -> 100 - _ -> :unknown - end - - statuses = [ - "Emergency", - "Priority", - "Special", - "Committed", - "Returning", - "In Service", - "En Route", - "Off Duty" - ] - - message_digits = - destination_field - |> String.codepoints() - |> Enum.take(3) - - [_, message_bit_1, message_type] = parse_mic_e_digit(Enum.at(message_digits, 0)) - [_, message_bit_2, _] = parse_mic_e_digit(Enum.at(message_digits, 1)) - [_, message_bit_3, _] = parse_mic_e_digit(Enum.at(message_digits, 2)) - - # Convert the bits to binary to get the array index - index = message_bit_1 * 4 + message_bit_2 * 2 + message_bit_3 - # need to invert this from the actual array index - display_index = (7 - index) |> to_string() |> String.pad_leading(2, "0") - - [message_code, message_description] = - case message_type do - :standard -> - ["M" <> display_index, Enum.at(statuses, index)] - - :custom -> - ["C" <> display_index, "Custom-#{display_index}"] - - nil -> - ["", Enum.at(statuses, index)] - end - - %{ - lat_degrees: deg, - lat_minutes: min, - lat_fractional: fractional, - lat_direction: north_south_indicator, - lon_direction: east_west_indicator, - longitude_offset: longitude_offset, - message_code: message_code, - message_description: message_description - } - end - - def parse_mic_e_information( - <> = _information_field, - longitude_offset - ) do - m = - case m28 - 28 do - x when x >= 60 -> x - 60 - x -> x - end - - sp = - case sp28 - 28 do - x when x >= 80 -> x - 80 - x -> x - end - - dc = dc28 - 28 - quotient = div(dc, 10) - remainder = rem(dc, 10) - dc = sp * 10 + quotient - heading = (remainder - 4) * 100 + (se28 - 28) - - # Messages should at least have a starting and ending symbol, and an optional message in between - # But, there might not be any symbols either, so it could look like any of the following: - # >^ <- TH-D74 - # nil <- who knows - # ]\"55}146.820 MHz T103 -0600= <- Kenwood DM-710 - - regex = ~r/^(?.?)(?.*)(?.)(?.)$/i - result = Parser.Helpers.find_matches(regex, message) - - symbol1 = - if result["first"] == "" do - result["secondtolast"] - else - result["first"] - end - - manufacturer = - Parser.Helpers.parse_manufacturer(symbol1 <> result["secondtolast"] <> result["last"]) - - %{ - dti: dti, - lon_degrees: d28 - 28 + longitude_offset, - lon_minutes: m, - lon_fractional: f28 - 28, - speed: dc, - heading: heading, - symbol: symbol, - table: table, - manufacturer: manufacturer, - message: message - } - end - @spec parse_manufacturer(binary()) :: String.t() def parse_manufacturer(symbols) do Aprs.DeviceIdentification.identify_device(symbols) diff --git a/lib/parser/mic_e.ex b/lib/parser/mic_e.ex new file mode 100644 index 0000000..3526767 --- /dev/null +++ b/lib/parser/mic_e.ex @@ -0,0 +1,185 @@ +defmodule Parser.MicE do + @moduledoc """ + Parses Mic-E encoded APRS packets. + """ + + @spec parse(binary(), String.t()) :: map() + def parse(data, destination \\ nil) do + with {:ok, dest_info} <- parse_destination(destination), + {:ok, info_info} <- parse_information(data, dest_info.longitude_offset) do + lat = + Decimal.add( + Decimal.new(dest_info.lat_degrees), + Decimal.div( + Decimal.add( + Decimal.new(dest_info.lat_minutes), + dest_info.lat_hundredths |> Decimal.new() |> Decimal.div(100) + ), + 60 + ) + ) + + lat = if dest_info.lat_direction == :south, do: Decimal.negate(lat), else: lat + + lon = + Decimal.add( + Decimal.new(info_info.lon_degrees), + Decimal.div( + Decimal.add( + Decimal.new(info_info.lon_minutes), + info_info.lon_hundredths |> Decimal.new() |> Decimal.div(100) + ), + 60 + ) + ) + + lon = if dest_info.lon_direction == :west, do: Decimal.negate(lon), else: lon + + %{ + latitude: lat, + longitude: lon, + message_bits: dest_info.message_bits, + message_type: dest_info.message_type, + speed: info_info.speed, + course: info_info.course, + symbol_code: info_info.symbol_code, + symbol_table_id: info_info.symbol_table_id, + comment: info_info.comment, + data_type: :mic_e + } + else + _error -> + %{ + latitude: nil, + longitude: nil, + error: "Failed to parse Mic-E packet", + data_type: :mic_e_error + } + end + end + + defp parse_destination(destination) do + if byte_size(destination) == 6 do + try do + <> = destination + + d1 = decode_digit(c1) + d2 = decode_digit(c2) + d3 = decode_digit(c3) + d4 = decode_digit(c4) + d5 = decode_digit(c5) + d6 = decode_digit(c6) + + lat_degrees = d1.digit * 10 + d2.digit + lat_minutes = d3.digit * 10 + d4.digit + lat_hundredths = d5.digit * 10 + d6.digit + + lat_direction = + case c4 do + c when c in ?0..?9 -> :south + ?L -> :south + c when c in ?P..?Z -> :north + _ -> :unknown + end + + longitude_offset = + case c5 do + c when c in ?0..?9 -> 0 + ?L -> 0 + c when c in ?P..?Z -> 100 + _ -> 0 + end + + lon_direction = + case c6 do + c when c in ?0..?9 -> :east + ?L -> :east + c when c in ?P..?Z -> :west + _ -> :unknown + end + + message_bits = {d1.msg_bit, d2.msg_bit, d3.msg_bit} + + message_type = + cond do + d1.msg_type != nil -> d1.msg_type + d2.msg_type != nil -> d2.msg_type + d3.msg_type != nil -> d3.msg_type + true -> nil + end + + {:ok, + %{ + lat_degrees: lat_degrees, + lat_minutes: lat_minutes, + lat_hundredths: lat_hundredths, + lat_direction: lat_direction, + lon_direction: lon_direction, + longitude_offset: longitude_offset, + message_bits: message_bits, + message_type: message_type + }} + rescue + _ -> {:error, :invalid_character_in_destination} + end + else + {:error, :invalid_destination_length} + end + end + + defp decode_digit(char) do + case char do + c when c in ?0..?9 -> %{digit: c - ?0, msg_bit: 0, msg_type: nil} + c when c in ?A..?K -> %{digit: c - ?A, msg_bit: 1, msg_type: :custom} + ?L -> %{digit: 0, msg_bit: 0, msg_type: nil} + c when c in ?P..?Z -> %{digit: c - ?P, msg_bit: 1, msg_type: :standard} + end + end + + defp parse_information(data, lon_offset) do + if byte_size(data) < 8 do + {:error, :invalid_information_field_length} + else + <> = data + + lon_deg = + case lon_deg_c - 28 do + d when d >= 108 and d <= 117 -> d - 80 + d when d >= 118 and d <= 127 -> d - 190 + d -> d + end + lon_offset + + lon_min = + case lon_min_c - 28 do + m when m >= 60 -> m - 60 + m -> m + end + + lon_hmin = lon_hmin_c - 28 + + sp = sp_c - 28 + dc = dc_c - 28 + se = se_c - 28 + + speed = div(sp, 10) * 100 + rem(sp, 10) * 10 + div(dc, 10) + speed = if speed >= 800, do: speed - 800, else: speed + # Convert to knots from mph + speed = speed * 0.868976 + + course = rem(dc, 10) * 100 + se + course = if course >= 400, do: course - 400, else: course + + {:ok, + %{ + lon_degrees: lon_deg, + lon_minutes: lon_min, + lon_hundredths: lon_hmin, + speed: speed, + course: course, + symbol_code: <>, + symbol_table_id: <>, + comment: comment + }} + end + end +end