mirror of
https://github.com/craigerl/aprsd.git
synced 2026-08-16 00:23:34 -04:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e0de9c5ac | |||
| 71cd7e0ab5 | |||
| d485f484ec | |||
| f810c02d5d | |||
| 50e24abb81 | |||
| 10d023dd7b | |||
| cb9456b29d | |||
| c37e1d58bb | |||
| 0ca5ceee7e | |||
| 2e9c9d40e1 | |||
| 66004f639f | |||
| 0b0afd39ed | |||
| aec88d4a7e | |||
| 24bbea1d49 | |||
| 5d3f42f411 | |||
| 44a98850c9 | |||
| 2cb9c2a31c | |||
| 2fefa9fcd6 | |||
| d092a43ec9 | |||
| d1a09fc6b5 | |||
| ff051bc285 | |||
| 5fd91a2172 | |||
| a4630c15be | |||
| 6a7d7ad79b | |||
| 7a5b55fa77 | |||
| a1e21e795d | |||
| cb291de047 | |||
| e9c48c1914 | |||
| f0ad6d7577 | |||
| 38fe408c82 | |||
| 8264c94bd6 | |||
| 1ad2e135dc | |||
| 1e4f0ca65a | |||
| 41185416cb | |||
| 68f23d8ca7 | |||
| 11f1e9533e | |||
| 275bf67b9e | |||
| 968345944a | |||
| df2798eafb | |||
| e89f8a805b | |||
| b14307270c | |||
| ebee8e1439 | |||
| a7e30b0bed | |||
| 1a5c5f0dce | |||
| a00c4ea840 | |||
| a88de2f09c | |||
| d6f0f05315 | |||
| 03c58f83cd | |||
| a4230d324a | |||
| 8bceb827ec | |||
| 12a3113192 | |||
| 026a64c003 | |||
| 682e138ec2 | |||
| e4e9c6e98b | |||
| f02824b796 | |||
| 530ac30a09 | |||
| 9350cf6534 | |||
| 651cf014b7 | |||
| b6df9de8aa | |||
| 0fd7daaae0 | |||
| 0433768784 | |||
| a8f73610fe |
@@ -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}}"
|
||||
@@ -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: |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -58,3 +58,5 @@ AUTHORS
|
||||
.idea
|
||||
|
||||
Makefile.venv
|
||||
# Copilot
|
||||
.DS_Store
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,9 +1,88 @@
|
||||
CHANGES
|
||||
=======
|
||||
|
||||
v3.3.3
|
||||
------
|
||||
|
||||
* Fix for sample-config warning
|
||||
|
||||
v3.3.2
|
||||
------
|
||||
|
||||
* Changelog for 3.3.2
|
||||
* Remove warning during sample-config
|
||||
* Removed print in utils
|
||||
|
||||
v3.3.1
|
||||
------
|
||||
|
||||
* Updates for 3.3.1
|
||||
* Fixed failure with fetch-stats
|
||||
* Fixed problem with list-plugins
|
||||
|
||||
v3.3.0
|
||||
------
|
||||
|
||||
* Changelog for 3.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
|
||||
|
||||
@@ -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
@@ -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
-1
@@ -50,6 +50,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):
|
||||
@@ -104,7 +138,7 @@ def process_standard_options_no_config(f: F) -> F:
|
||||
ctx.obj["loglevel"] = kwargs["loglevel"]
|
||||
ctx.obj["config_file"] = kwargs["config_file"]
|
||||
ctx.obj["quiet"] = kwargs["quiet"]
|
||||
log.setup_logging_no_config(
|
||||
log.setup_logging(
|
||||
ctx.obj["loglevel"],
|
||||
ctx.obj["quiet"],
|
||||
)
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -58,7 +58,11 @@ def fetch_stats(ctx, host, port, magic_word):
|
||||
with console.status(msg):
|
||||
client = rpc_client.RPCClient(host, port, magic_word)
|
||||
stats = client.get_stats_dict()
|
||||
console.print_json(data=stats)
|
||||
if stats:
|
||||
console.print_json(data=stats)
|
||||
else:
|
||||
LOG.error(f"Failed to fetch stats via RPC aprsd server at {host}:{port}")
|
||||
return
|
||||
aprsd_title = (
|
||||
"APRSD "
|
||||
f"[bold cyan]v{stats['aprsd']['version']}[/] "
|
||||
|
||||
+87
-10
@@ -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
@@ -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
@@ -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
@@ -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.BoolOpt(
|
||||
"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
@@ -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,
|
||||
|
||||
@@ -18,6 +18,11 @@ owm_wx_group = cfg.OptGroup(
|
||||
title="Options for the OWMWeatherPlugin",
|
||||
)
|
||||
|
||||
location_group = cfg.OptGroup(
|
||||
name="location_plugin",
|
||||
title="Options for the LocationPlugin",
|
||||
)
|
||||
|
||||
aprsfi_opts = [
|
||||
cfg.StrOpt(
|
||||
"apiKey",
|
||||
@@ -62,6 +67,106 @@ avwx_opts = [
|
||||
),
|
||||
]
|
||||
|
||||
location_opts = [
|
||||
cfg.StrOpt(
|
||||
"geopy_geocoder",
|
||||
choices=[
|
||||
"ArcGIS", "AzureMaps", "Baidu", "Bing", "GoogleV3", "HERE",
|
||||
"Nominatim", "OpenCage", "TomTom", "USGov", "What3Words", "Woosmap",
|
||||
],
|
||||
default="Nominatim",
|
||||
help="The geopy geocoder to use. Default is Nominatim."
|
||||
"See https://geopy.readthedocs.io/en/stable/#module-geopy.geocoders"
|
||||
"for more information.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"user_agent",
|
||||
default="APRSD",
|
||||
help="The user agent to use for the Nominatim geocoder."
|
||||
"See https://geopy.readthedocs.io/en/stable/#module-geopy.geocoders"
|
||||
"for more information.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"arcgis_username",
|
||||
default=None,
|
||||
help="The username to use for the ArcGIS geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#arcgis"
|
||||
"for more information."
|
||||
"Only used for the ArcGIS geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"arcgis_password",
|
||||
default=None,
|
||||
help="The password to use for the ArcGIS geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#arcgis"
|
||||
"for more information."
|
||||
"Only used for the ArcGIS geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"azuremaps_subscription_key",
|
||||
help="The subscription key to use for the AzureMaps geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#azuremaps"
|
||||
"for more information."
|
||||
"Only used for the AzureMaps geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"baidu_api_key",
|
||||
help="The API key to use for the Baidu geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#baidu"
|
||||
"for more information."
|
||||
"Only used for the Baidu geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"bing_api_key",
|
||||
help="The API key to use for the Bing geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#bing"
|
||||
"for more information."
|
||||
"Only used for the Bing geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"google_api_key",
|
||||
help="The API key to use for the Google geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#googlev3"
|
||||
"for more information."
|
||||
"Only used for the Google geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"here_api_key",
|
||||
help="The API key to use for the HERE geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#here"
|
||||
"for more information."
|
||||
"Only used for the HERE geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"opencage_api_key",
|
||||
help="The API key to use for the OpenCage geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#opencage"
|
||||
"for more information."
|
||||
"Only used for the OpenCage geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"tomtom_api_key",
|
||||
help="The API key to use for the TomTom geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#tomtom"
|
||||
"for more information."
|
||||
"Only used for the TomTom geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"what3words_api_key",
|
||||
help="The API key to use for the What3Words geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#what3words"
|
||||
"for more information."
|
||||
"Only used for the What3Words geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"woosmap_api_key",
|
||||
help="The API key to use for the Woosmap geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#woosmap"
|
||||
"for more information."
|
||||
"Only used for the Woosmap geocoder.",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def register_opts(config):
|
||||
config.register_group(aprsfi_group)
|
||||
@@ -72,6 +177,8 @@ def register_opts(config):
|
||||
config.register_opts(owm_wx_opts, group=owm_wx_group)
|
||||
config.register_group(avwx_group)
|
||||
config.register_opts(avwx_opts, group=avwx_group)
|
||||
config.register_group(location_group)
|
||||
config.register_opts(location_opts, group=location_group)
|
||||
|
||||
|
||||
def list_opts():
|
||||
@@ -80,4 +187,5 @@ def list_opts():
|
||||
query_group.name: query_plugin_opts,
|
||||
owm_wx_group.name: owm_wx_opts,
|
||||
avwx_group.name: avwx_opts,
|
||||
location_group.name: location_opts,
|
||||
}
|
||||
|
||||
+72
-64
@@ -1,13 +1,12 @@
|
||||
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,75 +14,84 @@ 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)
|
||||
|
||||
|
||||
def setup_logging_no_config(loglevel, quiet):
|
||||
log_level = conf.log.LOG_LEVELS[loglevel]
|
||||
LOG.setLevel(log_level)
|
||||
log_format = CONF.logging.logformat
|
||||
date_format = CONF.logging.date_format
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
fh = NullHandler()
|
||||
|
||||
fh.setFormatter(log_formatter)
|
||||
LOG.addHandler(fh)
|
||||
|
||||
if not quiet:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
LOG.addHandler(sh)
|
||||
# configure loguru
|
||||
logger.configure(handlers=handlers)
|
||||
|
||||
@@ -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
|
||||
+11
-12
@@ -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")
|
||||
@@ -145,9 +145,8 @@ def sample_config(ctx):
|
||||
if not sys.argv[1:]:
|
||||
raise SystemExit
|
||||
raise
|
||||
LOG.warning(conf.namespace)
|
||||
return
|
||||
generator.generate(conf)
|
||||
return
|
||||
|
||||
|
||||
@cli.command()
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -76,6 +76,7 @@ def fetch_openweathermap(api_key, lat, lon, units="metric", exclude=None):
|
||||
exclude,
|
||||
)
|
||||
)
|
||||
LOG.debug(f"Fetching OWM weather '{url}'")
|
||||
response = requests.get(url)
|
||||
except Exception as e:
|
||||
LOG.error(e)
|
||||
|
||||
@@ -8,6 +8,8 @@ from aprsd.utils import trace
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
DEFAULT_FORTUNE_PATH = '/usr/games/fortune'
|
||||
|
||||
|
||||
class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
"""Fortune."""
|
||||
@@ -19,7 +21,8 @@ class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
fortune_path = None
|
||||
|
||||
def setup(self):
|
||||
self.fortune_path = shutil.which("fortune")
|
||||
self.fortune_path = shutil.which(DEFAULT_FORTUNE_PATH)
|
||||
LOG.info(f"Fortune path {self.fortune_path}")
|
||||
if not self.fortune_path:
|
||||
self.enabled = False
|
||||
else:
|
||||
|
||||
@@ -2,7 +2,8 @@ import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
from geopy.geocoders import Nominatim
|
||||
from geopy.geocoders import ArcGIS, AzureMaps, Baidu, Bing, GoogleV3
|
||||
from geopy.geocoders import HereV7, Nominatim, OpenCage, TomTom, What3WordsV3, Woosmap
|
||||
from oslo_config import cfg
|
||||
|
||||
from aprsd import packets, plugin, plugin_utils
|
||||
@@ -13,6 +14,82 @@ CONF = cfg.CONF
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class UsLocation:
|
||||
raw = {}
|
||||
|
||||
def __init__(self, info):
|
||||
self.info = info
|
||||
|
||||
def __str__(self):
|
||||
return self.info
|
||||
|
||||
|
||||
class USGov:
|
||||
"""US Government geocoder that uses the geopy API.
|
||||
|
||||
This is a dummy class the implements the geopy reverse API,
|
||||
so the factory can return an object that conforms to the API.
|
||||
"""
|
||||
def reverse(self, coordinates):
|
||||
"""Reverse geocode a coordinate."""
|
||||
LOG.info(f"USGov reverse geocode {coordinates}")
|
||||
coords = coordinates.split(",")
|
||||
lat = float(coords[0])
|
||||
lon = float(coords[1])
|
||||
result = plugin_utils.get_weather_gov_for_gps(lat, lon)
|
||||
# LOG.info(f"WEATHER: {result}")
|
||||
# LOG.info(f"area description {result['location']['areaDescription']}")
|
||||
if 'location' in result:
|
||||
loc = UsLocation(result['location']['areaDescription'])
|
||||
else:
|
||||
loc = UsLocation("Unknown Location")
|
||||
|
||||
LOG.info(f"USGov reverse geocode LOC {loc}")
|
||||
return loc
|
||||
|
||||
|
||||
def geopy_factory():
|
||||
"""Factory function for geopy geocoders."""
|
||||
geocoder = CONF.location_plugin.geopy_geocoder
|
||||
LOG.info(f"Using geocoder: {geocoder}")
|
||||
user_agent = CONF.location_plugin.user_agent
|
||||
LOG.info(f"Using user_agent: {user_agent}")
|
||||
|
||||
if geocoder == "Nominatim":
|
||||
return Nominatim(user_agent=user_agent)
|
||||
elif geocoder == "USGov":
|
||||
return USGov()
|
||||
elif geocoder == "ArcGIS":
|
||||
return ArcGIS(
|
||||
username=CONF.location_plugin.arcgis_username,
|
||||
password=CONF.location_plugin.arcgis_password,
|
||||
user_agent=user_agent,
|
||||
)
|
||||
elif geocoder == "AzureMaps":
|
||||
return AzureMaps(
|
||||
user_agent=user_agent,
|
||||
subscription_key=CONF.location_plugin.azuremaps_subscription_key,
|
||||
)
|
||||
elif geocoder == "Baidu":
|
||||
return Baidu(user_agent=user_agent, api_key=CONF.location_plugin.baidu_api_key)
|
||||
elif geocoder == "Bing":
|
||||
return Bing(user_agent=user_agent, api_key=CONF.location_plugin.bing_api_key)
|
||||
elif geocoder == "GoogleV3":
|
||||
return GoogleV3(user_agent=user_agent, api_key=CONF.location_plugin.google_api_key)
|
||||
elif geocoder == "HERE":
|
||||
return HereV7(user_agent=user_agent, api_key=CONF.location_plugin.here_api_key)
|
||||
elif geocoder == "OpenCage":
|
||||
return OpenCage(user_agent=user_agent, api_key=CONF.location_plugin.opencage_api_key)
|
||||
elif geocoder == "TomTom":
|
||||
return TomTom(user_agent=user_agent, api_key=CONF.location_plugin.tomtom_api_key)
|
||||
elif geocoder == "What3Words":
|
||||
return What3WordsV3(user_agent=user_agent, api_key=CONF.location_plugin.what3words_api_key)
|
||||
elif geocoder == "Woosmap":
|
||||
return Woosmap(user_agent=user_agent, api_key=CONF.location_plugin.woosmap_api_key)
|
||||
else:
|
||||
raise ValueError(f"Unknown geocoder: {geocoder}")
|
||||
|
||||
|
||||
class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
||||
"""Location!"""
|
||||
|
||||
@@ -57,19 +134,24 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
||||
# Get some information about their location
|
||||
try:
|
||||
tic = time.perf_counter()
|
||||
geolocator = Nominatim(user_agent="APRSD")
|
||||
geolocator = geopy_factory()
|
||||
LOG.info(f"Using GEOLOCATOR: {geolocator}")
|
||||
coordinates = f"{lat:0.6f}, {lon:0.6f}"
|
||||
location = geolocator.reverse(coordinates)
|
||||
address = location.raw.get("address")
|
||||
LOG.debug(f"GEOLOCATOR address: {address}")
|
||||
toc = time.perf_counter()
|
||||
if address:
|
||||
LOG.info(f"Geopy address {address} took {toc - tic:0.4f}")
|
||||
if address.get("country_code") == "us":
|
||||
area_info = f"{address.get('county')}, {address.get('state')}"
|
||||
if address.get("country_code") == "us":
|
||||
area_info = f"{address.get('county')}, {address.get('state')}"
|
||||
else:
|
||||
# what to do for address for non US?
|
||||
area_info = f"{address.get('country'), 'Unknown'}"
|
||||
else:
|
||||
# what to do for address for non US?
|
||||
area_info = f"{address.get('country'), 'Unknown'}"
|
||||
area_info = str(location)
|
||||
except Exception as ex:
|
||||
LOG.error(ex)
|
||||
LOG.error(f"Failed to fetch Geopy address {ex}")
|
||||
area_info = "Unknown Location"
|
||||
|
||||
|
||||
@@ -26,7 +26,9 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
|
||||
"weather" - returns weather near the calling callsign
|
||||
"""
|
||||
|
||||
command_regex = r"^([w][x]|[w][x]\s|weather)"
|
||||
# command_regex = r"^([w][x]|[w][x]\s|weather)"
|
||||
command_regex = r"^[wW]"
|
||||
|
||||
command_name = "USWeather"
|
||||
short_description = "Provide USA only weather of GPS Beacon location"
|
||||
|
||||
@@ -189,7 +191,9 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
|
||||
"""
|
||||
|
||||
command_regex = r"^([w][x]|[w][x]\s|weather)"
|
||||
# command_regex = r"^([w][x]|[w][x]\s|weather)"
|
||||
command_regex = r"^[wW]"
|
||||
|
||||
command_name = "OpenWeatherMap"
|
||||
short_description = "OpenWeatherMap weather of GPS Beacon location"
|
||||
|
||||
@@ -211,7 +215,7 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
fromcall = packet.get("from")
|
||||
fromcall = packet.get("from_call")
|
||||
message = packet.get("message_text", None)
|
||||
# ack = packet.get("msgNo", "0")
|
||||
LOG.info(f"OWMWeather Plugin '{message}'")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
-1
@@ -4,6 +4,7 @@ import errno
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import update_checker
|
||||
|
||||
@@ -18,7 +19,7 @@ from .ring_buffer import RingBuffer # noqa: F401
|
||||
if sys.version_info.major == 3 and sys.version_info.minor >= 3:
|
||||
from collections.abc import MutableMapping
|
||||
else:
|
||||
from collections import MutableMapping
|
||||
from collections.abc import MutableMapping
|
||||
|
||||
|
||||
def env(*vars, **kwargs):
|
||||
@@ -131,3 +132,20 @@ 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"""
|
||||
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)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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 |
@@ -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'] + " Lon " + msg['lon'];
|
||||
loc += "@" + msg['course'] + "°";
|
||||
//loc += " Distance " + msg['distance'] + " km";
|
||||
loc += " " + 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+' ';
|
||||
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 += ' <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+' ';
|
||||
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');
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
+2
-1
@@ -23,7 +23,7 @@ RUN set -ex \
|
||||
&& apt-get update \
|
||||
&& apt-get upgrade -y \
|
||||
&& apt-get install -y git build-essential curl libffi-dev \
|
||||
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo \
|
||||
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo fortune \
|
||||
# Install dependencies
|
||||
# Clean up
|
||||
&& apt-get autoremove -y \
|
||||
@@ -41,6 +41,7 @@ RUN mkdir /config
|
||||
RUN chown -R appuser:appgroup /app
|
||||
RUN chown -R appuser:appgroup /config
|
||||
USER appuser
|
||||
RUN echo "PATH=\$PATH:/usr/games" >> /app/.bashrc
|
||||
RUN which aprsd
|
||||
RUN aprsd sample-config > /config/aprsd.conf
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN set -ex \
|
||||
# Upgrade the package index and install security upgrades
|
||||
&& apt-get update \
|
||||
&& apt-get upgrade -y \
|
||||
&& apt-get install -y git build-essential curl libffi-dev \
|
||||
&& apt-get install -y git build-essential curl libffi-dev fortune \
|
||||
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo \
|
||||
# Install dependencies
|
||||
# Clean up
|
||||
@@ -40,6 +40,7 @@ RUN mkdir /config
|
||||
RUN chown -R appuser:appgroup /app
|
||||
RUN chown -R appuser:appgroup /config
|
||||
USER appuser
|
||||
RUN echo "PATH=\$PATH:/usr/games" >> /app/.bashrc
|
||||
RUN which aprsd
|
||||
RUN aprsd sample-config > /config/aprsd.conf
|
||||
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
Reference in New Issue
Block a user