Refactored packets

this patch removes the need for dacite2 package for creating
packet objects from the aprslib decoded packet dictionary.

moved the factory method from the base Packet object
to the core module.
This commit is contained in:
Hemna 2024-03-20 09:34:31 -04:00
parent 6f1d6b4122
commit 1477e61b0f
11 changed files with 298 additions and 182 deletions

View File

@ -137,7 +137,7 @@ class APRSISClient(Client):
def decode_packet(self, *args, **kwargs): def decode_packet(self, *args, **kwargs):
"""APRS lib already decodes this.""" """APRS lib already decodes this."""
return core.Packet.factory(args[0]) return core.factory(args[0])
def setup_connection(self): def setup_connection(self):
user = CONF.aprs_network.login user = CONF.aprs_network.login
@ -238,7 +238,7 @@ class KISSClient(Client):
# LOG.debug(f"Decoding {msg}") # LOG.debug(f"Decoding {msg}")
raw = aprslib.parse(str(frame)) raw = aprslib.parse(str(frame))
packet = core.Packet.factory(raw) packet = core.factory(raw)
if isinstance(packet, core.ThirdParty): if isinstance(packet, core.ThirdParty):
return packet.subpacket return packet.subpacket
else: else:

View File

@ -67,7 +67,7 @@ class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
# Generate packets here? # Generate packets here?
raw = "GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW" raw = "GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW"
pkt_raw = aprslib.parse(raw) pkt_raw = aprslib.parse(raw)
pkt = core.Packet.factory(pkt_raw) pkt = core.factory(pkt_raw)
callback(packet=pkt) callback(packet=pkt)
LOG.debug(f"END blocking FAKE consumer {self}") LOG.debug(f"END blocking FAKE consumer {self}")
time.sleep(8) time.sleep(8)

View File

@ -1,6 +1,6 @@
from aprsd.packets.core import ( # noqa: F401 from aprsd.packets.core import ( # noqa: F401
AckPacket, BeaconPacket, GPSPacket, MessagePacket, MicEPacket, Packet, AckPacket, BeaconPacket, GPSPacket, MessagePacket, MicEPacket, Packet,
RejectPacket, StatusPacket, WeatherPacket, RejectPacket, StatusPacket, ThirdPartyPacket, WeatherPacket, factory,
) )
from aprsd.packets.packet_list import PacketList # noqa: F401 from aprsd.packets.packet_list import PacketList # noqa: F401
from aprsd.packets.seen_list import SeenList # noqa: F401 from aprsd.packets.seen_list import SeenList # noqa: F401

View File

