mirror of
https://github.com/craigerl/aprsd.git
synced 2026-08-18 17:44:02 -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
|
- uses: actions/checkout@v3
|
||||||
- name: Get Branch Name
|
- name: Get Branch Name
|
||||||
id: branch-name
|
id: branch-name
|
||||||
uses: tj-actions/branch-names@v7
|
uses: tj-actions/branch-names@v8
|
||||||
- name: Extract Branch
|
- name: Extract Branch
|
||||||
id: extract_branch
|
id: extract_branch
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Get Branch Name
|
- name: Get Branch Name
|
||||||
id: branch-name
|
id: branch-name
|
||||||
uses: tj-actions/branch-names@v6
|
uses: tj-actions/branch-names@v8
|
||||||
- name: Setup QEMU
|
- name: Setup QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v2
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Get Branch Name
|
- name: Get Branch Name
|
||||||
id: branch-name
|
id: branch-name
|
||||||
uses: tj-actions/branch-names@v6
|
uses: tj-actions/branch-names@v8
|
||||||
- name: Setup QEMU
|
- name: Setup QEMU
|
||||||
uses: docker/setup-qemu-action@v2
|
uses: docker/setup-qemu-action@v2
|
||||||
- name: Setup Docker Buildx
|
- name: Setup Docker Buildx
|
||||||
|
|||||||
@@ -58,3 +58,5 @@ AUTHORS
|
|||||||
.idea
|
.idea
|
||||||
|
|
||||||
Makefile.venv
|
Makefile.venv
|
||||||
|
# Copilot
|
||||||
|
.DS_Store
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v3.4.0
|
rev: v4.5.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
@@ -12,11 +12,11 @@ repos:
|
|||||||
- id: check-builtin-literals
|
- id: check-builtin-literals
|
||||||
|
|
||||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||||
rev: v1.16.0
|
rev: v2.5.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: setup-cfg-fmt
|
- id: setup-cfg-fmt
|
||||||
|
|
||||||
- repo: https://github.com/dizballanze/gray
|
- repo: https://github.com/dizballanze/gray
|
||||||
rev: v0.10.1
|
rev: v0.14.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: gray
|
- id: gray
|
||||||
|
|||||||
@@ -1,9 +1,88 @@
|
|||||||
CHANGES
|
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
|
v3.2.2
|
||||||
------
|
------
|
||||||
|
|
||||||
|
* Update for v3.2.2 release
|
||||||
* Fix for types
|
* Fix for types
|
||||||
* Fix wsgi for prod
|
* Fix wsgi for prod
|
||||||
* pep8 fixes
|
* pep8 fixes
|
||||||
|
|||||||
@@ -84,5 +84,5 @@ update-requirements: dev ## Update the requirements.txt and dev-requirements.tx
|
|||||||
rm dev-requirements.txt
|
rm dev-requirements.txt
|
||||||
touch requirements.txt
|
touch requirements.txt
|
||||||
touch dev-requirements.txt
|
touch dev-requirements.txt
|
||||||
$(VENV)/pip-compile --resolver backtracking --annotation-style line requirements.in
|
$(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 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 <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
|
What is APRSD
|
||||||
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
|
APRSD is a python application for interacting with the APRS network and providing
|
||||||
provide responding to messages to check email, get location, ping,
|
APRS services for HAM radio operators.
|
||||||
time of day, get weather, and fortune telling as well as version information
|
|
||||||
of aprsd itself.
|
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!
|
Please `read the docs`_ to learn more!
|
||||||
|
|
||||||
|
|
||||||
.. contents:: :local:
|
|
||||||
|
|
||||||
|
|
||||||
APRSD Overview Diagram
|
APRSD Overview Diagram
|
||||||
======================
|
======================
|
||||||
|
|
||||||
.. image:: https://raw.githubusercontent.com/craigerl/aprsd/master/docs/_static/aprsd_overview.svg?sanitize=true
|
.. image:: https://raw.githubusercontent.com/craigerl/aprsd/master/docs/_static/aprsd_overview.svg?sanitize=true
|
||||||
|
|
||||||
|
|
||||||
Typical use case
|
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
|
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
|
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
|
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.
|
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
|
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
|
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
|
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(options):
|
||||||
def _add_options(func):
|
def _add_options(func):
|
||||||
for option in reversed(options):
|
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["loglevel"] = kwargs["loglevel"]
|
||||||
ctx.obj["config_file"] = kwargs["config_file"]
|
ctx.obj["config_file"] = kwargs["config_file"]
|
||||||
ctx.obj["quiet"] = kwargs["quiet"]
|
ctx.obj["quiet"] = kwargs["quiet"]
|
||||||
log.setup_logging_no_config(
|
log.setup_logging(
|
||||||
ctx.obj["loglevel"],
|
ctx.obj["loglevel"],
|
||||||
ctx.obj["quiet"],
|
ctx.obj["quiet"],
|
||||||
)
|
)
|
||||||
|
|||||||
+3
-3
@@ -25,7 +25,7 @@ TRANSPORT_FAKE = "fake"
|
|||||||
factory = None
|
factory = None
|
||||||
|
|
||||||
|
|
||||||
class Client(metaclass=trace.TraceWrapperMetaclass):
|
class Client:
|
||||||
"""Singleton client class that constructs the aprslib connection."""
|
"""Singleton client class that constructs the aprslib connection."""
|
||||||
|
|
||||||
_instance = None
|
_instance = None
|
||||||
@@ -90,7 +90,7 @@ class Client(metaclass=trace.TraceWrapperMetaclass):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class APRSISClient(Client, metaclass=trace.TraceWrapperMetaclass):
|
class APRSISClient(Client):
|
||||||
|
|
||||||
_client = None
|
_client = None
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ class APRSISClient(Client, metaclass=trace.TraceWrapperMetaclass):
|
|||||||
return aprs_client
|
return aprs_client
|
||||||
|
|
||||||
|
|
||||||
class KISSClient(Client, metaclass=trace.TraceWrapperMetaclass):
|
class KISSClient(Client):
|
||||||
|
|
||||||
_client = None
|
_client = None
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,6 @@ class Aprsdis(aprslib.IS):
|
|||||||
self._sendall(login_str)
|
self._sendall(login_str)
|
||||||
self.sock.settimeout(5)
|
self.sock.settimeout(5)
|
||||||
test = self.sock.recv(len(login_str) + 100)
|
test = self.sock.recv(len(login_str) + 100)
|
||||||
self.logger.debug("Server: '%s'", test)
|
|
||||||
if is_py3:
|
if is_py3:
|
||||||
test = test.decode("latin-1")
|
test = test.decode("latin-1")
|
||||||
test = test.rstrip()
|
test = test.rstrip()
|
||||||
|
|||||||
+31
-2
@@ -125,8 +125,37 @@ def test_plugin(
|
|||||||
LOG.info(f"P'{plugin_path}' F'{fromcall}' C'{message}'")
|
LOG.info(f"P'{plugin_path}' F'{fromcall}' C'{message}'")
|
||||||
|
|
||||||
for x in range(number):
|
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.
|
# Plugin might have threads, so lets stop them so we can exit.
|
||||||
# obj.stop_threads()
|
# 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()
|
pm.stop()
|
||||||
|
|||||||
@@ -58,7 +58,11 @@ def fetch_stats(ctx, host, port, magic_word):
|
|||||||
with console.status(msg):
|
with console.status(msg):
|
||||||
client = rpc_client.RPCClient(host, port, magic_word)
|
client = rpc_client.RPCClient(host, port, magic_word)
|
||||||
stats = client.get_stats_dict()
|
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_title = (
|
||||||
"APRSD "
|
"APRSD "
|
||||||
f"[bold cyan]v{stats['aprsd']['version']}[/] "
|
f"[bold cyan]v{stats['aprsd']['version']}[/] "
|
||||||
|
|||||||
+87
-10
@@ -26,6 +26,7 @@ from aprsd.plugins import (
|
|||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
PYPI_URL = "https://pypi.org/search/"
|
||||||
|
|
||||||
|
|
||||||
def onerror(name):
|
def onerror(name):
|
||||||
@@ -89,18 +90,35 @@ def get_module_info(package_name, module_name, module_path):
|
|||||||
return obj_list
|
return obj_list
|
||||||
|
|
||||||
|
|
||||||
def get_installed_plugins():
|
def _get_installed_aprsd_items():
|
||||||
# installed plugins
|
# installed plugins
|
||||||
ip = {}
|
plugins = {}
|
||||||
|
extensions = {}
|
||||||
for finder, name, ispkg in pkgutil.iter_modules():
|
for finder, name, ispkg in pkgutil.iter_modules():
|
||||||
if name.startswith("aprsd_"):
|
if name.startswith("aprsd_"):
|
||||||
|
print(f"Found aprsd_ module: {name}")
|
||||||
if ispkg:
|
if ispkg:
|
||||||
module = importlib.import_module(name)
|
module = importlib.import_module(name)
|
||||||
pkgs = walk_package(module)
|
pkgs = walk_package(module)
|
||||||
for pkg in pkgs:
|
for pkg in pkgs:
|
||||||
pkg_info = get_module_info(module.__name__, pkg.name, module.__path__[0])
|
pkg_info = get_module_info(module.__name__, pkg.name, module.__path__[0])
|
||||||
ip[name] = pkg_info
|
if "plugin" in name:
|
||||||
return ip
|
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):
|
def show_built_in_plugins(console):
|
||||||
@@ -144,22 +162,27 @@ def show_built_in_plugins(console):
|
|||||||
console.print(table)
|
console.print(table)
|
||||||
|
|
||||||
|
|
||||||
def show_pypi_plugins(installed_plugins, console):
|
def _get_pypi_packages():
|
||||||
query = "aprsd"
|
query = "aprsd"
|
||||||
api_url = "https://pypi.org/search/"
|
|
||||||
snippets = []
|
snippets = []
|
||||||
s = requests.Session()
|
s = requests.Session()
|
||||||
for page in range(1, 3):
|
for page in range(1, 3):
|
||||||
params = {"q": query, "page": page}
|
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")
|
soup = BeautifulSoup(r.text, "html.parser")
|
||||||
snippets += soup.select('a[class*="snippet"]')
|
snippets += soup.select('a[class*="snippet"]')
|
||||||
if not hasattr(s, "start_url"):
|
if not hasattr(s, "start_url"):
|
||||||
s.start_url = r.url.rsplit("&page", maxsplit=1).pop(0)
|
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(
|
title = Text.assemble(
|
||||||
("Pypi.org APRSD Installable Plugin Packages\n\n", "bold magenta"),
|
("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"),
|
("'pip install ", "bold white"),
|
||||||
("<Plugin Package Name>'", "cyan"),
|
("<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("Released", style="bold green", justify="center")
|
||||||
table.add_column("Installed?", style="red", justify="center")
|
table.add_column("Installed?", style="red", justify="center")
|
||||||
for snippet in snippets:
|
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())
|
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())
|
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())
|
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("\n")
|
||||||
console.print(table)
|
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):
|
def show_installed_plugins(installed_plugins, console):
|
||||||
@@ -240,3 +303,17 @@ def list_plugins(ctx):
|
|||||||
|
|
||||||
status.update("Looking for installed APRSD plugins")
|
status.update("Looking for installed APRSD plugins")
|
||||||
show_installed_plugins(installed_plugins, console)
|
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 import packets, plugin, threads, utils
|
||||||
from aprsd.main import cli
|
from aprsd.main import cli
|
||||||
from aprsd.rpc import server as rpc_server
|
from aprsd.rpc import server as rpc_server
|
||||||
from aprsd.threads import rx
|
from aprsd.threads import registry, rx, tx
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@@ -107,6 +107,15 @@ def server(ctx, flush):
|
|||||||
process_thread.start()
|
process_thread.start()
|
||||||
|
|
||||||
packets.PacketTrack().restart()
|
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:
|
if CONF.rpc_settings.enabled:
|
||||||
rpc = rpc_server.APRSDRPCThread()
|
rpc = rpc_server.APRSDRPCThread()
|
||||||
|
|||||||
+245
-45
@@ -1,7 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from logging.handlers import RotatingFileHandler
|
import math
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
@@ -11,17 +11,20 @@ from aprslib import util as aprslib_util
|
|||||||
import click
|
import click
|
||||||
import flask
|
import flask
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask.logging import default_handler
|
|
||||||
from flask_httpauth import HTTPBasicAuth
|
from flask_httpauth import HTTPBasicAuth
|
||||||
from flask_socketio import Namespace, SocketIO
|
from flask_socketio import Namespace, SocketIO
|
||||||
|
from geopy.distance import geodesic
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from werkzeug.security import check_password_hash, generate_password_hash
|
from werkzeug.security import check_password_hash, generate_password_hash
|
||||||
import wrapt
|
import wrapt
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import cli_helper, client, conf, packets, stats, threads, utils
|
from aprsd import (
|
||||||
from aprsd.log import rich as aprsd_logging
|
cli_helper, client, packets, plugin_utils, stats, threads, utils,
|
||||||
|
)
|
||||||
|
from aprsd.log import log
|
||||||
from aprsd.main import cli
|
from aprsd.main import cli
|
||||||
|
from aprsd.threads import aprsd as aprsd_threads
|
||||||
from aprsd.threads import rx, tx
|
from aprsd.threads import rx, tx
|
||||||
from aprsd.utils import trace
|
from aprsd.utils import trace
|
||||||
|
|
||||||
@@ -32,6 +35,16 @@ auth = HTTPBasicAuth()
|
|||||||
users = {}
|
users = {}
|
||||||
socketio = None
|
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(
|
flask_app = flask.Flask(
|
||||||
"aprsd",
|
"aprsd",
|
||||||
static_url_path="/static",
|
static_url_path="/static",
|
||||||
@@ -121,8 +134,188 @@ def verify_password(username, password):
|
|||||||
return username
|
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 WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
|
||||||
"""Class that handles packets being sent to us."""
|
"""Class that handles packets being sent to us."""
|
||||||
|
|
||||||
def __init__(self, packet_queue, socketio):
|
def __init__(self, packet_queue, socketio):
|
||||||
self.socketio = socketio
|
self.socketio = socketio
|
||||||
self.connected = False
|
self.connected = False
|
||||||
@@ -132,20 +325,53 @@ class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
|
|||||||
super().process_ack_packet(packet)
|
super().process_ack_packet(packet)
|
||||||
ack_num = packet.get("msgNo")
|
ack_num = packet.get("msgNo")
|
||||||
SentMessages().ack(ack_num)
|
SentMessages().ack(ack_num)
|
||||||
self.socketio.emit(
|
msg = SentMessages().get(ack_num)
|
||||||
"ack", SentMessages().get(ack_num),
|
if msg:
|
||||||
namespace="/sendmsg",
|
self.socketio.emit(
|
||||||
)
|
"ack", msg,
|
||||||
|
namespace="/sendmsg",
|
||||||
|
)
|
||||||
self.got_ack = True
|
self.got_ack = True
|
||||||
|
|
||||||
def process_our_message_packet(self, packet: packets.MessagePacket):
|
def process_our_message_packet(self, packet: packets.MessagePacket):
|
||||||
|
global callsign_locations
|
||||||
LOG.info(f"process MessagePacket {repr(packet)}")
|
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(
|
self.socketio.emit(
|
||||||
"new", packet.__dict__,
|
"new", packet.__dict__,
|
||||||
namespace="/sendmsg",
|
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():
|
def set_config():
|
||||||
global users
|
global users
|
||||||
|
|
||||||
@@ -181,6 +407,12 @@ def _get_transport(stats):
|
|||||||
return transport, aprs_connection
|
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
|
@auth.login_required
|
||||||
@flask_app.route("/")
|
@flask_app.route("/")
|
||||||
def index():
|
def index():
|
||||||
@@ -216,7 +448,7 @@ def index():
|
|||||||
|
|
||||||
|
|
||||||
@auth.login_required
|
@auth.login_required
|
||||||
@flask_app.route("//send-message-status")
|
@flask_app.route("/send-message-status")
|
||||||
def send_message_status():
|
def send_message_status():
|
||||||
LOG.debug(request)
|
LOG.debug(request)
|
||||||
msgs = SentMessages()
|
msgs = SentMessages()
|
||||||
@@ -331,53 +563,21 @@ class SendMessageNamespace(Namespace):
|
|||||||
def handle_json(self, data):
|
def handle_json(self, data):
|
||||||
LOG.debug(f"WS json {data}")
|
LOG.debug(f"WS json {data}")
|
||||||
|
|
||||||
|
def on_get_callsign_location(self, data):
|
||||||
def setup_logging(flask_app, loglevel, quiet):
|
LOG.debug(f"on_callsign_location {data}")
|
||||||
flask_log = logging.getLogger("werkzeug")
|
populate_callsign_location(data["callsign"])
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def init_flask(loglevel, quiet):
|
def init_flask(loglevel, quiet):
|
||||||
global socketio, flask_app
|
global socketio, flask_app
|
||||||
|
|
||||||
setup_logging(flask_app, loglevel, quiet)
|
log.setup_logging(loglevel, quiet)
|
||||||
|
|
||||||
socketio = SocketIO(
|
socketio = SocketIO(
|
||||||
flask_app, logger=False, engineio_logger=False,
|
flask_app, logger=False, engineio_logger=False,
|
||||||
async_mode="threading",
|
async_mode="threading",
|
||||||
)
|
)
|
||||||
# async_mode="gevent",
|
|
||||||
# async_mode="eventlet",
|
|
||||||
# import eventlet
|
|
||||||
# eventlet.monkey_patch()
|
|
||||||
|
|
||||||
socketio.on_namespace(
|
socketio.on_namespace(
|
||||||
SendMessageNamespace(
|
SendMessageNamespace(
|
||||||
|
|||||||
+68
-1
@@ -24,6 +24,11 @@ webchat_group = cfg.OptGroup(
|
|||||||
title="Settings specific to the webchat command",
|
title="Settings specific to the webchat command",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
registry_group = cfg.OptGroup(
|
||||||
|
name="aprs_registry",
|
||||||
|
title="APRS Registry settings",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
aprsd_opts = [
|
aprsd_opts = [
|
||||||
cfg.StrOpt(
|
cfg.StrOpt(
|
||||||
@@ -67,9 +72,35 @@ aprsd_opts = [
|
|||||||
),
|
),
|
||||||
cfg.IntOpt(
|
cfg.IntOpt(
|
||||||
"packet_dupe_timeout",
|
"packet_dupe_timeout",
|
||||||
default=60,
|
default=300,
|
||||||
help="The number of seconds before a packet is not considered a duplicate.",
|
help="The number of seconds before a packet is not considered a duplicate.",
|
||||||
),
|
),
|
||||||
|
cfg.BoolOpt(
|
||||||
|
"enable_beacon",
|
||||||
|
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 = [
|
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):
|
def register_opts(config):
|
||||||
config.register_opts(aprsd_opts)
|
config.register_opts(aprsd_opts)
|
||||||
@@ -208,6 +272,8 @@ def register_opts(config):
|
|||||||
config.register_opts(rpc_opts, group=rpc_group)
|
config.register_opts(rpc_opts, group=rpc_group)
|
||||||
config.register_group(webchat_group)
|
config.register_group(webchat_group)
|
||||||
config.register_opts(webchat_opts, 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():
|
def list_opts():
|
||||||
@@ -217,4 +283,5 @@ def list_opts():
|
|||||||
watch_list_group.name: watch_list_opts,
|
watch_list_group.name: watch_list_opts,
|
||||||
rpc_group.name: rpc_opts,
|
rpc_group.name: rpc_opts,
|
||||||
webchat_group.name: webchat_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]"
|
" %(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(
|
logging_group = cfg.OptGroup(
|
||||||
name="logging",
|
name="logging",
|
||||||
title="Logging options",
|
title="Logging options",
|
||||||
)
|
)
|
||||||
logging_opts = [
|
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(
|
cfg.StrOpt(
|
||||||
"logfile",
|
"logfile",
|
||||||
default=None,
|
default=None,
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ owm_wx_group = cfg.OptGroup(
|
|||||||
title="Options for the OWMWeatherPlugin",
|
title="Options for the OWMWeatherPlugin",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
location_group = cfg.OptGroup(
|
||||||
|
name="location_plugin",
|
||||||
|
title="Options for the LocationPlugin",
|
||||||
|
)
|
||||||
|
|
||||||
aprsfi_opts = [
|
aprsfi_opts = [
|
||||||
cfg.StrOpt(
|
cfg.StrOpt(
|
||||||
"apiKey",
|
"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):
|
def register_opts(config):
|
||||||
config.register_group(aprsfi_group)
|
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_opts(owm_wx_opts, group=owm_wx_group)
|
||||||
config.register_group(avwx_group)
|
config.register_group(avwx_group)
|
||||||
config.register_opts(avwx_opts, 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():
|
def list_opts():
|
||||||
@@ -80,4 +187,5 @@ def list_opts():
|
|||||||
query_group.name: query_plugin_opts,
|
query_group.name: query_plugin_opts,
|
||||||
owm_wx_group.name: owm_wx_opts,
|
owm_wx_group.name: owm_wx_opts,
|
||||||
avwx_group.name: avwx_opts,
|
avwx_group.name: avwx_opts,
|
||||||
|
location_group.name: location_opts,
|
||||||
}
|
}
|
||||||
|
|||||||
+72
-64
@@ -1,13 +1,12 @@
|
|||||||
import logging
|
import logging
|
||||||
from logging import NullHandler
|
from logging.handlers import QueueHandler
|
||||||
from logging.handlers import RotatingFileHandler
|
|
||||||
import queue
|
import queue
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
from aprsd import conf
|
from aprsd import conf
|
||||||
from aprsd.log import rich as aprsd_logging
|
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@@ -15,75 +14,84 @@ LOG = logging.getLogger("APRSD")
|
|||||||
logging_queue = queue.Queue()
|
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
|
# Setup the log faciility
|
||||||
# to disable log to stdout, but still log to file
|
# to disable log to stdout, but still log to file
|
||||||
# use the --quiet option on the cmdln
|
# use the --quiet option on the cmdln
|
||||||
def setup_logging(loglevel, quiet):
|
def setup_logging(loglevel=None, quiet=False):
|
||||||
log_level = conf.log.LOG_LEVELS[loglevel]
|
if not loglevel:
|
||||||
LOG.setLevel(log_level)
|
log_level = CONF.logging.log_level
|
||||||
date_format = CONF.logging.date_format
|
else:
|
||||||
rh = None
|
log_level = conf.log.LOG_LEVELS[loglevel]
|
||||||
fh = None
|
|
||||||
|
|
||||||
rich_logging = False
|
# intercept everything at the root logger
|
||||||
if CONF.logging.get("rich_logging", False) and not quiet:
|
logging.root.handlers = [InterceptHandler()]
|
||||||
log_format = "%(message)s"
|
logging.root.setLevel(log_level)
|
||||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
|
||||||
rh = aprsd_logging.APRSDRichHandler(
|
imap_list = [
|
||||||
show_thread=True, thread_width=20,
|
"imapclient.imaplib", "imaplib", "imapclient",
|
||||||
rich_tracebacks=True, omit_repeated_times=False,
|
"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:
|
if CONF.email_plugin.enabled and CONF.email_plugin.debug:
|
||||||
imap_logger = logging.getLogger("imapclient.imaplib")
|
for name in imap_list:
|
||||||
imap_logger.setLevel(log_level)
|
logging.getLogger(name).propagate = True
|
||||||
if rh:
|
|
||||||
imap_logger.addHandler(rh)
|
|
||||||
if fh:
|
|
||||||
imap_logger.addHandler(fh)
|
|
||||||
|
|
||||||
if CONF.admin.web_enabled:
|
if CONF.admin.web_enabled:
|
||||||
qh = logging.handlers.QueueHandler(logging_queue)
|
qh = QueueHandler(logging_queue)
|
||||||
q_log_formatter = logging.Formatter(
|
handlers.append(
|
||||||
fmt=CONF.logging.logformat,
|
{
|
||||||
datefmt=CONF.logging.date_format,
|
"sink": qh, "serialize": False,
|
||||||
|
"format": CONF.logging.logformat,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
qh.setFormatter(q_log_formatter)
|
|
||||||
LOG.addHandler(qh)
|
|
||||||
|
|
||||||
if not quiet and not rich_logging:
|
# configure loguru
|
||||||
sh = logging.StreamHandler(sys.stdout)
|
logger.configure(handlers=handlers)
|
||||||
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)
|
|
||||||
|
|||||||
@@ -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_completion.init()
|
||||||
|
|
||||||
|
|
||||||
@click.group(context_settings=CONTEXT_SETTINGS)
|
@click.group(cls=cli_helper.AliasedGroup, context_settings=CONTEXT_SETTINGS)
|
||||||
@click.version_option()
|
@click.version_option()
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def cli(ctx):
|
def cli(ctx):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def load_commands():
|
||||||
# First import all the possible commands for the CLI
|
|
||||||
# The commands themselves live in the cmds directory
|
|
||||||
from .cmds import ( # noqa
|
from .cmds import ( # noqa
|
||||||
completion, dev, fetch_stats, healthcheck, list_plugins, listen,
|
completion, dev, fetch_stats, healthcheck, list_plugins, listen,
|
||||||
send_message, server, webchat,
|
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")
|
cli(auto_envvar_prefix="APRSD")
|
||||||
|
|
||||||
|
|
||||||
@@ -120,12 +125,7 @@ def sample_config(ctx):
|
|||||||
def get_namespaces():
|
def get_namespaces():
|
||||||
args = []
|
args = []
|
||||||
|
|
||||||
all = imp.entry_points()
|
selected = imp.entry_points(group="oslo.config.opts")
|
||||||
selected = []
|
|
||||||
if "oslo.config.opts" in all:
|
|
||||||
for x in all["oslo.config.opts"]:
|
|
||||||
if x.group == "oslo.config.opts":
|
|
||||||
selected.append(x)
|
|
||||||
for entry in selected:
|
for entry in selected:
|
||||||
if "aprsd" in entry.name:
|
if "aprsd" in entry.name:
|
||||||
args.append("--namespace")
|
args.append("--namespace")
|
||||||
@@ -145,9 +145,8 @@ def sample_config(ctx):
|
|||||||
if not sys.argv[1:]:
|
if not sys.argv[1:]:
|
||||||
raise SystemExit
|
raise SystemExit
|
||||||
raise
|
raise
|
||||||
LOG.warning(conf.namespace)
|
|
||||||
return
|
|
||||||
generator.generate(conf)
|
generator.generate(conf)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from aprsd.packets.core import ( # noqa: F401
|
from aprsd.packets.core import ( # noqa: F401
|
||||||
AckPacket, GPSPacket, MessagePacket, MicEPacket, Packet, RejectPacket,
|
AckPacket, BeaconPacket, GPSPacket, MessagePacket, MicEPacket, Packet,
|
||||||
StatusPacket, WeatherPacket,
|
RejectPacket, StatusPacket, WeatherPacket,
|
||||||
)
|
)
|
||||||
from aprsd.packets.packet_list import PacketList # noqa: F401
|
from aprsd.packets.packet_list import PacketList # noqa: F401
|
||||||
from aprsd.packets.seen_list import SeenList # 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.from_call,
|
||||||
self.payload,
|
self.payload,
|
||||||
)
|
)
|
||||||
LOG.debug(f"_build_raw: payload '{self.payload}' raw '{self.raw}'")
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def factory(raw_packet):
|
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
|
@dataclass
|
||||||
class MicEPacket(GPSPacket):
|
class MicEPacket(GPSPacket):
|
||||||
messagecapable: bool = False
|
messagecapable: bool = False
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ def fetch_openweathermap(api_key, lat, lon, units="metric", exclude=None):
|
|||||||
exclude,
|
exclude,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
LOG.debug(f"Fetching OWM weather '{url}'")
|
||||||
response = requests.get(url)
|
response = requests.get(url)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(e)
|
LOG.error(e)
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ from aprsd.utils import trace
|
|||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
DEFAULT_FORTUNE_PATH = '/usr/games/fortune'
|
||||||
|
|
||||||
|
|
||||||
class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
|
class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
|
||||||
"""Fortune."""
|
"""Fortune."""
|
||||||
@@ -19,7 +21,8 @@ class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
|
|||||||
fortune_path = None
|
fortune_path = None
|
||||||
|
|
||||||
def setup(self):
|
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:
|
if not self.fortune_path:
|
||||||
self.enabled = False
|
self.enabled = False
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import logging
|
|||||||
import re
|
import re
|
||||||
import time
|
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 oslo_config import cfg
|
||||||
|
|
||||||
from aprsd import packets, plugin, plugin_utils
|
from aprsd import packets, plugin, plugin_utils
|
||||||
@@ -13,6 +14,82 @@ CONF = cfg.CONF
|
|||||||
LOG = logging.getLogger("APRSD")
|
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):
|
class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
||||||
"""Location!"""
|
"""Location!"""
|
||||||
|
|
||||||
@@ -57,19 +134,24 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
|||||||
# Get some information about their location
|
# Get some information about their location
|
||||||
try:
|
try:
|
||||||
tic = time.perf_counter()
|
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}"
|
coordinates = f"{lat:0.6f}, {lon:0.6f}"
|
||||||
location = geolocator.reverse(coordinates)
|
location = geolocator.reverse(coordinates)
|
||||||
address = location.raw.get("address")
|
address = location.raw.get("address")
|
||||||
|
LOG.debug(f"GEOLOCATOR address: {address}")
|
||||||
toc = time.perf_counter()
|
toc = time.perf_counter()
|
||||||
if address:
|
if address:
|
||||||
LOG.info(f"Geopy address {address} took {toc - tic:0.4f}")
|
LOG.info(f"Geopy address {address} took {toc - tic:0.4f}")
|
||||||
if address.get("country_code") == "us":
|
if address.get("country_code") == "us":
|
||||||
area_info = f"{address.get('county')}, {address.get('state')}"
|
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:
|
else:
|
||||||
# what to do for address for non US?
|
area_info = str(location)
|
||||||
area_info = f"{address.get('country'), 'Unknown'}"
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
|
LOG.error(ex)
|
||||||
LOG.error(f"Failed to fetch Geopy address {ex}")
|
LOG.error(f"Failed to fetch Geopy address {ex}")
|
||||||
area_info = "Unknown Location"
|
area_info = "Unknown Location"
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
|
|||||||
"weather" - returns weather near the calling callsign
|
"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"
|
command_name = "USWeather"
|
||||||
short_description = "Provide USA only weather of GPS Beacon location"
|
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"
|
command_name = "OpenWeatherMap"
|
||||||
short_description = "OpenWeatherMap weather of GPS Beacon location"
|
short_description = "OpenWeatherMap weather of GPS Beacon location"
|
||||||
|
|
||||||
@@ -211,7 +215,7 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
|||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def process(self, packet):
|
def process(self, packet):
|
||||||
fromcall = packet.get("from")
|
fromcall = packet.get("from_call")
|
||||||
message = packet.get("message_text", None)
|
message = packet.get("message_text", None)
|
||||||
# ack = packet.get("msgNo", "0")
|
# ack = packet.get("msgNo", "0")
|
||||||
LOG.info(f"OWMWeather Plugin '{message}'")
|
LOG.info(f"OWMWeather Plugin '{message}'")
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import queue
|
|||||||
# aprsd.threads
|
# aprsd.threads
|
||||||
from .aprsd import APRSDThread, APRSDThreadList # noqa: F401
|
from .aprsd import APRSDThread, APRSDThreadList # noqa: F401
|
||||||
from .keep_alive import KeepAliveThread # 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)
|
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
|
pass
|
||||||
|
|
||||||
|
|
||||||
class APRSDPluginRXThread(APRSDRXThread):
|
class APRSDDupeRXThread(APRSDRXThread):
|
||||||
"""Process received packets.
|
"""Process received packets.
|
||||||
|
|
||||||
This is the main APRSD Server command thread that
|
This is the main APRSD Server command thread that
|
||||||
receives packets from APRIS and then sends them for
|
receives packets and makes sure the packet
|
||||||
processing in the PluginProcessPacketThread.
|
hasn't been seen previously before sending it on
|
||||||
|
to be processed.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def process_packet(self, *args, **kwargs):
|
def process_packet(self, *args, **kwargs):
|
||||||
@@ -118,6 +119,13 @@ class APRSDPluginRXThread(APRSDRXThread):
|
|||||||
self.packet_queue.put(packet)
|
self.packet_queue.put(packet)
|
||||||
|
|
||||||
|
|
||||||
|
class APRSDPluginRXThread(APRSDDupeRXThread):
|
||||||
|
""""Process received packets.
|
||||||
|
|
||||||
|
For backwards compatibility, we keep the APRSDPluginRXThread.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class APRSDProcessPacketThread(APRSDThread):
|
class APRSDProcessPacketThread(APRSDThread):
|
||||||
"""Base class for processing received packets.
|
"""Base class for processing received packets.
|
||||||
|
|
||||||
@@ -157,7 +165,7 @@ class APRSDProcessPacketThread(APRSDThread):
|
|||||||
|
|
||||||
def process_packet(self, packet):
|
def process_packet(self, packet):
|
||||||
"""Process a packet received from aprs-is server."""
|
"""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()
|
our_call = CONF.callsign.lower()
|
||||||
|
|
||||||
from_call = packet.from_call
|
from_call = packet.from_call
|
||||||
@@ -202,7 +210,7 @@ class APRSDProcessPacketThread(APRSDThread):
|
|||||||
self.process_other_packet(
|
self.process_other_packet(
|
||||||
packet, for_us=(to_call.lower() == our_call),
|
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
|
return False
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ msg_throttle_decorator = decorator.ThrottleDecorator(throttle=msg_t)
|
|||||||
ack_throttle_decorator = decorator.ThrottleDecorator(throttle=ack_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):
|
def send(packet: core.Packet, direct=False, aprs_client=None):
|
||||||
"""Send a packet either in a thread or directly to the client."""
|
"""Send a packet either in a thread or directly to the client."""
|
||||||
# prepare the packet for sending.
|
# prepare the packet for sending.
|
||||||
@@ -194,3 +195,42 @@ class SendAckThread(aprsd_threads.APRSDThread):
|
|||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
self.loop_count += 1
|
self.loop_count += 1
|
||||||
return True
|
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 os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
import update_checker
|
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:
|
if sys.version_info.major == 3 and sys.version_info.minor >= 3:
|
||||||
from collections.abc import MutableMapping
|
from collections.abc import MutableMapping
|
||||||
else:
|
else:
|
||||||
from collections import MutableMapping
|
from collections.abc import MutableMapping
|
||||||
|
|
||||||
|
|
||||||
def env(*vars, **kwargs):
|
def env(*vars, **kwargs):
|
||||||
@@ -131,3 +132,20 @@ def parse_delta_str(s):
|
|||||||
return {key: float(val) for key, val in m.groupdict().items()}
|
return {key: float(val) for key, val in m.groupdict().items()}
|
||||||
else:
|
else:
|
||||||
return {}
|
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):
|
def __len__(self):
|
||||||
return len(self.data)
|
return len(self.data)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self.data)
|
||||||
|
|
||||||
def get_all(self):
|
def get_all(self):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
return self.data
|
return self.data
|
||||||
@@ -96,11 +99,14 @@ class ObjectStoreMixin:
|
|||||||
LOG.debug(
|
LOG.debug(
|
||||||
f"{self.__class__.__name__}::Loaded {len(self)} entries from disk.",
|
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:
|
except (pickle.UnpicklingError, Exception) as ex:
|
||||||
LOG.error(f"Failed to UnPickle {self._save_filename()}")
|
LOG.error(f"Failed to UnPickle {self._save_filename()}")
|
||||||
LOG.error(ex)
|
LOG.error(ex)
|
||||||
self.data = {}
|
self.data = {}
|
||||||
|
else:
|
||||||
|
LOG.debug(f"{self.__class__.__name__}::No save file found.")
|
||||||
|
|
||||||
def flush(self):
|
def flush(self):
|
||||||
"""Nuke the old pickle file that stored the old results from last aprsd run."""
|
"""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 cleared = false;
|
||||||
var callsign_list = {};
|
var callsign_list = {};
|
||||||
|
var callsign_location = {};
|
||||||
var message_list = {};
|
var message_list = {};
|
||||||
var from_msg_list = {};
|
var from_msg_list = {};
|
||||||
var selected_tab_callsign = null;
|
var selected_tab_callsign = null;
|
||||||
@@ -9,6 +10,35 @@ MSG_TYPE_TX = "tx";
|
|||||||
MSG_TYPE_RX = "rx";
|
MSG_TYPE_RX = "rx";
|
||||||
MSG_TYPE_ACK = "ack";
|
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 size_dict(d){c=0; for (i in d) ++c; return c}
|
||||||
|
|
||||||
function raise_error(msg) {
|
function raise_error(msg) {
|
||||||
@@ -31,8 +61,6 @@ function init_chat() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
socket.on("sent", function(msg) {
|
socket.on("sent", function(msg) {
|
||||||
console.log("SENT: ");
|
|
||||||
console.log(msg);
|
|
||||||
if (cleared === false) {
|
if (cleared === false) {
|
||||||
var msgsdiv = $("#msgsTabsDiv");
|
var msgsdiv = $("#msgsTabsDiv");
|
||||||
msgsdiv.html('');
|
msgsdiv.html('');
|
||||||
@@ -57,6 +85,20 @@ function init_chat() {
|
|||||||
from_msg(msg);
|
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) {
|
$("#sendform").submit(function(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
to_call = $('#to_call').val();
|
to_call = $('#to_call').val();
|
||||||
@@ -71,7 +113,7 @@ function init_chat() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
msg = {'to': to_call, 'message': message, 'path': path};
|
msg = {'to': to_call, 'message': message, 'path': path};
|
||||||
console.log(msg);
|
//console.log(msg);
|
||||||
socket.emit("send", msg);
|
socket.emit("send", msg);
|
||||||
$('#message').val('');
|
$('#message').val('');
|
||||||
}
|
}
|
||||||
@@ -82,6 +124,7 @@ function init_chat() {
|
|||||||
init_messages();
|
init_messages();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function tab_string(callsign, id=false) {
|
function tab_string(callsign, id=false) {
|
||||||
name = "msgs"+callsign;
|
name = "msgs"+callsign;
|
||||||
if (id) {
|
if (id) {
|
||||||
@@ -121,6 +164,14 @@ function callsign_tab(callsign) {
|
|||||||
return "#"+tab_string(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) {
|
function bubble_msg_id(msg, id=false) {
|
||||||
// The id of the div that contains a specific message
|
// The id of the div that contains a specific message
|
||||||
name = msg["from_call"] + "_" + msg["msgNo"];
|
name = msg["from_call"] + "_" + msg["msgNo"];
|
||||||
@@ -155,20 +206,26 @@ function save_data() {
|
|||||||
// Save the relevant data to local storage
|
// Save the relevant data to local storage
|
||||||
localStorage.setItem('callsign_list', JSON.stringify(callsign_list));
|
localStorage.setItem('callsign_list', JSON.stringify(callsign_list));
|
||||||
localStorage.setItem('message_list', JSON.stringify(message_list));
|
localStorage.setItem('message_list', JSON.stringify(message_list));
|
||||||
|
localStorage.setItem('callsign_location', JSON.stringify(callsign_location));
|
||||||
}
|
}
|
||||||
|
|
||||||
function init_messages() {
|
function init_messages() {
|
||||||
// This tries to load any previous conversations from local storage
|
// This tries to load any previous conversations from local storage
|
||||||
callsign_list = JSON.parse(localStorage.getItem('callsign_list'));
|
callsign_list = JSON.parse(localStorage.getItem('callsign_list'));
|
||||||
message_list = JSON.parse(localStorage.getItem('message_list'));
|
message_list = JSON.parse(localStorage.getItem('message_list'));
|
||||||
|
callsign_location = JSON.parse(localStorage.getItem('callsign_location'));
|
||||||
if (callsign_list == null) {
|
if (callsign_list == null) {
|
||||||
callsign_list = {};
|
callsign_list = {};
|
||||||
}
|
}
|
||||||
if (message_list == null) {
|
if (message_list == null) {
|
||||||
message_list = {};
|
message_list = {};
|
||||||
}
|
}
|
||||||
//console.log(callsign_list);
|
if (callsign_location == null) {
|
||||||
//console.log(message_list);
|
callsign_location = {};
|
||||||
|
}
|
||||||
|
console.log(callsign_list);
|
||||||
|
console.log(message_list);
|
||||||
|
console.log(callsign_location);
|
||||||
|
|
||||||
// Now loop through each callsign and add the tabs
|
// Now loop through each callsign and add the tabs
|
||||||
first_callsign = null;
|
first_callsign = null;
|
||||||
@@ -245,6 +302,7 @@ function create_callsign_tab(callsign, active=false) {
|
|||||||
tab_id_li = tab_li_string(callsign);
|
tab_id_li = tab_li_string(callsign);
|
||||||
tab_notify_id = tab_notification_id(callsign);
|
tab_notify_id = tab_notification_id(callsign);
|
||||||
tab_content = tab_content_name(callsign);
|
tab_content = tab_content_name(callsign);
|
||||||
|
popover_id = callsign_location_popover(callsign);
|
||||||
if (active) {
|
if (active) {
|
||||||
active_str = "active";
|
active_str = "active";
|
||||||
} else {
|
} 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 '+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 += '<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 += 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 += '<span onclick="delete_tab(\''+callsign+'\');">×</span>';
|
||||||
item_html += '</button></li>'
|
item_html += '</button></li>'
|
||||||
|
|
||||||
callsignTabs.append(item_html);
|
callsignTabs.append(item_html);
|
||||||
create_callsign_tab_content(callsign, active);
|
create_callsign_tab_content(callsign, active);
|
||||||
}
|
}
|
||||||
@@ -273,7 +331,22 @@ function create_callsign_tab_content(callsign, active=false) {
|
|||||||
active_str = '';
|
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="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 class="speech-wrapper" id="'+wrapper_id+'"></div>';
|
||||||
item_html += '</div>';
|
item_html += '</div>';
|
||||||
callsignTabsContent.append(item_html);
|
callsignTabsContent.append(item_html);
|
||||||
@@ -288,6 +361,7 @@ function delete_tab(callsign) {
|
|||||||
$(tab_content).remove();
|
$(tab_content).remove();
|
||||||
delete callsign_list[callsign];
|
delete callsign_list[callsign];
|
||||||
delete message_list[callsign];
|
delete message_list[callsign];
|
||||||
|
delete callsign_location[callsign];
|
||||||
|
|
||||||
// Now select the first tab
|
// Now select the first tab
|
||||||
first_tab = $("#msgsTabList").children().first().children().first();
|
first_tab = $("#msgsTabList").children().first().children().first();
|
||||||
@@ -312,10 +386,9 @@ function add_callsign(callsign, msg) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function update_callsign_path(callsign, path) {
|
function update_callsign_path(callsign, msg) {
|
||||||
//Get the selected path to save for this callsign
|
//Get the selected path to save for this callsign
|
||||||
path = msg['path']
|
path = msg['path']
|
||||||
console.log("Path is " + path);
|
|
||||||
$('#pkt_path').val(path);
|
$('#pkt_path').val(path);
|
||||||
callsign_list[callsign] = path;
|
callsign_list[callsign] = path;
|
||||||
|
|
||||||
@@ -324,7 +397,6 @@ function update_callsign_path(callsign, path) {
|
|||||||
function append_message(callsign, msg, msg_html) {
|
function append_message(callsign, msg, msg_html) {
|
||||||
new_callsign = false
|
new_callsign = false
|
||||||
if (!message_list.hasOwnProperty(callsign)) {
|
if (!message_list.hasOwnProperty(callsign)) {
|
||||||
//message_list[callsign] = new Array();
|
|
||||||
message_list[callsign] = {};
|
message_list[callsign] = {};
|
||||||
}
|
}
|
||||||
ts_id = message_ts_id(msg);
|
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
|
// Find the right div to place the html
|
||||||
|
|
||||||
new_callsign = add_callsign(callsign, msg);
|
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);
|
append_message_html(callsign, msg_html, new_callsign);
|
||||||
if (new_callsign) {
|
len = Object.keys(callsign_list).length;
|
||||||
//Now click the tab
|
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 = callsign_tab(callsign);
|
||||||
$(callsign_tab_id).click();
|
$(callsign_tab_id).click();
|
||||||
callsign_select(callsign);
|
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;
|
date_str = date + " " + time;
|
||||||
sane_date_str = date_str.replace(/ /g,"").replaceAll("/","").replaceAll(":","");
|
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 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 += '<div class="bubble-text">';
|
||||||
msg_html += '<p class="'+ bubble_name_class +'">'+from+' ';
|
msg_html += '<p class="'+ bubble_name_class +'">'+from+' ';
|
||||||
msg_html += '<span class="bubble-timestamp">'+date_str+'</span>';
|
msg_html += '<span class="bubble-timestamp">'+date_str+'</span>';
|
||||||
|
|
||||||
if (ack_id) {
|
if (ack_id) {
|
||||||
if (acked) {
|
if (acked) {
|
||||||
msg_html += '<span class="material-symbols-rounded md-10" id="' + ack_id + '">thumb_up</span>';
|
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>";
|
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 += '<p class="' +bubble_msg_class+ '">'+message+'</p>';
|
||||||
msg_html += '<div class="'+ bubble_arrow_class + '"></div>';
|
msg_html += '<div class="'+ bubble_arrow_class + '"></div>';
|
||||||
msg_html += "</div></div></div>";
|
msg_html += "</div></div></div>";
|
||||||
|
|
||||||
popover_html = '\n<script>$(function () {$(\'[data-bs-toggle="popover"]\').popover('
|
return msg_html
|
||||||
popover_html += '{title: "APRS Raw Packet", html: false, trigger: \'hover\', placement: \''+popover_placement+'\'});})';
|
|
||||||
popover_html += '</script>'
|
|
||||||
|
|
||||||
return msg_html+popover_html
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function flash_message(msg) {
|
function flash_message(msg) {
|
||||||
@@ -440,12 +512,11 @@ function from_msg(msg) {
|
|||||||
|
|
||||||
if (msg["msgNo"] in from_msg_list[msg["from_call"]]) {
|
if (msg["msgNo"] in from_msg_list[msg["from_call"]]) {
|
||||||
// We already have this message
|
// 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?
|
// Do some flashy thing?
|
||||||
flash_message(msg);
|
flash_message(msg);
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
console.log("Adding message " + msg["msgNo"] + " to " + msg["from_call"]);
|
|
||||||
from_msg_list[msg["from_call"]][msg["msgNo"]] = msg
|
from_msg_list[msg["from_call"]][msg["msgNo"]] = msg
|
||||||
}
|
}
|
||||||
info = time_ack_from_msg(msg);
|
info = time_ack_from_msg(msg);
|
||||||
@@ -502,3 +573,10 @@ function callsign_select(callsign) {
|
|||||||
// Now update the path
|
// Now update the path
|
||||||
$('#pkt_path').val(callsign_list[callsign]);
|
$('#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);
|
to_call.val(callsign);
|
||||||
selected_tab_callsign = callsign;
|
selected_tab_callsign = callsign;
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
/*$('[data-bs-toggle="popover"]').popover(
|
||||||
|
{html: true, animation: true}
|
||||||
|
);*/
|
||||||
|
reload_popovers();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -95,7 +99,7 @@
|
|||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<label for="pkt_path" class="visually-hidden">PATH</label>
|
<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;">
|
<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">WIDE1-1</option>
|
||||||
<option value="WIDE1-1,WIDE2-1">WIDE1-1,WIDE2-1</option>
|
<option value="WIDE1-1,WIDE2-1">WIDE1-1,WIDE2-1</option>
|
||||||
<option value="ARISS">ARISS</option>
|
<option value="ARISS">ARISS</option>
|
||||||
|
|||||||
+4
-39
@@ -3,12 +3,10 @@ import importlib.metadata as imp
|
|||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from logging.handlers import RotatingFileHandler
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask.logging import default_handler
|
|
||||||
from flask_httpauth import HTTPBasicAuth
|
from flask_httpauth import HTTPBasicAuth
|
||||||
from oslo_config import cfg, generator
|
from oslo_config import cfg, generator
|
||||||
import socketio
|
import socketio
|
||||||
@@ -16,7 +14,7 @@ from werkzeug.security import check_password_hash
|
|||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import cli_helper, client, conf, packets, plugin, threads
|
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
|
from aprsd.rpc import client as aprsd_rpc_client
|
||||||
|
|
||||||
|
|
||||||
@@ -314,39 +312,6 @@ class LoggingNamespace(socketio.Namespace):
|
|||||||
self.log_thread.stop()
|
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):
|
def init_app(config_file=None, log_level=None):
|
||||||
default_config_file = cli_helper.DEFAULT_CONFIG_FILE
|
default_config_file = cli_helper.DEFAULT_CONFIG_FILE
|
||||||
if not config_file:
|
if not config_file:
|
||||||
@@ -368,7 +333,7 @@ if __name__ == "__main__":
|
|||||||
sio = socketio.Server(logger=True, async_mode=async_mode)
|
sio = socketio.Server(logger=True, async_mode=async_mode)
|
||||||
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
|
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
|
||||||
log_level = init_app(log_level="DEBUG")
|
log_level = init_app(log_level="DEBUG")
|
||||||
setup_logging(app, log_level)
|
log.setup_logging(app, log_level)
|
||||||
sio.register_namespace(LoggingNamespace("/logs"))
|
sio.register_namespace(LoggingNamespace("/logs"))
|
||||||
CONF.log_opt_values(LOG, logging.DEBUG)
|
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||||
app.run(
|
app.run(
|
||||||
@@ -392,7 +357,7 @@ if __name__ == "uwsgi_file_aprsd_wsgi":
|
|||||||
# Commented out for local development.
|
# Commented out for local development.
|
||||||
# config_file=cli_helper.DEFAULT_CONFIG_FILE
|
# config_file=cli_helper.DEFAULT_CONFIG_FILE
|
||||||
)
|
)
|
||||||
setup_logging(app, log_level)
|
log.setup_logging(app, log_level)
|
||||||
sio.register_namespace(LoggingNamespace("/logs"))
|
sio.register_namespace(LoggingNamespace("/logs"))
|
||||||
CONF.log_opt_values(LOG, logging.DEBUG)
|
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||||
|
|
||||||
@@ -410,6 +375,6 @@ if __name__ == "aprsd.wsgi":
|
|||||||
config_file="/config/aprsd.conf",
|
config_file="/config/aprsd.conf",
|
||||||
# config_file=cli_helper.DEFAULT_CONFIG_FILE,
|
# config_file=cli_helper.DEFAULT_CONFIG_FILE,
|
||||||
)
|
)
|
||||||
setup_logging(app, log_level)
|
log.setup_logging(app, log_level)
|
||||||
sio.register_namespace(LoggingNamespace("/logs"))
|
sio.register_namespace(LoggingNamespace("/logs"))
|
||||||
CONF.log_opt_values(LOG, logging.DEBUG)
|
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||||
|
|||||||
+46
-50
@@ -5,83 +5,79 @@
|
|||||||
# pip-compile --annotation-style=line dev-requirements.in
|
# pip-compile --annotation-style=line dev-requirements.in
|
||||||
#
|
#
|
||||||
add-trailing-comma==3.1.0 # via gray
|
add-trailing-comma==3.1.0 # via gray
|
||||||
alabaster==0.7.13 # via sphinx
|
alabaster==0.7.16 # via sphinx
|
||||||
attrs==23.1.0 # via jsonschema, referencing
|
|
||||||
autoflake==1.5.3 # via gray
|
autoflake==1.5.3 # via gray
|
||||||
babel==2.13.1 # via sphinx
|
babel==2.14.0 # via sphinx
|
||||||
black==23.11.0 # via gray
|
black==24.2.0 # via gray
|
||||||
build==1.0.3 # via pip-tools
|
build==1.1.1 # via pip-tools
|
||||||
cachetools==5.3.2 # via tox
|
cachetools==5.3.3 # via tox
|
||||||
certifi==2023.7.22 # via requests
|
certifi==2024.2.2 # via requests
|
||||||
cfgv==3.4.0 # via pre-commit
|
cfgv==3.4.0 # via pre-commit
|
||||||
chardet==5.2.0 # via tox
|
chardet==5.2.0 # via tox
|
||||||
charset-normalizer==3.3.2 # via requests
|
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
|
colorama==0.4.6 # via tox
|
||||||
commonmark==0.9.1 # via rich
|
commonmark==0.9.1 # via rich
|
||||||
configargparse==1.7 # via gray
|
configargparse==1.7 # via gray
|
||||||
coverage[toml]==7.3.2 # via coverage, pytest-cov
|
coverage[toml]==7.4.3 # via pytest-cov
|
||||||
distlib==0.3.7 # via virtualenv
|
distlib==0.3.8 # via virtualenv
|
||||||
docutils==0.20.1 # via sphinx
|
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
|
filelock==3.13.1 # via tox, virtualenv
|
||||||
fixit==0.1.4 # via gray
|
fixit==2.1.0 # via gray
|
||||||
flake8==6.1.0 # via -r dev-requirements.in, fixit, pep8-naming
|
flake8==7.0.0 # via -r dev-requirements.in, pep8-naming
|
||||||
gray==0.13.0 # via -r dev-requirements.in
|
gray==0.14.0 # via -r dev-requirements.in
|
||||||
identify==2.5.31 # via pre-commit
|
identify==2.5.35 # via pre-commit
|
||||||
idna==3.4 # via requests
|
idna==3.6 # via requests
|
||||||
imagesize==1.4.1 # via sphinx
|
imagesize==1.4.1 # via sphinx
|
||||||
importlib-resources==6.1.1 # via fixit
|
|
||||||
iniconfig==2.0.0 # via pytest
|
iniconfig==2.0.0 # via pytest
|
||||||
isort==5.12.0 # via -r dev-requirements.in, gray
|
isort==5.13.2 # via -r dev-requirements.in, gray
|
||||||
jinja2==3.1.2 # via sphinx
|
jinja2==3.1.3 # via sphinx
|
||||||
jsonschema==4.20.0 # via fixit
|
libcst==1.2.0 # via fixit
|
||||||
jsonschema-specifications==2023.11.1 # via jsonschema
|
markupsafe==2.1.5 # via jinja2
|
||||||
libcst==1.1.0 # via fixit
|
|
||||||
markupsafe==2.1.3 # via jinja2
|
|
||||||
mccabe==0.7.0 # via flake8
|
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
|
mypy-extensions==1.0.0 # via black, mypy, typing-inspect
|
||||||
nodeenv==1.8.0 # via pre-commit
|
nodeenv==1.8.0 # via pre-commit
|
||||||
packaging==23.2 # via black, build, pyproject-api, pytest, sphinx, tox
|
packaging==23.2 # via black, build, fixit, pyproject-api, pytest, sphinx, tox
|
||||||
pathspec==0.11.2 # via black
|
pathspec==0.12.1 # via black, trailrunner
|
||||||
pep8-naming==0.13.3 # via -r dev-requirements.in
|
pep8-naming==0.13.3 # via -r dev-requirements.in
|
||||||
pip-tools==7.3.0 # via -r dev-requirements.in
|
pip-tools==7.4.1 # via -r dev-requirements.in
|
||||||
platformdirs==3.11.0 # via black, tox, virtualenv
|
platformdirs==4.2.0 # via black, tox, virtualenv
|
||||||
pluggy==1.3.0 # via pytest, tox
|
pluggy==1.4.0 # via pytest, tox
|
||||||
pre-commit==3.5.0 # via -r dev-requirements.in
|
pre-commit==3.6.2 # via -r dev-requirements.in
|
||||||
pycodestyle==2.11.1 # via flake8
|
pycodestyle==2.11.1 # via flake8
|
||||||
pyflakes==3.1.0 # via autoflake, flake8
|
pyflakes==3.2.0 # via autoflake, flake8
|
||||||
pygments==2.16.1 # via rich, sphinx
|
pygments==2.17.2 # via rich, sphinx
|
||||||
pyproject-api==1.6.1 # via tox
|
pyproject-api==1.6.1 # via tox
|
||||||
pyproject-hooks==1.0.0 # via build
|
pyproject-hooks==1.0.0 # via build, pip-tools
|
||||||
pytest==7.4.3 # via -r dev-requirements.in, pytest-cov
|
pytest==8.0.2 # via -r dev-requirements.in, pytest-cov
|
||||||
pytest-cov==4.1.0 # via -r dev-requirements.in
|
pytest-cov==4.1.0 # via -r dev-requirements.in
|
||||||
pyupgrade==3.15.0 # via gray
|
pyupgrade==3.15.1 # via gray
|
||||||
pyyaml==6.0.1 # via fixit, libcst, pre-commit
|
pyyaml==6.0.1 # via libcst, pre-commit
|
||||||
referencing==0.31.0 # via jsonschema, jsonschema-specifications
|
|
||||||
requests==2.31.0 # via sphinx
|
requests==2.31.0 # via sphinx
|
||||||
rich==12.6.0 # via gray
|
rich==12.6.0 # via gray
|
||||||
rpds-py==0.13.0 # via jsonschema, referencing
|
|
||||||
snowballstemmer==2.2.0 # via sphinx
|
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
|
sphinx==7.2.6 # via -r dev-requirements.in
|
||||||
sphinxcontrib-applehelp==1.0.7 # via sphinx
|
sphinxcontrib-applehelp==1.0.8 # via sphinx
|
||||||
sphinxcontrib-devhelp==1.0.5 # via sphinx
|
sphinxcontrib-devhelp==1.0.6 # via sphinx
|
||||||
sphinxcontrib-htmlhelp==2.0.4 # via sphinx
|
sphinxcontrib-htmlhelp==2.0.5 # via sphinx
|
||||||
sphinxcontrib-jsmath==1.0.1 # via sphinx
|
sphinxcontrib-jsmath==1.0.1 # via sphinx
|
||||||
sphinxcontrib-qthelp==1.0.6 # via sphinx
|
sphinxcontrib-qthelp==1.0.7 # via sphinx
|
||||||
sphinxcontrib-serializinghtml==1.1.9 # via sphinx
|
sphinxcontrib-serializinghtml==1.1.10 # via sphinx
|
||||||
tokenize-rt==5.2.0 # via add-trailing-comma, pyupgrade
|
tokenize-rt==5.2.0 # via add-trailing-comma, pyupgrade
|
||||||
toml==0.10.2 # via autoflake
|
toml==0.10.2 # via autoflake
|
||||||
tomli==2.0.1 # via black, build, coverage, mypy, pip-tools, pyproject-api, pyproject-hooks, pytest, tox
|
tomli==2.0.1 # via black, build, coverage, fixit, mypy, pip-tools, pyproject-api, pyproject-hooks, pytest, tox
|
||||||
tox==4.11.3 # via -r dev-requirements.in
|
tox==4.14.0 # via -r dev-requirements.in
|
||||||
typing-extensions==4.8.0 # via black, libcst, mypy, typing-inspect
|
trailrunner==1.4.0 # via fixit
|
||||||
|
typing-extensions==4.10.0 # via black, libcst, mypy, typing-inspect
|
||||||
typing-inspect==0.9.0 # via libcst
|
typing-inspect==0.9.0 # via libcst
|
||||||
unify==0.5 # via gray
|
unify==0.5 # via gray
|
||||||
untokenize==0.1.1 # via unify
|
untokenize==0.1.1 # via unify
|
||||||
urllib3==2.1.0 # via requests
|
urllib3==2.2.1 # via requests
|
||||||
virtualenv==20.24.6 # via pre-commit, tox
|
virtualenv==20.25.1 # via pre-commit, tox
|
||||||
wheel==0.41.3 # via pip-tools
|
wheel==0.42.0 # via pip-tools
|
||||||
|
|
||||||
# The following packages are considered to be unsafe in a requirements file:
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
# pip
|
# pip
|
||||||
|
|||||||
+2
-1
@@ -23,7 +23,7 @@ RUN set -ex \
|
|||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get upgrade -y \
|
&& apt-get upgrade -y \
|
||||||
&& apt-get install -y git build-essential curl libffi-dev \
|
&& 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
|
# Install dependencies
|
||||||
# Clean up
|
# Clean up
|
||||||
&& apt-get autoremove -y \
|
&& apt-get autoremove -y \
|
||||||
@@ -41,6 +41,7 @@ RUN mkdir /config
|
|||||||
RUN chown -R appuser:appgroup /app
|
RUN chown -R appuser:appgroup /app
|
||||||
RUN chown -R appuser:appgroup /config
|
RUN chown -R appuser:appgroup /config
|
||||||
USER appuser
|
USER appuser
|
||||||
|
RUN echo "PATH=\$PATH:/usr/games" >> /app/.bashrc
|
||||||
RUN which aprsd
|
RUN which aprsd
|
||||||
RUN aprsd sample-config > /config/aprsd.conf
|
RUN aprsd sample-config > /config/aprsd.conf
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ RUN set -ex \
|
|||||||
# Upgrade the package index and install security upgrades
|
# Upgrade the package index and install security upgrades
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get upgrade -y \
|
&& 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 \
|
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo \
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
# Clean up
|
# Clean up
|
||||||
@@ -40,6 +40,7 @@ RUN mkdir /config
|
|||||||
RUN chown -R appuser:appgroup /app
|
RUN chown -R appuser:appgroup /app
|
||||||
RUN chown -R appuser:appgroup /config
|
RUN chown -R appuser:appgroup /config
|
||||||
USER appuser
|
USER appuser
|
||||||
|
RUN echo "PATH=\$PATH:/usr/games" >> /app/.bashrc
|
||||||
RUN which aprsd
|
RUN which aprsd
|
||||||
RUN aprsd sample-config > /config/aprsd.conf
|
RUN aprsd sample-config > /config/aprsd.conf
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,17 @@ if [ ! -z "${APRSD_PLUGINS}" ]; then
|
|||||||
pip3 install --user $plugin
|
pip3 install --user $plugin
|
||||||
done
|
done
|
||||||
fi
|
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
|
if [ -z "${LOG_LEVEL}" ] || [[ ! "${LOG_LEVEL}" =~ ^(CRITICAL|ERROR|WARNING|INFO)$ ]]; then
|
||||||
LOG_LEVEL="DEBUG"
|
LOG_LEVEL="DEBUG"
|
||||||
|
|||||||
@@ -12,6 +12,17 @@ if [ ! -z "${APRSD_PLUGINS}" ]; then
|
|||||||
pip3 install --user $plugin
|
pip3 install --user $plugin
|
||||||
done
|
done
|
||||||
fi
|
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
|
if [ -z "${LOG_LEVEL}" ] || [[ ! "${LOG_LEVEL}" =~ ^(CRITICAL|ERROR|WARNING|INFO)$ ]]; then
|
||||||
LOG_LEVEL="DEBUG"
|
LOG_LEVEL="DEBUG"
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ if [ ! -z "${APRSD_PLUGINS}" ]; then
|
|||||||
done
|
done
|
||||||
fi
|
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
|
if [ -z "${LOG_LEVEL}" ] || [[ ! "${LOG_LEVEL}" =~ ^(CRITICAL|ERROR|WARNING|INFO)$ ]]; then
|
||||||
LOG_LEVEL="DEBUG"
|
LOG_LEVEL="DEBUG"
|
||||||
fi
|
fi
|
||||||
|
|||||||
+3
-2
@@ -20,7 +20,7 @@ gevent
|
|||||||
eventlet
|
eventlet
|
||||||
tabulate
|
tabulate
|
||||||
# Pinned due to gray needing 12.6.0
|
# Pinned due to gray needing 12.6.0
|
||||||
rich==12.6.0
|
rich~=12.6.0
|
||||||
# For the list-plugins pypi.org search scraping
|
# For the list-plugins pypi.org search scraping
|
||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
wrapt
|
wrapt
|
||||||
@@ -30,10 +30,11 @@ attrs
|
|||||||
dataclasses
|
dataclasses
|
||||||
dacite2
|
dacite2
|
||||||
oslo.config
|
oslo.config
|
||||||
rpyc
|
rpyc>=6.0.0
|
||||||
# Pin this here so it doesn't require a compile on
|
# Pin this here so it doesn't require a compile on
|
||||||
# raspi
|
# raspi
|
||||||
shellingham
|
shellingham
|
||||||
geopy
|
geopy
|
||||||
rush
|
rush
|
||||||
dataclasses-json
|
dataclasses-json
|
||||||
|
loguru
|
||||||
|
|||||||
+38
-37
@@ -5,79 +5,80 @@
|
|||||||
# pip-compile --annotation-style=line requirements.in
|
# pip-compile --annotation-style=line requirements.in
|
||||||
#
|
#
|
||||||
aprslib==0.7.2 # via -r 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
|
ax253==0.1.5.post1 # via kiss3
|
||||||
beautifulsoup4==4.12.2 # via -r requirements.in
|
beautifulsoup4==4.12.3 # via -r requirements.in
|
||||||
bidict==0.22.1 # via python-socketio
|
bidict==0.23.1 # via python-socketio
|
||||||
bitarray==2.8.3 # via ax253, kiss3
|
bitarray==2.9.2 # via ax253, kiss3
|
||||||
blinker==1.7.0 # via flask
|
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
|
charset-normalizer==3.3.2 # via requests
|
||||||
click==8.1.7 # via -r requirements.in, click-completion, click-params, flask
|
click==8.1.7 # via -r requirements.in, click-completion, click-params, flask
|
||||||
click-completion==0.5.2 # via -r requirements.in
|
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
|
commonmark==0.9.1 # via rich
|
||||||
dacite2==2.0.0 # via -r requirements.in
|
dacite2==2.0.0 # via -r requirements.in
|
||||||
dataclasses==0.6 # via -r requirements.in
|
dataclasses==0.6 # via -r requirements.in
|
||||||
dataclasses-json==0.6.2 # via -r requirements.in
|
dataclasses-json==0.6.4 # via -r requirements.in
|
||||||
debtcollector==2.5.0 # via oslo-config
|
debtcollector==3.0.0 # via oslo-config
|
||||||
decorator==5.1.1 # via validators
|
deprecated==1.2.14 # via click-params
|
||||||
dnspython==2.4.2 # via eventlet
|
dnspython==2.6.1 # via eventlet
|
||||||
eventlet==0.33.3 # via -r requirements.in
|
eventlet==0.35.2 # via -r requirements.in
|
||||||
flask==3.0.0 # via -r requirements.in, flask-httpauth, flask-socketio
|
flask==3.0.2 # via -r requirements.in, flask-httpauth, flask-socketio
|
||||||
flask-httpauth==4.8.0 # via -r requirements.in
|
flask-httpauth==4.8.0 # via -r requirements.in
|
||||||
flask-socketio==5.3.6 # via -r requirements.in
|
flask-socketio==5.3.6 # via -r requirements.in
|
||||||
geographiclib==2.0 # via geopy
|
geographiclib==2.0 # via geopy
|
||||||
geopy==2.4.0 # via -r requirements.in
|
geopy==2.4.1 # via -r requirements.in
|
||||||
gevent==23.9.1 # via -r requirements.in
|
gevent==24.2.1 # via -r requirements.in
|
||||||
greenlet==3.0.1 # via eventlet, gevent
|
greenlet==3.0.3 # via eventlet, gevent
|
||||||
h11==0.14.0 # via wsproto
|
h11==0.14.0 # via wsproto
|
||||||
idna==3.4 # via requests
|
idna==3.6 # via requests
|
||||||
imapclient==3.0.0 # via -r requirements.in
|
imapclient==3.0.1 # via -r requirements.in
|
||||||
importlib-metadata==6.8.0 # via ax253, kiss3
|
importlib-metadata==7.0.1 # via ax253, kiss3
|
||||||
itsdangerous==2.1.2 # via flask
|
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
|
kiss3==8.0.0 # via -r requirements.in
|
||||||
markupsafe==2.1.3 # via jinja2, werkzeug
|
loguru==0.7.2 # via -r requirements.in
|
||||||
marshmallow==3.20.1 # via dataclasses-json
|
markupsafe==2.1.5 # via jinja2, werkzeug
|
||||||
|
marshmallow==3.21.1 # via dataclasses-json
|
||||||
mypy-extensions==1.0.0 # via typing-inspect
|
mypy-extensions==1.0.0 # via typing-inspect
|
||||||
netaddr==0.9.0 # via oslo-config
|
netaddr==1.2.1 # via oslo-config
|
||||||
oslo-config==9.2.0 # via -r requirements.in
|
oslo-config==9.4.0 # via -r requirements.in
|
||||||
oslo-i18n==6.2.0 # via oslo-config
|
oslo-i18n==6.3.0 # via oslo-config
|
||||||
packaging==23.2 # via marshmallow
|
packaging==23.2 # via marshmallow
|
||||||
pbr==6.0.0 # via -r requirements.in, oslo-i18n, stevedore
|
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
|
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==3.5 # via pyserial-asyncio
|
||||||
pyserial-asyncio==0.6 # via kiss3
|
pyserial-asyncio==0.6 # via kiss3
|
||||||
python-engineio==4.8.0 # via python-socketio
|
python-engineio==4.9.0 # via python-socketio
|
||||||
python-socketio==5.10.0 # via -r requirements.in, flask-socketio
|
python-socketio==5.11.1 # via -r requirements.in, flask-socketio
|
||||||
pytz==2023.3.post1 # via -r requirements.in
|
pytz==2024.1 # via -r requirements.in
|
||||||
pyyaml==6.0.1 # via -r requirements.in, oslo-config
|
pyyaml==6.0.1 # via -r requirements.in, oslo-config
|
||||||
requests==2.31.0 # via -r requirements.in, oslo-config, update-checker
|
requests==2.31.0 # via -r requirements.in, oslo-config, update-checker
|
||||||
rfc3986==2.0.0 # via oslo-config
|
rfc3986==2.0.0 # via oslo-config
|
||||||
rich==12.6.0 # via -r requirements.in
|
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
|
rush==2021.4.0 # via -r requirements.in
|
||||||
shellingham==1.5.4 # via -r requirements.in, click-completion
|
shellingham==1.5.4 # via -r requirements.in, click-completion
|
||||||
simple-websocket==1.0.0 # via python-engineio
|
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
|
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
|
tabulate==0.9.0 # via -r requirements.in
|
||||||
thesmuggler==1.0.1 # 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
|
typing-inspect==0.9.0 # via dataclasses-json
|
||||||
update-checker==0.18.0 # via -r requirements.in
|
update-checker==0.18.0 # via -r requirements.in
|
||||||
urllib3==2.1.0 # via requests
|
urllib3==2.2.1 # via requests
|
||||||
validators==0.20.0 # via click-params
|
validators==0.22.0 # via click-params
|
||||||
werkzeug==3.0.1 # via -r requirements.in, flask
|
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
|
wsproto==1.2.0 # via simple-websocket
|
||||||
zipp==3.17.0 # via importlib-metadata
|
zipp==3.17.0 # via importlib-metadata
|
||||||
zope-event==5.0 # via gevent
|
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:
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
# setuptools
|
# setuptools
|
||||||
|
|||||||
@@ -56,17 +56,20 @@ class TestSendMessageCommand(unittest.TestCase):
|
|||||||
msg_number=1,
|
msg_number=1,
|
||||||
message_format=core.PACKET_TYPE_ACK,
|
message_format=core.PACKET_TYPE_ACK,
|
||||||
)
|
)
|
||||||
|
mock_queue = mock.MagicMock()
|
||||||
socketio = mock.MagicMock()
|
socketio = mock.MagicMock()
|
||||||
wcp = webchat.WebChatProcessPacketThread(packet, socketio)
|
wcp = webchat.WebChatProcessPacketThread(mock_queue, socketio)
|
||||||
|
|
||||||
wcp.process_ack_packet(packet)
|
wcp.process_ack_packet(packet)
|
||||||
mock_remove.called_once()
|
mock_remove.called_once()
|
||||||
mock_socketio.called_once()
|
mock_socketio.called_once()
|
||||||
|
|
||||||
|
@mock.patch("aprsd.threads.tx.send")
|
||||||
@mock.patch("aprsd.packets.PacketList.rx")
|
@mock.patch("aprsd.packets.PacketList.rx")
|
||||||
@mock.patch("aprsd.cmds.webchat.socketio")
|
@mock.patch("aprsd.cmds.webchat.socketio")
|
||||||
def test_process_our_message_packet(
|
def test_process_our_message_packet(
|
||||||
self,
|
self,
|
||||||
|
mock_tx_send,
|
||||||
mock_packet_add,
|
mock_packet_add,
|
||||||
mock_socketio,
|
mock_socketio,
|
||||||
):
|
):
|
||||||
@@ -77,8 +80,9 @@ class TestSendMessageCommand(unittest.TestCase):
|
|||||||
msg_number=1,
|
msg_number=1,
|
||||||
message_format=core.PACKET_TYPE_MESSAGE,
|
message_format=core.PACKET_TYPE_MESSAGE,
|
||||||
)
|
)
|
||||||
|
mock_queue = mock.MagicMock()
|
||||||
socketio = mock.MagicMock()
|
socketio = mock.MagicMock()
|
||||||
wcp = webchat.WebChatProcessPacketThread(packet, socketio)
|
wcp = webchat.WebChatProcessPacketThread(mock_queue, socketio)
|
||||||
|
|
||||||
wcp.process_our_message_packet(packet)
|
wcp.process_our_message_packet(packet)
|
||||||
mock_packet_add.called_once()
|
mock_packet_add.called_once()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
minversion = 2.9.0
|
minversion = 2.9.0
|
||||||
skipdist = True
|
skipdist = True
|
||||||
skip_missing_interpreters = true
|
skip_missing_interpreters = true
|
||||||
envlist = pep8,py{39,310,311}
|
envlist = pep8,py{39,310}
|
||||||
#requires = tox-pipenv
|
#requires = tox-pipenv
|
||||||
# pip==22.0.4
|
# pip==22.0.4
|
||||||
# pip-tools==5.4.0
|
# pip-tools==5.4.0
|
||||||
@@ -78,6 +78,7 @@ python =
|
|||||||
3.7: py38, pep8
|
3.7: py38, pep8
|
||||||
3.8: py38, pep8
|
3.8: py38, pep8
|
||||||
3.9: py39, pep8, type-check, docs
|
3.9: py39, pep8, type-check, docs
|
||||||
|
3.10: py39, pep8, type-check, docs
|
||||||
|
|
||||||
[testenv:fmt]
|
[testenv:fmt]
|
||||||
# This will reformat your code to comply with pep8
|
# This will reformat your code to comply with pep8
|
||||||
|
|||||||
Reference in New Issue
Block a user