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

Compare commits

...

43 Commits

Author SHA1 Message Date
hemna c37e1d58bb Changelog for 3.3.0 2024-03-12 10:08:38 -04:00
hemna 0ca5ceee7e sample-config fix
This patch makes a change on how it's calling importlib.entry_points
to only fetch the group we want, which is 'oslo.config.opts'.
This fixes a problem with python 3.12 compatibility.
2024-03-11 11:53:28 -04:00
hemna 2e9c9d40e1 Fixed registry url post 2024-03-08 11:49:10 -05:00
hemna 66004f639f Changed processpkt message
this includes the pkt.key in the log entry
2024-03-08 11:25:46 -05:00
hemna 0b0afd39ed Fixed RegistryThread not sending requests 2024-03-08 09:18:28 -05:00
hemna aec88d4a7e use log.setup_logging 2024-03-07 12:43:10 -05:00
hemna 24bbea1d49 Disable debug logs for aprslib
This patch adds a disable of propogating the debug logs
from the aprslib parsing.  We don't really need to see
this in our aprsd services.
2024-03-07 09:46:36 -05:00
hemna 5d3f42f411 Make registry thread sleep
This patch adds a required sleep of 1 second in each
registry thread loop to prevent runaway cpu usage
2024-03-07 08:37:09 -05:00
hemna 44a98850c9 Merge pull request #147 from craigerl/loguru
Replace slow rich logging with loguru
2024-03-06 14:18:19 -05:00
hemna 2cb9c2a31c Put threads first after date/time 2024-03-06 13:39:51 -05:00
hemna 2fefa9fcd6 Replace slow rich logging with loguru
This patch removes the rich logging with
the modern loguru logging
2024-03-06 13:00:52 -05:00
hemna d092a43ec9 Updated requirements 2024-03-06 12:59:21 -05:00
hemna d1a09fc6b5 Fixed pep8 2024-02-28 16:24:01 -05:00
hemna ff051bc285 Added list-extensions and updated README.rst
This patch adds the list-extensions command to support
showing the available extensions for APRSD that live on
pypi.
2024-02-28 16:10:55 -05:00
hemna 5fd91a2172 Change defaults for beacon and registry
The beacon frequency is now every 30 minutes by default.
The registry call is now every hour.
2024-02-28 13:23:11 -05:00
hemna a4630c15be Add log info for Beacon and Registry threads 2024-02-27 16:01:15 -05:00
hemna 6a7d7ad79b fixed frequency_seconds to IntOpt 2024-02-27 15:53:03 -05:00
hemna 7a5b55fa77 fixed references to conf 2024-02-27 15:48:58 -05:00
hemna a1e21e795d changed the default packet timeout to 5 minutes 2024-02-27 15:11:39 -05:00
hemna cb291de047 Fixed default service registry url 2024-02-27 15:10:21 -05:00
hemna e9c48c1914 fix pep8 failures 2024-02-27 14:21:04 -05:00
hemna f0ad6d7577 py311 fails in github 2024-02-27 13:46:28 -05:00
hemna 38fe408c82 Don't send uptime to registry 2024-02-27 13:40:39 -05:00
hemna 8264c94bd6 Added sending software string to registry
This patch adds sending the APRSD signature and url
along with the regsitry request.
2024-02-27 11:05:41 -05:00
hemna 1ad2e135dc add py310 gh actions 2024-02-27 08:14:05 -05:00
hemna 1e4f0ca65a Added the new APRS Registry thread
This patch adds the new APRSRegistryThread,
which enabled in config, will send a small
packet of information to the as yet deployed
APRS service registry every 900 seconds.

The data that this thread will send is
the service callsign, a description of the service,
a website url for the service.

The idea being that the registry website that this thread
sends information to, will show all the services that are
running on the ARPS network, so Ham operators can discover
them and try them out.
2024-02-26 18:28:52 -05:00
hemna 41185416cb Added installing extensions to Docker run
This patch adds the installation of APRSD via pip during startup
time for the main server run.sh, admin.sh and listen.sh
2024-02-25 15:05:45 -05:00
hemna 68f23d8ca7 Cleanup some logs
This patch removes some debug logging from the clients.
2024-02-25 15:04:26 -05:00
hemna 11f1e9533e Added BeaconPacket
This patch adds the BeaconPacket and BeaconSendThread.
This will enable APRSD server to send a beacon if enabled in
the config.
2024-02-25 14:21:17 -05:00
hemna 275bf67b9e updated requirements files 2024-02-24 14:30:08 -05:00
hemna 968345944a removed some unneeded code
removed the callsigns locations iterator
2024-02-24 14:28:37 -05:00
hemna df2798eafb Added iterator to objectstore
Since the objectstore mixin uses a iterable to store it's data,
it was easy to add an __iter__ to the objectstore class itself.
2024-02-24 14:27:39 -05:00
hemna e89f8a805b Added some missing classes to threads
Added new APRSDupeThread
2024-02-24 14:26:55 -05:00
hemna b14307270c Added support for loading extensions
This patch adds support for loading extenions
to APRSD!!

You can create another separate aprsd project, and register
your extension in your setup.cfg as a new entry point for aprsd
like

[entry_points]
aprsd.extension =
    cool = my_project.extension

in your my_project/extension.py file
import your commmands and away you go.
2024-02-23 16:53:42 -05:00
hemna ebee8e1439 Merge pull request #146 from craigerl/webchat-location
Added location for callsign tabs in webchat
2024-02-20 10:30:21 -05:00
hemna a7e30b0bed Added location for callsign tabs in webchat
This patch adds the new feature of trying to fetch the location
distance and bearing for each callsign in the webchat tabs.
This is handy when out on the go, you can get a general idea
where the other callsign is when chatting with them.

First aprsd webchat tries to fetch the location with aprs.fi
REST api call.  This assumes internet access.  If this fails,
then webchat will send a special message to REPEAT to ask it for
the location information for the callsign.   This will send over
the air.
2024-02-20 10:18:22 -05:00
hemna 1a5c5f0dce updated gitignore 2024-02-15 14:42:04 -05:00
hemna a00c4ea840 Create codeql.yml
Try out code scanning
2024-02-07 09:43:29 -05:00
hemna a88de2f09c update github action branchs to v8 2024-02-07 08:55:24 -05:00
hemna d6f0f05315 Added Location info on webchat interface
This patch adds a new popover in the webchat tab to show
the location information for a callsign.