@ -1,14 +1,12 @@
import abc
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
from datetime import datetime from datetime import datetime
import logging import logging
import re import re
import time import time
# Due to a failure in python 3.8 # Due to a failure in python 3.8
from typing import List from typing import List, Optional
import dacite from dataclasses_json import DataClassJsonMixin
from dataclasses_json import dataclass_json
from aprsd.utils import counter from aprsd.utils import counter
@ -19,7 +17,8 @@ PACKET_TYPE_MESSAGE = "message"
PACKET_TYPE_ACK = "ack" PACKET_TYPE_ACK = "ack"
PACKET_TYPE_REJECT = "reject" PACKET_TYPE_REJECT = "reject"
PACKET_TYPE_MICE = "mic-e" PACKET_TYPE_MICE = "mic-e"
PACKET_TYPE_WX = "weather" PACKET_TYPE_WX = "wx"
PACKET_TYPE_WEATHER = "weather"
PACKET_TYPE_OBJECT = "object" PACKET_TYPE_OBJECT = "object"
PACKET_TYPE_UNKNOWN = "unknown" PACKET_TYPE_UNKNOWN = "unknown"
PACKET_TYPE_STATUS = "status" PACKET_TYPE_STATUS = "status"
@ -52,68 +51,65 @@ def _init_msgNo(): # noqa: N802
return c.value return c.value
def factory_from_dict(packet_dict): def _translate_fields(raw: dict) -> dict:
pkt_type = get_packet_type(packet_dict) translate_fields = {
if pkt_type: "from": "from_call",
cls = TYPE_LOOKUP[pkt_type] "to": "to_call",
return cls.from_dict(packet_dict) }
# First translate some fields
for key in translate_fields:
if key in raw:
raw[translate_fields[key]] = raw[key]
del raw[key]
# addresse overrides to_call
if "addresse" in raw:
raw["to_call"] = raw["addresse"]
return raw
def factory_from_json(packet_dict):
pkt_type = get_packet_type(packet_dict)
if pkt_type:
return TYPE_LOOKUP[pkt_type].from_json(packet_dict)
@dataclass_json
@dataclass(unsafe_hash=True) @dataclass(unsafe_hash=True)
class Packet(metaclass=abc.ABCMeta): class Packet(DataClassJsonMixin):
from_call: str = field(default=None) _type: str = field(default="Packet", hash=False)
to_call: str = field(default=None) from_call: Optional[str] = field(default=None)
addresse: str = field(default=None) to_call: Optional[str] = field(default=None)
format: str = field(default=None) addresse: Optional[str] = field(default=None)
format: Optional[str] = field(default=None)
msgNo: str = field(default_factory=_init_msgNo) # noqa: N815 msgNo: str = field(default_factory=_init_msgNo) # noqa: N815
packet_type: str = field(default=None) packet_type: Optional[str] = field(default=None)
timestamp: float = field(default_factory=_init_timestamp, compare=False, hash=False) timestamp: float = field(default_factory=_init_timestamp, compare=False, hash=False)
# Holds the raw text string to be sent over the wire # Holds the raw text string to be sent over the wire
# or holds the raw string from input packet # or holds the raw string from input packet
raw: str = field(default=None, compare=False, hash=False) raw: Optional[str] = field(default=None, compare=False, hash=False)
raw_dict: dict = field(repr=False, default_factory=lambda: {}, compare=False, hash=False) raw_dict: dict = field(repr=False, default_factory=lambda: {}, compare=False, hash=False)
# Built by calling prepare(). raw needs this built first. # Built by calling prepare(). raw needs this built first.
payload: str = field(default=None) payload: Optional[str] = field(default=None)
# Fields related to sending packets out # Fields related to sending packets out
send_count: int = field(repr=False, default=0, compare=False, hash=False) send_count: int = field(repr=False, default=0, compare=False, hash=False)
retry_count: int = field(repr=False, default=3, compare=False, hash=False) retry_count: int = field(repr=False, default=3, compare=False, hash=False)
# last_send_time: datetime = field(
# metadata=dc_json_config(
# encoder=datetime.isoformat,
# decoder=datetime.fromisoformat,
# ),
# repr=True,
# default_factory=_init_send_time,
# compare=False,
# hash=False
# )
last_send_time: float = field(repr=False, default=0, compare=False, hash=False) last_send_time: float = field(repr=False, default=0, compare=False, hash=False)
last_send_attempt: int = field(repr=False, default=0, compare=False, hash=False) last_send_attempt: int = field(repr=False, default=0, compare=False, hash=False)
# Do we allow this packet to be saved to send later? # Do we allow this packet to be saved to send later?
allow_delay: bool = field(repr=False, default=True, compare=False, hash=False) allow_delay: bool = field(repr=False, default=True, compare=False, hash=False)
path: List[str] = field(default_factory=list, compare=False, hash=False) path: List[str] = field(default_factory=list, compare=False, hash=False)
via: str = field(default=None, compare=False, hash=False) via: Optional[str] = field(default=None, compare=False, hash=False)
def __post__init__(self):
LOG.warning(f"POST INIT {self}")
@property @property
def json(self): def json(self) -> str:
""" """get the json formated string"""
get the json formated string # comes from the DataClassJsonMixin
"""
return self.to_json() return self.to_json()
def get(self, key, default=None): @property
def dict(self) -> dict:
"""get the dict formated string"""
# comes from the DataClassJsonMixin
return self.to_dict()
def get(self, key: str, default: Optional[str] = None):
"""Emulate a getter on a dict.""" """Emulate a getter on a dict."""
if hasattr(self, key): if hasattr(self, key):
return getattr(self, key) return getattr(self, key)
@ -121,116 +117,37 @@ class Packet(metaclass=abc.ABCMeta):
return default return default
@property @property
def key(self): def key(self) -> str:
"""Build a key for finding this packet in a dict.""" """Build a key for finding this packet in a dict."""
return f"{self.from_call}:{self.addresse}:{self.msgNo}" return f"{self.from_call}:{self.addresse}:{self.msgNo}"
def update_timestamp(self): def update_timestamp(self) -> None:
self.timestamp = _init_timestamp() self.timestamp = _init_timestamp()
def prepare(self): def prepare(self) -> None:
"""Do stuff here that is needed prior to sending over the air.""" """Do stuff here that is needed prior to sending over the air."""
# now build the raw message for sending # now build the raw message for sending
self._build_payload() self._build_payload()
self._build_raw() self._build_raw()
def _build_payload(self): def _build_payload(self) -> None:
"""The payload is the non headers portion of the packet.""" """The payload is the non headers portion of the packet."""
if not self.to_call:
raise ValueError("to_call isn't set. Must set to_call before calling prepare()")
msg = self._filter_for_send().rstrip("\n") msg = self._filter_for_send().rstrip("\n")
self.payload = ( self.payload = (
f":{self.to_call.ljust(9)}" f":{self.to_call.ljust(9)}"
f":{msg}" f":{msg}"
) )
def _build_raw(self): def _build_raw(self) -> None:
"""Build the self.raw which is what is sent over the air.""" """Build the self.raw which is what is sent over the air."""
self.raw = "{}>APZ100:{}".format( self.raw = "{}>APZ100:{}".format(
self.from_call, self.from_call,
self.payload, self.payload,
) )
@staticmethod def log(self, header: Optional[str] = None) -> None:
def factory(raw_packet):
"""Factory method to create a packet from a raw packet string."""
raw = raw_packet
raw["raw_dict"] = raw.copy()
translate_fields = {
"from": "from_call",
"to": "to_call",
}
# First translate some fields
for key in translate_fields:
if key in raw:
raw[translate_fields[key]] = raw[key]
del raw[key]
if "addresse" in raw:
raw["to_call"] = raw["addresse"]
packet_type = get_packet_type(raw)
raw["packet_type"] = packet_type
class_name = TYPE_LOOKUP[packet_type]
if packet_type == PACKET_TYPE_THIRDPARTY:
# We have an encapsulated packet!
# So we need to decode it and return the inner packet
# as the packet we are going to process.
# This is a recursive call to the factory
subpacket_raw = raw["subpacket"]
subpacket = Packet.factory(subpacket_raw)
del raw["subpacket"]
# raw["subpacket"] = subpacket
packet = dacite.from_dict(data_class=class_name, data=raw)
packet.subpacket = subpacket
return packet
if packet_type == PACKET_TYPE_UNKNOWN:
# Try and figure it out here
if "latitude" in raw:
class_name = GPSPacket
if packet_type == PACKET_TYPE_WX:
# the weather information is in a dict
# this brings those values out to the outer dict
for key in raw["weather"]:
raw[key] = raw["weather"][key]
# If we have the broken aprslib, then we need to
# Convert the course and speed to wind_speed and wind_direction
# aprslib issue #80
# https://github.com/rossengeorgiev/aprs-python/issues/80
# Wind speed and course is option in the SPEC.
# For some reason aprslib multiplies the speed by 1.852.
if "wind_speed" not in raw and "wind_direction" not in raw:
# Most likely this is the broken aprslib
# So we need to convert the wind_gust speed
raw["wind_gust"] = round(raw.get("wind_gust", 0) / 0.44704, 3)
if "wind_speed" not in raw:
wind_speed = raw.get("speed")
if wind_speed:
raw["wind_speed"] = round(wind_speed / 1.852, 3)
raw["weather"]["wind_speed"] = raw["wind_speed"]
if "speed" in raw:
del raw["speed"]
# Let's adjust the rain numbers as well, since it's wrong
raw["rain_1h"] = round((raw.get("rain_1h", 0) / .254) * .01, 3)
raw["weather"]["rain_1h"] = raw["rain_1h"]
raw["rain_24h"] = round((raw.get("rain_24h", 0) / .254) * .01, 3)
raw["weather"]["rain_24h"] = raw["rain_24h"]
raw["rain_since_midnight"] = round((raw.get("rain_since_midnight", 0) / .254) * .01, 3)
raw["weather"]["rain_since_midnight"] = raw["rain_since_midnight"]
if "wind_direction" not in raw:
wind_direction = raw.get("course")
if wind_direction:
raw["wind_direction"] = wind_direction
raw["weather"]["wind_direction"] = raw["wind_direction"]
if "course" in raw:
del raw["course"]
return dacite.from_dict(data_class=class_name, data=raw)
def log(self, header=None):
"""LOG a packet to the logfile.""" """LOG a packet to the logfile."""
asdict(self) asdict(self)
log_list = ["\n"] log_list = ["\n"]
@ -273,29 +190,39 @@ class Packet(metaclass=abc.ABCMeta):
# and ftm400-send is max 64. setting this to # and ftm400-send is max 64. setting this to
# 67 displays 64 on the ftm400. (+3 {01 suffix) # 67 displays 64 on the ftm400. (+3 {01 suffix)
# feature req: break long ones into two msgs # feature req: break long ones into two msgs
if not self.raw:
raise ValueError("No message text to send. call prepare() first.")
message = self.raw[:67] message = self.raw[:67]
# We all miss George Carlin # We all miss George Carlin
return re.sub("fuck|shit|cunt|piss|cock|bitch", "****", message) return re.sub("fuck|shit|cunt|piss|cock|bitch", "****", message)
def __str__(self): def __str__(self) -> str:
"""Show the raw version of the packet""" """Show the raw version of the packet"""
self.prepare() self.prepare()
if not self.raw:
raise ValueError("self.raw is unset")
return self.raw return self.raw
def __repr__(self): def __repr__(self) -> str:
"""Build the repr version of the packet.""" """Build the repr version of the packet."""
repr = ( repr = (
f"{self.__class__.__name__}:" f"{self.__class__.__name__}:"
f" From: {self.from_call} " f" From: {self.from_call} "
" To: " f" To: {self.to_call}"
) )
return repr return repr
@dataclass(unsafe_hash=True) @dataclass(unsafe_hash=True)
class AckPacket(Packet): class AckPacket(Packet):
response: str = field(default=None) _type: str = field(default="AckPacket", hash=False)
response: Optional[str] = field(default=None)
@staticmethod
def from_aprslib_dict(raw: dict) -> "AckPacket":
raw = _translate_fields(raw)
return AckPacket(**raw)
def __post__init__(self): def __post__init__(self):
if self.response: if self.response:
@ -307,7 +234,13 @@ class AckPacket(Packet):
@dataclass(unsafe_hash=True) @dataclass(unsafe_hash=True)
class RejectPacket(Packet): class RejectPacket(Packet):
response: str = field(default=None) _type: str = field(default="RejectPacket", hash=False)
response: Optional[str] = field(default=None)
@staticmethod
def from_aprslib_dict(raw: dict) -> "RejectPacket":
raw = _translate_fields(raw)
return RejectPacket(**raw)
def __post__init__(self): def __post__init__(self):
if self.response: if self.response:
@ -317,10 +250,10 @@ class RejectPacket(Packet):
self.payload = f":{self.to_call.ljust(9)} :rej{self.msgNo}" self.payload = f":{self.to_call.ljust(9)} :rej{self.msgNo}"
@dataclass_json
@dataclass(unsafe_hash=True) @dataclass(unsafe_hash=True)
class MessagePacket(Packet): class MessagePacket(Packet):
message_text: str = field(default=None) _type: str = field(default="MessagePacket", hash=False)
message_text: Optional[str] = field(default=None)
def _filter_for_send(self) -> str: def _filter_for_send(self) -> str:
"""Filter and format message string for FCC.""" """Filter and format message string for FCC."""
@ -328,6 +261,9 @@ class MessagePacket(Packet):
# and ftm400-send is max 64. setting this to # and ftm400-send is max 64. setting this to
# 67 displays 64 on the ftm400. (+3 {01 suffix) # 67 displays 64 on the ftm400. (+3 {01 suffix)
# feature req: break long ones into two msgs # feature req: break long ones into two msgs
if not self.message_text:
raise ValueError("No message text to send. Populate message_text field.")
message = self.message_text[:67] message = self.message_text[:67]
# We all miss George Carlin # We all miss George Carlin
return re.sub("fuck|shit|cunt|piss|cock|bitch", "****", message) return re.sub("fuck|shit|cunt|piss|cock|bitch", "****", message)
@ -339,12 +275,23 @@ class MessagePacket(Packet):
str(self.msgNo), str(self.msgNo),
) )
@staticmethod
def from_aprslib_dict(raw: dict) -> "MessagePacket":
raw = _translate_fields(raw)
return MessagePacket(**raw)
@dataclass(unsafe_hash=True) @dataclass(unsafe_hash=True)
class StatusPacket(Packet): class StatusPacket(Packet):
status: str = field(default=None) _type: str = field(default="StatusPacket", hash=False)
status: Optional[str] = field(default=None)
messagecapable: bool = field(default=False) messagecapable: bool = field(default=False)
comment: str = field(default=None) comment: Optional[str] = field(default=None)
@staticmethod
def from_aprslib_dict(raw: dict) -> "StatusPacket":
raw = _translate_fields(raw)
return StatusPacket(**raw)
def _build_payload(self): def _build_payload(self):
raise NotImplementedError raise NotImplementedError
@ -352,14 +299,27 @@ class StatusPacket(Packet):
@dataclass(unsafe_hash=True) @dataclass(unsafe_hash=True)
class GPSPacket(Packet): class GPSPacket(Packet):
_type: str = field(default="GPSPacket", hash=False)
latitude: float = field(default=0.00) latitude: float = field(default=0.00)
longitude: float = field(default=0.00) longitude: float = field(default=0.00)
altitude: float = field(default=0.00) altitude: float = field(default=0.00)
rng: float = field(default=0.00) rng: float = field(default=0.00)
posambiguity: int = field(default=0) posambiguity: int = field(default=0)
comment: str = field(default=None) messagecapable: bool = field(default=False)
comment: Optional[str] = field(default=None)
symbol: str = field(default="l") symbol: str = field(default="l")
symbol_table: str = field(default="/") symbol_table: str = field(default="/")
raw_timestamp: Optional[str] = field(default=None)
object_name: Optional[str] = field(default=None)
object_format: Optional[str] = field(default=None)
alive: Optional[bool] = field(default=None)
course: Optional[int] = field(default=None)
speed: Optional[float] = field(default=None)
@staticmethod
def from_aprslib_dict(raw: dict) -> "GPSPacket":
raw = _translate_fields(raw)
return GPSPacket(**raw)
def decdeg2dms(self, degrees_decimal): def decdeg2dms(self, degrees_decimal):
is_positive = degrees_decimal >= 0 is_positive = degrees_decimal >= 0
@ -467,6 +427,13 @@ class GPSPacket(Packet):
@dataclass(unsafe_hash=True) @dataclass(unsafe_hash=True)
class BeaconPacket(GPSPacket): class BeaconPacket(GPSPacket):
_type: str = field(default="BeaconPacket", hash=False)
@staticmethod
def from_aprslib_dict(raw: dict) -> "BeaconPacket":
raw = _translate_fields(raw)
return BeaconPacket(**raw)
def _build_payload(self): def _build_payload(self):
"""The payload is the non headers portion of the packet.""" """The payload is the non headers portion of the packet."""
time_zulu = self._build_time_zulu() time_zulu = self._build_time_zulu()
@ -487,9 +454,10 @@ class BeaconPacket(GPSPacket):
@dataclass @dataclass
class MicEPacket(GPSPacket): class MicEPacket(GPSPacket):
_type: str = field(default="MicEPacket", hash=False)
messagecapable: bool = False messagecapable: bool = False
mbits: str = None mbits: Optional[str] = None
mtype: str = None mtype: Optional[str] = None
# in MPH # in MPH
speed: float = 0.00 speed: float = 0.00
# 0 to 360 # 0 to 360
@ -501,14 +469,20 @@ class MicEPacket(GPSPacket):
@dataclass @dataclass
class ObjectPacket(GPSPacket): class ObjectPacket(GPSPacket):
_type: str = field(default="ObjectPacket", hash=False)
alive: bool = True alive: bool = True
raw_timestamp: str = None raw_timestamp: Optional[str] = None
symbol: str = field(default="r") symbol: str = field(default="r")
# in MPH # in MPH
speed: float = 0.00 speed: float = 0.00
# 0 to 360 # 0 to 360
course: int = 0 course: int = 0
@staticmethod
def from_aprslib_dict(raw: dict) -> "ObjectPacket":
raw = _translate_fields(raw)
return ObjectPacket(**raw)
def _build_payload(self): def _build_payload(self):
time_zulu = self._build_time_zulu() time_zulu = self._build_time_zulu()
lat = self.convert_latitude(self.latitude) lat = self.convert_latitude(self.latitude)
@ -541,6 +515,7 @@ class ObjectPacket(GPSPacket):
@dataclass() @dataclass()
class WeatherPacket(GPSPacket): class WeatherPacket(GPSPacket):
_type: str = field(default="WeatherPacket", hash=False)
symbol: str = "_" symbol: str = "_"
wind_speed: float = 0.00 wind_speed: float = 0.00
wind_direction: int = 0 wind_direction: int = 0
@ -552,7 +527,57 @@ class WeatherPacket(GPSPacket):
rain_since_midnight: float = 0.00 rain_since_midnight: float = 0.00
humidity: int = 0 humidity: int = 0
pressure: float = 0.00 pressure: float = 0.00
comment: str = None comment: Optional[str] = field(default=None)
luminosity: Optional[int] = field(default=None)
wx_raw_timestamp: Optional[str] = field(default=None)
course: Optional[int] = field(default=None)
speed: Optional[float] = field(default=None)
@staticmethod
def from_aprslib_dict(raw: dict) -> "WeatherPacket":
"""Create from a dictionary that has come directly from aprslib parse"""
# Because from is a reserved word in python, we need to translate it
# from -> from_call and to -> to_call
raw = _translate_fields(raw)
for key in raw["weather"]:
raw[key] = raw["weather"][key]
# If we have the broken aprslib, then we need to
# Convert the course and speed to wind_speed and wind_direction
# aprslib issue #80
# https://github.com/rossengeorgiev/aprs-python/issues/80
# Wind speed and course is option in the SPEC.
# For some reason aprslib multiplies the speed by 1.852.
if "wind_speed" not in raw and "wind_direction" not in raw:
# Most likely this is the broken aprslib
# So we need to convert the wind_gust speed
raw["wind_gust"] = round(raw.get("wind_gust", 0) / 0.44704, 3)
if "wind_speed" not in raw:
wind_speed = raw.get("speed")
if wind_speed:
raw["wind_speed"] = round(wind_speed / 1.852, 3)
raw["weather"]["wind_speed"] = raw["wind_speed"]
if "speed" in raw:
del raw["speed"]
# Let's adjust the rain numbers as well, since it's wrong
raw["rain_1h"] = round((raw.get("rain_1h", 0) / .254) * .01, 3)
raw["weather"]["rain_1h"] = raw["rain_1h"]
raw["rain_24h"] = round((raw.get("rain_24h", 0) / .254) * .01, 3)
raw["weather"]["rain_24h"] = raw["rain_24h"]
raw["rain_since_midnight"] = round((raw.get("rain_since_midnight", 0) / .254) * .01, 3)
raw["weather"]["rain_since_midnight"] = raw["rain_since_midnight"]
if "wind_direction" not in raw:
wind_direction = raw.get("course")
if wind_direction:
raw["wind_direction"] = wind_direction
raw["weather"]["wind_direction"] = raw["wind_direction"]
if "course" in raw:
del raw["course"]
del raw["weather"]
return WeatherPacket(**raw)
def _build_payload(self): def _build_payload(self):
"""Build an uncompressed weather packet """Build an uncompressed weather packet
@ -614,9 +639,11 @@ class WeatherPacket(GPSPacket):
) )
class ThirdParty(Packet): @dataclass()
class ThirdPartyPacket(Packet):
_type: str = "ThirdPartyPacket"
# Holds the encapsulated packet # Holds the encapsulated packet
subpacket: Packet = field(default=None, compare=True, hash=False) subpacket: Optional[type[Packet]] = field(default=None, compare=True, hash=False)
def __repr__(self): def __repr__(self):
"""Build the repr version of the packet.""" """Build the repr version of the packet."""
@ -629,26 +656,50 @@ class ThirdParty(Packet):
return repr_str return repr_str
@staticmethod
def from_aprslib_dict(raw: dict) -> "ThirdPartyPacket":
"""Create from a dictionary that has come directly from aprslib parse"""
# Because from is a reserved word in python, we need to translate it
# from -> from_call and to -> to_call
raw = _translate_fields(raw)
subpacket = raw.get("subpacket")
del raw["subpacket"]
return ThirdPartyPacket(**raw, subpacket=factory(subpacket))
TYPE_LOOKUP = {
TYPE_LOOKUP: dict[str, type[Packet]] = {
PACKET_TYPE_WX: WeatherPacket, PACKET_TYPE_WX: WeatherPacket,
PACKET_TYPE_WEATHER: WeatherPacket,
PACKET_TYPE_MESSAGE: MessagePacket, PACKET_TYPE_MESSAGE: MessagePacket,
PACKET_TYPE_ACK: AckPacket, PACKET_TYPE_ACK: AckPacket,
PACKET_TYPE_REJECT: RejectPacket, PACKET_TYPE_REJECT: RejectPacket,
PACKET_TYPE_MICE: MicEPacket, PACKET_TYPE_MICE: MicEPacket,
PACKET_TYPE_OBJECT: ObjectPacket, PACKET_TYPE_OBJECT: ObjectPacket,
PACKET_TYPE_STATUS: StatusPacket, PACKET_TYPE_STATUS: StatusPacket,
PACKET_TYPE_BEACON: GPSPacket, PACKET_TYPE_BEACON: BeaconPacket,
PACKET_TYPE_UNKNOWN: Packet, PACKET_TYPE_UNKNOWN: Packet,
PACKET_TYPE_THIRDPARTY: ThirdParty, PACKET_TYPE_THIRDPARTY: ThirdPartyPacket,
}
OBJ_LOOKUP: dict[str, type[Packet]] = {
"MessagePacket": MessagePacket,
"AckPacket": AckPacket,
"RejectPacket": RejectPacket,
"MicEPacket": MicEPacket,
"ObjectPacket": ObjectPacket,
"StatusPacket": StatusPacket,
"GPSPacket": GPSPacket,
"BeaconPacket": BeaconPacket,
"WeatherPacket": WeatherPacket,
"ThirdPartyPacket": ThirdPartyPacket,
} }
def get_packet_type(packet: dict): def get_packet_type(packet: dict) -> str:
"""Decode the packet type from the packet.""" """Decode the packet type from the packet."""
pkt_format = packet.get("format", None) pkt_format = packet.get("format")
msg_response = packet.get("response", None) msg_response = packet.get("response")
packet_type = PACKET_TYPE_UNKNOWN packet_type = PACKET_TYPE_UNKNOWN
if pkt_format == "message" and msg_response == "ack": if pkt_format == "message" and msg_response == "ack":
packet_type = PACKET_TYPE_ACK packet_type = PACKET_TYPE_ACK
@ -664,9 +715,11 @@ def get_packet_type(packet: dict):
packet_type = PACKET_TYPE_STATUS packet_type = PACKET_TYPE_STATUS
elif pkt_format == PACKET_TYPE_BEACON: elif pkt_format == PACKET_TYPE_BEACON:
packet_type = PACKET_TYPE_BEACON packet_type = PACKET_TYPE_BEACON
elif pkt_format == PACKET_TYPE_WX:
packet_type = PACKET_TYPE_WEATHER
elif pkt_format == PACKET_TYPE_UNCOMPRESSED: elif pkt_format == PACKET_TYPE_UNCOMPRESSED:
if packet.get("symbol", None) == "_": if packet.get("symbol") == "_":
packet_type = PACKET_TYPE_WX packet_type = PACKET_TYPE_WEATHER
elif pkt_format == PACKET_TYPE_THIRDPARTY: elif pkt_format == PACKET_TYPE_THIRDPARTY:
packet_type = PACKET_TYPE_THIRDPARTY packet_type = PACKET_TYPE_THIRDPARTY
@ -676,13 +729,43 @@ def get_packet_type(packet: dict):
return packet_type return packet_type
def is_message_packet(packet): def is_message_packet(packet: dict) -> bool:
return get_packet_type(packet) == PACKET_TYPE_MESSAGE return get_packet_type(packet) == PACKET_TYPE_MESSAGE
def is_ack_packet(packet): def is_ack_packet(packet: dict) -> bool:
return get_packet_type(packet) == PACKET_TYPE_ACK return get_packet_type(packet) == PACKET_TYPE_ACK
def is_mice_packet(packet): def is_mice_packet(packet: dict) -> bool:
return get_packet_type(packet) == PACKET_TYPE_MICE return get_packet_type(packet) == PACKET_TYPE_MICE
def factory(raw_packet: dict) -> type[Packet]:
"""Factory method to create a packet from a raw packet string."""
raw = raw_packet
if "_type" in raw:
cls = globals()[raw["_type"]]
return cls.from_dict(raw)
raw["raw_dict"] = raw.copy()
raw = _translate_fields(raw)
packet_type = get_packet_type(raw)
raw["packet_type"] = packet_type
class_name = TYPE_LOOKUP[packet_type]
if packet_type == PACKET_TYPE_WX:
# the weather information is in a dict
# this brings those values out to the outer dict
class_name = WeatherPacket
elif packet_type == PACKET_TYPE_OBJECT and "weather" in raw:
class_name = WeatherPacket
elif packet_type == PACKET_TYPE_UNKNOWN:
# Try and figure it out here
if "latitude" in raw:
class_name = GPSPacket
else:
raise Exception(f"Unknown packet type {packet_type} {raw}")
return class_name.from_aprslib_dict(raw)

