1
0
mirror of https://github.com/craigerl/aprsd.git synced 2026-08-16 16:43:52 -04:00

Compare commits

...

22 Commits

Author SHA1 Message Date
hemna 6dba56f74d Changelog for 4.1.2 2025-03-06 17:11:57 -05:00
hemna d262589313 Allow passing in a custom handler to setup_logging
This adds the ability to pass in a custom log handler during
startup in the log.setup_logging().
2025-03-06 17:09:40 -05:00
hemna 7ed8028307 4.1.1 release 2025-03-05 17:00:12 -05:00
hemna 94ba915ed4 Fixed some more ruff checks
this updates some code to fix any more 'ruff check' failures.
2025-03-05 16:48:18 -05:00
hemna 2b185ee1b8 Update requirements
This removes the hard coded requirement for the old rich lib.
2025-03-04 14:01:27 -05:00
hemna c1c89fd2c2 Added threads.service
This is just a handy wrapper to inject all the threads a service wants
to start and then start them all at once, and then join them all.
2025-02-28 17:16:53 -05:00
hemna 0fa5b07d4b Added new config to disable logging to console
This patch adds a new config setting to the logging
section that allows the user to disable logging to stdout.
This is useful for terminal UI apps :)
2025-02-26 17:41:40 -05:00
hemna a3cda9f37d Update Changelog for 4.1.0 release 2025-02-20 15:15:31 -05:00
hemna 06bdb34642 CONF.logging.enable_color option added
This allows the user to turn off ANSI color output for
logging to the console.
2025-02-20 12:44:16 -05:00
hemna 4fd64a3c25 Merge pull request #184 from craigerl/packet_filtering
Added new PacketFilter mechanism
2025-02-19 16:45:10 -05:00
hemna 361663e7d2 Changed Objectstore log to debug
This should help silence the log a bit.
2025-02-19 16:38:48 -05:00
hemna fd517b3218 updated gitignore 2025-02-19 16:38:48 -05:00
hemna e9e7e6b59f Fixed some pep8 failures. 2025-02-19 16:38:47 -05:00
hemna 52dac7e0a0 Added new PacketFilter mechanism
This patch adds the new PacketFilter class as a generic mechanism
for doing packet filtering during the packet processing phase of
recieving packets.

The packet phases are:
1. reception and stats collection
2. packet processing.

Each phase has a single thread for handling that phase.

Phase 1:
The ARPSDRXThread connects to the APRS client, and gets packets
from the client.  Then it puts the packet through the Collector
for stats and tracking.  Then the packet is put into the packet_queue.

Phase 2:
Packets are pulled from the packet_queue.  Then packets are run
through the PacketFilter mechanism, then processed depending
on the command being run.
By default there is 1 loaded packet filter, which is the
DupePacketFilter which removes "duplicate" packets that aprsd has
already seen and processed within the configured time frame.

