1
0
mirror of https://github.com/craigerl/aprsd.git synced 2026-08-17 17:13:49 -04:00

Compare commits

..

35 Commits

Author SHA1 Message Date
hemna a3cda9f37d Update Changelog for 4.1.0 release 2025-02-20 15:15:31 -05:00
hemna 06bdb34642 CONF.logging.enable_color option added
This allows the user to turn off ANSI color output for
logging to the console.
2025-02-20 12:44:16 -05:00
hemna 4fd64a3c25 Merge pull request #184 from craigerl/packet_filtering
Added new PacketFilter mechanism
2025-02-19 16:45:10 -05:00
hemna 361663e7d2 Changed Objectstore log to debug
This should help silence the log a bit.
2025-02-19 16:38:48 -05:00
hemna fd517b3218 updated gitignore 2025-02-19 16:38:48 -05:00
hemna e9e7e6b59f Fixed some pep8 failures. 2025-02-19 16:38:47 -05:00
hemna 52dac7e0a0 Added new PacketFilter mechanism
This patch adds the new PacketFilter class as a generic mechanism
for doing packet filtering during the packet processing phase of
recieving packets.

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

Each phase has a single thread for handling that phase.

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

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

This PacketFilter mechanism allows an external extension or plugin
to add/remove packet filters at will depending on the function
of the extension or plugin.   For example, this allows an extension
to get a packet and push the packet into an MQTT queue.
2025-02-19 16:38:47 -05:00
hemna b6da0ebb0d Fix runaway KISS driver on failed connnection
This patch fixes an issue when the KISS connection fails to start
and or goes away during the lifetime of the active connection.
Aprsd would runaway in a tight loop eating 100% cpu.  We now detect
when the underlying asyncio connection has failed and raise, which
induces a sleep in the consumer to try again.
2025-02-15 18:55:58 -05:00
hemna d82a81a2c3 fix for None packet in rx thread
This patch updates the process_packet to ensure when we
try to decode a packet we actually get one.
2025-02-14 11:06:19 -05:00
hemna 6cd7e99713 Remove sleep in main RX thread
We had a bottleneck of pulling down packets as fast as possible,
which was caused by the time.sleep(1) call in the main RX
thread that used to be needed.
2025-02-03 13:27:57 -08:00
hemna 101904ca77 Try and stop chardet logging!
This patch sets settings on the logger to hopefully
stop any chardet logs from leaking into the aprsd logs.
2025-01-30 10:16:09 -08:00
hemna 227ddbf148 Update StatsStore to use existing lock
The base class for StatsStore already creates a lock, use that instead.
2025-01-30 10:07:28 -08:00
hemna 19c12e70f3 Updated packet_list to allow infinit max store
This patch adds logic of setting packet_list_stats_maxlen -1
meaning keep every packet for stats.
2025-01-30 10:04:59 -08:00
hemna 1606585d41 Updated APRSIS driver
This patch adds an override to _connect to set some more
socket keepalive options.
2025-01-30 10:03:14 -08:00
hemna 3b57e7597d Update to build from pypi 2025-01-25 13:41:00 -05:00
hemna 000adef6d4 Prep for 4.0.2 2025-01-25 13:29:04 -05:00
hemna bea481555b update the install from github in Dockerfile 2025-01-25 13:06:53 -05:00
hemna 3c4e200d70 Fix the testing of fortune path 2025-01-25 12:54:18 -05:00
hemna 2f26eb86f4 Added uv.lock 2025-01-25 12:33:29 -05:00
hemna 97ffffc10d Look in multiple places for fortune bin
Update the fortune plugin to look in multiple places for the
fortune binary.  It lives in different places for debian vs alpine.
2025-01-25 12:32:15 -05:00
hemna c1319c3ab8 Removed some verbose output from KISS
This removes some overly verbose output from the KISS driver.
2025-01-25 12:31:27 -05:00
hemna 1f65bbe13a Pass branch in github release_build
Try and pass the build arg BRANCH along with the
building of the image.
2025-01-24 20:50:33 -05:00
hemna 9501a63bd6 Trap for failed parsing of packets on KISS
This adds a try block around the aprslib.parse() for packets incoming
on the KISS interface.  Often times we'll get invalid packets and
this prevents stack dumps to the log.
2025-01-24 17:44:58 -05:00
hemna edeba7f514 Fix for KISS/Fake client drivers
They were both missing a setting of aprsd_keepalive to test
for the logging of the keepalive last time called.
2025-01-24 17:28:59 -05:00
hemna 24f567224c Updated Changelog 2025-01-24 16:46:02 -05:00
hemna e08039431e Update pyproject for README.rst -> md
Updated the pyproject.toml to reflect the name change of
README.rst -> README.md
2025-01-24 16:43:52 -05:00
hemna 934ebd236d Updated ChangeLog for 4.0.0 2025-01-24 16:38:30 -05:00
hemna 4a7a902a33 Updated requirements 2025-01-24 16:35:20 -05:00
github-actions[bot] c556f5126f chore: update AUTHORS [skip ci] 2025-01-24 21:32:14 +00:00
hemna 7e4d4b3e80 Merge pull request #183 from craigerl/refactor-extraction
Migrate admin web out of aprsd.
2025-01-24 13:32:03 -08:00
hemna 375a5e5b34 Updated README.md TOC
Updated the table of contents
2025-01-24 16:23:42 -05:00
hemna cf4a29f0cb reduced logo size 50% 2025-01-23 08:55:06 -05:00
hemna 447451c6c9 Added plugin and extension links
This patch updates the README.md file to link to existing plugins
and extensions.
2025-01-23 08:51:23 -05:00
hemna 0ed648f8f8 Added APRSD logo 2025-01-23 08:23:38 -05:00
hemna e5d8796cda Enable packet stats for listen command in Docker
Update the docker listen.sh to enable tracking packet stats.
2024-12-12 08:59:28 -05:00
33 changed files with 2268 additions and 507 deletions
+3 -2
View File
@@ -6,7 +6,7 @@ on:
aprsd_version:
required: true
options:
- 3.0.0
- 4.0.0
logLevel:
description: 'Log level'
required: true
@@ -41,9 +41,10 @@ jobs:
platforms: linux/amd64,linux/arm64
file: ./Dockerfile
build-args: |
INSTALL_TYPE=pypi
VERSION=${{ inputs.aprsd_version }}
BUILDX_QEMU_ENV=true
push: true
tags: |
hemna6969/aprsd:v${{ inputs.aprsd_version }}
hemna6969/aprsd:${{ inputs.aprsd_version }}
hemna6969/aprsd:latest
+6
View File
@@ -60,3 +60,9 @@ AUTHORS
Makefile.venv
# Copilot
.DS_Store
.python-version
.fleet
.vscode
.envrc
.doit.db
+1
View File
@@ -0,0 +1 @@
waboring@hemna.com : 1
+96
View File
@@ -4,6 +4,101 @@ 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.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)
#### [4.0.2](https://github.com/craigerl/aprsd/compare/4.0.1...4.0.2)
> 25 January 2025
- Fix for KISS/Fake client drivers [`edeba7f`](https://github.com/craigerl/aprsd/commit/edeba7f5141c3197a3ddff5fe45127894c861e07)
- Trap for failed parsing of packets on KISS [`9501a63`](https://github.com/craigerl/aprsd/commit/9501a63bd61a681daf9eb9e41704e31570b7b385)
- Pass branch in github release_build [`1f65bbe`](https://github.com/craigerl/aprsd/commit/1f65bbe13a33c1fffad650c87c949bbe844d27f6)
- Removed some verbose output from KISS [`c1319c3`](https://github.com/craigerl/aprsd/commit/c1319c3ab8822f24c0e3b2c74d151d4e13a0039a)
- Look in multiple places for fortune bin [`97ffffc`](https://github.com/craigerl/aprsd/commit/97ffffc10dd05bd785134877ddb94437cf9b7f97)
- 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 January 2025
- 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.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)
- removed email reference [`fcd1629`](https://github.com/craigerl/aprsd/commit/fcd1629fdebb485fc763e19d73bfdafc10de6251)
- Removed more email references. [`f0c0260`](https://github.com/craigerl/aprsd/commit/f0c02606ebfc5170b80b55c73de191d27a8778a0)
- 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)
- 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)
- fixed list-plugins [`ec1adf4`](https://github.com/craigerl/aprsd/commit/ec1adf418203fc7d1a8fb175a075c73fa27d772b)
- Updated README.md [`fd74405`](https://github.com/craigerl/aprsd/commit/fd74405b5fb2ec3d0b432e95d715a8934d76dcd8)
- updated workflow [`df14eb8`](https://github.com/craigerl/aprsd/commit/df14eb8f288aa8cd8cab9b9263ca64093bb63c34)
- Some cleanup with list plugins [`5274c5d`](https://github.com/craigerl/aprsd/commit/5274c5dc563b40f87436378558ad843517be6939)
- Updated README.md [`275e335`](https://github.com/craigerl/aprsd/commit/275e33538db7f014eeb02deddd76bfbb63516871)
- removed BeautifulSoup usage [`a21432f`](https://github.com/craigerl/aprsd/commit/a21432fb249577489795e8a9f5d969b63c2af716)
- updated github workflows [`e3a7e7f`](https://github.com/craigerl/aprsd/commit/e3a7e7fb8a8448f7893fbe3f8cf7e876a5e2ff58)
- updated requirements-dev [`fbec716`](https://github.com/craigerl/aprsd/commit/fbec7168eb37621c0f3ca54274148704c857f506)
- updated requirements [`7f2c1d7`](https://github.com/craigerl/aprsd/commit/7f2c1d712417b55cd3535888d18d41c6a00e4d07)
- updated plugin example [`0073865`](https://github.com/craigerl/aprsd/commit/007386505ab539200b8eef438250bc73dac0079f)
- updated docs rst files [`3cd9bfa`](https://github.com/craigerl/aprsd/commit/3cd9bfa7bb79fb2ebaad7d921c7d9fd2128392f3)
- added authors.yml [`c8735c2`](https://github.com/craigerl/aprsd/commit/c8735c257a32b31974508c2ed9c1cdc8848b5150)
- updated action versions [`7702d68`](https://github.com/craigerl/aprsd/commit/7702d68cf722746952b94782ac4ed5cba935d53b)
- update to py 3.10 [`3ee422b`](https://github.com/craigerl/aprsd/commit/3ee422b5c9f79e9ee2d757abd1de9b9f2d0fc52e)
- 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)
#### [3.4.4](https://github.com/craigerl/aprsd/compare/3.4.3...3.4.4)
> 6 December 2024
@@ -67,6 +162,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- Update requirements.dev [`0d8a1ac`](https://github.com/craigerl/aprsd/commit/0d8a1ac33441cf85270423b80264110fe9286754)
- Added new acked property to the core Packet [`3c058f3`](https://github.com/craigerl/aprsd/commit/3c058f3e7b89227c613234f8ae91687fbdf19ae1)
- Fixed some pep8 failures [`63bcd41`](https://github.com/craigerl/aprsd/commit/63bcd4139bb40718152b66002dfa45f236c54e11)
- Update Changelog for 3.4.4 [`bc709b3`](https://github.com/craigerl/aprsd/commit/bc709b377c423412e9093e8ecc78e561031023b8)
#### [3.4.3](https://github.com/craigerl/aprsd/compare/v3.4.3...3.4.3)
+60 -38
View File
@@ -1,4 +1,4 @@
# APRSD - Ham radio APRS-IS Message plugin server
# APRSD - Ham radio APRS-IS Message platform software
## KM6LYW and WB4BOR
@@ -11,42 +11,39 @@
[![down](https://static.pepy.tech/personalized-badge/aprsd?period=month&units=international_system&left_color=black&right_color=orange&left_text=Downloads)](https://pepy.tech/project/aprsd)
[APRSD](http://github.com/craigerl/aprsd) is a Ham radio
[APRS](http://aprs.org) message command gateway built on python.
[APRS](http://aprs.org) message platform built with python.
### Table of Contents
![image](./aprsd_logo.png)
1. [What is APRSD](#what-is-aprsd)
2. [APRSD Overview Diagram](#aprsd-overview-diagram)
3. [Typical Use Case](#typical-use-case)
4. [Installation](#installation)
5. [Example Usage](#example-usage)
6. [Help](#help)
7. [Commands](#commands)
- [Configuration](#configuration)
- [Server](#server)
- [Current List of Built-in
Plugins](#current-list-of-built-in-plugins)
- [Pypi.org APRSD Installable Plugin
Packages](#pypiorg-aprsd-installable-plugin-packages)
- [🐍 APRSD Installed 3rd Party
Plugins](#aprsd-installed-3rd-party-plugins)
- [Send Message](#send-message)
- [Send Email (Radio to SMTP
Server)](#send-email-radio-to-smtp-server)
- [Receive Email (IMAP Server to
Radio)](#receive-email-imap-server-to-radio)
- [Location](#location)
- [Web Admin Interface](#web-admin-interface)
8. [Development](#development)
- [Building Your Own APRSD
Plugins](#building-your-own-aprsd-plugins)
9. [Workflow](#workflow)
10. [Release](#release)
11. [Docker Container](#docker-container)
- [Building](#building-1)
- [Official Build](#official-build)
- [Development Build](#development-build)
- [Running the Container](#running-the-container)
# Table of Contents
1. [APRSD - Ham radio APRS-IS Message platform software](#aprsd---ham-radio-aprs-is-message-platform-software)
2. [What is APRSD](#what-is-aprsd)
3. [APRSD Plugins/Extensions](#aprsd-pluginsextensions)
4. [List of existing plugins - APRS Message processing/responders](#list-of-existing-plugins---aprs-message-processingresponders)
5. [List of existing extensions - Add new capabilities to APRSD](#list-of-existing-extensions---add-new-capabilities-to-aprsd)
6. [APRSD Overview Diagram](#aprsd-overview-diagram)
7. [Typical use case](#typical-use-case)
8. [Installation](#installation)
9. [Example usage](#example-usage)
10. [Help](#help)
11. [Commands](#commands)
12. [Configuration](#configuration)
13. [server](#server)
14. [Current list plugins](#current-list-plugins)
15. [Current list extensions](#current-list-extensions)
16. [send-message](#send-message)
17. [Development](#development)
18. [Release](#release)
19. [Building your own APRSD plugins](#building-your-own-aprsd-plugins)
20. [Overview](#overview)
21. [Docker Container](#docker-container)
22. [Building](#building)
23. [Official Build](#official-build)
24. [Development Build](#development-build)
25. [Running the container](#running-the-container)
26. [Activity](#activity)
27. [Star History](#star-history)
---
@@ -58,7 +55,7 @@
### What is APRSD
APRSD is a python application for interacting with the APRS network and
APRSD is a python application for interacting with the APRS network and Ham radios with KISS interfaces and
providing APRS services for HAM radio operators.
APRSD currently has 4 main commands to use.
@@ -75,7 +72,7 @@ APRSD currently has 4 main commands to use.
Each of those commands can connect to the APRS-IS network if internet
connectivity is available. If internet is not available, then APRS can
be configured to talk to a TCP KISS TNC for radio connectivity.
be configured to talk to a TCP KISS TNC for radio connectivity directly.
Please [read the docs](https://aprsd.readthedocs.io) to learn more!
@@ -87,12 +84,37 @@ APRSD Has the ability to add plugins and extensions. Plugins add new message fi
You can see the [available plugins/extensions on pypi here:](https://pypi.org/search/?q=aprsd) [https://pypi.org/search/?q=aprsd](https://pypi.org/search/?q=aprsd)
> [!NOTE]
> aprsd admin and webchat have been extracted into separate extensions.
> aprsd admin and webchat commands have been extracted into separate extensions.
* [See admin extension here](https://github.com/hemna/aprsd-admin-extension) <div id="admin logo" align="left"><img src="https://raw.githubusercontent.com/hemna/aprsd-admin-extension/refs/heads/master/screenshot.png" alt="Web Admin" width="340"/></div>
* [See webchat extension here](https://github.com/hemna/aprsd-webchat-extension) <div id="webchat logo" align="left"><img src="https://raw.githubusercontent.com/hemna/aprsd-webchat-extension/master/screenshot.png" alt="Webchat" width="340"/></div>
### List of existing plugins - APRS Message processing/responders
- [aprsd-email-plugin](https://github.com/hemna/aprsd-email-plugin) - send/receive email!
- [aprsd-location-plugin](https://github.com/hemna/aprsd-location-plugin) - get latest GPS location.
- [aprsd-locationdata-plugin](https://github.com/hemna/aprsd-locationdata-plugin) - get latest GPS location
- [aprsd-digipi-plugin](https://github.com/hemna/aprsd-digipi-plugin) - Look for digipi beacon packets
- [aprsd-w3w-plugin](https://github.com/hemna/aprsd-w3w-plugin) - get your w3w coordinates
- [aprsd-mqtt-plugin](https://github.com/hemna/aprsd-mqtt-plugin) - send aprs packets to an MQTT topic
- [aprsd-telegram-plugin](https://github.com/hemna/aprsd-telegram-plugin) - send/receive messages to telegram
- [aprsd-borat-plugin](https://github.com/hemna/aprsd-borat-plugin) - get Borat quotes
- [aprsd-wxnow-plugin](https://github.com/hemna/aprsd-wxnow-plugin) - get closest N weather station reports
- [aprsd-weewx-plugin](https://github.com/hemna/aprsd-weewx-plugin) - get weather from your weewx weather station
- [aprsd-slack-plugin](https://github.com/hemna/aprsd-slack-plugin) - send/receive messages to a slack channel
- [aprsd-sentry-plugin](https://github.com/hemna/aprsd-sentry-plugin) -
- [aprsd-repeat-plugins](https://github.com/hemna/aprsd-repeat-plugins) - plugins for the REPEAT service. Get nearest Ham radio repeaters!
- [aprsd-twitter-plugin](https://github.com/hemna/aprsd-twitter-plugin) - make tweets from your Ham Radio!
- [aprsd-timeopencage-plugin](https://github.com/hemna/aprsd-timeopencage-plugin) - Get local time for a callsign
- [aprsd-stock-plugin](https://github.com/hemna/aprsd-stock-plugin) - get stock quotes from your Ham radio
### List of existing extensions - Add new capabilities to APRSD
- [aprsd-admin-extension](https://github.com/hemna/aprsd-admin-extension) - Web Administration page for APRSD
- [aprsd-webchat-extension](https://github.com/hemna/aprsd-webchat-extension) - Web page for APRS Messaging
- [aprsd-irc-extension](https://github.com/hemna/aprsd-irc-extension) - an IRC like server command for APRS
### APRSD Overview Diagram
![image](https://raw.githubusercontent.com/craigerl/aprsd/master/docs/_static/aprsd_overview.svg?sanitize=true)
+87 -35
View File
@@ -1,6 +1,7 @@
import datetime
import logging
import select
import socket
import threading
import aprslib
@@ -18,7 +19,7 @@ from aprslib.exceptions import (
import aprsd
from aprsd.packets import core
LOG = logging.getLogger("APRSD")
LOG = logging.getLogger('APRSD')
class Aprsdis(aprslib.IS):
@@ -31,7 +32,7 @@ class Aprsdis(aprslib.IS):
aprsd_keepalive = datetime.datetime.now()
# Which server we are connected to?
server_string = "None"
server_string = 'None'
# timeout in seconds
select_timeout = 1
@@ -39,10 +40,10 @@ class Aprsdis(aprslib.IS):
def stop(self):
self.thread_stop = True
LOG.warning("Shutdown Aprsdis client.")
LOG.warning('Shutdown Aprsdis client.')
def close(self):
LOG.warning("Closing Aprsdis client.")
LOG.warning('Closing Aprsdis client.')
super().close()
@wrapt.synchronized(lock)
@@ -54,6 +55,57 @@ class Aprsdis(aprslib.IS):
"""If the connection is alive or not."""
return self._connected
def _connect(self):
"""
Attemps connection to the server
"""
self.logger.info(
'Attempting connection to %s:%s', self.server[0], self.server[1]
)
try:
self._open_socket()
peer = self.sock.getpeername()
self.logger.info('Connected to %s', str(peer))
# 5 second timeout to receive server banner
self.sock.setblocking(1)
self.sock.settimeout(5)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
# MACOS doesn't have TCP_KEEPIDLE
if hasattr(socket, 'TCP_KEEPIDLE'):
self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 1)
self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 3)
self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 5)
banner = self.sock.recv(512)
if is_py3:
banner = banner.decode('latin-1')
if banner[0] == '#':
self.logger.debug('Banner: %s', banner.rstrip())
else:
raise ConnectionError('invalid banner from server')
except ConnectionError as e:
self.logger.error(str(e))
self.close()
raise
except (socket.error, socket.timeout) as e:
self.close()
self.logger.error('Socket error: %s' % str(e))
if str(e) == 'timed out':
raise ConnectionError('no banner from server') from e
else:
raise ConnectionError(e) from e
self._connected = True
def _socket_readlines(self, blocking=False):
"""
Generator for complete lines, received from the server
@@ -61,12 +113,12 @@ class Aprsdis(aprslib.IS):
try:
self.sock.setblocking(0)
except OSError as e:
self.logger.error(f"socket error when setblocking(0): {str(e)}")
raise aprslib.ConnectionDrop("connection dropped")
self.logger.error(f'socket error when setblocking(0): {str(e)}')
raise aprslib.ConnectionDrop('connection dropped') from e
while not self.thread_stop:
short_buf = b""
newline = b"\r\n"
short_buf = b''
newline = b'\r\n'
# set a select timeout, so we get a chance to exit
# when user hits CTRL-C
@@ -91,11 +143,11 @@ class Aprsdis(aprslib.IS):
# We could just not be blocking, so empty is expected
continue
else:
self.logger.error("socket.recv(): returned empty")
raise aprslib.ConnectionDrop("connection dropped")
self.logger.error('socket.recv(): returned empty')
raise aprslib.ConnectionDrop('connection dropped')
except OSError as e:
# self.logger.error("socket error on recv(): %s" % str(e))
if "Resource temporarily unavailable" in str(e):
if 'Resource temporarily unavailable' in str(e):
if not blocking:
if len(self.buf) == 0:
break
@@ -111,22 +163,22 @@ class Aprsdis(aprslib.IS):
"""
Sends login string to server
"""
login_str = "user {0} pass {1} vers github.com/craigerl/aprsd {3}{2}\r\n"
login_str = 'user {0} pass {1} vers Python-APRSD {3}{2}\r\n'
login_str = login_str.format(
self.callsign,
self.passwd,
(" filter " + self.filter) if self.filter != "" else "",
(' filter ' + self.filter) if self.filter != '' else '',
aprsd.__version__,
)
self.logger.debug("Sending login information")
self.logger.debug('Sending login information')
try:
self._sendall(login_str)
self.sock.settimeout(5)
test = self.sock.recv(len(login_str) + 100)
if is_py3:
test = test.decode("latin-1")
test = test.decode('latin-1')
test = test.rstrip()
self.logger.debug("Server: '%s'", test)
@@ -134,26 +186,26 @@ class Aprsdis(aprslib.IS):
if not test:
raise LoginError(f"Server Response Empty: '{test}'")
_, _, callsign, status, e = test.split(" ", 4)
s = e.split(",")
_, _, callsign, status, e = test.split(' ', 4)
s = e.split(',')
if len(s):
server_string = s[0].replace("server ", "")
server_string = s[0].replace('server ', '')
else:
server_string = e.replace("server ", "")
server_string = e.replace('server ', '')
if callsign == "":
raise LoginError("Server responded with empty callsign???")
if callsign == '':
raise LoginError('Server responded with empty callsign???')
if callsign != self.callsign:
raise LoginError(f"Server: {test}")
if status != "verified," and self.passwd != "-1":
raise LoginError("Password is incorrect")
raise LoginError(f'Server: {test}')
if status != 'verified,' and self.passwd != '-1':
raise LoginError('Password is incorrect')
if self.passwd == "-1":
self.logger.info("Login successful (receive only)")
if self.passwd == '-1':
self.logger.info('Login successful (receive only)')
else:
self.logger.info("Login successful")
self.logger.info('Login successful')
self.logger.info(f"Connected to {server_string}")
self.logger.info(f'Connected to {server_string}')
self.server_string = server_string
except LoginError as e:
@@ -164,7 +216,7 @@ class Aprsdis(aprslib.IS):
self.close()
self.logger.error(f"Failed to login '{e}'")
self.logger.exception(e)
raise LoginError("Failed to login")
raise LoginError('Failed to login') from e
def consumer(self, callback, blocking=True, immortal=False, raw=False):
"""
@@ -180,21 +232,21 @@ class Aprsdis(aprslib.IS):
"""
if not self._connected:
raise ConnectionError("not connected to a server")
raise ConnectionError('not connected to a server')
line = b""
line = b''
while True and not self.thread_stop:
try:
for line in self._socket_readlines(blocking):
if line[0:1] != b"#":
if line[0:1] != b'#':
self.aprsd_keepalive = datetime.datetime.now()
if raw:
callback(line)
else:
callback(self._parse(line))
else:
self.logger.debug("Server: %s", line.decode("utf8"))
self.logger.debug('Server: %s', line.decode('utf8'))
self.aprsd_keepalive = datetime.datetime.now()
except ParseError as exp:
self.logger.log(
@@ -211,7 +263,7 @@ class Aprsdis(aprslib.IS):
exp.packet,
)
except LoginError as exp:
self.logger.error("%s: %s", exp.__class__.__name__, exp)
self.logger.error('%s: %s', exp.__class__.__name__, exp)
except (KeyboardInterrupt, SystemExit):
raise
except (ConnectionDrop, ConnectionError):
@@ -227,7 +279,7 @@ class Aprsdis(aprslib.IS):
except StopIteration:
break
except Exception:
self.logger.error("APRS Packet: %s", line)
self.logger.error('APRS Packet: %s', line)
raise
if not blocking:
+5
View File
@@ -1,3 +1,4 @@
import datetime
import logging
import threading
import time
@@ -20,6 +21,9 @@ class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
# flag to tell us to stop
thread_stop = False
# date for last time we heard from the server
aprsd_keepalive = datetime.datetime.now()
lock = threading.Lock()
path = []
@@ -63,6 +67,7 @@ class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
raw = 'GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW'
pkt_raw = aprslib.parse(raw)
pkt = core.factory(pkt_raw)
self.aprsd_keepalive = datetime.datetime.now()
callback(packet=pkt)
LOG.debug(f'END blocking FAKE consumer {self}')
time.sleep(8)
+45 -20
View File
@@ -1,32 +1,36 @@
import datetime
import logging
from ax253 import Frame
import kiss
from ax253 import Frame
from oslo_config import cfg
from aprsd import conf # noqa
from aprsd.packets import core
from aprsd.utils import trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
LOG = logging.getLogger('APRSD')
class KISS3Client:
path = []
# date for last time we heard from the server
aprsd_keepalive = datetime.datetime.now()
_connected = False
def __init__(self):
self.setup()
def is_alive(self):
return True
return self._connected
def setup(self):
# we can be TCP kiss or Serial kiss
if CONF.kiss_serial.enabled:
LOG.debug(
"KISS({}) Serial connection to {}".format(
'KISS({}) Serial connection to {}'.format(
kiss.__version__,
CONF.kiss_serial.device,
),
@@ -39,7 +43,7 @@ class KISS3Client:
self.path = CONF.kiss_serial.path
elif CONF.kiss_tcp.enabled:
LOG.debug(
"KISS({}) TCP Connection to {}:{}".format(
'KISS({}) TCP Connection to {}:{}'.format(
kiss.__version__,
CONF.kiss_tcp.host,
CONF.kiss_tcp.port,
@@ -52,18 +56,34 @@ class KISS3Client:
)
self.path = CONF.kiss_tcp.path
LOG.debug("Starting KISS interface connection")
self.kiss.start()
LOG.debug('Starting KISS interface connection')
try:
self.kiss.start()
if self.kiss.protocol.transport.is_closing():
LOG.warning('KISS transport is closing, not setting consumer callback')
self._connected = False
else:
self._connected = True
except Exception:
LOG.error('Failed to start KISS interface.')
self._connected = False
@trace.trace
def stop(self):
if not self._connected:
# do nothing since we aren't connected
return
try:
self.kiss.stop()
self.kiss.loop.call_soon_threadsafe(
self.kiss.protocol.transport.close,
)
except Exception 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.
@@ -74,18 +94,23 @@ class KISS3Client:
frame = Frame.from_bytes(frame_bytes)
# Now parse it with aprslib
kwargs = {
"frame": frame,
'frame': frame,
}
self._parse_callback(**kwargs)
self.aprsd_keepalive = datetime.datetime.now()
except Exception as ex:
LOG.error("Failed to parse bytes received from KISS interface.")
LOG.error('Failed to parse bytes received from KISS interface.')
LOG.exception(ex)
def consumer(self, callback):
LOG.debug("Start blocking KISS consumer")
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)
LOG.debug(f"END blocking KISS consumer {self.kiss}")
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."""
@@ -94,24 +119,24 @@ class KISS3Client:
path = self.path
if isinstance(packet, core.Packet):
packet.prepare()
payload = packet.payload.encode("US-ASCII")
payload = packet.payload.encode('US-ASCII')
if packet.path:
path = packet.path
else:
msg_payload = f"{packet.raw}{{{str(packet.msgNo)}"
msg_payload = f'{packet.raw}{{{str(packet.msgNo)}'
payload = (
":{:<9}:{}".format(
':{:<9}:{}'.format(
packet.to_call,
msg_payload,
)
).encode("US-ASCII")
).encode('US-ASCII')
LOG.debug(
f"KISS Send '{payload}' TO '{packet.to_call}' From "
f"'{packet.from_call}' with PATH '{path}'",
)
frame = Frame.ui(
destination="APZ100",
destination='APZ100',
source=packet.from_call,
path=path,
info=payload,
+29 -25
View File
@@ -12,7 +12,7 @@ from aprsd.client.drivers import kiss
from aprsd.packets import core
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
LOG = logging.getLogger('APRSD')
LOGU = logger
@@ -27,15 +27,15 @@ class KISSClient(base.APRSClient):
if serializable:
keepalive = keepalive.isoformat()
stats = {
"connected": self.is_connected,
"connection_keepalive": keepalive,
"transport": self.transport(),
'connected': self.is_connected,
'connection_keepalive': keepalive,
'transport': self.transport(),
}
if self.transport() == client.TRANSPORT_TCPKISS:
stats["host"] = CONF.kiss_tcp.host
stats["port"] = CONF.kiss_tcp.port
stats['host'] = CONF.kiss_tcp.host
stats['port'] = CONF.kiss_tcp.port
elif self.transport() == client.TRANSPORT_SERIALKISS:
stats["device"] = CONF.kiss_serial.device
stats['device'] = CONF.kiss_serial.device
return stats
@staticmethod
@@ -56,15 +56,15 @@ class KISSClient(base.APRSClient):
transport = KISSClient.transport()
if transport == client.TRANSPORT_SERIALKISS:
if not CONF.kiss_serial.device:
LOG.error("KISS serial enabled, but no device is set.")
LOG.error('KISS serial enabled, but no device is set.')
raise exception.MissingConfigOptionException(
"kiss_serial.device is not set.",
'kiss_serial.device is not set.',
)
elif transport == client.TRANSPORT_TCPKISS:
if not CONF.kiss_tcp.host:
LOG.error("KISS TCP enabled, but no host is set.")
LOG.error('KISS TCP enabled, but no host is set.')
raise exception.MissingConfigOptionException(
"kiss_tcp.host is not set.",
'kiss_tcp.host is not set.',
)
return True
@@ -91,8 +91,8 @@ class KISSClient(base.APRSClient):
if ka := self._client.aprsd_keepalive:
keepalive = timeago.format(ka)
else:
keepalive = "N/A"
LOGU.opt(colors=True).info(f"<green>Client keepalive {keepalive}</green>")
keepalive = 'N/A'
LOGU.opt(colors=True).info(f'<green>Client keepalive {keepalive}</green>')
@staticmethod
def transport():
@@ -104,8 +104,8 @@ class KISSClient(base.APRSClient):
def decode_packet(self, *args, **kwargs):
"""We get a frame, which has to be decoded."""
LOG.debug(f"kwargs {kwargs}")
frame = kwargs["frame"]
LOG.debug(f'kwargs {kwargs}')
frame = kwargs['frame']
LOG.debug(f"Got an APRS Frame '{frame}'")
# try and nuke the * from the fromcall sign.
# frame.header._source._ch = False
@@ -114,20 +114,23 @@ class KISSClient(base.APRSClient):
# msg = frame.tnc2
# LOG.debug(f"Decoding {msg}")
raw = aprslib.parse(str(frame))
packet = core.factory(raw)
if isinstance(packet, core.ThirdPartyPacket):
return packet.subpacket
else:
return packet
try:
raw = aprslib.parse(str(frame))
packet = core.factory(raw)
if isinstance(packet, core.ThirdPartyPacket):
return packet.subpacket
else:
return packet
except Exception as ex:
LOG.error(f'Error decoding packet: {ex}')
def setup_connection(self):
try:
self._client = kiss.KISS3Client()
self.connected = self.login_status["success"] = True
self.connected = self.login_status['success'] = True
except Exception as ex:
self.connected = self.login_status["success"] = False
self.login_status["message"] = str(ex)
self.connected = self.login_status['success'] = False
self.login_status['message'] = str(ex)
return self._client
def consumer(self, callback, blocking=False, immortal=False, raw=False):
@@ -135,5 +138,6 @@ class KISSClient(base.APRSClient):
self._client.consumer(callback)
self.keepalive = datetime.datetime.now()
except Exception as ex:
LOG.error(f"Consumer failed {ex}")
LOG.error(f'Consumer failed {ex}')
LOG.error(ex)
raise ex
+85 -92
View File
@@ -20,8 +20,10 @@ from aprsd import cli_helper, packets, plugin, threads, utils
from aprsd.client import client_factory
from aprsd.main import cli
from aprsd.packets import collector as packet_collector
from aprsd.packets import core, seen_list
from aprsd.packets import log as packet_log
from aprsd.packets import seen_list
from aprsd.packets.filter import PacketFilter
from aprsd.packets.filters import dupe_filter, packet_type
from aprsd.stats import collector
from aprsd.threads import keepalive, rx
from aprsd.threads import stats as stats_thread
@@ -29,7 +31,7 @@ from aprsd.threads.aprsd import APRSDThread
# setup the global logger
# log.basicConfig(level=log.DEBUG) # level=10
LOG = logging.getLogger("APRSD")
LOG = logging.getLogger('APRSD')
CONF = cfg.CONF
LOGU = logger
console = Console()
@@ -37,9 +39,9 @@ console = Console()
def signal_handler(sig, frame):
threads.APRSDThreadList().stop_all()
if "subprocess" not in str(frame):
if 'subprocess' not in str(frame):
LOG.info(
"Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}".format(
'Ctrl+C, Sending all threads exit! Can take up to 10 seconds {}'.format(
datetime.datetime.now(),
),
)
@@ -48,90 +50,66 @@ def signal_handler(sig, frame):
collector.Collector().collect()
class APRSDListenThread(rx.APRSDRXThread):
class APRSDListenProcessThread(rx.APRSDFilterThread):
def __init__(
self,
packet_queue,
packet_filter=None,
plugin_manager=None,
enabled_plugins=[],
enabled_plugins=None,
log_packets=False,
):
super().__init__(packet_queue)
super().__init__('ListenProcThread', packet_queue)
self.packet_filter = packet_filter
self.plugin_manager = plugin_manager
if self.plugin_manager:
LOG.info(f"Plugins {self.plugin_manager.get_message_plugins()}")
LOG.info(f'Plugins {self.plugin_manager.get_message_plugins()}')
self.log_packets = log_packets
def process_packet(self, *args, **kwargs):
packet = self._client.decode_packet(*args, **kwargs)
filters = {
packets.Packet.__name__: packets.Packet,
packets.AckPacket.__name__: packets.AckPacket,
packets.BeaconPacket.__name__: packets.BeaconPacket,
packets.GPSPacket.__name__: packets.GPSPacket,
packets.MessagePacket.__name__: packets.MessagePacket,
packets.MicEPacket.__name__: packets.MicEPacket,
packets.ObjectPacket.__name__: packets.ObjectPacket,
packets.StatusPacket.__name__: packets.StatusPacket,
packets.ThirdPartyPacket.__name__: packets.ThirdPartyPacket,
packets.WeatherPacket.__name__: packets.WeatherPacket,
packets.UnknownPacket.__name__: packets.UnknownPacket,
}
def print_packet(self, packet):
if self.log_packets:
packet_log.log(packet)
if self.packet_filter:
filter_class = filters[self.packet_filter]
if isinstance(packet, filter_class):
if self.log_packets:
packet_log.log(packet)
if self.plugin_manager:
# Don't do anything with the reply
# This is the listen only command.
self.plugin_manager.run(packet)
else:
if self.log_packets:
packet_log.log(packet)
if self.plugin_manager:
# Don't do anything with the reply.
# This is the listen only command.
self.plugin_manager.run(packet)
packet_collector.PacketCollector().rx(packet)
def process_packet(self, packet: type[core.Packet]):
if self.plugin_manager:
# Don't do anything with the reply.
# This is the listen only command.
self.plugin_manager.run(packet)
class ListenStatsThread(APRSDThread):
"""Log the stats from the PacketList."""
def __init__(self):
super().__init__("PacketStatsLog")
super().__init__('PacketStatsLog')
self._last_total_rx = 0
self.period = 31
def loop(self):
if self.loop_count % 10 == 0:
if self.loop_count % self.period == 0:
# log the stats every 10 seconds
stats_json = collector.Collector().collect()
stats = stats_json["PacketList"]
total_rx = stats["rx"]
packet_count = len(stats["packets"])
stats = stats_json['PacketList']
total_rx = stats['rx']
packet_count = len(stats['packets'])
rx_delta = total_rx - self._last_total_rx
rate = rx_delta / 10
rate = rx_delta / self.period
# Log summary stats
LOGU.opt(colors=True).info(
f"<green>RX Rate: {rate} pps</green> "
f"<yellow>Total RX: {total_rx}</yellow> "
f"<red>RX Last 10 secs: {rx_delta}</red> "
f"<white>Packets in PacketList: {packet_count}</white>",
f'<green>RX Rate: {rate:.2f} pps</green> '
f'<yellow>Total RX: {total_rx}</yellow> '
f'<red>RX Last {self.period} secs: {rx_delta}</red> '
f'<white>Packets in PacketListStats: {packet_count}</white>',
)
self._last_total_rx = total_rx
# Log individual type stats
for k, v in stats["types"].items():
thread_hex = f"fg {utils.hex_from_name(k)}"
for k, v in stats['types'].items():
thread_hex = f'fg {utils.hex_from_name(k)}'
LOGU.opt(colors=True).info(
f"<{thread_hex}>{k:<15}</{thread_hex}> "
f"<blue>RX: {v['rx']}</blue> <red>TX: {v['tx']}</red>",
f'<{thread_hex}>{k:<15}</{thread_hex}> '
f'<blue>RX: {v["rx"]}</blue> <red>TX: {v["tx"]}</red>',
)
time.sleep(1)
@@ -141,19 +119,19 @@ class ListenStatsThread(APRSDThread):
@cli.command()
@cli_helper.add_options(cli_helper.common_options)
@click.option(
"--aprs-login",
envvar="APRS_LOGIN",
'--aprs-login',
envvar='APRS_LOGIN',
show_envvar=True,
help="What callsign to send the message from.",
help='What callsign to send the message from.',
)
@click.option(
"--aprs-password",
envvar="APRS_PASSWORD",
'--aprs-password',
envvar='APRS_PASSWORD',
show_envvar=True,
help="the APRS-IS password for APRS_LOGIN",
help='the APRS-IS password for APRS_LOGIN',
)
@click.option(
"--packet-filter",
'--packet-filter',
type=click.Choice(
[
packets.AckPacket.__name__,
@@ -170,35 +148,37 @@ class ListenStatsThread(APRSDThread):
],
case_sensitive=False,
),
help="Filter by packet type",
)
@click.option(
"--enable-plugin",
multiple=True,
help="Enable a plugin. This is the name of the file in the plugins directory.",
default=[],
help='Filter by packet type',
)
@click.option(
"--load-plugins",
'--enable-plugin',
multiple=True,
help='Enable a plugin. This is the name of the file in the plugins directory.',
)
@click.option(
'--load-plugins',
default=False,
is_flag=True,
help="Load plugins as enabled in aprsd.conf ?",
help='Load plugins as enabled in aprsd.conf ?',
)
@click.argument(
"filter",
'filter',
nargs=-1,
required=True,
)
@click.option(
"--log-packets",
'--log-packets',
default=False,
is_flag=True,
help="Log incoming packets.",
help='Log incoming packets.',
)
@click.option(
"--enable-packet-stats",
'--enable-packet-stats',
default=False,
is_flag=True,
help="Enable packet stats periodic logging.",
help='Enable packet stats periodic logging.',
)
@click.pass_context
@cli_helper.process_standard_options
@@ -228,46 +208,46 @@ def listen(
if not aprs_login:
click.echo(ctx.get_help())
click.echo("")
ctx.fail("Must set --aprs-login or APRS_LOGIN")
click.echo('')
ctx.fail('Must set --aprs-login or APRS_LOGIN')
ctx.exit()
if not aprs_password:
click.echo(ctx.get_help())
click.echo("")
ctx.fail("Must set --aprs-password or APRS_PASSWORD")
click.echo('')
ctx.fail('Must set --aprs-password or APRS_PASSWORD')
ctx.exit()
# CONF.aprs_network.login = aprs_login
# config["aprs"]["password"] = aprs_password
LOG.info(f"APRSD Listen Started version: {aprsd.__version__}")
LOG.info(f'APRSD Listen Started version: {aprsd.__version__}')
CONF.log_opt_values(LOG, logging.DEBUG)
collector.Collector()
# Try and load saved MsgTrack list
LOG.debug("Loading saved MsgTrack object.")
LOG.debug('Loading saved MsgTrack object.')
# Initialize the client factory and create
# The correct client object ready for use
# Make sure we have 1 client transport enabled
if not client_factory.is_client_enabled():
LOG.error("No Clients are enabled in config.")
LOG.error('No Clients are enabled in config.')
sys.exit(-1)
# Creates the client object
LOG.info("Creating client connection")
LOG.info('Creating client connection')
aprs_client = client_factory.create()
LOG.info(aprs_client)
if not aprs_client.login_success:
# We failed to login, will just quit!
msg = f"Login Failure: {aprs_client.login_failure}"
msg = f'Login Failure: {aprs_client.login_failure}'
LOG.error(msg)
print(msg)
sys.exit(-1)
LOG.debug(f"Filter by '{filter}'")
LOG.debug(f"Filter messages on aprsis server by '{filter}'")
aprs_client.set_filter(filter)
keepalive_thread = keepalive.KeepAliveThread()
@@ -276,10 +256,19 @@ def listen(
# just deregister the class from the packet collector
packet_collector.PacketCollector().unregister(seen_list.SeenList)
# we don't want the dupe filter to run here.
PacketFilter().unregister(dupe_filter.DupePacketFilter)
if packet_filter:
LOG.info('Enabling packet filtering for {packet_filter}')
packet_type.PacketTypeFilter().set_allow_list(packet_filter)
PacketFilter().register(packet_type.PacketTypeFilter)
else:
LOG.info('No packet filtering enabled.')
pm = None
if load_plugins:
pm = plugin.PluginManager()
LOG.info("Loading plugins")
LOG.info('Loading plugins')
pm.setup_plugins(load_help_plugin=False)
elif enable_plugin:
pm = plugin.PluginManager()
@@ -290,33 +279,37 @@ def listen(
else:
LOG.warning(
"Not Loading any plugins use --load-plugins to load what's "
"defined in the config file.",
'defined in the config file.',
)
if pm:
for p in pm.get_plugins():
LOG.info("Loaded plugin %s", p.__class__.__name__)
LOG.info('Loaded plugin %s', p.__class__.__name__)
stats = stats_thread.APRSDStatsStoreThread()
stats.start()
LOG.debug("Create APRSDListenThread")
listen_thread = APRSDListenThread(
LOG.debug('Start APRSDRxThread')
rx_thread = rx.APRSDRXThread(packet_queue=threads.packet_queue)
rx_thread.start()
LOG.debug('Create APRSDListenProcessThread')
listen_thread = APRSDListenProcessThread(
packet_queue=threads.packet_queue,
packet_filter=packet_filter,
plugin_manager=pm,
enabled_plugins=enable_plugin,
log_packets=log_packets,
)
LOG.debug("Start APRSDListenThread")
LOG.debug('Start APRSDListenProcessThread')
listen_thread.start()
if enable_packet_stats:
listen_stats = ListenStatsThread()
listen_stats.start()
keepalive_thread.start()
LOG.debug("keepalive Join")
LOG.debug('keepalive Join')
keepalive_thread.join()
LOG.debug("listen_thread Join")
rx_thread.join()
listen_thread.join()
stats.join()
+1 -1
View File
@@ -147,7 +147,7 @@ def server(ctx, flush):
server_threads.register(keepalive.KeepAliveThread())
server_threads.register(stats_thread.APRSDStatsStoreThread())
server_threads.register(
rx.APRSDPluginRXThread(
rx.APRSDRXThread(
packet_queue=threads.packet_queue,
),
)
+100 -101
View File
@@ -3,220 +3,219 @@ from pathlib import Path
from oslo_config import cfg
home = str(Path.home())
DEFAULT_CONFIG_DIR = f"{home}/.config/aprsd/"
APRSD_DEFAULT_MAGIC_WORD = "CHANGEME!!!"
DEFAULT_CONFIG_DIR = f'{home}/.config/aprsd/'
APRSD_DEFAULT_MAGIC_WORD = 'CHANGEME!!!'
watch_list_group = cfg.OptGroup(
name="watch_list",
title="Watch List settings",
name='watch_list',
title='Watch List settings',
)
registry_group = cfg.OptGroup(
name="aprs_registry",
title="APRS Registry settings",
name='aprs_registry',
title='APRS Registry settings',
)
aprsd_opts = [
cfg.StrOpt(
"callsign",
'callsign',
required=True,
help="Callsign to use for messages sent by APRSD",
help='Callsign to use for messages sent by APRSD',
),
cfg.BoolOpt(
"enable_save",
'enable_save',
default=True,
help="Enable saving of watch list, packet tracker between restarts.",
help='Enable saving of watch list, packet tracker between restarts.',
),
cfg.StrOpt(
"save_location",
'save_location',
default=DEFAULT_CONFIG_DIR,
help="Save location for packet tracking files.",
help='Save location for packet tracking files.',
),
cfg.BoolOpt(
"trace_enabled",
'trace_enabled',
default=False,
help="Enable code tracing",
help='Enable code tracing',
),
cfg.StrOpt(
"units",
default="imperial",
help="Units for display, imperial or metric",
'units',
default='imperial',
help='Units for display, imperial or metric',
),
cfg.IntOpt(
"ack_rate_limit_period",
'ack_rate_limit_period',
default=1,
help="The wait period in seconds per Ack packet being sent."
"1 means 1 ack packet per second allowed."
"2 means 1 pack packet every 2 seconds allowed",
help='The wait period in seconds per Ack packet being sent.'
'1 means 1 ack packet per second allowed.'
'2 means 1 pack packet every 2 seconds allowed',
),
cfg.IntOpt(
"msg_rate_limit_period",
'msg_rate_limit_period',
default=2,
help="Wait period in seconds per non AckPacket being sent."
"2 means 1 packet every 2 seconds allowed."
"5 means 1 pack packet every 5 seconds allowed",
help='Wait period in seconds per non AckPacket being sent.'
'2 means 1 packet every 2 seconds allowed.'
'5 means 1 pack packet every 5 seconds allowed',
),
cfg.IntOpt(
"packet_dupe_timeout",
'packet_dupe_timeout',
default=300,
help="The number of seconds before a packet is not considered a duplicate.",
help='The number of seconds before a packet is not considered a duplicate.',
),
cfg.BoolOpt(
"enable_beacon",
'enable_beacon',
default=False,
help="Enable sending of a GPS Beacon packet to locate this service. "
"Requires latitude and longitude to be set.",
help='Enable sending of a GPS Beacon packet to locate this service. '
'Requires latitude and longitude to be set.',
),
cfg.IntOpt(
"beacon_interval",
'beacon_interval',
default=1800,
help="The number of seconds between beacon packets.",
help='The number of seconds between beacon packets.',
),
cfg.StrOpt(
"beacon_symbol",
default="/",
help="The symbol to use for the GPS Beacon packet. See: http://www.aprs.net/vm/DOS/SYMBOLS.HTM",
'beacon_symbol',
default='/',
help='The symbol to use for the GPS Beacon packet. See: http://www.aprs.net/vm/DOS/SYMBOLS.HTM',
),
cfg.StrOpt(
"latitude",
'latitude',
default=None,
help="Latitude for the GPS Beacon button. If not set, the button will not be enabled.",
help='Latitude for the GPS Beacon button. If not set, the button will not be enabled.',
),
cfg.StrOpt(
"longitude",
'longitude',
default=None,
help="Longitude for the GPS Beacon button. If not set, the button will not be enabled.",
help='Longitude for the GPS Beacon button. If not set, the button will not be enabled.',
),
cfg.StrOpt(
"log_packet_format",
choices=["compact", "multiline", "both"],
default="compact",
'log_packet_format',
choices=['compact', 'multiline', 'both'],
default='compact',
help="When logging packets 'compact' will use a single line formatted for each packet."
"'multiline' will use multiple lines for each packet and is the traditional format."
"both will log both compact and multiline.",
'both will log both compact and multiline.',
),
cfg.IntOpt(
"default_packet_send_count",
'default_packet_send_count',
default=3,
help="The number of times to send a non ack packet before giving up.",
help='The number of times to send a non ack packet before giving up.',
),
cfg.IntOpt(
"default_ack_send_count",
'default_ack_send_count',
default=3,
help="The number of times to send an ack packet in response to recieving a packet.",
help='The number of times to send an ack packet in response to recieving a packet.',
),
cfg.IntOpt(
"packet_list_maxlen",
'packet_list_maxlen',
default=100,
help="The maximum number of packets to store in the packet list.",
help='The maximum number of packets to store in the packet list.',
),
cfg.IntOpt(
"packet_list_stats_maxlen",
'packet_list_stats_maxlen',
default=20,
help="The maximum number of packets to send in the stats dict for admin ui.",
help='The maximum number of packets to send in the stats dict for admin ui. -1 means no max.',
),
cfg.BoolOpt(
"enable_seen_list",
'enable_seen_list',
default=True,
help="Enable the Callsign seen list tracking feature. This allows aprsd to keep track of "
"callsigns that have been seen and when they were last seen.",
help='Enable the Callsign seen list tracking feature. This allows aprsd to keep track of '
'callsigns that have been seen and when they were last seen.',
),
cfg.BoolOpt(
"enable_packet_logging",
'enable_packet_logging',
default=True,
help="Set this to False, to disable logging of packets to the log file.",
help='Set this to False, to disable logging of packets to the log file.',
),
cfg.BoolOpt(
"load_help_plugin",
'load_help_plugin',
default=True,
help="Set this to False to disable the help plugin.",
help='Set this to False to disable the help plugin.',
),
cfg.BoolOpt(
"enable_sending_ack_packets",
'enable_sending_ack_packets',
default=True,
help="Set this to False, to disable sending of ack packets. This will entirely stop"
"APRSD from sending ack packets.",
help='Set this to False, to disable sending of ack packets. This will entirely stop'
'APRSD from sending ack packets.',
),
]
watch_list_opts = [
cfg.BoolOpt(
"enabled",
'enabled',
default=False,
help="Enable the watch list feature. Still have to enable "
"the correct plugin. Built-in plugin to use is "
"aprsd.plugins.notify.NotifyPlugin",
help='Enable the watch list feature. Still have to enable '
'the correct plugin. Built-in plugin to use is '
'aprsd.plugins.notify.NotifyPlugin',
),
cfg.ListOpt(
"callsigns",
help="Callsigns to watch for messsages",
'callsigns',
help='Callsigns to watch for messsages',
),
cfg.StrOpt(
"alert_callsign",
help="The Ham Callsign to send messages to for watch list alerts.",
'alert_callsign',
help='The Ham Callsign to send messages to for watch list alerts.',
),
cfg.IntOpt(
"packet_keep_count",
'packet_keep_count',
default=10,
help="The number of packets to store.",
help='The number of packets to store.',
),
cfg.IntOpt(
"alert_time_seconds",
'alert_time_seconds',
default=3600,
help="Time to wait before alert is sent on new message for "
"users in callsigns.",
help='Time to wait before alert is sent on new message for users in callsigns.',
),
]
enabled_plugins_opts = [
cfg.ListOpt(
"enabled_plugins",
'enabled_plugins',
default=[
"aprsd.plugins.fortune.FortunePlugin",
"aprsd.plugins.location.LocationPlugin",
"aprsd.plugins.ping.PingPlugin",
"aprsd.plugins.time.TimePlugin",
"aprsd.plugins.weather.OWMWeatherPlugin",
"aprsd.plugins.version.VersionPlugin",
"aprsd.plugins.notify.NotifySeenPlugin",
'aprsd.plugins.fortune.FortunePlugin',
'aprsd.plugins.location.LocationPlugin',
'aprsd.plugins.ping.PingPlugin',
'aprsd.plugins.time.TimePlugin',
'aprsd.plugins.weather.OWMWeatherPlugin',
'aprsd.plugins.version.VersionPlugin',
'aprsd.plugins.notify.NotifySeenPlugin',
],
help="Comma separated list of enabled plugins for APRSD."
"To enable installed external plugins add them here."
"The full python path to the class name must be used",
help='Comma separated list of enabled plugins for APRSD.'
'To enable installed external plugins add them here.'
'The full python path to the class name must be used',
),
]
registry_opts = [
cfg.BoolOpt(
"enabled",
'enabled',
default=False,
help="Enable sending aprs registry information. This will let the "
help='Enable sending aprs registry information. This will let the '
"APRS registry know about your service and it's uptime. "
"No personal information is sent, just the callsign, uptime and description. "
"The service callsign is the callsign set in [DEFAULT] section.",
'No personal information is sent, just the callsign, uptime and description. '
'The service callsign is the callsign set in [DEFAULT] section.',
),
cfg.StrOpt(
"description",
'description',
default=None,
help="Description of the service to send to the APRS registry. "
"This is what will show up in the APRS registry."
"If not set, the description will be the same as the callsign.",
help='Description of the service to send to the APRS registry. '
'This is what will show up in the APRS registry.'
'If not set, the description will be the same as the callsign.',
),
cfg.StrOpt(
"registry_url",
default="https://aprs.hemna.com/api/v1/registry",
help="The APRS registry domain name to send the information to.",
'registry_url',
default='https://aprs.hemna.com/api/v1/registry',
help='The APRS registry domain name to send the information to.',
),
cfg.StrOpt(
"service_website",
'service_website',
default=None,
help="The website for your APRS service to send to the APRS registry.",
help='The website for your APRS service to send to the APRS registry.',
),
cfg.IntOpt(
"frequency_seconds",
'frequency_seconds',
default=3600,
help="The frequency in seconds to send the APRS registry information.",
help='The frequency in seconds to send the APRS registry information.',
),
]
@@ -232,7 +231,7 @@ def register_opts(config):
def list_opts():
return {
"DEFAULT": (aprsd_opts + enabled_plugins_opts),
'DEFAULT': (aprsd_opts + enabled_plugins_opts),
watch_list_group.name: watch_list_opts,
registry_group.name: registry_opts,
}
+27 -22
View File
@@ -7,47 +7,52 @@ 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',
),
]
+24 -14
View File
@@ -63,11 +63,21 @@ 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():
@@ -76,24 +86,24 @@ def setup_logging(loglevel=None, quiet=False):
handlers = [
{
"sink": sys.stdout,
"serialize": False,
"format": CONF.logging.logformat,
"colorize": True,
"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,
'sink': CONF.logging.logfile,
'serialize': False,
'format': CONF.logging.logformat,
'colorize': False,
'level': log_level,
},
)
# configure loguru
logger.configure(handlers=handlers)
logger.level("DEBUG", color="<fg #BABABA>")
logger.level('DEBUG', color='<fg #BABABA>')
+6
View File
@@ -15,6 +15,8 @@ from aprsd.packets.core import ( # noqa: F401
WeatherPacket,
factory,
)
from aprsd.packets.filter import PacketFilter
from aprsd.packets.filters.dupe_filter import DupePacketFilter
from aprsd.packets.packet_list import PacketList # noqa: F401
from aprsd.packets.seen_list import SeenList # noqa: F401
from aprsd.packets.tracker import PacketTrack # noqa: F401
@@ -26,5 +28,9 @@ collector.PacketCollector().register(SeenList)
collector.PacketCollector().register(PacketTrack)
collector.PacketCollector().register(WatchList)
# Register all the packet filters for normal processing
# For specific commands you can deregister these if you don't want them.
PacketFilter().register(DupePacketFilter)
NULL_MESSAGE = -1
+5 -2
View File
@@ -106,6 +106,8 @@ class Packet:
last_send_time: float = field(repr=False, default=0, compare=False, hash=False)
# Was the packet acked?
acked: bool = field(repr=False, default=False, compare=False, hash=False)
# Was the packet previously processed (for dupe checking)
processed: bool = field(repr=False, default=False, compare=False, hash=False)
# Do we allow this packet to be saved to send later?
allow_delay: bool = field(repr=False, default=True, compare=False, hash=False)
@@ -186,12 +188,11 @@ class Packet:
def __repr__(self) -> str:
"""Build the repr version of the packet."""
repr = (
return (
f"{self.__class__.__name__}:"
f" From: {self.from_call} "
f" To: {self.to_call}"
)
return repr
@dataclass_json
@@ -694,6 +695,8 @@ class UnknownPacket:
path: List[str] = field(default_factory=list, compare=False, hash=False)
packet_type: Optional[str] = field(default=None)
via: Optional[str] = field(default=None, compare=False, hash=False)
# Was the packet previously processed (for dupe checking)
processed: bool = field(repr=False, default=False, compare=False, hash=False)
@property
def key(self) -> str:
+58
View File
@@ -0,0 +1,58 @@
import logging
from typing import Callable, Protocol, runtime_checkable, Union, Dict
from aprsd.packets import core
from aprsd.utils import singleton
LOG = logging.getLogger("APRSD")
@runtime_checkable
class PacketFilterProtocol(Protocol):
"""Protocol API for a packet filter class.
"""
def filter(self, packet: type[core.Packet]) -> Union[type[core.Packet], None]:
"""When we get a packet from the network.
Return a Packet object if the filter passes. Return None if the
Packet is filtered out.
"""
...
@singleton
class PacketFilter:
def __init__(self):
self.filters: Dict[str, Callable] = {}
def register(self, packet_filter: Callable) -> None:
if not isinstance(packet_filter, PacketFilterProtocol):
raise TypeError(f"class {packet_filter} is not a PacketFilterProtocol object")
if packet_filter not in self.filters:
self.filters[packet_filter] = packet_filter()
def unregister(self, packet_filter: Callable) -> None:
if not isinstance(packet_filter, PacketFilterProtocol):
raise TypeError(f"class {packet_filter} is not a PacketFilterProtocol object")
if packet_filter in self.filters:
del self.filters[packet_filter]
def filter(self, packet: type[core.Packet]) -> Union[type[core.Packet], None]:
"""Run through each of the filters.
This will step through each registered filter class
and call filter on it.
If the filter object returns None, we are done filtering.
If the filter object returns the packet, we continue filtering.
"""
for packet_filter in self.filters:
try:
if not self.filters[packet_filter].filter(packet):
LOG.debug(f"{self.filters[packet_filter].__class__.__name__} dropped {packet.__class__.__name__}:{packet.human_info}")
return None
except Exception as ex:
LOG.error(f"{packet_filter.__clas__.__name__} failed filtering packet {packet.__class__.__name__} : {ex}")
return packet
View File
+68
View File
@@ -0,0 +1,68 @@
import logging
from typing import Union
from oslo_config import cfg
from aprsd import packets
from aprsd.packets import core
CONF = cfg.CONF
LOG = logging.getLogger('APRSD')
class DupePacketFilter:
"""This is a packet filter to detect duplicate packets.
This Uses the PacketList object to see if a packet exists
already. If it does exist in the PacketList, then we need to
check the flag on the packet to see if it's been processed before.
If the packet has been processed already within the allowed
timeframe, then it's a dupe.
"""
def filter(self, packet: type[core.Packet]) -> Union[type[core.Packet], None]:
# LOG.debug(f"{self.__class__.__name__}.filter called for packet {packet}")
"""Filter a packet out if it's already been seen and processed."""
if isinstance(packet, core.AckPacket):
# We don't need to drop AckPackets, those should be
# processed.
# Send the AckPacket to the queue for processing elsewhere.
return packet
else:
# Make sure we aren't re-processing the same packet
# For RF based APRS Clients we can get duplicate packets
# So we need to track them and not process the dupes.
pkt_list = packets.PacketList()
found = False
try:
# Find the packet in the list of already seen packets
# Based on the packet.key
found = pkt_list.find(packet)
if not packet.msgNo:
# If the packet doesn't have a message id
# then there is no reliable way to detect
# if it's a dupe, so we just pass it on.
# it shouldn't get acked either.
found = False
except KeyError:
found = False
if not found:
# We haven't seen this packet before, so we process it.
return packet
if not packet.processed:
# We haven't processed this packet through the plugins.
return packet
elif packet.timestamp - found.timestamp < CONF.packet_dupe_timeout:
# If the packet came in within N seconds of the
# Last time seeing the packet, then we drop it as a dupe.
LOG.warning(
f'Packet {packet.from_call}:{packet.msgNo} already tracked, dropping.'
)
else:
LOG.warning(
f'Packet {packet.from_call}:{packet.msgNo} already tracked '
f'but older than {CONF.packet_dupe_timeout} seconds. processing.',
)
return packet
+53
View File
@@ -0,0 +1,53 @@
import logging
from typing import Union
from oslo_config import cfg
from aprsd import packets
from aprsd.packets import core
from aprsd.utils import singleton
CONF = cfg.CONF
LOG = logging.getLogger('APRSD')
@singleton
class PacketTypeFilter:
"""This filter is used to filter out packets that don't match a specific type.
To use this, register it with the PacketFilter class,
then instante it and call set_allow_list() with a list of packet types
you want to allow to pass the filtering. All other packets will be
filtered out.
"""
filters = {
packets.Packet.__name__: packets.Packet,
packets.AckPacket.__name__: packets.AckPacket,
packets.BeaconPacket.__name__: packets.BeaconPacket,
packets.GPSPacket.__name__: packets.GPSPacket,
packets.MessagePacket.__name__: packets.MessagePacket,
packets.MicEPacket.__name__: packets.MicEPacket,
packets.ObjectPacket.__name__: packets.ObjectPacket,
packets.StatusPacket.__name__: packets.StatusPacket,
packets.ThirdPartyPacket.__name__: packets.ThirdPartyPacket,
packets.WeatherPacket.__name__: packets.WeatherPacket,
packets.UnknownPacket.__name__: packets.UnknownPacket,
}
allow_list = ()
def set_allow_list(self, filter_list):
tmp_list = []
for filter in filter_list:
LOG.warning(
f'Setting filter {filter} : {self.filters[filter]} to tmp {tmp_list}'
)
tmp_list.append(self.filters[filter])
self.allow_list = tuple(tmp_list)
def filter(self, packet: type[core.Packet]) -> Union[type[core.Packet], None]:
"""Only allow packets of certain types to filter through."""
if self.allow_list:
if isinstance(packet, self.allow_list):
return packet
+33 -27
View File
@@ -7,7 +7,7 @@ from aprsd.packets import core
from aprsd.utils import objectstore
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
LOG = logging.getLogger('APRSD')
class PacketList(objectstore.ObjectStoreMixin):
@@ -27,8 +27,8 @@ class PacketList(objectstore.ObjectStoreMixin):
def _init_data(self):
self.data = {
"types": {},
"packets": OrderedDict(),
'types': {},
'packets': OrderedDict(),
}
def rx(self, packet: type[core.Packet]):
@@ -37,11 +37,11 @@ class PacketList(objectstore.ObjectStoreMixin):
self._total_rx += 1
self._add(packet)
ptype = packet.__class__.__name__
type_stats = self.data["types"].setdefault(
type_stats = self.data['types'].setdefault(
ptype,
{"tx": 0, "rx": 0},
{'tx': 0, 'rx': 0},
)
type_stats["rx"] += 1
type_stats['rx'] += 1
def tx(self, packet: type[core.Packet]):
"""Add a packet that was received."""
@@ -49,32 +49,32 @@ class PacketList(objectstore.ObjectStoreMixin):
self._total_tx += 1
self._add(packet)
ptype = packet.__class__.__name__
type_stats = self.data["types"].setdefault(
type_stats = self.data['types'].setdefault(
ptype,
{"tx": 0, "rx": 0},
{'tx': 0, 'rx': 0},
)
type_stats["tx"] += 1
type_stats['tx'] += 1
def add(self, packet):
with self.lock:
self._add(packet)
def _add(self, packet):
if not self.data.get("packets"):
if not self.data.get('packets'):
self._init_data()
if packet.key in self.data["packets"]:
self.data["packets"].move_to_end(packet.key)
elif len(self.data["packets"]) == self.maxlen:
self.data["packets"].popitem(last=False)
self.data["packets"][packet.key] = packet
if packet.key in self.data['packets']:
self.data['packets'].move_to_end(packet.key)
elif len(self.data['packets']) == self.maxlen:
self.data['packets'].popitem(last=False)
self.data['packets'][packet.key] = packet
def find(self, packet):
with self.lock:
return self.data["packets"][packet.key]
return self.data['packets'][packet.key]
def __len__(self):
with self.lock:
return len(self.data["packets"])
return len(self.data['packets'])
def total_rx(self):
with self.lock:
@@ -87,17 +87,23 @@ class PacketList(objectstore.ObjectStoreMixin):
def stats(self, serializable=False) -> dict:
with self.lock:
# Get last N packets directly using list slicing
packets_list = list(self.data.get("packets", {}).values())
pkts = packets_list[-CONF.packet_list_stats_maxlen :][::-1]
if CONF.packet_list_stats_maxlen >= 0:
packets_list = list(self.data.get('packets', {}).values())
pkts = packets_list[-CONF.packet_list_stats_maxlen :][::-1]
else:
# We have to copy here, because this get() results in a pointer
# to the packets internally here, which can change after this
# function returns, which would cause a problem trying to save
# the stats to disk.
pkts = self.data.get('packets', {}).copy()
stats = {
"total_tracked": self._total_rx
'total_tracked': self._total_rx
+ self._total_tx, # Fixed typo: was rx + rx
"rx": self._total_rx,
"tx": self._total_tx,
"types": self.data.get("types", {}), # Changed default from [] to {}
"packet_count": len(self.data.get("packets", [])),
"maxlen": self.maxlen,
"packets": pkts,
'rx': self._total_rx,
'tx': self._total_tx,
'types': self.data.get('types', {}), # Changed default from [] to {}
'packet_count': len(self.data.get('packets', [])),
'maxlen': self.maxlen,
'packets': pkts,
}
return stats
+21 -15
View File
@@ -5,24 +5,30 @@ import subprocess
from aprsd import packets, plugin
from aprsd.utils import trace
LOG = logging.getLogger('APRSD')
LOG = logging.getLogger("APRSD")
DEFAULT_FORTUNE_PATH = "/usr/games/fortune"
FORTUNE_PATHS = [
'/usr/games/fortune',
'/usr/local/bin/fortune',
'/usr/bin/fortune',
]
class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
"""Fortune."""
command_regex = r"^([f]|[f]\s|fortune)"
command_name = "fortune"
short_description = "Give me a fortune"
command_regex = r'^([f]|[f]\s|fortune)'
command_name = 'fortune'
short_description = 'Give me a fortune'
fortune_path = None
def setup(self):
self.fortune_path = shutil.which(DEFAULT_FORTUNE_PATH)
LOG.info(f"Fortune path {self.fortune_path}")
for path in FORTUNE_PATHS:
self.fortune_path = shutil.which(path)
LOG.info(f'Fortune path {self.fortune_path}')
if self.fortune_path:
break
if not self.fortune_path:
self.enabled = False
else:
@@ -30,7 +36,7 @@ class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
@trace.trace
def process(self, packet: packets.MessagePacket):
LOG.info("FortunePlugin")
LOG.info('FortunePlugin')
# fromcall = packet.get("from")
# message = packet.get("message_text", None)
@@ -39,8 +45,8 @@ class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
reply = None
try:
cmnd = [self.fortune_path, "-s", "-n 60"]
command = " ".join(cmnd)
cmnd = [self.fortune_path, '-s', '-n 60']
command = ' '.join(cmnd)
output = subprocess.check_output(
command,
shell=True,
@@ -48,10 +54,10 @@ class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
text=True,
)
output = (
output.replace("\r", "")
.replace("\n", "")
.replace(" ", "")
.replace("\t", " ")
output.replace('\r', '')
.replace('\n', '')
.replace(' ', '')
.replace('\t', ' ')
)
except subprocess.CalledProcessError as ex:
reply = f"Fortune command failed '{ex.output}'"
+1 -2
View File
@@ -4,9 +4,8 @@ import queue
# aprsd.threads
from .aprsd import APRSDThread, APRSDThreadList # noqa: F401
from .rx import ( # noqa: F401
APRSDDupeRXThread,
APRSDProcessPacketThread,
APRSDRXThread,
)
packet_queue = queue.Queue(maxsize=20)
packet_queue = queue.Queue(maxsize=500)
+83 -75
View File
@@ -8,20 +8,32 @@ from oslo_config import cfg
from aprsd import packets, plugin
from aprsd.client import client_factory
from aprsd.packets import collector
from aprsd.packets import collector, filter
from aprsd.packets import log as packet_log
from aprsd.threads import APRSDThread, tx
from aprsd.utils import trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
LOG = logging.getLogger('APRSD')
class APRSDRXThread(APRSDThread):
"""Main Class to connect to an APRS Client and recieve packets.
A packet is received in the main loop and then sent to the
process_packet method, which sends the packet through the collector
to track the packet for stats, and then put into the packet queue
for processing in a separate thread.
"""
_client = None
# This is the queue that packets are sent to for processing.
# We process packets in a separate thread to help prevent
# getting blocked by the APRS server trying to send us packets.
packet_queue = None
def __init__(self, packet_queue):
super().__init__("RX_PKT")
super().__init__('RX_PKT')
self.packet_queue = packet_queue
def stop(self):
@@ -52,7 +64,7 @@ class APRSDRXThread(APRSDThread):
# kwargs. :(
# https://github.com/rossengeorgiev/aprs-python/pull/56
self._client.consumer(
self._process_packet,
self.process_packet,
raw=False,
blocking=False,
)
@@ -60,7 +72,7 @@ class APRSDRXThread(APRSDThread):
aprslib.exceptions.ConnectionDrop,
aprslib.exceptions.ConnectionError,
):
LOG.error("Connection dropped, reconnecting")
LOG.error('Connection dropped, reconnecting')
# Force the deletion of the client object connected to aprs
# This will cause a reconnect, next time client.get_client()
# is called
@@ -68,45 +80,18 @@ class APRSDRXThread(APRSDThread):
time.sleep(5)
except Exception:
# LOG.exception(ex)
LOG.error("Resetting connection and trying again.")
LOG.error('Resetting connection and trying again.')
self._client.reset()
time.sleep(5)
# Continue to loop
time.sleep(1)
return True
def _process_packet(self, *args, **kwargs):
"""Intermediate callback so we can update the keepalive time."""
# Now call the 'real' packet processing for a RX'x packet
self.process_packet(*args, **kwargs)
@abc.abstractmethod
def process_packet(self, *args, **kwargs):
pass
class APRSDDupeRXThread(APRSDRXThread):
"""Process received packets.
This is the main APRSD Server command thread that
receives packets and makes sure the packet
hasn't been seen previously before sending it on
to be processed.
"""
@trace.trace
def process_packet(self, *args, **kwargs):
"""This handles the processing of an inbound packet.
When a packet is received by the connected client object,
it sends the raw packet into this function. This function then
decodes the packet via the client, and then processes the packet.
Ack Packets are sent to the PluginProcessPacketThread for processing.
All other packets have to be checked as a dupe, and then only after
we haven't seen this packet before, do we send it to the
PluginProcessPacketThread for processing.
"""
packet = self._client.decode_packet(*args, **kwargs)
if not packet:
LOG.error(
'No packet received from decode_packet. Most likely a failure to parse'
)
return
packet_log.log(packet)
pkt_list = packets.PacketList()
@@ -140,26 +125,55 @@ class APRSDDupeRXThread(APRSDRXThread):
# If the packet came in within N seconds of the
# Last time seeing the packet, then we drop it as a dupe.
LOG.warning(
f"Packet {packet.from_call}:{packet.msgNo} already tracked, dropping."
f'Packet {packet.from_call}:{packet.msgNo} already tracked, dropping.'
)
else:
LOG.warning(
f"Packet {packet.from_call}:{packet.msgNo} already tracked "
f"but older than {CONF.packet_dupe_timeout} seconds. processing.",
f'Packet {packet.from_call}:{packet.msgNo} already tracked '
f'but older than {CONF.packet_dupe_timeout} seconds. processing.',
)
collector.PacketCollector().rx(packet)
self.packet_queue.put(packet)
class APRSDPluginRXThread(APRSDDupeRXThread):
""" "Process received packets.
class APRSDFilterThread(APRSDThread):
def __init__(self, thread_name, packet_queue):
super().__init__(thread_name)
self.packet_queue = packet_queue
For backwards compatibility, we keep the APRSDPluginRXThread.
"""
def filter_packet(self, packet):
# Do any packet filtering prior to processing
if not filter.PacketFilter().filter(packet):
return None
return packet
def print_packet(self, packet):
"""Allow a child of this class to override this.
This is helpful if for whatever reason the child class
doesn't want to log packets.
"""
packet_log.log(packet)
def loop(self):
try:
packet = self.packet_queue.get(timeout=1)
self.print_packet(packet)
if packet:
if self.filter_packet(packet):
self.process_packet(packet)
except queue.Empty:
pass
return True
class APRSDProcessPacketThread(APRSDThread):
"""Base class for processing received packets.
class APRSDProcessPacketThread(APRSDFilterThread):
"""Base class for processing received packets after they have been filtered.
Packets are received from the client, then filtered for dupes,
then sent to the packet queue. This thread pulls packets from
the packet queue for processing.
This is the base class for processing packets coming from
the consumer. This base class handles sending ack packets and
@@ -167,44 +181,38 @@ class APRSDProcessPacketThread(APRSDThread):
for processing."""
def __init__(self, packet_queue):
self.packet_queue = packet_queue
super().__init__("ProcessPKT")
super().__init__('ProcessPKT', packet_queue=packet_queue)
if not CONF.enable_sending_ack_packets:
LOG.warning(
"Sending ack packets is disabled, messages "
"will not be acknowledged.",
'Sending ack packets is disabled, messages will not be acknowledged.',
)
def process_ack_packet(self, packet):
"""We got an ack for a message, no need to resend it."""
ack_num = packet.msgNo
LOG.debug(f"Got ack for message {ack_num}")
LOG.debug(f'Got ack for message {ack_num}')
collector.PacketCollector().rx(packet)
def process_piggyback_ack(self, packet):
"""We got an ack embedded in a packet."""
ack_num = packet.ackMsgNo
LOG.debug(f"Got PiggyBackAck for message {ack_num}")
LOG.debug(f'Got PiggyBackAck for message {ack_num}')
collector.PacketCollector().rx(packet)
def process_reject_packet(self, packet):
"""We got a reject message for a packet. Stop sending the message."""
ack_num = packet.msgNo
LOG.debug(f"Got REJECT for message {ack_num}")
LOG.debug(f'Got REJECT for message {ack_num}')
collector.PacketCollector().rx(packet)
def loop(self):
try:
packet = self.packet_queue.get(timeout=1)
if packet:
self.process_packet(packet)
except queue.Empty:
pass
return True
def process_packet(self, packet):
"""Process a packet received from aprs-is server."""
LOG.debug(f"ProcessPKT-LOOP {self.loop_count}")
LOG.debug(f'ProcessPKT-LOOP {self.loop_count}')
# set this now as we are going to process it.
# This is used during dupe checking, so set it early
packet.processed = True
our_call = CONF.callsign.lower()
from_call = packet.from_call
@@ -227,7 +235,7 @@ class APRSDProcessPacketThread(APRSDThread):
):
self.process_reject_packet(packet)
else:
if hasattr(packet, "ackMsgNo") and packet.ackMsgNo:
if hasattr(packet, 'ackMsgNo') and packet.ackMsgNo:
# we got an ack embedded in this packet
# we need to handle the ack
self.process_piggyback_ack(packet)
@@ -267,7 +275,7 @@ class APRSDProcessPacketThread(APRSDThread):
if not for_us:
LOG.info("Got a packet meant for someone else '{packet.to_call}'")
else:
LOG.info("Got a non AckPacket/MessagePacket")
LOG.info('Got a non AckPacket/MessagePacket')
class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
@@ -287,7 +295,7 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
tx.send(subreply)
else:
wl = CONF.watch_list
to_call = wl["alert_callsign"]
to_call = wl['alert_callsign']
tx.send(
packets.MessagePacket(
from_call=CONF.callsign,
@@ -299,7 +307,7 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
# We have a message based object.
tx.send(reply)
except Exception as ex:
LOG.error("Plugin failed!!!")
LOG.error('Plugin failed!!!')
LOG.exception(ex)
def process_our_message_packet(self, packet):
@@ -355,11 +363,11 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
if to_call == CONF.callsign and not replied:
# Tailor the messages accordingly
if CONF.load_help_plugin:
LOG.warning("Sending help!")
LOG.warning('Sending help!')
message_text = "Unknown command! Send 'help' message for help"
else:
LOG.warning("Unknown command!")
message_text = "Unknown command!"
LOG.warning('Unknown command!')
message_text = 'Unknown command!'
tx.send(
packets.MessagePacket(
@@ -369,11 +377,11 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
),
)
except Exception as ex:
LOG.error("Plugin failed!!!")
LOG.error('Plugin failed!!!')
LOG.exception(ex)
# Do we need to send a reply?
if to_call == CONF.callsign:
reply = "A Plugin failed! try again?"
reply = 'A Plugin failed! try again?'
tx.send(
packets.MessagePacket(
from_call=CONF.callsign,
@@ -382,4 +390,4 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
),
)
LOG.debug("Completed process_our_message_packet")
LOG.debug('Completed process_our_message_packet')
+4 -9
View File
@@ -1,8 +1,6 @@
import logging
import threading
import time
import wrapt
from oslo_config import cfg
from aprsd.stats import collector
@@ -10,18 +8,15 @@ from aprsd.threads import APRSDThread
from aprsd.utils import objectstore
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
LOG = logging.getLogger('APRSD')
class StatsStore(objectstore.ObjectStoreMixin):
"""Container to save the stats from the collector."""
lock = threading.Lock()
data = {}
@wrapt.synchronized(lock)
def add(self, stats: dict):
self.data = stats
with self.lock:
self.data = stats
class APRSDStatsStoreThread(APRSDThread):
@@ -31,7 +26,7 @@ class APRSDStatsStoreThread(APRSDThread):
save_interval = 10
def __init__(self):
super().__init__("StatsStore")
super().__init__('StatsStore')
def loop(self):
if self.loop_count % self.save_interval == 0:
+12 -13
View File
@@ -6,9 +6,8 @@ import threading
from oslo_config import cfg
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
LOG = logging.getLogger('APRSD')
class ObjectStoreMixin:
@@ -63,7 +62,7 @@ class ObjectStoreMixin:
def _save_filename(self):
save_location = CONF.save_location
return "{}/{}.p".format(
return '{}/{}.p'.format(
save_location,
self.__class__.__name__.lower(),
)
@@ -75,13 +74,13 @@ class ObjectStoreMixin:
self._init_store()
save_filename = self._save_filename()
if len(self) > 0:
LOG.info(
f"{self.__class__.__name__}::Saving"
f" {len(self)} entries to disk at "
f"{save_filename}",
LOG.debug(
f'{self.__class__.__name__}::Saving'
f' {len(self)} entries to disk at '
f'{save_filename}',
)
with self.lock:
with open(save_filename, "wb+") as fp:
with open(save_filename, 'wb+') as fp:
pickle.dump(self.data, fp)
else:
LOG.debug(
@@ -97,21 +96,21 @@ class ObjectStoreMixin:
return
if os.path.exists(self._save_filename()):
try:
with open(self._save_filename(), "rb") as fp:
with open(self._save_filename(), 'rb') as fp:
raw = pickle.load(fp)
if raw:
self.data = raw
LOG.debug(
f"{self.__class__.__name__}::Loaded {len(self)} entries from disk.",
f'{self.__class__.__name__}::Loaded {len(self)} entries from disk.',
)
else:
LOG.debug(f"{self.__class__.__name__}::No data to load.")
LOG.debug(f'{self.__class__.__name__}::No data to load.')
except (pickle.UnpicklingError, Exception) as ex:
LOG.error(f"Failed to UnPickle {self._save_filename()}")
LOG.error(f'Failed to UnPickle {self._save_filename()}')
LOG.error(ex)
self.data = {}
else:
LOG.debug(f"{self.__class__.__name__}::No save file found.")
LOG.debug(f'{self.__class__.__name__}::No save file found.')
def flush(self):
"""Nuke the old pickle file that stored the old results from last aprsd run."""
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

+2 -1
View File
@@ -55,7 +55,8 @@ RUN if [ "$INSTALL_TYPE" = "pypi" ]; then \
uv pip install aprsd==$APRSD_PIP_VERSION; \
elif [ "$INSTALL_TYPE" = "github" ]; then \
git clone -b $APRSD_BRANCH https://github.com/craigerl/aprsd; \
cd /app/aprsd && uv pip install .; \
ls -al /app/aprsd; \
uv pip install /app/aprsd; \
ls -al /app/.venv/lib/python3.11/site-packages/aprsd*; \
fi
# RUN uv pip install gevent uwsgi
+1 -1
View File
@@ -39,4 +39,4 @@ fi
export COLUMNS=200
python3 -m rich.diagnose
exec aprsd listen -c $APRSD_CONFIG --loglevel ${LOG_LEVEL} ${APRSD_LOAD_PLUGINS} ${APRSD_LISTEN_FILTER}
exec aprsd listen -c $APRSD_CONFIG --loglevel ${LOG_LEVEL} --enable-packet-stats ${APRSD_LOAD_PLUGINS} ${APRSD_LISTEN_FILTER}
+1 -1
View File
@@ -30,7 +30,7 @@ dynamic = ["version", "dependencies", "optional-dependencies"]
#
# This field corresponds to the "Description" metadata field:
# https://packaging.python.org/specifications/core-metadata/#description-optional
readme = {file = "README.rst", content-type = "text/x-rst"}
readme = {file = "README.md", content-type = "text/markdown"}
# This is either text indicating the license for the distribution, or a file
+8 -8
View File
@@ -3,7 +3,7 @@
alabaster==1.0.0 # via sphinx
babel==2.16.0 # via sphinx
build==1.2.2.post1 # via pip-tools, -r requirements-dev.in
cachetools==5.5.0 # via tox
cachetools==5.5.1 # via tox
certifi==2024.12.14 # via requests
cfgv==3.4.0 # via pre-commit
chardet==5.2.0 # via tox
@@ -12,8 +12,8 @@ click==8.1.8 # via pip-tools
colorama==0.4.6 # via tox
distlib==0.3.9 # via virtualenv
docutils==0.21.2 # via m2r, sphinx
filelock==3.16.1 # via tox, virtualenv
identify==2.6.5 # via pre-commit
filelock==3.17.0 # via tox, virtualenv
identify==2.6.6 # via pre-commit
idna==3.10 # via requests
imagesize==1.4.1 # via sphinx
jinja2==3.1.5 # via sphinx
@@ -26,13 +26,13 @@ pip==24.3.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
pre-commit==4.0.1 # via -r requirements-dev.in
pre-commit==4.1.0 # via -r requirements-dev.in
pygments==2.19.1 # via sphinx
pyproject-api==1.8.0 # via tox
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.7.0 # via pip-tools
setuptools==75.8.0 # 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
@@ -42,8 +42,8 @@ sphinxcontrib-jsmath==1.0.1 # via sphinx
sphinxcontrib-qthelp==2.0.0 # via sphinx
sphinxcontrib-serializinghtml==2.0.0 # via sphinx
tomli==2.2.1 # via build, pip-tools, pyproject-api, sphinx, tox
tox==4.23.2 # via -r requirements-dev.in
tox==4.24.1 # via -r requirements-dev.in
typing-extensions==4.12.2 # via tox
urllib3==2.3.0 # via requests
virtualenv==20.28.1 # via pre-commit, tox
virtualenv==20.29.1 # via pre-commit, tox
wheel==0.45.1 # via pip-tools, -r requirements-dev.in
+3 -3
View File
@@ -12,10 +12,10 @@ dataclasses-json==0.6.7 # via -r requirements.in
debtcollector==3.0.0 # via oslo-config
haversine==2.9.0 # via -r requirements.in
idna==3.10 # via requests
importlib-metadata==8.5.0 # via ax253, kiss3
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.24.1 # via dataclasses-json
marshmallow==3.26.0 # via dataclasses-json
mypy-extensions==1.0.0 # via typing-inspect
netaddr==1.3.0 # via oslo-config
oslo-config==9.7.0 # via -r requirements.in
@@ -40,5 +40,5 @@ typing-inspect==0.9.0 # via dataclasses-json
tzlocal==5.2 # via -r requirements.in
update-checker==0.18.0 # via -r requirements.in
urllib3==2.3.0 # via requests
wrapt==1.17.0 # via debtcollector, -r requirements.in
wrapt==1.17.2 # via debtcollector, -r requirements.in
zipp==3.21.0 # via importlib-metadata
Generated
+1340
View File
File diff suppressed because it is too large Load Diff