View File

@ -28,7 +28,6 @@ wrapt
kiss3 kiss3
attrs attrs
dataclasses dataclasses
dacite2
oslo.config oslo.config
rpyc>=6.0.0 rpyc>=6.0.0
# Pin this here so it doesn't require a compile on # Pin this here so it doesn't require a compile on

View File

@ -17,7 +17,6 @@ click==8.1.7 # via -r requirements.in, click-completion, click-para
click-completion==0.5.2 # via -r requirements.in click-completion==0.5.2 # via -r requirements.in
click-params==0.5.0 # via -r requirements.in click-params==0.5.0 # via -r requirements.in
commonmark==0.9.1 # via rich commonmark==0.9.1 # via rich
dacite2==2.0.0 # via -r requirements.in
dataclasses==0.6 # via -r requirements.in dataclasses==0.6 # via -r requirements.in
dataclasses-json==0.6.4 # via -r requirements.in dataclasses-json==0.6.4 # via -r requirements.in
debtcollector==3.0.0 # via oslo-config debtcollector==3.0.0 # via oslo-config
@ -34,7 +33,7 @@ greenlet==3.0.3 # via eventlet, gevent
h11==0.14.0 # via wsproto h11==0.14.0 # via wsproto
idna==3.6 # via requests idna==3.6 # via requests
imapclient==3.0.1 # via -r requirements.in imapclient==3.0.1 # via -r requirements.in
importlib-metadata==7.0.1 # via ax253, kiss3 importlib-metadata==7.0.2 # via ax253, kiss3
itsdangerous==2.1.2 # via flask itsdangerous==2.1.2 # via flask
jinja2==3.1.3 # via click-completion, flask jinja2==3.1.3 # via click-completion, flask
kiss3==8.0.0 # via -r requirements.in kiss3==8.0.0 # via -r requirements.in
@ -45,7 +44,7 @@ mypy-extensions==1.0.0 # via typing-inspect
netaddr==1.2.1 # via oslo-config netaddr==1.2.1 # via oslo-config
oslo-config==9.4.0 # via -r requirements.in oslo-config==9.4.0 # via -r requirements.in
oslo-i18n==6.3.0 # via oslo-config oslo-i18n==6.3.0 # via oslo-config
packaging==23.2 # via marshmallow packaging==24.0 # via marshmallow
pbr==6.0.0 # via -r requirements.in, oslo-i18n, stevedore pbr==6.0.0 # via -r requirements.in, oslo-i18n, stevedore
pluggy==1.4.0 # via -r requirements.in pluggy==1.4.0 # via -r requirements.in
plumbum==1.8.2 # via rpyc plumbum==1.8.2 # via rpyc
@ -76,7 +75,7 @@ validators==0.22.0 # via click-params
werkzeug==3.0.1 # via -r requirements.in, flask werkzeug==3.0.1 # via -r requirements.in, flask
wrapt==1.16.0 # via -r requirements.in, debtcollector, deprecated wrapt==1.16.0 # via -r requirements.in, debtcollector, deprecated
wsproto==1.2.0 # via simple-websocket wsproto==1.2.0 # via simple-websocket
zipp==3.17.0 # via importlib-metadata zipp==3.18.1 # via importlib-metadata
zope-event==5.0 # via gevent zope-event==5.0 # via gevent
zope-interface==6.2 # via gevent zope-interface==6.2 # via gevent

