mirror of
https://github.com/craigerl/aprsd.git
synced 2026-08-16 16:43:52 -04:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dba56f74d | |||
| d262589313 | |||
| 7ed8028307 | |||
| 94ba915ed4 | |||
| 2b185ee1b8 | |||
| c1c89fd2c2 | |||
| 0fa5b07d4b | |||
| a3cda9f37d | |||
| 06bdb34642 | |||
| 4fd64a3c25 | |||
| 361663e7d2 | |||
| fd517b3218 | |||
| e9e7e6b59f | |||
| 52dac7e0a0 | |||
| b6da0ebb0d | |||
| d82a81a2c3 | |||
| 6cd7e99713 | |||
| 101904ca77 | |||
| 227ddbf148 | |||
| 19c12e70f3 | |||
| 1606585d41 | |||
| 3b57e7597d |
@@ -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: |
|
||||
|
||||
@@ -60,3 +60,9 @@ AUTHORS
|
||||
Makefile.venv
|
||||
# Copilot
|
||||
.DS_Store
|
||||
|
||||
.python-version
|
||||
.fleet
|
||||
.vscode
|
||||
.envrc
|
||||
.doit.db
|
||||
|
||||
+59
-17
@@ -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
@@ -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
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
@@ -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']
|
||||
|
||||
@@ -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
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
@@ -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
@@ -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."""
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user