1
0
mirror of https://github.com/craigerl/aprsd.git synced 2025-04-19 09:49:01 -04:00
This commit is contained in:
Walter A. Boring IV 2025-04-15 23:48:11 +00:00 committed by GitHub
commit bd0bf49f8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 2841 additions and 1664 deletions

View File

@ -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'

View File

@ -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

View File

@ -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
View 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)

View File

@ -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)

View File

@ -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

View File

@ -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',
}

View File

@ -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)

View File

View 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

View 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')

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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!

View File

@ -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')

View File

@ -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!

View File

@ -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()

View File

@ -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(

View File

@ -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:

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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
View File

View File

View 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()

View 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()

View 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()

View File

@ -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()

View File

@ -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()

View File

@ -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())

View 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()

View 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

View File

@ -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__

View File

@ -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()

View File

@ -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)

View File

@ -25,7 +25,7 @@ deps =
pytest-cov
pytest
commands =
pytest -v --cov-report term-missing --cov=aprsd {posargs}
pytest -s -v --cov-report term-missing --cov=aprsd {posargs}
coverage: coverage report -m
coverage: coverage xml