aprs.me/lib/aprsme_web/controllers/api/v1/json/callsign_json.ex
Graham McIntire ca05b497b9
Consolidate 24 packet columns into JSONB data field
Drop 10 dead parser-compat columns (srccallsign, dstcallsign,
origpacket, body, header, alive, posambiguity, symboltable,
symbolcode, messaging) and move 14 display-only columns into a
single `data` JSONB column (PHG, telemetry, radiorange,
information_field, format, posresolution, position_ambiguity,
luminosity, rain_midnight).

Reduces row width from ~70 to ~48 columns. Table is ephemeral
so migration uses DROP/recreate. Also fixes snow_24h -> snow bug
in weather check query and adds null byte stripping for JSONB
string sanitization.
2026-02-20 13:02:47 -06:00

152 lines
4.6 KiB
Elixir

defmodule AprsmeWeb.Api.V1.CallsignJSON do
@moduledoc """
Renders callsign and packet data for API v1.
"""
alias Aprsme.DeviceCache
alias Aprsme.Packet
def render("show.json", %{packet: packet}) do
%{data: packet_json(packet)}
end
def render("index.json", %{packets: packets}) do
%{data: Enum.map(packets, &packet_json/1)}
end
def render("not_found.json", %{callsign: callsign}) do
%{
data: nil,
message: "No packets found for callsign #{callsign}"
}
end
defp packet_json(%Packet{} = packet) do
%{
id: packet.id,
callsign: format_callsign(packet.base_callsign, packet.ssid),
base_callsign: packet.base_callsign,
ssid: packet.ssid,
sender: packet.sender,
destination: packet.destination,
path: packet.path,
data_type: packet.data_type,
information_field: get_in(packet.data || %{}, ["information_field"]),
raw_packet: sanitize_raw_packet(packet.raw_packet),
received_at: packet.received_at,
region: packet.region,
position: position_json(packet),
symbol: symbol_json(packet),
comment: packet.comment,
timestamp: packet.timestamp,
aprs_messaging: packet.aprs_messaging,
weather: weather_json(packet),
equipment: equipment_json(packet),
message: message_json(packet),
has_position: packet.has_position,
inserted_at: packet.inserted_at,
updated_at: packet.updated_at
}
end
defp format_callsign(base_callsign, nil), do: base_callsign
defp format_callsign(base_callsign, "0"), do: base_callsign
defp format_callsign(base_callsign, ssid), do: "#{base_callsign}-#{ssid}"
defp position_json(%Packet{has_position: false}), do: nil
defp position_json(%Packet{lat: nil, lon: nil}), do: nil
defp position_json(%Packet{} = packet) do
base_position = %{
latitude: to_float(packet.lat),
longitude: to_float(packet.lon)
}
# Add course, speed, and altitude if available
base_position
|> maybe_add(:course, packet.course)
|> maybe_add(:speed, packet.speed)
|> maybe_add(:altitude, packet.altitude)
end
defp symbol_json(%Packet{symbol_code: nil, symbol_table_id: nil}), do: nil
defp symbol_json(%Packet{} = packet) do
%{
code: packet.symbol_code,
table_id: packet.symbol_table_id
}
end
defp weather_json(%Packet{} = packet) do
weather_data =
%{}
|> maybe_add(:temperature, packet.temperature)
|> maybe_add(:humidity, packet.humidity)
|> maybe_add(:wind_speed, packet.wind_speed)
|> maybe_add(:wind_direction, packet.wind_direction)
|> maybe_add(:wind_gust, packet.wind_gust)
|> maybe_add(:pressure, packet.pressure)
|> maybe_add(:rain_1h, packet.rain_1h)
|> maybe_add(:rain_24h, packet.rain_24h)
|> maybe_add(:rain_since_midnight, packet.rain_since_midnight)
case weather_data do
empty when empty == %{} -> nil
data -> data
end
end
defp equipment_json(%Packet{} = packet) do
# Look up device info based on device_identifier
device =
case packet.device_identifier do
nil -> nil
"" -> nil
identifier -> DeviceCache.lookup_device(identifier)
end
equipment_data =
%{}
|> maybe_add(:device_identifier, packet.device_identifier)
|> maybe_add(:manufacturer, packet.manufacturer)
|> maybe_add(:equipment_type, packet.equipment_type)
|> add_device_attributes(device)
case equipment_data do
empty when empty == %{} -> nil
data -> data
end
end
defp message_json(%Packet{addressee: nil, message_text: nil, message_number: nil}), do: nil
defp message_json(%Packet{} = packet) do
%{}
|> maybe_add(:addressee, packet.addressee)
|> maybe_add(:text, packet.message_text)
|> maybe_add(:number, packet.message_number)
end
defp maybe_add(map, _key, nil), do: map
defp maybe_add(map, _key, ""), do: map
defp maybe_add(map, key, value), do: Map.put(map, key, value)
defp to_float(%Decimal{} = decimal), do: Decimal.to_float(decimal)
defp to_float(value) when is_number(value), do: value
defp to_float(_), do: nil
defp sanitize_raw_packet(raw_packet) when is_binary(raw_packet) do
Aprsme.EncodingUtils.sanitize_string(raw_packet)
end
defp sanitize_raw_packet(raw_packet), do: raw_packet
defp add_device_attributes(equipment_data, device) do
equipment_data
|> maybe_add(:device_model, device && device.model)
|> maybe_add(:device_vendor, device && device.vendor)
|> maybe_add(:device_contact, device && device.contact)
|> maybe_add(:device_class, device && device.class)
end
end