This PacketFilter mechanism allows an external extension or plugin
to add/remove packet filters at will depending on the function
of the extension or plugin.   For example, this allows an extension
to get a packet and push the packet into an MQTT queue.
2025-02-19 16:38:47 -05:00
hemna b6da0ebb0d Fix runaway KISS driver on failed connnection
This patch fixes an issue when the KISS connection fails to start
and or goes away during the lifetime of the active connection.
Aprsd would runaway in a tight loop eating 100% cpu.  We now detect
when the underlying asyncio connection has failed and raise, which
induces a sleep in the consumer to try again.
2025-02-15 18:55:58 -05:00
hemna d82a81a2c3 fix for None packet in rx thread
This patch updates the process_packet to ensure when we
try to decode a packet we actually get one.
2025-02-14 11:06:19 -05:00
hemna 6cd7e99713 Remove sleep in main RX thread
We had a bottleneck of pulling down packets as fast as possible,
which was caused by the time.sleep(1) call in the main RX
thread that used to be needed.
2025-02-03 13:27:57 -08:00
hemna 101904ca77 Try and stop chardet logging!
This patch sets settings on the logger to hopefully
stop any chardet logs from leaking into the aprsd logs.
2025-01-30 10:16:09 -08:00
hemna 227ddbf148 Update StatsStore to use existing lock
The base class for StatsStore already creates a lock, use that instead.
2025-01-30 10:07:28 -08:00
hemna 19c12e70f3 Updated packet_list to allow infinit max store
This patch adds logic of setting packet_list_stats_maxlen -1
meaning keep every packet for stats.
2025-01-30 10:04:59 -08:00
hemna 1606585d41 Updated APRSIS driver
This patch adds an override to _connect to set some more
socket keepalive options.
2025-01-30 10:03:14 -08:00
hemna 3b57e7597d Update to build from pypi 2025-01-25 13:41:00 -05:00
33 changed files with 1136 additions and 785 deletions
+1 -1
View File
@@ -41,8 +41,8 @@ jobs:
platforms: linux/amd64,linux/arm64
file: ./Dockerfile
build-args: |
INSTALL_TYPE=pypi
VERSION=${{ inputs.aprsd_version }}
BRANCH=${{ inputs.aprsd_version }}
BUILDX_QEMU_ENV=true
push: true
tags: |
+6
View File
@@ -60,3 +60,9 @@ AUTHORS
Makefile.venv
# Copilot
.DS_Store
.python-version
.fleet
.vscode
.envrc
.doit.db
+59 -17
View File
@@ -4,6 +4,41 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [4.1.2](https://github.com/craigerl/aprsd/compare/4.1.1...4.1.2)
> 6 March 2025
- Allow passing in a custom handler to setup_logging [`d262589`](https://github.com/craigerl/aprsd/commit/d2625893134f498748859da3b1684b04d456f790)
#### [4.1.1](https://github.com/craigerl/aprsd/compare/4.1.0...4.1.1)
> 5 March 2025
- Added new config to disable logging to console [`0fa5b07`](https://github.com/craigerl/aprsd/commit/0fa5b07d4bf4bc5d5aaad1de52b78058e472fe24)
- Added threads.service [`c1c89fd`](https://github.com/craigerl/aprsd/commit/c1c89fd2c2c69c5e6c5d29a736a7b89e3d45cfe2)
- Update requirements [`2b185ee`](https://github.com/craigerl/aprsd/commit/2b185ee1b84598c832d8a5d73753cb428854b932)
- Fixed some more ruff checks [`94ba915`](https://github.com/craigerl/aprsd/commit/94ba915ed44b11eaabc885e033669d67d8c341a5)
- 4.1.1 release [`7ed8028`](https://github.com/craigerl/aprsd/commit/7ed80283071c1ccebf1e3373727608edd0a56ee9)
#### [4.1.0](https://github.com/craigerl/aprsd/compare/4.0.2...4.1.0)
> 20 February 2025
- Added new PacketFilter mechanism [`#184`](https://github.com/craigerl/aprsd/pull/184)
- Update to build from pypi [`3b57e75`](https://github.com/craigerl/aprsd/commit/3b57e7597d77303ffc03b082370283bb2fea2838)
- Updated APRSIS driver [`1606585`](https://github.com/craigerl/aprsd/commit/1606585d41f69133192199d139b53344bb320fa9)
- Updated packet_list to allow infinit max store [`19c12e7`](https://github.com/craigerl/aprsd/commit/19c12e70f30a6f1f7d223a2f0fd3bf1182579fa4)
- Update StatsStore to use existing lock [`227ddbf`](https://github.com/craigerl/aprsd/commit/227ddbf148be2e14d4b4f27e48a4b091a98f15df)
- Try and stop chardet logging! [`101904c`](https://github.com/craigerl/aprsd/commit/101904ca77d816ae9e70bc7d22e6d8516fc3c5ce)
- Fixed some pep8 failures. [`e9e7e6b`](https://github.com/craigerl/aprsd/commit/e9e7e6b59f9f93f3f09142e56407bc87603a44cb)
- updated gitignore [`fd517b3`](https://github.com/craigerl/aprsd/commit/fd517b32188fdf15835a74fbd515ce417e7ef1f5)
- Remove sleep in main RX thread [`6cd7e99`](https://github.com/craigerl/aprsd/commit/6cd7e997139e8f2687bee753d9e0d2b22b1c42a3)
- Changed Objectstore log to debug [`361663e`](https://github.com/craigerl/aprsd/commit/361663e7d2cf43bd2fd53da0d8c5205bb848dbc2)
- fix for None packet in rx thread [`d82a81a`](https://github.com/craigerl/aprsd/commit/d82a81a2c3c1a7f50177a0a6435a555daeb858aa)
- Fix runaway KISS driver on failed connnection [`b6da0eb`](https://github.com/craigerl/aprsd/commit/b6da0ebb0d2f4d7078dbbf91d8c03715412d89ea)
- CONF.logging.enable_color option added [`06bdb34`](https://github.com/craigerl/aprsd/commit/06bdb34642640d91ea96e3c6e8d8b5a4b8230611)
- Update Changelog for 4.1.0 release [`a3cda9f`](https://github.com/craigerl/aprsd/commit/a3cda9f37d4c9b955b523f46b2eb8cf412a84407)
#### [4.0.2](https://github.com/craigerl/aprsd/compare/4.0.1...4.0.2)
> 25 January 2025
@@ -16,6 +51,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Added uv.lock [`2f26eb8`](https://github.com/craigerl/aprsd/commit/2f26eb86f44625547f72f7c3612494b1bc44bc99)
- Fix the testing of fortune path [`3c4e200`](https://github.com/craigerl/aprsd/commit/3c4e200d700c24125479bb754b5f68bdf35b85a6)
- update the install from github in Dockerfile [`bea4815`](https://github.com/craigerl/aprsd/commit/bea481555bc1270ab371a22c69973d648e526d54)
- Prep for 4.0.2 [`000adef`](https://github.com/craigerl/aprsd/commit/000adef6d4f2792d33980d59d37f4b139e0c693c)
#### [4.0.1](https://github.com/craigerl/aprsd/compare/4.0.0...4.0.1)
@@ -24,11 +60,33 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Update pyproject for README.rst -> md [`e080394`](https://github.com/craigerl/aprsd/commit/e08039431ebde92a162ab422c05391dc55d3d3fa)
- Updated Changelog [`24f5672`](https://github.com/craigerl/aprsd/commit/24f567224cf8ecdebd51f49804425565883acb94)
### [4.0.0](https://github.com/craigerl/aprsd/compare/3.4.4...4.0.0)
### [4.0.0](https://github.com/craigerl/aprsd/compare/3.5.0...4.0.0)
> 24 January 2025
- Migrate admin web out of aprsd. [`#183`](https://github.com/craigerl/aprsd/pull/183)
- Enable packet stats for listen command in Docker [`e5d8796`](https://github.com/craigerl/aprsd/commit/e5d8796cda1a007aa868c760b96b50b364351519)
- Added activity to README [`cdd297c`](https://github.com/craigerl/aprsd/commit/cdd297c5bbc8b93f4739f5850a3e5971ce8baeba)
- Added star history to readme [`02e2940`](https://github.com/craigerl/aprsd/commit/02e29405ce2f8310e4f87f68498dfd6575c2e43b)
- removed pytest from README [`1cba31f`](https://github.com/craigerl/aprsd/commit/1cba31f0ac9bd5ee532721a909fc752f023f3b06)
- Updated Docker for using alpine and uv [`24db814`](https://github.com/craigerl/aprsd/commit/24db814c82c9bb6634566d7428603bf7a9ae37d1)
- Update the admin and setup.sh for container [`044ea4c`](https://github.com/craigerl/aprsd/commit/044ea4cc9a0059101851d6e722e986ee236833e8)
- added healthcheck.sh [`1054999`](https://github.com/craigerl/aprsd/commit/10549995686b08e4c166f780efdec5bdae496cab)
- updated healthcheck.sh [`dabb48c`](https://github.com/craigerl/aprsd/commit/dabb48c6f64062c1fed8f83a4f0b8ffba0c206a5)
- try making image for webchat [`ba8acdc`](https://github.com/craigerl/aprsd/commit/ba8acdc5849fc7b2d8a1ee11af6f5e317cf30f45)
- Added APRSD logo [`0ed648f`](https://github.com/craigerl/aprsd/commit/0ed648f8f8a961dbbd9e22bcebadcde525ee41ae)
- Added plugin and extension links [`447451c`](https://github.com/craigerl/aprsd/commit/447451c6c97e1f2d3d0bf580db21ecd176690258)
- reduced logo size 50% [`cf4a29f`](https://github.com/craigerl/aprsd/commit/cf4a29f0cb3ed366b21ec3120a189614e0955180)
- Updated README.md TOC [`375a5e5`](https://github.com/craigerl/aprsd/commit/375a5e5b34718cadc6ee8a51484fc91441440a61)
- chore: update AUTHORS [skip ci] [`c556f51`](https://github.com/craigerl/aprsd/commit/c556f5126f725904822a75427475d46986f8e9f3)
- Updated requirements [`4a7a902`](https://github.com/craigerl/aprsd/commit/4a7a902a337759a352560d4d92dc314b1726412a)
- Updated ChangeLog for 4.0.0 [`934ebd2`](https://github.com/craigerl/aprsd/commit/934ebd236d044625b911dd8ca45293f6c5680a68)
#### [3.5.0](https://github.com/craigerl/aprsd/compare/3.4.4...3.5.0)
> 10 January 2025
- Migrate admin web out of aprsd. [`c48ff8d`](https://github.com/craigerl/aprsd/commit/c48ff8dfd4bd4ce2f95b36e71dce13da5446a658)
- Remove webchat as a built in command. [`8f8887f`](https://github.com/craigerl/aprsd/commit/8f8887f0e496d960b0e71275893b75408a40fdb2)
- Remove email plugin [`0880a35`](https://github.com/craigerl/aprsd/commit/0880a356e6df1a0924cbf6e815e68cba5f5c6cf1)
- Fixed make clean [`ae28dbb`](https://github.com/craigerl/aprsd/commit/ae28dbb0e6bc216bf78c0bd9d7804f57b39091d1)
@@ -37,7 +95,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Removed LocationPlugin from aprsd core [`3bba8a1`](https://github.com/craigerl/aprsd/commit/3bba8a19da88b0912064cea786bc9f8203038946)
- Include haversine library [`bbdbb9a`](https://github.com/craigerl/aprsd/commit/bbdbb9aba189d536497ea3cd7d30911fe3d9d706)
- Update Makefile [`caa4bb8`](https://github.com/craigerl/aprsd/commit/caa4bb8bd01cbd2e02024d75e1c8af97acf6c657)
- Enable packet stats for listen command in Docker [`e5d8796`](https://github.com/craigerl/aprsd/commit/e5d8796cda1a007aa868c760b96b50b364351519)
- Added new KeepAliveCollector [`30d1eb5`](https://github.com/craigerl/aprsd/commit/30d1eb57dd249c609f5b092d8084c40cadda7bd9)
- Changed to ruff [`72d068c`](https://github.com/craigerl/aprsd/commit/72d068c0b8944c8c9eed494fc23de8d7179ee09b)
- Changed README.rst -> README.md [`b1a830d`](https://github.com/craigerl/aprsd/commit/b1a830d54e9dec473074b34f9566f161bdec0030)
@@ -58,21 +115,6 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Added .mailmap [`8d98546`](https://github.com/craigerl/aprsd/commit/8d9854605584fa35117af888fe219df610fb7cb4)
- updated tools in pre-commit [`e4f82d6`](https://github.com/craigerl/aprsd/commit/e4f82d6054d4d859023423bccdd5c402d7a83494)
- some cleanup [`e332d7c`](https://github.com/craigerl/aprsd/commit/e332d7c9d046066e2686ea0522ae06b86d2f162d)
- Added activity to README [`cdd297c`](https://github.com/craigerl/aprsd/commit/cdd297c5bbc8b93f4739f5850a3e5971ce8baeba)
- Added star history to readme [`02e2940`](https://github.com/craigerl/aprsd/commit/02e29405ce2f8310e4f87f68498dfd6575c2e43b)
- removed pytest from README [`1cba31f`](https://github.com/craigerl/aprsd/commit/1cba31f0ac9bd5ee532721a909fc752f023f3b06)
- Updated Docker for using alpine and uv [`24db814`](https://github.com/craigerl/aprsd/commit/24db814c82c9bb6634566d7428603bf7a9ae37d1)
- Update the admin and setup.sh for container [`044ea4c`](https://github.com/craigerl/aprsd/commit/044ea4cc9a0059101851d6e722e986ee236833e8)
- added healthcheck.sh [`1054999`](https://github.com/craigerl/aprsd/commit/10549995686b08e4c166f780efdec5bdae496cab)
- updated healthcheck.sh [`dabb48c`](https://github.com/craigerl/aprsd/commit/dabb48c6f64062c1fed8f83a4f0b8ffba0c206a5)
- try making image for webchat [`ba8acdc`](https://github.com/craigerl/aprsd/commit/ba8acdc5849fc7b2d8a1ee11af6f5e317cf30f45)
- Added APRSD logo [`0ed648f`](https://github.com/craigerl/aprsd/commit/0ed648f8f8a961dbbd9e22bcebadcde525ee41ae)
- Added plugin and extension links [`447451c`](https://github.com/craigerl/aprsd/commit/447451c6c97e1f2d3d0bf580db21ecd176690258)
- reduced logo size 50% [`cf4a29f`](https://github.com/craigerl/aprsd/commit/cf4a29f0cb3ed366b21ec3120a189614e0955180)
- Updated README.md TOC [`375a5e5`](https://github.com/craigerl/aprsd/commit/375a5e5b34718cadc6ee8a51484fc91441440a61)
- chore: update AUTHORS [skip ci] [`c556f51`](https://github.com/craigerl/aprsd/commit/c556f5126f725904822a75427475d46986f8e9f3)
- Updated requirements [`4a7a902`](https://github.com/craigerl/aprsd/commit/4a7a902a337759a352560d4d92dc314b1726412a)
- Updated ChangeLog for 4.0.0 [`934ebd2`](https://github.com/craigerl/aprsd/commit/934ebd236d044625b911dd8ca45293f6c5680a68)
#### [3.4.4](https://github.com/craigerl/aprsd/compare/3.4.3...3.4.4)
+36 -35
View File
@@ -13,35 +13,35 @@ from aprsd.utils import trace
CONF = cfg.CONF
home = str(Path.home())
DEFAULT_CONFIG_DIR = f"{home}/.config/aprsd/"
DEFAULT_SAVE_FILE = f"{home}/.config/aprsd/aprsd.p"
DEFAULT_CONFIG_FILE = f"{home}/.config/aprsd/aprsd.conf"
DEFAULT_CONFIG_DIR = f'{home}/.config/aprsd/'
DEFAULT_SAVE_FILE = f'{home}/.config/aprsd/aprsd.p'
DEFAULT_CONFIG_FILE = f'{home}/.config/aprsd/aprsd.conf'
F = t.TypeVar("F", bound=t.Callable[..., t.Any])
F = t.TypeVar('F', bound=t.Callable[..., t.Any])
common_options = [
click.option(
"--loglevel",
default="INFO",
'--loglevel',
default='INFO',
show_default=True,
type=click.Choice(
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"],
['CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'],
case_sensitive=False,
),
show_choices=True,
help="The log level to use for aprsd.log",
help='The log level to use for aprsd.log',
),
click.option(
"-c",
"--config",
"config_file",
'-c',
'--config',
'config_file',
show_default=True,
default=DEFAULT_CONFIG_FILE,
help="The aprsd config file to use for options.",
help='The aprsd config file to use for options.',
),
click.option(
"--quiet",
'--quiet',
is_flag=True,
default=False,
help="Don't log to stdout",
@@ -59,7 +59,7 @@ class AliasedGroup(click.Group):
"""
def decorator(f):
aliases = kwargs.pop("aliases", [])
aliases = kwargs.pop('aliases', [])
cmd = click.decorators.command(*args, **kwargs)(f)
self.add_command(cmd)
for alias in aliases:
@@ -77,7 +77,7 @@ class AliasedGroup(click.Group):
"""
def decorator(f):
aliases = kwargs.pop("aliases", [])
aliases = kwargs.pop('aliases', [])
cmd = click.decorators.group(*args, **kwargs)(f)
self.add_command(cmd)
for alias in aliases:
@@ -101,36 +101,37 @@ def process_standard_options(f: F) -> F:
ctx = args[0]
ctx.ensure_object(dict)
config_file_found = True
if kwargs["config_file"]:
default_config_files = [kwargs["config_file"]]
if kwargs['config_file']:
default_config_files = [kwargs['config_file']]
else:
default_config_files = None
try:
CONF(
[],
project="aprsd",
project='aprsd',
version=aprsd.__version__,
default_config_files=default_config_files,
)
except cfg.ConfigFilesNotFoundError:
config_file_found = False
ctx.obj["loglevel"] = kwargs["loglevel"]
ctx.obj['loglevel'] = kwargs['loglevel']
# ctx.obj["config_file"] = kwargs["config_file"]
ctx.obj["quiet"] = kwargs["quiet"]
ctx.obj['quiet'] = kwargs['quiet']
log.setup_logging(
ctx.obj["loglevel"],
ctx.obj["quiet"],
ctx.obj['loglevel'],
ctx.obj['quiet'],
)
if CONF.trace_enabled:
trace.setup_tracing(["method", "api"])
trace.setup_tracing(['method', 'api'])
if not config_file_found:
LOG = logging.getLogger("APRSD") # noqa: N806
LOG = logging.getLogger('APRSD') # noqa: N806
LOG.error("No config file found!! run 'aprsd sample-config'")
del kwargs["loglevel"]
del kwargs["config_file"]
del kwargs["quiet"]
del kwargs['loglevel']
del kwargs['config_file']
del kwargs['quiet']
return f(*args, **kwargs)
return update_wrapper(t.cast(F, new_func), f)
@@ -142,17 +143,17 @@ def process_standard_options_no_config(f: F) -> F:
def new_func(*args, **kwargs):
ctx = args[0]
ctx.ensure_object(dict)
ctx.obj["loglevel"] = kwargs["loglevel"]
ctx.obj["config_file"] = kwargs["config_file"]
ctx.obj["quiet"] = kwargs["quiet"]
ctx.obj['loglevel'] = kwargs['loglevel']
ctx.obj['config_file'] = kwargs['config_file']
ctx.obj['quiet'] = kwargs['quiet']
log.setup_logging(
ctx.obj["loglevel"],
ctx.obj["quiet"],
ctx.obj['loglevel'],
ctx.obj['quiet'],
)
del kwargs["loglevel"]
del kwargs["config_file"]
del kwargs["quiet"]
del kwargs['loglevel']
del kwargs['config_file']
del kwargs['quiet']
return f(*args, **kwargs)
return update_wrapper(t.cast(F, new_func), f)
+14 -11
View File
@@ -9,7 +9,7 @@ from aprsd.packets import core
from aprsd.utils import keepalive_collector
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
LOG = logging.getLogger('APRSD')
class APRSClient:
@@ -20,8 +20,8 @@ class APRSClient:
connected = False
login_status = {
"success": False,
"message": None,
'success': False,
'message': None,
}
filter = None
lock = threading.Lock()
@@ -59,17 +59,20 @@ class APRSClient:
@property
def login_success(self):
return self.login_status.get("success", False)
return self.login_status.get('success', False)
@property
def login_failure(self):
return self.login_status["message"]
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:
@@ -80,16 +83,16 @@ class APRSClient:
try:
self._client = self.setup_connection()
if self.filter:
LOG.info("Creating APRS client 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}")
LOG.error(f'Failed to create APRS client: {e}')
self._client = None
raise
def stop(self):
if self._client:
LOG.info("Stopping client connection.")
LOG.info('Stopping client connection.')
self._client.stop()
def send(self, packet: core.Packet) -> None:
@@ -103,16 +106,16 @@ class APRSClient:
@wrapt.synchronized(lock)
def reset(self) -> None:
"""Call this to force a rebuild/reconnect."""
LOG.info("Resetting client connection.")
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.")
LOG.warning('Client not initialized, nothing to reset.')
# Recreate the client
LOG.info(f"Creating new client {self.client}")
LOG.info(f'Creating new client {self.client}')
@abc.abstractmethod
def setup_connection(self):
+87 -35
View File
@@ -1,6 +1,7 @@
import datetime
import logging
import select
import socket
import threading
import aprslib
@@ -18,7 +19,7 @@ from aprslib.exceptions import (
import aprsd
from aprsd.packets import core
LOG = logging.getLogger("APRSD")
LOG = logging.getLogger('APRSD')
class Aprsdis(aprslib.IS):
@@ -31,7 +32,7 @@ class Aprsdis(aprslib.IS):
aprsd_keepalive = datetime.datetime.now()
# Which server we are connected to?
server_string = "None"
server_string = 'None'
# timeout in seconds
select_timeout = 1
@@ -39,10 +40,10 @@ class Aprsdis(aprslib.IS):
def stop(self):
self.thread_stop = True
LOG.warning("Shutdown Aprsdis client.")
LOG.warning('Shutdown Aprsdis client.')
def close(self):
LOG.warning("Closing Aprsdis client.")
LOG.warning('Closing Aprsdis client.')
super().close()
@wrapt.synchronized(lock)
@@ -54,6 +55,57 @@ class Aprsdis(aprslib.IS):
"""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
@@ -61,12 +113,12 @@ class Aprsdis(aprslib.IS):
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")
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"
short_buf = b''
newline = b'\r\n'
# set a select timeout, so we get a chance to exit
# when user hits CTRL-C
@@ -91,11 +143,11 @@ class Aprsdis(aprslib.IS):
# We could just not be blocking, so empty is expected
continue
else:
self.logger.error("socket.recv(): returned empty")
raise aprslib.ConnectionDrop("connection dropped")
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 'Resource temporarily unavailable' in str(e):
if not blocking:
if len(self.buf) == 0:
break
@@ -111,22 +163,22 @@ class Aprsdis(aprslib.IS):
"""
Sends login string to server
"""
login_str = "user {0} pass {1} vers github.com/craigerl/aprsd {3}{2}\r\n"
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 "",
(' filter ' + self.filter) if self.filter != '' else '',
aprsd.__version__,
)
self.logger.debug("Sending login information")
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.decode('latin-1')
test = test.rstrip()
self.logger.debug("Server: '%s'", test)
@@ -134,26 +186,26 @@ class Aprsdis(aprslib.IS):
if not test:
raise LoginError(f"Server Response Empty: '{test}'")
_, _, callsign, status, e = test.split(" ", 4)
s = e.split(",")
_, _, callsign, status, e = test.split(' ', 4)
s = e.split(',')
if len(s):
server_string = s[0].replace("server ", "")
server_string = s[0].replace('server ', '')
else:
server_string = e.replace("server ", "")
server_string = e.replace('server ', '')
if callsign == "":
raise LoginError("Server responded with empty callsign???")
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")
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)")
if self.passwd == '-1':
self.logger.info('Login successful (receive only)')
else:
self.logger.info("Login successful")
self.logger.info('Login successful')
self.logger.info(f"Connected to {server_string}")
self.logger.info(f'Connected to {server_string}')
self.server_string = server_string
except LoginError as e:
@@ -164,7 +216,7 @@ class Aprsdis(aprslib.IS):
self.close()
self.logger.error(f"Failed to login '{e}'")
self.logger.exception(e)
raise LoginError("Failed to login")
raise LoginError('Failed to login') from e
def consumer(self, callback, blocking=True, immortal=False, raw=False):
"""
@@ -180,21 +232,21 @@ class Aprsdis(aprslib.IS):
"""
if not self._connected:
raise ConnectionError("not connected to a server")
raise ConnectionError('not connected to a server')
line = b""
line = b''
while True and not self.thread_stop:
try:
for line in self._socket_readlines(blocking):
if line[0:1] != b"#":
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.logger.debug('Server: %s', line.decode('utf8'))
self.aprsd_keepalive = datetime.datetime.now()
except ParseError as exp:
self.logger.log(
@@ -211,7 +263,7 @@ class Aprsdis(aprslib.IS):
exp.packet,
)
except LoginError as exp:
self.logger.error("%s: %s", exp.__class__.__name__, exp)
self.logger.error('%s: %s', exp.__class__.__name__, exp)
except (KeyboardInterrupt, SystemExit):
raise
except (ConnectionDrop, ConnectionError):
@@ -227,7 +279,7 @@ class Aprsdis(aprslib.IS):
except StopIteration:
break
except Exception:
self.logger.error("APRS Packet: %s", line)
self.logger.error('APRS Packet: %s', line)
raise
if not blocking:
+28 -5
View File
@@ -18,12 +18,13 @@ class KISS3Client:
# 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 True
return self._connected
def setup(self):
# we can be TCP kiss or Serial kiss
@@ -56,17 +57,33 @@ class KISS3Client:
self.path = CONF.kiss_tcp.path
LOG.debug('Starting KISS interface connection')
self.kiss.start()
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 as ex:
LOG.exception(ex)
except Exception:
LOG.error('Failed to stop KISS interface.')
def close(self):
self.stop()
def set_filter(self, filter):
# This does nothing right now.
@@ -86,8 +103,14 @@ class KISS3Client:
LOG.exception(ex)
def consumer(self, callback):
if not self._connected:
raise Exception('KISS transport is not connected')
self._parse_callback = callback
self.kiss.read(callback=self.parse_frame, min_frames=None)
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."""
+1
View File
@@ -140,3 +140,4 @@ class KISSClient(base.APRSClient):
except Exception as ex:
LOG.error(f'Consumer failed {ex}')
LOG.error(ex)
raise ex
+85 -92
View File
@@ -20,8 +20,10 @@ from aprsd import cli_helper, packets, plugin, threads, utils
from aprsd.client import client_factory
from aprsd.main import cli
from aprsd.packets import collector as packet_collector
from aprsd.packets import core, seen_list
from aprsd.packets import log as packet_log
from aprsd.packets import seen_list
from aprsd.packets.filter import PacketFilter
from aprsd.packets.filters import dupe_filter, packet_type
from aprsd.stats import collector
from aprsd.threads import keepalive, rx
from aprsd.threads import stats as stats_thread
@@ -29,7 +31,7 @@ from aprsd.threads.aprsd import APRSDThread
# setup the global logger
# log.basicConfig(level=log.DEBUG) # level=10
LOG = logging.getLogger("APRSD")
LOG = logging.getLogger('APRSD')
CONF = cfg.CONF
LOGU = logger
console = Console()
@@ -37,9 +39,9 @@ console = Console()
def signal_handler(sig, frame):
threads.APRSDThreadList().stop_all()
if "subprocess" not in str(frame):
if 'subprocess' not in str(frame):
LOG.info(
"Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format(
'Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}'.format(
datetime.datetime.now(),
),
)
@@ -48,90 +50,66 @@ def signal_handler(sig, frame):
collector.Collector().collect()
class APRSDListenThread(rx.APRSDRXThread):
class APRSDListenProcessThread(rx.APRSDFilterThread):
def __init__(
self,
packet_queue,
packet_filter=None,
plugin_manager=None,
enabled_plugins=[],
enabled_plugins=None,
log_packets=False,
):
super().__init__(packet_queue)
super().__init__('ListenProcThread', packet_queue)
self.packet_filter = packet_filter
self.plugin_manager = plugin_manager
if self.plugin_manager:
LOG.info(f"Plugins {self.plugin_manager.get_message_plugins()}")
LOG.info(f'Plugins {self.plugin_manager.get_message_plugins()}')
self.log_packets = log_packets
def process_packet(self, *args, **kwargs):
packet = self._client.decode_packet(*args, **kwargs)
filters = {
packets.Packet.__name__: packets.Packet,
packets.AckPacket.__name__: packets.AckPacket,
packets.BeaconPacket.__name__: packets.BeaconPacket,
packets.GPSPacket.__name__: packets.GPSPacket,
packets.MessagePacket.__name__: packets.MessagePacket,
packets.MicEPacket.__name__: packets.MicEPacket,
packets.ObjectPacket.__name__: packets.ObjectPacket,
packets.StatusPacket.__name__: packets.StatusPacket,
packets.ThirdPartyPacket.__name__: packets.ThirdPartyPacket,
packets.WeatherPacket.__name__: packets.WeatherPacket,
packets.UnknownPacket.__name__: packets.UnknownPacket,
}
def print_packet(self, packet):
if self.log_packets:
packet_log.log(packet)
if self.packet_filter:
filter_class = filters[self.packet_filter]
if isinstance(packet, filter_class):
if self.log_packets:
packet_log.log(packet)
if self.plugin_manager:
# Don't do anything with the reply
# This is the listen only command.
self.plugin_manager.run(packet)
else:
if self.log_packets:
packet_log.log(packet)
if self.plugin_manager:
# Don't do anything with the reply.
# This is the listen only command.
self.plugin_manager.run(packet)
packet_collector.PacketCollector().rx(packet)
def process_packet(self, packet: type[core.Packet]):
if self.plugin_manager:
# Don't do anything with the reply.
# This is the listen only command.
self.plugin_manager.run(packet)
class ListenStatsThread(APRSDThread):
"""Log the stats from the PacketList."""
def __init__(self):
super().__init__("PacketStatsLog")
super().__init__('PacketStatsLog')
self._last_total_rx = 0
self.period = 31
def loop(self):
if self.loop_count % 10 == 0:
if self.loop_count % self.period == 0:
# log the stats every 10 seconds
stats_json = collector.Collector().collect()
stats = stats_json["PacketList"]
total_rx = stats["rx"]
packet_count = len(stats["packets"])
stats = stats_json['PacketList']
total_rx = stats['rx']
packet_count = len(stats['packets'])
rx_delta = total_rx - self._last_total_rx
rate = rx_delta / 10
rate = rx_delta / self.period
# Log summary stats
LOGU.opt(colors=True).info(
f"<green>RX Rate: {rate} pps</green> "
f"<yellow>Total RX: {total_rx}</yellow> "
f"<red>RX Last 10 secs: {rx_delta}</red> "
f"<white>Packets in PacketList: {packet_count}</white>",
f'<green>RX Rate: {rate:.2f} pps</green> '
f'<yellow>Total RX: {total_rx}</yellow> '
f'<red>RX Last {self.period} secs: {rx_delta}</red> '
f'<white>Packets in PacketListStats: {packet_count}</white>',
)
self._last_total_rx = total_rx
# Log individual type stats
for k, v in stats["types"].items():
thread_hex = f"fg {utils.hex_from_name(k)}"
for k, v in stats['types'].items():
thread_hex = f'fg {utils.hex_from_name(k)}'
LOGU.opt(colors=True).info(
f"<{thread_hex}>{k:<15}</{thread_hex}> "
f"<blue>RX: {v['rx']}</blue> <red>TX: {v['tx']}</red>",
f'<{thread_hex}>{k:<15}</{thread_hex}> '
f'<blue>RX: {v["rx"]}</blue> <red>TX: {v["tx"]}</red>',
)
time.sleep(1)
@@ -141,19 +119,19 @@ class ListenStatsThread(APRSDThread):
@cli.command()
@cli_helper.add_options(cli_helper.common_options)
@click.option(
"--aprs-login",
envvar="APRS_LOGIN",
'--aprs-login',
envvar='APRS_LOGIN',
show_envvar=True,
help="What callsign to send the message from.",
help='What callsign to send the message from.',
)
@click.option(
"--aprs-password",
envvar="APRS_PASSWORD",
'--aprs-password',
envvar='APRS_PASSWORD',
show_envvar=True,
help="the APRS-IS password for APRS_LOGIN",
help='the APRS-IS password for APRS_LOGIN',
)
@click.option(
"--packet-filter",
'--packet-filter',
type=click.Choice(
[
packets.AckPacket.__name__,
@@ -170,35 +148,37 @@ class ListenStatsThread(APRSDThread):
],
case_sensitive=False,
),
help="Filter by packet type",
)
@click.option(
"--enable-plugin",
multiple=True,
help="Enable a plugin. This is the name of the file in the plugins directory.",
default=[],
help='Filter by packet type',
)
@click.option(
"--load-plugins",
'--enable-plugin',
multiple=True,
help='Enable a plugin. This is the name of the file in the plugins directory.',
)
@click.option(
'--load-plugins',
default=False,
is_flag=True,
help="Load plugins as enabled in aprsd.conf ?",
help='Load plugins as enabled in aprsd.conf ?',
)
@click.argument(
"filter",
'filter',
nargs=-1,
required=True,
)
@click.option(
"--log-packets",
'--log-packets',
default=False,
is_flag=True,
help="Log incoming packets.",
help='Log incoming packets.',
)
@click.option(
"--enable-packet-stats",
'--enable-packet-stats',
default=False,
is_flag=True,
help="Enable packet stats periodic logging.",
help='Enable packet stats periodic logging.',
)
@click.pass_context
@cli_helper.process_standard_options
@@ -228,46 +208,46 @@ def listen(
if not aprs_login:
click.echo(ctx.get_help())
click.echo("")
ctx.fail("Must set --aprs-login or APRS_LOGIN")
click.echo('')
ctx.fail('Must set --aprs-login or APRS_LOGIN')
ctx.exit()
if not aprs_password:
click.echo(ctx.get_help())
click.echo("")
ctx.fail("Must set --aprs-password or APRS_PASSWORD")
click.echo('')
ctx.fail('Must set --aprs-password or APRS_PASSWORD')
ctx.exit()
# CONF.aprs_network.login = aprs_login
# config["aprs"]["password"] = aprs_password
LOG.info(f"APRSD Listen Started version: {aprsd.__version__}")
LOG.info(f'APRSD Listen Started version: {aprsd.__version__}')
CONF.log_opt_values(LOG, logging.DEBUG)
collector.Collector()
# Try and load saved MsgTrack list
LOG.debug("Loading saved MsgTrack object.")
LOG.debug('Loading saved MsgTrack object.')
# 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():
LOG.error("No Clients are enabled in config.")
LOG.error('No Clients are enabled in config.')
sys.exit(-1)
# Creates the client object
LOG.info("Creating client connection")
LOG.info('Creating client connection')
aprs_client = client_factory.create()
LOG.info(aprs_client)
if not aprs_client.login_success:
# We failed to login, will just quit!
msg = f"Login Failure: {aprs_client.login_failure}"
msg = f'Login Failure: {aprs_client.login_failure}'
LOG.error(msg)
print(msg)
sys.exit(-1)
LOG.debug(f"Filter by '{filter}'")
LOG.debug(f"Filter messages on aprsis server by '{filter}'")
aprs_client.set_filter(filter)
keepalive_thread = keepalive.KeepAliveThread()
@@ -276,10 +256,19 @@ def listen(
# just deregister the class from the packet collector
packet_collector.PacketCollector().unregister(seen_list.SeenList)
# we don't want the dupe filter to run here.
PacketFilter().unregister(dupe_filter.DupePacketFilter)
if packet_filter:
LOG.info('Enabling packet filtering for {packet_filter}')
packet_type.PacketTypeFilter().set_allow_list(packet_filter)
PacketFilter().register(packet_type.PacketTypeFilter)
else:
LOG.info('No packet filtering enabled.')
pm = None
if load_plugins:
pm = plugin.PluginManager()
LOG.info("Loading plugins")
LOG.info('Loading plugins')
pm.setup_plugins(load_help_plugin=False)
elif enable_plugin:
pm = plugin.PluginManager()
@@ -290,33 +279,37 @@ def listen(
else:
LOG.warning(
"Not Loading any plugins use --load-plugins to load what's "
"defined in the config file.",
'defined in the config file.',
)
if pm:
for p in pm.get_plugins():
LOG.info("Loaded plugin %s", p.__class__.__name__)
LOG.info('Loaded plugin %s', p.__class__.__name__)
stats = stats_thread.APRSDStatsStoreThread()
stats.start()
LOG.debug("Create APRSDListenThread")
listen_thread = APRSDListenThread(
LOG.debug('Start APRSDRxThread')
rx_thread = rx.APRSDRXThread(packet_queue=threads.packet_queue)
rx_thread.start()
LOG.debug('Create APRSDListenProcessThread')
listen_thread = APRSDListenProcessThread(
packet_queue=threads.packet_queue,
packet_filter=packet_filter,
plugin_manager=pm,
enabled_plugins=enable_plugin,
log_packets=log_packets,
)
LOG.debug("Start APRSDListenThread")
LOG.debug('Start APRSDListenProcessThread')
listen_thread.start()
if enable_packet_stats:
listen_stats = ListenStatsThread()
listen_stats.start()
keepalive_thread.start()
LOG.debug("keepalive Join")
LOG.debug('keepalive Join')
keepalive_thread.join()
LOG.debug("listen_thread Join")
rx_thread.join()
listen_thread.join()
stats.join()
+30 -28
View File
@@ -3,58 +3,60 @@ import sys
import time
import aprslib
from aprslib.exceptions import LoginError
import click
from aprslib.exceptions import LoginError
from oslo_config import cfg
import aprsd
from aprsd import cli_helper, packets
from aprsd import conf # noqa : F401
import aprsd.packets # noqa : F401
from aprsd import (
cli_helper,
conf, # noqa : F401
packets,
)
from aprsd.client import client_factory
from aprsd.main import cli
import aprsd.packets # noqa : F401
from aprsd.packets import collector
from aprsd.packets import log as packet_log
from aprsd.threads import tx
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
LOG = logging.getLogger('APRSD')
@cli.command()
@cli_helper.add_options(cli_helper.common_options)
@click.option(
"--aprs-login",
envvar="APRS_LOGIN",
'--aprs-login',
envvar='APRS_LOGIN',
show_envvar=True,
help="What callsign to send the message from. Defaults to config entry.",
help='What callsign to send the message from. Defaults to config entry.',
)
@click.option(
"--aprs-password",
envvar="APRS_PASSWORD",
'--aprs-password',
envvar='APRS_PASSWORD',
show_envvar=True,
help="the APRS-IS password for APRS_LOGIN. Defaults to config entry.",
help='the APRS-IS password for APRS_LOGIN. Defaults to config entry.',
)
@click.option(
"--no-ack",
"-n",
'--no-ack',
'-n',
is_flag=True,
show_default=True,
default=False,
help="Don't wait for an ack, just sent it to APRS-IS and bail.",
)
@click.option(
"--wait-response",
"-w",
'--wait-response',
'-w',
is_flag=True,
show_default=True,
default=False,
help="Wait for a response to the message?",
help='Wait for a response to the message?',
)
@click.option("--raw", default=None, help="Send a raw message. Implies --no-ack")
@click.argument("tocallsign", required=True)
@click.argument("command", nargs=-1, required=True)
@click.option('--raw', default=None, help='Send a raw message. Implies --no-ack')
@click.argument('tocallsign', required=True)
@click.argument('command', nargs=-1, required=True)
@click.pass_context
@cli_helper.process_standard_options
def send_message(
@@ -69,11 +71,11 @@ def send_message(
):
"""Send a message to a callsign via APRS_IS."""
global got_ack, got_response
quiet = ctx.obj["quiet"]
quiet = ctx.obj['quiet']
if not aprs_login:
if CONF.aprs_network.login == conf.client.DEFAULT_LOGIN:
click.echo("Must set --aprs_login or APRS_LOGIN")
click.echo('Must set --aprs_login or APRS_LOGIN')
ctx.exit(-1)
return
else:
@@ -81,15 +83,15 @@ def send_message(
if not aprs_password:
if not CONF.aprs_network.password:
click.echo("Must set --aprs-password or APRS_PASSWORD")
click.echo('Must set --aprs-password or APRS_PASSWORD')
ctx.exit(-1)
return
else:
aprs_password = CONF.aprs_network.password
LOG.info(f"APRSD LISTEN Started version: {aprsd.__version__}")
LOG.info(f'APRSD LISTEN Started version: {aprsd.__version__}')
if type(command) is tuple:
command = " ".join(command)
command = ' '.join(command)
if not quiet:
if raw:
LOG.info(f"L'{aprs_login}' R'{raw}'")
@@ -129,7 +131,7 @@ def send_message(
sys.exit(0)
try:
client_factory.create().client
client_factory.create().client # noqa: B018
except LoginError:
sys.exit(-1)
@@ -140,7 +142,7 @@ def send_message(
# message
if raw:
tx.send(
packets.Packet(from_call="", to_call="", raw=raw),
packets.Packet(from_call='', to_call='', raw=raw),
direct=True,
)
sys.exit(0)
@@ -164,7 +166,7 @@ def send_message(
aprs_client = client_factory.create().client
aprs_client.consumer(rx_packet, raw=False)
except aprslib.exceptions.ConnectionDrop:
LOG.error("Connection dropped, reconnecting")
LOG.error('Connection dropped, reconnecting')
time.sleep(5)
# Force the deletion of the client object connected to aprs
# This will cause a reconnect, next time client.get_client()
+29 -64
View File
@@ -12,59 +12,24 @@ from aprsd.client import client_factory
from aprsd.main import cli
from aprsd.packets import collector as packet_collector
from aprsd.packets import seen_list
from aprsd.threads import aprsd as aprsd_threads
from aprsd.threads import keepalive, registry, rx, tx
from aprsd.threads import keepalive, registry, rx, service, tx
from aprsd.threads import stats as stats_thread
from aprsd.utils import singleton
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
@singleton
class ServerThreads:
"""Registry for threads that the server command runs.
This enables extensions to register a thread to run during
the server command.
"""
def __init__(self):
self.threads: list[aprsd_threads.APRSDThread] = []
def register(self, thread: aprsd_threads.APRSDThread):
if not isinstance(thread, aprsd_threads.APRSDThread):
raise TypeError(f"Thread {thread} is not an APRSDThread")
self.threads.append(thread)
def unregister(self, thread: aprsd_threads.APRSDThread):
if not isinstance(thread, aprsd_threads.APRSDThread):
raise TypeError(f"Thread {thread} is not an APRSDThread")
self.threads.remove(thread)
def start(self):
"""Start all threads in the list."""
for thread in self.threads:
thread.start()
def join(self):
"""Join all the threads in the list"""
for thread in self.threads:
thread.join()
LOG = logging.getLogger('APRSD')
# main() ###
@cli.command()
@cli_helper.add_options(cli_helper.common_options)
@click.option(
"-f",
"--flush",
"flush",
'-f',
'--flush',
'flush',
is_flag=True,
show_default=True,
default=False,
help="Flush out all old aged messages on disk.",
help='Flush out all old aged messages on disk.',
)
@click.pass_context
@cli_helper.process_standard_options
@@ -73,37 +38,37 @@ def server(ctx, flush):
signal.signal(signal.SIGINT, aprsd_main.signal_handler)
signal.signal(signal.SIGTERM, aprsd_main.signal_handler)
server_threads = ServerThreads()
service_threads = service.ServiceThreads()
level, msg = utils._check_version()
if level:
LOG.warning(msg)
else:
LOG.info(msg)
LOG.info(f"APRSD Started version: {aprsd.__version__}")
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.")
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():
LOG.error("No Clients are enabled in config.")
LOG.error('No Clients are enabled in config.')
sys.exit(-1)
if not client_factory.is_client_configured():
LOG.error("APRS client is not properly configured in config file.")
LOG.error('APRS client is not properly configured in config file.')
sys.exit(-1)
# Creates the client object
LOG.info("Creating client connection")
LOG.info('Creating client connection')
aprs_client = client_factory.create()
LOG.info(aprs_client)
if not aprs_client.login_success:
# We failed to login, will just quit!
msg = f"Login Failure: {aprs_client.login_failure}"
msg = f'Login Failure: {aprs_client.login_failure}'
LOG.error(msg)
print(msg)
sys.exit(-1)
@@ -114,7 +79,7 @@ def server(ctx, flush):
# We register plugins first here so we can register each
# plugins config options, so we can dump them all in the
# log file output.
LOG.info("Loading Plugin Manager and registering plugins")
LOG.info('Loading Plugin Manager and registering plugins')
plugin_manager = plugin.PluginManager()
plugin_manager.setup_plugins(load_help_plugin=CONF.load_help_plugin)
@@ -122,10 +87,10 @@ def server(ctx, flush):
CONF.log_opt_values(LOG, logging.DEBUG)
message_plugins = plugin_manager.get_message_plugins()
watchlist_plugins = plugin_manager.get_watchlist_plugins()
LOG.info("Message Plugins enabled and running:")
LOG.info('Message Plugins enabled and running:')
for p in message_plugins:
LOG.info(p)
LOG.info("Watchlist Plugins enabled and running:")
LOG.info('Watchlist Plugins enabled and running:')
for p in watchlist_plugins:
LOG.info(p)
@@ -135,37 +100,37 @@ def server(ctx, flush):
# Now load the msgTrack from disk if any
if flush:
LOG.debug("Flushing All packet tracking objects.")
LOG.debug('Flushing All packet tracking objects.')
packet_collector.PacketCollector().flush()
else:
# Try and load saved MsgTrack list
LOG.debug("Loading saved packet tracking data.")
LOG.debug('Loading saved packet tracking data.')
packet_collector.PacketCollector().load()
# Now start all the main processing threads.
server_threads.register(keepalive.KeepAliveThread())
server_threads.register(stats_thread.APRSDStatsStoreThread())
server_threads.register(
rx.APRSDPluginRXThread(
service_threads.register(keepalive.KeepAliveThread())
service_threads.register(stats_thread.APRSDStatsStoreThread())
service_threads.register(
rx.APRSDRXThread(
packet_queue=threads.packet_queue,
),
)
server_threads.register(
service_threads.register(
rx.APRSDPluginProcessPacketThread(
packet_queue=threads.packet_queue,
),
)
if CONF.enable_beacon:
LOG.info("Beacon Enabled. Starting Beacon thread.")
server_threads.register(tx.BeaconSendThread())
LOG.info('Beacon Enabled. Starting Beacon thread.')
service_threads.register(tx.BeaconSendThread())
if CONF.aprs_registry.enabled:
LOG.info("Registry Enabled. Starting Registry thread.")
server_threads.register(registry.APRSRegistryThread())
LOG.info('Registry Enabled. Starting Registry thread.')
service_threads.register(registry.APRSRegistryThread())
server_threads.start()
server_threads.join()
service_threads.start()
service_threads.join()
return 0
+100 -101
View File
@@ -3,220 +3,219 @@ from pathlib import Path
from oslo_config import cfg
home = str(Path.home())
DEFAULT_CONFIG_DIR = f"{home}/.config/aprsd/"
APRSD_DEFAULT_MAGIC_WORD = "CHANGEME!!!"
DEFAULT_CONFIG_DIR = f'{home}/.config/aprsd/'
APRSD_DEFAULT_MAGIC_WORD = 'CHANGEME!!!'
watch_list_group = cfg.OptGroup(
name="watch_list",
title="Watch List settings",
name='watch_list',
title='Watch List settings',
)
registry_group = cfg.OptGroup(
name="aprs_registry",
title="APRS Registry settings",
name='aprs_registry',
title='APRS Registry settings',
)
aprsd_opts = [
cfg.StrOpt(
"callsign",
'callsign',
required=True,
help="Callsign to use for messages sent by APRSD",
help='Callsign to use for messages sent by APRSD',
),
cfg.BoolOpt(
"enable_save",
'enable_save',
default=True,
help="Enable saving of watch list, packet tracker between restarts.",
help='Enable saving of watch list, packet tracker between restarts.',
),
cfg.StrOpt(
"save_location",
'save_location',
default=DEFAULT_CONFIG_DIR,
help="Save location for packet tracking files.",
help='Save location for packet tracking files.',
),
cfg.BoolOpt(
"trace_enabled",
'trace_enabled',
default=False,
help="Enable code tracing",
help='Enable code tracing',
),
cfg.StrOpt(
"units",
default="imperial",
help="Units for display, imperial or metric",
'units',
default='imperial',
help='Units for display, imperial or metric',
),
cfg.IntOpt(
"ack_rate_limit_period",
'ack_rate_limit_period',
default=1,
help="The wait period in seconds per Ack packet being sent."
"1 means 1 ack packet per second allowed."
"2 means 1 pack packet every 2 seconds allowed",
help='The wait period in seconds per Ack packet being sent.'
'1 means 1 ack packet per second allowed.'
'2 means 1 pack packet every 2 seconds allowed',
),
cfg.IntOpt(
"msg_rate_limit_period",
'msg_rate_limit_period',
default=2,
help="Wait period in seconds per non AckPacket being sent."
"2 means 1 packet every 2 seconds allowed."
"5 means 1 pack packet every 5 seconds allowed",
help='Wait period in seconds per non AckPacket being sent.'
'2 means 1 packet every 2 seconds allowed.'
'5 means 1 pack packet every 5 seconds allowed',
),
cfg.IntOpt(
"packet_dupe_timeout",
'packet_dupe_timeout',
default=300,
help="The number of seconds before a packet is not considered a duplicate.",
help='The number of seconds before a packet is not considered a duplicate.',
),
cfg.BoolOpt(
"enable_beacon",
'enable_beacon',
default=False,
help="Enable sending of a GPS Beacon packet to locate this service. "
"Requires latitude and longitude to be set.",
help='Enable sending of a GPS Beacon packet to locate this service. '
'Requires latitude and longitude to be set.',
),
cfg.IntOpt(
"beacon_interval",
'beacon_interval',
default=1800,
help="The number of seconds between beacon packets.",
help='The number of seconds between beacon packets.',
),
cfg.StrOpt(
"beacon_symbol",
default="/",
help="The symbol to use for the GPS Beacon packet. See: http://www.aprs.net/vm/DOS/SYMBOLS.HTM",
'beacon_symbol',
default='/',
help='The symbol to use for the GPS Beacon packet. See: http://www.aprs.net/vm/DOS/SYMBOLS.HTM',
),
cfg.StrOpt(
"latitude",
'latitude',
default=None,
help="Latitude for the GPS Beacon button. If not set, the button will not be enabled.",
help='Latitude for the GPS Beacon button. If not set, the button will not be enabled.',
),
cfg.StrOpt(
"longitude",
'longitude',
default=None,
help="Longitude for the GPS Beacon button. If not set, the button will not be enabled.",
help='Longitude for the GPS Beacon button. If not set, the button will not be enabled.',
),
cfg.StrOpt(
"log_packet_format",
choices=["compact", "multiline", "both"],
default="compact",
'log_packet_format',
choices=['compact', 'multiline', 'both'],
default='compact',
help="When logging packets 'compact' will use a single line formatted for each packet."
"'multiline' will use multiple lines for each packet and is the traditional format."
"both will log both compact and multiline.",
'both will log both compact and multiline.',
),
cfg.IntOpt(
"default_packet_send_count",
'default_packet_send_count',
default=3,
help="The number of times to send a non ack packet before giving up.",
help='The number of times to send a non ack packet before giving up.',
),
cfg.IntOpt(
"default_ack_send_count",
'default_ack_send_count',
default=3,
help="The number of times to send an ack packet in response to recieving a packet.",
help='The number of times to send an ack packet in response to recieving a packet.',
),
cfg.IntOpt(
"packet_list_maxlen",
'packet_list_maxlen',
default=100,
help="The maximum number of packets to store in the packet list.",
help='The maximum number of packets to store in the packet list.',
),
cfg.IntOpt(
"packet_list_stats_maxlen",
'packet_list_stats_maxlen',
default=20,
help="The maximum number of packets to send in the stats dict for admin ui.",
help='The maximum number of packets to send in the stats dict for admin ui. -1 means no max.',
),
cfg.BoolOpt(
"enable_seen_list",
'enable_seen_list',
default=True,
help="Enable the Callsign seen list tracking feature. This allows aprsd to keep track of "
"callsigns that have been seen and when they were last seen.",
help='Enable the Callsign seen list tracking feature. This allows aprsd to keep track of '
'callsigns that have been seen and when they were last seen.',
),
cfg.BoolOpt(
"enable_packet_logging",
'enable_packet_logging',
default=True,
help="Set this to False, to disable logging of packets to the log file.",
help='Set this to False, to disable logging of packets to the log file.',
),
cfg.BoolOpt(
"load_help_plugin",
'load_help_plugin',
default=True,
help="Set this to False to disable the help plugin.",
help='Set this to False to disable the help plugin.',
),
cfg.BoolOpt(
"enable_sending_ack_packets",
'enable_sending_ack_packets',
default=True,
help="Set this to False, to disable sending of ack packets. This will entirely stop"
"APRSD from sending ack packets.",
help='Set this to False, to disable sending of ack packets. This will entirely stop'
'APRSD from sending ack packets.',
),
]
watch_list_opts = [
cfg.BoolOpt(
"enabled",
'enabled',
default=False,
help="Enable the watch list feature. Still have to enable "
"the correct plugin. Built-in plugin to use is "
"aprsd.plugins.notify.NotifyPlugin",
help='Enable the watch list feature. Still have to enable '
'the correct plugin. Built-in plugin to use is '
'aprsd.plugins.notify.NotifyPlugin',
),
cfg.ListOpt(
"callsigns",
help="Callsigns to watch for messsages",
'callsigns',
help='Callsigns to watch for messsages',
),
cfg.StrOpt(
"alert_callsign",
help="The Ham Callsign to send messages to for watch list alerts.",
'alert_callsign',
help='The Ham Callsign to send messages to for watch list alerts.',
),
cfg.IntOpt(
"packet_keep_count",
'packet_keep_count',
default=10,
help="The number of packets to store.",
help='The number of packets to store.',
),
cfg.IntOpt(
"alert_time_seconds",
'alert_time_seconds',
default=3600,
help="Time to wait before alert is sent on new message for "
"users in callsigns.",
help='Time to wait before alert is sent on new message for users in callsigns.',
),
]
enabled_plugins_opts = [
cfg.ListOpt(
"enabled_plugins",
'enabled_plugins',
default=[
"aprsd.plugins.fortune.FortunePlugin",
"aprsd.plugins.location.LocationPlugin",
"aprsd.plugins.ping.PingPlugin",
"aprsd.plugins.time.TimePlugin",
"aprsd.plugins.weather.OWMWeatherPlugin",
"aprsd.plugins.version.VersionPlugin",
"aprsd.plugins.notify.NotifySeenPlugin",
'aprsd.plugins.fortune.FortunePlugin',
'aprsd.plugins.location.LocationPlugin',
'aprsd.plugins.ping.PingPlugin',
'aprsd.plugins.time.TimePlugin',
'aprsd.plugins.weather.OWMWeatherPlugin',
'aprsd.plugins.version.VersionPlugin',
'aprsd.plugins.notify.NotifySeenPlugin',
],
help="Comma separated list of enabled plugins for APRSD."
"To enable installed external plugins add them here."
"The full python path to the class name must be used",
help='Comma separated list of enabled plugins for APRSD.'
'To enable installed external plugins add them here.'
'The full python path to the class name must be used',
),
]
registry_opts = [
cfg.BoolOpt(
"enabled",
'enabled',
default=False,
help="Enable sending aprs registry information. This will let the "
help='Enable sending aprs registry information. This will let the '
"APRS registry know about your service and it's uptime. "
"No personal information is sent, just the callsign, uptime and description. "
"The service callsign is the callsign set in [DEFAULT] section.",
'No personal information is sent, just the callsign, uptime and description. '
'The service callsign is the callsign set in [DEFAULT] section.',
),
cfg.StrOpt(
"description",
'description',
default=None,
help="Description of the service to send to the APRS registry. "
"This is what will show up in the APRS registry."
"If not set, the description will be the same as the callsign.",
help='Description of the service to send to the APRS registry. '
'This is what will show up in the APRS registry.'
'If not set, the description will be the same as the callsign.',
),
cfg.StrOpt(
"registry_url",
default="https://aprs.hemna.com/api/v1/registry",
help="The APRS registry domain name to send the information to.",
'registry_url',
default='https://aprs.hemna.com/api/v1/registry',
help='The APRS registry domain name to send the information to.',
),
cfg.StrOpt(
"service_website",
'service_website',
default=None,
help="The website for your APRS service to send to the APRS registry.",
help='The website for your APRS service to send to the APRS registry.',
),
cfg.IntOpt(
"frequency_seconds",
'frequency_seconds',
default=3600,
help="The frequency in seconds to send the APRS registry information.",
help='The frequency in seconds to send the APRS registry information.',
),
]
@@ -232,7 +231,7 @@ def register_opts(config):
def list_opts():
return {
"DEFAULT": (aprsd_opts + enabled_plugins_opts),
'DEFAULT': (aprsd_opts + enabled_plugins_opts),
watch_list_group.name: watch_list_opts,
registry_group.name: registry_opts,
}
+32 -22
View File
@@ -7,47 +7,57 @@ import logging
from oslo_config import cfg
LOG_LEVELS = {
"CRITICAL": logging.CRITICAL,
"ERROR": logging.ERROR,
"WARNING": logging.WARNING,
"INFO": logging.INFO,
"DEBUG": logging.DEBUG,
'CRITICAL': logging.CRITICAL,
'ERROR': logging.ERROR,
'WARNING': logging.WARNING,
'INFO': logging.INFO,
'DEBUG': logging.DEBUG,
}
DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p"
DEFAULT_DATE_FORMAT = '%m/%d/%Y %I:%M:%S %p'
DEFAULT_LOG_FORMAT = (
"[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s]"
" %(message)s - [%(pathname)s:%(lineno)d]"
'[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s]'
' %(message)s - [%(pathname)s:%(lineno)d]'
)
DEFAULT_LOG_FORMAT = (
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
"<yellow>{thread.name: <18}</yellow> | "
"<level>{level: <8}</level> | "
"<level>{message}</level> | "
"<cyan>{name}</cyan>:<cyan>{function:}</cyan>:<magenta>{line:}</magenta>"
'<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | '
'<yellow>{thread.name: <18}</yellow> | '
'<level>{level: <8}</level> | '
'<level>{message}</level> | '
'<cyan>{name}</cyan>:<cyan>{function:}</cyan>:<magenta>{line:}</magenta>'
)
logging_group = cfg.OptGroup(
name="logging",
title="Logging options",
name='logging',
title='Logging options',
)
logging_opts = [
cfg.StrOpt(
"logfile",
'logfile',
default=None,
help="File to log to",
help='File to log to',
),
cfg.StrOpt(
"logformat",
'logformat',
default=DEFAULT_LOG_FORMAT,
help="Log file format, unless rich_logging enabled.",
help='Log file format, unless rich_logging enabled.',
),
cfg.StrOpt(
"log_level",
default="INFO",
'log_level',
default='INFO',
choices=LOG_LEVELS.keys(),
help="Log level for logging of events.",
help='Log level for logging of events.',
),
cfg.BoolOpt(
'enable_color',
default=True,
help='Enable ANSI color codes in logging',
),
cfg.BoolOpt(
'enable_console_stdout',
default=True,
help='Enable logging to the console/stdout.',
),
]
+36 -20
View File
@@ -51,7 +51,7 @@ class InterceptHandler(logging.Handler):
# Setup the log faciility
# to disable log to stdout, but still log to file
# use the --quiet option on the cmdln
def setup_logging(loglevel=None, quiet=False):
def setup_logging(loglevel=None, quiet=False, custom_handler=None):
if not loglevel:
log_level = CONF.logging.log_level
else:
@@ -63,37 +63,53 @@ def setup_logging(loglevel=None, quiet=False):
# We don't really want to see the aprslib parsing debug output.
disable_list = [
"aprslib",
"aprslib.parsing",
"aprslib.exceptions",
'aprslib',
'aprslib.parsing',
'aprslib.exceptions',
]
chardet_list = [
'chardet',
'chardet.charsetprober',
'chardet.eucjpprober',
]
for name in chardet_list:
disable = logging.getLogger(name)
disable.setLevel(logging.ERROR)
# remove every other logger's handlers
# and propagate to root logger
for name in logging.root.manager.loggerDict.keys():
logging.getLogger(name).handlers = []
logging.getLogger(name).propagate = name not in disable_list
handlers = [
{
"sink": sys.stdout,
"serialize": False,
"format": CONF.logging.logformat,
"colorize": True,
"level": log_level,
},
]
if CONF.logging.logfile:
handlers = []
if CONF.logging.enable_console_stdout:
handlers.append(
{
"sink": CONF.logging.logfile,
"serialize": False,
"format": CONF.logging.logformat,
"colorize": False,
"level": log_level,
'sink': sys.stdout,
'serialize': False,
'format': CONF.logging.logformat,
'colorize': CONF.logging.enable_color,
'level': log_level,
},
)
if CONF.logging.logfile:
handlers.append(
{
'sink': CONF.logging.logfile,
'serialize': False,
'format': CONF.logging.logformat,
'colorize': False,
'level': log_level,
},
)
if custom_handler:
handlers.append(custom_handler)
# configure loguru
logger.configure(handlers=handlers)
logger.level("DEBUG", color="<fg #BABABA>")
logger.level('DEBUG', color='<fg #BABABA>')
+22 -22
View File
@@ -39,8 +39,8 @@ from aprsd.stats import collector
# setup the global logger
# log.basicConfig(level=log.DEBUG) # level=10
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
LOG = logging.getLogger('APRSD')
CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
flask_enabled = False
@@ -68,18 +68,18 @@ def main():
# First import all the possible commands for the CLI
# The commands themselves live in the cmds directory
load_commands()
utils.load_entry_points("aprsd.extension")
cli(auto_envvar_prefix="APRSD")
utils.load_entry_points('aprsd.extension')
cli(auto_envvar_prefix='APRSD')
def signal_handler(sig, frame):
global flask_enabled
click.echo("signal_handler: called")
click.echo('signal_handler: called')
threads.APRSDThreadList().stop_all()
if "subprocess" not in str(frame):
if 'subprocess' not in str(frame):
LOG.info(
"Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format(
'Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}'.format(
datetime.datetime.now(),
),
)
@@ -91,7 +91,7 @@ def signal_handler(sig, frame):
packets.PacketList().save()
collector.Collector().collect()
except Exception as e:
LOG.error(f"Failed to save data: {e}")
LOG.error(f'Failed to save data: {e}')
sys.exit(0)
# signal.signal(signal.SIGTERM, sys.exit(0))
# sys.exit(0)
@@ -108,9 +108,9 @@ def check_version(ctx):
"""Check this version against the latest in pypi.org."""
level, msg = utils._check_version()
if level:
click.secho(msg, fg="yellow")
click.secho(msg, fg='yellow')
else:
click.secho(msg, fg="green")
click.secho(msg, fg='green')
@cli.command()
@@ -124,12 +124,12 @@ def sample_config(ctx):
if sys.version_info < (3, 10):
all = imp.entry_points()
selected = []
if "oslo.config.opts" in all:
for x in all["oslo.config.opts"]:
if x.group == "oslo.config.opts":
if 'oslo.config.opts' in all:
for x in all['oslo.config.opts']:
if x.group == 'oslo.config.opts':
selected.append(x)
else:
selected = imp.entry_points(group="oslo.config.opts")
selected = imp.entry_points(group='oslo.config.opts')
return selected
@@ -139,23 +139,23 @@ def sample_config(ctx):
# selected = imp.entry_points(group="oslo.config.opts")
selected = _get_selected_entry_points()
for entry in selected:
if "aprsd" in entry.name:
args.append("--namespace")
if 'aprsd' in entry.name:
args.append('--namespace')
args.append(entry.name)
return args
args = get_namespaces()
config_version = metadata_version("oslo.config")
config_version = metadata_version('oslo.config')
logging.basicConfig(level=logging.WARN)
conf = cfg.ConfigOpts()
generator.register_cli_opts(conf)
try:
conf(args, version=config_version)
except cfg.RequiredOptError:
except cfg.RequiredOptError as ex:
conf.print_help()
if not sys.argv[1:]:
raise SystemExit
raise SystemExit from ex
raise
generator.generate(conf)
return
@@ -165,9 +165,9 @@ def sample_config(ctx):
@click.pass_context
def version(ctx):
"""Show the APRSD version."""
click.echo(click.style("APRSD Version : ", fg="white"), nl=False)
click.secho(f"{aprsd.__version__}", fg="yellow", bold=True)
click.echo(click.style('APRSD Version : ', fg='white'), nl=False)
click.secho(f'{aprsd.__version__}', fg='yellow', bold=True)
if __name__ == "__main__":
if __name__ == '__main__':
main()
+6
View File
@@ -15,6 +15,8 @@ from aprsd.packets.core import ( # noqa: F401
WeatherPacket,
factory,
)
from aprsd.packets.filter import PacketFilter
from aprsd.packets.filters.dupe_filter import DupePacketFilter
from aprsd.packets.packet_list import PacketList # noqa: F401
from aprsd.packets.seen_list import SeenList # noqa: F401
from aprsd.packets.tracker import PacketTrack # noqa: F401
@@ -26,5 +28,9 @@ collector.PacketCollector().register(SeenList)
collector.PacketCollector().register(PacketTrack)
collector.PacketCollector().register(WatchList)
# Register all the packet filters for normal processing
# For specific commands you can deregister these if you don't want them.
PacketFilter().register(DupePacketFilter)
NULL_MESSAGE = -1
+5 -2
View File
@@ -106,6 +106,8 @@ class Packet:
last_send_time: float = field(repr=False, default=0, compare=False, hash=False)
# Was the packet acked?
acked: bool = field(repr=False, default=False, compare=False, hash=False)
# Was the packet previously processed (for dupe checking)
processed: bool = field(repr=False, default=False, compare=False, hash=False)
# Do we allow this packet to be saved to send later?
allow_delay: bool = field(repr=False, default=True, compare=False, hash=False)
@@ -186,12 +188,11 @@ class Packet:
def __repr__(self) -> str:
"""Build the repr version of the packet."""
repr = (
return (
f"{self.__class__.__name__}:"
f" From: {self.from_call} "
f" To: {self.to_call}"
)
return repr
@dataclass_json
@@ -694,6 +695,8 @@ class UnknownPacket:
path: List[str] = field(default_factory=list, compare=False, hash=False)
packet_type: Optional[str] = field(default=None)
via: Optional[str] = field(default=None, compare=False, hash=False)
# Was the packet previously processed (for dupe checking)
processed: bool = field(repr=False, default=False, compare=False, hash=False)
@property
def key(self) -> str:
+58
View File
@@ -0,0 +1,58 @@
import logging
from typing import Callable, Protocol, runtime_checkable, Union, Dict
from aprsd.packets import core
from aprsd.utils import singleton
LOG = logging.getLogger("APRSD")
@runtime_checkable
class PacketFilterProtocol(Protocol):
"""Protocol API for a packet filter class.
"""
def filter(self, packet: type[core.Packet]) -> Union[type[core.Packet], None]:
"""When we get a packet from the network.
Return a Packet object if the filter passes. Return None if the
Packet is filtered out.
"""
...
@singleton
class PacketFilter:
def __init__(self):
self.filters: Dict[str, Callable] = {}
def register(self, packet_filter: Callable) -> None:
if not isinstance(packet_filter, PacketFilterProtocol):
raise TypeError(f"class {packet_filter} is not a PacketFilterProtocol object")
if packet_filter not in self.filters:
self.filters[packet_filter] = packet_filter()
def unregister(self, packet_filter: Callable) -> None:
if not isinstance(packet_filter, PacketFilterProtocol):
raise TypeError(f"class {packet_filter} is not a PacketFilterProtocol object")
if packet_filter in self.filters:
del self.filters[packet_filter]
def filter(self, packet: type[core.Packet]) -> Union[type[core.Packet], None]:
"""Run through each of the filters.
This will step through each registered filter class
and call filter on it.
If the filter object returns None, we are done filtering.
If the filter object returns the packet, we continue filtering.
"""
for packet_filter in self.filters:
try:
if not self.filters[packet_filter].filter(packet):
LOG.debug(f"{self.filters[packet_filter].__class__.__name__} dropped {packet.__class__.__name__}:{packet.human_info}")
return None
except Exception as ex:
LOG.error(f"{packet_filter.__clas__.__name__} failed filtering packet {packet.__class__.__name__} : {ex}")
return packet
View File
+68
View File
@@ -0,0 +1,68 @@
import logging
from typing import Union
from oslo_config import cfg
from aprsd import packets
from aprsd.packets import core
CONF = cfg.CONF
LOG = logging.getLogger('APRSD')
class DupePacketFilter:
"""This is a packet filter to detect duplicate packets.
This Uses the PacketList object to see if a packet exists
already. If it does exist in the PacketList, then we need to
check the flag on the packet to see if it's been processed before.
If the packet has been processed already within the allowed
timeframe, then it's a dupe.
"""
def filter(self, packet: type[core.Packet]) -> Union[type[core.Packet], None]:
# LOG.debug(f"{self.__class__.__name__}.filter called for packet {packet}")
"""Filter a packet out if it's already been seen and processed."""
if isinstance(packet, core.AckPacket):
# We don't need to drop AckPackets, those should be
# processed.
# Send the AckPacket to the queue for processing elsewhere.
return packet
else:
# Make sure we aren't re-processing the same packet
# For RF based APRS Clients we can get duplicate packets
# So we need to track them and not process the dupes.
pkt_list = packets.PacketList()
found = False
try:
# Find the packet in the list of already seen packets
# Based on the packet.key
found = pkt_list.find(packet)
if not packet.msgNo:
# If the packet doesn't have a message id
# then there is no reliable way to detect
# if it's a dupe, so we just pass it on.
# it shouldn't get acked either.
found = False
except KeyError:
found = False
if not found:
# We haven't seen this packet before, so we process it.
return packet
if not packet.processed:
# We haven't processed this packet through the plugins.
return packet
elif packet.timestamp - found.timestamp < CONF.packet_dupe_timeout:
# If the packet came in within N seconds of the
# Last time seeing the packet, then we drop it as a dupe.
LOG.warning(
f'Packet {packet.from_call}:{packet.msgNo} already tracked, dropping.'
)
else:
LOG.warning(
f'Packet {packet.from_call}:{packet.msgNo} already tracked '
f'but older than {CONF.packet_dupe_timeout} seconds. processing.',
)
return packet
+53
View File
@@ -0,0 +1,53 @@
import logging
from typing import Union
from oslo_config import cfg
from aprsd import packets
from aprsd.packets import core
from aprsd.utils import singleton
CONF = cfg.CONF
LOG = logging.getLogger('APRSD')
@singleton
class PacketTypeFilter:
"""This filter is used to filter out packets that don't match a specific type.
To use this, register it with the PacketFilter class,
then instante it and call set_allow_list() with a list of packet types
you want to allow to pass the filtering. All other packets will be
filtered out.
"""
filters = {
packets.Packet.__name__: packets.Packet,
packets.AckPacket.__name__: packets.AckPacket,
packets.BeaconPacket.__name__: packets.BeaconPacket,
packets.GPSPacket.__name__: packets.GPSPacket,
packets.MessagePacket.__name__: packets.MessagePacket,
packets.MicEPacket.__name__: packets.MicEPacket,
packets.ObjectPacket.__name__: packets.ObjectPacket,
packets.StatusPacket.__name__: packets.StatusPacket,
packets.ThirdPartyPacket.__name__: packets.ThirdPartyPacket,
packets.WeatherPacket.__name__: packets.WeatherPacket,
packets.UnknownPacket.__name__: packets.UnknownPacket,
}
allow_list = ()
def set_allow_list(self, filter_list):
tmp_list = []
for filter in filter_list:
LOG.warning(
f'Setting filter {filter} : {self.filters[filter]} to tmp {tmp_list}'
)
tmp_list.append(self.filters[filter])
self.allow_list = tuple(tmp_list)
def filter(self, packet: type[core.Packet]) -> Union[type[core.Packet], None]:
"""Only allow packets of certain types to filter through."""
if self.allow_list:
if isinstance(packet, self.allow_list):
return packet
+33 -27
View File
@@ -7,7 +7,7 @@ from aprsd.packets import core
from aprsd.utils import objectstore
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
LOG = logging.getLogger('APRSD')
class PacketList(objectstore.ObjectStoreMixin):
@@ -27,8 +27,8 @@ class PacketList(objectstore.ObjectStoreMixin):
def _init_data(self):
self.data = {
"types": {},
"packets": OrderedDict(),
'types': {},
'packets': OrderedDict(),
}
def rx(self, packet: type[core.Packet]):
@@ -37,11 +37,11 @@ class PacketList(objectstore.ObjectStoreMixin):
self._total_rx += 1
self._add(packet)
ptype = packet.__class__.__name__
type_stats = self.data["types"].setdefault(
type_stats = self.data['types'].setdefault(
ptype,
{"tx": 0, "rx": 0},
{'tx': 0, 'rx': 0},
)
type_stats["rx"] += 1
type_stats['rx'] += 1
def tx(self, packet: type[core.Packet]):
"""Add a packet that was received."""
@@ -49,32 +49,32 @@ class PacketList(objectstore.ObjectStoreMixin):
self._total_tx += 1
self._add(packet)
ptype = packet.__class__.__name__
type_stats = self.data["types"].setdefault(
type_stats = self.data['types'].setdefault(
ptype,
{"tx": 0, "rx": 0},
{'tx': 0, 'rx': 0},
)
type_stats["tx"] += 1
type_stats['tx'] += 1
def add(self, packet):
with self.lock:
self._add(packet)
def _add(self, packet):
if not self.data.get("packets"):
if not self.data.get('packets'):
self._init_data()
if packet.key in self.data["packets"]:
self.data["packets"].move_to_end(packet.key)
elif len(self.data["packets"]) == self.maxlen:
self.data["packets"].popitem(last=False)
self.data["packets"][packet.key] = packet
if packet.key in self.data['packets']:
self.data['packets'].move_to_end(packet.key)
elif len(self.data['packets']) == self.maxlen:
self.data['packets'].popitem(last=False)
self.data['packets'][packet.key] = packet
def find(self, packet):
with self.lock:
return self.data["packets"][packet.key]
return self.data['packets'][packet.key]
def __len__(self):
with self.lock:
return len(self.data["packets"])
return len(self.data['packets'])
def total_rx(self):
with self.lock:
@@ -87,17 +87,23 @@ class PacketList(objectstore.ObjectStoreMixin):
def stats(self, serializable=False) -> dict:
with self.lock:
# Get last N packets directly using list slicing
packets_list = list(self.data.get("packets", {}).values())
pkts = packets_list[-CONF.packet_list_stats_maxlen :][::-1]
if CONF.packet_list_stats_maxlen >= 0:
packets_list = list(self.data.get('packets', {}).values())
pkts = packets_list[-CONF.packet_list_stats_maxlen :][::-1]
else:
# We have to copy here, because this get() results in a pointer
# to the packets internally here, which can change after this
# function returns, which would cause a problem trying to save
# the stats to disk.
pkts = self.data.get('packets', {}).copy()
stats = {
"total_tracked": self._total_rx
'total_tracked': self._total_rx
+ self._total_tx, # Fixed typo: was rx + rx
"rx": self._total_rx,
"tx": self._total_tx,
"types": self.data.get("types", {}), # Changed default from [] to {}
"packet_count": len(self.data.get("packets", [])),
"maxlen": self.maxlen,
"packets": pkts,
'rx': self._total_rx,
'tx': self._total_tx,
'types': self.data.get('types', {}), # Changed default from [] to {}
'packet_count': len(self.data.get('packets', [])),
'maxlen': self.maxlen,
'packets': pkts,
}
return stats
+52 -52
View File
@@ -17,24 +17,24 @@ from aprsd.packets import watch_list
# setup the global logger
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
LOG = logging.getLogger('APRSD')
CORE_MESSAGE_PLUGINS = [
"aprsd.plugins.email.EmailPlugin",
"aprsd.plugins.fortune.FortunePlugin",
"aprsd.plugins.location.LocationPlugin",
"aprsd.plugins.ping.PingPlugin",
"aprsd.plugins.time.TimePlugin",
"aprsd.plugins.weather.USWeatherPlugin",
"aprsd.plugins.version.VersionPlugin",
'aprsd.plugins.email.EmailPlugin',
'aprsd.plugins.fortune.FortunePlugin',
'aprsd.plugins.location.LocationPlugin',
'aprsd.plugins.ping.PingPlugin',
'aprsd.plugins.time.TimePlugin',
'aprsd.plugins.weather.USWeatherPlugin',
'aprsd.plugins.version.VersionPlugin',
]
CORE_NOTIFY_PLUGINS = [
"aprsd.plugins.notify.NotifySeenPlugin",
'aprsd.plugins.notify.NotifySeenPlugin',
]
hookspec = pluggy.HookspecMarker("aprsd")
hookimpl = pluggy.HookimplMarker("aprsd")
hookspec = pluggy.HookspecMarker('aprsd')
hookimpl = pluggy.HookimplMarker('aprsd')
class APRSDPluginSpec:
@@ -76,14 +76,14 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
else:
LOG.error(
"Can't start thread {}:{}, Must be a child "
"of aprsd.threads.APRSDThread".format(
'of aprsd.threads.APRSDThread'.format(
self,
thread,
),
)
except Exception:
LOG.error(
"Failed to start threads for plugin {}".format(
'Failed to start threads for plugin {}'.format(
self,
),
)
@@ -93,7 +93,7 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
return self.message_counter
def help(self) -> str:
return "Help!"
return 'Help!'
@abc.abstractmethod
def setup(self):
@@ -147,10 +147,10 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
# make sure the timeout is set or this doesn't work
if watch_list:
aprs_client = client.client_factory.create().client
filter_str = "b/{}".format("/".join(watch_list))
filter_str = 'b/{}'.format('/'.join(watch_list))
aprs_client.set_filter(filter_str)
else:
LOG.warning("Watch list enabled, but no callsigns set.")
LOG.warning('Watch list enabled, but no callsigns set.')
@hookimpl
def filter(self, packet: type[packets.Packet]) -> str | packets.MessagePacket:
@@ -164,7 +164,7 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
result = self.process(packet)
except Exception as ex:
LOG.error(
"Plugin {} failed to process packet {}".format(
'Plugin {} failed to process packet {}'.format(
self.__class__,
ex,
),
@@ -172,7 +172,7 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
if result:
self.tx_inc()
else:
LOG.warning(f"{self.__class__} plugin is not enabled")
LOG.warning(f'{self.__class__} plugin is not enabled')
return result
@@ -196,7 +196,7 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
raise NotImplementedError
def help(self):
return "{}: {}".format(
return '{}: {}'.format(
self.command_name.lower(),
self.command_regex,
)
@@ -207,7 +207,7 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
@hookimpl
def filter(self, packet: packets.MessagePacket) -> str | packets.MessagePacket:
LOG.debug(f"{self.__class__.__name__} called")
LOG.debug(f'{self.__class__.__name__} called')
if not self.enabled:
result = f"{self.__class__.__name__} isn't enabled"
LOG.warning(result)
@@ -215,7 +215,7 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
if not isinstance(packet, packets.MessagePacket):
LOG.warning(
f"{self.__class__.__name__} Got a {packet.__class__.__name__} ignoring"
f'{self.__class__.__name__} Got a {packet.__class__.__name__} ignoring'
)
return packets.NULL_MESSAGE
@@ -237,7 +237,7 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
result = self.process(packet)
except Exception as ex:
LOG.error(
"Plugin {} failed to process packet {}".format(
'Plugin {} failed to process packet {}'.format(
self.__class__,
ex,
),
@@ -254,7 +254,7 @@ class APRSFIKEYMixin:
def ensure_aprs_fi_key(self):
if not CONF.aprs_fi.apiKey:
LOG.error("Config aprs_fi.apiKey is not set")
LOG.error('Config aprs_fi.apiKey is not set')
self.enabled = False
else:
self.enabled = True
@@ -266,25 +266,25 @@ class HelpPlugin(APRSDRegexCommandPluginBase):
This plugin is in this file to prevent a circular import.
"""
command_regex = "^[hH]"
command_name = "help"
command_regex = '^[hH]'
command_name = 'help'
def help(self):
return "Help: send APRS help or help <plugin>"
return 'Help: send APRS help or help <plugin>'
def process(self, packet: packets.MessagePacket):
LOG.info("HelpPlugin")
LOG.info('HelpPlugin')
# fromcall = packet.get("from")
message = packet.message_text
# ack = packet.get("msgNo", "0")
a = re.search(r"^.*\s+(.*)", message)
a = re.search(r'^.*\s+(.*)', message)
command_name = None
if a is not None:
command_name = a.group(1).lower()
pm = PluginManager()
if command_name and "?" not in command_name:
if command_name and '?' not in command_name:
# user wants help for a specific plugin
reply = None
for p in pm.get_plugins():
@@ -303,20 +303,20 @@ class HelpPlugin(APRSDRegexCommandPluginBase):
LOG.debug(p)
if p.enabled and isinstance(p, APRSDRegexCommandPluginBase):
name = p.command_name.lower()
if name not in list and "help" not in name:
if name not in list and 'help' not in name:
list.append(name)
list.sort()
reply = " ".join(list)
reply = ' '.join(list)
lines = textwrap.wrap(reply, 60)
replies = ["Send APRS MSG of 'help' or 'help <plugin>'"]
for line in lines:
replies.append(f"plugins: {line}")
replies.append(f'plugins: {line}')
for entry in replies:
LOG.debug(f"{len(entry)} {entry}")
LOG.debug(f'{len(entry)} {entry}')
LOG.debug(f"{replies}")
LOG.debug(f'{replies}')
return replies
@@ -341,17 +341,17 @@ class PluginManager:
return cls._instance
def _init(self):
self._pluggy_pm = pluggy.PluginManager("aprsd")
self._pluggy_pm = pluggy.PluginManager('aprsd')
self._pluggy_pm.add_hookspecs(APRSDPluginSpec)
# For the watchlist plugins
self._watchlist_pm = pluggy.PluginManager("aprsd")
self._watchlist_pm = pluggy.PluginManager('aprsd')
self._watchlist_pm.add_hookspecs(APRSDPluginSpec)
def stats(self, serializable=False) -> dict:
"""Collect and return stats for all plugins."""
def full_name_with_qualname(obj):
return "{}.{}".format(
return '{}.{}'.format(
obj.__class__.__module__,
obj.__class__.__qualname__,
)
@@ -361,10 +361,10 @@ class PluginManager:
if plugins:
for p in plugins:
plugin_stats[full_name_with_qualname(p)] = {
"enabled": p.enabled,
"rx": p.rx_count,
"tx": p.tx_count,
"version": p.version,
'enabled': p.enabled,
'rx': p.rx_count,
'tx': p.tx_count,
'version': p.version,
}
return plugin_stats
@@ -392,19 +392,19 @@ class PluginManager:
module_name = None
class_name = None
try:
module_name, class_name = module_class_string.rsplit(".", 1)
module_name, class_name = module_class_string.rsplit('.', 1)
module = importlib.import_module(module_name)
# Commented out because the email thread starts in a different context
# and hence gives a different singleton for the EmailStats
# module = importlib.reload(module)
except Exception as ex:
if not module_name:
LOG.error(f"Failed to load Plugin {module_class_string}")
LOG.error(f'Failed to load Plugin {module_class_string}')
else:
LOG.error(f"Failed to load Plugin '{module_name}' : '{ex}'")
return
assert hasattr(module, class_name), "class {} is not in {}".format(
assert hasattr(module, class_name), 'class {} is not in {}'.format(
class_name,
module_name,
)
@@ -412,7 +412,7 @@ class PluginManager:
# class_name, module_name))
cls = getattr(module, class_name)
if super_cls is not None:
assert issubclass(cls, super_cls), "class {} should inherit from {}".format(
assert issubclass(cls, super_cls), 'class {} should inherit from {}'.format(
class_name,
super_cls.__name__,
)
@@ -444,7 +444,7 @@ class PluginManager:
self._watchlist_pm.register(plugin_obj)
else:
LOG.warning(
f"Plugin {plugin_obj.__class__.__name__} is disabled"
f'Plugin {plugin_obj.__class__.__name__} is disabled'
)
elif isinstance(plugin_obj, APRSDRegexCommandPluginBase):
if plugin_obj.enabled:
@@ -458,7 +458,7 @@ class PluginManager:
self._pluggy_pm.register(plugin_obj)
else:
LOG.warning(
f"Plugin {plugin_obj.__class__.__name__} is disabled"
f'Plugin {plugin_obj.__class__.__name__} is disabled'
)
elif isinstance(plugin_obj, APRSDPluginBase):
if plugin_obj.enabled:
@@ -471,7 +471,7 @@ class PluginManager:
self._pluggy_pm.register(plugin_obj)
else:
LOG.warning(
f"Plugin {plugin_obj.__class__.__name__} is disabled"
f'Plugin {plugin_obj.__class__.__name__} is disabled'
)
except Exception as ex:
LOG.error(f"Couldn't load plugin '{plugin_name}'")
@@ -485,11 +485,11 @@ class PluginManager:
def setup_plugins(
self,
load_help_plugin=True,
plugin_list=[],
plugin_list=None,
):
"""Create the plugin manager and register plugins."""
LOG.info("Loading APRSD Plugins")
LOG.info('Loading APRSD Plugins')
# Help plugin is always enabled.
if load_help_plugin:
_help = HelpPlugin()
@@ -509,7 +509,7 @@ class PluginManager:
for p_name in CORE_MESSAGE_PLUGINS:
self._load_plugin(p_name)
LOG.info("Completed Plugin Loading.")
LOG.info('Completed Plugin Loading.')
def run(self, packet: packets.MessagePacket):
"""Execute all the plugins run method."""
@@ -524,7 +524,7 @@ class PluginManager:
"""Stop all threads created by all plugins."""
with self.lock:
for p in self.get_plugins():
if hasattr(p, "stop_threads"):
if hasattr(p, 'stop_threads'):
p.stop_threads()
def register_msg(self, obj):
+20 -21
View File
@@ -4,21 +4,20 @@ import logging
import requests
LOG = logging.getLogger("APRSD")
LOG = logging.getLogger('APRSD')
def get_aprs_fi(api_key, callsign):
LOG.debug(f"Fetch aprs.fi location for '{callsign}'")
try:
url = (
"http://api.aprs.fi/api/get?"
"&what=loc&apikey={}&format=json"
"&name={}".format(api_key, callsign)
'http://api.aprs.fi/api/get?&what=loc&apikey={}&format=json&name={}'.format(
api_key, callsign
)
)
response = requests.get(url)
except Exception:
raise Exception("Failed to get aprs.fi location")
except Exception as e:
raise Exception('Failed to get aprs.fi location') from e
else:
response.raise_for_status()
return json.loads(response.text)
@@ -26,22 +25,22 @@ def get_aprs_fi(api_key, callsign):
def get_weather_gov_for_gps(lat, lon):
# FIXME(hemna) This is currently BROKEN
LOG.debug(f"Fetch station at {lat}, {lon}")
LOG.debug(f'Fetch station at {lat}, {lon}')
headers = requests.utils.default_headers()
headers.update(
{"User-Agent": "(aprsd, waboring@hemna.com)"},
{'User-Agent': '(aprsd, waboring@hemna.com)'},
)
try:
url2 = (
"https://forecast.weather.gov/MapClick.php?lat=%s"
"&lon=%s&FcstType=json" % (lat, lon)
'https://forecast.weather.gov/MapClick.php?lat=%s'
'&lon=%s&FcstType=json' % (lat, lon)
# f"https://api.weather.gov/points/{lat},{lon}"
)
LOG.debug(f"Fetching weather '{url2}'")
response = requests.get(url2, headers=headers)
except Exception as e:
LOG.error(e)
raise Exception("Failed to get weather")
raise Exception('Failed to get weather') from e
else:
response.raise_for_status()
return json.loads(response.text)
@@ -50,25 +49,25 @@ def get_weather_gov_for_gps(lat, lon):
def get_weather_gov_metar(station):
LOG.debug(f"Fetch metar for station '{station}'")
try:
url = "https://api.weather.gov/stations/{}/observations/latest".format(
url = 'https://api.weather.gov/stations/{}/observations/latest'.format(
station,
)
response = requests.get(url)
except Exception:
raise Exception("Failed to fetch metar")
except Exception as e:
raise Exception('Failed to fetch metar') from e
else:
response.raise_for_status()
return json.loads(response)
def fetch_openweathermap(api_key, lat, lon, units="metric", exclude=None):
LOG.debug(f"Fetch openweathermap for {lat}, {lon}")
def fetch_openweathermap(api_key, lat, lon, units='metric', exclude=None):
LOG.debug(f'Fetch openweathermap for {lat}, {lon}')
if not exclude:
exclude = "minutely,hourly,daily,alerts"
exclude = 'minutely,hourly,daily,alerts'
try:
url = (
"https://api.openweathermap.org/data/2.5/onecall?"
"lat={}&lon={}&appid={}&units={}&exclude={}".format(
'https://api.openweathermap.org/data/2.5/onecall?'
'lat={}&lon={}&appid={}&units={}&exclude={}'.format(
lat,
lon,
api_key,
@@ -80,7 +79,7 @@ def fetch_openweathermap(api_key, lat, lon, units="metric", exclude=None):
response = requests.get(url)
except Exception as e:
LOG.error(e)
raise Exception("Failed to get weather")
raise Exception('Failed to get weather') from e
else:
response.raise_for_status()
return json.loads(response.text)
+110 -109
View File
@@ -9,7 +9,7 @@ from aprsd import plugin, plugin_utils
from aprsd.utils import trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
LOG = logging.getLogger('APRSD')
class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
@@ -26,22 +26,22 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
"""
# command_regex = r"^([w][x]|[w][x]\s|weather)"
command_regex = r"^[wW]"
command_regex = r'^[wW]'
command_name = "USWeather"
short_description = "Provide USA only weather of GPS Beacon location"
command_name = 'USWeather'
short_description = 'Provide USA only weather of GPS Beacon location'
def setup(self):
self.ensure_aprs_fi_key()
@trace.trace
def process(self, packet):
LOG.info("Weather Plugin")
LOG.info('Weather Plugin')
fromcall = packet.from_call
message = packet.get("message_text", None)
message = packet.get('message_text', None)
# message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0")
a = re.search(r"^.*\s+(.*)", message)
a = re.search(r'^.*\s+(.*)', message)
if a is not None:
searchcall = a.group(1)
searchcall = searchcall.upper()
@@ -51,34 +51,34 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
try:
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
except Exception as ex:
LOG.error(f"Failed to fetch aprs.fi data {ex}")
return "Failed to fetch aprs.fi location"
LOG.error(f'Failed to fetch aprs.fi data {ex}')
return 'Failed to fetch aprs.fi location'
LOG.debug(f"LocationPlugin: aprs_data = {aprs_data}")
if not len(aprs_data["entries"]):
LOG.debug(f'LocationPlugin: aprs_data = {aprs_data}')
if not len(aprs_data['entries']):
LOG.error("Didn't get any entries from aprs.fi")
return "Failed to fetch aprs.fi location"
return 'Failed to fetch aprs.fi location'
lat = aprs_data["entries"][0]["lat"]
lon = aprs_data["entries"][0]["lng"]
lat = aprs_data['entries'][0]['lat']
lon = aprs_data['entries'][0]['lng']
try:
wx_data = plugin_utils.get_weather_gov_for_gps(lat, lon)
except Exception as ex:
LOG.error(f"Couldn't fetch forecast.weather.gov '{ex}'")
return "Unable to get weather"
return 'Unable to get weather'
LOG.info(f"WX data {wx_data}")
LOG.info(f'WX data {wx_data}')
reply = (
"%sF(%sF/%sF) %s. %s, %s."
'%sF(%sF/%sF) %s. %s, %s.'
% (
wx_data["currentobservation"]["Temp"],
wx_data["data"]["temperature"][0],
wx_data["data"]["temperature"][1],
wx_data["data"]["weather"][0],
wx_data["time"]["startPeriodName"][1],
wx_data["data"]["weather"][1],
wx_data['currentobservation']['Temp'],
wx_data['data']['temperature'][0],
wx_data['data']['temperature'][1],
wx_data['data']['weather'][0],
wx_data['time']['startPeriodName'][1],
wx_data['data']['weather'][1],
)
).rstrip()
LOG.debug(f"reply: '{reply}' ")
@@ -100,31 +100,31 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
"""
command_regex = r"^([m]|[M]|[m]\s|metar)"
command_name = "USMetar"
short_description = "USA only METAR of GPS Beacon location"
command_regex = r'^([m]|[M]|[m]\s|metar)'
command_name = 'USMetar'
short_description = 'USA only METAR of GPS Beacon location'
def setup(self):
self.ensure_aprs_fi_key()
@trace.trace
def process(self, packet):
fromcall = packet.get("from")
message = packet.get("message_text", None)
fromcall = packet.get('from')
message = packet.get('message_text', None)
# ack = packet.get("msgNo", "0")
LOG.info(f"WX Plugin '{message}'")
a = re.search(r"^.*\s+(.*)", message)
a = re.search(r'^.*\s+(.*)', message)
if a is not None:
searchcall = a.group(1)
station = searchcall.upper()
try:
resp = plugin_utils.get_weather_gov_metar(station)
except Exception as e:
LOG.debug(f"Weather failed with: {str(e)}")
reply = "Unable to find station METAR"
LOG.debug(f'Weather failed with: {str(e)}')
reply = 'Unable to find station METAR'
else:
station_data = json.loads(resp.text)
reply = station_data["properties"]["rawMessage"]
reply = station_data['properties']['rawMessage']
return reply
else:
@@ -136,36 +136,36 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
try:
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
except Exception as ex:
LOG.error(f"Failed to fetch aprs.fi data {ex}")
return "Failed to fetch aprs.fi location"
LOG.error(f'Failed to fetch aprs.fi data {ex}')
return 'Failed to fetch aprs.fi location'
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
if not len(aprs_data["entries"]):
LOG.error("Found no entries from aprs.fi!")
return "Failed to fetch aprs.fi location"
if not len(aprs_data['entries']):
LOG.error('Found no entries from aprs.fi!')
return 'Failed to fetch aprs.fi location'
lat = aprs_data["entries"][0]["lat"]
lon = aprs_data["entries"][0]["lng"]
lat = aprs_data['entries'][0]['lat']
lon = aprs_data['entries'][0]['lng']
try:
wx_data = plugin_utils.get_weather_gov_for_gps(lat, lon)
except Exception as ex:
LOG.error(f"Couldn't fetch forecast.weather.gov '{ex}'")
return "Unable to metar find station."
return 'Unable to metar find station.'
if wx_data["location"]["metar"]:
station = wx_data["location"]["metar"]
if wx_data['location']['metar']:
station = wx_data['location']['metar']
try:
resp = plugin_utils.get_weather_gov_metar(station)
except Exception as e:
LOG.debug(f"Weather failed with: {str(e)}")
reply = "Failed to get Metar"
LOG.debug(f'Weather failed with: {str(e)}')
reply = 'Failed to get Metar'
else:
station_data = json.loads(resp.text)
reply = station_data["properties"]["rawMessage"]
reply = station_data['properties']['rawMessage']
else:
# Couldn't find a station
reply = "No Metar station found"
reply = 'No Metar station found'
return reply
@@ -190,35 +190,36 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
"""
# command_regex = r"^([w][x]|[w][x]\s|weather)"
command_regex = r"^[wW]"
command_regex = r'^[wW]'
command_name = "OpenWeatherMap"
short_description = "OpenWeatherMap weather of GPS Beacon location"
command_name = 'OpenWeatherMap'
short_description = 'OpenWeatherMap weather of GPS Beacon location'
def setup(self):
if not CONF.owm_weather_plugin.apiKey:
LOG.error("Config.owm_weather_plugin.apiKey is not set. Disabling")
LOG.error('Config.owm_weather_plugin.apiKey is not set. Disabling')
self.enabled = False
else:
self.enabled = True
def help(self):
_help = [
"openweathermap: Send {} to get weather " "from your location".format(
'openweathermap: Send {} to get weather from your location'.format(
self.command_regex
),
'openweathermap: Send {} <callsign> to get weather from <callsign>'.format(
self.command_regex
),
"openweathermap: Send {} <callsign> to get "
"weather from <callsign>".format(self.command_regex),
]
return _help
@trace.trace
def process(self, packet):
fromcall = packet.get("from_call")
message = packet.get("message_text", None)
fromcall = packet.get('from_call')
message = packet.get('message_text', None)
# ack = packet.get("msgNo", "0")
LOG.info(f"OWMWeather Plugin '{message}'")
a = re.search(r"^.*\s+(.*)", message)
a = re.search(r'^.*\s+(.*)', message)
if a is not None:
searchcall = a.group(1)
searchcall = searchcall.upper()
@@ -230,16 +231,16 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
try:
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
except Exception as ex:
LOG.error(f"Failed to fetch aprs.fi data {ex}")
return "Failed to fetch location"
LOG.error(f'Failed to fetch aprs.fi data {ex}')
return 'Failed to fetch location'
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
if not len(aprs_data["entries"]):
LOG.error("Found no entries from aprs.fi!")
return "Failed to fetch location"
if not len(aprs_data['entries']):
LOG.error('Found no entries from aprs.fi!')
return 'Failed to fetch location'
lat = aprs_data["entries"][0]["lat"]
lon = aprs_data["entries"][0]["lng"]
lat = aprs_data['entries'][0]['lat']
lon = aprs_data['entries'][0]['lng']
units = CONF.units
api_key = CONF.owm_weather_plugin.apiKey
@@ -249,40 +250,40 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
lat,
lon,
units=units,
exclude="minutely,hourly",
exclude='minutely,hourly',
)
except Exception as ex:
LOG.error(f"Couldn't fetch openweathermap api '{ex}'")
# default to UTC
return "Unable to get weather"
return 'Unable to get weather'
if units == "metric":
degree = "C"
if units == 'metric':
degree = 'C'
else:
degree = "F"
degree = 'F'
if "wind_gust" in wx_data["current"]:
wind = "{:.0f}@{}G{:.0f}".format(
wx_data["current"]["wind_speed"],
wx_data["current"]["wind_deg"],
wx_data["current"]["wind_gust"],
if 'wind_gust' in wx_data['current']:
wind = '{:.0f}@{}G{:.0f}'.format(
wx_data['current']['wind_speed'],
wx_data['current']['wind_deg'],
wx_data['current']['wind_gust'],
)
else:
wind = "{:.0f}@{}".format(
wx_data["current"]["wind_speed"],
wx_data["current"]["wind_deg"],
wind = '{:.0f}@{}'.format(
wx_data['current']['wind_speed'],
wx_data['current']['wind_deg'],
)
# LOG.debug(wx_data["current"])
# LOG.debug(wx_data["daily"])
reply = "{} {:.1f}{}/{:.1f}{} Wind {} {}%".format(
wx_data["current"]["weather"][0]["description"],
wx_data["current"]["temp"],
reply = '{} {:.1f}{}/{:.1f}{} Wind {} {}%'.format(
wx_data['current']['weather'][0]['description'],
wx_data['current']['temp'],
degree,
wx_data["current"]["dew_point"],
wx_data['current']['dew_point'],
degree,
wind,
wx_data["current"]["humidity"],
wx_data['current']['humidity'],
)
return reply
@@ -311,26 +312,26 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
docker build -f Dockerfile -t avwx-api:master .
"""
command_regex = r"^([m]|[m]|[m]\s|metar)"
command_name = "AVWXWeather"
short_description = "AVWX weather of GPS Beacon location"
command_regex = r'^([m]|[m]|[m]\s|metar)'
command_name = 'AVWXWeather'
short_description = 'AVWX weather of GPS Beacon location'
def setup(self):
if not CONF.avwx_plugin.base_url:
LOG.error("Config avwx_plugin.base_url not specified. Disabling")
LOG.error('Config avwx_plugin.base_url not specified. Disabling')
return False
elif not CONF.avwx_plugin.apiKey:
LOG.error("Config avwx_plugin.apiKey not specified. Disabling")
LOG.error('Config avwx_plugin.apiKey not specified. Disabling')
return False
else:
return True
def help(self):
_help = [
"avwxweather: Send {} to get weather " "from your location".format(
'avwxweather: Send {} to get weather from your location'.format(
self.command_regex
),
"avwxweather: Send {} <callsign> to get " "weather from <callsign>".format(
'avwxweather: Send {} <callsign> to get weather from <callsign>'.format(
self.command_regex
),
]
@@ -338,11 +339,11 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
@trace.trace
def process(self, packet):
fromcall = packet.get("from")
message = packet.get("message_text", None)
fromcall = packet.get('from')
message = packet.get('message_text', None)
# ack = packet.get("msgNo", "0")
LOG.info(f"AVWXWeather Plugin '{message}'")
a = re.search(r"^.*\s+(.*)", message)
a = re.search(r'^.*\s+(.*)', message)
if a is not None:
searchcall = a.group(1)
searchcall = searchcall.upper()
@@ -353,43 +354,43 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
try:
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
except Exception as ex:
LOG.error(f"Failed to fetch aprs.fi data {ex}")
return "Failed to fetch location"
LOG.error(f'Failed to fetch aprs.fi data {ex}')
return 'Failed to fetch location'
# LOG.debug("LocationPlugin: aprs_data = {}".format(aprs_data))
if not len(aprs_data["entries"]):
LOG.error("Found no entries from aprs.fi!")
return "Failed to fetch location"
if not len(aprs_data['entries']):
LOG.error('Found no entries from aprs.fi!')
return 'Failed to fetch location'
lat = aprs_data["entries"][0]["lat"]
lon = aprs_data["entries"][0]["lng"]
lat = aprs_data['entries'][0]['lat']
lon = aprs_data['entries'][0]['lng']
api_key = CONF.avwx_plugin.apiKey
base_url = CONF.avwx_plugin.base_url
token = f"TOKEN {api_key}"
headers = {"Authorization": token}
token = f'TOKEN {api_key}'
headers = {'Authorization': token}
try:
coord = f"{lat},{lon}"
coord = f'{lat},{lon}'
url = (
"{}/api/station/near/{}?"
"n=1&airport=false&reporting=true&format=json".format(base_url, coord)
'{}/api/station/near/{}?'
'n=1&airport=false&reporting=true&format=json'.format(base_url, coord)
)
LOG.debug(f"Get stations near me '{url}'")
response = requests.get(url, headers=headers)
except Exception as ex:
LOG.error(ex)
raise Exception(f"Failed to get the weather '{ex}'")
raise Exception(f"Failed to get the weather '{ex}'") from ex
else:
wx_data = json.loads(response.text)
# LOG.debug(wx_data)
station = wx_data[0]["station"]["icao"]
station = wx_data[0]['station']['icao']
try:
url = (
"{}/api/metar/{}?options=info,translate,summary"
"&airport=true&reporting=true&format=json&onfail=cache".format(
'{}/api/metar/{}?options=info,translate,summary'
'&airport=true&reporting=true&format=json&onfail=cache'.format(
base_url,
station,
)
@@ -399,9 +400,9 @@ class AVWXWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
response = requests.get(url, headers=headers)
except Exception as ex:
LOG.error(ex)
raise Exception(f"Failed to get metar {ex}")
raise Exception(f'Failed to get metar {ex}') from ex
else:
metar_data = json.loads(response.text)
# LOG.debug(metar_data)
return metar_data["raw"]
return metar_data['raw']
+1 -2
View File
@@ -4,9 +4,8 @@ import queue
# aprsd.threads
from .aprsd import APRSDThread, APRSDThreadList # noqa: F401
from .rx import ( # noqa: F401
APRSDDupeRXThread,
APRSDProcessPacketThread,
APRSDRXThread,
)
packet_queue = queue.Queue(maxsize=20)
packet_queue = queue.Queue(maxsize=500)
+83 -75
View File
@@ -8,20 +8,32 @@ from oslo_config import cfg
from aprsd import packets, plugin
from aprsd.client import client_factory
from aprsd.packets import collector
from aprsd.packets import collector, filter
from aprsd.packets import log as packet_log
from aprsd.threads import APRSDThread, tx
from aprsd.utils import trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
LOG = logging.getLogger('APRSD')
class APRSDRXThread(APRSDThread):
"""Main Class to connect to an APRS Client and recieve packets.
A packet is received in the main loop and then sent to the
process_packet method, which sends the packet through the collector
to track the packet for stats, and then put into the packet queue
for processing in a separate thread.
"""
_client = None
# This is the queue that packets are sent to for processing.
# We process packets in a separate thread to help prevent
# getting blocked by the APRS server trying to send us packets.
packet_queue = None
def __init__(self, packet_queue):
super().__init__("RX_PKT")
super().__init__('RX_PKT')
self.packet_queue = packet_queue
def stop(self):
@@ -52,7 +64,7 @@ class APRSDRXThread(APRSDThread):
# kwargs. :(
# https://github.com/rossengeorgiev/aprs-python/pull/56
self._client.consumer(
self._process_packet,
self.process_packet,
raw=False,
blocking=False,
)
@@ -60,7 +72,7 @@ class APRSDRXThread(APRSDThread):
aprslib.exceptions.ConnectionDrop,
aprslib.exceptions.ConnectionError,
):
LOG.error("Connection dropped, reconnecting")
LOG.error('Connection dropped, reconnecting')
# Force the deletion of the client object connected to aprs
# This will cause a reconnect, next time client.get_client()
# is called
@@ -68,45 +80,18 @@ class APRSDRXThread(APRSDThread):
time.sleep(5)
except Exception:
# LOG.exception(ex)
LOG.error("Resetting connection and trying again.")
LOG.error('Resetting connection and trying again.')
self._client.reset()
time.sleep(5)
# Continue to loop
time.sleep(1)
return True
def _process_packet(self, *args, **kwargs):
"""Intermediate callback so we can update the keepalive time."""
# Now call the 'real' packet processing for a RX'x packet
self.process_packet(*args, **kwargs)
@abc.abstractmethod
def process_packet(self, *args, **kwargs):
pass
class APRSDDupeRXThread(APRSDRXThread):
"""Process received packets.
This is the main APRSD Server command thread that
receives packets and makes sure the packet
hasn't been seen previously before sending it on
to be processed.
"""
@trace.trace
def process_packet(self, *args, **kwargs):
"""This handles the processing of an inbound packet.
When a packet is received by the connected client object,
it sends the raw packet into this function. This function then
decodes the packet via the client, and then processes the packet.
Ack Packets are sent to the PluginProcessPacketThread for processing.
All other packets have to be checked as a dupe, and then only after
we haven't seen this packet before, do we send it to the
PluginProcessPacketThread for processing.
"""
packet = self._client.decode_packet(*args, **kwargs)
if not packet:
LOG.error(
'No packet received from decode_packet. Most likely a failure to parse'
)
return
packet_log.log(packet)
pkt_list = packets.PacketList()
@@ -140,26 +125,55 @@ class APRSDDupeRXThread(APRSDRXThread):
# If the packet came in within N seconds of the
# Last time seeing the packet, then we drop it as a dupe.
LOG.warning(
f"Packet {packet.from_call}:{packet.msgNo} already tracked, dropping."
f'Packet {packet.from_call}:{packet.msgNo} already tracked, dropping.'
)
else:
LOG.warning(
f"Packet {packet.from_call}:{packet.msgNo} already tracked "
f"but older than {CONF.packet_dupe_timeout} seconds. processing.",
f'Packet {packet.from_call}:{packet.msgNo} already tracked '
f'but older than {CONF.packet_dupe_timeout} seconds. processing.',
)
collector.PacketCollector().rx(packet)
self.packet_queue.put(packet)
class APRSDPluginRXThread(APRSDDupeRXThread):
""" "Process received packets.
class APRSDFilterThread(APRSDThread):
def __init__(self, thread_name, packet_queue):
super().__init__(thread_name)
self.packet_queue = packet_queue
For backwards compatibility, we keep the APRSDPluginRXThread.
"""
def filter_packet(self, packet):
# Do any packet filtering prior to processing
if not filter.PacketFilter().filter(packet):
return None
return packet
def print_packet(self, packet):
"""Allow a child of this class to override this.
This is helpful if for whatever reason the child class
doesn't want to log packets.
"""
packet_log.log(packet)
def loop(self):
try:
packet = self.packet_queue.get(timeout=1)
self.print_packet(packet)
if packet:
if self.filter_packet(packet):
self.process_packet(packet)
except queue.Empty:
pass
return True
class APRSDProcessPacketThread(APRSDThread):
"""Base class for processing received packets.
class APRSDProcessPacketThread(APRSDFilterThread):
"""Base class for processing received packets after they have been filtered.
Packets are received from the client, then filtered for dupes,
then sent to the packet queue. This thread pulls packets from
the packet queue for processing.
This is the base class for processing packets coming from
the consumer. This base class handles sending ack packets and
@@ -167,44 +181,38 @@ class APRSDProcessPacketThread(APRSDThread):
for processing."""
def __init__(self, packet_queue):
self.packet_queue = packet_queue
super().__init__("ProcessPKT")
super().__init__('ProcessPKT', packet_queue=packet_queue)
if not CONF.enable_sending_ack_packets:
LOG.warning(
"Sending ack packets is disabled, messages "
"will not be acknowledged.",
'Sending ack packets is disabled, messages will not be acknowledged.',
)
def process_ack_packet(self, packet):
"""We got an ack for a message, no need to resend it."""
ack_num = packet.msgNo
LOG.debug(f"Got ack for message {ack_num}")
LOG.debug(f'Got ack for message {ack_num}')
collector.PacketCollector().rx(packet)
def process_piggyback_ack(self, packet):
"""We got an ack embedded in a packet."""
ack_num = packet.ackMsgNo
LOG.debug(f"Got PiggyBackAck for message {ack_num}")
LOG.debug(f'Got PiggyBackAck for message {ack_num}')
collector.PacketCollector().rx(packet)
def process_reject_packet(self, packet):
"""We got a reject message for a packet. Stop sending the message."""
ack_num = packet.msgNo
LOG.debug(f"Got REJECT for message {ack_num}")
LOG.debug(f'Got REJECT for message {ack_num}')
collector.PacketCollector().rx(packet)
def loop(self):
try:
packet = self.packet_queue.get(timeout=1)
if packet:
self.process_packet(packet)
except queue.Empty:
pass
return True
def process_packet(self, packet):
"""Process a packet received from aprs-is server."""
LOG.debug(f"ProcessPKT-LOOP {self.loop_count}")
LOG.debug(f'ProcessPKT-LOOP {self.loop_count}')
# set this now as we are going to process it.
# This is used during dupe checking, so set it early
packet.processed = True
our_call = CONF.callsign.lower()
from_call = packet.from_call
@@ -227,7 +235,7 @@ class APRSDProcessPacketThread(APRSDThread):
):
self.process_reject_packet(packet)
else:
if hasattr(packet, "ackMsgNo") and packet.ackMsgNo:
if hasattr(packet, 'ackMsgNo') and packet.ackMsgNo:
# we got an ack embedded in this packet
# we need to handle the ack
self.process_piggyback_ack(packet)
@@ -267,7 +275,7 @@ class APRSDProcessPacketThread(APRSDThread):
if not for_us:
LOG.info("Got a packet meant for someone else '{packet.to_call}'")
else:
LOG.info("Got a non AckPacket/MessagePacket")
LOG.info('Got a non AckPacket/MessagePacket')
class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
@@ -287,7 +295,7 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
tx.send(subreply)
else:
wl = CONF.watch_list
to_call = wl["alert_callsign"]
to_call = wl['alert_callsign']
tx.send(
packets.MessagePacket(
from_call=CONF.callsign,
@@ -299,7 +307,7 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
# We have a message based object.
tx.send(reply)
except Exception as ex:
LOG.error("Plugin failed!!!")
LOG.error('Plugin failed!!!')
LOG.exception(ex)
def process_our_message_packet(self, packet):
@@ -355,11 +363,11 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
if to_call == CONF.callsign and not replied:
# Tailor the messages accordingly
if CONF.load_help_plugin:
LOG.warning("Sending help!")
LOG.warning('Sending help!')
message_text = "Unknown command! Send 'help' message for help"
else:
LOG.warning("Unknown command!")
message_text = "Unknown command!"
LOG.warning('Unknown command!')
message_text = 'Unknown command!'
tx.send(
packets.MessagePacket(
@@ -369,11 +377,11 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
),
)
except Exception as ex:
LOG.error("Plugin failed!!!")
LOG.error('Plugin failed!!!')
LOG.exception(ex)
# Do we need to send a reply?
if to_call == CONF.callsign:
reply = "A Plugin failed! try again?"
reply = 'A Plugin failed! try again?'
tx.send(
packets.MessagePacket(
from_call=CONF.callsign,
@@ -382,4 +390,4 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
),
)
LOG.debug("Completed process_our_message_packet")
LOG.debug('Completed process_our_message_packet')
+42
View File
@@ -0,0 +1,42 @@
# aprsd/aprsd/threads/service.py
#
# This module is used to register threads that the service command runs.
#
# The service command is used to start and stop the APRS service.
# This is a mechanism to register threads that the service or command
# needs to run, and then start stop them as needed.
from aprsd.threads import aprsd as aprsd_threads
from aprsd.utils import singleton
@singleton
class ServiceThreads:
"""Registry for threads that the service command runs.
This enables extensions to register a thread to run during
the service command.
"""
def __init__(self):
self.threads: list[aprsd_threads.APRSDThread] = []
def register(self, thread: aprsd_threads.APRSDThread):
if not isinstance(thread, aprsd_threads.APRSDThread):
raise TypeError(f'Thread {thread} is not an APRSDThread')
self.threads.append(thread)
def unregister(self, thread: aprsd_threads.APRSDThread):
if not isinstance(thread, aprsd_threads.APRSDThread):
raise TypeError(f'Thread {thread} is not an APRSDThread')
self.threads.remove(thread)
def start(self):
"""Start all threads in the list."""
for thread in self.threads:
thread.start()
def join(self):
"""Join all the threads in the list"""
for thread in self.threads:
thread.join()
+4 -9
View File
@@ -1,8 +1,6 @@
import logging
import threading
import time
import wrapt
from oslo_config import cfg
from aprsd.stats import collector
@@ -10,18 +8,15 @@ from aprsd.threads import APRSDThread
from aprsd.utils import objectstore
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
LOG = logging.getLogger('APRSD')
class StatsStore(objectstore.ObjectStoreMixin):
"""Container to save the stats from the collector."""
lock = threading.Lock()
data = {}
@wrapt.synchronized(lock)
def add(self, stats: dict):
self.data = stats
with self.lock:
self.data = stats
class APRSDStatsStoreThread(APRSDThread):
@@ -31,7 +26,7 @@ class APRSDStatsStoreThread(APRSDThread):
save_interval = 10
def __init__(self):
super().__init__("StatsStore")
super().__init__('StatsStore')
def loop(self):
if self.loop_count % self.save_interval == 0:
+12 -13
View File
@@ -6,9 +6,8 @@ import threading
from oslo_config import cfg
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
LOG = logging.getLogger('APRSD')
class ObjectStoreMixin:
@@ -63,7 +62,7 @@ class ObjectStoreMixin:
def _save_filename(self):
save_location = CONF.save_location
return "{}/{}.p".format(
return '{}/{}.p'.format(
save_location,
self.__class__.__name__.lower(),
)
@@ -75,13 +74,13 @@ class ObjectStoreMixin:
self._init_store()
save_filename = self._save_filename()
if len(self) > 0:
LOG.info(
f"{self.__class__.__name__}::Saving"
f" {len(self)} entries to disk at "
f"{save_filename}",
LOG.debug(
f'{self.__class__.__name__}::Saving'
f' {len(self)} entries to disk at '
f'{save_filename}',
)
with self.lock:
with open(save_filename, "wb+") as fp:
with open(save_filename, 'wb+') as fp:
pickle.dump(self.data, fp)
else:
LOG.debug(
@@ -97,21 +96,21 @@ class ObjectStoreMixin:
return
if os.path.exists(self._save_filename()):
try:
with open(self._save_filename(), "rb") as fp:
with open(self._save_filename(), 'rb') as fp:
raw = pickle.load(fp)
if raw:
self.data = raw
LOG.debug(
f"{self.__class__.__name__}::Loaded {len(self)} entries from disk.",
f'{self.__class__.__name__}::Loaded {len(self)} entries from disk.',
)
else:
LOG.debug(f"{self.__class__.__name__}::No data to load.")
LOG.debug(f'{self.__class__.__name__}::No data to load.')
except (pickle.UnpicklingError, Exception) as ex:
LOG.error(f"Failed to UnPickle {self._save_filename()}")
LOG.error(f'Failed to UnPickle {self._save_filename()}')
LOG.error(ex)
self.data = {}
else:
LOG.debug(f"{self.__class__.__name__}::No save file found.")
LOG.debug(f'{self.__class__.__name__}::No save file found.')
def flush(self):
"""Nuke the old pickle file that stored the old results from last aprsd run."""
+7 -7
View File
@@ -1,10 +1,10 @@
# This file was autogenerated by uv via the following command:
# uv pip compile --resolver backtracking --annotation-style=line requirements-dev.in -o requirements-dev.txt
alabaster==1.0.0 # via sphinx
babel==2.16.0 # via sphinx
babel==2.17.0 # via sphinx
build==1.2.2.post1 # via pip-tools, -r requirements-dev.in
cachetools==5.5.1 # via tox
certifi==2024.12.14 # via requests
cachetools==5.5.2 # via tox
certifi==2025.1.31 # via requests
cfgv==3.4.0 # via pre-commit
chardet==5.2.0 # via tox
charset-normalizer==3.4.1 # via requests
@@ -13,7 +13,7 @@ colorama==0.4.6 # via tox
distlib==0.3.9 # via virtualenv
docutils==0.21.2 # via m2r, sphinx
filelock==3.17.0 # via tox, virtualenv
identify==2.6.6 # via pre-commit
identify==2.6.8 # via pre-commit
idna==3.10 # via requests
imagesize==1.4.1 # via sphinx
jinja2==3.1.5 # via sphinx
@@ -22,7 +22,7 @@ markupsafe==3.0.2 # via jinja2
mistune==0.8.4 # via m2r
nodeenv==1.9.1 # via pre-commit
packaging==24.2 # via build, pyproject-api, sphinx, tox
pip==24.3.1 # via pip-tools, -r requirements-dev.in
pip==25.0.1 # via pip-tools, -r requirements-dev.in
pip-tools==7.4.1 # via -r requirements-dev.in
platformdirs==4.3.6 # via tox, virtualenv
pluggy==1.5.0 # via tox
@@ -32,7 +32,7 @@ pyproject-api==1.9.0 # via tox
pyproject-hooks==1.2.0 # via build, pip-tools
pyyaml==6.0.2 # via pre-commit
requests==2.32.3 # via sphinx
setuptools==75.8.0 # via pip-tools
setuptools==75.8.2 # via pip-tools
snowballstemmer==2.2.0 # via sphinx
sphinx==8.1.3 # via -r requirements-dev.in
sphinxcontrib-applehelp==2.0.0 # via sphinx
@@ -45,5 +45,5 @@ tomli==2.2.1 # via build, pip-tools, pyproject-api, sphinx, tox
tox==4.24.1 # via -r requirements-dev.in
typing-extensions==4.12.2 # via tox
urllib3==2.3.0 # via requests
virtualenv==20.29.1 # via pre-commit, tox
virtualenv==20.29.2 # via pre-commit, tox
wheel==0.45.1 # via pip-tools, -r requirements-dev.in
+1 -2
View File
@@ -7,8 +7,7 @@ loguru
oslo.config
pluggy
requests
# Pinned due to gray needing 12.6.0
rich~=12.6.0
rich
rush
thesmuggler
tzlocal
+15 -13
View File
@@ -1,13 +1,12 @@
# This file was autogenerated by uv via the following command:
# uv pip compile --resolver backtracking --annotation-style=line requirements.in -o requirements.txt
aprslib==0.7.2 # via -r requirements.in
attrs==24.3.0 # via ax253, kiss3, rush
attrs==25.1.0 # via ax253, kiss3, rush
ax253==0.1.5.post1 # via kiss3
bitarray==3.0.0 # via ax253, kiss3
certifi==2024.12.14 # via requests
bitarray==3.1.0 # via ax253, kiss3
certifi==2025.1.31 # via requests
charset-normalizer==3.4.1 # via requests
click==8.1.8 # via -r requirements.in
commonmark==0.9.1 # via rich
dataclasses-json==0.6.7 # via -r requirements.in
debtcollector==3.0.0 # via oslo-config
haversine==2.9.0 # via -r requirements.in
@@ -15,29 +14,32 @@ idna==3.10 # via requests
importlib-metadata==8.6.1 # via ax253, kiss3
kiss3==8.0.0 # via -r requirements.in
loguru==0.7.3 # via -r requirements.in
marshmallow==3.26.0 # via dataclasses-json
markdown-it-py==3.0.0 # via rich
marshmallow==3.26.1 # via dataclasses-json
mdurl==0.1.2 # via markdown-it-py
mypy-extensions==1.0.0 # via typing-inspect
netaddr==1.3.0 # via oslo-config
oslo-config==9.7.0 # via -r requirements.in
oslo-i18n==6.5.0 # via oslo-config
oslo-config==9.7.1 # via -r requirements.in
oslo-i18n==6.5.1 # via oslo-config
packaging==24.2 # via marshmallow
pbr==6.1.0 # via oslo-i18n, stevedore
pbr==6.1.1 # via oslo-i18n, stevedore
pluggy==1.5.0 # via -r requirements.in
pygments==2.19.1 # via rich
pyserial==3.5 # via pyserial-asyncio
pyserial-asyncio==0.6 # via kiss3
pytz==2024.2 # via -r requirements.in
pytz==2025.1 # via -r requirements.in
pyyaml==6.0.2 # via oslo-config
requests==2.32.3 # via oslo-config, update-checker, -r requirements.in
rfc3986==2.0.0 # via oslo-config
rich==12.6.0 # via -r requirements.in
rich==13.9.4 # via -r requirements.in
rush==2021.4.0 # via -r requirements.in
stevedore==5.4.0 # via oslo-config
setuptools==75.8.2 # via pbr
stevedore==5.4.1 # via oslo-config
thesmuggler==1.0.1 # via -r requirements.in
timeago==1.0.16 # via -r requirements.in
typing-extensions==4.12.2 # via typing-inspect
typing-extensions==4.12.2 # via rich, typing-inspect
typing-inspect==0.9.0 # via dataclasses-json
tzlocal==5.2 # via -r requirements.in
tzlocal==5.3 # via -r requirements.in
update-checker==0.18.0 # via -r requirements.in
urllib3==2.3.0 # via requests
wrapt==1.17.2 # via debtcollector, -r requirements.in