mirror of
https://github.com/craigerl/aprsd.git
synced 2025-04-19 09:49:01 -04:00
Merge cbd69d5d86
into ad1e62b17c
This commit is contained in:
commit
bd0bf49f8b
@ -1,13 +1,5 @@
|
||||
from aprsd.client import aprsis, factory, fake, kiss
|
||||
|
||||
|
||||
TRANSPORT_APRSIS = "aprsis"
|
||||
TRANSPORT_TCPKISS = "tcpkiss"
|
||||
TRANSPORT_SERIALKISS = "serialkiss"
|
||||
TRANSPORT_FAKE = "fake"
|
||||
|
||||
|
||||
client_factory = factory.ClientFactory()
|
||||
client_factory.register(aprsis.APRSISClient)
|
||||
client_factory.register(kiss.KISSClient)
|
||||
client_factory.register(fake.APRSDFakeClient)
|
||||
# define the client transports here
|
||||
TRANSPORT_APRSIS = 'aprsis'
|
||||
TRANSPORT_TCPKISS = 'tcpkiss'
|
||||
TRANSPORT_SERIALKISS = 'serialkiss'
|
||||
TRANSPORT_FAKE = 'fake'
|
||||
|
@ -1,183 +0,0 @@
|
||||
import datetime
|
||||
import logging
|
||||
import time
|
||||
|
||||
import timeago
|
||||
from aprslib.exceptions import LoginError
|
||||
from loguru import logger
|
||||
from oslo_config import cfg
|
||||
|
||||
from aprsd import client, exception
|
||||
from aprsd.client import base
|
||||
from aprsd.client.drivers import aprsis
|
||||
from aprsd.packets import core
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger("APRSD")
|
||||
LOGU = logger
|
||||
|
||||
|
||||
class APRSISClient(base.APRSClient):
|
||||
_client = None
|
||||
_checks = False
|
||||
|
||||
def __init__(self):
|
||||
max_timeout = {"hours": 0.0, "minutes": 2, "seconds": 0}
|
||||
self.max_delta = datetime.timedelta(**max_timeout)
|
||||
|
||||
def stats(self, serializable=False) -> dict:
|
||||
stats = {}
|
||||
if self.is_configured():
|
||||
if self._client:
|
||||
keepalive = self._client.aprsd_keepalive
|
||||
server_string = self._client.server_string
|
||||
if serializable:
|
||||
keepalive = keepalive.isoformat()
|
||||
else:
|
||||
keepalive = "None"
|
||||
server_string = "None"
|
||||
stats = {
|
||||
"connected": self.is_connected,
|
||||
"filter": self.filter,
|
||||
"login_status": self.login_status,
|
||||
"connection_keepalive": keepalive,
|
||||
"server_string": server_string,
|
||||
"transport": self.transport(),
|
||||
}
|
||||
|
||||
return stats
|
||||
|
||||
def keepalive_check(self):
|
||||
# Don't check the first time through.
|
||||
if not self.is_alive() and self._checks:
|
||||
LOG.warning("Resetting client. It's not alive.")
|
||||
self.reset()
|
||||
self._checks = True
|
||||
|
||||
def keepalive_log(self):
|
||||
if ka := self._client.aprsd_keepalive:
|
||||
keepalive = timeago.format(ka)
|
||||
else:
|
||||
keepalive = "N/A"
|
||||
LOGU.opt(colors=True).info(f"<green>Client keepalive {keepalive}</green>")
|
||||
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
# Defaults to True if the enabled flag is non existent
|
||||
try:
|
||||
return CONF.aprs_network.enabled
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_configured():
|
||||
if APRSISClient.is_enabled():
|
||||
# Ensure that the config vars are correctly set
|
||||
if not CONF.aprs_network.login:
|
||||
LOG.error("Config aprs_network.login not set.")
|
||||
raise exception.MissingConfigOptionException(
|
||||
"aprs_network.login is not set.",
|
||||
)
|
||||
if not CONF.aprs_network.password:
|
||||
LOG.error("Config aprs_network.password not set.")
|
||||
raise exception.MissingConfigOptionException(
|
||||
"aprs_network.password is not set.",
|
||||
)
|
||||
if not CONF.aprs_network.host:
|
||||
LOG.error("Config aprs_network.host not set.")
|
||||
raise exception.MissingConfigOptionException(
|
||||
"aprs_network.host is not set.",
|
||||
)
|
||||
|
||||
return True
|
||||
return True
|
||||
|
||||
def _is_stale_connection(self):
|
||||
delta = datetime.datetime.now() - self._client.aprsd_keepalive
|
||||
if delta > self.max_delta:
|
||||
LOG.error(f"Connection is stale, last heard {delta} ago.")
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_alive(self):
|
||||
if not self._client:
|
||||
LOG.warning(f"APRS_CLIENT {self._client} alive? NO!!!")
|
||||
return False
|
||||
return self._client.is_alive() and not self._is_stale_connection()
|
||||
|
||||
def close(self):
|
||||
if self._client:
|
||||
self._client.stop()
|
||||
self._client.close()
|
||||
|
||||
@staticmethod
|
||||
def transport():
|
||||
return client.TRANSPORT_APRSIS
|
||||
|
||||
def decode_packet(self, *args, **kwargs):
|
||||
"""APRS lib already decodes this."""
|
||||
return core.factory(args[0])
|
||||
|
||||
def setup_connection(self):
|
||||
user = CONF.aprs_network.login
|
||||
password = CONF.aprs_network.password
|
||||
host = CONF.aprs_network.host
|
||||
port = CONF.aprs_network.port
|
||||
self.connected = False
|
||||
backoff = 1
|
||||
aprs_client = None
|
||||
retries = 3
|
||||
retry_count = 0
|
||||
while not self.connected:
|
||||
retry_count += 1
|
||||
if retry_count >= retries:
|
||||
break
|
||||
try:
|
||||
LOG.info(
|
||||
f"Creating aprslib client({host}:{port}) and logging in {user}."
|
||||
)
|
||||
aprs_client = aprsis.Aprsdis(
|
||||
user, passwd=password, host=host, port=port
|
||||
)
|
||||
# Force the log to be the same
|
||||
aprs_client.logger = LOG
|
||||
aprs_client.connect()
|
||||
self.connected = self.login_status["success"] = True
|
||||
self.login_status["message"] = aprs_client.server_string
|
||||
backoff = 1
|
||||
except LoginError as e:
|
||||
LOG.error(f"Failed to login to APRS-IS Server '{e}'")
|
||||
self.connected = self.login_status["success"] = False
|
||||
self.login_status["message"] = e.message
|
||||
LOG.error(e.message)
|
||||
time.sleep(backoff)
|
||||
except Exception as e:
|
||||
LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
|
||||
self.connected = self.login_status["success"] = False
|
||||
self.login_status["message"] = e.message
|
||||
time.sleep(backoff)
|
||||
# Don't allow the backoff to go to inifinity.
|
||||
if backoff > 5:
|
||||
backoff = 5
|
||||
else:
|
||||
backoff += 1
|
||||
continue
|
||||
self._client = aprs_client
|
||||
return aprs_client
|
||||
|
||||
def consumer(self, callback, blocking=False, immortal=False, raw=False):
|
||||
if self._client:
|
||||
try:
|
||||
self._client.consumer(
|
||||
callback,
|
||||
blocking=blocking,
|
||||
immortal=immortal,
|
||||
raw=raw,
|
||||
)
|
||||
except Exception as e:
|
||||
LOG.error(e)
|
||||
LOG.info(e.__cause__)
|
||||
raise e
|
||||
else:
|
||||
LOG.warning("client is None, might be resetting.")
|
||||
self.connected = False
|
@ -1,156 +0,0 @@
|
||||
import abc
|
||||
import logging
|
||||
import threading
|
||||
|
||||
import wrapt
|
||||
from oslo_config import cfg
|
||||
|
||||
from aprsd.packets import core
|
||||
from aprsd.utils import keepalive_collector
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger('APRSD')
|
||||
|
||||
|
||||
class APRSClient:
|
||||
"""Singleton client class that constructs the aprslib connection."""
|
||||
|
||||
_instance = None
|
||||
_client = None
|
||||
|
||||
connected = False
|
||||
login_status = {
|
||||
'success': False,
|
||||
'message': None,
|
||||
}
|
||||
filter = None
|
||||
lock = threading.Lock()
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""This magic turns this into a singleton."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
keepalive_collector.KeepAliveCollector().register(cls)
|
||||
# Put any initialization here.
|
||||
cls._instance._create_client()
|
||||
return cls._instance
|
||||
|
||||
@abc.abstractmethod
|
||||
def stats(self) -> dict:
|
||||
"""Return statistics about the client connection.
|
||||
|
||||
Returns:
|
||||
dict: Statistics about the connection and packet handling
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def keepalive_check(self) -> None:
|
||||
"""Called during keepalive run to check status."""
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
def keepalive_log(self) -> None:
|
||||
"""Log any keepalive information."""
|
||||
...
|
||||
|
||||
@property
|
||||
def is_connected(self):
|
||||
return self.connected
|
||||
|
||||
@property
|
||||
def login_success(self):
|
||||
return self.login_status.get('success', False)
|
||||
|
||||
@property
|
||||
def login_failure(self):
|
||||
return self.login_status['message']
|
||||
|
||||
def set_filter(self, filter):
|
||||
self.filter = filter
|
||||
if self._client:
|
||||
self._client.set_filter(filter)
|
||||
|
||||
def get_filter(self):
|
||||
return self.filter
|
||||
|
||||
@property
|
||||
def client(self):
|
||||
if not self._client:
|
||||
self._create_client()
|
||||
return self._client
|
||||
|
||||
def _create_client(self):
|
||||
try:
|
||||
self._client = self.setup_connection()
|
||||
if self.filter:
|
||||
LOG.info('Creating APRS client filter')
|
||||
self._client.set_filter(self.filter)
|
||||
except Exception as e:
|
||||
LOG.error(f'Failed to create APRS client: {e}')
|
||||
self._client = None
|
||||
raise
|
||||
|
||||
def stop(self):
|
||||
if self._client:
|
||||
LOG.info('Stopping client connection.')
|
||||
self._client.stop()
|
||||
|
||||
def send(self, packet: core.Packet) -> None:
|
||||
"""Send a packet to the network.
|
||||
|
||||
Args:
|
||||
packet: The APRS packet to send
|
||||
"""
|
||||
self.client.send(packet)
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def reset(self) -> None:
|
||||
"""Call this to force a rebuild/reconnect."""
|
||||
LOG.info('Resetting client connection.')
|
||||
if self._client:
|
||||
self._client.close()
|
||||
del self._client
|
||||
self._create_client()
|
||||
else:
|
||||
LOG.warning('Client not initialized, nothing to reset.')
|
||||
|
||||
# Recreate the client
|
||||
LOG.info(f'Creating new client {self.client}')
|
||||
|
||||
@abc.abstractmethod
|
||||
def setup_connection(self):
|
||||
"""Initialize and return the underlying APRS connection.
|
||||
|
||||
Returns:
|
||||
object: The initialized connection object
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
@abc.abstractmethod
|
||||
def is_enabled():
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@abc.abstractmethod
|
||||
def transport():
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def decode_packet(self, *args, **kwargs):
|
||||
"""Decode raw APRS packet data into a Packet object.
|
||||
|
||||
Returns:
|
||||
Packet: Decoded APRS packet
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def consumer(self, callback, blocking=False, immortal=False, raw=False):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def is_alive(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def close(self):
|
||||
pass
|
141
aprsd/client/client.py
Normal file
141
aprsd/client/client.py
Normal file
@ -0,0 +1,141 @@
|
||||
import logging
|
||||
import threading
|
||||
from typing import Callable
|
||||
|
||||
import timeago
|
||||
import wrapt
|
||||
from loguru import logger
|
||||
from oslo_config import cfg
|
||||
|
||||
from aprsd.client import drivers # noqa - ensure drivers are registered
|
||||
from aprsd.client.drivers.registry import DriverRegistry
|
||||
from aprsd.packets import core
|
||||
from aprsd.utils import keepalive_collector
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger('APRSD')
|
||||
LOGU = logger
|
||||
|
||||
|
||||
class APRSDClient:
|
||||
"""APRSD client class.
|
||||
|
||||
This is a singleton class that provides a single instance of the APRSD client.
|
||||
It is responsible for connecting to the appropriate APRSD client driver based on
|
||||
the configuration.
|
||||
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
driver = None
|
||||
lock = threading.Lock()
|
||||
filter = None
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""This magic turns this into a singleton."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
keepalive_collector.KeepAliveCollector().register(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
self.connected = False
|
||||
self.login_status = {
|
||||
'success': False,
|
||||
'message': None,
|
||||
}
|
||||
if not self.driver:
|
||||
self.driver = DriverRegistry().get_driver()
|
||||
self.driver.setup_connection()
|
||||
|
||||
def stats(self, serializable=False) -> dict:
|
||||
stats = {}
|
||||
if self.driver:
|
||||
stats = self.driver.stats(serializable=serializable)
|
||||
return stats
|
||||
|
||||
@property
|
||||
def is_enabled(self):
|
||||
if not self.driver:
|
||||
return False
|
||||
return self.driver.is_enabled()
|
||||
|
||||
@property
|
||||
def is_configured(self):
|
||||
if not self.driver:
|
||||
return False
|
||||
return self.driver.is_configured()
|
||||
|
||||
# @property
|
||||
# def is_connected(self):
|
||||
# if not self.driver:
|
||||
# return False
|
||||
# return self.driver.is_connected()
|
||||
|
||||
@property
|
||||
def login_success(self):
|
||||
if not self.driver:
|
||||
return False
|
||||
return self.driver.login_success
|
||||
|
||||
@property
|
||||
def login_failure(self):
|
||||
if not self.driver:
|
||||
return None
|
||||
return self.driver.login_failure
|
||||
|
||||
def set_filter(self, filter):
|
||||
self.filter = filter
|
||||
if not self.driver:
|
||||
return
|
||||
self.driver.set_filter(filter)
|
||||
|
||||
def get_filter(self):
|
||||
if not self.driver:
|
||||
return None
|
||||
return self.driver.filter
|
||||
|
||||
def is_alive(self):
|
||||
return self.driver.is_alive()
|
||||
|
||||
def close(self):
|
||||
if not self.driver:
|
||||
return
|
||||
self.driver.close()
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def reset(self):
|
||||
"""Call this to force a rebuild/reconnect."""
|
||||
LOG.info('Resetting client connection.')
|
||||
if self.driver:
|
||||
self.driver.close()
|
||||
self.driver.setup_connection()
|
||||
if self.filter:
|
||||
self.driver.set_filter(self.filter)
|
||||
else:
|
||||
LOG.warning('Client not initialized, nothing to reset.')
|
||||
|
||||
def send(self, packet: core.Packet) -> bool:
|
||||
return self.driver.send(packet)
|
||||
|
||||
# For the keepalive collector
|
||||
def keepalive_check(self):
|
||||
# Don't check the first time through.
|
||||
if not self.driver.is_alive and self._checks:
|
||||
LOG.warning("Resetting client. It's not alive.")
|
||||
self.reset()
|
||||
self._checks = True
|
||||
|
||||
# For the keepalive collector
|
||||
def keepalive_log(self):
|
||||
if ka := self.driver.keepalive:
|
||||
keepalive = timeago.format(ka)
|
||||
else:
|
||||
keepalive = 'N/A'
|
||||
LOGU.opt(colors=True).info(f'<green>Client keepalive {keepalive}</green>')
|
||||
|
||||
def consumer(self, callback: Callable, raw: bool = False):
|
||||
return self.driver.consumer(callback=callback, raw=raw)
|
||||
|
||||
def decode_packet(self, *args, **kwargs) -> core.Packet:
|
||||
return self.driver.decode_packet(*args, **kwargs)
|
@ -0,0 +1,10 @@
|
||||
# All client drivers must be registered here
|
||||
from aprsd.client.drivers.aprsis import APRSISDriver
|
||||
from aprsd.client.drivers.fake import APRSDFakeDriver
|
||||
from aprsd.client.drivers.registry import DriverRegistry
|
||||
from aprsd.client.drivers.tcpkiss import TCPKISSDriver
|
||||
|
||||
driver_registry = DriverRegistry()
|
||||
driver_registry.register(APRSDFakeDriver)
|
||||
driver_registry.register(APRSISDriver)
|
||||
driver_registry.register(TCPKISSDriver)
|
@ -1,286 +1,205 @@
|
||||
import datetime
|
||||
import logging
|
||||
import select
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from typing import Callable
|
||||
|
||||
import aprslib
|
||||
import wrapt
|
||||
from aprslib import is_py3
|
||||
from aprslib.exceptions import (
|
||||
ConnectionDrop,
|
||||
ConnectionError,
|
||||
GenericError,
|
||||
LoginError,
|
||||
ParseError,
|
||||
UnknownFormat,
|
||||
)
|
||||
from aprslib.exceptions import LoginError
|
||||
from loguru import logger
|
||||
from oslo_config import cfg
|
||||
|
||||
import aprsd
|
||||
from aprsd import client, exception
|
||||
from aprsd.client.drivers.lib.aprslib import APRSLibClient
|
||||
from aprsd.packets import core
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger('APRSD')
|
||||
LOGU = logger
|
||||
|
||||
|
||||
class Aprsdis(aprslib.IS):
|
||||
"""Extend the aprslib class so we can exit properly."""
|
||||
# class APRSISDriver(metaclass=trace.TraceWrapperMetaclass):
|
||||
class APRSISDriver:
|
||||
"""This is the APRS-IS driver for the APRSD client.
|
||||
|
||||
# flag to tell us to stop
|
||||
thread_stop = False
|
||||
This driver uses our modified aprslib.IS class to connect to the APRS-IS server.
|
||||
|
||||
# date for last time we heard from the server
|
||||
aprsd_keepalive = datetime.datetime.now()
|
||||
"""
|
||||
|
||||
# Which server we are connected to?
|
||||
server_string = 'None'
|
||||
_client = None
|
||||
_checks = False
|
||||
|
||||
# timeout in seconds
|
||||
select_timeout = 1
|
||||
lock = threading.Lock()
|
||||
def __init__(self):
|
||||
max_timeout = {'hours': 0.0, 'minutes': 2, 'seconds': 0}
|
||||
self.max_delta = datetime.timedelta(**max_timeout)
|
||||
self.login_status = {
|
||||
'success': False,
|
||||
'message': None,
|
||||
}
|
||||
|
||||
def stop(self):
|
||||
self.thread_stop = True
|
||||
LOG.warning('Shutdown Aprsdis client.')
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
# Defaults to True if the enabled flag is non existent
|
||||
try:
|
||||
return CONF.aprs_network.enabled
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_configured():
|
||||
if APRSISDriver.is_enabled():
|
||||
# Ensure that the config vars are correctly set
|
||||
if not CONF.aprs_network.login:
|
||||
LOG.error('Config aprs_network.login not set.')
|
||||
raise exception.MissingConfigOptionException(
|
||||
'aprs_network.login is not set.',
|
||||
)
|
||||
if not CONF.aprs_network.password:
|
||||
LOG.error('Config aprs_network.password not set.')
|
||||
raise exception.MissingConfigOptionException(
|
||||
'aprs_network.password is not set.',
|
||||
)
|
||||
if not CONF.aprs_network.host:
|
||||
LOG.error('Config aprs_network.host not set.')
|
||||
raise exception.MissingConfigOptionException(
|
||||
'aprs_network.host is not set.',
|
||||
)
|
||||
|
||||
return True
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_alive(self):
|
||||
if not self._client:
|
||||
LOG.warning(f'APRS_CLIENT {self._client} alive? NO!!!')
|
||||
return False
|
||||
return self._client.is_alive() and not self._is_stale_connection()
|
||||
|
||||
def close(self):
|
||||
LOG.warning('Closing Aprsdis client.')
|
||||
super().close()
|
||||
if self._client:
|
||||
self._client.stop()
|
||||
self._client.close()
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def send(self, packet: core.Packet):
|
||||
"""Send an APRS Message object."""
|
||||
self.sendall(packet.raw)
|
||||
def send(self, packet: core.Packet) -> bool:
|
||||
return self._client.send(packet)
|
||||
|
||||
def is_alive(self):
|
||||
"""If the connection is alive or not."""
|
||||
return self._connected
|
||||
|
||||
def _connect(self):
|
||||
"""
|
||||
Attemps connection to the server
|
||||
"""
|
||||
|
||||
self.logger.info(
|
||||
'Attempting connection to %s:%s', self.server[0], self.server[1]
|
||||
)
|
||||
|
||||
try:
|
||||
self._open_socket()
|
||||
|
||||
peer = self.sock.getpeername()
|
||||
|
||||
self.logger.info('Connected to %s', str(peer))
|
||||
|
||||
# 5 second timeout to receive server banner
|
||||
self.sock.setblocking(1)
|
||||
self.sock.settimeout(5)
|
||||
|
||||
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
# MACOS doesn't have TCP_KEEPIDLE
|
||||
if hasattr(socket, 'TCP_KEEPIDLE'):
|
||||
self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 1)
|
||||
self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 3)
|
||||
self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5)
|
||||
|
||||
banner = self.sock.recv(512)
|
||||
if is_py3:
|
||||
banner = banner.decode('latin-1')
|
||||
|
||||
if banner[0] == '#':
|
||||
self.logger.debug('Banner: %s', banner.rstrip())
|
||||
else:
|
||||
raise ConnectionError('invalid banner from server')
|
||||
|
||||
except ConnectionError as e:
|
||||
self.logger.error(str(e))
|
||||
self.close()
|
||||
raise
|
||||
except (socket.error, socket.timeout) as e:
|
||||
self.close()
|
||||
|
||||
self.logger.error('Socket error: %s' % str(e))
|
||||
if str(e) == 'timed out':
|
||||
raise ConnectionError('no banner from server') from e
|
||||
else:
|
||||
raise ConnectionError(e) from e
|
||||
|
||||
self._connected = True
|
||||
|
||||
def _socket_readlines(self, blocking=False):
|
||||
"""
|
||||
Generator for complete lines, received from the server
|
||||
"""
|
||||
try:
|
||||
self.sock.setblocking(0)
|
||||
except OSError as e:
|
||||
self.logger.error(f'socket error when setblocking(0): {str(e)}')
|
||||
raise aprslib.ConnectionDrop('connection dropped') from e
|
||||
|
||||
while not self.thread_stop:
|
||||
short_buf = b''
|
||||
newline = b'\r\n'
|
||||
|
||||
# set a select timeout, so we get a chance to exit
|
||||
# when user hits CTRL-C
|
||||
readable, writable, exceptional = select.select(
|
||||
[self.sock],
|
||||
[],
|
||||
[],
|
||||
self.select_timeout,
|
||||
)
|
||||
if not readable:
|
||||
if not blocking:
|
||||
break
|
||||
else:
|
||||
continue
|
||||
|
||||
try:
|
||||
short_buf = self.sock.recv(4096)
|
||||
|
||||
# sock.recv returns empty if the connection drops
|
||||
if not short_buf:
|
||||
if not blocking:
|
||||
# We could just not be blocking, so empty is expected
|
||||
continue
|
||||
else:
|
||||
self.logger.error('socket.recv(): returned empty')
|
||||
raise aprslib.ConnectionDrop('connection dropped')
|
||||
except OSError as e:
|
||||
# self.logger.error("socket error on recv(): %s" % str(e))
|
||||
if 'Resource temporarily unavailable' in str(e):
|
||||
if not blocking:
|
||||
if len(self.buf) == 0:
|
||||
break
|
||||
|
||||
self.buf += short_buf
|
||||
|
||||
while newline in self.buf:
|
||||
line, self.buf = self.buf.split(newline, 1)
|
||||
|
||||
yield line
|
||||
|
||||
def _send_login(self):
|
||||
"""
|
||||
Sends login string to server
|
||||
"""
|
||||
login_str = 'user {0} pass {1} vers Python-APRSD {3}{2}\r\n'
|
||||
login_str = login_str.format(
|
||||
self.callsign,
|
||||
self.passwd,
|
||||
(' filter ' + self.filter) if self.filter != '' else '',
|
||||
aprsd.__version__,
|
||||
)
|
||||
|
||||
self.logger.debug('Sending login information')
|
||||
|
||||
try:
|
||||
self._sendall(login_str)
|
||||
self.sock.settimeout(5)
|
||||
test = self.sock.recv(len(login_str) + 100)
|
||||
if is_py3:
|
||||
test = test.decode('latin-1')
|
||||
test = test.rstrip()
|
||||
|
||||
self.logger.debug("Server: '%s'", test)
|
||||
|
||||
if not test:
|
||||
raise LoginError(f"Server Response Empty: '{test}'")
|
||||
|
||||
_, _, callsign, status, e = test.split(' ', 4)
|
||||
s = e.split(',')
|
||||
if len(s):
|
||||
server_string = s[0].replace('server ', '')
|
||||
else:
|
||||
server_string = e.replace('server ', '')
|
||||
|
||||
if callsign == '':
|
||||
raise LoginError('Server responded with empty callsign???')
|
||||
if callsign != self.callsign:
|
||||
raise LoginError(f'Server: {test}')
|
||||
if status != 'verified,' and self.passwd != '-1':
|
||||
raise LoginError('Password is incorrect')
|
||||
|
||||
if self.passwd == '-1':
|
||||
self.logger.info('Login successful (receive only)')
|
||||
else:
|
||||
self.logger.info('Login successful')
|
||||
|
||||
self.logger.info(f'Connected to {server_string}')
|
||||
self.server_string = server_string
|
||||
|
||||
except LoginError as e:
|
||||
self.logger.error(str(e))
|
||||
self.close()
|
||||
raise
|
||||
except Exception as e:
|
||||
self.close()
|
||||
self.logger.error(f"Failed to login '{e}'")
|
||||
self.logger.exception(e)
|
||||
raise LoginError('Failed to login') from e
|
||||
|
||||
def consumer(self, callback, blocking=True, immortal=False, raw=False):
|
||||
"""
|
||||
When a position sentence is received, it will be passed to the callback function
|
||||
|
||||
blocking: if true (default), runs forever, otherwise will return after one sentence
|
||||
You can still exit the loop, by raising StopIteration in the callback function
|
||||
|
||||
immortal: When true, consumer will try to reconnect and stop propagation of Parse exceptions
|
||||
if false (default), consumer will return
|
||||
|
||||
raw: when true, raw packet is passed to callback, otherwise the result from aprs.parse()
|
||||
"""
|
||||
|
||||
if not self._connected:
|
||||
raise ConnectionError('not connected to a server')
|
||||
|
||||
line = b''
|
||||
|
||||
while True and not self.thread_stop:
|
||||
try:
|
||||
for line in self._socket_readlines(blocking):
|
||||
if line[0:1] != b'#':
|
||||
self.aprsd_keepalive = datetime.datetime.now()
|
||||
if raw:
|
||||
callback(line)
|
||||
else:
|
||||
callback(self._parse(line))
|
||||
else:
|
||||
self.logger.debug('Server: %s', line.decode('utf8'))
|
||||
self.aprsd_keepalive = datetime.datetime.now()
|
||||
except ParseError as exp:
|
||||
self.logger.log(
|
||||
11,
|
||||
"%s Packet: '%s'",
|
||||
exp,
|
||||
exp.packet,
|
||||
)
|
||||
except UnknownFormat as exp:
|
||||
self.logger.log(
|
||||
9,
|
||||
"%s Packet: '%s'",
|
||||
exp,
|
||||
exp.packet,
|
||||
)
|
||||
except LoginError as exp:
|
||||
self.logger.error('%s: %s', exp.__class__.__name__, exp)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except (ConnectionDrop, ConnectionError):
|
||||
self.close()
|
||||
|
||||
if not immortal:
|
||||
raise
|
||||
else:
|
||||
self.connect(blocking=blocking)
|
||||
continue
|
||||
except GenericError:
|
||||
pass
|
||||
except StopIteration:
|
||||
def setup_connection(self):
|
||||
user = CONF.aprs_network.login
|
||||
password = CONF.aprs_network.password
|
||||
host = CONF.aprs_network.host
|
||||
port = CONF.aprs_network.port
|
||||
self.connected = False
|
||||
backoff = 1
|
||||
retries = 3
|
||||
retry_count = 0
|
||||
while not self.connected:
|
||||
retry_count += 1
|
||||
if retry_count >= retries:
|
||||
break
|
||||
except Exception:
|
||||
self.logger.error('APRS Packet: %s', line)
|
||||
raise
|
||||
try:
|
||||
LOG.info(
|
||||
f'Creating aprslib client({host}:{port}) and logging in {user}.'
|
||||
)
|
||||
self._client = APRSLibClient(
|
||||
user, passwd=password, host=host, port=port
|
||||
)
|
||||
# Force the log to be the same
|
||||
self._client.logger = LOG
|
||||
self._client.connect()
|
||||
self.connected = self.login_status['success'] = True
|
||||
self.login_status['message'] = self._client.server_string
|
||||
backoff = 1
|
||||
except LoginError as e:
|
||||
LOG.error(f"Failed to login to APRS-IS Server '{e}'")
|
||||
self.connected = self.login_status['success'] = False
|
||||
self.login_status['message'] = (
|
||||
e.message if hasattr(e, 'message') else str(e)
|
||||
)
|
||||
LOG.error(self.login_status['message'])
|
||||
time.sleep(backoff)
|
||||
except Exception as e:
|
||||
LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
|
||||
self.connected = self.login_status['success'] = False
|
||||
self.login_status['message'] = getattr(e, 'message', str(e))
|
||||
time.sleep(backoff)
|
||||
# Don't allow the backoff to go to inifinity.
|
||||
if backoff > 5:
|
||||
backoff = 5
|
||||
else:
|
||||
backoff += 1
|
||||
continue
|
||||
|
||||
if not blocking:
|
||||
break
|
||||
def set_filter(self, filter):
|
||||
self._client.set_filter(filter)
|
||||
|
||||
def login_success(self) -> bool:
|
||||
return self.login_status.get('success', False)
|
||||
|
||||
def login_failure(self) -> str:
|
||||
return self.login_status.get('message', None)
|
||||
|
||||
@property
|
||||
def filter(self):
|
||||
return self._client.filter
|
||||
|
||||
@property
|
||||
def server_string(self):
|
||||
return self._client.server_string
|
||||
|
||||
@property
|
||||
def keepalive(self):
|
||||
return self._client.aprsd_keepalive
|
||||
|
||||
def _is_stale_connection(self):
|
||||
delta = datetime.datetime.now() - self._client.aprsd_keepalive
|
||||
if delta > self.max_delta:
|
||||
LOG.error(f'Connection is stale, last heard {delta} ago.')
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def transport():
|
||||
return client.TRANSPORT_APRSIS
|
||||
|
||||
def decode_packet(self, *args, **kwargs):
|
||||
"""APRS lib already decodes this."""
|
||||
return core.factory(args[0])
|
||||
|
||||
def consumer(self, callback: Callable, raw: bool = False):
|
||||
if self._client:
|
||||
try:
|
||||
self._client.consumer(
|
||||
callback,
|
||||
blocking=False,
|
||||
immortal=False,
|
||||
raw=raw,
|
||||
)
|
||||
except Exception as e:
|
||||
LOG.error(e)
|
||||
LOG.info(e.__cause__)
|
||||
raise e
|
||||
else:
|
||||
LOG.warning('client is None, might be resetting.')
|
||||
self.connected = False
|
||||
|
||||
def stats(self, serializable=False) -> dict:
|
||||
stats = {}
|
||||
if self.is_configured():
|
||||
if self._client:
|
||||
keepalive = self._client.aprsd_keepalive
|
||||
server_string = self._client.server_string
|
||||
if serializable:
|
||||
keepalive = keepalive.isoformat()
|
||||
filter = self.filter
|
||||
else:
|
||||
keepalive = 'None'
|
||||
server_string = 'None'
|
||||
filter = 'None'
|
||||
stats = {
|
||||
'connected': self.is_alive,
|
||||
'filter': filter,
|
||||
'login_status': self.login_status,
|
||||
'connection_keepalive': keepalive,
|
||||
'server_string': server_string,
|
||||
'transport': self.transport(),
|
||||
}
|
||||
|
||||
return stats
|
||||
|
@ -2,6 +2,7 @@ import datetime
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from typing import Callable
|
||||
|
||||
import aprslib
|
||||
import wrapt
|
||||
@ -15,7 +16,7 @@ CONF = cfg.CONF
|
||||
LOG = logging.getLogger('APRSD')
|
||||
|
||||
|
||||
class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
|
||||
class APRSDFakeDriver(metaclass=trace.TraceWrapperMetaclass):
|
||||
"""Fake client for testing."""
|
||||
|
||||
# flag to tell us to stop
|
||||
@ -28,17 +29,40 @@ class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
|
||||
path = []
|
||||
|
||||
def __init__(self):
|
||||
LOG.info('Starting APRSDFakeClient client.')
|
||||
LOG.info('Starting APRSDFakeDriver driver.')
|
||||
self.path = ['WIDE1-1', 'WIDE2-1']
|
||||
|
||||
def stop(self):
|
||||
self.thread_stop = True
|
||||
LOG.info('Shutdown APRSDFakeClient client.')
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
if CONF.fake_client.enabled:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_configured():
|
||||
return APRSDFakeDriver.is_enabled
|
||||
|
||||
def is_alive(self):
|
||||
"""If the connection is alive or not."""
|
||||
return not self.thread_stop
|
||||
|
||||
def close(self):
|
||||
self.thread_stop = True
|
||||
LOG.info('Shutdown APRSDFakeDriver driver.')
|
||||
|
||||
def setup_connection(self):
|
||||
# It's fake....
|
||||
pass
|
||||
|
||||
def set_filter(self, filter: str) -> None:
|
||||
pass
|
||||
|
||||
def login_success(self) -> bool:
|
||||
return True
|
||||
|
||||
def login_failure(self) -> str:
|
||||
return None
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def send(self, packet: core.Packet):
|
||||
"""Send an APRS Message object."""
|
||||
@ -61,13 +85,37 @@ class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
|
||||
f'\'{packet.from_call}\' with PATH "{self.path}"',
|
||||
)
|
||||
|
||||
def consumer(self, callback, blocking=False, immortal=False, raw=False):
|
||||
def consumer(self, callback: Callable, raw: bool = False):
|
||||
LOG.debug('Start non blocking FAKE consumer')
|
||||
# Generate packets here?
|
||||
raw = 'GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW'
|
||||
pkt_raw = aprslib.parse(raw)
|
||||
pkt = core.factory(pkt_raw)
|
||||
raw_str = 'GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW'
|
||||
self.aprsd_keepalive = datetime.datetime.now()
|
||||
callback(packet=pkt)
|
||||
if raw:
|
||||
callback(raw=raw_str)
|
||||
else:
|
||||
pkt_raw = aprslib.parse(raw_str)
|
||||
pkt = core.factory(pkt_raw)
|
||||
callback(packet=pkt)
|
||||
|
||||
LOG.debug(f'END blocking FAKE consumer {self}')
|
||||
time.sleep(8)
|
||||
time.sleep(1)
|
||||
|
||||
def decode_packet(self, *args, **kwargs):
|
||||
"""APRS lib already decodes this."""
|
||||
if not kwargs:
|
||||
return None
|
||||
|
||||
if kwargs.get('packet'):
|
||||
return kwargs.get('packet')
|
||||
|
||||
if kwargs.get('raw'):
|
||||
pkt_raw = aprslib.parse(kwargs.get('raw'))
|
||||
pkt = core.factory(pkt_raw)
|
||||
return pkt
|
||||
|
||||
def stats(self, serializable: bool = False) -> dict:
|
||||
return {
|
||||
'driver': self.__class__.__name__,
|
||||
'is_alive': self.is_alive(),
|
||||
'transport': 'fake',
|
||||
}
|
||||
|
@ -1,144 +0,0 @@
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
import kiss
|
||||
from ax253 import Frame
|
||||
from oslo_config import cfg
|
||||
|
||||
from aprsd import conf # noqa
|
||||
from aprsd.packets import core
|
||||
from aprsd.utils import trace
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger('APRSD')
|
||||
|
||||
|
||||
class KISS3Client:
|
||||
path = []
|
||||
|
||||
# date for last time we heard from the server
|
||||
aprsd_keepalive = datetime.datetime.now()
|
||||
_connected = False
|
||||
|
||||
def __init__(self):
|
||||
self.setup()
|
||||
|
||||
def is_alive(self):
|
||||
return self._connected
|
||||
|
||||
def setup(self):
|
||||
# we can be TCP kiss or Serial kiss
|
||||
if CONF.kiss_serial.enabled:
|
||||
LOG.debug(
|
||||
'KISS({}) Serial connection to {}'.format(
|
||||
kiss.__version__,
|
||||
CONF.kiss_serial.device,
|
||||
),
|
||||
)
|
||||
self.kiss = kiss.SerialKISS(
|
||||
port=CONF.kiss_serial.device,
|
||||
speed=CONF.kiss_serial.baudrate,
|
||||
strip_df_start=True,
|
||||
)
|
||||
self.path = CONF.kiss_serial.path
|
||||
elif CONF.kiss_tcp.enabled:
|
||||
LOG.debug(
|
||||
'KISS({}) TCP Connection to {}:{}'.format(
|
||||
kiss.__version__,
|
||||
CONF.kiss_tcp.host,
|
||||
CONF.kiss_tcp.port,
|
||||
),
|
||||
)
|
||||
self.kiss = kiss.TCPKISS(
|
||||
host=CONF.kiss_tcp.host,
|
||||
port=CONF.kiss_tcp.port,
|
||||
strip_df_start=True,
|
||||
)
|
||||
self.path = CONF.kiss_tcp.path
|
||||
|
||||
LOG.debug('Starting KISS interface connection')
|
||||
try:
|
||||
self.kiss.start()
|
||||
if self.kiss.protocol.transport.is_closing():
|
||||
LOG.warning('KISS transport is closing, not setting consumer callback')
|
||||
self._connected = False
|
||||
else:
|
||||
self._connected = True
|
||||
except Exception:
|
||||
LOG.error('Failed to start KISS interface.')
|
||||
self._connected = False
|
||||
|
||||
@trace.trace
|
||||
def stop(self):
|
||||
if not self._connected:
|
||||
# do nothing since we aren't connected
|
||||
return
|
||||
|
||||
try:
|
||||
self.kiss.stop()
|
||||
self.kiss.loop.call_soon_threadsafe(
|
||||
self.kiss.protocol.transport.close,
|
||||
)
|
||||
except Exception:
|
||||
LOG.error('Failed to stop KISS interface.')
|
||||
|
||||
def close(self):
|
||||
self.stop()
|
||||
|
||||
def set_filter(self, filter):
|
||||
# This does nothing right now.
|
||||
pass
|
||||
|
||||
def parse_frame(self, frame_bytes):
|
||||
try:
|
||||
frame = Frame.from_bytes(frame_bytes)
|
||||
# Now parse it with aprslib
|
||||
kwargs = {
|
||||
'frame': frame,
|
||||
}
|
||||
self._parse_callback(**kwargs)
|
||||
self.aprsd_keepalive = datetime.datetime.now()
|
||||
except Exception as ex:
|
||||
LOG.error('Failed to parse bytes received from KISS interface.')
|
||||
LOG.exception(ex)
|
||||
|
||||
def consumer(self, callback):
|
||||
if not self._connected:
|
||||
raise Exception('KISS transport is not connected')
|
||||
|
||||
self._parse_callback = callback
|
||||
if not self.kiss.protocol.transport.is_closing():
|
||||
self.kiss.read(callback=self.parse_frame, min_frames=1)
|
||||
else:
|
||||
self._connected = False
|
||||
|
||||
def send(self, packet):
|
||||
"""Send an APRS Message object."""
|
||||
|
||||
payload = None
|
||||
path = self.path
|
||||
if isinstance(packet, core.Packet):
|
||||
packet.prepare()
|
||||
payload = packet.payload.encode('US-ASCII')
|
||||
if packet.path:
|
||||
path = packet.path
|
||||
else:
|
||||
msg_payload = f'{packet.raw}{{{str(packet.msgNo)}'
|
||||
payload = (
|
||||
':{:<9}:{}'.format(
|
||||
packet.to_call,
|
||||
msg_payload,
|
||||
)
|
||||
).encode('US-ASCII')
|
||||
|
||||
LOG.debug(
|
||||
f"KISS Send '{payload}' TO '{packet.to_call}' From "
|
||||
f"'{packet.from_call}' with PATH '{path}'",
|
||||
)
|
||||
frame = Frame.ui(
|
||||
destination='APZ100',
|
||||
source=packet.from_call,
|
||||
path=path,
|
||||
info=payload,
|
||||
)
|
||||
self.kiss.write(frame)
|
0
aprsd/client/drivers/lib/__init__.py
Normal file
0
aprsd/client/drivers/lib/__init__.py
Normal file
295
aprsd/client/drivers/lib/aprslib.py
Normal file
295
aprsd/client/drivers/lib/aprslib.py
Normal file
@ -0,0 +1,295 @@
|
||||
import datetime
|
||||
import logging
|
||||
import select
|
||||
import socket
|
||||
import threading
|
||||
|
||||
import aprslib
|
||||
import wrapt
|
||||
from aprslib import is_py3
|
||||
from aprslib.exceptions import (
|
||||
ConnectionDrop,
|
||||
ConnectionError,
|
||||
GenericError,
|
||||
LoginError,
|
||||
ParseError,
|
||||
UnknownFormat,
|
||||
)
|
||||
|
||||
import aprsd
|
||||
from aprsd.packets import core
|
||||
|
||||
LOG = logging.getLogger('APRSD')
|
||||
|
||||
|
||||
class APRSLibClient(aprslib.IS):
|
||||
"""Extend the aprslib class so we can exit properly.
|
||||
|
||||
This is a modified version of the aprslib.IS class that adds a stop method to
|
||||
allow the client to exit cleanly.
|
||||
|
||||
The aprsis driver uses this class to connect to the APRS-IS server.
|
||||
"""
|
||||
|
||||
# flag to tell us to stop
|
||||
thread_stop = False
|
||||
|
||||
# date for last time we heard from the server
|
||||
aprsd_keepalive = datetime.datetime.now()
|
||||
|
||||
# Which server we are connected to?
|
||||
server_string = 'None'
|
||||
|
||||
# timeout in seconds
|
||||
select_timeout = 1
|
||||
lock = threading.Lock()
|
||||
|
||||
def stop(self):
|
||||
self.thread_stop = True
|
||||
LOG.warning('Shutdown Aprsdis client.')
|
||||
|
||||
def close(self):
|
||||
LOG.warning('Closing Aprsdis client.')
|
||||
super().close()
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def send(self, packet: core.Packet):
|
||||
"""Send an APRS Message object."""
|
||||
self.sendall(packet.raw)
|
||||
|
||||
def is_alive(self):
|
||||
"""If the connection is alive or not."""
|
||||
return self._connected
|
||||
|
||||
def _connect(self):
|
||||
"""
|
||||
Attemps connection to the server
|
||||
"""
|
||||
|
||||
self.logger.info(
|
||||
'Attempting connection to %s:%s', self.server[0], self.server[1]
|
||||
)
|
||||
|
||||
try:
|
||||
self._open_socket()
|
||||
|
||||
peer = self.sock.getpeername()
|
||||
|
||||
self.logger.info('Connected to %s', str(peer))
|
||||
|
||||
# 5 second timeout to receive server banner
|
||||
self.sock.setblocking(1)
|
||||
self.sock.settimeout(5)
|
||||
|
||||
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
# MACOS doesn't have TCP_KEEPIDLE
|
||||
if hasattr(socket, 'TCP_KEEPIDLE'):
|
||||
self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 1)
|
||||
self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 3)
|
||||
self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5)
|
||||
|
||||
banner = self.sock.recv(512)
|
||||
if is_py3:
|
||||
banner = banner.decode('latin-1')
|
||||
|
||||
if banner[0] == '#':
|
||||
self.logger.debug('Banner: %s', banner.rstrip())
|
||||
else:
|
||||
raise ConnectionError('invalid banner from server')
|
||||
|
||||
except ConnectionError as e:
|
||||
self.logger.error(str(e))
|
||||
self.close()
|
||||
raise
|
||||
except (socket.error, socket.timeout) as e:
|
||||
self.close()
|
||||
|
||||
self.logger.error('Socket error: %s' % str(e))
|
||||
if str(e) == 'timed out':
|
||||
raise ConnectionError('no banner from server') from e
|
||||
else:
|
||||
raise ConnectionError(e) from e
|
||||
|
||||
self._connected = True
|
||||
|
||||
def _socket_readlines(self, blocking=False):
|
||||
"""
|
||||
Generator for complete lines, received from the server
|
||||
"""
|
||||
try:
|
||||
self.sock.setblocking(0)
|
||||
except OSError as e:
|
||||
self.logger.error(f'socket error when setblocking(0): {str(e)}')
|
||||
raise aprslib.ConnectionDrop('connection dropped') from e
|
||||
|
||||
while not self.thread_stop:
|
||||
short_buf = b''
|
||||
newline = b'\r\n'
|
||||
|
||||
# set a select timeout, so we get a chance to exit
|
||||
# when user hits CTRL-C
|
||||
readable, writable, exceptional = select.select(
|
||||
[self.sock],
|
||||
[],
|
||||
[],
|
||||
self.select_timeout,
|
||||
)
|
||||
if not readable:
|
||||
if not blocking:
|
||||
break
|
||||
else:
|
||||
continue
|
||||
|
||||
try:
|
||||
short_buf = self.sock.recv(4096)
|
||||
|
||||
# sock.recv returns empty if the connection drops
|
||||
if not short_buf:
|
||||
if not blocking:
|
||||
# We could just not be blocking, so empty is expected
|
||||
continue
|
||||
else:
|
||||
self.logger.error('socket.recv(): returned empty')
|
||||
raise aprslib.ConnectionDrop('connection dropped')
|
||||
except OSError as e:
|
||||
# self.logger.error("socket error on recv(): %s" % str(e))
|
||||
if 'Resource temporarily unavailable' in str(e):
|
||||
if not blocking:
|
||||
if len(self.buf) == 0:
|
||||
break
|
||||
|
||||
self.buf += short_buf
|
||||
|
||||
while newline in self.buf:
|
||||
line, self.buf = self.buf.split(newline, 1)
|
||||
|
||||
yield line
|
||||
|
||||
def _send_login(self):
|
||||
"""
|
||||
Sends login string to server
|
||||
"""
|
||||
login_str = 'user {0} pass {1} vers Python-APRSD {3}{2}\r\n'
|
||||
login_str = login_str.format(
|
||||
self.callsign,
|
||||
self.passwd,
|
||||
(' filter ' + self.filter) if self.filter != '' else '',
|
||||
aprsd.__version__,
|
||||
)
|
||||
|
||||
self.logger.debug('Sending login information')
|
||||
|
||||
try:
|
||||
self._sendall(login_str)
|
||||
self.sock.settimeout(5)
|
||||
test = self.sock.recv(len(login_str) + 100)
|
||||
if is_py3:
|
||||
test = test.decode('latin-1')
|
||||
test = test.rstrip()
|
||||
|
||||
self.logger.debug("Server: '%s'", test)
|
||||
|
||||
if not test:
|
||||
raise LoginError(f"Server Response Empty: '{test}'")
|
||||
|
||||
_, _, callsign, status, e = test.split(' ', 4)
|
||||
s = e.split(',')
|
||||
if len(s):
|
||||
server_string = s[0].replace('server ', '')
|
||||
else:
|
||||
server_string = e.replace('server ', '')
|
||||
|
||||
if callsign == '':
|
||||
raise LoginError('Server responded with empty callsign???')
|
||||
if callsign != self.callsign:
|
||||
raise LoginError(f'Server: {test}')
|
||||
if status != 'verified,' and self.passwd != '-1':
|
||||
raise LoginError('Password is incorrect')
|
||||
|
||||
if self.passwd == '-1':
|
||||
self.logger.info('Login successful (receive only)')
|
||||
else:
|
||||
self.logger.info('Login successful')
|
||||
|
||||
self.logger.info(f'Connected to {server_string}')
|
||||
self.server_string = server_string
|
||||
|
||||
except LoginError as e:
|
||||
self.logger.error(str(e))
|
||||
self.close()
|
||||
raise
|
||||
except Exception as e:
|
||||
self.close()
|
||||
self.logger.error(f"Failed to login '{e}'")
|
||||
self.logger.exception(e)
|
||||
raise LoginError('Failed to login') from e
|
||||
|
||||
def consumer(self, callback, blocking=True, immortal=False, raw=False):
|
||||
"""
|
||||
When a position sentence is received, it will be passed to the callback function
|
||||
|
||||
blocking: if true (default), runs forever, otherwise will return after one sentence
|
||||
You can still exit the loop, by raising StopIteration in the callback function
|
||||
|
||||
immortal: When true, consumer will try to reconnect and stop propagation of Parse exceptions
|
||||
if false (default), consumer will return
|
||||
|
||||
raw: when true, raw packet is passed to callback, otherwise the result from aprs.parse()
|
||||
"""
|
||||
|
||||
if not self._connected:
|
||||
raise ConnectionError('not connected to a server')
|
||||
|
||||
line = b''
|
||||
|
||||
while True and not self.thread_stop:
|
||||
try:
|
||||
for line in self._socket_readlines(blocking):
|
||||
if line[0:1] != b'#':
|
||||
self.aprsd_keepalive = datetime.datetime.now()
|
||||
if raw:
|
||||
callback(line)
|
||||
else:
|
||||
callback(self._parse(line))
|
||||
else:
|
||||
self.logger.debug('Server: %s', line.decode('utf8'))
|
||||
self.aprsd_keepalive = datetime.datetime.now()
|
||||
except ParseError as exp:
|
||||
self.logger.log(
|
||||
11,
|
||||
"%s Packet: '%s'",
|
||||
exp,
|
||||
exp.packet,
|
||||
)
|
||||
except UnknownFormat as exp:
|
||||
self.logger.log(
|
||||
9,
|
||||
"%s Packet: '%s'",
|
||||
exp,
|
||||
exp.packet,
|
||||
)
|
||||
except LoginError as exp:
|
||||
self.logger.error('%s: %s', exp.__class__.__name__, exp)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except (ConnectionDrop, ConnectionError):
|
||||
self.close()
|
||||
|
||||
if not immortal:
|
||||
raise
|
||||
else:
|
||||
self.connect(blocking=blocking)
|
||||
continue
|
||||
except GenericError:
|
||||
pass
|
||||
except StopIteration:
|
||||
break
|
||||
except IOError:
|
||||
self.logger.error('IOError')
|
||||
break
|
||||
except Exception:
|
||||
self.logger.error('APRS Packet: %s', line)
|
||||
raise
|
||||
|
||||
if not blocking:
|
||||
break
|
86
aprsd/client/drivers/registry.py
Normal file
86
aprsd/client/drivers/registry.py
Normal file
@ -0,0 +1,86 @@
|
||||
from typing import Callable, Protocol, runtime_checkable
|
||||
|
||||
from aprsd.packets import core
|
||||
from aprsd.utils import singleton, trace
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ClientDriver(Protocol):
|
||||
"""Protocol for APRSD client drivers.
|
||||
|
||||
This protocol defines the methods that must be
|
||||
implemented by APRSD client drivers.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def is_enabled(self) -> bool:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def is_configured(self) -> bool:
|
||||
pass
|
||||
|
||||
def is_alive(self) -> bool:
|
||||
pass
|
||||
|
||||
def close(self) -> None:
|
||||
pass
|
||||
|
||||
def send(self, packet: core.Packet) -> bool:
|
||||
pass
|
||||
|
||||
def setup_connection(self) -> None:
|
||||
pass
|
||||
|
||||
def set_filter(self, filter: str) -> None:
|
||||
pass
|
||||
|
||||
def login_success(self) -> bool:
|
||||
pass
|
||||
|
||||
def login_failure(self) -> str:
|
||||
pass
|
||||
|
||||
def consumer(self, callback: Callable, raw: bool = False) -> None:
|
||||
pass
|
||||
|
||||
def decode_packet(self, *args, **kwargs) -> core.Packet:
|
||||
pass
|
||||
|
||||
def stats(self, serializable: bool = False) -> dict:
|
||||
pass
|
||||
|
||||
|
||||
@singleton
|
||||
class DriverRegistry(metaclass=trace.TraceWrapperMetaclass):
|
||||
"""Registry for APRSD client drivers.
|
||||
|
||||
This registry is used to register and unregister APRSD client drivers.
|
||||
|
||||
This allows us to dynamically load the configured driver at runtime.
|
||||
|
||||
All drivers are registered, then when aprsd needs the client, the
|
||||
registry provides the configured driver for the single instance of the
|
||||
single APRSD client.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.drivers = []
|
||||
|
||||
def register(self, driver: Callable):
|
||||
if not isinstance(driver, ClientDriver):
|
||||
raise ValueError('Driver must be of ClientDriver type')
|
||||
self.drivers.append(driver)
|
||||
|
||||
def unregister(self, driver: Callable):
|
||||
if driver in self.drivers:
|
||||
self.drivers.remove(driver)
|
||||
else:
|
||||
raise ValueError(f'Driver {driver} not found')
|
||||
|
||||
def get_driver(self) -> ClientDriver:
|
||||
"""Get the first enabled driver."""
|
||||
for driver in self.drivers:
|
||||
if driver.is_enabled() and driver.is_configured():
|
||||
return driver()
|
||||
raise ValueError('No enabled driver found')
|
408
aprsd/client/drivers/tcpkiss.py
Normal file
408
aprsd/client/drivers/tcpkiss.py
Normal file
@ -0,0 +1,408 @@
|
||||
"""
|
||||
APRSD KISS Client Driver using native KISS implementation.
|
||||
|
||||
This module provides a KISS client driver for APRSD using the new
|
||||
non-asyncio KISSInterface implementation.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import select
|
||||
import socket
|
||||
import time
|
||||
from typing import Any, Callable, Dict
|
||||
|
||||
import aprslib
|
||||
from ax253 import frame as ax25frame
|
||||
from kiss import constants as kiss_constants
|
||||
from kiss import util as kissutil
|
||||
from kiss.kiss import Command
|
||||
from oslo_config import cfg
|
||||
|
||||
from aprsd import ( # noqa
|
||||
client,
|
||||
conf, # noqa
|
||||
exception,
|
||||
)
|
||||
from aprsd.packets import core
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger('APRSD')
|
||||
|
||||
|
||||
def handle_fend(buffer: bytes, strip_df_start: bool = True) -> bytes:
|
||||
"""
|
||||
Handle FEND (end of frame) encountered in a KISS data stream.
|
||||
|
||||
:param buffer: the buffer containing the frame
|
||||
:param strip_df_start: remove leading null byte (DATA_FRAME opcode)
|
||||
:return: the bytes of the frame without escape characters or frame
|
||||
end markers (FEND)
|
||||
"""
|
||||
frame = kissutil.recover_special_codes(kissutil.strip_nmea(bytes(buffer)))
|
||||
if strip_df_start:
|
||||
frame = kissutil.strip_df_start(frame)
|
||||
LOG.warning(f'handle_fend {" ".join(f"{b:02X}" for b in bytes(frame))}')
|
||||
return bytes(frame)
|
||||
|
||||
|
||||
# class TCPKISSDriver(metaclass=trace.TraceWrapperMetaclass):
|
||||
class TCPKISSDriver:
|
||||
"""APRSD client driver for TCP KISS connections."""
|
||||
|
||||
# Class level attributes required by Client protocol
|
||||
packets_received = 0
|
||||
packets_sent = 0
|
||||
last_packet_sent = None
|
||||
last_packet_received = None
|
||||
keepalive = None
|
||||
client_name = None
|
||||
socket = None
|
||||
# timeout in seconds
|
||||
select_timeout = 1
|
||||
path = None
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the KISS client.
|
||||
|
||||
Args:
|
||||
client_name: Name of the client instance
|
||||
"""
|
||||
super().__init__()
|
||||
self._connected = False
|
||||
self.keepalive = datetime.datetime.now()
|
||||
self._running = False
|
||||
# This is initialized in setup_connection()
|
||||
self.socket = None
|
||||
|
||||
@property
|
||||
def transport(self) -> str:
|
||||
return client.TRANSPORT_TCPKISS
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls) -> bool:
|
||||
"""Check if KISS is enabled in configuration.
|
||||
|
||||
Returns:
|
||||
bool: True if either TCP is enabled
|
||||
"""
|
||||
return CONF.kiss_tcp.enabled
|
||||
|
||||
@staticmethod
|
||||
def is_configured():
|
||||
# Ensure that the config vars are correctly set
|
||||
if TCPKISSDriver.is_enabled():
|
||||
if not CONF.kiss_tcp.host:
|
||||
LOG.error('KISS TCP enabled, but no host is set.')
|
||||
raise exception.MissingConfigOptionException(
|
||||
'kiss_tcp.host is not set.',
|
||||
)
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def is_alive(self) -> bool:
|
||||
"""Check if the client is connected.
|
||||
|
||||
Returns:
|
||||
bool: True if connected to KISS TNC, False otherwise
|
||||
"""
|
||||
return self._connected
|
||||
|
||||
def close(self):
|
||||
"""Close the connection."""
|
||||
self.stop()
|
||||
|
||||
def send(self, packet: core.Packet):
|
||||
"""Send an APRS packet.
|
||||
|
||||
Args:
|
||||
packet: APRS packet to send (Packet or Message object)
|
||||
|
||||
Raises:
|
||||
Exception: If not connected or send fails
|
||||
"""
|
||||
if not self.socket:
|
||||
raise Exception('KISS interface not initialized')
|
||||
|
||||
payload = None
|
||||
path = self.path
|
||||
packet.prepare()
|
||||
payload = packet.payload.encode('US-ASCII')
|
||||
if packet.path:
|
||||
path = packet.path
|
||||
|
||||
LOG.debug(
|
||||
f"KISS Send '{payload}' TO '{packet.to_call}' From "
|
||||
f"'{packet.from_call}' with PATH '{path}'",
|
||||
)
|
||||
frame = ax25frame.Frame.ui(
|
||||
destination='APZ100',
|
||||
# destination=packet.to_call,
|
||||
source=packet.from_call,
|
||||
path=path,
|
||||
info=payload,
|
||||
)
|
||||
|
||||
# now escape the frame special characters
|
||||
frame_escaped = kissutil.escape_special_codes(bytes(frame))
|
||||
# and finally wrap the frame in KISS protocol
|
||||
command = Command.DATA_FRAME
|
||||
frame_kiss = b''.join(
|
||||
[kiss_constants.FEND, command.value, frame_escaped, kiss_constants.FEND]
|
||||
)
|
||||
self.socket.send(frame_kiss)
|
||||
# Update last packet sent time
|
||||
self.last_packet_sent = datetime.datetime.now()
|
||||
# Increment packets sent counter
|
||||
self.packets_sent += 1
|
||||
|
||||
def setup_connection(self):
|
||||
"""Set up the KISS interface."""
|
||||
if not self.is_enabled():
|
||||
LOG.error('KISS is not enabled in configuration')
|
||||
return
|
||||
|
||||
try:
|
||||
# Configure for TCP KISS
|
||||
if self.is_enabled():
|
||||
LOG.info(
|
||||
f'KISS TCP Connection to {CONF.kiss_tcp.host}:{CONF.kiss_tcp.port}'
|
||||
)
|
||||
self.path = CONF.kiss_tcp.path
|
||||
self.connect()
|
||||
if self._connected:
|
||||
LOG.info('KISS interface initialized')
|
||||
else:
|
||||
LOG.error('Failed to connect to KISS interface')
|
||||
|
||||
except Exception as ex:
|
||||
LOG.error('Failed to initialize KISS interface')
|
||||
LOG.exception(ex)
|
||||
self._connected = False
|
||||
|
||||
def set_filter(self, filter_text: str):
|
||||
"""Set packet filter (not implemented for KISS).
|
||||
|
||||
Args:
|
||||
filter_text: Filter specification (ignored for KISS)
|
||||
"""
|
||||
# KISS doesn't support filtering at the TNC level
|
||||
pass
|
||||
|
||||
@property
|
||||
def filter(self) -> str:
|
||||
"""Get packet filter (not implemented for KISS).
|
||||
Returns:
|
||||
str: Empty string (not implemented for KISS)
|
||||
"""
|
||||
return ''
|
||||
|
||||
def login_success(self) -> bool:
|
||||
"""There is no login for KISS."""
|
||||
if not self._connected:
|
||||
return False
|
||||
return True
|
||||
|
||||
def login_failure(self) -> str:
|
||||
"""There is no login for KISS."""
|
||||
return 'Login successful'
|
||||
|
||||
def consumer(self, callback: Callable, raw: bool = False):
|
||||
"""Start consuming frames with the given callback.
|
||||
|
||||
Args:
|
||||
callback: Function to call with received packets
|
||||
|
||||
Raises:
|
||||
Exception: If not connected to KISS TNC
|
||||
"""
|
||||
self._running = True
|
||||
while self._running:
|
||||
# Ensure connection
|
||||
if not self._connected:
|
||||
if not self.connect():
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
# Read frame
|
||||
frame = self.read_frame()
|
||||
if frame:
|
||||
LOG.warning(f'GOT FRAME: {frame} calling {callback}')
|
||||
kwargs = {
|
||||
'frame': frame,
|
||||
}
|
||||
callback(**kwargs)
|
||||
|
||||
def decode_packet(self, *args, **kwargs) -> core.Packet:
|
||||
"""Decode a packet from an AX.25 frame.
|
||||
|
||||
Args:
|
||||
frame: Received AX.25 frame
|
||||
"""
|
||||
frame = kwargs.get('frame')
|
||||
if not frame:
|
||||
LOG.warning('No frame received to decode?!?!')
|
||||
return None
|
||||
|
||||
LOG.warning(f'FRAME: {str(frame)}')
|
||||
try:
|
||||
aprslib_frame = aprslib.parse(str(frame))
|
||||
return core.factory(aprslib_frame)
|
||||
except Exception as e:
|
||||
LOG.error(f'Error decoding packet: {e}')
|
||||
return None
|
||||
|
||||
def stop(self):
|
||||
"""Stop the KISS interface."""
|
||||
self._running = False
|
||||
self._connected = False
|
||||
if self.socket:
|
||||
try:
|
||||
self.socket.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def stats(self, serializable: bool = False) -> Dict[str, Any]:
|
||||
"""Get client statistics.
|
||||
|
||||
Returns:
|
||||
Dict containing client statistics
|
||||
"""
|
||||
if serializable:
|
||||
keepalive = self.keepalive.isoformat()
|
||||
else:
|
||||
keepalive = self.keepalive
|
||||
stats = {
|
||||
'client': self.__class__.__name__,
|
||||
'transport': self.transport,
|
||||
'connected': self._connected,
|
||||
'path': self.path,
|
||||
'packets_sent': self.packets_sent,
|
||||
'packets_received': self.packets_received,
|
||||
'last_packet_sent': self.last_packet_sent,
|
||||
'last_packet_received': self.last_packet_received,
|
||||
'connection_keepalive': keepalive,
|
||||
'host': CONF.kiss_tcp.host,
|
||||
'port': CONF.kiss_tcp.port,
|
||||
}
|
||||
|
||||
return stats
|
||||
|
||||
def connect(self) -> bool:
|
||||
"""Establish TCP connection to the KISS host.
|
||||
|
||||
Returns:
|
||||
bool: True if connection successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
if self.socket:
|
||||
try:
|
||||
self.socket.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.socket.settimeout(5.0) # 5 second timeout for connection
|
||||
self.socket.connect((CONF.kiss_tcp.host, CONF.kiss_tcp.port))
|
||||
self.socket.settimeout(0.1) # Reset to shorter timeout for reads
|
||||
self._connected = True
|
||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
# MACOS doesn't have TCP_KEEPIDLE
|
||||
if hasattr(socket, 'TCP_KEEPIDLE'):
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 1)
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 3)
|
||||
self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5)
|
||||
return True
|
||||
|
||||
except ConnectionError as e:
|
||||
LOG.error(
|
||||
f'Failed to connect to {CONF.kiss_tcp.host}:{CONF.kiss_tcp.port} - {str(e)}'
|
||||
)
|
||||
self._connected = False
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
LOG.error(
|
||||
f'Failed to connect to {CONF.kiss_tcp.host}:{CONF.kiss_tcp.port} - {str(e)}'
|
||||
)
|
||||
self._connected = False
|
||||
return False
|
||||
|
||||
def fix_raw_frame(self, raw_frame: bytes) -> bytes:
|
||||
"""Fix the raw frame by recalculating the FCS."""
|
||||
ax25_data = raw_frame[2:-1] # Remove KISS markers
|
||||
return handle_fend(ax25_data)
|
||||
|
||||
def read_frame(self, blocking=False):
|
||||
"""
|
||||
Generator for complete lines, received from the server
|
||||
"""
|
||||
try:
|
||||
self.socket.setblocking(0)
|
||||
except OSError as e:
|
||||
LOG.error(f'socket error when setblocking(0): {str(e)}')
|
||||
raise aprslib.ConnectionDrop('connection dropped') from e
|
||||
|
||||
while self._running:
|
||||
short_buf = b''
|
||||
|
||||
try:
|
||||
readable, _, _ = select.select(
|
||||
[self.socket],
|
||||
[],
|
||||
[],
|
||||
self.select_timeout,
|
||||
)
|
||||
if not readable:
|
||||
if not blocking:
|
||||
break
|
||||
else:
|
||||
continue
|
||||
except Exception as e:
|
||||
LOG.error(f'Error in read loop: {e}')
|
||||
self._connected = False
|
||||
break
|
||||
|
||||
try:
|
||||
print('reading from socket')
|
||||
short_buf = self.socket.recv(1024)
|
||||
print(f'short_buf: {short_buf}')
|
||||
# sock.recv returns empty if the connection drops
|
||||
if not short_buf:
|
||||
if not blocking:
|
||||
# We could just not be blocking, so empty is expected
|
||||
continue
|
||||
else:
|
||||
self.logger.error('socket.recv(): returned empty')
|
||||
raise aprslib.ConnectionDrop('connection dropped')
|
||||
|
||||
raw_frame = self.fix_raw_frame(short_buf)
|
||||
return ax25frame.Frame.from_bytes(raw_frame)
|
||||
except OSError as e:
|
||||
# self.logger.error("socket error on recv(): %s" % str(e))
|
||||
if 'Resource temporarily unavailable' in str(e):
|
||||
if not blocking:
|
||||
if len(short_buf) == 0:
|
||||
break
|
||||
except socket.timeout:
|
||||
continue
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except ConnectionError:
|
||||
self.close()
|
||||
if not self.auto_reconnect:
|
||||
raise
|
||||
else:
|
||||
self.connect()
|
||||
continue
|
||||
except StopIteration:
|
||||
break
|
||||
except IOError:
|
||||
LOG.error('IOError')
|
||||
break
|
||||
except Exception as e:
|
||||
LOG.error(f'Error in read loop: {e}')
|
||||
self._connected = False
|
||||
if not self.auto_reconnect:
|
||||
break
|
@ -1,91 +0,0 @@
|
||||
import logging
|
||||
from typing import Callable, Protocol, runtime_checkable
|
||||
|
||||
from aprsd import exception
|
||||
from aprsd.packets import core
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class Client(Protocol):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def connect(self) -> bool:
|
||||
pass
|
||||
|
||||
def disconnect(self) -> bool:
|
||||
pass
|
||||
|
||||
def decode_packet(self, *args, **kwargs) -> type[core.Packet]:
|
||||
pass
|
||||
|
||||
def is_enabled(self) -> bool:
|
||||
pass
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
pass
|
||||
|
||||
def transport(self) -> str:
|
||||
pass
|
||||
|
||||
def send(self, message: str) -> bool:
|
||||
pass
|
||||
|
||||
def setup_connection(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class ClientFactory:
|
||||
_instance = None
|
||||
clients = []
|
||||
client = None
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""This magic turns this into a singleton."""
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
# Put any initialization here.
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
self.clients: list[Callable] = []
|
||||
|
||||
def register(self, aprsd_client: Callable):
|
||||
if isinstance(aprsd_client, Client):
|
||||
raise ValueError("Client must be a subclass of Client protocol")
|
||||
|
||||
self.clients.append(aprsd_client)
|
||||
|
||||
def create(self, key=None):
|
||||
for client in self.clients:
|
||||
if client.is_enabled():
|
||||
self.client = client()
|
||||
return self.client
|
||||
raise Exception("No client is configured!!")
|
||||
|
||||
def client_exists(self):
|
||||
return bool(self.client)
|
||||
|
||||
def is_client_enabled(self):
|
||||
"""Make sure at least one client is enabled."""
|
||||
enabled = False
|
||||
for client in self.clients:
|
||||
if client.is_enabled():
|
||||
enabled = True
|
||||
return enabled
|
||||
|
||||
def is_client_configured(self):
|
||||
enabled = False
|
||||
for client in self.clients:
|
||||
try:
|
||||
if client.is_configured():
|
||||
enabled = True
|
||||
except exception.MissingConfigOptionException as ex:
|
||||
LOG.error(ex.message)
|
||||
return False
|
||||
except exception.ConfigOptionBogusDefaultException as ex:
|
||||
LOG.error(ex.message)
|
||||
return False
|
||||
return enabled
|
@ -1,49 +0,0 @@
|
||||
import logging
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from aprsd import client
|
||||
from aprsd.client import base
|
||||
from aprsd.client.drivers import fake as fake_driver
|
||||
from aprsd.utils import trace
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class APRSDFakeClient(base.APRSClient, metaclass=trace.TraceWrapperMetaclass):
|
||||
def stats(self, serializable=False) -> dict:
|
||||
return {
|
||||
"transport": "Fake",
|
||||
"connected": True,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
if CONF.fake_client.enabled:
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_configured():
|
||||
return APRSDFakeClient.is_enabled()
|
||||
|
||||
def is_alive(self):
|
||||
return True
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
def setup_connection(self):
|
||||
self.connected = True
|
||||
return fake_driver.APRSDFakeClient()
|
||||
|
||||
@staticmethod
|
||||
def transport():
|
||||
return client.TRANSPORT_FAKE
|
||||
|
||||
def decode_packet(self, *args, **kwargs):
|
||||
LOG.debug(f"kwargs {kwargs}")
|
||||
pkt = kwargs["packet"]
|
||||
LOG.debug(f"Got an APRS Fake Packet '{pkt}'")
|
||||
return pkt
|
@ -1,143 +0,0 @@
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
import aprslib
|
||||
import timeago
|
||||
from loguru import logger
|
||||
from oslo_config import cfg
|
||||
|
||||
from aprsd import client, exception
|
||||
from aprsd.client import base
|
||||
from aprsd.client.drivers import kiss
|
||||
from aprsd.packets import core
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger('APRSD')
|
||||
LOGU = logger
|
||||
|
||||
|
||||
class KISSClient(base.APRSClient):
|
||||
_client = None
|
||||
keepalive = datetime.datetime.now()
|
||||
|
||||
def stats(self, serializable=False) -> dict:
|
||||
stats = {}
|
||||
if self.is_configured():
|
||||
keepalive = self.keepalive
|
||||
if serializable:
|
||||
keepalive = keepalive.isoformat()
|
||||
stats = {
|
||||
'connected': self.is_connected,
|
||||
'connection_keepalive': keepalive,
|
||||
'transport': self.transport(),
|
||||
}
|
||||
if self.transport() == client.TRANSPORT_TCPKISS:
|
||||
stats['host'] = CONF.kiss_tcp.host
|
||||
stats['port'] = CONF.kiss_tcp.port
|
||||
elif self.transport() == client.TRANSPORT_SERIALKISS:
|
||||
stats['device'] = CONF.kiss_serial.device
|
||||
return stats
|
||||
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
"""Return if tcp or serial KISS is enabled."""
|
||||
if CONF.kiss_serial.enabled:
|
||||
return True
|
||||
|
||||
if CONF.kiss_tcp.enabled:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_configured():
|
||||
# Ensure that the config vars are correctly set
|
||||
if KISSClient.is_enabled():
|
||||
transport = KISSClient.transport()
|
||||
if transport == client.TRANSPORT_SERIALKISS:
|
||||
if not CONF.kiss_serial.device:
|
||||
LOG.error('KISS serial enabled, but no device is set.')
|
||||
raise exception.MissingConfigOptionException(
|
||||
'kiss_serial.device is not set.',
|
||||
)
|
||||
elif transport == client.TRANSPORT_TCPKISS:
|
||||
if not CONF.kiss_tcp.host:
|
||||
LOG.error('KISS TCP enabled, but no host is set.')
|
||||
raise exception.MissingConfigOptionException(
|
||||
'kiss_tcp.host is not set.',
|
||||
)
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_alive(self):
|
||||
if self._client:
|
||||
return self._client.is_alive()
|
||||
else:
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
if self._client:
|
||||
self._client.stop()
|
||||
|
||||
def keepalive_check(self):
|
||||
# Don't check the first time through.
|
||||
if not self.is_alive() and self._checks:
|
||||
LOG.warning("Resetting client. It's not alive.")
|
||||
self.reset()
|
||||
self._checks = True
|
||||
|
||||
def keepalive_log(self):
|
||||
if ka := self._client.aprsd_keepalive:
|
||||
keepalive = timeago.format(ka)
|
||||
else:
|
||||
keepalive = 'N/A'
|
||||
LOGU.opt(colors=True).info(f'<green>Client keepalive {keepalive}</green>')
|
||||
|
||||
@staticmethod
|
||||
def transport():
|
||||
if CONF.kiss_serial.enabled:
|
||||
return client.TRANSPORT_SERIALKISS
|
||||
|
||||
if CONF.kiss_tcp.enabled:
|
||||
return client.TRANSPORT_TCPKISS
|
||||
|
||||
def decode_packet(self, *args, **kwargs):
|
||||
"""We get a frame, which has to be decoded."""
|
||||
LOG.debug(f'kwargs {kwargs}')
|
||||
frame = kwargs['frame']
|
||||
LOG.debug(f"Got an APRS Frame '{frame}'")
|
||||
# try and nuke the * from the fromcall sign.
|
||||
# frame.header._source._ch = False
|
||||
# payload = str(frame.payload.decode())
|
||||
# msg = f"{str(frame.header)}:{payload}"
|
||||
# msg = frame.tnc2
|
||||
# LOG.debug(f"Decoding {msg}")
|
||||
|
||||
try:
|
||||
raw = aprslib.parse(str(frame))
|
||||
packet = core.factory(raw)
|
||||
if isinstance(packet, core.ThirdPartyPacket):
|
||||
return packet.subpacket
|
||||
else:
|
||||
return packet
|
||||
except Exception as ex:
|
||||
LOG.error(f'Error decoding packet: {ex}')
|
||||
|
||||
def setup_connection(self):
|
||||
try:
|
||||
self._client = kiss.KISS3Client()
|
||||
self.connected = self.login_status['success'] = True
|
||||
except Exception as ex:
|
||||
self.connected = self.login_status['success'] = False
|
||||
self.login_status['message'] = str(ex)
|
||||
return self._client
|
||||
|
||||
def consumer(self, callback, blocking=False, immortal=False, raw=False):
|
||||
try:
|
||||
self._client.consumer(callback)
|
||||
self.keepalive = datetime.datetime.now()
|
||||
except Exception as ex:
|
||||
LOG.error(f'Consumer failed {ex}')
|
||||
LOG.error(ex)
|
||||
raise ex
|
@ -3,7 +3,7 @@ import threading
|
||||
import wrapt
|
||||
from oslo_config import cfg
|
||||
|
||||
from aprsd import client
|
||||
from aprsd.client.client import APRSDClient
|
||||
from aprsd.utils import singleton
|
||||
|
||||
CONF = cfg.CONF
|
||||
@ -15,4 +15,4 @@ class APRSClientStats:
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def stats(self, serializable=False):
|
||||
return client.client_factory.create().stats(serializable=serializable)
|
||||
return APRSDClient().stats(serializable=serializable)
|
||||
|
@ -11,7 +11,6 @@ from oslo_config import cfg
|
||||
from aprsd import cli_helper, conf, packets, plugin
|
||||
|
||||
# local imports here
|
||||
from aprsd.client import base
|
||||
from aprsd.main import cli
|
||||
from aprsd.utils import trace
|
||||
|
||||
@ -97,8 +96,6 @@ def test_plugin(
|
||||
if CONF.trace_enabled:
|
||||
trace.setup_tracing(['method', 'api'])
|
||||
|
||||
base.APRSClient()
|
||||
|
||||
pm = plugin.PluginManager()
|
||||
if load_all:
|
||||
pm.setup_plugins(load_help_plugin=CONF.load_help_plugin)
|
||||
|
@ -17,7 +17,7 @@ from rich.console import Console
|
||||
# local imports here
|
||||
import aprsd
|
||||
from aprsd import cli_helper, packets, plugin, threads, utils
|
||||
from aprsd.client import client_factory
|
||||
from aprsd.client.client import APRSDClient
|
||||
from aprsd.main import cli
|
||||
from aprsd.packets import collector as packet_collector
|
||||
from aprsd.packets import core, seen_list
|
||||
@ -232,13 +232,13 @@ def listen(
|
||||
# Initialize the client factory and create
|
||||
# The correct client object ready for use
|
||||
# Make sure we have 1 client transport enabled
|
||||
if not client_factory.is_client_enabled():
|
||||
if not APRSDClient().is_enabled:
|
||||
LOG.error('No Clients are enabled in config.')
|
||||
sys.exit(-1)
|
||||
|
||||
# Creates the client object
|
||||
LOG.info('Creating client connection')
|
||||
aprs_client = client_factory.create()
|
||||
aprs_client = APRSDClient()
|
||||
LOG.info(aprs_client)
|
||||
if not aprs_client.login_success:
|
||||
# We failed to login, will just quit!
|
||||
|
@ -14,7 +14,7 @@ from aprsd import (
|
||||
conf, # noqa : F401
|
||||
packets,
|
||||
)
|
||||
from aprsd.client import client_factory
|
||||
from aprsd.client.client import APRSDClient
|
||||
from aprsd.main import cli
|
||||
from aprsd.packets import collector
|
||||
from aprsd.packets import log as packet_log
|
||||
@ -103,7 +103,7 @@ def send_message(
|
||||
|
||||
def rx_packet(packet):
|
||||
global got_ack, got_response
|
||||
cl = client_factory.create()
|
||||
cl = APRSDClient()
|
||||
packet = cl.decode_packet(packet)
|
||||
collector.PacketCollector().rx(packet)
|
||||
packet_log.log(packet, tx=False)
|
||||
@ -131,7 +131,7 @@ def send_message(
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
client_factory.create().client # noqa: B018
|
||||
APRSDClient().client # noqa: B018
|
||||
except LoginError:
|
||||
sys.exit(-1)
|
||||
|
||||
@ -163,7 +163,7 @@ def send_message(
|
||||
# This will register a packet consumer with aprslib
|
||||
# When new packets come in the consumer will process
|
||||
# the packet
|
||||
aprs_client = client_factory.create().client
|
||||
aprs_client = APRSDClient()
|
||||
aprs_client.consumer(rx_packet, raw=False)
|
||||
except aprslib.exceptions.ConnectionDrop:
|
||||
LOG.error('Connection dropped, reconnecting')
|
||||
|
@ -8,7 +8,7 @@ from oslo_config import cfg
|
||||
import aprsd
|
||||
from aprsd import cli_helper, plugin, threads, utils
|
||||
from aprsd import main as aprsd_main
|
||||
from aprsd.client import client_factory
|
||||
from aprsd.client.client import APRSDClient
|
||||
from aprsd.main import cli
|
||||
from aprsd.packets import collector as packet_collector
|
||||
from aprsd.packets import seen_list
|
||||
@ -47,24 +47,18 @@ def server(ctx, flush):
|
||||
LOG.info(msg)
|
||||
LOG.info(f'APRSD Started version: {aprsd.__version__}')
|
||||
|
||||
# Initialize the client factory and create
|
||||
# The correct client object ready for use
|
||||
if not client_factory.is_client_enabled():
|
||||
LOG.error('No Clients are enabled in config.')
|
||||
sys.exit(-1)
|
||||
|
||||
# Make sure we have 1 client transport enabled
|
||||
if not client_factory.is_client_enabled():
|
||||
if not APRSDClient().is_enabled:
|
||||
LOG.error('No Clients are enabled in config.')
|
||||
sys.exit(-1)
|
||||
|
||||
if not client_factory.is_client_configured():
|
||||
if not APRSDClient().is_configured:
|
||||
LOG.error('APRS client is not properly configured in config file.')
|
||||
sys.exit(-1)
|
||||
|
||||
# Creates the client object
|
||||
LOG.info('Creating client connection')
|
||||
aprs_client = client_factory.create()
|
||||
aprs_client = APRSDClient()
|
||||
LOG.info(aprs_client)
|
||||
if not aprs_client.login_success:
|
||||
# We failed to login, will just quit!
|
||||
|
@ -41,7 +41,6 @@ from aprsd.stats import collector
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger('APRSD')
|
||||
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
|
||||
flask_enabled = False
|
||||
|
||||
|
||||
@click.group(cls=cli_helper.AliasedGroup, context_settings=CONTEXT_SETTINGS)
|
||||
@ -73,8 +72,6 @@ def main():
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
global flask_enabled
|
||||
|
||||
click.echo('signal_handler: called')
|
||||
threads.APRSDThreadList().stop_all()
|
||||
if 'subprocess' not in str(frame):
|
||||
@ -96,8 +93,7 @@ def signal_handler(sig, frame):
|
||||
# signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
# sys.exit(0)
|
||||
|
||||
if flask_enabled:
|
||||
signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
|
||||
|
||||
@cli.command()
|
||||
|
@ -19,26 +19,26 @@ from loguru import logger
|
||||
from aprsd.utils import counter
|
||||
|
||||
# For mypy to be happy
|
||||
A = TypeVar("A", bound="DataClassJsonMixin")
|
||||
A = TypeVar('A', bound='DataClassJsonMixin')
|
||||
Json = Union[dict, list, str, int, float, bool, None]
|
||||
|
||||
LOG = logging.getLogger()
|
||||
LOGU = logger
|
||||
|
||||
PACKET_TYPE_BULLETIN = "bulletin"
|
||||
PACKET_TYPE_MESSAGE = "message"
|
||||
PACKET_TYPE_ACK = "ack"
|
||||
PACKET_TYPE_REJECT = "reject"
|
||||
PACKET_TYPE_MICE = "mic-e"
|
||||
PACKET_TYPE_WX = "wx"
|
||||
PACKET_TYPE_WEATHER = "weather"
|
||||
PACKET_TYPE_OBJECT = "object"
|
||||
PACKET_TYPE_UNKNOWN = "unknown"
|
||||
PACKET_TYPE_STATUS = "status"
|
||||
PACKET_TYPE_BEACON = "beacon"
|
||||
PACKET_TYPE_THIRDPARTY = "thirdparty"
|
||||
PACKET_TYPE_TELEMETRY = "telemetry-message"
|
||||
PACKET_TYPE_UNCOMPRESSED = "uncompressed"
|
||||
PACKET_TYPE_BULLETIN = 'bulletin'
|
||||
PACKET_TYPE_MESSAGE = 'message'
|
||||
PACKET_TYPE_ACK = 'ack'
|
||||
PACKET_TYPE_REJECT = 'reject'
|
||||
PACKET_TYPE_MICE = 'mic-e'
|
||||
PACKET_TYPE_WX = 'wx'
|
||||
PACKET_TYPE_WEATHER = 'weather'
|
||||
PACKET_TYPE_OBJECT = 'object'
|
||||
PACKET_TYPE_UNKNOWN = 'unknown'
|
||||
PACKET_TYPE_STATUS = 'status'
|
||||
PACKET_TYPE_BEACON = 'beacon'
|
||||
PACKET_TYPE_THIRDPARTY = 'thirdparty'
|
||||
PACKET_TYPE_TELEMETRY = 'telemetry-message'
|
||||
PACKET_TYPE_UNCOMPRESSED = 'uncompressed'
|
||||
|
||||
NO_DATE = datetime(1900, 10, 24)
|
||||
|
||||
@ -67,14 +67,14 @@ def _init_msgNo(): # noqa: N802
|
||||
|
||||
def _translate_fields(raw: dict) -> dict:
|
||||
# Direct key checks instead of iteration
|
||||
if "from" in raw:
|
||||
raw["from_call"] = raw.pop("from")
|
||||
if "to" in raw:
|
||||
raw["to_call"] = raw.pop("to")
|
||||
if 'from' in raw:
|
||||
raw['from_call'] = raw.pop('from')
|
||||
if 'to' in raw:
|
||||
raw['to_call'] = raw.pop('to')
|
||||
|
||||
# addresse overrides to_call
|
||||
if "addresse" in raw:
|
||||
raw["to_call"] = raw["addresse"]
|
||||
if 'addresse' in raw:
|
||||
raw['to_call'] = raw['addresse']
|
||||
|
||||
return raw
|
||||
|
||||
@ -82,7 +82,7 @@ def _translate_fields(raw: dict) -> dict:
|
||||
@dataclass_json
|
||||
@dataclass(unsafe_hash=True)
|
||||
class Packet:
|
||||
_type: str = field(default="Packet", hash=False)
|
||||
_type: str = field(default='Packet', hash=False)
|
||||
from_call: Optional[str] = field(default=None)
|
||||
to_call: Optional[str] = field(default=None)
|
||||
addresse: Optional[str] = field(default=None)
|
||||
@ -120,7 +120,7 @@ class Packet:
|
||||
@property
|
||||
def key(self) -> str:
|
||||
"""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) -> None:
|
||||
self.timestamp = _init_timestamp()
|
||||
@ -133,7 +133,7 @@ class Packet:
|
||||
the human readable payload.
|
||||
"""
|
||||
self.prepare()
|
||||
msg = self._filter_for_send(self.raw).rstrip("\n")
|
||||
msg = self._filter_for_send(self.raw).rstrip('\n')
|
||||
return msg
|
||||
|
||||
def prepare(self, create_msg_number=False) -> None:
|
||||
@ -152,11 +152,11 @@ class Packet:
|
||||
)
|
||||
|
||||
# The base packet class has no real payload
|
||||
self.payload = f":{self.to_call.ljust(9)}"
|
||||
self.payload = f':{self.to_call.ljust(9)}'
|
||||
|
||||
def _build_raw(self) -> None:
|
||||
"""Build the self.raw which is what is sent over the air."""
|
||||
self.raw = "{}>APZ100:{}".format(
|
||||
self.raw = '{}>APZ100:{}'.format(
|
||||
self.from_call,
|
||||
self.payload,
|
||||
)
|
||||
@ -168,13 +168,13 @@ class Packet:
|
||||
# 67 displays 64 on the ftm400. (+3 {01 suffix)
|
||||
# feature req: break long ones into two msgs
|
||||
if not msg:
|
||||
return ""
|
||||
return ''
|
||||
|
||||
message = msg[:67]
|
||||
# We all miss George Carlin
|
||||
return re.sub(
|
||||
"fuck|shit|cunt|piss|cock|bitch",
|
||||
"****",
|
||||
'fuck|shit|cunt|piss|cock|bitch',
|
||||
'****',
|
||||
message,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
@ -183,100 +183,98 @@ class Packet:
|
||||
"""Show the raw version of the packet"""
|
||||
self.prepare()
|
||||
if not self.raw:
|
||||
raise ValueError("self.raw is unset")
|
||||
raise ValueError('self.raw is unset')
|
||||
return self.raw
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""Build the repr version of the packet."""
|
||||
return (
|
||||
f"{self.__class__.__name__}:"
|
||||
f" From: {self.from_call} "
|
||||
f" To: {self.to_call}"
|
||||
f'{self.__class__.__name__}: From: {self.from_call} To: {self.to_call}'
|
||||
)
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass(unsafe_hash=True)
|
||||
class AckPacket(Packet):
|
||||
_type: str = field(default="AckPacket", hash=False)
|
||||
_type: str = field(default='AckPacket', hash=False)
|
||||
|
||||
def _build_payload(self):
|
||||
self.payload = f":{self.to_call: <9}:ack{self.msgNo}"
|
||||
self.payload = f':{self.to_call: <9}:ack{self.msgNo}'
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass(unsafe_hash=True)
|
||||
class BulletinPacket(Packet):
|
||||
_type: str = "BulletinPacket"
|
||||
_type: str = 'BulletinPacket'
|
||||
# Holds the encapsulated packet
|
||||
bid: Optional[str] = field(default="1")
|
||||
bid: Optional[str] = field(default='1')
|
||||
message_text: Optional[str] = field(default=None)
|
||||
|
||||
@property
|
||||
def key(self) -> str:
|
||||
"""Build a key for finding this packet in a dict."""
|
||||
return f"{self.from_call}:BLN{self.bid}"
|
||||
return f'{self.from_call}:BLN{self.bid}'
|
||||
|
||||
@property
|
||||
def human_info(self) -> str:
|
||||
return f"BLN{self.bid} {self.message_text}"
|
||||
return f'BLN{self.bid} {self.message_text}'
|
||||
|
||||
def _build_payload(self) -> None:
|
||||
self.payload = f":BLN{self.bid:<9}" f":{self.message_text}"
|
||||
self.payload = f':BLN{self.bid:<9}:{self.message_text}'
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass(unsafe_hash=True)
|
||||
class RejectPacket(Packet):
|
||||
_type: str = field(default="RejectPacket", hash=False)
|
||||
_type: str = field(default='RejectPacket', hash=False)
|
||||
response: Optional[str] = field(default=None)
|
||||
|
||||
def __post__init__(self):
|
||||
if self.response:
|
||||
LOG.warning("Response set!")
|
||||
LOG.warning('Response set!')
|
||||
|
||||
def _build_payload(self):
|
||||
self.payload = f":{self.to_call: <9}:rej{self.msgNo}"
|
||||
self.payload = f':{self.to_call: <9}:rej{self.msgNo}'
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass(unsafe_hash=True)
|
||||
class MessagePacket(Packet):
|
||||
_type: str = field(default="MessagePacket", hash=False)
|
||||
_type: str = field(default='MessagePacket', hash=False)
|
||||
message_text: Optional[str] = field(default=None)
|
||||
|
||||
@property
|
||||
def human_info(self) -> str:
|
||||
self.prepare()
|
||||
return self._filter_for_send(self.message_text).rstrip("\n")
|
||||
return self._filter_for_send(self.message_text).rstrip('\n')
|
||||
|
||||
def _build_payload(self):
|
||||
if self.msgNo:
|
||||
self.payload = ":{}:{}{{{}".format(
|
||||
self.payload = ':{}:{}{{{}'.format(
|
||||
self.to_call.ljust(9),
|
||||
self._filter_for_send(self.message_text).rstrip("\n"),
|
||||
self._filter_for_send(self.message_text).rstrip('\n'),
|
||||
str(self.msgNo),
|
||||
)
|
||||
else:
|
||||
self.payload = ":{}:{}".format(
|
||||
self.payload = ':{}:{}'.format(
|
||||
self.to_call.ljust(9),
|
||||
self._filter_for_send(self.message_text).rstrip("\n"),
|
||||
self._filter_for_send(self.message_text).rstrip('\n'),
|
||||
)
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass(unsafe_hash=True)
|
||||
class StatusPacket(Packet):
|
||||
_type: str = field(default="StatusPacket", hash=False)
|
||||
_type: str = field(default='StatusPacket', hash=False)
|
||||
status: Optional[str] = field(default=None)
|
||||
messagecapable: bool = field(default=False)
|
||||
comment: Optional[str] = field(default=None)
|
||||
raw_timestamp: Optional[str] = field(default=None)
|
||||
|
||||
def _build_payload(self):
|
||||
self.payload = ":{}:{}{{{}".format(
|
||||
self.payload = ':{}:{}{{{}'.format(
|
||||
self.to_call.ljust(9),
|
||||
self._filter_for_send(self.status).rstrip("\n"),
|
||||
self._filter_for_send(self.status).rstrip('\n'),
|
||||
str(self.msgNo),
|
||||
)
|
||||
|
||||
@ -289,7 +287,7 @@ class StatusPacket(Packet):
|
||||
@dataclass_json
|
||||
@dataclass(unsafe_hash=True)
|
||||
class GPSPacket(Packet):
|
||||
_type: str = field(default="GPSPacket", hash=False)
|
||||
_type: str = field(default='GPSPacket', hash=False)
|
||||
latitude: float = field(default=0.00)
|
||||
longitude: float = field(default=0.00)
|
||||
altitude: float = field(default=0.00)
|
||||
@ -297,8 +295,8 @@ class GPSPacket(Packet):
|
||||
posambiguity: int = field(default=0)
|
||||
messagecapable: bool = field(default=False)
|
||||
comment: Optional[str] = field(default=None)
|
||||
symbol: str = field(default="l")
|
||||
symbol_table: str = field(default="/")
|
||||
symbol: str = field(default='l')
|
||||
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)
|
||||
@ -318,7 +316,7 @@ class GPSPacket(Packet):
|
||||
def _build_time_zulu(self):
|
||||
"""Build the timestamp in UTC/zulu."""
|
||||
if self.timestamp:
|
||||
return datetime.utcfromtimestamp(self.timestamp).strftime("%d%H%M")
|
||||
return datetime.utcfromtimestamp(self.timestamp).strftime('%d%H%M')
|
||||
|
||||
def _build_payload(self):
|
||||
"""The payload is the non headers portion of the packet."""
|
||||
@ -326,7 +324,7 @@ class GPSPacket(Packet):
|
||||
lat = aprslib_util.latitude_to_ddm(self.latitude)
|
||||
long = aprslib_util.longitude_to_ddm(self.longitude)
|
||||
payload = [
|
||||
"@" if self.timestamp else "!",
|
||||
'@' if self.timestamp else '!',
|
||||
time_zulu,
|
||||
lat,
|
||||
self.symbol_table,
|
||||
@ -337,34 +335,34 @@ class GPSPacket(Packet):
|
||||
if self.comment:
|
||||
payload.append(self._filter_for_send(self.comment))
|
||||
|
||||
self.payload = "".join(payload)
|
||||
self.payload = ''.join(payload)
|
||||
|
||||
def _build_raw(self):
|
||||
self.raw = f"{self.from_call}>{self.to_call},WIDE2-1:" f"{self.payload}"
|
||||
self.raw = f'{self.from_call}>{self.to_call},WIDE2-1:{self.payload}'
|
||||
|
||||
@property
|
||||
def human_info(self) -> str:
|
||||
h_str = []
|
||||
h_str.append(f"Lat:{self.latitude:03.3f}")
|
||||
h_str.append(f"Lon:{self.longitude:03.3f}")
|
||||
h_str.append(f'Lat:{self.latitude:03.3f}')
|
||||
h_str.append(f'Lon:{self.longitude:03.3f}')
|
||||
if self.altitude:
|
||||
h_str.append(f"Altitude {self.altitude:03.0f}")
|
||||
h_str.append(f'Altitude {self.altitude:03.0f}')
|
||||
if self.speed:
|
||||
h_str.append(f"Speed {self.speed:03.0f}MPH")
|
||||
h_str.append(f'Speed {self.speed:03.0f}MPH')
|
||||
if self.course:
|
||||
h_str.append(f"Course {self.course:03.0f}")
|
||||
h_str.append(f'Course {self.course:03.0f}')
|
||||
if self.rng:
|
||||
h_str.append(f"RNG {self.rng:03.0f}")
|
||||
h_str.append(f'RNG {self.rng:03.0f}')
|
||||
if self.phg:
|
||||
h_str.append(f"PHG {self.phg}")
|
||||
h_str.append(f'PHG {self.phg}')
|
||||
|
||||
return " ".join(h_str)
|
||||
return ' '.join(h_str)
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass(unsafe_hash=True)
|
||||
class BeaconPacket(GPSPacket):
|
||||
_type: str = field(default="BeaconPacket", hash=False)
|
||||
_type: str = field(default='BeaconPacket', hash=False)
|
||||
|
||||
def _build_payload(self):
|
||||
"""The payload is the non headers portion of the packet."""
|
||||
@ -372,38 +370,38 @@ class BeaconPacket(GPSPacket):
|
||||
lat = aprslib_util.latitude_to_ddm(self.latitude)
|
||||
lon = aprslib_util.longitude_to_ddm(self.longitude)
|
||||
|
||||
self.payload = f"@{time_zulu}z{lat}{self.symbol_table}" f"{lon}"
|
||||
self.payload = f'@{time_zulu}z{lat}{self.symbol_table}{lon}'
|
||||
|
||||
if self.comment:
|
||||
comment = self._filter_for_send(self.comment)
|
||||
self.payload = f"{self.payload}{self.symbol}{comment}"
|
||||
self.payload = f'{self.payload}{self.symbol}{comment}'
|
||||
else:
|
||||
self.payload = f"{self.payload}{self.symbol}APRSD Beacon"
|
||||
self.payload = f'{self.payload}{self.symbol}APRSD Beacon'
|
||||
|
||||
def _build_raw(self):
|
||||
self.raw = f"{self.from_call}>APZ100:" f"{self.payload}"
|
||||
self.raw = f'{self.from_call}>APZ100:{self.payload}'
|
||||
|
||||
@property
|
||||
def key(self) -> str:
|
||||
"""Build a key for finding this packet in a dict."""
|
||||
if self.raw_timestamp:
|
||||
return f"{self.from_call}:{self.raw_timestamp}"
|
||||
return f'{self.from_call}:{self.raw_timestamp}'
|
||||
else:
|
||||
return f"{self.from_call}:{self.human_info.replace(' ', '')}"
|
||||
return f'{self.from_call}:{self.human_info.replace(" ", "")}'
|
||||
|
||||
@property
|
||||
def human_info(self) -> str:
|
||||
h_str = []
|
||||
h_str.append(f"Lat:{self.latitude:03.3f}")
|
||||
h_str.append(f"Lon:{self.longitude:03.3f}")
|
||||
h_str.append(f"{self.comment}")
|
||||
return " ".join(h_str)
|
||||
h_str.append(f'Lat:{self.latitude:03.3f}')
|
||||
h_str.append(f'Lon:{self.longitude:03.3f}')
|
||||
h_str.append(f'{self.comment}')
|
||||
return ' '.join(h_str)
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass(unsafe_hash=True)
|
||||
class MicEPacket(GPSPacket):
|
||||
_type: str = field(default="MicEPacket", hash=False)
|
||||
_type: str = field(default='MicEPacket', hash=False)
|
||||
messagecapable: bool = False
|
||||
mbits: Optional[str] = None
|
||||
mtype: Optional[str] = None
|
||||
@ -416,18 +414,18 @@ class MicEPacket(GPSPacket):
|
||||
@property
|
||||
def key(self) -> str:
|
||||
"""Build a key for finding this packet in a dict."""
|
||||
return f"{self.from_call}:{self.human_info.replace(' ', '')}"
|
||||
return f'{self.from_call}:{self.human_info.replace(" ", "")}'
|
||||
|
||||
@property
|
||||
def human_info(self) -> str:
|
||||
h_info = super().human_info
|
||||
return f"{h_info} {self.mbits} mbits"
|
||||
return f'{h_info} {self.mbits} mbits'
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass(unsafe_hash=True)
|
||||
class TelemetryPacket(GPSPacket):
|
||||
_type: str = field(default="TelemetryPacket", hash=False)
|
||||
_type: str = field(default='TelemetryPacket', hash=False)
|
||||
messagecapable: bool = False
|
||||
mbits: Optional[str] = None
|
||||
mtype: Optional[str] = None
|
||||
@ -443,23 +441,23 @@ class TelemetryPacket(GPSPacket):
|
||||
def key(self) -> str:
|
||||
"""Build a key for finding this packet in a dict."""
|
||||
if self.raw_timestamp:
|
||||
return f"{self.from_call}:{self.raw_timestamp}"
|
||||
return f'{self.from_call}:{self.raw_timestamp}'
|
||||
else:
|
||||
return f"{self.from_call}:{self.human_info.replace(' ', '')}"
|
||||
return f'{self.from_call}:{self.human_info.replace(" ", "")}'
|
||||
|
||||
@property
|
||||
def human_info(self) -> str:
|
||||
h_info = super().human_info
|
||||
return f"{h_info} {self.telemetry}"
|
||||
return f'{h_info} {self.telemetry}'
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass(unsafe_hash=True)
|
||||
class ObjectPacket(GPSPacket):
|
||||
_type: str = field(default="ObjectPacket", hash=False)
|
||||
_type: str = field(default='ObjectPacket', hash=False)
|
||||
alive: bool = True
|
||||
raw_timestamp: Optional[str] = None
|
||||
symbol: str = field(default="r")
|
||||
symbol: str = field(default='r')
|
||||
# in MPH
|
||||
speed: float = 0.00
|
||||
# 0 to 360
|
||||
@ -470,11 +468,11 @@ class ObjectPacket(GPSPacket):
|
||||
lat = aprslib_util.latitude_to_ddm(self.latitude)
|
||||
long = aprslib_util.longitude_to_ddm(self.longitude)
|
||||
|
||||
self.payload = f"*{time_zulu}z{lat}{self.symbol_table}" f"{long}{self.symbol}"
|
||||
self.payload = f'*{time_zulu}z{lat}{self.symbol_table}{long}{self.symbol}'
|
||||
|
||||
if self.comment:
|
||||
comment = self._filter_for_send(self.comment)
|
||||
self.payload = f"{self.payload}{comment}"
|
||||
self.payload = f'{self.payload}{comment}'
|
||||
|
||||
def _build_raw(self):
|
||||
"""
|
||||
@ -487,18 +485,18 @@ class ObjectPacket(GPSPacket):
|
||||
The frequency, uplink_tone, offset is part of the comment
|
||||
"""
|
||||
|
||||
self.raw = f"{self.from_call}>APZ100:;{self.to_call:9s}" f"{self.payload}"
|
||||
self.raw = f'{self.from_call}>APZ100:;{self.to_call:9s}{self.payload}'
|
||||
|
||||
@property
|
||||
def human_info(self) -> str:
|
||||
h_info = super().human_info
|
||||
return f"{h_info} {self.comment}"
|
||||
return f'{h_info} {self.comment}'
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class WeatherPacket(GPSPacket, DataClassJsonMixin):
|
||||
_type: str = field(default="WeatherPacket", hash=False)
|
||||
symbol: str = "_"
|
||||
_type: str = field(default='WeatherPacket', hash=False)
|
||||
symbol: str = '_'
|
||||
wind_speed: float = 0.00
|
||||
wind_direction: int = 0
|
||||
wind_gust: float = 0.00
|
||||
@ -516,8 +514,8 @@ class WeatherPacket(GPSPacket, DataClassJsonMixin):
|
||||
speed: Optional[float] = field(default=None)
|
||||
|
||||
def _translate(self, raw: dict) -> dict:
|
||||
for key in raw["weather"]:
|
||||
raw[key] = raw["weather"][key]
|
||||
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
|
||||
@ -525,36 +523,36 @@ class WeatherPacket(GPSPacket, DataClassJsonMixin):
|
||||
# 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:
|
||||
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")
|
||||
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"]
|
||||
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) / 0.254) * 0.01, 3)
|
||||
raw["weather"]["rain_1h"] = raw["rain_1h"]
|
||||
raw["rain_24h"] = round((raw.get("rain_24h", 0) / 0.254) * 0.01, 3)
|
||||
raw["weather"]["rain_24h"] = raw["rain_24h"]
|
||||
raw["rain_since_midnight"] = round(
|
||||
(raw.get("rain_since_midnight", 0) / 0.254) * 0.01, 3
|
||||
raw['rain_1h'] = round((raw.get('rain_1h', 0) / 0.254) * 0.01, 3)
|
||||
raw['weather']['rain_1h'] = raw['rain_1h']
|
||||
raw['rain_24h'] = round((raw.get('rain_24h', 0) / 0.254) * 0.01, 3)
|
||||
raw['weather']['rain_24h'] = raw['rain_24h']
|
||||
raw['rain_since_midnight'] = round(
|
||||
(raw.get('rain_since_midnight', 0) / 0.254) * 0.01, 3
|
||||
)
|
||||
raw["weather"]["rain_since_midnight"] = raw["rain_since_midnight"]
|
||||
raw['weather']['rain_since_midnight'] = raw['rain_since_midnight']
|
||||
|
||||
if "wind_direction" not in raw:
|
||||
wind_direction = raw.get("course")
|
||||
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"]
|
||||
raw['wind_direction'] = wind_direction
|
||||
raw['weather']['wind_direction'] = raw['wind_direction']
|
||||
if 'course' in raw:
|
||||
del raw['course']
|
||||
|
||||
del raw["weather"]
|
||||
del raw['weather']
|
||||
return raw
|
||||
|
||||
@classmethod
|
||||
@ -567,20 +565,20 @@ class WeatherPacket(GPSPacket, DataClassJsonMixin):
|
||||
def key(self) -> str:
|
||||
"""Build a key for finding this packet in a dict."""
|
||||
if self.raw_timestamp:
|
||||
return f"{self.from_call}:{self.raw_timestamp}"
|
||||
return f'{self.from_call}:{self.raw_timestamp}'
|
||||
elif self.wx_raw_timestamp:
|
||||
return f"{self.from_call}:{self.wx_raw_timestamp}"
|
||||
return f'{self.from_call}:{self.wx_raw_timestamp}'
|
||||
|
||||
@property
|
||||
def human_info(self) -> str:
|
||||
h_str = []
|
||||
h_str.append(f"Temp {self.temperature:03.0f}F")
|
||||
h_str.append(f"Humidity {self.humidity}%")
|
||||
h_str.append(f"Wind {self.wind_speed:03.0f}MPH@{self.wind_direction}")
|
||||
h_str.append(f"Pressure {self.pressure}mb")
|
||||
h_str.append(f"Rain {self.rain_24h}in/24hr")
|
||||
h_str.append(f'Temp {self.temperature:03.0f}F')
|
||||
h_str.append(f'Humidity {self.humidity}%')
|
||||
h_str.append(f'Wind {self.wind_speed:03.0f}MPH@{self.wind_direction}')
|
||||
h_str.append(f'Pressure {self.pressure}mb')
|
||||
h_str.append(f'Rain {self.rain_24h}in/24hr')
|
||||
|
||||
return " ".join(h_str)
|
||||
return ' '.join(h_str)
|
||||
|
||||
def _build_payload(self):
|
||||
"""Build an uncompressed weather packet
|
||||
@ -610,49 +608,49 @@ class WeatherPacket(GPSPacket, DataClassJsonMixin):
|
||||
time_zulu = self._build_time_zulu()
|
||||
|
||||
contents = [
|
||||
f"@{time_zulu}z{self.latitude}{self.symbol_table}",
|
||||
f"{self.longitude}{self.symbol}",
|
||||
f"{self.wind_direction:03d}",
|
||||
f'@{time_zulu}z{self.latitude}{self.symbol_table}',
|
||||
f'{self.longitude}{self.symbol}',
|
||||
f'{self.wind_direction:03d}',
|
||||
# Speed = sustained 1 minute wind speed in mph
|
||||
f"{self.symbol_table}",
|
||||
f"{self.wind_speed:03.0f}",
|
||||
f'{self.symbol_table}',
|
||||
f'{self.wind_speed:03.0f}',
|
||||
# wind gust (peak wind speed in mph in the last 5 minutes)
|
||||
f"g{self.wind_gust:03.0f}",
|
||||
f'g{self.wind_gust:03.0f}',
|
||||
# Temperature in degrees F
|
||||
f"t{self.temperature:03.0f}",
|
||||
f't{self.temperature:03.0f}',
|
||||
# Rainfall (in hundredths of an inch) in the last hour
|
||||
f"r{self.rain_1h * 100:03.0f}",
|
||||
f'r{self.rain_1h * 100:03.0f}',
|
||||
# Rainfall (in hundredths of an inch) in last 24 hours
|
||||
f"p{self.rain_24h * 100:03.0f}",
|
||||
f'p{self.rain_24h * 100:03.0f}',
|
||||
# Rainfall (in hundredths of an inch) since midnigt
|
||||
f"P{self.rain_since_midnight * 100:03.0f}",
|
||||
f'P{self.rain_since_midnight * 100:03.0f}',
|
||||
# Humidity
|
||||
f"h{self.humidity:02d}",
|
||||
f'h{self.humidity:02d}',
|
||||
# Barometric pressure (in tenths of millibars/tenths of hPascal)
|
||||
f"b{self.pressure:05.0f}",
|
||||
f'b{self.pressure:05.0f}',
|
||||
]
|
||||
if self.comment:
|
||||
comment = self.filter_for_send(self.comment)
|
||||
comment = self._filter_for_send(self.comment)
|
||||
contents.append(comment)
|
||||
self.payload = "".join(contents)
|
||||
self.payload = ''.join(contents)
|
||||
|
||||
def _build_raw(self):
|
||||
self.raw = f"{self.from_call}>{self.to_call},WIDE1-1,WIDE2-1:" f"{self.payload}"
|
||||
self.raw = f'{self.from_call}>{self.to_call},WIDE1-1,WIDE2-1:{self.payload}'
|
||||
|
||||
|
||||
@dataclass(unsafe_hash=True)
|
||||
class ThirdPartyPacket(Packet, DataClassJsonMixin):
|
||||
_type: str = "ThirdPartyPacket"
|
||||
_type: str = 'ThirdPartyPacket'
|
||||
# Holds the encapsulated packet
|
||||
subpacket: Optional[type[Packet]] = field(default=None, compare=True, hash=False)
|
||||
|
||||
def __repr__(self):
|
||||
"""Build the repr version of the packet."""
|
||||
repr_str = (
|
||||
f"{self.__class__.__name__}:"
|
||||
f" From: {self.from_call} "
|
||||
f" To: {self.to_call} "
|
||||
f" Subpacket: {repr(self.subpacket)}"
|
||||
f'{self.__class__.__name__}:'
|
||||
f' From: {self.from_call} '
|
||||
f' To: {self.to_call} '
|
||||
f' Subpacket: {repr(self.subpacket)}'
|
||||
)
|
||||
|
||||
return repr_str
|
||||
@ -666,12 +664,12 @@ class ThirdPartyPacket(Packet, DataClassJsonMixin):
|
||||
@property
|
||||
def key(self) -> str:
|
||||
"""Build a key for finding this packet in a dict."""
|
||||
return f"{self.from_call}:{self.subpacket.key}"
|
||||
return f'{self.from_call}:{self.subpacket.key}'
|
||||
|
||||
@property
|
||||
def human_info(self) -> str:
|
||||
sub_info = self.subpacket.human_info
|
||||
return f"{self.from_call}->{self.to_call} {sub_info}"
|
||||
return f'{self.from_call}->{self.to_call} {sub_info}'
|
||||
|
||||
|
||||
@dataclass_json(undefined=Undefined.INCLUDE)
|
||||
@ -683,7 +681,7 @@ class UnknownPacket:
|
||||
"""
|
||||
|
||||
unknown_fields: CatchAll
|
||||
_type: str = "UnknownPacket"
|
||||
_type: str = 'UnknownPacket'
|
||||
from_call: Optional[str] = field(default=None)
|
||||
to_call: Optional[str] = field(default=None)
|
||||
msgNo: str = field(default_factory=_init_msgNo) # noqa: N815
|
||||
@ -701,7 +699,7 @@ class UnknownPacket:
|
||||
@property
|
||||
def key(self) -> str:
|
||||
"""Build a key for finding this packet in a dict."""
|
||||
return f"{self.from_call}:{self.packet_type}:{self.to_call}"
|
||||
return f'{self.from_call}:{self.packet_type}:{self.to_call}'
|
||||
|
||||
@property
|
||||
def human_info(self) -> str:
|
||||
@ -728,20 +726,20 @@ TYPE_LOOKUP: dict[str, type[Packet]] = {
|
||||
def get_packet_type(packet: dict) -> str:
|
||||
"""Decode the packet type from the packet."""
|
||||
|
||||
pkt_format = packet.get("format")
|
||||
msg_response = packet.get("response")
|
||||
pkt_format = packet.get('format')
|
||||
msg_response = packet.get('response')
|
||||
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
|
||||
elif pkt_format == "message" and msg_response == "rej":
|
||||
elif pkt_format == 'message' and msg_response == 'rej':
|
||||
packet_type = PACKET_TYPE_REJECT
|
||||
elif pkt_format == "message":
|
||||
elif pkt_format == 'message':
|
||||
packet_type = PACKET_TYPE_MESSAGE
|
||||
elif pkt_format == "mic-e":
|
||||
elif pkt_format == 'mic-e':
|
||||
packet_type = PACKET_TYPE_MICE
|
||||
elif pkt_format == "object":
|
||||
elif pkt_format == 'object':
|
||||
packet_type = PACKET_TYPE_OBJECT
|
||||
elif pkt_format == "status":
|
||||
elif pkt_format == 'status':
|
||||
packet_type = PACKET_TYPE_STATUS
|
||||
elif pkt_format == PACKET_TYPE_BULLETIN:
|
||||
packet_type = PACKET_TYPE_BULLETIN
|
||||
@ -752,13 +750,13 @@ def get_packet_type(packet: dict) -> str:
|
||||
elif pkt_format == PACKET_TYPE_WX:
|
||||
packet_type = PACKET_TYPE_WEATHER
|
||||
elif pkt_format == PACKET_TYPE_UNCOMPRESSED:
|
||||
if packet.get("symbol") == "_":
|
||||
if packet.get('symbol') == '_':
|
||||
packet_type = PACKET_TYPE_WEATHER
|
||||
elif pkt_format == PACKET_TYPE_THIRDPARTY:
|
||||
packet_type = PACKET_TYPE_THIRDPARTY
|
||||
|
||||
if packet_type == PACKET_TYPE_UNKNOWN:
|
||||
if "latitude" in packet:
|
||||
if 'latitude' in packet:
|
||||
packet_type = PACKET_TYPE_BEACON
|
||||
else:
|
||||
packet_type = PACKET_TYPE_UNKNOWN
|
||||
@ -780,32 +778,32 @@ def is_mice_packet(packet: dict[Any, Any]) -> bool:
|
||||
def factory(raw_packet: dict[Any, Any]) -> type[Packet]:
|
||||
"""Factory method to create a packet from a raw packet string."""
|
||||
raw = raw_packet
|
||||
if "_type" in raw:
|
||||
cls = globals()[raw["_type"]]
|
||||
if '_type' in raw:
|
||||
cls = globals()[raw['_type']]
|
||||
return cls.from_dict(raw)
|
||||
|
||||
raw["raw_dict"] = raw.copy()
|
||||
raw['raw_dict'] = raw.copy()
|
||||
raw = _translate_fields(raw)
|
||||
|
||||
packet_type = get_packet_type(raw)
|
||||
|
||||
raw["packet_type"] = packet_type
|
||||
raw['packet_type'] = packet_type
|
||||
packet_class = 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
|
||||
packet_class = WeatherPacket
|
||||
elif packet_type == PACKET_TYPE_OBJECT and "weather" in raw:
|
||||
elif packet_type == PACKET_TYPE_OBJECT and 'weather' in raw:
|
||||
packet_class = WeatherPacket
|
||||
elif packet_type == PACKET_TYPE_UNKNOWN:
|
||||
# Try and figure it out here
|
||||
if "latitude" in raw:
|
||||
if 'latitude' in raw:
|
||||
packet_class = GPSPacket
|
||||
else:
|
||||
# LOG.warning(raw)
|
||||
packet_class = UnknownPacket
|
||||
|
||||
raw.get("addresse", raw.get("to_call"))
|
||||
raw.get('addresse', raw.get('to_call'))
|
||||
|
||||
# TODO: Find a global way to enable/disable this
|
||||
# LOGU.opt(colors=True).info(
|
||||
|
@ -12,7 +12,8 @@ import pluggy
|
||||
from oslo_config import cfg
|
||||
|
||||
import aprsd
|
||||
from aprsd import client, packets, threads
|
||||
from aprsd import packets, threads
|
||||
from aprsd.client.client import APRSDClient
|
||||
from aprsd.packets import watch_list
|
||||
|
||||
# setup the global logger
|
||||
@ -146,7 +147,7 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
||||
watch_list = CONF.watch_list.callsigns
|
||||
# make sure the timeout is set or this doesn't work
|
||||
if watch_list:
|
||||
aprs_client = client.client_factory.create().client
|
||||
aprs_client = APRSDClient()
|
||||
filter_str = 'b/{}'.format('/'.join(watch_list))
|
||||
aprs_client.set_filter(filter_str)
|
||||
else:
|
||||
|
@ -3,7 +3,7 @@ from typing import Callable, Protocol, runtime_checkable
|
||||
|
||||
from aprsd.utils import singleton
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
LOG = logging.getLogger('APRSD')
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
@ -31,15 +31,16 @@ class Collector:
|
||||
serializable=serializable
|
||||
).copy()
|
||||
except Exception as e:
|
||||
LOG.error(f"Error in producer {name} (stats): {e}")
|
||||
LOG.error(f'Error in producer {name} (stats): {e}')
|
||||
raise e
|
||||
return stats
|
||||
|
||||
def register_producer(self, producer_name: Callable):
|
||||
if not isinstance(producer_name, StatsProducer):
|
||||
raise TypeError(f"Producer {producer_name} is not a StatsProducer")
|
||||
raise TypeError(f'Producer {producer_name} is not a StatsProducer')
|
||||
self.producers.append(producer_name)
|
||||
|
||||
def unregister_producer(self, producer_name: Callable):
|
||||
if not isinstance(producer_name, StatsProducer):
|
||||
raise TypeError(f"Producer {producer_name} is not a StatsProducer")
|
||||
raise TypeError(f'Producer {producer_name} is not a StatsProducer')
|
||||
self.producers.remove(producer_name)
|
||||
|
@ -7,7 +7,7 @@ import aprslib
|
||||
from oslo_config import cfg
|
||||
|
||||
from aprsd import packets, plugin
|
||||
from aprsd.client import client_factory
|
||||
from aprsd.client.client import APRSDClient
|
||||
from aprsd.packets import collector, filter
|
||||
from aprsd.packets import log as packet_log
|
||||
from aprsd.threads import APRSDThread, tx
|
||||
@ -39,16 +39,16 @@ class APRSDRXThread(APRSDThread):
|
||||
def stop(self):
|
||||
self.thread_stop = True
|
||||
if self._client:
|
||||
self._client.stop()
|
||||
self._client.close()
|
||||
|
||||
def loop(self):
|
||||
if not self._client:
|
||||
self._client = client_factory.create()
|
||||
self._client = APRSDClient()
|
||||
time.sleep(1)
|
||||
return True
|
||||
|
||||
if not self._client.is_connected:
|
||||
self._client = client_factory.create()
|
||||
if not self._client.is_alive:
|
||||
self._client = APRSDClient()
|
||||
time.sleep(1)
|
||||
return True
|
||||
|
||||
@ -66,7 +66,6 @@ class APRSDRXThread(APRSDThread):
|
||||
self._client.consumer(
|
||||
self.process_packet,
|
||||
raw=False,
|
||||
blocking=False,
|
||||
)
|
||||
except (
|
||||
aprslib.exceptions.ConnectionDrop,
|
||||
@ -78,8 +77,8 @@ class APRSDRXThread(APRSDThread):
|
||||
# is called
|
||||
self._client.reset()
|
||||
time.sleep(5)
|
||||
except Exception:
|
||||
# LOG.exception(ex)
|
||||
except Exception as ex:
|
||||
LOG.exception(ex)
|
||||
LOG.error('Resetting connection and trying again.')
|
||||
self._client.reset()
|
||||
time.sleep(5)
|
||||
|
@ -11,12 +11,12 @@ from rush.stores import dictionary
|
||||
|
||||
from aprsd import conf # noqa
|
||||
from aprsd import threads as aprsd_threads
|
||||
from aprsd.client import client_factory
|
||||
from aprsd.client.client import APRSDClient
|
||||
from aprsd.packets import collector, core, tracker
|
||||
from aprsd.packets import log as packet_log
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger("APRSD")
|
||||
LOG = logging.getLogger('APRSD')
|
||||
|
||||
msg_t = throttle.Throttle(
|
||||
limiter=periodic.PeriodicLimiter(
|
||||
@ -54,7 +54,7 @@ def send(packet: core.Packet, direct=False, aprs_client=None):
|
||||
if CONF.enable_sending_ack_packets:
|
||||
_send_ack(packet, direct=direct, aprs_client=aprs_client)
|
||||
else:
|
||||
LOG.info("Sending ack packets is disabled. Not sending AckPacket.")
|
||||
LOG.info('Sending ack packets is disabled. Not sending AckPacket.')
|
||||
else:
|
||||
_send_packet(packet, direct=direct, aprs_client=aprs_client)
|
||||
|
||||
@ -81,14 +81,14 @@ def _send_direct(packet, aprs_client=None):
|
||||
if aprs_client:
|
||||
cl = aprs_client
|
||||
else:
|
||||
cl = client_factory.create()
|
||||
cl = APRSDClient()
|
||||
|
||||
packet.update_timestamp()
|
||||
packet_log.log(packet, tx=True)
|
||||
try:
|
||||
cl.send(packet)
|
||||
except Exception as e:
|
||||
LOG.error(f"Failed to send packet: {packet}")
|
||||
LOG.error(f'Failed to send packet: {packet}')
|
||||
LOG.error(e)
|
||||
return False
|
||||
else:
|
||||
@ -100,7 +100,7 @@ class SendPacketThread(aprsd_threads.APRSDThread):
|
||||
|
||||
def __init__(self, packet):
|
||||
self.packet = packet
|
||||
super().__init__(f"TX-{packet.to_call}-{self.packet.msgNo}")
|
||||
super().__init__(f'TX-{packet.to_call}-{self.packet.msgNo}')
|
||||
|
||||
def loop(self):
|
||||
"""Loop until a message is acked or it gets delayed.
|
||||
@ -119,9 +119,9 @@ class SendPacketThread(aprsd_threads.APRSDThread):
|
||||
# The message has been removed from the tracking queue
|
||||
# So it got acked and we are done.
|
||||
LOG.info(
|
||||
f"{self.packet.__class__.__name__}"
|
||||
f"({self.packet.msgNo}) "
|
||||
"Message Send Complete via Ack.",
|
||||
f'{self.packet.__class__.__name__}'
|
||||
f'({self.packet.msgNo}) '
|
||||
'Message Send Complete via Ack.',
|
||||
)
|
||||
return False
|
||||
else:
|
||||
@ -130,10 +130,10 @@ class SendPacketThread(aprsd_threads.APRSDThread):
|
||||
# we reached the send limit, don't send again
|
||||
# TODO(hemna) - Need to put this in a delayed queue?
|
||||
LOG.info(
|
||||
f"{packet.__class__.__name__} "
|
||||
f"({packet.msgNo}) "
|
||||
"Message Send Complete. Max attempts reached"
|
||||
f" {packet.retry_count}",
|
||||
f'{packet.__class__.__name__} '
|
||||
f'({packet.msgNo}) '
|
||||
'Message Send Complete. Max attempts reached'
|
||||
f' {packet.retry_count}',
|
||||
)
|
||||
pkt_tracker.remove(packet.msgNo)
|
||||
return False
|
||||
@ -158,7 +158,7 @@ class SendPacketThread(aprsd_threads.APRSDThread):
|
||||
try:
|
||||
sent = _send_direct(packet)
|
||||
except Exception:
|
||||
LOG.error(f"Failed to send packet: {packet}")
|
||||
LOG.error(f'Failed to send packet: {packet}')
|
||||
else:
|
||||
# If an exception happens while sending
|
||||
# we don't want this attempt to count
|
||||
@ -178,7 +178,7 @@ class SendAckThread(aprsd_threads.APRSDThread):
|
||||
|
||||
def __init__(self, packet):
|
||||
self.packet = packet
|
||||
super().__init__(f"TXAck-{packet.to_call}-{self.packet.msgNo}")
|
||||
super().__init__(f'TXAck-{packet.to_call}-{self.packet.msgNo}')
|
||||
self.max_retries = CONF.default_ack_send_count
|
||||
|
||||
def loop(self):
|
||||
@ -188,10 +188,10 @@ class SendAckThread(aprsd_threads.APRSDThread):
|
||||
# we reached the send limit, don't send again
|
||||
# TODO(hemna) - Need to put this in a delayed queue?
|
||||
LOG.debug(
|
||||
f"{self.packet.__class__.__name__}"
|
||||
f"({self.packet.msgNo}) "
|
||||
"Send Complete. Max attempts reached"
|
||||
f" {self.max_retries}",
|
||||
f'{self.packet.__class__.__name__}'
|
||||
f'({self.packet.msgNo}) '
|
||||
'Send Complete. Max attempts reached'
|
||||
f' {self.max_retries}',
|
||||
)
|
||||
return False
|
||||
|
||||
@ -207,7 +207,7 @@ class SendAckThread(aprsd_threads.APRSDThread):
|
||||
# It's time to try to send it again
|
||||
send_now = True
|
||||
elif self.loop_count % 10 == 0:
|
||||
LOG.debug(f"Still wating. {delta}")
|
||||
LOG.debug(f'Still wating. {delta}')
|
||||
else:
|
||||
send_now = True
|
||||
|
||||
@ -216,7 +216,7 @@ class SendAckThread(aprsd_threads.APRSDThread):
|
||||
try:
|
||||
sent = _send_direct(self.packet)
|
||||
except Exception:
|
||||
LOG.error(f"Failed to send packet: {self.packet}")
|
||||
LOG.error(f'Failed to send packet: {self.packet}')
|
||||
else:
|
||||
# If an exception happens while sending
|
||||
# we don't want this attempt to count
|
||||
@ -240,18 +240,18 @@ class BeaconSendThread(aprsd_threads.APRSDThread):
|
||||
_loop_cnt: int = 1
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("BeaconSendThread")
|
||||
super().__init__('BeaconSendThread')
|
||||
self._loop_cnt = 1
|
||||
# Make sure Latitude and Longitude are set.
|
||||
if not CONF.latitude or not CONF.longitude:
|
||||
LOG.error(
|
||||
"Latitude and Longitude are not set in the config file."
|
||||
"Beacon will not be sent and thread is STOPPED.",
|
||||
'Latitude and Longitude are not set in the config file.'
|
||||
'Beacon will not be sent and thread is STOPPED.',
|
||||
)
|
||||
self.stop()
|
||||
LOG.info(
|
||||
"Beacon thread is running and will send "
|
||||
f"beacons every {CONF.beacon_interval} seconds.",
|
||||
'Beacon thread is running and will send '
|
||||
f'beacons every {CONF.beacon_interval} seconds.',
|
||||
)
|
||||
|
||||
def loop(self):
|
||||
@ -259,10 +259,10 @@ class BeaconSendThread(aprsd_threads.APRSDThread):
|
||||
if self._loop_cnt % CONF.beacon_interval == 0:
|
||||
pkt = core.BeaconPacket(
|
||||
from_call=CONF.callsign,
|
||||
to_call="APRS",
|
||||
to_call='APRS',
|
||||
latitude=float(CONF.latitude),
|
||||
longitude=float(CONF.longitude),
|
||||
comment="APRSD GPS Beacon",
|
||||
comment='APRSD GPS Beacon',
|
||||
symbol=CONF.beacon_symbol,
|
||||
)
|
||||
try:
|
||||
@ -270,8 +270,8 @@ class BeaconSendThread(aprsd_threads.APRSDThread):
|
||||
pkt.retry_count = 1
|
||||
send(pkt, direct=True)
|
||||
except Exception as e:
|
||||
LOG.error(f"Failed to send beacon: {e}")
|
||||
client_factory.create().reset()
|
||||
LOG.error(f'Failed to send beacon: {e}')
|
||||
APRSDClient().reset()
|
||||
time.sleep(5)
|
||||
|
||||
self._loop_cnt += 1
|
||||
|
@ -3,7 +3,7 @@ from typing import Callable, Protocol, runtime_checkable
|
||||
|
||||
from aprsd.utils import singleton
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
LOG = logging.getLogger('APRSD')
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
@ -33,7 +33,8 @@ class KeepAliveCollector:
|
||||
try:
|
||||
cls.keepalive_check()
|
||||
except Exception as e:
|
||||
LOG.error(f"Error in producer {name} (check): {e}")
|
||||
LOG.error(f'Error in producer {name} (check): {e}')
|
||||
raise e
|
||||
|
||||
def log(self) -> None:
|
||||
"""Log any relevant information during a KeepAlive check"""
|
||||
@ -42,14 +43,15 @@ class KeepAliveCollector:
|
||||
try:
|
||||
cls.keepalive_log()
|
||||
except Exception as e:
|
||||
LOG.error(f"Error in producer {name} (check): {e}")
|
||||
LOG.error(f'Error in producer {name} (check): {e}')
|
||||
raise e
|
||||
|
||||
def register(self, producer_name: Callable):
|
||||
if not isinstance(producer_name, KeepAliveProducer):
|
||||
raise TypeError(f"Producer {producer_name} is not a KeepAliveProducer")
|
||||
raise TypeError(f'Producer {producer_name} is not a KeepAliveProducer')
|
||||
self.producers.append(producer_name)
|
||||
|
||||
def unregister(self, producer_name: Callable):
|
||||
if not isinstance(producer_name, KeepAliveProducer):
|
||||
raise TypeError(f"Producer {producer_name} is not a KeepAliveProducer")
|
||||
raise TypeError(f'Producer {producer_name} is not a KeepAliveProducer')
|
||||
self.producers.remove(producer_name)
|
||||
|
0
tests/client/__init__.py
Normal file
0
tests/client/__init__.py
Normal file
0
tests/client/drivers/__init__.py
Normal file
0
tests/client/drivers/__init__.py
Normal file
440
tests/client/drivers/test_aprsis_driver.py
Normal file
440
tests/client/drivers/test_aprsis_driver.py
Normal file
@ -0,0 +1,440 @@
|
||||
import datetime
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from aprslib.exceptions import LoginError
|
||||
|
||||
from aprsd import exception
|
||||
from aprsd.client.drivers.aprsis import APRSISDriver
|
||||
from aprsd.client.drivers.registry import ClientDriver
|
||||
from aprsd.packets import core
|
||||
|
||||
|
||||
class TestAPRSISDriver(unittest.TestCase):
|
||||
"""Unit tests for the APRSISDriver class."""
|
||||
|
||||
def setUp(self):
|
||||
# Mock configuration
|
||||
self.conf_patcher = mock.patch('aprsd.client.drivers.aprsis.CONF')
|
||||
self.mock_conf = self.conf_patcher.start()
|
||||
|
||||
# Configure APRS-IS settings
|
||||
self.mock_conf.aprs_network.enabled = True
|
||||
self.mock_conf.aprs_network.login = 'TEST'
|
||||
self.mock_conf.aprs_network.password = '12345'
|
||||
self.mock_conf.aprs_network.host = 'rotate.aprs.net'
|
||||
self.mock_conf.aprs_network.port = 14580
|
||||
|
||||
# Mock APRS Lib Client
|
||||
self.aprslib_patcher = mock.patch('aprsd.client.drivers.aprsis.APRSLibClient')
|
||||
self.mock_aprslib = self.aprslib_patcher.start()
|
||||
self.mock_client = mock.MagicMock()
|
||||
self.mock_aprslib.return_value = self.mock_client
|
||||
|
||||
# Create an instance of the driver
|
||||
self.driver = APRSISDriver()
|
||||
|
||||
def tearDown(self):
|
||||
self.conf_patcher.stop()
|
||||
self.aprslib_patcher.stop()
|
||||
|
||||
def test_implements_client_driver_protocol(self):
|
||||
"""Test that APRSISDriver implements the ClientDriver Protocol."""
|
||||
# Verify the instance is recognized as implementing the Protocol
|
||||
self.assertIsInstance(self.driver, ClientDriver)
|
||||
|
||||
# Verify all required methods are present with correct signatures
|
||||
required_methods = [
|
||||
'is_enabled',
|
||||
'is_configured',
|
||||
'is_alive',
|
||||
'close',
|
||||
'send',
|
||||
'setup_connection',
|
||||
'set_filter',
|
||||
'login_success',
|
||||
'login_failure',
|
||||
'consumer',
|
||||
'decode_packet',
|
||||
'stats',
|
||||
]
|
||||
|
||||
for method_name in required_methods:
|
||||
self.assertTrue(
|
||||
hasattr(self.driver, method_name),
|
||||
f'Missing required method: {method_name}',
|
||||
)
|
||||
|
||||
def test_init(self):
|
||||
"""Test initialization sets default values."""
|
||||
self.assertIsInstance(self.driver.max_delta, datetime.timedelta)
|
||||
self.assertEqual(self.driver.max_delta, datetime.timedelta(minutes=2))
|
||||
self.assertFalse(self.driver.login_status['success'])
|
||||
self.assertIsNone(self.driver.login_status['message'])
|
||||
self.assertIsNone(self.driver._client)
|
||||
|
||||
def test_is_enabled_true(self):
|
||||
"""Test is_enabled returns True when APRS-IS is enabled."""
|
||||
self.mock_conf.aprs_network.enabled = True
|
||||
self.assertTrue(APRSISDriver.is_enabled())
|
||||
|
||||
def test_is_enabled_false(self):
|
||||
"""Test is_enabled returns False when APRS-IS is disabled."""
|
||||
self.mock_conf.aprs_network.enabled = False
|
||||
self.assertFalse(APRSISDriver.is_enabled())
|
||||
|
||||
def test_is_enabled_key_error(self):
|
||||
"""Test is_enabled returns False when enabled flag doesn't exist."""
|
||||
self.mock_conf.aprs_network = mock.MagicMock()
|
||||
type(self.mock_conf.aprs_network).enabled = mock.PropertyMock(
|
||||
side_effect=KeyError
|
||||
)
|
||||
self.assertFalse(APRSISDriver.is_enabled())
|
||||
|
||||
def test_is_configured_true(self):
|
||||
"""Test is_configured returns True when properly configured."""
|
||||
with mock.patch.object(APRSISDriver, 'is_enabled', return_value=True):
|
||||
self.mock_conf.aprs_network.login = 'TEST'
|
||||
self.mock_conf.aprs_network.password = '12345'
|
||||
self.mock_conf.aprs_network.host = 'rotate.aprs.net'
|
||||
|
||||
self.assertTrue(APRSISDriver.is_configured())
|
||||
|
||||
def test_is_configured_no_login(self):
|
||||
"""Test is_configured raises exception when login not set."""
|
||||
with mock.patch.object(APRSISDriver, 'is_enabled', return_value=True):
|
||||
self.mock_conf.aprs_network.login = None
|
||||
|
||||
with self.assertRaises(exception.MissingConfigOptionException):
|
||||
APRSISDriver.is_configured()
|
||||
|
||||
def test_is_configured_no_password(self):
|
||||
"""Test is_configured raises exception when password not set."""
|
||||
with mock.patch.object(APRSISDriver, 'is_enabled', return_value=True):
|
||||
self.mock_conf.aprs_network.login = 'TEST'
|
||||
self.mock_conf.aprs_network.password = None
|
||||
|
||||
with self.assertRaises(exception.MissingConfigOptionException):
|
||||
APRSISDriver.is_configured()
|
||||
|
||||
def test_is_configured_no_host(self):
|
||||
"""Test is_configured raises exception when host not set."""
|
||||
with mock.patch.object(APRSISDriver, 'is_enabled', return_value=True):
|
||||
self.mock_conf.aprs_network.login = 'TEST'
|
||||
self.mock_conf.aprs_network.password = '12345'
|
||||
self.mock_conf.aprs_network.host = None
|
||||
|
||||
with self.assertRaises(exception.MissingConfigOptionException):
|
||||
APRSISDriver.is_configured()
|
||||
|
||||
def test_is_configured_disabled(self):
|
||||
"""Test is_configured returns True when not enabled."""
|
||||
with mock.patch.object(APRSISDriver, 'is_enabled', return_value=False):
|
||||
self.assertTrue(APRSISDriver.is_configured())
|
||||
|
||||
def test_is_alive_no_client(self):
|
||||
"""Test is_alive returns False when no client."""
|
||||
self.driver._client = None
|
||||
self.assertFalse(self.driver.is_alive)
|
||||
|
||||
def test_is_alive_true(self):
|
||||
"""Test is_alive returns True when client is alive and connection is not stale."""
|
||||
self.driver._client = self.mock_client
|
||||
self.mock_client.is_alive.return_value = True
|
||||
|
||||
with mock.patch.object(self.driver, '_is_stale_connection', return_value=False):
|
||||
self.assertTrue(self.driver.is_alive)
|
||||
|
||||
def test_is_alive_client_not_alive(self):
|
||||
"""Test is_alive returns False when client is not alive."""
|
||||
self.driver._client = self.mock_client
|
||||
self.mock_client.is_alive.return_value = False
|
||||
|
||||
with mock.patch.object(self.driver, '_is_stale_connection', return_value=False):
|
||||
self.assertFalse(self.driver.is_alive)
|
||||
|
||||
def test_is_alive_stale_connection(self):
|
||||
"""Test is_alive returns False when connection is stale."""
|
||||
self.driver._client = self.mock_client
|
||||
self.mock_client.is_alive.return_value = True
|
||||
|
||||
with mock.patch.object(self.driver, '_is_stale_connection', return_value=True):
|
||||
self.assertFalse(self.driver.is_alive)
|
||||
|
||||
def test_close(self):
|
||||
"""Test close method stops and closes the client."""
|
||||
self.driver._client = self.mock_client
|
||||
|
||||
self.driver.close()
|
||||
|
||||
self.mock_client.stop.assert_called_once()
|
||||
self.mock_client.close.assert_called_once()
|
||||
|
||||
def test_close_no_client(self):
|
||||
"""Test close method handles no client gracefully."""
|
||||
self.driver._client = None
|
||||
|
||||
# Should not raise exception
|
||||
self.driver.close()
|
||||
|
||||
def test_send(self):
|
||||
"""Test send passes packet to client."""
|
||||
self.driver._client = self.mock_client
|
||||
mock_packet = mock.MagicMock(spec=core.Packet)
|
||||
|
||||
self.driver.send(mock_packet)
|
||||
|
||||
self.mock_client.send.assert_called_once_with(mock_packet)
|
||||
|
||||
@mock.patch('aprsd.client.drivers.aprsis.LOG')
|
||||
def test_setup_connection_success(self, mock_log):
|
||||
"""Test setup_connection successfully connects."""
|
||||
# Configure successful connection
|
||||
self.mock_client.server_string = 'Test APRS-IS Server'
|
||||
|
||||
self.driver.setup_connection()
|
||||
|
||||
# Check client created with correct parameters
|
||||
self.mock_aprslib.assert_called_once_with(
|
||||
self.mock_conf.aprs_network.login,
|
||||
passwd=self.mock_conf.aprs_network.password,
|
||||
host=self.mock_conf.aprs_network.host,
|
||||
port=self.mock_conf.aprs_network.port,
|
||||
)
|
||||
|
||||
# Check logger set and connection initialized
|
||||
self.assertEqual(self.mock_client.logger, mock_log)
|
||||
self.mock_client.connect.assert_called_once()
|
||||
|
||||
# Check status updated
|
||||
self.assertTrue(self.driver.connected)
|
||||
self.assertTrue(self.driver.login_status['success'])
|
||||
self.assertEqual(self.driver.login_status['message'], 'Test APRS-IS Server')
|
||||
|
||||
@mock.patch('aprsd.client.drivers.aprsis.LOG')
|
||||
@mock.patch('aprsd.client.drivers.aprsis.time.sleep')
|
||||
def test_setup_connection_login_error(self, mock_sleep, mock_log):
|
||||
"""Test setup_connection handles login error."""
|
||||
# Configure login error
|
||||
login_error = LoginError('Bad login')
|
||||
login_error.message = 'Invalid login credentials'
|
||||
self.mock_client.connect.side_effect = login_error
|
||||
|
||||
self.driver.setup_connection()
|
||||
|
||||
# Check error logged
|
||||
mock_log.error.assert_any_call("Failed to login to APRS-IS Server 'Bad login'")
|
||||
mock_log.error.assert_any_call('Invalid login credentials')
|
||||
|
||||
# Check status updated
|
||||
self.assertFalse(self.driver.connected)
|
||||
self.assertFalse(self.driver.login_status['success'])
|
||||
self.assertEqual(
|
||||
self.driver.login_status['message'], 'Invalid login credentials'
|
||||
)
|
||||
|
||||
# Check backoff used
|
||||
mock_sleep.assert_called()
|
||||
|
||||
@mock.patch('aprsd.client.drivers.aprsis.LOG')
|
||||
@mock.patch('aprsd.client.drivers.aprsis.time.sleep')
|
||||
def test_setup_connection_general_error(self, mock_sleep, mock_log):
|
||||
"""Test setup_connection handles general error."""
|
||||
# Configure general exception
|
||||
error_message = 'Connection error'
|
||||
error = Exception(error_message)
|
||||
# Standard exceptions don't have a message attribute
|
||||
self.mock_client.connect.side_effect = error
|
||||
|
||||
self.driver.setup_connection()
|
||||
|
||||
# Check error logged
|
||||
mock_log.error.assert_any_call(
|
||||
f"Unable to connect to APRS-IS server. '{error_message}' "
|
||||
)
|
||||
|
||||
# Check status updated
|
||||
self.assertFalse(self.driver.connected)
|
||||
self.assertFalse(self.driver.login_status['success'])
|
||||
|
||||
# Check login message contains the error message (more flexible than exact equality)
|
||||
self.assertIn(error_message, self.driver.login_status['message'])
|
||||
|
||||
# Check backoff used
|
||||
mock_sleep.assert_called()
|
||||
|
||||
def test_set_filter(self):
|
||||
"""Test set_filter passes filter to client."""
|
||||
self.driver._client = self.mock_client
|
||||
test_filter = 'm/50'
|
||||
|
||||
self.driver.set_filter(test_filter)
|
||||
|
||||
self.mock_client.set_filter.assert_called_once_with(test_filter)
|
||||
|
||||
def test_login_success(self):
|
||||
"""Test login_success returns login status."""
|
||||
self.driver.login_status['success'] = True
|
||||
self.assertTrue(self.driver.login_success())
|
||||
|
||||
self.driver.login_status['success'] = False
|
||||
self.assertFalse(self.driver.login_success())
|
||||
|
||||
def test_login_failure(self):
|
||||
"""Test login_failure returns error message."""
|
||||
self.driver.login_status['message'] = None
|
||||
self.assertIsNone(self.driver.login_failure())
|
||||
|
||||
self.driver.login_status['message'] = 'Test error'
|
||||
self.assertEqual(self.driver.login_failure(), 'Test error')
|
||||
|
||||
def test_filter_property(self):
|
||||
"""Test filter property returns client filter."""
|
||||
self.driver._client = self.mock_client
|
||||
test_filter = 'm/50'
|
||||
self.mock_client.filter = test_filter
|
||||
|
||||
self.assertEqual(self.driver.filter, test_filter)
|
||||
|
||||
def test_server_string_property(self):
|
||||
"""Test server_string property returns client server string."""
|
||||
self.driver._client = self.mock_client
|
||||
test_string = 'Test APRS-IS Server'
|
||||
self.mock_client.server_string = test_string
|
||||
|
||||
self.assertEqual(self.driver.server_string, test_string)
|
||||
|
||||
def test_keepalive_property(self):
|
||||
"""Test keepalive property returns client keepalive."""
|
||||
self.driver._client = self.mock_client
|
||||
test_time = datetime.datetime.now()
|
||||
self.mock_client.aprsd_keepalive = test_time
|
||||
|
||||
self.assertEqual(self.driver.keepalive, test_time)
|
||||
|
||||
@mock.patch('aprsd.client.drivers.aprsis.LOG')
|
||||
def test_is_stale_connection_true(self, mock_log):
|
||||
"""Test _is_stale_connection returns True when connection is stale."""
|
||||
self.driver._client = self.mock_client
|
||||
# Set keepalive to 3 minutes ago (exceeds max_delta of 2 minutes)
|
||||
self.mock_client.aprsd_keepalive = datetime.datetime.now() - datetime.timedelta(
|
||||
minutes=3
|
||||
)
|
||||
|
||||
result = self.driver._is_stale_connection()
|
||||
|
||||
self.assertTrue(result)
|
||||
mock_log.error.assert_called_once()
|
||||
|
||||
def test_is_stale_connection_false(self):
|
||||
"""Test _is_stale_connection returns False when connection is not stale."""
|
||||
self.driver._client = self.mock_client
|
||||
# Set keepalive to 1 minute ago (within max_delta of 2 minutes)
|
||||
self.mock_client.aprsd_keepalive = datetime.datetime.now() - datetime.timedelta(
|
||||
minutes=1
|
||||
)
|
||||
|
||||
result = self.driver._is_stale_connection()
|
||||
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_transport(self):
|
||||
"""Test transport returns appropriate transport type."""
|
||||
self.assertEqual(APRSISDriver.transport(), 'aprsis')
|
||||
|
||||
def test_decode_packet(self):
|
||||
"""Test decode_packet uses core.factory."""
|
||||
with mock.patch('aprsd.client.drivers.aprsis.core.factory') as mock_factory:
|
||||
raw_packet = {'from': 'TEST', 'to': 'APRS'}
|
||||
self.driver.decode_packet(raw_packet)
|
||||
mock_factory.assert_called_once_with(raw_packet)
|
||||
|
||||
@mock.patch('aprsd.client.drivers.aprsis.LOG')
|
||||
def test_consumer_success(self, mock_log):
|
||||
"""Test consumer forwards callback to client."""
|
||||
self.driver._client = self.mock_client
|
||||
mock_callback = mock.MagicMock()
|
||||
|
||||
self.driver.consumer(mock_callback, raw=True)
|
||||
|
||||
self.mock_client.consumer.assert_called_once_with(
|
||||
mock_callback, blocking=False, immortal=False, raw=True
|
||||
)
|
||||
|
||||
@mock.patch('aprsd.client.drivers.aprsis.LOG')
|
||||
def test_consumer_exception(self, mock_log):
|
||||
"""Test consumer handles exceptions."""
|
||||
self.driver._client = self.mock_client
|
||||
mock_callback = mock.MagicMock()
|
||||
test_error = Exception('Test error')
|
||||
self.mock_client.consumer.side_effect = test_error
|
||||
|
||||
with self.assertRaises(Exception): # noqa: B017
|
||||
self.driver.consumer(mock_callback)
|
||||
|
||||
mock_log.error.assert_called_with(test_error)
|
||||
|
||||
@mock.patch('aprsd.client.drivers.aprsis.LOG')
|
||||
def test_consumer_no_client(self, mock_log):
|
||||
"""Test consumer handles no client gracefully."""
|
||||
self.driver._client = None
|
||||
mock_callback = mock.MagicMock()
|
||||
|
||||
self.driver.consumer(mock_callback)
|
||||
|
||||
mock_log.warning.assert_called_once()
|
||||
self.assertFalse(self.driver.connected)
|
||||
|
||||
def test_stats_configured_with_client(self):
|
||||
"""Test stats returns correct data when configured with client."""
|
||||
# Configure driver
|
||||
with mock.patch.object(self.driver, 'is_configured', return_value=True):
|
||||
self.driver._client = self.mock_client
|
||||
self.mock_client.aprsd_keepalive = datetime.datetime.now()
|
||||
self.mock_client.server_string = 'Test Server'
|
||||
self.mock_client.filter = 'm/50'
|
||||
|
||||
stats = self.driver.stats()
|
||||
|
||||
self.assertEqual(stats['connected'], True)
|
||||
self.assertEqual(stats['filter'], 'm/50')
|
||||
self.assertEqual(stats['server_string'], 'Test Server')
|
||||
self.assertEqual(stats['transport'], 'aprsis')
|
||||
|
||||
def test_stats_serializable(self):
|
||||
"""Test stats with serializable=True converts datetime to ISO format."""
|
||||
# Configure driver
|
||||
with mock.patch.object(self.driver, 'is_configured', return_value=True):
|
||||
self.driver._client = self.mock_client
|
||||
test_time = datetime.datetime.now()
|
||||
self.mock_client.aprsd_keepalive = test_time
|
||||
|
||||
stats = self.driver.stats(serializable=True)
|
||||
|
||||
# Check keepalive is a string in ISO format
|
||||
self.assertIsInstance(stats['connection_keepalive'], str)
|
||||
# Try parsing it to verify it's a valid ISO format
|
||||
try:
|
||||
datetime.datetime.fromisoformat(stats['connection_keepalive'])
|
||||
except ValueError:
|
||||
self.fail('keepalive is not in valid ISO format')
|
||||
|
||||
def test_stats_no_client(self):
|
||||
"""Test stats with no client."""
|
||||
with mock.patch.object(self.driver, 'is_configured', return_value=True):
|
||||
self.driver._client = None
|
||||
|
||||
stats = self.driver.stats()
|
||||
|
||||
self.assertEqual(stats['connection_keepalive'], 'None')
|
||||
self.assertEqual(stats['server_string'], 'None')
|
||||
|
||||
def test_stats_not_configured(self):
|
||||
"""Test stats when not configured returns empty dict."""
|
||||
with mock.patch.object(self.driver, 'is_configured', return_value=False):
|
||||
stats = self.driver.stats()
|
||||
self.assertEqual(stats, {})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
191
tests/client/drivers/test_fake_driver.py
Normal file
191
tests/client/drivers/test_fake_driver.py
Normal file
@ -0,0 +1,191 @@
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from aprsd.client.drivers.fake import APRSDFakeDriver
|
||||
from aprsd.packets import core
|
||||
|
||||
|
||||
class TestAPRSDFakeDriver(unittest.TestCase):
|
||||
"""Unit tests for the APRSDFakeDriver class."""
|
||||
|
||||
def setUp(self):
|
||||
# Mock CONF for testing
|
||||
self.conf_patcher = mock.patch('aprsd.client.drivers.fake.CONF')
|
||||
self.mock_conf = self.conf_patcher.start()
|
||||
|
||||
# Configure fake_client.enabled
|
||||
self.mock_conf.fake_client.enabled = True
|
||||
|
||||
# Create an instance of the driver
|
||||
self.driver = APRSDFakeDriver()
|
||||
|
||||
def tearDown(self):
|
||||
self.conf_patcher.stop()
|
||||
|
||||
def test_init(self):
|
||||
"""Test initialization sets default values."""
|
||||
self.assertEqual(self.driver.path, ['WIDE1-1', 'WIDE2-1'])
|
||||
self.assertFalse(self.driver.thread_stop)
|
||||
|
||||
def test_is_enabled_true(self):
|
||||
"""Test is_enabled returns True when configured."""
|
||||
self.mock_conf.fake_client.enabled = True
|
||||
self.assertTrue(APRSDFakeDriver.is_enabled())
|
||||
|
||||
def test_is_enabled_false(self):
|
||||
"""Test is_enabled returns False when not configured."""
|
||||
self.mock_conf.fake_client.enabled = False
|
||||
self.assertFalse(APRSDFakeDriver.is_enabled())
|
||||
|
||||
def test_is_alive(self):
|
||||
"""Test is_alive returns True when thread_stop is False."""
|
||||
self.driver.thread_stop = False
|
||||
self.assertTrue(self.driver.is_alive())
|
||||
|
||||
self.driver.thread_stop = True
|
||||
self.assertFalse(self.driver.is_alive())
|
||||
|
||||
def test_close(self):
|
||||
"""Test close sets thread_stop to True."""
|
||||
self.driver.thread_stop = False
|
||||
self.driver.close()
|
||||
self.assertTrue(self.driver.thread_stop)
|
||||
|
||||
@mock.patch('aprsd.client.drivers.fake.LOG')
|
||||
def test_setup_connection(self, mock_log):
|
||||
"""Test setup_connection does nothing (it's fake)."""
|
||||
self.driver.setup_connection()
|
||||
# Method doesn't do anything, so just verify it doesn't crash
|
||||
|
||||
def test_set_filter(self):
|
||||
"""Test set_filter method does nothing (it's fake)."""
|
||||
# Just test it doesn't fail
|
||||
self.driver.set_filter('test/filter')
|
||||
|
||||
def test_login_success(self):
|
||||
"""Test login_success always returns True."""
|
||||
self.assertTrue(self.driver.login_success())
|
||||
|
||||
def test_login_failure(self):
|
||||
"""Test login_failure always returns None."""
|
||||
self.assertIsNone(self.driver.login_failure())
|
||||
|
||||
@mock.patch('aprsd.client.drivers.fake.LOG')
|
||||
def test_send_with_packet_object(self, mock_log):
|
||||
"""Test send with a Packet object."""
|
||||
mock_packet = mock.MagicMock(spec=core.Packet)
|
||||
mock_packet.payload = 'Test payload'
|
||||
mock_packet.to_call = 'TEST'
|
||||
mock_packet.from_call = 'FAKE'
|
||||
|
||||
self.driver.send(mock_packet)
|
||||
|
||||
mock_log.info.assert_called_once()
|
||||
mock_packet.prepare.assert_called_once()
|
||||
|
||||
@mock.patch('aprsd.client.drivers.fake.LOG')
|
||||
def test_send_with_non_packet_object(self, mock_log):
|
||||
"""Test send with a non-Packet object."""
|
||||
# Create a mock message-like object
|
||||
mock_msg = mock.MagicMock()
|
||||
mock_msg.raw = 'Test'
|
||||
mock_msg.msgNo = '123'
|
||||
mock_msg.to_call = 'TEST'
|
||||
mock_msg.from_call = 'FAKE'
|
||||
|
||||
self.driver.send(mock_msg)
|
||||
|
||||
mock_log.info.assert_called_once()
|
||||
mock_log.debug.assert_called_once()
|
||||
|
||||
@mock.patch('aprsd.client.drivers.fake.LOG')
|
||||
@mock.patch('aprsd.client.drivers.fake.time.sleep')
|
||||
def test_consumer_with_raw_true(self, mock_sleep, mock_log):
|
||||
"""Test consumer with raw=True."""
|
||||
mock_callback = mock.MagicMock()
|
||||
|
||||
self.driver.consumer(mock_callback, raw=True)
|
||||
|
||||
# Verify callback was called with raw data
|
||||
mock_callback.assert_called_once()
|
||||
call_args = mock_callback.call_args[1]
|
||||
self.assertIn('raw', call_args)
|
||||
mock_sleep.assert_called_once_with(1)
|
||||
|
||||
@mock.patch('aprsd.client.drivers.fake.LOG')
|
||||
@mock.patch('aprsd.client.drivers.fake.aprslib.parse')
|
||||
@mock.patch('aprsd.client.drivers.fake.core.factory')
|
||||
@mock.patch('aprsd.client.drivers.fake.time.sleep')
|
||||
def test_consumer_with_raw_false(
|
||||
self, mock_sleep, mock_factory, mock_parse, mock_log
|
||||
):
|
||||
"""Test consumer with raw=False."""
|
||||
mock_callback = mock.MagicMock()
|
||||
mock_packet = mock.MagicMock(spec=core.Packet)
|
||||
mock_factory.return_value = mock_packet
|
||||
|
||||
self.driver.consumer(mock_callback, raw=False)
|
||||
|
||||
# Verify the packet was created and passed to callback
|
||||
mock_parse.assert_called_once()
|
||||
mock_factory.assert_called_once()
|
||||
mock_callback.assert_called_once_with(packet=mock_packet)
|
||||
mock_sleep.assert_called_once_with(1)
|
||||
|
||||
def test_consumer_updates_keepalive(self):
|
||||
"""Test consumer updates keepalive timestamp."""
|
||||
mock_callback = mock.MagicMock()
|
||||
old_keepalive = self.driver.aprsd_keepalive
|
||||
|
||||
# Force a small delay to ensure timestamp changes
|
||||
import time
|
||||
|
||||
time.sleep(0.01)
|
||||
|
||||
with mock.patch('aprsd.client.drivers.fake.time.sleep'):
|
||||
self.driver.consumer(mock_callback)
|
||||
|
||||
self.assertNotEqual(old_keepalive, self.driver.aprsd_keepalive)
|
||||
self.assertGreater(self.driver.aprsd_keepalive, old_keepalive)
|
||||
|
||||
def test_decode_packet_with_empty_kwargs(self):
|
||||
"""Test decode_packet with empty kwargs."""
|
||||
result = self.driver.decode_packet()
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_decode_packet_with_packet(self):
|
||||
"""Test decode_packet with packet in kwargs."""
|
||||
mock_packet = mock.MagicMock(spec=core.Packet)
|
||||
result = self.driver.decode_packet(packet=mock_packet)
|
||||
self.assertEqual(result, mock_packet)
|
||||
|
||||
@mock.patch('aprsd.client.drivers.fake.aprslib.parse')
|
||||
@mock.patch('aprsd.client.drivers.fake.core.factory')
|
||||
def test_decode_packet_with_raw(self, mock_factory, mock_parse):
|
||||
"""Test decode_packet with raw in kwargs."""
|
||||
mock_packet = mock.MagicMock(spec=core.Packet)
|
||||
mock_factory.return_value = mock_packet
|
||||
raw_data = 'raw packet data'
|
||||
|
||||
result = self.driver.decode_packet(raw=raw_data)
|
||||
|
||||
mock_parse.assert_called_once_with(raw_data)
|
||||
mock_factory.assert_called_once_with(mock_parse.return_value)
|
||||
self.assertEqual(result, mock_packet)
|
||||
|
||||
def test_stats(self):
|
||||
"""Test stats returns correct information."""
|
||||
self.driver.thread_stop = False
|
||||
result = self.driver.stats()
|
||||
|
||||
self.assertEqual(result['driver'], 'APRSDFakeDriver')
|
||||
self.assertTrue(result['is_alive'])
|
||||
|
||||
# Test with serializable parameter
|
||||
result_serializable = self.driver.stats(serializable=True)
|
||||
self.assertEqual(result_serializable['driver'], 'APRSDFakeDriver')
|
||||
self.assertTrue(result_serializable['is_alive'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
498
tests/client/drivers/test_tcpkiss_driver.py
Normal file
498
tests/client/drivers/test_tcpkiss_driver.py
Normal file
@ -0,0 +1,498 @@
|
||||
import datetime
|
||||
import socket
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
import aprslib
|
||||
|
||||
from aprsd import exception
|
||||
from aprsd.client.drivers.registry import ClientDriver
|
||||
from aprsd.client.drivers.tcpkiss import TCPKISSDriver
|
||||
from aprsd.packets import core
|
||||
|
||||
|
||||
class TestTCPKISSDriver(unittest.TestCase):
|
||||
"""Unit tests for the TCPKISSDriver class."""
|
||||
|
||||
def setUp(self):
|
||||
# Mock configuration
|
||||
self.conf_patcher = mock.patch('aprsd.client.drivers.tcpkiss.CONF')
|
||||
self.mock_conf = self.conf_patcher.start()
|
||||
|
||||
# Configure KISS settings
|
||||
self.mock_conf.kiss_tcp.enabled = True
|
||||
self.mock_conf.kiss_tcp.host = '127.0.0.1'
|
||||
self.mock_conf.kiss_tcp.port = 8001
|
||||
self.mock_conf.kiss_tcp.path = ['WIDE1-1', 'WIDE2-1']
|
||||
|
||||
# Mock socket
|
||||
self.socket_patcher = mock.patch('aprsd.client.drivers.tcpkiss.socket')
|
||||
self.mock_socket_module = self.socket_patcher.start()
|
||||
self.mock_socket = mock.MagicMock()
|
||||
self.mock_socket_module.socket.return_value = self.mock_socket
|
||||
|
||||
# Mock select
|
||||
self.select_patcher = mock.patch('aprsd.client.drivers.tcpkiss.select')
|
||||
self.mock_select = self.select_patcher.start()
|
||||
|
||||
# Create an instance of the driver
|
||||
self.driver = TCPKISSDriver()
|
||||
|
||||
def tearDown(self):
|
||||
self.conf_patcher.stop()
|
||||
self.socket_patcher.stop()
|
||||
self.select_patcher.stop()
|
||||
|
||||
def test_implements_client_driver_protocol(self):
|
||||
"""Test that TCPKISSDriver implements the ClientDriver Protocol."""
|
||||
# Verify the instance is recognized as implementing the Protocol
|
||||
self.assertIsInstance(self.driver, ClientDriver)
|
||||
|
||||
# Verify all required methods are present with correct signatures
|
||||
required_methods = [
|
||||
'is_enabled',
|
||||
'is_configured',
|
||||
'is_alive',
|
||||
'close',
|
||||
'send',
|
||||
'setup_connection',
|
||||
'set_filter',
|
||||
'login_success',
|
||||
'login_failure',
|
||||
'consumer',
|
||||
'decode_packet',
|
||||
'stats',
|
||||
]
|
||||
|
||||
for method_name in required_methods:
|
||||
self.assertTrue(
|
||||
hasattr(self.driver, method_name),
|
||||
f'Missing required method: {method_name}',
|
||||
)
|
||||
|
||||
def test_init(self):
|
||||
"""Test initialization sets default values."""
|
||||
self.assertFalse(self.driver._connected)
|
||||
self.assertIsInstance(self.driver.keepalive, datetime.datetime)
|
||||
self.assertFalse(self.driver._running)
|
||||
|
||||
def test_transport_property(self):
|
||||
"""Test transport property returns correct value."""
|
||||
self.assertEqual(self.driver.transport, 'tcpkiss')
|
||||
|
||||
def test_is_enabled_true(self):
|
||||
"""Test is_enabled returns True when KISS TCP is enabled."""
|
||||
self.mock_conf.kiss_tcp.enabled = True
|
||||
self.assertTrue(TCPKISSDriver.is_enabled())
|
||||
|
||||
def test_is_enabled_false(self):
|
||||
"""Test is_enabled returns False when KISS TCP is disabled."""
|
||||
self.mock_conf.kiss_tcp.enabled = False
|
||||
self.assertFalse(TCPKISSDriver.is_enabled())
|
||||
|
||||
def test_is_configured_true(self):
|
||||
"""Test is_configured returns True when properly configured."""
|
||||
with mock.patch.object(TCPKISSDriver, 'is_enabled', return_value=True):
|
||||
self.mock_conf.kiss_tcp.host = '127.0.0.1'
|
||||
self.assertTrue(TCPKISSDriver.is_configured())
|
||||
|
||||
def test_is_configured_false_no_host(self):
|
||||
"""Test is_configured returns False when host not set."""
|
||||
with mock.patch.object(TCPKISSDriver, 'is_enabled', return_value=True):
|
||||
self.mock_conf.kiss_tcp.host = None
|
||||
with self.assertRaises(exception.MissingConfigOptionException):
|
||||
TCPKISSDriver.is_configured()
|
||||
|
||||
def test_is_configured_false_not_enabled(self):
|
||||
"""Test is_configured returns False when not enabled."""
|
||||
with mock.patch.object(TCPKISSDriver, 'is_enabled', return_value=False):
|
||||
self.assertFalse(TCPKISSDriver.is_configured())
|
||||
|
||||
def test_is_alive(self):
|
||||
"""Test is_alive property returns connection state."""
|
||||
self.driver._connected = True
|
||||
self.assertTrue(self.driver.is_alive)
|
||||
|
||||
self.driver._connected = False
|
||||
self.assertFalse(self.driver.is_alive)
|
||||
|
||||
def test_close(self):
|
||||
"""Test close method calls stop."""
|
||||
with mock.patch.object(self.driver, 'stop') as mock_stop:
|
||||
self.driver.close()
|
||||
mock_stop.assert_called_once()
|
||||
|
||||
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
|
||||
def test_setup_connection_success(self, mock_log):
|
||||
"""Test setup_connection successfully connects."""
|
||||
# Mock the connect method to succeed
|
||||
is_en = self.driver.is_enabled
|
||||
is_con = self.driver.is_configured
|
||||
self.driver.is_enabled = mock.MagicMock(return_value=True)
|
||||
self.driver.is_configured = mock.MagicMock(return_value=True)
|
||||
with mock.patch.object(
|
||||
self.driver, 'connect', return_value=True
|
||||
) as mock_connect:
|
||||
self.driver.setup_connection()
|
||||
mock_connect.assert_called_once()
|
||||
mock_log.info.assert_called_with('KISS TCP Connection to 127.0.0.1:8001')
|
||||
|
||||
self.driver.is_enabled = is_en
|
||||
self.driver.is_configured = is_con
|
||||
|
||||
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
|
||||
def test_setup_connection_failure(self, mock_log):
|
||||
"""Test setup_connection handles connection failure."""
|
||||
# Mock the connect method to fail
|
||||
with mock.patch.object(
|
||||
self.driver, 'connect', return_value=False
|
||||
) as mock_connect:
|
||||
self.driver.setup_connection()
|
||||
mock_connect.assert_called_once()
|
||||
mock_log.error.assert_called_with('Failed to connect to KISS interface')
|
||||
|
||||
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
|
||||
def test_setup_connection_exception(self, mock_log):
|
||||
"""Test setup_connection handles exceptions."""
|
||||
# Mock the connect method to raise an exception
|
||||
with mock.patch.object(
|
||||
self.driver, 'connect', side_effect=Exception('Test error')
|
||||
) as mock_connect:
|
||||
self.driver.setup_connection()
|
||||
mock_connect.assert_called_once()
|
||||
mock_log.error.assert_any_call('Failed to initialize KISS interface')
|
||||
mock_log.exception.assert_called_once()
|
||||
self.assertFalse(self.driver._connected)
|
||||
|
||||
def test_set_filter(self):
|
||||
"""Test set_filter does nothing for KISS."""
|
||||
# Just ensure it doesn't fail
|
||||
self.driver.set_filter('test/filter')
|
||||
|
||||
def test_login_success_when_connected(self):
|
||||
"""Test login_success returns True when connected."""
|
||||
self.driver._connected = True
|
||||
self.assertTrue(self.driver.login_success())
|
||||
|
||||
def test_login_success_when_not_connected(self):
|
||||
"""Test login_success returns False when not connected."""
|
||||
self.driver._connected = False
|
||||
self.assertFalse(self.driver.login_success())
|
||||
|
||||
def test_login_failure(self):
|
||||
"""Test login_failure returns success message."""
|
||||
self.assertEqual(self.driver.login_failure(), 'Login successful')
|
||||
|
||||
@mock.patch('aprsd.client.drivers.tcpkiss.ax25frame.Frame.ui')
|
||||
def test_send_packet(self, mock_frame_ui):
|
||||
"""Test sending an APRS packet."""
|
||||
# Create a mock frame
|
||||
mock_frame = mock.MagicMock()
|
||||
mock_frame_bytes = b'mock_frame_data'
|
||||
mock_frame.__bytes__ = mock.MagicMock(return_value=mock_frame_bytes)
|
||||
mock_frame_ui.return_value = mock_frame
|
||||
|
||||
# Set up the driver
|
||||
self.driver.socket = self.mock_socket
|
||||
self.driver.path = ['WIDE1-1', 'WIDE2-1']
|
||||
|
||||
# Create a mock packet
|
||||
mock_packet = mock.MagicMock(spec=core.Packet)
|
||||
mock_bytes = b'Test packet data'
|
||||
mock_packet.__bytes__ = mock.MagicMock(return_value=mock_bytes)
|
||||
# Add path attribute to the mock packet
|
||||
mock_packet.path = None
|
||||
|
||||
# Send the packet
|
||||
self.driver.send(mock_packet)
|
||||
|
||||
# Check that frame was created correctly
|
||||
mock_frame_ui.assert_called_once_with(
|
||||
destination='APZ100',
|
||||
source=mock_packet.from_call,
|
||||
path=self.driver.path,
|
||||
info=mock_packet.payload.encode('US-ASCII'),
|
||||
)
|
||||
|
||||
# Check that socket send was called
|
||||
self.mock_socket.send.assert_called_once()
|
||||
|
||||
# Verify packet counters updated
|
||||
self.assertEqual(self.driver.packets_sent, 1)
|
||||
self.assertIsNotNone(self.driver.last_packet_sent)
|
||||
|
||||
def test_send_with_no_socket(self):
|
||||
"""Test send raises exception when socket not initialized."""
|
||||
self.driver.socket = None
|
||||
mock_packet = mock.MagicMock(spec=core.Packet)
|
||||
|
||||
with self.assertRaises(Exception) as context:
|
||||
self.driver.send(mock_packet)
|
||||
self.assertIn('KISS interface not initialized', str(context.exception))
|
||||
|
||||
def test_stop(self):
|
||||
"""Test stop method cleans up properly."""
|
||||
self.driver._running = True
|
||||
self.driver._connected = True
|
||||
self.driver.socket = self.mock_socket
|
||||
|
||||
self.driver.stop()
|
||||
|
||||
self.assertFalse(self.driver._running)
|
||||
self.assertFalse(self.driver._connected)
|
||||
self.mock_socket.close.assert_called_once()
|
||||
|
||||
def test_stats(self):
|
||||
"""Test stats method returns correct data."""
|
||||
# Set up test data
|
||||
self.driver._connected = True
|
||||
self.driver.path = ['WIDE1-1', 'WIDE2-1']
|
||||
self.driver.packets_sent = 5
|
||||
self.driver.packets_received = 3
|
||||
self.driver.last_packet_sent = datetime.datetime.now()
|
||||
self.driver.last_packet_received = datetime.datetime.now()
|
||||
|
||||
# Get stats
|
||||
stats = self.driver.stats()
|
||||
|
||||
# Check stats contains expected keys
|
||||
expected_keys = [
|
||||
'client',
|
||||
'transport',
|
||||
'connected',
|
||||
'path',
|
||||
'packets_sent',
|
||||
'packets_received',
|
||||
'last_packet_sent',
|
||||
'last_packet_received',
|
||||
'connection_keepalive',
|
||||
'host',
|
||||
'port',
|
||||
]
|
||||
for key in expected_keys:
|
||||
self.assertIn(key, stats)
|
||||
|
||||
# Check some specific values
|
||||
self.assertEqual(stats['client'], 'TCPKISSDriver')
|
||||
self.assertEqual(stats['transport'], 'tcpkiss')
|
||||
self.assertEqual(stats['connected'], True)
|
||||
self.assertEqual(stats['packets_sent'], 5)
|
||||
self.assertEqual(stats['packets_received'], 3)
|
||||
|
||||
def test_stats_serializable(self):
|
||||
"""Test stats with serializable=True converts datetime to ISO format."""
|
||||
self.driver.keepalive = datetime.datetime.now()
|
||||
|
||||
stats = self.driver.stats(serializable=True)
|
||||
|
||||
# Check keepalive is a string in ISO format
|
||||
self.assertIsInstance(stats['connection_keepalive'], str)
|
||||
# Try parsing it to verify it's a valid ISO format
|
||||
try:
|
||||
datetime.datetime.fromisoformat(stats['connection_keepalive'])
|
||||
except ValueError:
|
||||
self.fail('keepalive is not in valid ISO format')
|
||||
|
||||
def test_connect_success(self):
|
||||
"""Test successful connection."""
|
||||
result = self.driver.connect()
|
||||
|
||||
self.assertTrue(result)
|
||||
self.assertTrue(self.driver._connected)
|
||||
self.mock_socket.connect.assert_called_once_with(
|
||||
(self.mock_conf.kiss_tcp.host, self.mock_conf.kiss_tcp.port)
|
||||
)
|
||||
self.mock_socket.settimeout.assert_any_call(5.0)
|
||||
self.mock_socket.settimeout.assert_any_call(0.1)
|
||||
|
||||
def test_connect_failure_socket_error(self):
|
||||
"""Test connection failure due to socket error."""
|
||||
self.mock_socket.connect.side_effect = socket.error('Test socket error')
|
||||
|
||||
result = self.driver.connect()
|
||||
|
||||
self.assertFalse(result)
|
||||
self.assertFalse(self.driver._connected)
|
||||
|
||||
def test_connect_failure_timeout(self):
|
||||
"""Test connection failure due to timeout."""
|
||||
self.mock_socket.connect.side_effect = socket.timeout('Test timeout')
|
||||
|
||||
result = self.driver.connect()
|
||||
|
||||
self.assertFalse(result)
|
||||
self.assertFalse(self.driver._connected)
|
||||
|
||||
def test_fix_raw_frame(self):
|
||||
"""Test fix_raw_frame removes KISS markers and handles FEND."""
|
||||
# Create a test frame with KISS markers
|
||||
with mock.patch(
|
||||
'aprsd.client.drivers.tcpkiss.handle_fend', return_value=b'fixed_frame'
|
||||
) as mock_handle_fend:
|
||||
raw_frame = b'\xc0\x00some_frame_data\xc0' # \xc0 is FEND
|
||||
|
||||
result = self.driver.fix_raw_frame(raw_frame)
|
||||
|
||||
mock_handle_fend.assert_called_once_with(b'some_frame_data')
|
||||
self.assertEqual(result, b'fixed_frame')
|
||||
|
||||
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
|
||||
def test_decode_packet_success(self, mock_log):
|
||||
"""Test successful packet decoding."""
|
||||
mock_frame = 'test frame data'
|
||||
mock_aprs_data = {'from': 'TEST-1', 'to': 'APRS'}
|
||||
mock_packet = mock.MagicMock(spec=core.Packet)
|
||||
|
||||
with mock.patch(
|
||||
'aprsd.client.drivers.tcpkiss.aprslib.parse', return_value=mock_aprs_data
|
||||
) as mock_parse:
|
||||
with mock.patch(
|
||||
'aprsd.client.drivers.tcpkiss.core.factory', return_value=mock_packet
|
||||
) as mock_factory:
|
||||
result = self.driver.decode_packet(frame=mock_frame)
|
||||
|
||||
mock_parse.assert_called_once_with(str(mock_frame))
|
||||
mock_factory.assert_called_once_with(mock_aprs_data)
|
||||
self.assertEqual(result, mock_packet)
|
||||
|
||||
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
|
||||
def test_decode_packet_no_frame(self, mock_log):
|
||||
"""Test decode_packet with no frame returns None."""
|
||||
result = self.driver.decode_packet()
|
||||
|
||||
self.assertIsNone(result)
|
||||
mock_log.warning.assert_called_once()
|
||||
|
||||
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
|
||||
def test_decode_packet_exception(self, mock_log):
|
||||
"""Test decode_packet handles exceptions."""
|
||||
mock_frame = 'invalid frame'
|
||||
|
||||
with mock.patch(
|
||||
'aprsd.client.drivers.tcpkiss.aprslib.parse',
|
||||
side_effect=Exception('Test error'),
|
||||
) as mock_parse:
|
||||
result = self.driver.decode_packet(frame=mock_frame)
|
||||
|
||||
mock_parse.assert_called_once()
|
||||
self.assertIsNone(result)
|
||||
mock_log.error.assert_called_once()
|
||||
|
||||
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
|
||||
def test_consumer_with_frame(self, mock_log):
|
||||
"""Test consumer processes frames and calls callback."""
|
||||
mock_callback = mock.MagicMock()
|
||||
mock_frame = mock.MagicMock()
|
||||
|
||||
# Configure driver for test
|
||||
self.driver._connected = True
|
||||
self.driver._running = True
|
||||
|
||||
# Set up read_frame to return one frame then stop
|
||||
def side_effect():
|
||||
self.driver._running = False
|
||||
return mock_frame
|
||||
|
||||
with mock.patch.object(
|
||||
self.driver, 'read_frame', side_effect=side_effect
|
||||
) as mock_read_frame:
|
||||
self.driver.consumer(mock_callback)
|
||||
|
||||
mock_read_frame.assert_called_once()
|
||||
mock_callback.assert_called_once_with(frame=mock_frame)
|
||||
|
||||
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
|
||||
def test_consumer_with_connect_reconnect(self, mock_log):
|
||||
"""Test consumer tries to reconnect when not connected."""
|
||||
mock_callback = mock.MagicMock()
|
||||
|
||||
# Configure driver for test
|
||||
self.driver._connected = False
|
||||
|
||||
# Setup to run once then stop
|
||||
call_count = 0
|
||||
|
||||
def connect_side_effect():
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
# On second call, connect successfully
|
||||
if call_count == 2:
|
||||
self.driver._running = False
|
||||
self.driver.socket = self.mock_socket
|
||||
return True
|
||||
return False
|
||||
|
||||
with mock.patch.object(
|
||||
self.driver, 'connect', side_effect=connect_side_effect
|
||||
) as mock_connect:
|
||||
with mock.patch('aprsd.client.drivers.tcpkiss.time.sleep') as mock_sleep:
|
||||
self.driver.consumer(mock_callback)
|
||||
|
||||
self.assertEqual(mock_connect.call_count, 2)
|
||||
mock_sleep.assert_called_once_with(1)
|
||||
|
||||
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
|
||||
def test_read_frame_success(self, mock_log):
|
||||
"""Test read_frame successfully reads a frame."""
|
||||
# Set up driver
|
||||
self.driver.socket = self.mock_socket
|
||||
self.driver._running = True
|
||||
|
||||
# Mock socket recv to return data
|
||||
raw_data = b'\xc0\x00test_frame\xc0'
|
||||
self.mock_socket.recv.return_value = raw_data
|
||||
|
||||
# Mock select to indicate socket is readable
|
||||
self.mock_select.select.return_value = ([self.mock_socket], [], [])
|
||||
|
||||
# Mock fix_raw_frame and Frame.from_bytes
|
||||
mock_fixed_frame = b'fixed_frame'
|
||||
mock_ax25_frame = mock.MagicMock()
|
||||
|
||||
with mock.patch.object(
|
||||
self.driver, 'fix_raw_frame', return_value=mock_fixed_frame
|
||||
) as mock_fix:
|
||||
with mock.patch(
|
||||
'aprsd.client.drivers.tcpkiss.ax25frame.Frame.from_bytes',
|
||||
return_value=mock_ax25_frame,
|
||||
) as mock_from_bytes:
|
||||
result = self.driver.read_frame()
|
||||
|
||||
self.mock_socket.setblocking.assert_called_once_with(0)
|
||||
self.mock_select.select.assert_called_once()
|
||||
self.mock_socket.recv.assert_called_once()
|
||||
mock_fix.assert_called_once_with(raw_data)
|
||||
mock_from_bytes.assert_called_once_with(mock_fixed_frame)
|
||||
self.assertEqual(result, mock_ax25_frame)
|
||||
|
||||
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
|
||||
def test_read_frame_select_timeout(self, mock_log):
|
||||
"""Test read_frame handles select timeout."""
|
||||
# Set up driver
|
||||
self.driver.socket = self.mock_socket
|
||||
self.driver._running = True
|
||||
|
||||
# Mock select to indicate no readable sockets
|
||||
self.mock_select.select.return_value = ([], [], [])
|
||||
|
||||
result = self.driver.read_frame()
|
||||
|
||||
self.assertIsNone(result)
|
||||
|
||||
@mock.patch('aprsd.client.drivers.tcpkiss.LOG')
|
||||
def test_read_frame_socket_error(self, mock_log):
|
||||
"""Test read_frame handles socket error."""
|
||||
# Set up driver
|
||||
self.driver.socket = self.mock_socket
|
||||
self.driver._running = True
|
||||
|
||||
# Mock setblocking to raise OSError
|
||||
self.mock_socket.setblocking.side_effect = OSError('Test error')
|
||||
|
||||
with self.assertRaises(aprslib.ConnectionDrop):
|
||||
self.driver.read_frame()
|
||||
mock_log.error.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -1,89 +0,0 @@
|
||||
import datetime
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from aprsd import exception
|
||||
from aprsd.client.aprsis import APRSISClient
|
||||
|
||||
|
||||
class TestAPRSISClient(unittest.TestCase):
|
||||
"""Test cases for APRSISClient."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
super().setUp()
|
||||
|
||||
# Mock the config
|
||||
self.mock_conf = mock.MagicMock()
|
||||
self.mock_conf.aprs_network.enabled = True
|
||||
self.mock_conf.aprs_network.login = "TEST"
|
||||
self.mock_conf.aprs_network.password = "12345"
|
||||
self.mock_conf.aprs_network.host = "localhost"
|
||||
self.mock_conf.aprs_network.port = 14580
|
||||
|
||||
@mock.patch("aprsd.client.base.APRSClient")
|
||||
@mock.patch("aprsd.client.drivers.aprsis.Aprsdis")
|
||||
def test_stats_not_configured(self, mock_aprsdis, mock_base):
|
||||
"""Test stats when client is not configured."""
|
||||
mock_client = mock.MagicMock()
|
||||
mock_aprsdis.return_value = mock_client
|
||||
|
||||
with mock.patch("aprsd.client.aprsis.cfg.CONF", self.mock_conf):
|
||||
self.client = APRSISClient()
|
||||
|
||||
with mock.patch.object(APRSISClient, "is_configured", return_value=False):
|
||||
stats = self.client.stats()
|
||||
self.assertEqual({}, stats)
|
||||
|
||||
@mock.patch("aprsd.client.base.APRSClient")
|
||||
@mock.patch("aprsd.client.drivers.aprsis.Aprsdis")
|
||||
def test_stats_configured(self, mock_aprsdis, mock_base):
|
||||
"""Test stats when client is configured."""
|
||||
mock_client = mock.MagicMock()
|
||||
mock_aprsdis.return_value = mock_client
|
||||
|
||||
with mock.patch("aprsd.client.aprsis.cfg.CONF", self.mock_conf):
|
||||
self.client = APRSISClient()
|
||||
|
||||
mock_client = mock.MagicMock()
|
||||
mock_client.server_string = "test.server:14580"
|
||||
mock_client.aprsd_keepalive = datetime.datetime.now()
|
||||
self.client._client = mock_client
|
||||
self.client.filter = "m/50"
|
||||
|
||||
with mock.patch.object(APRSISClient, "is_configured", return_value=True):
|
||||
stats = self.client.stats()
|
||||
from rich.console import Console
|
||||
|
||||
c = Console()
|
||||
c.print(stats)
|
||||
self.assertEqual(
|
||||
{
|
||||
"connected": True,
|
||||
"filter": "m/50",
|
||||
"login_status": {"message": mock.ANY, "success": True},
|
||||
"connection_keepalive": mock_client.aprsd_keepalive,
|
||||
"server_string": mock_client.server_string,
|
||||
"transport": "aprsis",
|
||||
},
|
||||
stats,
|
||||
)
|
||||
|
||||
def test_is_configured_missing_login(self):
|
||||
"""Test is_configured with missing login."""
|
||||
self.mock_conf.aprs_network.login = None
|
||||
with self.assertRaises(exception.MissingConfigOptionException):
|
||||
APRSISClient.is_configured()
|
||||
|
||||
def test_is_configured_missing_password(self):
|
||||
"""Test is_configured with missing password."""
|
||||
self.mock_conf.aprs_network.password = None
|
||||
with self.assertRaises(exception.MissingConfigOptionException):
|
||||
APRSISClient.is_configured()
|
||||
|
||||
def test_is_configured_missing_host(self):
|
||||
"""Test is_configured with missing host."""
|
||||
self.mock_conf.aprs_network.host = None
|
||||
with mock.patch("aprsd.client.aprsis.cfg.CONF", self.mock_conf):
|
||||
with self.assertRaises(exception.MissingConfigOptionException):
|
||||
APRSISClient.is_configured()
|
@ -1,141 +0,0 @@
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from aprsd.client.base import APRSClient
|
||||
from aprsd.packets import core
|
||||
|
||||
|
||||
class MockAPRSClient(APRSClient):
|
||||
"""Concrete implementation of APRSClient for testing."""
|
||||
|
||||
def stats(self):
|
||||
return {"packets_received": 0, "packets_sent": 0}
|
||||
|
||||
def setup_connection(self):
|
||||
mock_connection = mock.MagicMock()
|
||||
# Configure the mock with required methods
|
||||
mock_connection.close = mock.MagicMock()
|
||||
mock_connection.stop = mock.MagicMock()
|
||||
mock_connection.set_filter = mock.MagicMock()
|
||||
mock_connection.send = mock.MagicMock()
|
||||
self._client = mock_connection
|
||||
return mock_connection
|
||||
|
||||
def decode_packet(self, *args, **kwargs):
|
||||
return mock.MagicMock()
|
||||
|
||||
def consumer(self, callback, blocking=False, immortal=False, raw=False):
|
||||
pass
|
||||
|
||||
def is_alive(self):
|
||||
return True
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def transport():
|
||||
return "mock"
|
||||
|
||||
def reset(self):
|
||||
"""Mock implementation of reset."""
|
||||
if self._client:
|
||||
self._client.close()
|
||||
self._client = self.setup_connection()
|
||||
if self.filter:
|
||||
self._client.set_filter(self.filter)
|
||||
|
||||
|
||||
class TestAPRSClient(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Reset the singleton instance before each test
|
||||
APRSClient._instance = None
|
||||
APRSClient._client = None
|
||||
self.client = MockAPRSClient()
|
||||
|
||||
def test_singleton_pattern(self):
|
||||
"""Test that multiple instantiations return the same instance."""
|
||||
client1 = MockAPRSClient()
|
||||
client2 = MockAPRSClient()
|
||||
self.assertIs(client1, client2)
|
||||
|
||||
def test_set_filter(self):
|
||||
"""Test setting APRS filter."""
|
||||
# Get the existing mock client that was created in __init__
|
||||
mock_client = self.client._client
|
||||
|
||||
test_filter = "m/50"
|
||||
self.client.set_filter(test_filter)
|
||||
self.assertEqual(self.client.filter, test_filter)
|
||||
# The filter is set once during set_filter() and once during reset()
|
||||
mock_client.set_filter.assert_called_with(test_filter)
|
||||
|
||||
@mock.patch("aprsd.client.base.LOG")
|
||||
def test_reset(self, mock_log):
|
||||
"""Test client reset functionality."""
|
||||
# Create a new mock client with the necessary methods
|
||||
old_client = mock.MagicMock()
|
||||
self.client._client = old_client
|
||||
|
||||
self.client.reset()
|
||||
|
||||
# Verify the old client was closed
|
||||
old_client.close.assert_called_once()
|
||||
|
||||
# Verify a new client was created
|
||||
self.assertIsNotNone(self.client._client)
|
||||
self.assertNotEqual(old_client, self.client._client)
|
||||
|
||||
def test_send_packet(self):
|
||||
"""Test sending an APRS packet."""
|
||||
mock_packet = mock.Mock(spec=core.Packet)
|
||||
self.client.send(mock_packet)
|
||||
self.client._client.send.assert_called_once_with(mock_packet)
|
||||
|
||||
def test_stop(self):
|
||||
"""Test stopping the client."""
|
||||
# Ensure client is created first
|
||||
self.client._create_client()
|
||||
|
||||
self.client.stop()
|
||||
self.client._client.stop.assert_called_once()
|
||||
|
||||
@mock.patch("aprsd.client.base.LOG")
|
||||
def test_create_client_failure(self, mock_log):
|
||||
"""Test handling of client creation failure."""
|
||||
# Make setup_connection raise an exception
|
||||
with mock.patch.object(
|
||||
self.client,
|
||||
"setup_connection",
|
||||
side_effect=Exception("Connection failed"),
|
||||
):
|
||||
with self.assertRaises(Exception):
|
||||
self.client._create_client()
|
||||
|
||||
self.assertIsNone(self.client._client)
|
||||
mock_log.error.assert_called_once()
|
||||
|
||||
def test_client_property(self):
|
||||
"""Test the client property creates client if none exists."""
|
||||
self.client._client = None
|
||||
client = self.client.client
|
||||
self.assertIsNotNone(client)
|
||||
|
||||
def test_filter_applied_on_creation(self):
|
||||
"""Test that filter is applied when creating new client."""
|
||||
test_filter = "m/50"
|
||||
self.client.set_filter(test_filter)
|
||||
|
||||
# Force client recreation
|
||||
self.client.reset()
|
||||
|
||||
# Verify filter was applied to new client
|
||||
self.client._client.set_filter.assert_called_with(test_filter)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
@ -1,75 +0,0 @@
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from aprsd.client.factory import Client, ClientFactory
|
||||
|
||||
|
||||
class MockClient:
|
||||
"""Mock client for testing."""
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls):
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def is_configured(cls):
|
||||
return True
|
||||
|
||||
|
||||
class TestClientFactory(unittest.TestCase):
|
||||
"""Test cases for ClientFactory."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.factory = ClientFactory()
|
||||
# Clear any registered clients from previous tests
|
||||
self.factory.clients = []
|
||||
|
||||
def test_singleton(self):
|
||||
"""Test that ClientFactory is a singleton."""
|
||||
factory2 = ClientFactory()
|
||||
self.assertEqual(self.factory, factory2)
|
||||
|
||||
def test_register_client(self):
|
||||
"""Test registering a client."""
|
||||
self.factory.register(MockClient)
|
||||
self.assertIn(MockClient, self.factory.clients)
|
||||
|
||||
def test_register_invalid_client(self):
|
||||
"""Test registering an invalid client raises error."""
|
||||
invalid_client = mock.MagicMock(spec=Client)
|
||||
with self.assertRaises(ValueError):
|
||||
self.factory.register(invalid_client)
|
||||
|
||||
def test_create_client(self):
|
||||
"""Test creating a client."""
|
||||
self.factory.register(MockClient)
|
||||
client = self.factory.create()
|
||||
self.assertIsInstance(client, MockClient)
|
||||
|
||||
def test_create_no_clients(self):
|
||||
"""Test creating a client with no registered clients."""
|
||||
with self.assertRaises(Exception):
|
||||
self.factory.create()
|
||||
|
||||
def test_is_client_enabled(self):
|
||||
"""Test checking if any client is enabled."""
|
||||
self.factory.register(MockClient)
|
||||
self.assertTrue(self.factory.is_client_enabled())
|
||||
|
||||
def test_is_client_enabled_none(self):
|
||||
"""Test checking if any client is enabled when none are."""
|
||||
MockClient.is_enabled = classmethod(lambda cls: False)
|
||||
self.factory.register(MockClient)
|
||||
self.assertFalse(self.factory.is_client_enabled())
|
||||
|
||||
def test_is_client_configured(self):
|
||||
"""Test checking if any client is configured."""
|
||||
self.factory.register(MockClient)
|
||||
self.assertTrue(self.factory.is_client_configured())
|
||||
|
||||
def test_is_client_configured_none(self):
|
||||
"""Test checking if any client is configured when none are."""
|
||||
MockClient.is_configured = classmethod(lambda cls: False)
|
||||
self.factory.register(MockClient)
|
||||
self.assertFalse(self.factory.is_client_configured())
|
100
tests/client/test_registry.py
Normal file
100
tests/client/test_registry.py
Normal file
@ -0,0 +1,100 @@
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from aprsd.client.drivers.registry import DriverRegistry
|
||||
|
||||
from ..mock_client_driver import MockClientDriver
|
||||
|
||||
|
||||
class TestDriverRegistry(unittest.TestCase):
|
||||
"""Unit tests for the DriverRegistry class."""
|
||||
|
||||
def setUp(self):
|
||||
# Reset the singleton instance before each test
|
||||
DriverRegistry._singleton_instances = {}
|
||||
self.registry = DriverRegistry()
|
||||
self.registry.drivers = []
|
||||
|
||||
# Mock APRSISDriver completely
|
||||
self.aprsis_patcher = mock.patch('aprsd.client.drivers.aprsis.APRSISDriver')
|
||||
mock_aprsis_class = self.aprsis_patcher.start()
|
||||
mock_aprsis_class.is_enabled.return_value = False
|
||||
mock_aprsis_class.is_configured.return_value = False
|
||||
|
||||
# Mock the instance methods as well
|
||||
mock_instance = mock_aprsis_class.return_value
|
||||
mock_instance.is_enabled.return_value = False
|
||||
mock_instance.is_configured.return_value = False
|
||||
|
||||
# Mock CONF to prevent password check
|
||||
self.conf_patcher = mock.patch('aprsd.client.drivers.aprsis.CONF')
|
||||
mock_conf = self.conf_patcher.start()
|
||||
mock_conf.aprs_network.password = 'dummy'
|
||||
mock_conf.aprs_network.login = 'dummy'
|
||||
|
||||
def tearDown(self):
|
||||
# Reset the singleton instance after each test
|
||||
DriverRegistry().drivers = []
|
||||
self.aprsis_patcher.stop()
|
||||
self.conf_patcher.stop()
|
||||
|
||||
def test_get_driver_with_valid_driver(self):
|
||||
"""Test getting an enabled and configured driver."""
|
||||
# Add an enabled and configured driver
|
||||
driver = MockClientDriver
|
||||
driver.is_enabled = mock.MagicMock(return_value=True)
|
||||
driver.is_configured = mock.MagicMock(return_value=True)
|
||||
self.registry.register(MockClientDriver)
|
||||
|
||||
# Get the driver
|
||||
result = self.registry.get_driver()
|
||||
print(result)
|
||||
self.assertTrue(isinstance(result, MockClientDriver))
|
||||
|
||||
def test_get_driver_with_disabled_driver(self):
|
||||
"""Test getting a driver when only disabled drivers exist."""
|
||||
driver = MockClientDriver
|
||||
driver.is_enabled = mock.MagicMock(return_value=False)
|
||||
driver.is_configured = mock.MagicMock(return_value=False)
|
||||
self.registry.register(driver)
|
||||
|
||||
with self.assertRaises(ValueError) as context:
|
||||
self.registry.get_driver()
|
||||
self.assertIn('No enabled driver found', str(context.exception))
|
||||
|
||||
def test_get_driver_with_unconfigured_driver(self):
|
||||
"""Test getting a driver when only unconfigured drivers exist."""
|
||||
driver = MockClientDriver
|
||||
driver.is_enabled = mock.MagicMock(return_value=True)
|
||||
driver.is_configured = mock.MagicMock(return_value=False)
|
||||
self.registry.register(driver)
|
||||
|
||||
with self.assertRaises(ValueError) as context:
|
||||
self.registry.get_driver()
|
||||
self.assertIn('No enabled driver found', str(context.exception))
|
||||
|
||||
def test_get_driver_with_no_drivers(self):
|
||||
"""Test getting a driver when no drivers exist."""
|
||||
# Try to get a driver
|
||||
with self.assertRaises(ValueError) as context:
|
||||
self.registry.get_driver()
|
||||
self.assertIn('No enabled driver found', str(context.exception))
|
||||
|
||||
def test_get_driver_with_multiple_drivers(self):
|
||||
"""Test getting a driver when multiple valid drivers exist."""
|
||||
# Add multiple drivers
|
||||
driver1 = MockClientDriver
|
||||
driver1.is_enabled = mock.MagicMock(return_value=True)
|
||||
driver1.is_configured = mock.MagicMock(return_value=True)
|
||||
driver2 = MockClientDriver
|
||||
self.registry.register(driver1)
|
||||
self.registry.register(driver2)
|
||||
|
||||
# Get the driver - should return the first one
|
||||
result = self.registry.get_driver()
|
||||
# We can only check that it's a MockDriver instance
|
||||
self.assertTrue(isinstance(result, MockClientDriver))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
76
tests/mock_client_driver.py
Normal file
76
tests/mock_client_driver.py
Normal file
@ -0,0 +1,76 @@
|
||||
from unittest import mock
|
||||
|
||||
from aprsd.packets import core
|
||||
|
||||
|
||||
class MockClientDriver:
|
||||
"""Mock implementation of ClientDriver for testing."""
|
||||
|
||||
def __init__(self, enabled=True, configured=True):
|
||||
self.connected = False
|
||||
self._alive = True
|
||||
self._keepalive = None
|
||||
self.filter = None
|
||||
self._enabled = enabled
|
||||
self._configured = configured
|
||||
self.path = '/dev/ttyUSB0'
|
||||
self.login_status = {
|
||||
'success': True,
|
||||
'message': None,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def is_enabled():
|
||||
"""Static method to check if driver is enabled."""
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def is_configured():
|
||||
"""Static method to check if driver is configured."""
|
||||
return True
|
||||
|
||||
def is_alive(self):
|
||||
"""Instance method to check if driver is alive."""
|
||||
return self._alive
|
||||
|
||||
def stats(self, serializable=False):
|
||||
"""Return mock stats."""
|
||||
stats = {'packets_received': 0, 'packets_sent': 0}
|
||||
if serializable:
|
||||
stats['path'] = self.path
|
||||
return stats
|
||||
|
||||
@property
|
||||
def login_success(self):
|
||||
"""Property to get login success status."""
|
||||
return self.login_status['success']
|
||||
|
||||
@property
|
||||
def login_failure(self):
|
||||
"""Property to get login failure message."""
|
||||
return self.login_status['message']
|
||||
|
||||
def decode_packet(self, *args, **kwargs):
|
||||
"""Mock packet decoding."""
|
||||
packet = mock.MagicMock(spec=core.Packet)
|
||||
packet.raw = 'test packet'
|
||||
return packet
|
||||
|
||||
def close(self):
|
||||
self.connected = False
|
||||
|
||||
def setup_connection(self):
|
||||
self.connected = True
|
||||
|
||||
def send(self, packet):
|
||||
return True
|
||||
|
||||
def set_filter(self, filter_str):
|
||||
self.filter = filter_str
|
||||
|
||||
@property
|
||||
def keepalive(self):
|
||||
return self._keepalive
|
||||
|
||||
def consumer(self, callback, raw=False):
|
||||
pass
|
@ -7,9 +7,11 @@ from aprsd import ( # noqa: F401
|
||||
conf,
|
||||
packets,
|
||||
)
|
||||
from aprsd.client.drivers.registry import DriverRegistry
|
||||
from aprsd.plugins import notify as notify_plugin
|
||||
|
||||
from .. import fake, test_plugin
|
||||
from ..mock_client_driver import MockClientDriver
|
||||
|
||||
CONF = cfg.CONF
|
||||
DEFAULT_WATCHLIST_CALLSIGNS = fake.FAKE_FROM_CALLSIGN
|
||||
@ -17,9 +19,24 @@ DEFAULT_WATCHLIST_CALLSIGNS = fake.FAKE_FROM_CALLSIGN
|
||||
|
||||
class TestWatchListPlugin(test_plugin.TestPlugin):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.fromcall = fake.FAKE_FROM_CALLSIGN
|
||||
self.ack = 1
|
||||
|
||||
# Mock APRSISDriver
|
||||
self.aprsis_patcher = mock.patch('aprsd.client.drivers.aprsis.APRSISDriver')
|
||||
self.mock_aprsis = self.aprsis_patcher.start()
|
||||
self.mock_aprsis.is_enabled.return_value = False
|
||||
self.mock_aprsis.is_configured.return_value = False
|
||||
|
||||
# Register the mock driver
|
||||
DriverRegistry().register(MockClientDriver)
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
if hasattr(self, 'aprsis_patcher'):
|
||||
self.aprsis_patcher.stop()
|
||||
|
||||
def config_and_init(
|
||||
self,
|
||||
watchlist_enabled=True,
|
||||
@ -30,7 +47,9 @@ class TestWatchListPlugin(test_plugin.TestPlugin):
|
||||
):
|
||||
CONF.callsign = self.fromcall
|
||||
CONF.aprs_network.login = self.fromcall
|
||||
CONF.aprs_fi.apiKey = "something"
|
||||
CONF.aprs_fi.apiKey = 'something'
|
||||
# Add mock password
|
||||
CONF.aprs_network.password = '12345'
|
||||
|
||||
# Set the watchlist specific config options
|
||||
CONF.watch_list.enabled = watchlist_enabled
|
||||
@ -56,22 +75,20 @@ class TestAPRSDWatchListPluginBase(TestWatchListPlugin):
|
||||
plugin = fake.FakeWatchListPlugin()
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="version",
|
||||
message='version',
|
||||
msg_number=1,
|
||||
)
|
||||
actual = plugin.filter(packet)
|
||||
expected = packets.NULL_MESSAGE
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("aprsd.client.factory.ClientFactory", autospec=True)
|
||||
def test_watchlist_not_in_watchlist(self, mock_factory):
|
||||
client.client_factory = mock_factory
|
||||
def test_watchlist_not_in_watchlist(self):
|
||||
self.config_and_init()
|
||||
plugin = fake.FakeWatchListPlugin()
|
||||
|
||||
packet = fake.fake_packet(
|
||||
fromcall="FAKE",
|
||||
message="version",
|
||||
fromcall='FAKE',
|
||||
message='version',
|
||||
msg_number=1,
|
||||
)
|
||||
actual = plugin.filter(packet)
|
||||
@ -85,87 +102,77 @@ class TestNotifySeenPlugin(TestWatchListPlugin):
|
||||
plugin = notify_plugin.NotifySeenPlugin()
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="version",
|
||||
message='version',
|
||||
msg_number=1,
|
||||
)
|
||||
actual = plugin.filter(packet)
|
||||
expected = packets.NULL_MESSAGE
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("aprsd.client.factory.ClientFactory", autospec=True)
|
||||
def test_callsign_not_in_watchlist(self, mock_factory):
|
||||
client.client_factory = mock_factory
|
||||
def test_callsign_not_in_watchlist(self):
|
||||
self.config_and_init(watchlist_enabled=False)
|
||||
plugin = notify_plugin.NotifySeenPlugin()
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="version",
|
||||
message='version',
|
||||
msg_number=1,
|
||||
)
|
||||
actual = plugin.filter(packet)
|
||||
expected = packets.NULL_MESSAGE
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("aprsd.client.factory.ClientFactory", autospec=True)
|
||||
@mock.patch("aprsd.packets.WatchList.is_old")
|
||||
def test_callsign_in_watchlist_not_old(self, mock_is_old, mock_factory):
|
||||
client.client_factory = mock_factory
|
||||
@mock.patch('aprsd.packets.WatchList.is_old')
|
||||
def test_callsign_in_watchlist_not_old(self, mock_is_old):
|
||||
mock_is_old.return_value = False
|
||||
self.config_and_init(
|
||||
watchlist_enabled=True,
|
||||
watchlist_callsigns=["WB4BOR"],
|
||||
watchlist_callsigns=['WB4BOR'],
|
||||
)
|
||||
plugin = notify_plugin.NotifySeenPlugin()
|
||||
|
||||
packet = fake.fake_packet(
|
||||
fromcall="WB4BOR",
|
||||
message="ping",
|
||||
fromcall='WB4BOR',
|
||||
message='ping',
|
||||
msg_number=1,
|
||||
)
|
||||
actual = plugin.filter(packet)
|
||||
expected = packets.NULL_MESSAGE
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("aprsd.client.factory.ClientFactory", autospec=True)
|
||||
@mock.patch("aprsd.packets.WatchList.is_old")
|
||||
def test_callsign_in_watchlist_old_same_alert_callsign(
|
||||
self, mock_is_old, mock_factory
|
||||
):
|
||||
client.client_factory = mock_factory
|
||||
@mock.patch('aprsd.packets.WatchList.is_old')
|
||||
def test_callsign_in_watchlist_old_same_alert_callsign(self, mock_is_old):
|
||||
mock_is_old.return_value = True
|
||||
self.config_and_init(
|
||||
watchlist_enabled=True,
|
||||
watchlist_alert_callsign="WB4BOR",
|
||||
watchlist_callsigns=["WB4BOR"],
|
||||
watchlist_alert_callsign='WB4BOR',
|
||||
watchlist_callsigns=['WB4BOR'],
|
||||
)
|
||||
plugin = notify_plugin.NotifySeenPlugin()
|
||||
|
||||
packet = fake.fake_packet(
|
||||
fromcall="WB4BOR",
|
||||
message="ping",
|
||||
fromcall='WB4BOR',
|
||||
message='ping',
|
||||
msg_number=1,
|
||||
)
|
||||
actual = plugin.filter(packet)
|
||||
expected = packets.NULL_MESSAGE
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@mock.patch("aprsd.client.factory.ClientFactory", autospec=True)
|
||||
@mock.patch("aprsd.packets.WatchList.is_old")
|
||||
def test_callsign_in_watchlist_old_send_alert(self, mock_is_old, mock_factory):
|
||||
client.client_factory = mock_factory
|
||||
@mock.patch('aprsd.packets.WatchList.is_old')
|
||||
def test_callsign_in_watchlist_old_send_alert(self, mock_is_old):
|
||||
mock_is_old.return_value = True
|
||||
notify_callsign = fake.FAKE_TO_CALLSIGN
|
||||
fromcall = "WB4BOR"
|
||||
fromcall = 'WB4BOR'
|
||||
self.config_and_init(
|
||||
watchlist_enabled=True,
|
||||
watchlist_alert_callsign=notify_callsign,
|
||||
watchlist_callsigns=["WB4BOR"],
|
||||
watchlist_callsigns=['WB4BOR'],
|
||||
)
|
||||
plugin = notify_plugin.NotifySeenPlugin()
|
||||
|
||||
packet = fake.fake_packet(
|
||||
fromcall=fromcall,
|
||||
message="ping",
|
||||
message='ping',
|
||||
msg_number=1,
|
||||
)
|
||||
packet_type = packet.__class__.__name__
|
||||
|
@ -3,6 +3,7 @@ from unittest import mock
|
||||
from oslo_config import cfg
|
||||
|
||||
import aprsd
|
||||
from aprsd.client.drivers.fake import APRSDFakeDriver
|
||||
from aprsd.plugins import version as version_plugin
|
||||
|
||||
from .. import fake, test_plugin
|
||||
@ -11,16 +12,41 @@ CONF = cfg.CONF
|
||||
|
||||
|
||||
class TestVersionPlugin(test_plugin.TestPlugin):
|
||||
@mock.patch("aprsd.stats.app.APRSDStats.uptime")
|
||||
def test_version(self, mock_stats):
|
||||
mock_stats.return_value = "00:00:00"
|
||||
expected = f"APRSD ver:{aprsd.__version__} uptime:00:00:00"
|
||||
def setUp(self):
|
||||
# make sure the fake client driver is enabled
|
||||
# Mock CONF for testing
|
||||
super().setUp()
|
||||
self.conf_patcher = mock.patch('aprsd.client.drivers.fake.CONF')
|
||||
self.mock_conf = self.conf_patcher.start()
|
||||
|
||||
# Configure fake_client.enabled
|
||||
self.mock_conf.fake_client.enabled = True
|
||||
|
||||
# Create an instance of the driver
|
||||
self.driver = APRSDFakeDriver()
|
||||
self.fromcall = fake.FAKE_FROM_CALLSIGN
|
||||
|
||||
def tearDown(self):
|
||||
self.conf_patcher.stop()
|
||||
super().tearDown()
|
||||
|
||||
@mock.patch('aprsd.stats.collector.Collector')
|
||||
def test_version(self, mock_collector_class):
|
||||
# Set up the mock collector instance
|
||||
mock_collector_instance = mock_collector_class.return_value
|
||||
mock_collector_instance.collect.return_value = {
|
||||
'APRSDStats': {
|
||||
'uptime': '00:00:00',
|
||||
}
|
||||
}
|
||||
|
||||
expected = f'APRSD ver:{aprsd.__version__} uptime:00:00:00'
|
||||
CONF.callsign = fake.FAKE_TO_CALLSIGN
|
||||
version = version_plugin.VersionPlugin()
|
||||
version.enabled = True
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="No",
|
||||
message='No',
|
||||
msg_number=1,
|
||||
)
|
||||
|
||||
@ -28,8 +54,11 @@ class TestVersionPlugin(test_plugin.TestPlugin):
|
||||
self.assertEqual(None, actual)
|
||||
|
||||
packet = fake.fake_packet(
|
||||
message="version",
|
||||
message='version',
|
||||
msg_number=1,
|
||||
)
|
||||
actual = version.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
# Verify the mock was called exactly once
|
||||
mock_collector_instance.collect.assert_called_once()
|
||||
|
@ -9,9 +9,11 @@ from aprsd import ( # noqa: F401
|
||||
plugins,
|
||||
)
|
||||
from aprsd import plugin as aprsd_plugin
|
||||
from aprsd.client.drivers.registry import DriverRegistry
|
||||
from aprsd.packets import core
|
||||
|
||||
from . import fake
|
||||
from .mock_client_driver import MockClientDriver
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
@ -21,15 +23,24 @@ class TestPluginManager(unittest.TestCase):
|
||||
self.fromcall = fake.FAKE_FROM_CALLSIGN
|
||||
self.config_and_init()
|
||||
|
||||
self.mock_driver = MockClientDriver()
|
||||
# Mock the DriverRegistry to return our mock driver
|
||||
self.registry_patcher = mock.patch.object(
|
||||
DriverRegistry, 'get_driver', return_value=self.mock_driver
|
||||
)
|
||||
self.mock_registry = self.registry_patcher.start()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.config = None
|
||||
aprsd_plugin.PluginManager._instance = None
|
||||
self.registry_patcher.stop()
|
||||
self.mock_registry.stop()
|
||||
|
||||
def config_and_init(self):
|
||||
CONF.callsign = self.fromcall
|
||||
CONF.aprs_network.login = fake.FAKE_TO_CALLSIGN
|
||||
CONF.aprs_fi.apiKey = "something"
|
||||
CONF.enabled_plugins = "aprsd.plugins.ping.PingPlugin"
|
||||
CONF.aprs_fi.apiKey = 'something'
|
||||
CONF.enabled_plugins = 'aprsd.plugins.ping.PingPlugin'
|
||||
CONF.enable_save = False
|
||||
|
||||
def test_get_plugins_no_plugins(self):
|
||||
@ -39,7 +50,7 @@ class TestPluginManager(unittest.TestCase):
|
||||
self.assertEqual([], plugin_list)
|
||||
|
||||
def test_get_plugins_with_plugins(self):
|
||||
CONF.enabled_plugins = ["aprsd.plugins.ping.PingPlugin"]
|
||||
CONF.enabled_plugins = ['aprsd.plugins.ping.PingPlugin']
|
||||
pm = aprsd_plugin.PluginManager()
|
||||
plugin_list = pm.get_plugins()
|
||||
self.assertEqual([], plugin_list)
|
||||
@ -64,7 +75,7 @@ class TestPluginManager(unittest.TestCase):
|
||||
self.assertEqual(0, len(plugin_list))
|
||||
|
||||
def test_get_message_plugins(self):
|
||||
CONF.enabled_plugins = ["aprsd.plugins.ping.PingPlugin"]
|
||||
CONF.enabled_plugins = ['aprsd.plugins.ping.PingPlugin']
|
||||
pm = aprsd_plugin.PluginManager()
|
||||
plugin_list = pm.get_plugins()
|
||||
self.assertEqual([], plugin_list)
|
||||
@ -87,22 +98,31 @@ class TestPlugin(unittest.TestCase):
|
||||
self.ack = 1
|
||||
self.config_and_init()
|
||||
|
||||
self.mock_driver = MockClientDriver()
|
||||
# Mock the DriverRegistry to return our mock driver
|
||||
self.registry_patcher = mock.patch.object(
|
||||
DriverRegistry, 'get_driver', return_value=self.mock_driver
|
||||
)
|
||||
self.mock_registry = self.registry_patcher.start()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
packets.WatchList._instance = None
|
||||
packets.SeenList._instance = None
|
||||
packets.PacketTrack._instance = None
|
||||
self.config = None
|
||||
self.registry_patcher.stop()
|
||||
self.mock_registry.stop()
|
||||
|
||||
def config_and_init(self):
|
||||
CONF.callsign = self.fromcall
|
||||
CONF.aprs_network.login = fake.FAKE_TO_CALLSIGN
|
||||
CONF.aprs_fi.apiKey = "something"
|
||||
CONF.enabled_plugins = "aprsd.plugins.ping.PingPlugin"
|
||||
CONF.aprs_fi.apiKey = 'something'
|
||||
CONF.enabled_plugins = 'aprsd.plugins.ping.PingPlugin'
|
||||
CONF.enable_save = False
|
||||
|
||||
|
||||
class TestPluginBase(TestPlugin):
|
||||
@mock.patch.object(fake.FakeBaseNoThreadsPlugin, "process")
|
||||
@mock.patch.object(fake.FakeBaseNoThreadsPlugin, 'process')
|
||||
def test_base_plugin_no_threads(self, mock_process):
|
||||
p = fake.FakeBaseNoThreadsPlugin()
|
||||
|
||||
@ -110,7 +130,7 @@ class TestPluginBase(TestPlugin):
|
||||
actual = p.create_threads()
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
expected = "1.0"
|
||||
expected = '1.0'
|
||||
actual = p.version
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@ -123,7 +143,7 @@ class TestPluginBase(TestPlugin):
|
||||
self.assertEqual(expected, actual)
|
||||
mock_process.assert_not_called()
|
||||
|
||||
@mock.patch.object(fake.FakeBaseThreadsPlugin, "create_threads")
|
||||
@mock.patch.object(fake.FakeBaseThreadsPlugin, 'create_threads')
|
||||
def test_base_plugin_threads_created(self, mock_create):
|
||||
p = fake.FakeBaseThreadsPlugin()
|
||||
mock_create.assert_called_once()
|
||||
@ -135,17 +155,17 @@ class TestPluginBase(TestPlugin):
|
||||
self.assertTrue(isinstance(actual, fake.FakeThread))
|
||||
p.stop_threads()
|
||||
|
||||
@mock.patch.object(fake.FakeRegexCommandPlugin, "process")
|
||||
@mock.patch.object(fake.FakeRegexCommandPlugin, 'process')
|
||||
def test_regex_base_not_called(self, mock_process):
|
||||
CONF.callsign = fake.FAKE_TO_CALLSIGN
|
||||
p = fake.FakeRegexCommandPlugin()
|
||||
packet = fake.fake_packet(message="a")
|
||||
packet = fake.fake_packet(message='a')
|
||||
expected = None
|
||||
actual = p.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
mock_process.assert_not_called()
|
||||
|
||||
packet = fake.fake_packet(tocall="notMe", message="f")
|
||||
packet = fake.fake_packet(tocall='notMe', message='f')
|
||||
expected = None
|
||||
actual = p.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
@ -165,11 +185,11 @@ class TestPluginBase(TestPlugin):
|
||||
self.assertEqual(expected, actual)
|
||||
mock_process.assert_not_called()
|
||||
|
||||
@mock.patch.object(fake.FakeRegexCommandPlugin, "process")
|
||||
@mock.patch.object(fake.FakeRegexCommandPlugin, 'process')
|
||||
def test_regex_base_assert_called(self, mock_process):
|
||||
CONF.callsign = fake.FAKE_TO_CALLSIGN
|
||||
p = fake.FakeRegexCommandPlugin()
|
||||
packet = fake.fake_packet(message="f")
|
||||
packet = fake.fake_packet(message='f')
|
||||
p.filter(packet)
|
||||
mock_process.assert_called_once()
|
||||
|
||||
@ -177,22 +197,22 @@ class TestPluginBase(TestPlugin):
|
||||
CONF.callsign = fake.FAKE_TO_CALLSIGN
|
||||
p = fake.FakeRegexCommandPlugin()
|
||||
|
||||
packet = fake.fake_packet(message="f")
|
||||
packet = fake.fake_packet(message='f')
|
||||
expected = fake.FAKE_MESSAGE_TEXT
|
||||
actual = p.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
packet = fake.fake_packet(message="F")
|
||||
packet = fake.fake_packet(message='F')
|
||||
expected = fake.FAKE_MESSAGE_TEXT
|
||||
actual = p.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
packet = fake.fake_packet(message="fake")
|
||||
packet = fake.fake_packet(message='fake')
|
||||
expected = fake.FAKE_MESSAGE_TEXT
|
||||
actual = p.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
packet = fake.fake_packet(message="FAKE")
|
||||
packet = fake.fake_packet(message='FAKE')
|
||||
expected = fake.FAKE_MESSAGE_TEXT
|
||||
actual = p.filter(packet)
|
||||
self.assertEqual(expected, actual)
|
||||
|
Loading…
Reference in New Issue
Block a user