webchat will try to hit aprs.fi to fetch the location from the
callsign's last beacon.  If there is no internet, this will fail
and webchat will send a request to REPEAT callsign for the location
information.
2024-02-06 16:52:56 -05:00
hemna 03c58f83cd Updated dev test-plugin command
This patch updates the output of the aprsd dev test-plugin command
to show the packets that would actually get sent by the plugin
results.
2024-01-19 11:30:15 -05:00
hemna a4230d324a Update requirements.txt 2024-01-16 16:45:07 -05:00
hemna 8bceb827ec Update for v3.2.3 2024-01-09 09:12:01 -05:00
41 changed files with 1199 additions and 534 deletions
+84
View File
@@ -0,0 +1,84 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
schedule:
- cron: '36 8 * * 0'
jobs:
analyze:
name: Analyze
# Runner size impacts CodeQL analysis time. To learn more, please see:
# - https://gh.io/recommended-hardware-resources-for-running-codeql
# - https://gh.io/supported-runners-and-hardware-resources
# - https://gh.io/using-larger-runners
# Consider using larger runners for possible analysis time improvements.
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
permissions:
# required for all workflows
security-events: write
# only required for workflows in private repositories
actions: read
contents: read
strategy:
fail-fast: false
matrix:
language: [ 'javascript-typescript', 'python' ]
# CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ]
# Use only 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v4
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
- uses: actions/checkout@v3
- name: Get Branch Name
id: branch-name
uses: tj-actions/branch-names@v7
uses: tj-actions/branch-names@v8
- name: Extract Branch
id: extract_branch
run: |
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
- uses: actions/checkout@v3
- name: Get Branch Name
id: branch-name
uses: tj-actions/branch-names@v6
uses: tj-actions/branch-names@v8
- name: Setup QEMU
uses: docker/setup-qemu-action@v2
- name: Setup Docker Buildx
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
- uses: actions/checkout@v3
- name: Get Branch Name
id: branch-name
uses: tj-actions/branch-names@v6
uses: tj-actions/branch-names@v8
- name: Setup QEMU
uses: docker/setup-qemu-action@v2
- name: Setup Docker Buildx
+2
View File
@@ -58,3 +58,5 @@ AUTHORS
.idea
Makefile.venv
# Copilot
.DS_Store
+3 -3
View File
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.4.0
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@@ -12,11 +12,11 @@ repos:
- id: check-builtin-literals
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v1.16.0
rev: v2.5.0
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/dizballanze/gray
rev: v0.10.1
rev: v0.14.0
hooks:
- id: gray
+59
View File
@@ -1,9 +1,68 @@
CHANGES
=======
v3.3.0
------
* sample-config fix
* Fixed registry url post
* Changed processpkt message
* Fixed RegistryThread not sending requests
* use log.setup\_logging
* Disable debug logs for aprslib
* Make registry thread sleep
* Put threads first after date/time
* Replace slow rich logging with loguru
* Updated requirements
* Fixed pep8
* Added list-extensions and updated README.rst
* Change defaults for beacon and registry
* Add log info for Beacon and Registry threads
* fixed frequency\_seconds to IntOpt
* fixed references to conf
* changed the default packet timeout to 5 minutes
* Fixed default service registry url
* fix pep8 failures
* py311 fails in github
* Don't send uptime to registry
* Added sending software string to registry
* add py310 gh actions
* Added the new APRS Registry thread
* Added installing extensions to Docker run
* Cleanup some logs
* Added BeaconPacket
* updated requirements files
* removed some unneeded code
* Added iterator to objectstore
* Added some missing classes to threads
* Added support for loading extensions
* Added location for callsign tabs in webchat
* updated gitignore
* Create codeql.yml
* update github action branchs to v8
* Added Location info on webchat interface
* Updated dev test-plugin command
* Update requirements.txt
* Update for v3.2.3
v3.2.3
------
* Force fortune path during setup test
* added /usr/games to path
* Added fortune to Dockerfile-dev
* Added missing fortune app
* aprsd: main.py: Fix premature return in sample\_config
* Update weather.py because you can't sort icons by penis
* Update weather.py both weather plugins have new Ww regex
* Update weather.py
* Fixed a bug with OWMWeatherPlugin
* Rework Location Plugin
v3.2.2
------
* Update for v3.2.2 release
* Fix for types
* Fix wsgi for prod
* pep8 fixes
+2 -2
View File
@@ -84,5 +84,5 @@ update-requirements: dev ## Update the requirements.txt and dev-requirements.tx
rm dev-requirements.txt
touch requirements.txt
touch dev-requirements.txt
$(VENV)/pip-compile --resolver backtracking --annotation-style line requirements.in
$(VENV)/pip-compile --resolver backtracking --annotation-style line dev-requirements.in
$(VENV)/pip-compile --resolver backtracking --annotation-style=line requirements.in
$(VENV)/pip-compile --resolver backtracking --annotation-style=line dev-requirements.in
+70 -63
View File
@@ -10,32 +10,38 @@ ____________________
`APRSD <http://github.com/craigerl/aprsd>`_ is a Ham radio `APRS <http://aprs.org>`_ message command gateway built on python.
APRSD listens on amateur radio aprs-is network for messages and respond to them.
It has a plugin architecture for extensibility. Users of APRSD can write their own
plugins that can respond to APRS-IS messages.
You must have an amateur radio callsign to use this software. APRSD gets
messages for the configured HAM callsign, and sends those messages to a
list of plugins for processing. There are a set of core plugins that
provide responding to messages to check email, get location, ping,
time of day, get weather, and fortune telling as well as version information
of aprsd itself.
What is APRSD
=============
APRSD is a python application for interacting with the APRS network and providing
APRS services for HAM radio operators.
APRSD currently has 4 main commands to use.
* server - Connect to APRS and listen/respond to APRS messages
* webchat - web based chat program over APRS
* send-message - Send a message to a callsign via APRS_IS.
* listen - Listen to packets on the APRS-IS Network based on FILTER.
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.
Please `read the docs`_ to learn more!
.. contents:: :local:
APRSD Overview Diagram
======================
.. image:: https://raw.githubusercontent.com/craigerl/aprsd/master/docs/_static/aprsd_overview.svg?sanitize=true
Typical use case
================
APRSD's typical use case is that of providing an APRS wide service to all HAM
radio operators. For example the callsign 'REPEAT' on the APRS network is actually
an instance of APRSD that can provide a list of HAM repeaters in the area of the
callsign that sent the message.
Ham radio operator using an APRS enabled HAM radio sends a message to check
the weather. An APRS message is sent, and then picked up by APRSD. The
APRS packet is decoded, and the message is sent through the list of plugins
@@ -46,55 +52,6 @@ callsigns to look out for. The watch list can notify you when a HAM callsign
in the list is seen and now available to message on the APRS network.
Current list of built-in plugins
======================================
::
└─> aprsd list-plugins
🐍 APRSD Built-in Plugins 🐍
┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Plugin Name ┃ Info ┃ Type ┃ Plugin Path ┃
┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ AVWXWeatherPlugin │ AVWX weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.AVWXWeatherPlugin │
│ EmailPlugin │ Send and Receive email │ RegexCommand │ aprsd.plugins.email.EmailPlugin │
│ FortunePlugin │ Give me a fortune │ RegexCommand │ aprsd.plugins.fortune.FortunePlugin │
│ LocationPlugin │ Where in the world is a CALLSIGN's last GPS beacon? │ RegexCommand │ aprsd.plugins.location.LocationPlugin │
│ NotifySeenPlugin │ Notify me when a CALLSIGN is recently seen on APRS-IS │ WatchList │ aprsd.plugins.notify.NotifySeenPlugin │
│ OWMWeatherPlugin │ OpenWeatherMap weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.OWMWeatherPlugin │
│ PingPlugin │ reply with a Pong! │ RegexCommand │ aprsd.plugins.ping.PingPlugin │
│ QueryPlugin │ APRSD Owner command to query messages in the MsgTrack │ RegexCommand │ aprsd.plugins.query.QueryPlugin │
│ TimeOWMPlugin │ Current time of GPS beacon's timezone. Uses OpenWeatherMap │ RegexCommand │ aprsd.plugins.time.TimeOWMPlugin │
│ TimePlugin │ What is the current local time. │ RegexCommand │ aprsd.plugins.time.TimePlugin │
│ USMetarPlugin │ USA only METAR of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USMetarPlugin │
│ USWeatherPlugin │ Provide USA only weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USWeatherPlugin │
│ VersionPlugin │ What is the APRSD Version │ RegexCommand │ aprsd.plugins.version.VersionPlugin │
└───────────────────┴────────────────────────────────────────────────────────────┴──────────────┴─────────────────────────────────────────┘
Pypi.org APRSD Installable Plugin Packages
Install any of the following plugins with 'pip install <Plugin Package Name>'
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Plugin Package Name ┃ Description ┃ Version ┃ Released ┃ Installed? ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ 📂 aprsd-stock-plugin │ Ham Radio APRSD Plugin for fetching stock quotes │ 0.1.3 │ Dec 2, 2022 │ No │
│ 📂 aprsd-sentry-plugin │ Ham radio APRSD plugin that does.... │ 0.1.2 │ Dec 2, 2022 │ No │
│ 📂 aprsd-timeopencage-plugin │ APRSD plugin for fetching time based on GPS location │ 0.1.0 │ Dec 2, 2022 │ No │
│ 📂 aprsd-weewx-plugin │ HAM Radio APRSD that reports weather from a weewx weather station. │ 0.1.4 │ Dec 7, 2021 │ Yes │
│ 📂 aprsd-repeat-plugins │ APRSD Plugins for the REPEAT service │ 1.0.12 │ Dec 2, 2022 │ No │
│ 📂 aprsd-telegram-plugin │ Ham Radio APRS APRSD plugin for Telegram IM service │ 0.1.3 │ Dec 2, 2022 │ No │
│ 📂 aprsd-twitter-plugin │ Python APRSD plugin to send tweets │ 0.3.0 │ Dec 7, 2021 │ No │
│ 📂 aprsd-slack-plugin │ Amateur radio APRS daemon which listens for messages and responds │ 1.0.5 │ Dec 18, 2022 │ No │
└──────────────────────────────┴────────────────────────────────────────────────────────────────────┴─────────┴──────────────┴────────────┘
🐍 APRSD Installed 3rd party Plugins 🐍
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Package Name ┃ Plugin Name ┃ Version ┃ Type ┃ Plugin Path ┃
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ aprsd-weewx-plugin │ WeewxMQTTPlugin │ 1.0 │ RegexCommand │ aprsd_weewx_plugin.weewx.WeewxMQTTPlugin │
└────────────────────┴─────────────────┴─────────┴──────────────┴──────────────────────────────────────────┘
Installation
=============
@@ -187,6 +144,56 @@ look for incomming commands to the callsign configured in the config file
12/07/2021 03:16:17 PM MainThread INFO aprs.logfile = /tmp/aprsd.log server.py:60
Current list of built-in plugins
======================================
::
└─> aprsd list-plugins
🐍 APRSD Built-in Plugins 🐍
┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Plugin Name ┃ Info ┃ Type ┃ Plugin Path ┃
┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ AVWXWeatherPlugin │ AVWX weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.AVWXWeatherPlugin │
│ EmailPlugin │ Send and Receive email │ RegexCommand │ aprsd.plugins.email.EmailPlugin │
│ FortunePlugin │ Give me a fortune │ RegexCommand │ aprsd.plugins.fortune.FortunePlugin │
│ LocationPlugin │ Where in the world is a CALLSIGN's last GPS beacon? │ RegexCommand │ aprsd.plugins.location.LocationPlugin │
│ NotifySeenPlugin │ Notify me when a CALLSIGN is recently seen on APRS-IS │ WatchList │ aprsd.plugins.notify.NotifySeenPlugin │
│ OWMWeatherPlugin │ OpenWeatherMap weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.OWMWeatherPlugin │
│ PingPlugin │ reply with a Pong! │ RegexCommand │ aprsd.plugins.ping.PingPlugin │
│ QueryPlugin │ APRSD Owner command to query messages in the MsgTrack │ RegexCommand │ aprsd.plugins.query.QueryPlugin │
│ TimeOWMPlugin │ Current time of GPS beacon's timezone. Uses OpenWeatherMap │ RegexCommand │ aprsd.plugins.time.TimeOWMPlugin │
│ TimePlugin │ What is the current local time. │ RegexCommand │ aprsd.plugins.time.TimePlugin │
│ USMetarPlugin │ USA only METAR of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USMetarPlugin │
│ USWeatherPlugin │ Provide USA only weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USWeatherPlugin │
│ VersionPlugin │ What is the APRSD Version │ RegexCommand │ aprsd.plugins.version.VersionPlugin │
└───────────────────┴────────────────────────────────────────────────────────────┴──────────────┴─────────────────────────────────────────┘
Pypi.org APRSD Installable Plugin Packages
Install any of the following plugins with 'pip install <Plugin Package Name>'
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
┃ Plugin Package Name ┃ Description ┃ Version ┃ Released ┃ Installed? ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
│ 📂 aprsd-stock-plugin │ Ham Radio APRSD Plugin for fetching stock quotes │ 0.1.3 │ Dec 2, 2022 │ No │
│ 📂 aprsd-sentry-plugin │ Ham radio APRSD plugin that does.... │ 0.1.2 │ Dec 2, 2022 │ No │
│ 📂 aprsd-timeopencage-plugin │ APRSD plugin for fetching time based on GPS location │ 0.1.0 │ Dec 2, 2022 │ No │
│ 📂 aprsd-weewx-plugin │ HAM Radio APRSD that reports weather from a weewx weather station. │ 0.1.4 │ Dec 7, 2021 │ Yes │
│ 📂 aprsd-repeat-plugins │ APRSD Plugins for the REPEAT service │ 1.0.12 │ Dec 2, 2022 │ No │
│ 📂 aprsd-telegram-plugin │ Ham Radio APRS APRSD plugin for Telegram IM service │ 0.1.3 │ Dec 2, 2022 │ No │
│ 📂 aprsd-twitter-plugin │ Python APRSD plugin to send tweets │ 0.3.0 │ Dec 7, 2021 │ No │
│ 📂 aprsd-slack-plugin │ Amateur radio APRS daemon which listens for messages and responds │ 1.0.5 │ Dec 18, 2022 │ No │
└──────────────────────────────┴────────────────────────────────────────────────────────────────────┴─────────┴──────────────┴────────────┘
🐍 APRSD Installed 3rd party Plugins 🐍
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Package Name ┃ Plugin Name ┃ Version ┃ Type ┃ Plugin Path ┃
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ aprsd-weewx-plugin │ WeewxMQTTPlugin │ 1.0 │ RegexCommand │ aprsd_weewx_plugin.weewx.WeewxMQTTPlugin │
└────────────────────┴─────────────────┴─────────┴──────────────┴──────────────────────────────────────────┘
send-message
+35 -2
View File
@@ -1,9 +1,8 @@
import click
from functools import update_wrapper
import logging
from pathlib import Path
import typing as t
import click
from oslo_config import cfg
import aprsd
@@ -50,6 +49,40 @@ common_options = [
]
class AliasedGroup(click.Group):
def command(self, *args, **kwargs):
"""A shortcut decorator for declaring and attaching a command to
the group. This takes the same arguments as :func:`command` but
immediately registers the created command with this instance by
calling into :meth:`add_command`.
Copied from `click` and extended for `aliases`.
"""
def decorator(f):
aliases = kwargs.pop('aliases', [])
cmd = click.decorators.command(*args, **kwargs)(f)
self.add_command(cmd)
for alias in aliases:
self.add_command(cmd, name=alias)
return cmd
return decorator
def group(self, *args, **kwargs):
"""A shortcut decorator for declaring and attaching a group to
the group. This takes the same arguments as :func:`group` but
immediately registers the created command with this instance by
calling into :meth:`add_command`.
Copied from `click` and extended for `aliases`.
"""
def decorator(f):
aliases = kwargs.pop('aliases', [])
cmd = click.decorators.group(*args, **kwargs)(f)
self.add_command(cmd)
for alias in aliases:
self.add_command(cmd, name=alias)
return cmd
return decorator
def add_options(options):
def _add_options(func):
for option in reversed(options):
+3 -3
View File
@@ -25,7 +25,7 @@ TRANSPORT_FAKE = "fake"
factory = None
class Client(metaclass=trace.TraceWrapperMetaclass):
class Client:
"""Singleton client class that constructs the aprslib connection."""
_instance = None
@@ -90,7 +90,7 @@ class Client(metaclass=trace.TraceWrapperMetaclass):
pass
class APRSISClient(Client, metaclass=trace.TraceWrapperMetaclass):
class APRSISClient(Client):
_client = None
@@ -175,7 +175,7 @@ class APRSISClient(Client, metaclass=trace.TraceWrapperMetaclass):
return aprs_client
class KISSClient(Client, metaclass=trace.TraceWrapperMetaclass):
class KISSClient(Client):
_client = None
-1
View File
@@ -112,7 +112,6 @@ class Aprsdis(aprslib.IS):
self._sendall(login_str)
self.sock.settimeout(5)
test = self.sock.recv(len(login_str) + 100)
self.logger.debug("Server: '%s'", test)
if is_py3:
test = test.decode("latin-1")
test = test.rstrip()
+31 -2
View File
@@ -125,8 +125,37 @@ def test_plugin(
LOG.info(f"P'{plugin_path}' F'{fromcall}' C'{message}'")
for x in range(number):
reply = pm.run(packet)
replies = pm.run(packet)
# Plugin might have threads, so lets stop them so we can exit.
# obj.stop_threads()
LOG.info(f"Result{x} = '{reply}'")
for reply in replies:
if isinstance(reply, list):
# one of the plugins wants to send multiple messages
for subreply in reply:
if isinstance(subreply, packets.Packet):
LOG.info(subreply)
else:
LOG.info(
packets.MessagePacket(
from_call=CONF.callsign,
to_call=fromcall,
message_text=subreply,
),
)
elif isinstance(reply, packets.Packet):
# We have a message based object.
LOG.info(reply)
else:
# A plugin can return a null message flag which signals
# us that they processed the message correctly, but have
# nothing to reply with, so we avoid replying with a
# usage string
if reply is not packets.NULL_MESSAGE:
LOG.info(
packets.MessagePacket(
from_call=CONF.callsign,
to_call=fromcall,
message_text=reply,
),
)
pm.stop()
+87 -10
View File
@@ -26,6 +26,7 @@ from aprsd.plugins import (
LOG = logging.getLogger("APRSD")
PYPI_URL = "https://pypi.org/search/"
def onerror(name):
@@ -89,18 +90,35 @@ def get_module_info(package_name, module_name, module_path):
return obj_list
def get_installed_plugins():
def _get_installed_aprsd_items():
# installed plugins
ip = {}
plugins = {}
extensions = {}
for finder, name, ispkg in pkgutil.iter_modules():
if name.startswith("aprsd_"):
print(f"Found aprsd_ module: {name}")
if ispkg:
module = importlib.import_module(name)
pkgs = walk_package(module)
for pkg in pkgs:
pkg_info = get_module_info(module.__name__, pkg.name, module.__path__[0])
ip[name] = pkg_info
return ip
if "plugin" in name:
plugins[name] = pkg_info
elif "extension" in name:
extensions[name] = pkg_info
return plugins, extensions
def get_installed_plugins():
# installed plugins
plugins, extensions = _get_installed_aprsd_items()
return plugins
def get_installed_extensions():
# installed plugins
plugins, extensions = _get_installed_aprsd_items()
return extensions
def show_built_in_plugins(console):
@@ -144,22 +162,27 @@ def show_built_in_plugins(console):
console.print(table)
def show_pypi_plugins(installed_plugins, console):
def _get_pypi_packages():
query = "aprsd"
api_url = "https://pypi.org/search/"
snippets = []
s = requests.Session()
for page in range(1, 3):
params = {"q": query, "page": page}
r = s.get(api_url, params=params)
r = s.get(PYPI_URL, params=params)
soup = BeautifulSoup(r.text, "html.parser")
snippets += soup.select('a[class*="snippet"]')
if not hasattr(s, "start_url"):
s.start_url = r.url.rsplit("&page", maxsplit=1).pop(0)
return snippets
def show_pypi_plugins(installed_plugins, console):
snippets = _get_pypi_packages()
title = Text.assemble(
("Pypi.org APRSD Installable Plugin Packages\n\n", "bold magenta"),
("Install any of the following plugins with ", "bold yellow"),
("Install any of the following plugins with\n", "bold yellow"),
("'pip install ", "bold white"),
("<Plugin Package Name>'", "cyan"),
)
@@ -171,7 +194,7 @@ def show_pypi_plugins(installed_plugins, console):
table.add_column("Released", style="bold green", justify="center")
table.add_column("Installed?", style="red", justify="center")
for snippet in snippets:
link = urljoin(api_url, snippet.get("href"))
link = urljoin(PYPI_URL, snippet.get("href"))
package = re.sub(r"\s+", " ", snippet.select_one('span[class*="name"]').text.strip())
version = re.sub(r"\s+", " ", snippet.select_one('span[class*="version"]').text.strip())
created = re.sub(r"\s+", " ", snippet.select_one('span[class*="created"]').text.strip())
@@ -194,7 +217,47 @@ def show_pypi_plugins(installed_plugins, console):
console.print("\n")
console.print(table)
return
def show_pypi_extensions(installed_extensions, console):
snippets = _get_pypi_packages()
title = Text.assemble(
("Pypi.org APRSD Installable Extension Packages\n\n", "bold magenta"),
("Install any of the following extensions by running\n", "bold yellow"),
("'pip install ", "bold white"),
("<Plugin Package Name>'", "cyan"),
)
table = Table(title=title)
table.add_column("Extension Package Name", style="cyan", no_wrap=True)
table.add_column("Description", style="yellow")
table.add_column("Version", style="yellow", justify="center")
table.add_column("Released", style="bold green", justify="center")
table.add_column("Installed?", style="red", justify="center")
for snippet in snippets:
link = urljoin(PYPI_URL, snippet.get("href"))
package = re.sub(r"\s+", " ", snippet.select_one('span[class*="name"]').text.strip())
version = re.sub(r"\s+", " ", snippet.select_one('span[class*="version"]').text.strip())
created = re.sub(r"\s+", " ", snippet.select_one('span[class*="created"]').text.strip())
description = re.sub(r"\s+", " ", snippet.select_one('p[class*="description"]').text.strip())
emoji = ":open_file_folder:"
if "aprsd-" not in package or "-extension" not in package:
continue
under = package.replace("-", "_")
if under in installed_extensions:
installed = "Yes"
else:
installed = "No"
table.add_row(
f"[link={link}]{emoji}[/link] {package}",
description, version, created, installed,
)
console.print("\n")
console.print(table)
def show_installed_plugins(installed_plugins, console):
@@ -240,3 +303,17 @@ def list_plugins(ctx):
status.update("Looking for installed APRSD plugins")
show_installed_plugins(installed_plugins, console)
@cli.command()
@cli_helper.add_options(cli_helper.common_options)
@click.pass_context
@cli_helper.process_standard_options_no_config
def list_extensions(ctx):
"""List the built in plugins available to APRSD."""
console = Console()
with console.status("Show APRSD Extensions") as status:
status.update("Fetching pypi.org APRSD Extensions")
installed_extensions = get_installed_extensions()
show_pypi_extensions(installed_extensions, console)
+10 -1
View File
@@ -11,7 +11,7 @@ from aprsd import main as aprsd_main
from aprsd import packets, plugin, threads, utils
from aprsd.main import cli
from aprsd.rpc import server as rpc_server
from aprsd.threads import rx
from aprsd.threads import registry, rx, tx
CONF = cfg.CONF
@@ -107,6 +107,15 @@ def server(ctx, flush):
process_thread.start()
packets.PacketTrack().restart()
if CONF.enable_beacon:
LOG.info("Beacon Enabled. Starting Beacon thread.")
bcn_thread = tx.BeaconSendThread()
bcn_thread.start()
if CONF.aprs_registry.enabled:
LOG.info("Registry Enabled. Starting Registry thread.")
registry_thread = registry.APRSRegistryThread()
registry_thread.start()
if CONF.rpc_settings.enabled:
rpc = rpc_server.APRSDRPCThread()
+245 -45
View File
@@ -1,7 +1,7 @@
import datetime
import json
import logging
from logging.handlers import RotatingFileHandler
import math
import signal
import sys
import threading
@@ -11,17 +11,20 @@ from aprslib import util as aprslib_util
import click
import flask
from flask import request
from flask.logging import default_handler
from flask_httpauth import HTTPBasicAuth
from flask_socketio import Namespace, SocketIO
from geopy.distance import geodesic
from oslo_config import cfg
from werkzeug.security import check_password_hash, generate_password_hash
import wrapt
import aprsd
from aprsd import cli_helper, client, conf, packets, stats, threads, utils
from aprsd.log import rich as aprsd_logging
from aprsd import (
cli_helper, client, packets, plugin_utils, stats, threads, utils,
)
from aprsd.log import log
from aprsd.main import cli
from aprsd.threads import aprsd as aprsd_threads
from aprsd.threads import rx, tx
from aprsd.utils import trace
@@ -32,6 +35,16 @@ auth = HTTPBasicAuth()
users = {}
socketio = None
# List of callsigns that we don't want to track/fetch their location
callsign_no_track = [
"REPEAT", "WB4BOR-11", "APDW16", "WXNOW", "WXBOT", "BLN0", "BLN1", "BLN2",
"BLN3", "BLN4", "BLN5", "BLN6", "BLN7", "BLN8", "BLN9",
]
# Callsign location information
# callsign: {lat: 0.0, long: 0.0, last_update: datetime}
callsign_locations = {}
flask_app = flask.Flask(
"aprsd",
static_url_path="/static",
@@ -121,8 +134,188 @@ def verify_password(username, password):
return username
def calculate_initial_compass_bearing(point_a, point_b):
"""
Calculates the bearing between two points.
The formulae used is the following:
θ = atan2(sin(Δlong).cos(lat2),
cos(lat1).sin(lat2) sin(lat1).cos(lat2).cos(Δlong))
:Parameters:
- `pointA: The tuple representing the latitude/longitude for the
first point. Latitude and longitude must be in decimal degrees
- `pointB: The tuple representing the latitude/longitude for the
second point. Latitude and longitude must be in decimal degrees
:Returns:
The bearing in degrees
:Returns Type:
float
"""
if (type(point_a) is not tuple) or (type(point_b) is not tuple):
raise TypeError("Only tuples are supported as arguments")
lat1 = math.radians(point_a[0])
lat2 = math.radians(point_b[0])
diff_long = math.radians(point_b[1] - point_a[1])
x = math.sin(diff_long) * math.cos(lat2)
y = math.cos(lat1) * math.sin(lat2) - (
math.sin(lat1)
* math.cos(lat2) * math.cos(diff_long)
)
initial_bearing = math.atan2(x, y)
# Now we have the initial bearing but math.atan2 return values
# from -180° to + 180° which is not what we want for a compass bearing
# The solution is to normalize the initial bearing as shown below
initial_bearing = math.degrees(initial_bearing)
compass_bearing = (initial_bearing + 360) % 360
return compass_bearing
def _build_location_from_repeat(message):
# This is a location message Format is
# ^ld^callsign:latitude,longitude,altitude,course,speed,timestamp
a = message.split(":")
LOG.warning(a)
if len(a) == 2:
callsign = a[0].replace("^ld^", "")
b = a[1].split(",")
LOG.warning(b)
if len(b) == 6:
lat = float(b[0])
lon = float(b[1])
alt = float(b[2])
course = float(b[3])
speed = float(b[4])
time = int(b[5])
data = {
"callsign": callsign,
"lat": lat,
"lon": lon,
"altitude": alt,
"course": course,
"speed": speed,
"lasttime": time,
}
LOG.warning(f"Location data from REPEAT {data}")
return data
def _calculate_location_data(location_data):
"""Calculate all of the location data from data from aprs.fi or REPEAT."""
lat = location_data["lat"]
lon = location_data["lon"]
alt = location_data["altitude"]
speed = location_data["speed"]
lasttime = location_data["lasttime"]
# now calculate distance from our own location
distance = 0
if CONF.webchat.latitude and CONF.webchat.longitude:
our_lat = float(CONF.webchat.latitude)
our_lon = float(CONF.webchat.longitude)
distance = geodesic((our_lat, our_lon), (lat, lon)).kilometers
bearing = calculate_initial_compass_bearing(
(our_lat, our_lon),
(lat, lon),
)
return {
"callsign": location_data["callsign"],
"lat": lat,
"lon": lon,
"altitude": alt,
"course": f"{bearing:0.1f}",
"speed": speed,
"lasttime": lasttime,
"distance": f"{distance:0.3f}",
}
def send_location_data_to_browser(location_data):
global socketio
callsign = location_data["callsign"]
LOG.info(f"Got location for {callsign} {callsign_locations[callsign]}")
socketio.emit(
"callsign_location", callsign_locations[callsign],
namespace="/sendmsg",
)
def populate_callsign_location(callsign, data=None):
"""Populate the location for the callsign.
if data is passed in, then we have the location already from
an APRS packet. If data is None, then we need to fetch the
location from aprs.fi or REPEAT.
"""
global socketio
"""Fetch the location for the callsign."""
LOG.debug(f"populate_callsign_location {callsign}")
if data:
location_data = _calculate_location_data(data)
callsign_locations[callsign] = location_data
send_location_data_to_browser(location_data)
return
# First we are going to try to get the location from aprs.fi
# if there is no internets, then this will fail and we will
# fallback to calling REPEAT for the location for the callsign.
fallback = False
if not CONF.aprs_fi.apiKey:
LOG.warning(
"Config aprs_fi.apiKey is not set. Can't get location from aprs.fi "
" falling back to sending REPEAT to get location.",
)
fallback = True
else:
try:
aprs_data = plugin_utils.get_aprs_fi(CONF.aprs_fi.apiKey, callsign)
if not len(aprs_data["entries"]):
LOG.error("Didn't get any entries from aprs.fi")
return
lat = float(aprs_data["entries"][0]["lat"])
lon = float(aprs_data["entries"][0]["lng"])
try: # altitude not always provided
alt = float(aprs_data["entries"][0]["altitude"])
except Exception:
alt = 0
location_data = {
"callsign": callsign,
"lat": lat,
"lon": lon,
"altitude": alt,
"lasttime": int(aprs_data["entries"][0]["lasttime"]),
"course": float(aprs_data["entries"][0].get("course", 0)),
"speed": float(aprs_data["entries"][0].get("speed", 0)),
}
location_data = _calculate_location_data(location_data)
callsign_locations[callsign] = location_data
send_location_data_to_browser(location_data)
return
except Exception as ex:
LOG.error(f"Failed to fetch aprs.fi '{ex}'")
LOG.error(ex)
fallback = True
if fallback:
# We don't have the location data
# and we can't get it from aprs.fi
# Send a special message to REPEAT to get the location data
LOG.info(f"Sending REPEAT to get location for callsign {callsign}.")
tx.send(
packets.MessagePacket(
from_call=CONF.callsign,
to_call="REPEAT",
message_text=f"ld {callsign}",
),
)
class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
"""Class that handles packets being sent to us."""
def __init__(self, packet_queue, socketio):
self.socketio = socketio
self.connected = False
@@ -132,20 +325,53 @@ class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
super().process_ack_packet(packet)
ack_num = packet.get("msgNo")
SentMessages().ack(ack_num)
self.socketio.emit(
"ack", SentMessages().get(ack_num),
namespace="/sendmsg",
)
msg = SentMessages().get(ack_num)
if msg:
self.socketio.emit(
"ack", msg,
namespace="/sendmsg",
)
self.got_ack = True
def process_our_message_packet(self, packet: packets.MessagePacket):
global callsign_locations
LOG.info(f"process MessagePacket {repr(packet)}")
# ok lets see if we have the location for the
# person we just sent a message to.
from_call = packet.get("from_call").upper()
if from_call == "REPEAT":
# We got a message from REPEAT. Is this a location message?
message = packet.get("message_text")
if message.startswith("^ld^"):
location_data = _build_location_from_repeat(message)
callsign = location_data["callsign"]
location_data = _calculate_location_data(location_data)
callsign_locations[callsign] = location_data
send_location_data_to_browser(location_data)
return
elif (
from_call not in callsign_locations
and from_call not in callsign_no_track
):
# We have to ask aprs for the location for the callsign
# We send a message packet to wb4bor-11 asking for location.
populate_callsign_location(from_call)
# Send the packet to the browser.
self.socketio.emit(
"new", packet.__dict__,
namespace="/sendmsg",
)
class LocationProcessingThread(aprsd_threads.APRSDThread):
"""Class to handle the location processing."""
def __init__(self):
super().__init__("LocationProcessingThread")
def loop(self):
pass
def set_config():
global users
@@ -181,6 +407,12 @@ def _get_transport(stats):
return transport, aprs_connection
@flask_app.route("/location/<callsign>", methods=["POST"])
def location(callsign):
LOG.debug(f"Fetch location for callsign {callsign}")
populate_callsign_location(callsign)
@auth.login_required
@flask_app.route("/")
def index():
@@ -216,7 +448,7 @@ def index():
@auth.login_required
@flask_app.route("//send-message-status")
@flask_app.route("/send-message-status")
def send_message_status():
LOG.debug(request)
msgs = SentMessages()
@@ -331,53 +563,21 @@ class SendMessageNamespace(Namespace):
def handle_json(self, data):
LOG.debug(f"WS json {data}")
def setup_logging(flask_app, loglevel, quiet):
flask_log = logging.getLogger("werkzeug")
flask_app.logger.removeHandler(default_handler)
flask_log.removeHandler(default_handler)
log_level = conf.log.LOG_LEVELS[loglevel]
flask_log.setLevel(log_level)
date_format = CONF.logging.date_format
if CONF.logging.rich_logging and not quiet:
log_format = "%(message)s"
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
rh = aprsd_logging.APRSDRichHandler(
show_thread=True, thread_width=15,
rich_tracebacks=True, omit_repeated_times=False,
)
rh.setFormatter(log_formatter)
flask_log.addHandler(rh)
log_file = CONF.logging.logfile
if log_file:
log_format = CONF.logging.logformat
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
fh = RotatingFileHandler(
log_file, maxBytes=(10248576 * 5),
backupCount=4,
)
fh.setFormatter(log_formatter)
flask_log.addHandler(fh)
def on_get_callsign_location(self, data):
LOG.debug(f"on_callsign_location {data}")
populate_callsign_location(data["callsign"])
@trace.trace
def init_flask(loglevel, quiet):
global socketio, flask_app
setup_logging(flask_app, loglevel, quiet)
log.setup_logging(loglevel, quiet)
socketio = SocketIO(
flask_app, logger=False, engineio_logger=False,
async_mode="threading",
)
# async_mode="gevent",
# async_mode="eventlet",
# import eventlet
# eventlet.monkey_patch()
socketio.on_namespace(
SendMessageNamespace(
+68 -1
View File
@@ -24,6 +24,11 @@ webchat_group = cfg.OptGroup(
title="Settings specific to the webchat command",
)
registry_group = cfg.OptGroup(
name="aprs_registry",
title="APRS Registry settings",
)
aprsd_opts = [
cfg.StrOpt(
@@ -67,9 +72,35 @@ aprsd_opts = [
),
cfg.IntOpt(
"packet_dupe_timeout",
default=60,
default=300,
help="The number of seconds before a packet is not considered a duplicate.",
),
cfg.BoolOpt(
"enable_beacon",
default=False,
help="Enable sending of a GPS Beacon packet to locate this service. "
"Requires latitude and longitude to be set.",
),
cfg.IntOpt(
"beacon_interval",
default=1800,
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",
),
cfg.StrOpt(
"latitude",
default=None,
help="Latitude for the GPS Beacon button. If not set, the button will not be enabled.",
),
cfg.StrOpt(
"longitude",
default=None,
help="Longitude for the GPS Beacon button. If not set, the button will not be enabled.",
),
]
watch_list_opts = [
@@ -196,6 +227,39 @@ webchat_opts = [
),
]
registry_opts = [
cfg.StrOpt(
"enabled",
default=False,
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.",
),
cfg.StrOpt(
"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.",
),
cfg.StrOpt(
"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",
default=None,
help="The website for your APRS service to send to the APRS registry.",
),
cfg.IntOpt(
"frequency_seconds",
default=3600,
help="The frequency in seconds to send the APRS registry information.",
),
]
def register_opts(config):
config.register_opts(aprsd_opts)
@@ -208,6 +272,8 @@ def register_opts(config):
config.register_opts(rpc_opts, group=rpc_group)
config.register_group(webchat_group)
config.register_opts(webchat_opts, group=webchat_group)
config.register_group(registry_group)
config.register_opts(registry_opts, group=registry_group)
def list_opts():
@@ -217,4 +283,5 @@ def list_opts():
watch_list_group.name: watch_list_opts,
rpc_group.name: rpc_opts,
webchat_group.name: webchat_opts,
registry_group.name: registry_opts,
}
+8 -10
View File
@@ -20,21 +20,19 @@ DEFAULT_LOG_FORMAT = (
" %(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>"
)
logging_group = cfg.OptGroup(
name="logging",
title="Logging options",
)
logging_opts = [
cfg.StrOpt(
"date_format",
default=DEFAULT_DATE_FORMAT,
help="Date format for log entries",
),
cfg.BoolOpt(
"rich_logging",
default=True,
help="Enable Rich log",
),
cfg.StrOpt(
"logfile",
default=None,
+72 -46
View File
@@ -1,13 +1,13 @@
import logging
from logging import NullHandler
from logging.handlers import RotatingFileHandler
from logging.handlers import QueueHandler
import queue
import sys
from loguru import logger
from oslo_config import cfg
from aprsd import conf
from aprsd.log import rich as aprsd_logging
CONF = cfg.CONF
@@ -15,61 +15,87 @@ LOG = logging.getLogger("APRSD")
logging_queue = queue.Queue()
class InterceptHandler(logging.Handler):
def emit(self, record):
# get corresponding Loguru level if it exists
try:
level = logger.level(record.levelname).name
except ValueError:
level = record.levelno
# find caller from where originated the logged message
frame, depth = sys._getframe(6), 6
while frame and frame.f_code.co_filename == logging.__file__:
frame = frame.f_back
depth += 1
logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
# 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, quiet):
log_level = conf.log.LOG_LEVELS[loglevel]
LOG.setLevel(log_level)
date_format = CONF.logging.date_format
rh = None
fh = None
def setup_logging(loglevel=None, quiet=False):
if not loglevel:
log_level = CONF.logging.log_level
else:
log_level = conf.log.LOG_LEVELS[loglevel]
rich_logging = False
if CONF.logging.get("rich_logging", False) and not quiet:
log_format = "%(message)s"
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
rh = aprsd_logging.APRSDRichHandler(
show_thread=True, thread_width=20,
rich_tracebacks=True, omit_repeated_times=False,
# intercept everything at the root logger
logging.root.handlers = [InterceptHandler()]
logging.root.setLevel(log_level)
imap_list = [
"imapclient.imaplib", "imaplib", "imapclient",
"imapclient.util",
]
aprslib_list = [
"aprslib",
"aprslib.parsing",
"aprslib.exceptions",
]
# We don't really want to see the aprslib parsing debug output.
disable_list = imap_list + aprslib_list
# remove every other logger's handlers
# and propagate to root logger
for name in logging.root.manager.loggerDict.keys():
logging.getLogger(name).handlers = []
if name in disable_list:
logging.getLogger(name).propagate = False
else:
logging.getLogger(name).propagate = True
handlers = [
{
"sink": sys.stdout, "serialize": False,
"format": CONF.logging.logformat,
},
]
if CONF.logging.logfile:
handlers.append(
{
"sink": CONF.logging.logfile, "serialize": False,
"format": CONF.logging.logformat,
},
)
rh.setFormatter(log_formatter)
LOG.addHandler(rh)
rich_logging = True
log_file = CONF.logging.logfile
log_format = CONF.logging.logformat
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
if log_file:
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
fh.setFormatter(log_formatter)
LOG.addHandler(fh)
imap_logger = None
if CONF.email_plugin.enabled and CONF.email_plugin.debug:
imap_logger = logging.getLogger("imapclient.imaplib")
imap_logger.setLevel(log_level)
if rh:
imap_logger.addHandler(rh)
if fh:
imap_logger.addHandler(fh)
for name in imap_list:
logging.getLogger(name).propagate = True
if CONF.admin.web_enabled:
qh = logging.handlers.QueueHandler(logging_queue)
q_log_formatter = logging.Formatter(
fmt=CONF.logging.logformat,
datefmt=CONF.logging.date_format,
qh = QueueHandler(logging_queue)
handlers.append(
{
"sink": qh, "serialize": False,
"format": CONF.logging.logformat,
},
)
qh.setFormatter(q_log_formatter)
LOG.addHandler(qh)
if not quiet and not rich_logging:
sh = logging.StreamHandler(sys.stdout)
sh.setFormatter(log_formatter)
LOG.addHandler(sh)
if imap_logger:
imap_logger.addHandler(sh)
# configure loguru
logger.configure(handlers=handlers)
def setup_logging_no_config(loglevel, quiet):
-160
View File
@@ -1,160 +0,0 @@
from datetime import datetime
from logging import LogRecord
from pathlib import Path
from typing import TYPE_CHECKING, Callable, Iterable, List, Optional, Union
from rich._log_render import LogRender
from rich.logging import RichHandler
from rich.text import Text, TextType
from rich.traceback import Traceback
if TYPE_CHECKING:
from rich.console import Console, ConsoleRenderable, RenderableType
from rich.table import Table
from aprsd import utils
FormatTimeCallable = Callable[[datetime], Text]
class APRSDRichLogRender(LogRender):
def __init__(
self, *args,
show_thread: bool = False,
thread_width: Optional[int] = 10,
**kwargs,
):
super().__init__(*args, **kwargs)
self.show_thread = show_thread
self.thread_width = thread_width
def __call__(
self,
console: "Console",
renderables: Iterable["ConsoleRenderable"],
log_time: Optional[datetime] = None,
time_format: Optional[Union[str, FormatTimeCallable]] = None,
level: TextType = "",
path: Optional[str] = None,
line_no: Optional[int] = None,
link_path: Optional[str] = None,
thread_name: Optional[str] = None,
) -> "Table":
from rich.containers import Renderables
from rich.table import Table
output = Table.grid(padding=(0, 1))
output.expand = True
if self.show_time:
output.add_column(style="log.time")
if self.show_thread:
rgb = str(utils.rgb_from_name(thread_name)).replace(" ", "")
output.add_column(style=f"rgb{rgb}", width=self.thread_width)
if self.show_level:
output.add_column(style="log.level", width=self.level_width)
output.add_column(ratio=1, style="log.message", overflow="fold")
if self.show_path and path:
output.add_column(style="log.path")
row: List["RenderableType"] = []
if self.show_time:
log_time = log_time or console.get_datetime()
time_format = time_format or self.time_format
if callable(time_format):
log_time_display = time_format(log_time)
else:
log_time_display = Text(log_time.strftime(time_format))
if log_time_display == self._last_time and self.omit_repeated_times:
row.append(Text(" " * len(log_time_display)))
else:
row.append(log_time_display)
self._last_time = log_time_display
if self.show_thread:
row.append(thread_name)
if self.show_level:
row.append(level)
row.append(Renderables(renderables))
if self.show_path and path:
path_text = Text()
path_text.append(
path, style=f"link file://{link_path}" if link_path else "",
)
if line_no:
path_text.append(":")
path_text.append(
f"{line_no}",
style=f"link file://{link_path}#{line_no}" if link_path else "",
)
row.append(path_text)
output.add_row(*row)
return output
class APRSDRichHandler(RichHandler):
"""APRSD's extension of rich's RichHandler to show threads.
show_thread (bool, optional): Show the name of the thread in log entry. Defaults to False.
thread_width (int, optional): The number of characters to show for thread name. Defaults to 10.
"""
def __init__(
self, *args,
show_thread: bool = True,
thread_width: Optional[int] = 10,
**kwargs,
):
super().__init__(*args, **kwargs)
self.show_thread = show_thread
self.thread_width = thread_width
kwargs["show_thread"] = show_thread
kwargs["thread_width"] = thread_width
self._log_render = APRSDRichLogRender(
show_time=True,
show_level=True,
show_path=True,
omit_repeated_times=False,
level_width=None,
show_thread=show_thread,
thread_width=thread_width,
)
def render(
self, *, record: LogRecord,
traceback: Optional[Traceback],
message_renderable: "ConsoleRenderable",
) -> "ConsoleRenderable":
"""Render log for display.
Args:
record (LogRecord): log Record.
traceback (Optional[Traceback]): Traceback instance or None for no Traceback.
message_renderable (ConsoleRenderable): Renderable (typically Text) containing log message contents.
Returns:
ConsoleRenderable: Renderable to display log.
"""
path = Path(record.pathname).name
level = self.get_level_text(record)
time_format = None if self.formatter is None else self.formatter.datefmt
log_time = datetime.fromtimestamp(record.created)
thread_name = record.threadName
log_renderable = self._log_render(
self.console,
[message_renderable] if not traceback else [
message_renderable,
traceback,
],
log_time=log_time,
time_format=time_format,
level=level,
path=path,
line_no=record.lineno,
link_path=record.pathname if self.enable_link_path else None,
thread_name=thread_name,
)
return log_renderable
+10 -10
View File
@@ -59,20 +59,25 @@ click_completion.core.startswith = custom_startswith
click_completion.init()
@click.group(context_settings=CONTEXT_SETTINGS)
@click.group(cls=cli_helper.AliasedGroup, context_settings=CONTEXT_SETTINGS)
@click.version_option()
@click.pass_context
def cli(ctx):
pass
def main():
# First import all the possible commands for the CLI
# The commands themselves live in the cmds directory
def load_commands():
from .cmds import ( # noqa
completion, dev, fetch_stats, healthcheck, list_plugins, listen,
send_message, server, webchat,
)
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")
@@ -120,12 +125,7 @@ def sample_config(ctx):
def get_namespaces():
args = []
all = imp.entry_points()
selected = []
if "oslo.config.opts" in all:
for x in all["oslo.config.opts"]:
if x.group == "oslo.config.opts":
selected.append(x)
selected = imp.entry_points(group="oslo.config.opts")
for entry in selected:
if "aprsd" in entry.name:
args.append("--namespace")
+2 -2
View File
@@ -1,6 +1,6 @@
from aprsd.packets.core import ( # noqa: F401
AckPacket, GPSPacket, MessagePacket, MicEPacket, Packet, RejectPacket,
StatusPacket, WeatherPacket,
AckPacket, BeaconPacket, GPSPacket, MessagePacket, MicEPacket, Packet,
RejectPacket, StatusPacket, WeatherPacket,
)
from aprsd.packets.packet_list import PacketList # noqa: F401
from aprsd.packets.seen_list import SeenList # noqa: F401
+20 -1
View File
@@ -148,7 +148,6 @@ class Packet(metaclass=abc.ABCMeta):
self.from_call,
self.payload,
)
LOG.debug(f"_build_raw: payload '{self.payload}' raw '{self.raw}'")
@staticmethod
def factory(raw_packet):
@@ -466,6 +465,26 @@ class GPSPacket(Packet):
)
@dataclass(unsafe_hash=True)
class BeaconPacket(GPSPacket):
def _build_payload(self):
"""The payload is the non headers portion of the packet."""
time_zulu = self._build_time_zulu()
lat = self.convert_latitude(self.latitude)
long = self.convert_longitude(self.longitude)
self.payload = (
f"@{time_zulu}z{lat}{self.symbol_table}"
f"{long}{self.symbol}APRSD Beacon"
)
def _build_raw(self):
self.raw = (
f"{self.from_call}>APZ100:"
f"{self.payload}"
)
@dataclass
class MicEPacket(GPSPacket):
messagecapable: bool = False
+1 -1
View File
@@ -4,7 +4,7 @@ import queue
# aprsd.threads
from .aprsd import APRSDThread, APRSDThreadList # noqa: F401
from .keep_alive import KeepAliveThread # noqa: F401
from .rx import APRSDRXThread # noqa: F401
from .rx import APRSDRXThread, APRSDDupeRXThread, APRSDProcessPacketThread # noqa: F401
packet_queue = queue.Queue(maxsize=20)
+56
View File
@@ -0,0 +1,56 @@
import logging
import time
from oslo_config import cfg
import requests
import aprsd
from aprsd import threads as aprsd_threads
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
class APRSRegistryThread(aprsd_threads.APRSDThread):
"""This sends service information to the configured APRS Registry."""
_loop_cnt: int = 1
def __init__(self):
super().__init__("APRSRegistryThread")
self._loop_cnt = 1
if not CONF.aprs_registry.enabled:
LOG.error(
"APRS Registry is not enabled. ",
)
LOG.error(
"APRS Registry thread is STOPPING.",
)
self.stop()
LOG.info(
"APRS Registry thread is running and will send "
f"info every {CONF.aprs_registry.frequency_seconds} seconds "
f"to {CONF.aprs_registry.registry_url}.",
)
def loop(self):
# Only call the registry every N seconds
if self._loop_cnt % CONF.aprs_registry.frequency_seconds == 0:
info = {
"callsign": CONF.callsign,
"description": CONF.aprs_registry.description,
"service_website": CONF.aprs_registry.service_website,
"software": f"APRSD version {aprsd.__version__} "
"https://github.com/craigerl/aprsd",
}
try:
requests.post(
f"{CONF.aprs_registry.registry_url}",
json=info,
)
except Exception as e:
LOG.error(f"Failed to send registry info: {e}")
time.sleep(1)
self._loop_cnt += 1
return True
+13 -5
View File
@@ -58,12 +58,13 @@ class APRSDRXThread(APRSDThread):
pass
class APRSDPluginRXThread(APRSDRXThread):
class APRSDDupeRXThread(APRSDRXThread):
"""Process received packets.
This is the main APRSD Server command thread that
receives packets from APRIS and then sends them for
processing in the PluginProcessPacketThread.
receives packets and makes sure the packet
hasn't been seen previously before sending it on
to be processed.
"""
def process_packet(self, *args, **kwargs):
@@ -118,6 +119,13 @@ class APRSDPluginRXThread(APRSDRXThread):
self.packet_queue.put(packet)
class APRSDPluginRXThread(APRSDDupeRXThread):
""""Process received packets.
For backwards compatibility, we keep the APRSDPluginRXThread.
"""
class APRSDProcessPacketThread(APRSDThread):
"""Base class for processing received packets.
@@ -157,7 +165,7 @@ class APRSDProcessPacketThread(APRSDThread):
def process_packet(self, packet):
"""Process a packet received from aprs-is server."""
LOG.debug(f"RXPKT-LOOP {self._loop_cnt}")
LOG.debug(f"ProcessPKT-LOOP {self._loop_cnt}")
our_call = CONF.callsign.lower()
from_call = packet.from_call
@@ -202,7 +210,7 @@ class APRSDProcessPacketThread(APRSDThread):
self.process_other_packet(
packet, for_us=(to_call.lower() == our_call),
)
LOG.debug("Packet processing complete")
LOG.debug(f"Packet processing complete for pkt '{packet.key}'")
return False
@abc.abstractmethod
+40
View File
@@ -37,6 +37,7 @@ msg_throttle_decorator = decorator.ThrottleDecorator(throttle=msg_t)
ack_throttle_decorator = decorator.ThrottleDecorator(throttle=ack_t)
@msg_throttle_decorator.sleep_and_retry
def send(packet: core.Packet, direct=False, aprs_client=None):
"""Send a packet either in a thread or directly to the client."""
# prepare the packet for sending.
@@ -194,3 +195,42 @@ class SendAckThread(aprsd_threads.APRSDThread):
time.sleep(1)
self.loop_count += 1
return True
class BeaconSendThread(aprsd_threads.APRSDThread):
"""Thread that sends a GPS beacon packet periodically.
Settings are in the [DEFAULT] section of the config file.
"""
_loop_cnt: int = 1
def __init__(self):
super().__init__("BeaconSendThread")
self._loop_cnt = 1
# Make sure Latitude and Longitude are set.
if not CONF.latitude or not CONF.longitude:
LOG.error(
"Latitude and Longitude are not set in the config file."
"Beacon will not be sent and thread is STOPPED.",
)
self.stop()
LOG.info(
"Beacon thread is running and will send "
f"beacons every {CONF.beacon_interval} seconds.",
)
def loop(self):
# Only dump out the stats every N seconds
if self._loop_cnt % CONF.beacon_interval == 0:
pkt = core.BeaconPacket(
from_call=CONF.callsign,
to_call="APRS",
latitude=float(CONF.latitude),
longitude=float(CONF.longitude),
comment="APRSD GPS Beacon",
symbol=CONF.beacon_symbol,
)
send(pkt, direct=True)
self._loop_cnt += 1
time.sleep(1)
return True
+19
View File
@@ -4,6 +4,7 @@ import errno
import os
import re
import sys
import traceback
import update_checker
@@ -131,3 +132,21 @@ def parse_delta_str(s):
return {key: float(val) for key, val in m.groupdict().items()}
else:
return {}
def load_entry_points(group):
"""Load all extensions registered to the given entry point group"""
print(f"Loading extensions for group {group}")
try:
import importlib_metadata
except ImportError:
# For python 3.10 and later
import importlib.metadata as importlib_metadata
eps = importlib_metadata.entry_points(group=group)
for ep in eps:
try:
ep.load()
except Exception as e:
print(f"Extension {ep.name} of group {group} failed to load with {e}", file=sys.stderr)
print(traceback.format_exc(), file=sys.stderr)
+7 -1
View File
@@ -28,6 +28,9 @@ class ObjectStoreMixin:
def __len__(self):
return len(self.data)
def __iter__(self):
return iter(self.data)
def get_all(self):
with self.lock:
return self.data
@@ -96,11 +99,14 @@ class ObjectStoreMixin:
LOG.debug(
f"{self.__class__.__name__}::Loaded {len(self)} entries from disk.",
)
LOG.debug(f"{self.data}")
else:
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(ex)
self.data = {}
else:
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."""
+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-globe" viewBox="0 0 16 16">
<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m7.5-6.923c-.67.204-1.335.82-1.887 1.855A8 8 0 0 0 5.145 4H7.5zM4.09 4a9.3 9.3 0 0 1 .64-1.539 7 7 0 0 1 .597-.933A7.03 7.03 0 0 0 2.255 4zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a7 7 0 0 0-.656 2.5zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5zM8.5 5v2.5h2.99a12.5 12.5 0 0 0-.337-2.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5zM5.145 12q.208.58.468 1.068c.552 1.035 1.218 1.65 1.887 1.855V12zm.182 2.472a7 7 0 0 1-.597-.933A9.3 9.3 0 0 1 4.09 12H2.255a7 7 0 0 0 3.072 2.472M3.82 11a13.7 13.7 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5zm6.853 3.472A7 7 0 0 0 13.745 12H11.91a9.3 9.3 0 0 1-.64 1.539 7 7 0 0 1-.597.933M8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855q.26-.487.468-1.068zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.7 13.7 0 0 1-.312 2.5m2.802-3.5a7 7 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7 7 0 0 0-3.072-2.472c.218.284.418.598.597.933M10.855 4a8 8 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+107 -29
View File
@@ -1,5 +1,6 @@
var cleared = false;
var callsign_list = {};
var callsign_location = {};
var message_list = {};
var from_msg_list = {};
var selected_tab_callsign = null;
@@ -9,6 +10,35 @@ MSG_TYPE_TX = "tx";
MSG_TYPE_RX = "rx";
MSG_TYPE_ACK = "ack";
function reload_popovers() {
$('[data-bs-toggle="popover"]').popover(
{html: true, animation: true}
);
}
function build_location_string(msg) {
dt = new Date(parseInt(msg['lasttime']) * 1000);
loc = "Last Location Update: " + dt.toLocaleString();
loc += "<br>Latitude: " + msg['lat'] + "<br>Longitude: " + msg['lon'];
loc += "<br>" + "Altitude: " + msg['altitude'] + " m";
loc += "<br>" + "Speed: " + msg['speed'] + " kph";
loc += "<br>" + "Bearing: " + msg['course'] + "°";
loc += "<br>" + "distance: " + msg['distance'] + " km";
return loc;
}
function build_location_string_small(msg) {
dt = new Date(parseInt(msg['lasttime']) * 1000);
loc = "" + msg['distance'] + "km";
//loc += "Lat " + msg['lat'] + "&nbsp;Lon " + msg['lon'];
loc += "@" + msg['course'] + "°";
//loc += "&nbsp;Distance " + msg['distance'] + " km";
loc += "&nbsp;" + dt.toLocaleString();
return loc;
}
function size_dict(d){c=0; for (i in d) ++c; return c}
function raise_error(msg) {
@@ -31,8 +61,6 @@ function init_chat() {
});
socket.on("sent", function(msg) {
console.log("SENT: ");
console.log(msg);
if (cleared === false) {
var msgsdiv = $("#msgsTabsDiv");
msgsdiv.html('');
@@ -57,6 +85,20 @@ function init_chat() {
from_msg(msg);
});
socket.on("callsign_location", function(msg) {
console.log("CALLSIGN Location!");
console.log(msg);
now = new Date();
msg['last_updated'] = now;
callsign_location[msg['callsign']] = msg;
location_id = callsign_location_content(msg['callsign'], true);
location_string = build_location_string_small(msg);
$(location_id).html(location_string);
$(location_id+"Spinner").addClass('d-none');
save_data();
});
$("#sendform").submit(function(event) {
event.preventDefault();
to_call = $('#to_call').val();
@@ -71,7 +113,7 @@ function init_chat() {
return false;
}
msg = {'to': to_call, 'message': message, 'path': path};
console.log(msg);
//console.log(msg);
socket.emit("send", msg);
$('#message').val('');
}
@@ -82,6 +124,7 @@ function init_chat() {
init_messages();
}
function tab_string(callsign, id=false) {
name = "msgs"+callsign;
if (id) {
@@ -121,6 +164,14 @@ function callsign_tab(callsign) {
return "#"+tab_string(callsign);
}
function callsign_location_popover(callsign, id=false) {
return tab_string(callsign, id)+"Location";
}
function callsign_location_content(callsign, id=false) {
return tab_string(callsign, id)+"LocationContent";
}
function bubble_msg_id(msg, id=false) {
// The id of the div that contains a specific message
name = msg["from_call"] + "_" + msg["msgNo"];
@@ -155,20 +206,26 @@ function save_data() {
// Save the relevant data to local storage
localStorage.setItem('callsign_list', JSON.stringify(callsign_list));
localStorage.setItem('message_list', JSON.stringify(message_list));
localStorage.setItem('callsign_location', JSON.stringify(callsign_location));
}
function init_messages() {
// This tries to load any previous conversations from local storage
callsign_list = JSON.parse(localStorage.getItem('callsign_list'));
message_list = JSON.parse(localStorage.getItem('message_list'));
callsign_location = JSON.parse(localStorage.getItem('callsign_location'));
if (callsign_list == null) {
callsign_list = {};
}
if (message_list == null) {
message_list = {};
}
//console.log(callsign_list);
//console.log(message_list);
if (callsign_location == null) {
callsign_location = {};
}
console.log(callsign_list);
console.log(message_list);
console.log(callsign_location);
// Now loop through each callsign and add the tabs
first_callsign = null;
@@ -245,6 +302,7 @@ function create_callsign_tab(callsign, active=false) {
tab_id_li = tab_li_string(callsign);
tab_notify_id = tab_notification_id(callsign);
tab_content = tab_content_name(callsign);
popover_id = callsign_location_popover(callsign);
if (active) {
active_str = "active";
} else {
@@ -255,9 +313,9 @@ function create_callsign_tab(callsign, active=false) {
//item_html += '<button onClick="callsign_select(\''+callsign+'\');" callsign="'+callsign+'" class="nav-link '+active_str+'" id="'+tab_id+'" data-bs-toggle="tab" data-bs-target="#'+tab_content+'" type="button" role="tab" aria-controls="'+callsign+'" aria-selected="true">';
item_html += '<button onClick="callsign_select(\''+callsign+'\');" callsign="'+callsign+'" class="nav-link position-relative '+active_str+'" id="'+tab_id+'" data-bs-toggle="tab" data-bs-target="#'+tab_content+'" type="button" role="tab" aria-controls="'+callsign+'" aria-selected="true">';
item_html += callsign+'&nbsp;&nbsp;';
item_html += '<span id="'+tab_notify_id+'" class="position-absolute top-0 start-80 translate-middle badge bg-danger border border-light rounded-pill visually-hidden">0</span>';
item_html += '<span onclick="delete_tab(\''+callsign+'\');">×</span>';
item_html += '</button></li>'
callsignTabs.append(item_html);
create_callsign_tab_content(callsign, active);
}
@@ -273,7 +331,22 @@ function create_callsign_tab_content(callsign, active=false) {
active_str = '';
}
location_str = "Unknown Location"
if (callsign in callsign_location) {
location_str = build_location_string_small(callsign_location[callsign]);
location_class = '';
}
location_id = callsign_location_content(callsign);
item_html = '<div class="tab-pane fade '+active_str+'" id="'+tab_content+'" role="tabpanel" aria-labelledby="'+tab_id+'">';
item_html += '<div class="" style="border: 1px solid #999999;background-color:#aaaaaa;">';
item_html += '<div class="row" style="padding-top:4px;padding-bottom:4px;background-color:#aaaaaa;margin:0px;">';
item_html += '<div class="d-flex col-md-10 justify-content-left" style="padding:0px;margin:0px;">';
item_html += '<button onclick="call_callsign_location(\''+callsign+'\');" style="margin-left:2px;padding: 0px 4px 0px 4px;" type="button" class="btn btn-primary">';
item_html += '<span id="'+location_id+'Spinner" class="d-none spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>Update</button>';
item_html += '&nbsp;<span id="'+location_id+'">'+location_str+'</span></div>';
item_html += '</div>';
item_html += '<div class="speech-wrapper" id="'+wrapper_id+'"></div>';
item_html += '</div>';
callsignTabsContent.append(item_html);
@@ -288,6 +361,7 @@ function delete_tab(callsign) {
$(tab_content).remove();
delete callsign_list[callsign];
delete message_list[callsign];
delete callsign_location[callsign];
// Now select the first tab
first_tab = $("#msgsTabList").children().first().children().first();
@@ -312,10 +386,9 @@ function add_callsign(callsign, msg) {
return true;
}
function update_callsign_path(callsign, path) {
function update_callsign_path(callsign, msg) {
//Get the selected path to save for this callsign
path = msg['path']
console.log("Path is " + path);
$('#pkt_path').val(path);
callsign_list[callsign] = path;
@@ -324,7 +397,6 @@ function update_callsign_path(callsign, path) {
function append_message(callsign, msg, msg_html) {
new_callsign = false
if (!message_list.hasOwnProperty(callsign)) {
//message_list[callsign] = new Array();
message_list[callsign] = {};
}
ts_id = message_ts_id(msg);
@@ -343,10 +415,11 @@ function append_message(callsign, msg, msg_html) {
// Find the right div to place the html
new_callsign = add_callsign(callsign, msg);
update_callsign_path(callsign, msg['path']);
update_callsign_path(callsign, msg);
append_message_html(callsign, msg_html, new_callsign);
if (new_callsign) {
//Now click the tab
len = Object.keys(callsign_list).length;
if (new_callsign && len == 1) {
//Now click the tab if and only if there is only one tab
callsign_tab_id = callsign_tab(callsign);
$(callsign_tab_id).click();
callsign_select(callsign);
@@ -382,11 +455,23 @@ function create_message_html(date, time, from, to, message, ack_id, msg, acked=f
date_str = date + " " + time;
sane_date_str = date_str.replace(/ /g,"").replaceAll("/","").replaceAll(":","");
bubble_msg_class = "bubble-message";
if (ack_id) {
bubble_arrow_class = "bubble-arrow alt";
popover_placement = "left";
} else {
bubble_arrow_class = "bubble-arrow";
popover_placement = "right";
}
msg_html = '<div class="bubble-row'+alt+'">';
msg_html += '<div id="'+bubble_msgid+'" class="'+ bubble_class + '" data-bs-toggle="popover" data-bs-content="'+msg['raw']+'">';
msg_html += '<div id="'+bubble_msgid+'" class="'+ bubble_class + '" ';
msg_html += 'title="APRS Raw Packet" data-bs-placement="'+popover_placement+'" data-bs-toggle="popover" ';
msg_html += 'data-bs-trigger="hover" data-bs-content="'+msg['raw']+'">';
msg_html += '<div class="bubble-text">';
msg_html += '<p class="'+ bubble_name_class +'">'+from+'&nbsp;&nbsp;';
msg_html += '<span class="bubble-timestamp">'+date_str+'</span>';
if (ack_id) {
if (acked) {
msg_html += '<span class="material-symbols-rounded md-10" id="' + ack_id + '">thumb_up</span>';
@@ -395,24 +480,11 @@ function create_message_html(date, time, from, to, message, ack_id, msg, acked=f
}
}
msg_html += "</p>";
bubble_msg_class = "bubble-message"
if (ack_id) {
bubble_arrow_class = "bubble-arrow alt"
popover_placement = "left"
} else {
bubble_arrow_class = "bubble-arrow"
popover_placement = "right"
}
msg_html += '<p class="' +bubble_msg_class+ '">'+message+'</p>';
msg_html += '<div class="'+ bubble_arrow_class + '"></div>';
msg_html += "</div></div></div>";
popover_html = '\n<script>$(function () {$(\'[data-bs-toggle="popover"]\').popover('
popover_html += '{title: "APRS Raw Packet", html: false, trigger: \'hover\', placement: \''+popover_placement+'\'});})';
popover_html += '</script>'
return msg_html+popover_html
return msg_html
}
function flash_message(msg) {
@@ -440,12 +512,11 @@ function from_msg(msg) {
if (msg["msgNo"] in from_msg_list[msg["from_call"]]) {
// We already have this message
console.log("We already have this message msgNo=" + msg["msgNo"]);
//console.log("We already have this message msgNo=" + msg["msgNo"]);
// Do some flashy thing?
flash_message(msg);
return false
} else {
console.log("Adding message " + msg["msgNo"] + " to " + msg["from_call"]);
from_msg_list[msg["from_call"]][msg["msgNo"]] = msg
}
info = time_ack_from_msg(msg);
@@ -502,3 +573,10 @@ function callsign_select(callsign) {
// Now update the path
$('#pkt_path').val(callsign_list[callsign]);
}
function call_callsign_location(callsign) {
msg = {'callsign': callsign};
socket.emit("get_callsign_location", msg);
location_id = callsign_location_content(callsign, true)+"Spinner";
$(location_id).removeClass('d-none');
}
+6 -2
View File
@@ -64,8 +64,12 @@
to_call.val(callsign);
selected_tab_callsign = callsign;
});
});
/*$('[data-bs-toggle="popover"]').popover(
{html: true, animation: true}
);*/
reload_popovers();
});
</script>
</head>
@@ -95,7 +99,7 @@
<div class="col-auto">
<label for="pkt_path" class="visually-hidden">PATH</label>
<select class="form-control mb-2 mr-sm-2" name="pkt_path" id="pkt_path" style="width:auto;">
<option value="" selected>Default Path</option>
<option value="" disabled selected>Default Path</option>
<option value="WIDE1-1">WIDE1-1</option>
<option value="WIDE1-1,WIDE2-1">WIDE1-1,WIDE2-1</option>
<option value="ARISS">ARISS</option>
+4 -39
View File
@@ -3,12 +3,10 @@ import importlib.metadata as imp
import io
import json
import logging
from logging.handlers import RotatingFileHandler
import time
import flask
from flask import Flask
from flask.logging import default_handler
from flask_httpauth import HTTPBasicAuth
from oslo_config import cfg, generator
import socketio
@@ -16,7 +14,7 @@ from werkzeug.security import check_password_hash
import aprsd
from aprsd import cli_helper, client, conf, packets, plugin, threads
from aprsd.log import rich as aprsd_logging
from aprsd.log import log
from aprsd.rpc import client as aprsd_rpc_client
@@ -314,39 +312,6 @@ class LoggingNamespace(socketio.Namespace):
self.log_thread.stop()
def setup_logging(flask_app, loglevel):
global app, LOG
log_level = conf.log.LOG_LEVELS[loglevel]
app.logger.setLevel(log_level)
flask_app.logger.removeHandler(default_handler)
date_format = CONF.logging.date_format
if CONF.logging.rich_logging:
log_format = "%(message)s"
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
rh = aprsd_logging.APRSDRichHandler(
show_thread=True, thread_width=15,
rich_tracebacks=True, omit_repeated_times=False,
)
rh.setFormatter(log_formatter)
app.logger.addHandler(rh)
log_file = CONF.logging.logfile
if log_file:
log_format = CONF.logging.logformat
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
fh = RotatingFileHandler(
log_file, maxBytes=(10248576 * 5),
backupCount=4,
)
fh.setFormatter(log_formatter)
app.logger.addHandler(fh)
LOG = app.logger
def init_app(config_file=None, log_level=None):
default_config_file = cli_helper.DEFAULT_CONFIG_FILE
if not config_file:
@@ -368,7 +333,7 @@ if __name__ == "__main__":
sio = socketio.Server(logger=True, async_mode=async_mode)
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
log_level = init_app(log_level="DEBUG")
setup_logging(app, log_level)
log.setup_logging(app, log_level)
sio.register_namespace(LoggingNamespace("/logs"))
CONF.log_opt_values(LOG, logging.DEBUG)
app.run(
@@ -392,7 +357,7 @@ if __name__ == "uwsgi_file_aprsd_wsgi":
# Commented out for local development.
# config_file=cli_helper.DEFAULT_CONFIG_FILE
)
setup_logging(app, log_level)
log.setup_logging(app, log_level)
sio.register_namespace(LoggingNamespace("/logs"))
CONF.log_opt_values(LOG, logging.DEBUG)
@@ -410,6 +375,6 @@ if __name__ == "aprsd.wsgi":
config_file="/config/aprsd.conf",
# config_file=cli_helper.DEFAULT_CONFIG_FILE,
)
setup_logging(app, log_level)
log.setup_logging(app, log_level)
sio.register_namespace(LoggingNamespace("/logs"))
CONF.log_opt_values(LOG, logging.DEBUG)
+46 -50
View File
@@ -5,83 +5,79 @@
# pip-compile --annotation-style=line dev-requirements.in
#
add-trailing-comma==3.1.0 # via gray
alabaster==0.7.13 # via sphinx
attrs==23.1.0 # via jsonschema, referencing
alabaster==0.7.16 # via sphinx
autoflake==1.5.3 # via gray
babel==2.13.1 # via sphinx
black==23.11.0 # via gray
build==1.0.3 # via pip-tools
cachetools==5.3.2 # via tox
certifi==2023.7.22 # via requests
babel==2.14.0 # via sphinx
black==24.2.0 # via gray
build==1.1.1 # via pip-tools
cachetools==5.3.3 # via tox
certifi==2024.2.2 # via requests
cfgv==3.4.0 # via pre-commit
chardet==5.2.0 # via tox
charset-normalizer==3.3.2 # via requests
click==8.1.7 # via black, pip-tools
click==8.1.7 # via black, fixit, moreorless, pip-tools
colorama==0.4.6 # via tox
commonmark==0.9.1 # via rich
configargparse==1.7 # via gray
coverage[toml]==7.3.2 # via coverage, pytest-cov
distlib==0.3.7 # via virtualenv
coverage[toml]==7.4.3 # via pytest-cov
distlib==0.3.8 # via virtualenv
docutils==0.20.1 # via sphinx
exceptiongroup==1.1.3 # via pytest
exceptiongroup==1.2.0 # via pytest
filelock==3.13.1 # via tox, virtualenv
fixit==0.1.4 # via gray
flake8==6.1.0 # via -r dev-requirements.in, fixit, pep8-naming
gray==0.13.0 # via -r dev-requirements.in
identify==2.5.31 # via pre-commit
idna==3.4 # via requests
fixit==2.1.0 # via gray
flake8==7.0.0 # via -r dev-requirements.in, pep8-naming
gray==0.14.0 # via -r dev-requirements.in
identify==2.5.35 # via pre-commit
idna==3.6 # via requests
imagesize==1.4.1 # via sphinx
importlib-resources==6.1.1 # via fixit
iniconfig==2.0.0 # via pytest
isort==5.12.0 # via -r dev-requirements.in, gray
jinja2==3.1.2 # via sphinx
jsonschema==4.20.0 # via fixit
jsonschema-specifications==2023.11.1 # via jsonschema
libcst==1.1.0 # via fixit
markupsafe==2.1.3 # via jinja2
isort==5.13.2 # via -r dev-requirements.in, gray
jinja2==3.1.3 # via sphinx
libcst==1.2.0 # via fixit
markupsafe==2.1.5 # via jinja2
mccabe==0.7.0 # via flake8
mypy==1.7.0 # via -r dev-requirements.in
moreorless==0.4.0 # via fixit
mypy==1.8.0 # via -r dev-requirements.in
mypy-extensions==1.0.0 # via black, mypy, typing-inspect
nodeenv==1.8.0 # via pre-commit
packaging==23.2 # via black, build, pyproject-api, pytest, sphinx, tox
pathspec==0.11.2 # via black
packaging==23.2 # via black, build, fixit, pyproject-api, pytest, sphinx, tox
pathspec==0.12.1 # via black, trailrunner
pep8-naming==0.13.3 # via -r dev-requirements.in
pip-tools==7.3.0 # via -r dev-requirements.in
platformdirs==3.11.0 # via black, tox, virtualenv
pluggy==1.3.0 # via pytest, tox
pre-commit==3.5.0 # via -r dev-requirements.in
pip-tools==7.4.1 # via -r dev-requirements.in
platformdirs==4.2.0 # via black, tox, virtualenv
pluggy==1.4.0 # via pytest, tox
pre-commit==3.6.2 # via -r dev-requirements.in
pycodestyle==2.11.1 # via flake8
pyflakes==3.1.0 # via autoflake, flake8
pygments==2.16.1 # via rich, sphinx
pyflakes==3.2.0 # via autoflake, flake8
pygments==2.17.2 # via rich, sphinx
pyproject-api==1.6.1 # via tox
pyproject-hooks==1.0.0 # via build
pytest==7.4.3 # via -r dev-requirements.in, pytest-cov
pyproject-hooks==1.0.0 # via build, pip-tools
pytest==8.0.2 # via -r dev-requirements.in, pytest-cov
pytest-cov==4.1.0 # via -r dev-requirements.in
pyupgrade==3.15.0 # via gray
pyyaml==6.0.1 # via fixit, libcst, pre-commit
referencing==0.31.0 # via jsonschema, jsonschema-specifications
pyupgrade==3.15.1 # via gray
pyyaml==6.0.1 # via libcst, pre-commit
requests==2.31.0 # via sphinx
rich==12.6.0 # via gray
rpds-py==0.13.0 # via jsonschema, referencing
snowballstemmer==2.2.0 # via sphinx
sphinx==7.2.6 # via -r dev-requirements.in, sphinxcontrib-applehelp, sphinxcontrib-devhelp, sphinxcontrib-htmlhelp, sphinxcontrib-qthelp, sphinxcontrib-serializinghtml
sphinxcontrib-applehelp==1.0.7 # via sphinx
sphinxcontrib-devhelp==1.0.5 # via sphinx
sphinxcontrib-htmlhelp==2.0.4 # via sphinx
sphinx==7.2.6 # via -r dev-requirements.in
sphinxcontrib-applehelp==1.0.8 # via sphinx
sphinxcontrib-devhelp==1.0.6 # via sphinx
sphinxcontrib-htmlhelp==2.0.5 # via sphinx
sphinxcontrib-jsmath==1.0.1 # via sphinx
sphinxcontrib-qthelp==1.0.6 # via sphinx
sphinxcontrib-serializinghtml==1.1.9 # via sphinx
sphinxcontrib-qthelp==1.0.7 # via sphinx
sphinxcontrib-serializinghtml==1.1.10 # via sphinx
tokenize-rt==5.2.0 # via add-trailing-comma, pyupgrade
toml==0.10.2 # via autoflake
tomli==2.0.1 # via black, build, coverage, mypy, pip-tools, pyproject-api, pyproject-hooks, pytest, tox
tox==4.11.3 # via -r dev-requirements.in
typing-extensions==4.8.0 # via black, libcst, mypy, typing-inspect
tomli==2.0.1 # via black, build, coverage, fixit, mypy, pip-tools, pyproject-api, pyproject-hooks, pytest, tox
tox==4.14.0 # via -r dev-requirements.in
trailrunner==1.4.0 # via fixit
typing-extensions==4.10.0 # via black, libcst, mypy, typing-inspect
typing-inspect==0.9.0 # via libcst
unify==0.5 # via gray
untokenize==0.1.1 # via unify
urllib3==2.1.0 # via requests
virtualenv==20.24.6 # via pre-commit, tox
wheel==0.41.3 # via pip-tools
urllib3==2.2.1 # via requests
virtualenv==20.25.1 # via pre-commit, tox
wheel==0.42.0 # via pip-tools
# The following packages are considered to be unsafe in a requirements file:
# pip
+11
View File
@@ -12,6 +12,17 @@ if [ ! -z "${APRSD_PLUGINS}" ]; then
pip3 install --user $plugin
done
fi
if [ ! -z "${APRSD_EXTENSIONS}" ]; then
OLDIFS=$IFS
IFS=','
echo "Installing APRSD extensions from pypi '$APRSD_EXTENSIONS'";
for extension in ${APRSD_EXTENSIONS}; do
IFS=$OLDIFS
# call your procedure/other scripts here below
echo "Installing '$extension'"
pip3 install --user $extension
done
fi
if [ -z "${LOG_LEVEL}" ] || [[ ! "${LOG_LEVEL}" =~ ^(CRITICAL|ERROR|WARNING|INFO)$ ]]; then
LOG_LEVEL="DEBUG"
+11
View File
@@ -12,6 +12,17 @@ if [ ! -z "${APRSD_PLUGINS}" ]; then
pip3 install --user $plugin
done
fi
if [ ! -z "${APRSD_EXTENSIONS}" ]; then
OLDIFS=$IFS
IFS=','
echo "Installing APRSD extensions from pypi '$APRSD_EXTENSIONS'";
for extension in ${APRSD_EXTENSIONS}; do
IFS=$OLDIFS
# call your procedure/other scripts here below
echo "Installing '$extension'"
pip3 install --user $extension
done
fi
if [ -z "${LOG_LEVEL}" ] || [[ ! "${LOG_LEVEL}" =~ ^(CRITICAL|ERROR|WARNING|INFO)$ ]]; then
LOG_LEVEL="DEBUG"
+12
View File
@@ -13,6 +13,18 @@ if [ ! -z "${APRSD_PLUGINS}" ]; then
done
fi
if [ ! -z "${APRSD_EXTENSIONS}" ]; then
OLDIFS=$IFS
IFS=','
echo "Installing APRSD extensions from pypi '$APRSD_EXTENSIONS'";
for extension in ${APRSD_EXTENSIONS}; do
IFS=$OLDIFS
# call your procedure/other scripts here below
echo "Installing '$extension'"
pip3 install --user $extension
done
fi
if [ -z "${LOG_LEVEL}" ] || [[ ! "${LOG_LEVEL}" =~ ^(CRITICAL|ERROR|WARNING|INFO)$ ]]; then
LOG_LEVEL="DEBUG"
fi
+3 -2
View File
@@ -20,7 +20,7 @@ gevent
eventlet
tabulate
# Pinned due to gray needing 12.6.0
rich==12.6.0
rich~=12.6.0
# For the list-plugins pypi.org search scraping
beautifulsoup4
wrapt
@@ -30,10 +30,11 @@ attrs
dataclasses
dacite2
oslo.config
rpyc
rpyc>=6.0.0
# Pin this here so it doesn't require a compile on
# raspi
shellingham
geopy
rush
dataclasses-json
loguru
+38 -37
View File
@@ -5,79 +5,80 @@
# pip-compile --annotation-style=line requirements.in
#
aprslib==0.7.2 # via -r requirements.in
attrs==23.1.0 # via -r requirements.in, ax253, kiss3, rush
attrs==23.2.0 # via -r requirements.in, ax253, kiss3, rush
ax253==0.1.5.post1 # via kiss3
beautifulsoup4==4.12.2 # via -r requirements.in
bidict==0.22.1 # via python-socketio
bitarray==2.8.3 # via ax253, kiss3
beautifulsoup4==4.12.3 # via -r requirements.in
bidict==0.23.1 # via python-socketio
bitarray==2.9.2 # via ax253, kiss3
blinker==1.7.0 # via flask
certifi==2023.7.22 # via requests
certifi==2024.2.2 # via requests
charset-normalizer==3.3.2 # via requests
click==8.1.7 # via -r requirements.in, click-completion, click-params, flask
click-completion==0.5.2 # via -r requirements.in
click-params==0.4.1 # via -r requirements.in
click-params==0.5.0 # via -r requirements.in
commonmark==0.9.1 # via rich
dacite2==2.0.0 # via -r requirements.in
dataclasses==0.6 # via -r requirements.in
dataclasses-json==0.6.2 # via -r requirements.in
debtcollector==2.5.0 # via oslo-config
decorator==5.1.1 # via validators
dnspython==2.4.2 # via eventlet
eventlet==0.33.3 # via -r requirements.in
flask==3.0.0 # via -r requirements.in, flask-httpauth, flask-socketio
dataclasses-json==0.6.4 # via -r requirements.in
debtcollector==3.0.0 # via oslo-config
deprecated==1.2.14 # via click-params
dnspython==2.6.1 # via eventlet
eventlet==0.35.2 # via -r requirements.in
flask==3.0.2 # via -r requirements.in, flask-httpauth, flask-socketio
flask-httpauth==4.8.0 # via -r requirements.in
flask-socketio==5.3.6 # via -r requirements.in
geographiclib==2.0 # via geopy
geopy==2.4.0 # via -r requirements.in
gevent==23.9.1 # via -r requirements.in
greenlet==3.0.1 # via eventlet, gevent
geopy==2.4.1 # via -r requirements.in
gevent==24.2.1 # via -r requirements.in
greenlet==3.0.3 # via eventlet, gevent
h11==0.14.0 # via wsproto
idna==3.4 # via requests
imapclient==3.0.0 # via -r requirements.in
importlib-metadata==6.8.0 # via ax253, kiss3
idna==3.6 # via requests
imapclient==3.0.1 # via -r requirements.in
importlib-metadata==7.0.1 # via ax253, kiss3
itsdangerous==2.1.2 # via flask
jinja2==3.1.2 # via click-completion, flask
jinja2==3.1.3 # via click-completion, flask
kiss3==8.0.0 # via -r requirements.in
markupsafe==2.1.3 # via jinja2, werkzeug
marshmallow==3.20.1 # via dataclasses-json
loguru==0.7.2 # via -r requirements.in
markupsafe==2.1.5 # via jinja2, werkzeug
marshmallow==3.21.1 # via dataclasses-json
mypy-extensions==1.0.0 # via typing-inspect
netaddr==0.9.0 # via oslo-config
oslo-config==9.2.0 # via -r requirements.in
oslo-i18n==6.2.0 # via oslo-config
netaddr==1.2.1 # via oslo-config
oslo-config==9.4.0 # via -r requirements.in
oslo-i18n==6.3.0 # via oslo-config
packaging==23.2 # via marshmallow
pbr==6.0.0 # via -r requirements.in, oslo-i18n, stevedore
pluggy==1.3.0 # via -r requirements.in
pluggy==1.4.0 # via -r requirements.in
plumbum==1.8.2 # via rpyc
pygments==2.16.1 # via rich
pygments==2.17.2 # via rich
pyserial==3.5 # via pyserial-asyncio
pyserial-asyncio==0.6 # via kiss3
python-engineio==4.8.0 # via python-socketio
python-socketio==5.10.0 # via -r requirements.in, flask-socketio
pytz==2023.3.post1 # via -r requirements.in
python-engineio==4.9.0 # via python-socketio
python-socketio==5.11.1 # via -r requirements.in, flask-socketio
pytz==2024.1 # via -r requirements.in
pyyaml==6.0.1 # via -r requirements.in, oslo-config
requests==2.31.0 # via -r requirements.in, oslo-config, update-checker
rfc3986==2.0.0 # via oslo-config
rich==12.6.0 # via -r requirements.in
rpyc==5.3.1 # via -r requirements.in
rpyc==6.0.0 # via -r requirements.in
rush==2021.4.0 # via -r requirements.in
shellingham==1.5.4 # via -r requirements.in, click-completion
simple-websocket==1.0.0 # via python-engineio
six==1.16.0 # via -r requirements.in, click-completion, eventlet
six==1.16.0 # via -r requirements.in, click-completion
soupsieve==2.5 # via beautifulsoup4
stevedore==5.1.0 # via oslo-config
stevedore==5.2.0 # via oslo-config
tabulate==0.9.0 # via -r requirements.in
thesmuggler==1.0.1 # via -r requirements.in
typing-extensions==4.8.0 # via typing-inspect
typing-extensions==4.10.0 # via typing-inspect
typing-inspect==0.9.0 # via dataclasses-json
update-checker==0.18.0 # via -r requirements.in
urllib3==2.1.0 # via requests
validators==0.20.0 # via click-params
urllib3==2.2.1 # via requests
validators==0.22.0 # via click-params
werkzeug==3.0.1 # via -r requirements.in, flask
wrapt==1.16.0 # via -r requirements.in, debtcollector
wrapt==1.16.0 # via -r requirements.in, debtcollector, deprecated
wsproto==1.2.0 # via simple-websocket
zipp==3.17.0 # via importlib-metadata
zope-event==5.0 # via gevent
zope-interface==6.1 # via gevent
zope-interface==6.2 # via gevent
# The following packages are considered to be unsafe in a requirements file:
# setuptools
+6 -2
View File
@@ -56,17 +56,20 @@ class TestSendMessageCommand(unittest.TestCase):
msg_number=1,
message_format=core.PACKET_TYPE_ACK,
)
mock_queue = mock.MagicMock()
socketio = mock.MagicMock()
wcp = webchat.WebChatProcessPacketThread(packet, socketio)
wcp = webchat.WebChatProcessPacketThread(mock_queue, socketio)
wcp.process_ack_packet(packet)
mock_remove.called_once()
mock_socketio.called_once()
@mock.patch("aprsd.threads.tx.send")
@mock.patch("aprsd.packets.PacketList.rx")
@mock.patch("aprsd.cmds.webchat.socketio")
def test_process_our_message_packet(
self,
mock_tx_send,
mock_packet_add,
mock_socketio,
):
@@ -77,8 +80,9 @@ class TestSendMessageCommand(unittest.TestCase):
msg_number=1,
message_format=core.PACKET_TYPE_MESSAGE,
)
mock_queue = mock.MagicMock()
socketio = mock.MagicMock()
wcp = webchat.WebChatProcessPacketThread(packet, socketio)
wcp = webchat.WebChatProcessPacketThread(mock_queue, socketio)
wcp.process_our_message_packet(packet)
mock_packet_add.called_once()
+2 -1
View File
@@ -2,7 +2,7 @@
minversion = 2.9.0
skipdist = True
skip_missing_interpreters = true
envlist = pep8,py{39,310,311}
envlist = pep8,py{39,310}
#requires = tox-pipenv
# pip==22.0.4
# pip-tools==5.4.0
@@ -78,6 +78,7 @@ python =
3.7: py38, pep8
3.8: py38, pep8
3.9: py39, pep8, type-check, docs
3.10: py39, pep8, type-check, docs
[testenv:fmt]
# This will reformat your code to comply with pep8