View File

@ -51,11 +51,8 @@ class TestSendMessageCommand(unittest.TestCase):
): ):
self.config_and_init() self.config_and_init()
mock_socketio.emit = mock.MagicMock() mock_socketio.emit = mock.MagicMock()
packet = fake.fake_packet( # Create an ACK packet
message="blah", packet = fake.fake_ack_packet()
msg_number=1,
message_format=core.PACKET_TYPE_ACK,
)
mock_queue = mock.MagicMock() mock_queue = mock.MagicMock()
socketio = mock.MagicMock() socketio = mock.MagicMock()
wcp = webchat.WebChatProcessPacketThread(mock_queue, socketio) wcp = webchat.WebChatProcessPacketThread(mock_queue, socketio)

View File

@ -1,4 +1,4 @@
from aprsd import packets, plugin, threads from aprsd import plugin, threads
from aprsd.packets import core from aprsd.packets import core
@ -13,6 +13,7 @@ def fake_packet(
message=None, message=None,
msg_number=None, msg_number=None,
message_format=core.PACKET_TYPE_MESSAGE, message_format=core.PACKET_TYPE_MESSAGE,
response=None,
): ):
packet_dict = { packet_dict = {
"from": fromcall, "from": fromcall,
@ -27,7 +28,17 @@ def fake_packet(
if msg_number: if msg_number:
packet_dict["msgNo"] = str(msg_number) packet_dict["msgNo"] = str(msg_number)
return packets.Packet.factory(packet_dict) if response:
packet_dict["response"] = response
return core.factory(packet_dict)
def fake_ack_packet():
return fake_packet(
msg_number=12,
response=core.PACKET_TYPE_ACK,
)
class FakeBaseNoThreadsPlugin(plugin.APRSDPluginBase): class FakeBaseNoThreadsPlugin(plugin.APRSDPluginBase):

View File

@ -11,7 +11,7 @@ from .. import fake, test_plugin
CONF = cfg.CONF CONF = cfg.CONF
class TestUSWeatherPluginPlugin(test_plugin.TestPlugin): class TestUSWeatherPlugin(test_plugin.TestPlugin):
def test_not_enabled_missing_aprs_fi_key(self): def test_not_enabled_missing_aprs_fi_key(self):
# When the aprs.fi api key isn't set, then # When the aprs.fi api key isn't set, then

View File

@ -1,6 +1,8 @@
import unittest import unittest
from unittest import mock from unittest import mock
import aprslib
from aprsd import packets from aprsd import packets
from aprsd.packets import core from aprsd.packets import core
@ -55,7 +57,7 @@ class TestPluginBase(unittest.TestCase):
def test_packet_factory(self): def test_packet_factory(self):
pkt_dict = self._fake_dict() pkt_dict = self._fake_dict()
pkt = packets.Packet.factory(pkt_dict) pkt = packets.factory(pkt_dict)
self.assertIsInstance(pkt, packets.MessagePacket) self.assertIsInstance(pkt, packets.MessagePacket)
self.assertEqual(fake.FAKE_FROM_CALLSIGN, pkt.from_call) self.assertEqual(fake.FAKE_FROM_CALLSIGN, pkt.from_call)
@ -71,7 +73,7 @@ class TestPluginBase(unittest.TestCase):
"comment": "Home!", "comment": "Home!",
} }
pkt_dict["format"] = core.PACKET_TYPE_UNCOMPRESSED pkt_dict["format"] = core.PACKET_TYPE_UNCOMPRESSED
pkt = packets.Packet.factory(pkt_dict) pkt = packets.factory(pkt_dict)
self.assertIsInstance(pkt, packets.WeatherPacket) self.assertIsInstance(pkt, packets.WeatherPacket)
@mock.patch("aprsd.packets.core.GPSPacket._build_time_zulu") @mock.patch("aprsd.packets.core.GPSPacket._build_time_zulu")
@ -100,3 +102,31 @@ class TestPluginBase(unittest.TestCase):
wx.prepare() wx.prepare()
expected = "KFAKE>KMINE,WIDE1-1,WIDE2-1:@221450z0.0/0.0_000/000g000t000r001p000P000h00b00000" expected = "KFAKE>KMINE,WIDE1-1,WIDE2-1:@221450z0.0/0.0_000/000g000t000r001p000P000h00b00000"
self.assertEqual(expected, wx.raw) self.assertEqual(expected, wx.raw)
def test_beacon_factory(self):
"""Test to ensure a beacon packet is created."""
packet_raw = "WB4BOR-12>APZ100,WIDE2-1:@161647z3724.15N107847.58W$ APRSD WebChat"
packet_dict = aprslib.parse(packet_raw)
packet = packets.factory(packet_dict)
self.assertIsInstance(packet, packets.BeaconPacket)
def test_reject_factory(self):
"""Test to ensure a reject packet is created."""
packet_raw = "HB9FDL-1>APK102,HB9FM-4*,WIDE2,qAR,HB9FEF-11::REPEAT :rej4139"
packet_dict = aprslib.parse(packet_raw)
packet = packets.factory(packet_dict)
self.assertIsInstance(packet, packets.RejectPacket)
def test_thirdparty_factory(self):
"""Test to ensure a third party packet is created."""
packet_raw = "GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW"
packet_dict = aprslib.parse(packet_raw)
packet = packets.factory(packet_dict)
self.assertIsInstance(packet, packets.ThirdPartyPacket)
def test_weather_factory(self):
"""Test to ensure a weather packet is created."""
packet_raw = "FW9222>APRS,TCPXX*,qAX,CWOP-6:@122025z2953.94N/08423.77W_232/003g006t084r000p032P000h80b10157L745.DsWLL"
packet_dict = aprslib.parse(packet_raw)
packet = packets.factory(packet_dict)
self.assertIsInstance(packet, packets.WeatherPacket)

View File

@ -45,7 +45,6 @@ class TestPluginManager(unittest.TestCase):
self.assertEqual([], plugin_list) self.assertEqual([], plugin_list)
pm.setup_plugins() pm.setup_plugins()
plugin_list = pm.get_plugins() plugin_list = pm.get_plugins()
print(plugin_list)
self.assertIsInstance(plugin_list, list) self.assertIsInstance(plugin_list, list)
self.assertIsInstance( self.assertIsInstance(
plugin_list[0], plugin_list[0],
@ -163,9 +162,7 @@ class TestPluginBase(TestPlugin):
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
mock_process.assert_not_called() mock_process.assert_not_called()
packet = fake.fake_packet( packet = fake.fake_ack_packet()
message_format=core.PACKET_TYPE_ACK,
)
expected = packets.NULL_MESSAGE expected = packets.NULL_MESSAGE
actual = p.filter(packet) actual = p.filter(packet)
self.assertEqual(expected, actual) self.assertEqual(expected, actual)