mirror of https://github.com/craigerl/aprsd.git
Compare commits
170 Commits
Author | SHA1 | Date |
---|---|---|
Walter A. Boring IV | 1828342ef2 | |
Hemna | b317d0eb63 | |
Walter A. Boring IV | 63962acfe6 | |
Walter A. Boring IV | 44a72e813e | |
Hemna | afeb11a085 | |
dependabot[bot] | 18fb2a9e2b | |
Hemna | fa2d2d965d | |
Hemna | 2abf8bc750 | |
Hemna | f15974131c | |
Walter A. Boring IV | 4d1dfadbde | |
Hemna | 93a9cce0c0 | |
dependabot[bot] | 321260ff7a | |
Hemna | cb2a3441b4 | |
Hemna | fc9ab4aa74 | |
Hemna | a5680a7cbb | |
Hemna | c4b17eee9d | |
Hemna | 63f3de47b7 | |
Hemna | c206f52a76 | |
Hemna | 2b2bf6c92d | |
Hemna | 992485e9c7 | |
Hemna | f02db20c3e | |
Hemna | 09b97086bc | |
Hemna | c43652dbea | |
Hemna | 29d97d9f0c | |
Hemna | 813bc7ea29 | |
Hemna | bef32059f4 | |
Hemna | 717db6083e | |
Hemna | 4c7e27c88b | |
Hemna | 88d26241f5 | |
Hemna | 27359d61aa | |
Hemna | 7541f13174 | |
Hemna | a656d93263 | |
Hemna | cb0cfeea0b | |
Hemna | 8d86764c23 | |
Hemna | dc4879a367 | |
Hemna | 4542c0a643 | |
Hemna | 3e8716365e | |
Hemna | 758ea432ed | |
Hemna | 1c9f25a3b3 | |
Hemna | 7c935345e5 | |
Hemna | c2f8af06bc | |
Hemna | 5b2a59fae3 | |
Hemna | 8392d6b8ef | |
Hemna | 1a7694e7e2 | |
Hemna | f2d39e5fd2 | |
Hemna | 3bd7adda44 | |
Hemna | 91ba6d10ce | |
Hemna | c6079f897d | |
Hemna | 66e4850353 | |
Hemna | 40c028c844 | |
Hemna | 4c2a40b7a7 | |
Hemna | f682890ef0 | |
Hemna | 026dc6e376 | |
Hemna | f59b65d13c | |
Hemna | 5ff62c9bdf | |
Hemna | 5fa4eaf909 | |
Hemna | f34120c2df | |
Hemna | 3bef1314f8 | |
Hemna | 94f36e0aad | |
Craig Lamparter | 886ad9be09 | |
Craig Lamparter | aa6e732935 | |
Hemna | b3889896b9 | |
Hemna | 8f6f8007f4 | |
Hemna | 2e9cf3ce88 | |
Hemna | 8728926bf4 | |
Hemna | 2c5bc6c1f7 | |
Hemna | 80705cb341 | |
Hemna | a839dbd3c5 | |
Walter A. Boring IV | 1267a53ec8 | |
Hemna | da882b4f9b | |
Hemna | 6845d266f2 | |
Hemna | db2fbce079 | |
Hemna | bc3bdc48d2 | |
Hemna | 7114269cee | |
Hemna | fcc02f29af | |
Hemna | 0ca9072c97 | |
Hemna | 333feee805 | |
Hemna | a8d56a9967 | |
Hemna | 50e491bab4 | |
Hemna | 71d72adf06 | |
Hemna | e2e58530b2 | |
Hemna | 01cd0a0327 | |
Hemna | f92b2ee364 | |
Hemna | a270c75263 | |
Hemna | bd005f628d | |
Walter A. Boring IV | 200944f37a | |
Hemna | a62e490353 | |
Hemna | 428edaced9 | |
Hemna | 8f588e653d | |
Walter A. Boring IV | 144ad34ae5 | |
Hemna | 0321cb6cf1 | |
Hemna | c0623596cd | |
Hemna | f400c6004e | |
Hemna | 873fc06608 | |
Hemna | f53df24988 | |
Hemna | f4356e4a20 | |
Hemna | c581dc5020 | |
Hemna | da7b7124d7 | |
Hemna | 9e26df26d6 | |
Hemna | b461231c00 | |
Hemna | 1e6c483002 | |
Hemna | 127d3b3f26 | |
Hemna | f450238348 | |
Hemna | 9858955d34 | |
Hemna | e386e91f6e | |
Hemna | 386d2bea62 | |
Hemna | eada5e9ce2 | |
Hemna | 00e185b4e7 | |
Hemna | 1477e61b0f | |
Hemna | 6f1d6b4122 | |
Hemna | 90f212e6dc | |
Hemna | 9c77ca26be | |
Hemna | d80277c9d8 | |
Hemna | 29b4b04eee | |
Hemna | 12dab284cb | |
Hemna | d0f53c563f | |
Walter A. Boring IV | 24830ae810 | |
dependabot[bot] | 52896a1c6f | |
Hemna | 82b3761628 | |
Hemna | 8797dfd072 | |
Hemna | c1acdc2510 | |
Hemna | 71cd7e0ab5 | |
Hemna | d485f484ec | |
Hemna | f810c02d5d | |
Hemna | 50e24abb81 | |
Hemna | 10d023dd7b | |
Hemna | cb9456b29d | |
Hemna | c37e1d58bb | |
Hemna | 0ca5ceee7e | |
Hemna | 2e9c9d40e1 | |
Hemna | 66004f639f | |
Hemna | 0b0afd39ed | |
Hemna | aec88d4a7e | |
Hemna | 24bbea1d49 | |
Hemna | 5d3f42f411 | |
Walter A. Boring IV | 44a98850c9 | |
Hemna | 2cb9c2a31c | |
Hemna | 2fefa9fcd6 | |
Hemna | d092a43ec9 | |
Hemna | d1a09fc6b5 | |
Hemna | ff051bc285 | |
Hemna | 5fd91a2172 | |
Hemna | a4630c15be | |
Hemna | 6a7d7ad79b | |
Hemna | 7a5b55fa77 | |
Hemna | a1e21e795d | |
Hemna | cb291de047 | |
Hemna | e9c48c1914 | |
Hemna | f0ad6d7577 | |
Hemna | 38fe408c82 | |
Hemna | 8264c94bd6 | |
Hemna | 1ad2e135dc | |
Hemna | 1e4f0ca65a | |
Hemna | 41185416cb | |
Hemna | 68f23d8ca7 | |
Hemna | 11f1e9533e | |
Hemna | 275bf67b9e | |
Hemna | 968345944a | |
Hemna | df2798eafb | |
Hemna | e89f8a805b | |
Hemna | b14307270c | |
Walter A. Boring IV | ebee8e1439 | |
Hemna | a7e30b0bed | |
Hemna | 1a5c5f0dce | |
Walter A. Boring IV | a00c4ea840 | |
Hemna | a88de2f09c | |
Hemna | d6f0f05315 | |
Hemna | 03c58f83cd | |
Hemna | a4230d324a | |
Hemna | 8bceb827ec |
|
@ -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: |
|
||||||
|
@ -43,8 +43,9 @@ jobs:
|
||||||
with:
|
with:
|
||||||
context: "{{defaultContext}}:docker"
|
context: "{{defaultContext}}:docker"
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
file: ./Dockerfile-dev
|
file: ./Dockerfile
|
||||||
build-args: |
|
build-args: |
|
||||||
|
INSTALL_TYPE=github
|
||||||
BRANCH=${{ steps.extract_branch.outputs.branch }}
|
BRANCH=${{ steps.extract_branch.outputs.branch }}
|
||||||
BUILDX_QEMU_ENV=true
|
BUILDX_QEMU_ENV=true
|
||||||
push: true
|
push: true
|
||||||
|
|
|
@ -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
|
||||||
|
@ -53,8 +53,9 @@ jobs:
|
||||||
with:
|
with:
|
||||||
context: "{{defaultContext}}:docker"
|
context: "{{defaultContext}}:docker"
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
file: ./Dockerfile-dev
|
file: ./Dockerfile
|
||||||
build-args: |
|
build-args: |
|
||||||
|
INSTALL_TYPE=github
|
||||||
BRANCH=${{ steps.branch-name.outputs.current_branch }}
|
BRANCH=${{ steps.branch-name.outputs.current_branch }}
|
||||||
BUILDX_QEMU_ENV=true
|
BUILDX_QEMU_ENV=true
|
||||||
push: true
|
push: true
|
||||||
|
|
|
@ -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
|
||||||
|
|
185
ChangeLog
185
ChangeLog
|
@ -1,9 +1,194 @@
|
||||||
CHANGES
|
CHANGES
|
||||||
=======
|
=======
|
||||||
|
|
||||||
|
* Put an upper bound on the QueueHandler queue
|
||||||
|
|
||||||
|
v3.4.0
|
||||||
|
------
|
||||||
|
|
||||||
|
* Updated Changelog for 3.4.0
|
||||||
|
* Change setup.h
|
||||||
|
* Fixed docker setup.sh comparison
|
||||||
|
* Fixed unit tests failing with WatchList
|
||||||
|
* Added config enable\_packet\_logging
|
||||||
|
* Make all the Objectstore children use the same lock
|
||||||
|
* Fixed PacketTrack with UnknownPacket
|
||||||
|
* Removed the requirement on click-completion
|
||||||
|
* Update Dockerfiles
|
||||||
|
* Added fox for entry\_points with old python
|
||||||
|
* Added config for enable\_seen\_list
|
||||||
|
* Fix APRSDStats start\_time
|
||||||
|
* Added default\_packet\_send\_count config
|
||||||
|
* Call packet collecter after prepare during tx
|
||||||
|
* Added PacketTrack to packet collector
|
||||||
|
* Webchat Send Beacon uses Path selected in UI
|
||||||
|
* Added try except blocks in collectors
|
||||||
|
* Remove error logs from watch list
|
||||||
|
* Fixed issue with PacketList being empty
|
||||||
|
* Added new PacketCollector
|
||||||
|
* Fixed Keepalive access to email stats
|
||||||
|
* Added support for RX replyacks
|
||||||
|
* Changed Stats Collector registration
|
||||||
|
* Added PacketList.set\_maxlen()
|
||||||
|
* another fix for tx send
|
||||||
|
* removed Packet.last\_send\_attempt and just use send\_count
|
||||||
|
* Fix access to PacketList.\_maxlen
|
||||||
|
* added packet\_count in packet\_list stats
|
||||||
|
* force uwsgi to 2.0.24
|
||||||
|
* ismall update
|
||||||
|
* Added new config optons for PacketList
|
||||||
|
* Update requirements
|
||||||
|
* Added threads chart to admin ui graphs
|
||||||
|
* set packetlist max back to 100
|
||||||
|
* ensure thread count is updated
|
||||||
|
* Added threads table in the admin web ui
|
||||||
|
* Fixed issue with APRSDThreadList stats()
|
||||||
|
* Added new default\_ack\_send\_count config option
|
||||||
|
* Remove packet from tracker after max attempts
|
||||||
|
* Limit packets to 50 in PacketList
|
||||||
|
* syncronize the add for StatsStore
|
||||||
|
* Lock on stats for PacketList
|
||||||
|
* Fixed PacketList maxlen
|
||||||
|
* Fixed a problem with the webchat tab notification
|
||||||
|
* Another fix for ACK packets
|
||||||
|
* Fix issue not tracking RX Ack packets for stats
|
||||||
|
* Fix time plugin
|
||||||
|
* add GATE route to webchat along with WIDE1, etc
|
||||||
|
* Update webchat, include GATE route along with WIDE, ARISS, etc
|
||||||
|
* Get rid of some useless warning logs
|
||||||
|
* Added human\_info property to MessagePackets
|
||||||
|
* Fixed scrolling problem with new webchat sent msg
|
||||||
|
* Fix some issues with listen command
|
||||||
|
* Admin interface catch empty stats
|
||||||
|
* Ensure StatsStore has empty data
|
||||||
|
* Ensure latest pip is in docker image
|
||||||
|
* LOG failed requests post to admin ui
|
||||||
|
* changed admin web\_ip to StrOpt
|
||||||
|
* Updated prism to 1.29
|
||||||
|
* Removed json-viewer
|
||||||
|
* Remove rpyc as a requirement
|
||||||
|
* Delete more stats from webchat
|
||||||
|
* Admin UI working again
|
||||||
|
* Removed RPC Server and client
|
||||||
|
* Remove the logging of the conf password if not set
|
||||||
|
* Lock around client reset
|
||||||
|
* Allow stats collector to serialize upon creation
|
||||||
|
* Fixed issues with watch list at startup
|
||||||
|
* Fixed access to log\_monitor
|
||||||
|
* Got unit tests working again
|
||||||
|
* Fixed pep8 errors and missing files
|
||||||
|
* Reworked the stats making the rpc server obsolete
|
||||||
|
* Update client.py to add consumer in the API
|
||||||
|
* Fix for sample-config warning
|
||||||
|
* update requirements
|
||||||
|
* Put packet.json back in
|
||||||
|
* Change debug log color
|
||||||
|
* Fix for filtering curse words
|
||||||
|
* added packet counter random int
|
||||||
|
* More packet cleanup and tests
|
||||||
|
* Show comment in multiline packet output
|
||||||
|
* Added new config option log\_packet\_format
|
||||||
|
* Some packet cleanup
|
||||||
|
* Added new webchat config option for logging
|
||||||
|
* Fix some pep8 issues
|
||||||
|
* Completely redo logging of packets!!
|
||||||
|
* Fixed some logging in webchat
|
||||||
|
* Added missing packet types in listen command
|
||||||
|
* Don't call stats so often in webchat
|
||||||
|
* Eliminated need for from\_aprslib\_dict
|
||||||
|
* Fix for micE packet decoding with mbits
|
||||||
|
* updated dev-requirements
|
||||||
|
* Fixed some tox errors related to mypy
|
||||||
|
* Refactored packets
|
||||||
|
* removed print
|
||||||
|
* small refactor of stats usage in version plugin
|
||||||
|
* Added type setting on pluging.py for mypy
|
||||||
|
* Moved Threads list for mypy
|
||||||
|
* No need to synchronize on stats
|
||||||
|
* Start to add types
|
||||||
|
* Update tox for mypy runs
|
||||||
|
* Bump black from 24.2.0 to 24.3.0
|
||||||
|
* replaced access to conf from uwsgi
|
||||||
|
* Fixed call to setup\_logging in uwsgi
|
||||||
|
* Fixed access to conf.log in logging\_setup
|
||||||
|
|
||||||
|
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
|
||||||
|
|
13
Makefile
13
Makefile
|
@ -17,7 +17,7 @@ Makefile.venv:
|
||||||
help: # Help for the Makefile
|
help: # Help for the Makefile
|
||||||
@egrep -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
@egrep -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
||||||
dev: REQUIREMENTS_TXT = requirements.txt dev-requirements.txt
|
dev: REQUIREMENTS_TXT = requirements.txt requirements-dev.txt
|
||||||
dev: venv ## Create a python virtual environment for development of aprsd
|
dev: venv ## Create a python virtual environment for development of aprsd
|
||||||
|
|
||||||
run: venv ## Create a virtual environment for running aprsd commands
|
run: venv ## Create a virtual environment for running aprsd commands
|
||||||
|
@ -39,7 +39,6 @@ clean-build: ## remove build artifacts
|
||||||
clean-pyc: ## remove Python file artifacts
|
clean-pyc: ## remove Python file artifacts
|
||||||
find . -name '*.pyc' -exec rm -f {} +
|
find . -name '*.pyc' -exec rm -f {} +
|
||||||
find . -name '*.pyo' -exec rm -f {} +
|
find . -name '*.pyo' -exec rm -f {} +
|
||||||
find . -name '*~' -exec rm -f {} +
|
|
||||||
find . -name '__pycache__' -exec rm -fr {} +
|
find . -name '__pycache__' -exec rm -fr {} +
|
||||||
|
|
||||||
clean-test: ## remove test and coverage artifacts
|
clean-test: ## remove test and coverage artifacts
|
||||||
|
@ -57,7 +56,7 @@ test: dev ## Run all the tox tests
|
||||||
|
|
||||||
build: test ## Make the build artifact prior to doing an upload
|
build: test ## Make the build artifact prior to doing an upload
|
||||||
$(VENV)/pip install twine
|
$(VENV)/pip install twine
|
||||||
$(VENV)/python3 setup.py sdist bdist_wheel
|
$(VENV)/python3 -m build
|
||||||
$(VENV)/twine check dist/*
|
$(VENV)/twine check dist/*
|
||||||
|
|
||||||
upload: build ## Upload a new version of the plugin
|
upload: build ## Upload a new version of the plugin
|
||||||
|
@ -81,8 +80,8 @@ docker-dev: test ## Make a development docker container tagged with hemna6969/a
|
||||||
|
|
||||||
update-requirements: dev ## Update the requirements.txt and dev-requirements.txt files
|
update-requirements: dev ## Update the requirements.txt and dev-requirements.txt files
|
||||||
rm requirements.txt
|
rm requirements.txt
|
||||||
rm dev-requirements.txt
|
rm requirements-dev.txt
|
||||||
touch requirements.txt
|
touch requirements.txt
|
||||||
touch dev-requirements.txt
|
touch requirements-dev.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 requirements-dev.in
|
||||||
|
|
133
README.rst
133
README.rst
|
@ -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
|
||||||
|
|
|
@ -10,7 +10,10 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import pbr.version
|
from importlib.metadata import PackageNotFoundError, version
|
||||||
|
|
||||||
|
|
||||||
__version__ = pbr.version.VersionInfo("aprsd").version_string()
|
try:
|
||||||
|
__version__ = version("aprsd")
|
||||||
|
except PackageNotFoundError:
|
||||||
|
pass
|
||||||
|
|
|
@ -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"],
|
||||||
)
|
)
|
||||||
|
|
348
aprsd/client.py
348
aprsd/client.py
|
@ -1,348 +0,0 @@
|
||||||
import abc
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
|
|
||||||
import aprslib
|
|
||||||
from aprslib.exceptions import LoginError
|
|
||||||
from oslo_config import cfg
|
|
||||||
|
|
||||||
from aprsd import exception
|
|
||||||
from aprsd.clients import aprsis, fake, kiss
|
|
||||||
from aprsd.packets import core, packet_list
|
|
||||||
from aprsd.utils import trace
|
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
LOG = logging.getLogger("APRSD")
|
|
||||||
TRANSPORT_APRSIS = "aprsis"
|
|
||||||
TRANSPORT_TCPKISS = "tcpkiss"
|
|
||||||
TRANSPORT_SERIALKISS = "serialkiss"
|
|
||||||
TRANSPORT_FAKE = "fake"
|
|
||||||
|
|
||||||
# Main must create this from the ClientFactory
|
|
||||||
# object such that it's populated with the
|
|
||||||
# Correct config
|
|
||||||
factory = None
|
|
||||||
|
|
||||||
|
|
||||||
class Client(metaclass=trace.TraceWrapperMetaclass):
|
|
||||||
"""Singleton client class that constructs the aprslib connection."""
|
|
||||||
|
|
||||||
_instance = None
|
|
||||||
_client = None
|
|
||||||
|
|
||||||
connected = False
|
|
||||||
server_string = None
|
|
||||||
filter = None
|
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
|
||||||
"""This magic turns this into a singleton."""
|
|
||||||
if cls._instance is None:
|
|
||||||
cls._instance = super().__new__(cls)
|
|
||||||
# Put any initialization here.
|
|
||||||
return cls._instance
|
|
||||||
|
|
||||||
def set_filter(self, filter):
|
|
||||||
self.filter = filter
|
|
||||||
if self._client:
|
|
||||||
self._client.set_filter(filter)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def client(self):
|
|
||||||
if not self._client:
|
|
||||||
LOG.info("Creating APRS client")
|
|
||||||
self._client = self.setup_connection()
|
|
||||||
if self.filter:
|
|
||||||
LOG.info("Creating APRS client filter")
|
|
||||||
self._client.set_filter(self.filter)
|
|
||||||
return self._client
|
|
||||||
|
|
||||||
def send(self, packet: core.Packet):
|
|
||||||
packet_list.PacketList().tx(packet)
|
|
||||||
self.client.send(packet)
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
"""Call this to force a rebuild/reconnect."""
|
|
||||||
if self._client:
|
|
||||||
del self._client
|
|
||||||
else:
|
|
||||||
LOG.warning("Client not initialized, nothing to reset.")
|
|
||||||
|
|
||||||
# Recreate the client
|
|
||||||
LOG.info(f"Creating new client {self.client}")
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def setup_connection(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
@abc.abstractmethod
|
|
||||||
def is_enabled():
|
|
||||||
pass
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
@abc.abstractmethod
|
|
||||||
def transport():
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def decode_packet(self, *args, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class APRSISClient(Client, metaclass=trace.TraceWrapperMetaclass):
|
|
||||||
|
|
||||||
_client = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_enabled():
|
|
||||||
# Defaults to True if the enabled flag is non existent
|
|
||||||
try:
|
|
||||||
return CONF.aprs_network.enabled
|
|
||||||
except KeyError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_configured():
|
|
||||||
if APRSISClient.is_enabled():
|
|
||||||
# Ensure that the config vars are correctly set
|
|
||||||
if not CONF.aprs_network.login:
|
|
||||||
LOG.error("Config aprs_network.login not set.")
|
|
||||||
raise exception.MissingConfigOptionException(
|
|
||||||
"aprs_network.login is not set.",
|
|
||||||
)
|
|
||||||
if not CONF.aprs_network.password:
|
|
||||||
LOG.error("Config aprs_network.password not set.")
|
|
||||||
raise exception.MissingConfigOptionException(
|
|
||||||
"aprs_network.password is not set.",
|
|
||||||
)
|
|
||||||
if not CONF.aprs_network.host:
|
|
||||||
LOG.error("Config aprs_network.host not set.")
|
|
||||||
raise exception.MissingConfigOptionException(
|
|
||||||
"aprs_network.host is not set.",
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
|
||||||
return True
|
|
||||||
|
|
||||||
def is_alive(self):
|
|
||||||
if self._client:
|
|
||||||
return self._client.is_alive()
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def transport():
|
|
||||||
return TRANSPORT_APRSIS
|
|
||||||
|
|
||||||
def decode_packet(self, *args, **kwargs):
|
|
||||||
"""APRS lib already decodes this."""
|
|
||||||
return core.Packet.factory(args[0])
|
|
||||||
|
|
||||||
def setup_connection(self):
|
|
||||||
user = CONF.aprs_network.login
|
|
||||||
password = CONF.aprs_network.password
|
|
||||||
host = CONF.aprs_network.host
|
|
||||||
port = CONF.aprs_network.port
|
|
||||||
connected = False
|
|
||||||
backoff = 1
|
|
||||||
aprs_client = None
|
|
||||||
while not connected:
|
|
||||||
try:
|
|
||||||
LOG.info("Creating aprslib client")
|
|
||||||
aprs_client = aprsis.Aprsdis(user, passwd=password, host=host, port=port)
|
|
||||||
# Force the log to be the same
|
|
||||||
aprs_client.logger = LOG
|
|
||||||
aprs_client.connect()
|
|
||||||
connected = True
|
|
||||||
backoff = 1
|
|
||||||
except LoginError as e:
|
|
||||||
LOG.error(f"Failed to login to APRS-IS Server '{e}'")
|
|
||||||
connected = False
|
|
||||||
time.sleep(backoff)
|
|
||||||
except Exception as e:
|
|
||||||
LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
|
|
||||||
connected = False
|
|
||||||
time.sleep(backoff)
|
|
||||||
# Don't allow the backoff to go to inifinity.
|
|
||||||
if backoff > 5:
|
|
||||||
backoff = 5
|
|
||||||
else:
|
|
||||||
backoff += 1
|
|
||||||
continue
|
|
||||||
LOG.debug(f"Logging in to APRS-IS with user '{user}'")
|
|
||||||
self._client = aprs_client
|
|
||||||
return aprs_client
|
|
||||||
|
|
||||||
|
|
||||||
class KISSClient(Client, metaclass=trace.TraceWrapperMetaclass):
|
|
||||||
|
|
||||||
_client = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_enabled():
|
|
||||||
"""Return if tcp or serial KISS is enabled."""
|
|
||||||
if CONF.kiss_serial.enabled:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if CONF.kiss_tcp.enabled:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_configured():
|
|
||||||
# Ensure that the config vars are correctly set
|
|
||||||
if KISSClient.is_enabled():
|
|
||||||
transport = KISSClient.transport()
|
|
||||||
if transport == TRANSPORT_SERIALKISS:
|
|
||||||
if not CONF.kiss_serial.device:
|
|
||||||
LOG.error("KISS serial enabled, but no device is set.")
|
|
||||||
raise exception.MissingConfigOptionException(
|
|
||||||
"kiss_serial.device is not set.",
|
|
||||||
)
|
|
||||||
elif transport == TRANSPORT_TCPKISS:
|
|
||||||
if not CONF.kiss_tcp.host:
|
|
||||||
LOG.error("KISS TCP enabled, but no host is set.")
|
|
||||||
raise exception.MissingConfigOptionException(
|
|
||||||
"kiss_tcp.host is not set.",
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_alive(self):
|
|
||||||
if self._client:
|
|
||||||
return self._client.is_alive()
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def transport():
|
|
||||||
if CONF.kiss_serial.enabled:
|
|
||||||
return TRANSPORT_SERIALKISS
|
|
||||||
|
|
||||||
if CONF.kiss_tcp.enabled:
|
|
||||||
return TRANSPORT_TCPKISS
|
|
||||||
|
|
||||||
def decode_packet(self, *args, **kwargs):
|
|
||||||
"""We get a frame, which has to be decoded."""
|
|
||||||
LOG.debug(f"kwargs {kwargs}")
|
|
||||||
frame = kwargs["frame"]
|
|
||||||
LOG.debug(f"Got an APRS Frame '{frame}'")
|
|
||||||
# try and nuke the * from the fromcall sign.
|
|
||||||
# frame.header._source._ch = False
|
|
||||||
# payload = str(frame.payload.decode())
|
|
||||||
# msg = f"{str(frame.header)}:{payload}"
|
|
||||||
# msg = frame.tnc2
|
|
||||||
# LOG.debug(f"Decoding {msg}")
|
|
||||||
|
|
||||||
raw = aprslib.parse(str(frame))
|
|
||||||
packet = core.Packet.factory(raw)
|
|
||||||
if isinstance(packet, core.ThirdParty):
|
|
||||||
return packet.subpacket
|
|
||||||
else:
|
|
||||||
return packet
|
|
||||||
|
|
||||||
def setup_connection(self):
|
|
||||||
self._client = kiss.KISS3Client()
|
|
||||||
return self._client
|
|
||||||
|
|
||||||
|
|
||||||
class APRSDFakeClient(Client, metaclass=trace.TraceWrapperMetaclass):
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_enabled():
|
|
||||||
if CONF.fake_client.enabled:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_configured():
|
|
||||||
return APRSDFakeClient.is_enabled()
|
|
||||||
|
|
||||||
def is_alive(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def setup_connection(self):
|
|
||||||
return fake.APRSDFakeClient()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def transport():
|
|
||||||
return TRANSPORT_FAKE
|
|
||||||
|
|
||||||
def decode_packet(self, *args, **kwargs):
|
|
||||||
LOG.debug(f"kwargs {kwargs}")
|
|
||||||
pkt = kwargs["packet"]
|
|
||||||
LOG.debug(f"Got an APRS Fake Packet '{pkt}'")
|
|
||||||
return pkt
|
|
||||||
|
|
||||||
|
|
||||||
class ClientFactory:
|
|
||||||
_instance = None
|
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
|
||||||
"""This magic turns this into a singleton."""
|
|
||||||
if cls._instance is None:
|
|
||||||
cls._instance = super().__new__(cls)
|
|
||||||
# Put any initialization here.
|
|
||||||
return cls._instance
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._builders = {}
|
|
||||||
|
|
||||||
def register(self, key, builder):
|
|
||||||
self._builders[key] = builder
|
|
||||||
|
|
||||||
def create(self, key=None):
|
|
||||||
if not key:
|
|
||||||
if APRSISClient.is_enabled():
|
|
||||||
key = TRANSPORT_APRSIS
|
|
||||||
elif KISSClient.is_enabled():
|
|
||||||
key = KISSClient.transport()
|
|
||||||
elif APRSDFakeClient.is_enabled():
|
|
||||||
key = TRANSPORT_FAKE
|
|
||||||
|
|
||||||
builder = self._builders.get(key)
|
|
||||||
LOG.debug(f"Creating client {key}")
|
|
||||||
if not builder:
|
|
||||||
raise ValueError(key)
|
|
||||||
return builder()
|
|
||||||
|
|
||||||
def is_client_enabled(self):
|
|
||||||
"""Make sure at least one client is enabled."""
|
|
||||||
enabled = False
|
|
||||||
for key in self._builders.keys():
|
|
||||||
try:
|
|
||||||
enabled |= self._builders[key].is_enabled()
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return enabled
|
|
||||||
|
|
||||||
def is_client_configured(self):
|
|
||||||
enabled = False
|
|
||||||
for key in self._builders.keys():
|
|
||||||
try:
|
|
||||||
enabled |= self._builders[key].is_configured()
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
except exception.MissingConfigOptionException as ex:
|
|
||||||
LOG.error(ex.message)
|
|
||||||
return False
|
|
||||||
except exception.ConfigOptionBogusDefaultException as ex:
|
|
||||||
LOG.error(ex.message)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return enabled
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def setup():
|
|
||||||
"""Create and register all possible client objects."""
|
|
||||||
global factory
|
|
||||||
|
|
||||||
factory = ClientFactory()
|
|
||||||
factory.register(TRANSPORT_APRSIS, APRSISClient)
|
|
||||||
factory.register(TRANSPORT_TCPKISS, KISSClient)
|
|
||||||
factory.register(TRANSPORT_SERIALKISS, KISSClient)
|
|
||||||
factory.register(TRANSPORT_FAKE, APRSDFakeClient)
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
from aprsd.client import aprsis, factory, fake, kiss
|
||||||
|
|
||||||
|
|
||||||
|
TRANSPORT_APRSIS = "aprsis"
|
||||||
|
TRANSPORT_TCPKISS = "tcpkiss"
|
||||||
|
TRANSPORT_SERIALKISS = "serialkiss"
|
||||||
|
TRANSPORT_FAKE = "fake"
|
||||||
|
|
||||||
|
|
||||||
|
client_factory = factory.ClientFactory()
|
||||||
|
client_factory.register(aprsis.APRSISClient)
|
||||||
|
client_factory.register(kiss.KISSClient)
|
||||||
|
client_factory.register(fake.APRSDFakeClient)
|
|
@ -0,0 +1,132 @@
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
|
||||||
|
from aprslib.exceptions import LoginError
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from aprsd import client, exception
|
||||||
|
from aprsd.client import base
|
||||||
|
from aprsd.client.drivers import aprsis
|
||||||
|
from aprsd.packets import core
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
class APRSISClient(base.APRSClient):
|
||||||
|
|
||||||
|
_client = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
max_timeout = {"hours": 0.0, "minutes": 2, "seconds": 0}
|
||||||
|
self.max_delta = datetime.timedelta(**max_timeout)
|
||||||
|
|
||||||
|
def stats(self) -> dict:
|
||||||
|
stats = {}
|
||||||
|
if self.is_configured():
|
||||||
|
stats = {
|
||||||
|
"server_string": self._client.server_string,
|
||||||
|
"sever_keepalive": self._client.aprsd_keepalive,
|
||||||
|
"filter": self.filter,
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_enabled():
|
||||||
|
# Defaults to True if the enabled flag is non existent
|
||||||
|
try:
|
||||||
|
return CONF.aprs_network.enabled
|
||||||
|
except KeyError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_configured():
|
||||||
|
if APRSISClient.is_enabled():
|
||||||
|
# Ensure that the config vars are correctly set
|
||||||
|
if not CONF.aprs_network.login:
|
||||||
|
LOG.error("Config aprs_network.login not set.")
|
||||||
|
raise exception.MissingConfigOptionException(
|
||||||
|
"aprs_network.login is not set.",
|
||||||
|
)
|
||||||
|
if not CONF.aprs_network.password:
|
||||||
|
LOG.error("Config aprs_network.password not set.")
|
||||||
|
raise exception.MissingConfigOptionException(
|
||||||
|
"aprs_network.password is not set.",
|
||||||
|
)
|
||||||
|
if not CONF.aprs_network.host:
|
||||||
|
LOG.error("Config aprs_network.host not set.")
|
||||||
|
raise exception.MissingConfigOptionException(
|
||||||
|
"aprs_network.host is not set.",
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _is_stale_connection(self):
|
||||||
|
delta = datetime.datetime.now() - self._client.aprsd_keepalive
|
||||||
|
if delta > self.max_delta:
|
||||||
|
LOG.error(f"Connection is stale, last heard {delta} ago.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def is_alive(self):
|
||||||
|
if self._client:
|
||||||
|
return self._client.is_alive() and not self._is_stale_connection()
|
||||||
|
else:
|
||||||
|
LOG.warning(f"APRS_CLIENT {self._client} alive? NO!!!")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self._client:
|
||||||
|
self._client.stop()
|
||||||
|
self._client.close()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def transport():
|
||||||
|
return client.TRANSPORT_APRSIS
|
||||||
|
|
||||||
|
def decode_packet(self, *args, **kwargs):
|
||||||
|
"""APRS lib already decodes this."""
|
||||||
|
return core.factory(args[0])
|
||||||
|
|
||||||
|
def setup_connection(self):
|
||||||
|
user = CONF.aprs_network.login
|
||||||
|
password = CONF.aprs_network.password
|
||||||
|
host = CONF.aprs_network.host
|
||||||
|
port = CONF.aprs_network.port
|
||||||
|
self.connected = False
|
||||||
|
backoff = 1
|
||||||
|
aprs_client = None
|
||||||
|
while not self.connected:
|
||||||
|
try:
|
||||||
|
LOG.info(f"Creating aprslib client({host}:{port}) and logging in {user}.")
|
||||||
|
aprs_client = aprsis.Aprsdis(user, passwd=password, host=host, port=port)
|
||||||
|
# Force the log to be the same
|
||||||
|
aprs_client.logger = LOG
|
||||||
|
aprs_client.connect()
|
||||||
|
self.connected = True
|
||||||
|
backoff = 1
|
||||||
|
except LoginError as e:
|
||||||
|
LOG.error(f"Failed to login to APRS-IS Server '{e}'")
|
||||||
|
self.connected = False
|
||||||
|
time.sleep(backoff)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
|
||||||
|
self.connected = False
|
||||||
|
time.sleep(backoff)
|
||||||
|
# Don't allow the backoff to go to inifinity.
|
||||||
|
if backoff > 5:
|
||||||
|
backoff = 5
|
||||||
|
else:
|
||||||
|
backoff += 1
|
||||||
|
continue
|
||||||
|
self._client = aprs_client
|
||||||
|
return aprs_client
|
||||||
|
|
||||||
|
def consumer(self, callback, blocking=False, immortal=False, raw=False):
|
||||||
|
self._client.consumer(
|
||||||
|
callback, blocking=blocking,
|
||||||
|
immortal=immortal, raw=raw,
|
||||||
|
)
|
|
@ -0,0 +1,105 @@
|
||||||
|
import abc
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
import wrapt
|
||||||
|
|
||||||
|
from aprsd.packets import core
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
class APRSClient:
|
||||||
|
"""Singleton client class that constructs the aprslib connection."""
|
||||||
|
|
||||||
|
_instance = None
|
||||||
|
_client = None
|
||||||
|
|
||||||
|
connected = False
|
||||||
|
filter = None
|
||||||
|
lock = threading.Lock()
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
"""This magic turns this into a singleton."""
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
# Put any initialization here.
|
||||||
|
cls._instance._create_client()
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def stats(self) -> dict:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_filter(self, filter):
|
||||||
|
self.filter = filter
|
||||||
|
if self._client:
|
||||||
|
self._client.set_filter(filter)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self):
|
||||||
|
if not self._client:
|
||||||
|
self._create_client()
|
||||||
|
return self._client
|
||||||
|
|
||||||
|
def _create_client(self):
|
||||||
|
self._client = self.setup_connection()
|
||||||
|
if self.filter:
|
||||||
|
LOG.info("Creating APRS client filter")
|
||||||
|
self._client.set_filter(self.filter)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if self._client:
|
||||||
|
LOG.info("Stopping client connection.")
|
||||||
|
self._client.stop()
|
||||||
|
|
||||||
|
def send(self, packet: core.Packet):
|
||||||
|
"""Send a packet to the network."""
|
||||||
|
self.client.send(packet)
|
||||||
|
|
||||||
|
@wrapt.synchronized(lock)
|
||||||
|
def reset(self):
|
||||||
|
"""Call this to force a rebuild/reconnect."""
|
||||||
|
LOG.info("Resetting client connection.")
|
||||||
|
if self._client:
|
||||||
|
self._client.close()
|
||||||
|
del self._client
|
||||||
|
self._create_client()
|
||||||
|
else:
|
||||||
|
LOG.warning("Client not initialized, nothing to reset.")
|
||||||
|
|
||||||
|
# Recreate the client
|
||||||
|
LOG.info(f"Creating new client {self.client}")
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def setup_connection(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abc.abstractmethod
|
||||||
|
def is_enabled():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abc.abstractmethod
|
||||||
|
def transport():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def decode_packet(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def consumer(self, callback, blocking=False, immortal=False, raw=False):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def is_alive(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def close(self):
|
||||||
|
pass
|
|
@ -1,3 +1,4 @@
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import select
|
import select
|
||||||
import threading
|
import threading
|
||||||
|
@ -11,7 +12,6 @@ from aprslib.exceptions import (
|
||||||
import wrapt
|
import wrapt
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import stats
|
|
||||||
from aprsd.packets import core
|
from aprsd.packets import core
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,6 +24,9 @@ class Aprsdis(aprslib.IS):
|
||||||
# flag to tell us to stop
|
# flag to tell us to stop
|
||||||
thread_stop = False
|
thread_stop = False
|
||||||
|
|
||||||
|
# date for last time we heard from the server
|
||||||
|
aprsd_keepalive = datetime.datetime.now()
|
||||||
|
|
||||||
# timeout in seconds
|
# timeout in seconds
|
||||||
select_timeout = 1
|
select_timeout = 1
|
||||||
lock = threading.Lock()
|
lock = threading.Lock()
|
||||||
|
@ -112,7 +115,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()
|
||||||
|
@ -143,7 +145,6 @@ class Aprsdis(aprslib.IS):
|
||||||
|
|
||||||
self.logger.info(f"Connected to {server_string}")
|
self.logger.info(f"Connected to {server_string}")
|
||||||
self.server_string = server_string
|
self.server_string = server_string
|
||||||
stats.APRSDStats().set_aprsis_server(server_string)
|
|
||||||
|
|
||||||
except LoginError as e:
|
except LoginError as e:
|
||||||
self.logger.error(str(e))
|
self.logger.error(str(e))
|
||||||
|
@ -177,13 +178,14 @@ class Aprsdis(aprslib.IS):
|
||||||
try:
|
try:
|
||||||
for line in self._socket_readlines(blocking):
|
for line in self._socket_readlines(blocking):
|
||||||
if line[0:1] != b"#":
|
if line[0:1] != b"#":
|
||||||
|
self.aprsd_keepalive = datetime.datetime.now()
|
||||||
if raw:
|
if raw:
|
||||||
callback(line)
|
callback(line)
|
||||||
else:
|
else:
|
||||||
callback(self._parse(line))
|
callback(self._parse(line))
|
||||||
else:
|
else:
|
||||||
self.logger.debug("Server: %s", line.decode("utf8"))
|
self.logger.debug("Server: %s", line.decode("utf8"))
|
||||||
stats.APRSDStats().set_aprsis_keepalive()
|
self.aprsd_keepalive = datetime.datetime.now()
|
||||||
except ParseError as exp:
|
except ParseError as exp:
|
||||||
self.logger.log(
|
self.logger.log(
|
||||||
11,
|
11,
|
|
@ -67,7 +67,7 @@ class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
|
||||||
# Generate packets here?
|
# Generate packets here?
|
||||||
raw = "GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW"
|
raw = "GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW"
|
||||||
pkt_raw = aprslib.parse(raw)
|
pkt_raw = aprslib.parse(raw)
|
||||||
pkt = core.Packet.factory(pkt_raw)
|
pkt = core.factory(pkt_raw)
|
||||||
callback(packet=pkt)
|
callback(packet=pkt)
|
||||||
LOG.debug(f"END blocking FAKE consumer {self}")
|
LOG.debug(f"END blocking FAKE consumer {self}")
|
||||||
time.sleep(8)
|
time.sleep(8)
|
|
@ -81,7 +81,7 @@ class KISS3Client:
|
||||||
LOG.error("Failed to parse bytes received from KISS interface.")
|
LOG.error("Failed to parse bytes received from KISS interface.")
|
||||||
LOG.exception(ex)
|
LOG.exception(ex)
|
||||||
|
|
||||||
def consumer(self, callback, blocking=False, immortal=False, raw=False):
|
def consumer(self, callback):
|
||||||
LOG.debug("Start blocking KISS consumer")
|
LOG.debug("Start blocking KISS consumer")
|
||||||
self._parse_callback = callback
|
self._parse_callback = callback
|
||||||
self.kiss.read(callback=self.parse_frame, min_frames=None)
|
self.kiss.read(callback=self.parse_frame, min_frames=None)
|
|
@ -0,0 +1,88 @@
|
||||||
|
import logging
|
||||||
|
from typing import Callable, Protocol, runtime_checkable
|
||||||
|
|
||||||
|
from aprsd import exception
|
||||||
|
from aprsd.packets import core
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class Client(Protocol):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def connect(self) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def disconnect(self) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def decode_packet(self, *args, **kwargs) -> type[core.Packet]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def is_enabled(self) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def is_configured(self) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def transport(self) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send(self, message: str) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def setup_connection(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ClientFactory:
|
||||||
|
_instance = None
|
||||||
|
clients = []
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
"""This magic turns this into a singleton."""
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
# Put any initialization here.
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.clients: list[Callable] = []
|
||||||
|
|
||||||
|
def register(self, aprsd_client: Callable):
|
||||||
|
if isinstance(aprsd_client, Client):
|
||||||
|
raise ValueError("Client must be a subclass of Client protocol")
|
||||||
|
|
||||||
|
self.clients.append(aprsd_client)
|
||||||
|
|
||||||
|
def create(self, key=None):
|
||||||
|
for client in self.clients:
|
||||||
|
if client.is_enabled():
|
||||||
|
return client()
|
||||||
|
raise Exception("No client is configured!!")
|
||||||
|
|
||||||
|
def is_client_enabled(self):
|
||||||
|
"""Make sure at least one client is enabled."""
|
||||||
|
enabled = False
|
||||||
|
for client in self.clients:
|
||||||
|
if client.is_enabled():
|
||||||
|
enabled = True
|
||||||
|
return enabled
|
||||||
|
|
||||||
|
def is_client_configured(self):
|
||||||
|
enabled = False
|
||||||
|
for client in self.clients:
|
||||||
|
try:
|
||||||
|
if client.is_configured():
|
||||||
|
enabled = True
|
||||||
|
except exception.MissingConfigOptionException as ex:
|
||||||
|
LOG.error(ex.message)
|
||||||
|
return False
|
||||||
|
except exception.ConfigOptionBogusDefaultException as ex:
|
||||||
|
LOG.error(ex.message)
|
||||||
|
return False
|
||||||
|
return enabled
|
|
@ -0,0 +1,48 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from aprsd import client
|
||||||
|
from aprsd.client import base
|
||||||
|
from aprsd.client.drivers import fake as fake_driver
|
||||||
|
from aprsd.utils import trace
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
class APRSDFakeClient(base.APRSClient, metaclass=trace.TraceWrapperMetaclass):
|
||||||
|
|
||||||
|
def stats(self) -> dict:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_enabled():
|
||||||
|
if CONF.fake_client.enabled:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_configured():
|
||||||
|
return APRSDFakeClient.is_enabled()
|
||||||
|
|
||||||
|
def is_alive(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def setup_connection(self):
|
||||||
|
self.connected = True
|
||||||
|
return fake_driver.APRSDFakeClient()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def transport():
|
||||||
|
return client.TRANSPORT_FAKE
|
||||||
|
|
||||||
|
def decode_packet(self, *args, **kwargs):
|
||||||
|
LOG.debug(f"kwargs {kwargs}")
|
||||||
|
pkt = kwargs["packet"]
|
||||||
|
LOG.debug(f"Got an APRS Fake Packet '{pkt}'")
|
||||||
|
return pkt
|
|
@ -0,0 +1,103 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import aprslib
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from aprsd import client, exception
|
||||||
|
from aprsd.client import base
|
||||||
|
from aprsd.client.drivers import kiss
|
||||||
|
from aprsd.packets import core
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
class KISSClient(base.APRSClient):
|
||||||
|
|
||||||
|
_client = None
|
||||||
|
|
||||||
|
def stats(self) -> dict:
|
||||||
|
stats = {}
|
||||||
|
if self.is_configured():
|
||||||
|
return {
|
||||||
|
"transport": self.transport(),
|
||||||
|
}
|
||||||
|
return stats
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_enabled():
|
||||||
|
"""Return if tcp or serial KISS is enabled."""
|
||||||
|
if CONF.kiss_serial.enabled:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if CONF.kiss_tcp.enabled:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def is_configured():
|
||||||
|
# Ensure that the config vars are correctly set
|
||||||
|
if KISSClient.is_enabled():
|
||||||
|
transport = KISSClient.transport()
|
||||||
|
if transport == client.TRANSPORT_SERIALKISS:
|
||||||
|
if not CONF.kiss_serial.device:
|
||||||
|
LOG.error("KISS serial enabled, but no device is set.")
|
||||||
|
raise exception.MissingConfigOptionException(
|
||||||
|
"kiss_serial.device is not set.",
|
||||||
|
)
|
||||||
|
elif transport == client.TRANSPORT_TCPKISS:
|
||||||
|
if not CONF.kiss_tcp.host:
|
||||||
|
LOG.error("KISS TCP enabled, but no host is set.")
|
||||||
|
raise exception.MissingConfigOptionException(
|
||||||
|
"kiss_tcp.host is not set.",
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_alive(self):
|
||||||
|
if self._client:
|
||||||
|
return self._client.is_alive()
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self._client:
|
||||||
|
self._client.stop()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def transport():
|
||||||
|
if CONF.kiss_serial.enabled:
|
||||||
|
return client.TRANSPORT_SERIALKISS
|
||||||
|
|
||||||
|
if CONF.kiss_tcp.enabled:
|
||||||
|
return client.TRANSPORT_TCPKISS
|
||||||
|
|
||||||
|
def decode_packet(self, *args, **kwargs):
|
||||||
|
"""We get a frame, which has to be decoded."""
|
||||||
|
LOG.debug(f"kwargs {kwargs}")
|
||||||
|
frame = kwargs["frame"]
|
||||||
|
LOG.debug(f"Got an APRS Frame '{frame}'")
|
||||||
|
# try and nuke the * from the fromcall sign.
|
||||||
|
# frame.header._source._ch = False
|
||||||
|
# payload = str(frame.payload.decode())
|
||||||
|
# msg = f"{str(frame.header)}:{payload}"
|
||||||
|
# msg = frame.tnc2
|
||||||
|
# LOG.debug(f"Decoding {msg}")
|
||||||
|
|
||||||
|
raw = aprslib.parse(str(frame))
|
||||||
|
packet = core.factory(raw)
|
||||||
|
if isinstance(packet, core.ThirdParty):
|
||||||
|
return packet.subpacket
|
||||||
|
else:
|
||||||
|
return packet
|
||||||
|
|
||||||
|
def setup_connection(self):
|
||||||
|
self._client = kiss.KISS3Client()
|
||||||
|
self.connected = True
|
||||||
|
return self._client
|
||||||
|
|
||||||
|
def consumer(self, callback, blocking=False, immortal=False, raw=False):
|
||||||
|
self._client.consumer(callback)
|
|
@ -0,0 +1,38 @@
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
import wrapt
|
||||||
|
|
||||||
|
from aprsd import client
|
||||||
|
from aprsd.utils import singleton
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
@singleton
|
||||||
|
class APRSClientStats:
|
||||||
|
|
||||||
|
lock = threading.Lock()
|
||||||
|
|
||||||
|
@wrapt.synchronized(lock)
|
||||||
|
def stats(self, serializable=False):
|
||||||
|
cl = client.client_factory.create()
|
||||||
|
stats = {
|
||||||
|
"transport": cl.transport(),
|
||||||
|
"filter": cl.filter,
|
||||||
|
"connected": cl.connected,
|
||||||
|
}
|
||||||
|
|
||||||
|
if cl.transport() == client.TRANSPORT_APRSIS:
|
||||||
|
stats["server_string"] = cl.client.server_string
|
||||||
|
keepalive = cl.client.aprsd_keepalive
|
||||||
|
if serializable:
|
||||||
|
keepalive = keepalive.isoformat()
|
||||||
|
stats["server_keepalive"] = keepalive
|
||||||
|
elif cl.transport() == client.TRANSPORT_TCPKISS:
|
||||||
|
stats["host"] = CONF.kiss_tcp.host
|
||||||
|
stats["port"] = CONF.kiss_tcp.port
|
||||||
|
elif cl.transport() == client.TRANSPORT_SERIALKISS:
|
||||||
|
stats["device"] = CONF.kiss_serial.device
|
||||||
|
return stats
|
|
@ -1,5 +1,5 @@
|
||||||
import click
|
import click
|
||||||
import click_completion
|
import click.shell_completion
|
||||||
|
|
||||||
from aprsd.main import cli
|
from aprsd.main import cli
|
||||||
|
|
||||||
|
@ -7,30 +7,16 @@ from aprsd.main import cli
|
||||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||||
|
|
||||||
|
|
||||||
@cli.group(help="Click Completion subcommands", context_settings=CONTEXT_SETTINGS)
|
@cli.command()
|
||||||
@click.pass_context
|
@click.argument("shell", type=click.Choice(list(click.shell_completion._available_shells)))
|
||||||
def completion(ctx):
|
def completion(shell):
|
||||||
pass
|
"""Show the shell completion code"""
|
||||||
|
from click.utils import _detect_program_name
|
||||||
|
|
||||||
|
cls = click.shell_completion.get_completion_class(shell)
|
||||||
# show dumps out the completion code for a particular shell
|
prog_name = _detect_program_name()
|
||||||
@completion.command(help="Show completion code for shell", name="show")
|
complete_var = f"_{prog_name}_COMPLETE".replace("-", "_").upper()
|
||||||
@click.option("-i", "--case-insensitive/--no-case-insensitive", help="Case insensitive completion")
|
print(cls(cli, {}, prog_name, complete_var).source())
|
||||||
@click.argument("shell", required=False, type=click_completion.DocumentedChoice(click_completion.core.shells))
|
print("# Add the following line to your shell configuration file to have aprsd command line completion")
|
||||||
def show(shell, case_insensitive):
|
print("# but remove the leading '#' character.")
|
||||||
"""Show the click-completion-command completion code"""
|
print(f"# eval \"$(aprsd completion {shell})\"")
|
||||||
extra_env = {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} if case_insensitive else {}
|
|
||||||
click.echo(click_completion.core.get_code(shell, extra_env=extra_env))
|
|
||||||
|
|
||||||
|
|
||||||
# install will install the completion code for a particular shell
|
|
||||||
@completion.command(help="Install completion code for a shell", name="install")
|
|
||||||
@click.option("--append/--overwrite", help="Append the completion code to the file", default=None)
|
|
||||||
@click.option("-i", "--case-insensitive/--no-case-insensitive", help="Case insensitive completion")
|
|
||||||
@click.argument("shell", required=False, type=click_completion.DocumentedChoice(click_completion.core.shells))
|
|
||||||
@click.argument("path", required=False)
|
|
||||||
def install(append, case_insensitive, shell, path):
|
|
||||||
"""Install the click-completion-command completion"""
|
|
||||||
extra_env = {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} if case_insensitive else {}
|
|
||||||
shell, path = click_completion.core.install(shell=shell, path=path, append=append, extra_env=extra_env)
|
|
||||||
click.echo(f"{shell} completion installed in {path}")
|
|
||||||
|
|
|
@ -8,8 +8,9 @@ import logging
|
||||||
import click
|
import click
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from aprsd import cli_helper, conf, packets, plugin
|
||||||
# local imports here
|
# local imports here
|
||||||
from aprsd import cli_helper, client, conf, packets, plugin
|
from aprsd.client import base
|
||||||
from aprsd.main import cli
|
from aprsd.main import cli
|
||||||
from aprsd.utils import trace
|
from aprsd.utils import trace
|
||||||
|
|
||||||
|
@ -96,7 +97,7 @@ def test_plugin(
|
||||||
if CONF.trace_enabled:
|
if CONF.trace_enabled:
|
||||||
trace.setup_tracing(["method", "api"])
|
trace.setup_tracing(["method", "api"])
|
||||||
|
|
||||||
client.Client()
|
base.APRSClient()
|
||||||
|
|
||||||
pm = plugin.PluginManager()
|
pm = plugin.PluginManager()
|
||||||
if load_all:
|
if load_all:
|
||||||
|
@ -125,8 +126,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()
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
# Fetch active stats from a remote running instance of aprsd server
|
# Fetch active stats from a remote running instance of aprsd admin web interface.
|
||||||
# This uses the RPC server to fetch the stats from the remote server.
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
import requests
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
|
||||||
|
@ -12,7 +11,6 @@ from rich.table import Table
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import cli_helper
|
from aprsd import cli_helper
|
||||||
from aprsd.main import cli
|
from aprsd.main import cli
|
||||||
from aprsd.rpc import client as rpc_client
|
|
||||||
|
|
||||||
|
|
||||||
# setup the global logger
|
# setup the global logger
|
||||||
|
@ -26,83 +24,80 @@ CONF = cfg.CONF
|
||||||
@click.option(
|
@click.option(
|
||||||
"--host", type=str,
|
"--host", type=str,
|
||||||
default=None,
|
default=None,
|
||||||
help="IP address of the remote aprsd server to fetch stats from.",
|
help="IP address of the remote aprsd admin web ui fetch stats from.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--port", type=int,
|
"--port", type=int,
|
||||||
default=None,
|
default=None,
|
||||||
help="Port of the remote aprsd server rpc port to fetch stats from.",
|
help="Port of the remote aprsd web admin interface to fetch stats from.",
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
"--magic-word", type=str,
|
|
||||||
default=None,
|
|
||||||
help="Magic word of the remote aprsd server rpc port to fetch stats from.",
|
|
||||||
)
|
)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
@cli_helper.process_standard_options
|
@cli_helper.process_standard_options
|
||||||
def fetch_stats(ctx, host, port, magic_word):
|
def fetch_stats(ctx, host, port):
|
||||||
"""Fetch stats from a remote running instance of aprsd server."""
|
"""Fetch stats from a APRSD admin web interface."""
|
||||||
LOG.info(f"APRSD Fetch-Stats started version: {aprsd.__version__}")
|
console = Console()
|
||||||
|
console.print(f"APRSD Fetch-Stats started version: {aprsd.__version__}")
|
||||||
|
|
||||||
CONF.log_opt_values(LOG, logging.DEBUG)
|
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||||
if not host:
|
if not host:
|
||||||
host = CONF.rpc_settings.ip
|
host = CONF.admin.web_ip
|
||||||
if not port:
|
if not port:
|
||||||
port = CONF.rpc_settings.port
|
port = CONF.admin.web_port
|
||||||
if not magic_word:
|
|
||||||
magic_word = CONF.rpc_settings.magic_word
|
|
||||||
|
|
||||||
msg = f"Fetching stats from {host}:{port} with magic word '{magic_word}'"
|
msg = f"Fetching stats from {host}:{port}"
|
||||||
console = Console()
|
|
||||||
console.print(msg)
|
console.print(msg)
|
||||||
with console.status(msg):
|
with console.status(msg):
|
||||||
client = rpc_client.RPCClient(host, port, magic_word)
|
response = requests.get(f"http://{host}:{port}/stats", timeout=120)
|
||||||
stats = client.get_stats_dict()
|
if not response:
|
||||||
console.print_json(data=stats)
|
console.print(
|
||||||
|
f"Failed to fetch stats from {host}:{port}?",
|
||||||
|
style="bold red",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
stats = response.json()
|
||||||
|
if not stats:
|
||||||
|
console.print(
|
||||||
|
f"Failed to fetch stats from aprsd admin ui at {host}:{port}",
|
||||||
|
style="bold red",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
aprsd_title = (
|
aprsd_title = (
|
||||||
"APRSD "
|
"APRSD "
|
||||||
f"[bold cyan]v{stats['aprsd']['version']}[/] "
|
f"[bold cyan]v{stats['APRSDStats']['version']}[/] "
|
||||||
f"Callsign [bold green]{stats['aprsd']['callsign']}[/] "
|
f"Callsign [bold green]{stats['APRSDStats']['callsign']}[/] "
|
||||||
f"Uptime [bold yellow]{stats['aprsd']['uptime']}[/]"
|
f"Uptime [bold yellow]{stats['APRSDStats']['uptime']}[/]"
|
||||||
)
|
)
|
||||||
|
|
||||||
console.rule(f"Stats from {host}:{port} with magic word '{magic_word}'")
|
console.rule(f"Stats from {host}:{port}")
|
||||||
console.print("\n\n")
|
console.print("\n\n")
|
||||||
console.rule(aprsd_title)
|
console.rule(aprsd_title)
|
||||||
|
|
||||||
# Show the connection to APRS
|
# Show the connection to APRS
|
||||||
# It can be a connection to an APRS-IS server or a local TNC via KISS or KISSTCP
|
# It can be a connection to an APRS-IS server or a local TNC via KISS or KISSTCP
|
||||||
if "aprs-is" in stats:
|
if "aprs-is" in stats:
|
||||||
title = f"APRS-IS Connection {stats['aprs-is']['server']}"
|
title = f"APRS-IS Connection {stats['APRSClientStats']['server_string']}"
|
||||||
table = Table(title=title)
|
table = Table(title=title)
|
||||||
table.add_column("Key")
|
table.add_column("Key")
|
||||||
table.add_column("Value")
|
table.add_column("Value")
|
||||||
for key, value in stats["aprs-is"].items():
|
for key, value in stats["APRSClientStats"].items():
|
||||||
table.add_row(key, value)
|
table.add_row(key, value)
|
||||||
console.print(table)
|
console.print(table)
|
||||||
|
|
||||||
threads_table = Table(title="Threads")
|
threads_table = Table(title="Threads")
|
||||||
threads_table.add_column("Name")
|
threads_table.add_column("Name")
|
||||||
threads_table.add_column("Alive?")
|
threads_table.add_column("Alive?")
|
||||||
for name, alive in stats["aprsd"]["threads"].items():
|
for name, alive in stats["APRSDThreadList"].items():
|
||||||
threads_table.add_row(name, str(alive))
|
threads_table.add_row(name, str(alive))
|
||||||
|
|
||||||
console.print(threads_table)
|
console.print(threads_table)
|
||||||
|
|
||||||
msgs_table = Table(title="Messages")
|
|
||||||
msgs_table.add_column("Key")
|
|
||||||
msgs_table.add_column("Value")
|
|
||||||
for key, value in stats["messages"].items():
|
|
||||||
msgs_table.add_row(key, str(value))
|
|
||||||
|
|
||||||
console.print(msgs_table)
|
|
||||||
|
|
||||||
packet_totals = Table(title="Packet Totals")
|
packet_totals = Table(title="Packet Totals")
|
||||||
packet_totals.add_column("Key")
|
packet_totals.add_column("Key")
|
||||||
packet_totals.add_column("Value")
|
packet_totals.add_column("Value")
|
||||||
packet_totals.add_row("Total Received", str(stats["packets"]["total_received"]))
|
packet_totals.add_row("Total Received", str(stats["PacketList"]["rx"]))
|
||||||
packet_totals.add_row("Total Sent", str(stats["packets"]["total_sent"]))
|
packet_totals.add_row("Total Sent", str(stats["PacketList"]["tx"]))
|
||||||
packet_totals.add_row("Total Tracked", str(stats["packets"]["total_tracked"]))
|
|
||||||
console.print(packet_totals)
|
console.print(packet_totals)
|
||||||
|
|
||||||
# Show each of the packet types
|
# Show each of the packet types
|
||||||
|
@ -110,47 +105,52 @@ def fetch_stats(ctx, host, port, magic_word):
|
||||||
packets_table.add_column("Packet Type")
|
packets_table.add_column("Packet Type")
|
||||||
packets_table.add_column("TX")
|
packets_table.add_column("TX")
|
||||||
packets_table.add_column("RX")
|
packets_table.add_column("RX")
|
||||||
for key, value in stats["packets"]["by_type"].items():
|
for key, value in stats["PacketList"]["packets"].items():
|
||||||
packets_table.add_row(key, str(value["tx"]), str(value["rx"]))
|
packets_table.add_row(key, str(value["tx"]), str(value["rx"]))
|
||||||
|
|
||||||
console.print(packets_table)
|
console.print(packets_table)
|
||||||
|
|
||||||
if "plugins" in stats:
|
if "plugins" in stats:
|
||||||
count = len(stats["plugins"])
|
count = len(stats["PluginManager"])
|
||||||
plugins_table = Table(title=f"Plugins ({count})")
|
plugins_table = Table(title=f"Plugins ({count})")
|
||||||
plugins_table.add_column("Plugin")
|
plugins_table.add_column("Plugin")
|
||||||
plugins_table.add_column("Enabled")
|
plugins_table.add_column("Enabled")
|
||||||
plugins_table.add_column("Version")
|
plugins_table.add_column("Version")
|
||||||
plugins_table.add_column("TX")
|
plugins_table.add_column("TX")
|
||||||
plugins_table.add_column("RX")
|
plugins_table.add_column("RX")
|
||||||
for key, value in stats["plugins"].items():
|
plugins = stats["PluginManager"]
|
||||||
|
for key, value in plugins.items():
|
||||||
plugins_table.add_row(
|
plugins_table.add_row(
|
||||||
key,
|
key,
|
||||||
str(stats["plugins"][key]["enabled"]),
|
str(plugins[key]["enabled"]),
|
||||||
stats["plugins"][key]["version"],
|
plugins[key]["version"],
|
||||||
str(stats["plugins"][key]["tx"]),
|
str(plugins[key]["tx"]),
|
||||||
str(stats["plugins"][key]["rx"]),
|
str(plugins[key]["rx"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
console.print(plugins_table)
|
console.print(plugins_table)
|
||||||
|
|
||||||
if "seen_list" in stats["aprsd"]:
|
seen_list = stats.get("SeenList")
|
||||||
count = len(stats["aprsd"]["seen_list"])
|
|
||||||
|
if seen_list:
|
||||||
|
count = len(seen_list)
|
||||||
seen_table = Table(title=f"Seen List ({count})")
|
seen_table = Table(title=f"Seen List ({count})")
|
||||||
seen_table.add_column("Callsign")
|
seen_table.add_column("Callsign")
|
||||||
seen_table.add_column("Message Count")
|
seen_table.add_column("Message Count")
|
||||||
seen_table.add_column("Last Heard")
|
seen_table.add_column("Last Heard")
|
||||||
for key, value in stats["aprsd"]["seen_list"].items():
|
for key, value in seen_list.items():
|
||||||
seen_table.add_row(key, str(value["count"]), value["last"])
|
seen_table.add_row(key, str(value["count"]), value["last"])
|
||||||
|
|
||||||
console.print(seen_table)
|
console.print(seen_table)
|
||||||
|
|
||||||
if "watch_list" in stats["aprsd"]:
|
watch_list = stats.get("WatchList")
|
||||||
count = len(stats["aprsd"]["watch_list"])
|
|
||||||
|
if watch_list:
|
||||||
|
count = len(watch_list)
|
||||||
watch_table = Table(title=f"Watch List ({count})")
|
watch_table = Table(title=f"Watch List ({count})")
|
||||||
watch_table.add_column("Callsign")
|
watch_table.add_column("Callsign")
|
||||||
watch_table.add_column("Last Heard")
|
watch_table.add_column("Last Heard")
|
||||||
for key, value in stats["aprsd"]["watch_list"].items():
|
for key, value in watch_list.items():
|
||||||
watch_table.add_row(key, value["last"])
|
watch_table.add_row(key, value["last"])
|
||||||
|
|
||||||
console.print(watch_table)
|
console.print(watch_table)
|
||||||
|
|
|
@ -13,11 +13,11 @@ from oslo_config import cfg
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import cli_helper, utils
|
from aprsd import cli_helper
|
||||||
from aprsd import conf # noqa
|
from aprsd import conf # noqa
|
||||||
# local imports here
|
# local imports here
|
||||||
from aprsd.main import cli
|
from aprsd.main import cli
|
||||||
from aprsd.rpc import client as aprsd_rpc_client
|
from aprsd.threads import stats as stats_threads
|
||||||
|
|
||||||
|
|
||||||
# setup the global logger
|
# setup the global logger
|
||||||
|
@ -39,46 +39,48 @@ console = Console()
|
||||||
@cli_helper.process_standard_options
|
@cli_helper.process_standard_options
|
||||||
def healthcheck(ctx, timeout):
|
def healthcheck(ctx, timeout):
|
||||||
"""Check the health of the running aprsd server."""
|
"""Check the health of the running aprsd server."""
|
||||||
console.log(f"APRSD HealthCheck version: {aprsd.__version__}")
|
ver_str = f"APRSD HealthCheck version: {aprsd.__version__}"
|
||||||
if not CONF.rpc_settings.enabled:
|
console.log(ver_str)
|
||||||
LOG.error("Must enable rpc_settings.enabled to use healthcheck")
|
|
||||||
sys.exit(-1)
|
|
||||||
if not CONF.rpc_settings.ip:
|
|
||||||
LOG.error("Must enable rpc_settings.ip to use healthcheck")
|
|
||||||
sys.exit(-1)
|
|
||||||
if not CONF.rpc_settings.magic_word:
|
|
||||||
LOG.error("Must enable rpc_settings.magic_word to use healthcheck")
|
|
||||||
sys.exit(-1)
|
|
||||||
|
|
||||||
with console.status(f"APRSD HealthCheck version: {aprsd.__version__}") as status:
|
with console.status(ver_str):
|
||||||
try:
|
try:
|
||||||
status.update(f"Contacting APRSD via RPC {CONF.rpc_settings.ip}")
|
stats_obj = stats_threads.StatsStore()
|
||||||
stats = aprsd_rpc_client.RPCClient().get_stats_dict()
|
stats_obj.load()
|
||||||
|
stats = stats_obj.data
|
||||||
|
# console.print(stats)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
console.log(f"Failed to fetch healthcheck : '{ex}'")
|
console.log(f"Failed to load stats: '{ex}'")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
else:
|
else:
|
||||||
|
now = datetime.datetime.now()
|
||||||
if not stats:
|
if not stats:
|
||||||
console.log("No stats from aprsd")
|
console.log("No stats from aprsd")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
email_thread_last_update = stats["email"]["thread_last_update"]
|
|
||||||
|
|
||||||
if email_thread_last_update != "never":
|
email_stats = stats.get("EmailStats")
|
||||||
delta = utils.parse_delta_str(email_thread_last_update)
|
if email_stats:
|
||||||
d = datetime.timedelta(**delta)
|
email_thread_last_update = email_stats["last_check_time"]
|
||||||
|
|
||||||
|
if email_thread_last_update != "never":
|
||||||
|
d = now - email_thread_last_update
|
||||||
|
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
||||||
|
max_delta = datetime.timedelta(**max_timeout)
|
||||||
|
if d > max_delta:
|
||||||
|
console.log(f"Email thread is very old! {d}")
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
client_stats = stats.get("APRSClientStats")
|
||||||
|
if not client_stats:
|
||||||
|
console.log("No APRSClientStats")
|
||||||
|
sys.exit(-1)
|
||||||
|
else:
|
||||||
|
aprsis_last_update = client_stats["server_keepalive"]
|
||||||
|
d = now - aprsis_last_update
|
||||||
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
||||||
max_delta = datetime.timedelta(**max_timeout)
|
max_delta = datetime.timedelta(**max_timeout)
|
||||||
if d > max_delta:
|
if d > max_delta:
|
||||||
console.log(f"Email thread is very old! {d}")
|
LOG.error(f"APRS-IS last update is very old! {d}")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
aprsis_last_update = stats["aprs-is"]["last_update"]
|
console.log("OK")
|
||||||
delta = utils.parse_delta_str(aprsis_last_update)
|
|
||||||
d = datetime.timedelta(**delta)
|
|
||||||
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
|
||||||
max_delta = datetime.timedelta(**max_timeout)
|
|
||||||
if d > max_delta:
|
|
||||||
LOG.error(f"APRS-IS last update is very old! {d}")
|
|
||||||
sys.exit(-1)
|
|
||||||
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
|
@ -21,11 +21,12 @@ from aprsd import cli_helper
|
||||||
from aprsd import plugin as aprsd_plugin
|
from aprsd import plugin as aprsd_plugin
|
||||||
from aprsd.main import cli
|
from aprsd.main import cli
|
||||||
from aprsd.plugins import (
|
from aprsd.plugins import (
|
||||||
email, fortune, location, notify, ping, query, time, version, weather,
|
email, fortune, location, notify, ping, time, version, weather,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
PYPI_URL = "https://pypi.org/search/"
|
||||||
|
|
||||||
|
|
||||||
def onerror(name):
|
def onerror(name):
|
||||||
|
@ -89,22 +90,39 @@ 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):
|
||||||
modules = [email, fortune, location, notify, ping, query, time, version, weather]
|
modules = [email, fortune, location, notify, ping, time, version, weather]
|
||||||
plugins = []
|
plugins = []
|
||||||
|
|
||||||
for module in modules:
|
for module in modules:
|
||||||
|
@ -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)
|
||||||
|
|
|
@ -15,10 +15,15 @@ from rich.console import Console
|
||||||
|
|
||||||
# local imports here
|
# local imports here
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import cli_helper, client, packets, plugin, stats, threads
|
from aprsd import cli_helper, packets, plugin, threads
|
||||||
|
from aprsd.client import client_factory
|
||||||
from aprsd.main import cli
|
from aprsd.main import cli
|
||||||
from aprsd.rpc import server as rpc_server
|
from aprsd.packets import collector as packet_collector
|
||||||
from aprsd.threads import rx
|
from aprsd.packets import log as packet_log
|
||||||
|
from aprsd.packets import seen_list
|
||||||
|
from aprsd.stats import collector
|
||||||
|
from aprsd.threads import keep_alive, rx
|
||||||
|
from aprsd.threads import stats as stats_thread
|
||||||
|
|
||||||
|
|
||||||
# setup the global logger
|
# setup the global logger
|
||||||
|
@ -37,7 +42,7 @@ def signal_handler(sig, frame):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
LOG.info(stats.APRSDStats())
|
LOG.info(collector.Collector().collect())
|
||||||
|
|
||||||
|
|
||||||
class APRSDListenThread(rx.APRSDRXThread):
|
class APRSDListenThread(rx.APRSDRXThread):
|
||||||
|
@ -53,29 +58,33 @@ class APRSDListenThread(rx.APRSDRXThread):
|
||||||
filters = {
|
filters = {
|
||||||
packets.Packet.__name__: packets.Packet,
|
packets.Packet.__name__: packets.Packet,
|
||||||
packets.AckPacket.__name__: packets.AckPacket,
|
packets.AckPacket.__name__: packets.AckPacket,
|
||||||
|
packets.BeaconPacket.__name__: packets.BeaconPacket,
|
||||||
packets.GPSPacket.__name__: packets.GPSPacket,
|
packets.GPSPacket.__name__: packets.GPSPacket,
|
||||||
packets.MessagePacket.__name__: packets.MessagePacket,
|
packets.MessagePacket.__name__: packets.MessagePacket,
|
||||||
packets.MicEPacket.__name__: packets.MicEPacket,
|
packets.MicEPacket.__name__: packets.MicEPacket,
|
||||||
|
packets.ObjectPacket.__name__: packets.ObjectPacket,
|
||||||
|
packets.StatusPacket.__name__: packets.StatusPacket,
|
||||||
|
packets.ThirdPartyPacket.__name__: packets.ThirdPartyPacket,
|
||||||
packets.WeatherPacket.__name__: packets.WeatherPacket,
|
packets.WeatherPacket.__name__: packets.WeatherPacket,
|
||||||
|
packets.UnknownPacket.__name__: packets.UnknownPacket,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.packet_filter:
|
if self.packet_filter:
|
||||||
filter_class = filters[self.packet_filter]
|
filter_class = filters[self.packet_filter]
|
||||||
if isinstance(packet, filter_class):
|
if isinstance(packet, filter_class):
|
||||||
packet.log(header="RX")
|
packet_log.log(packet)
|
||||||
if self.plugin_manager:
|
if self.plugin_manager:
|
||||||
# Don't do anything with the reply
|
# Don't do anything with the reply
|
||||||
# This is the listen only command.
|
# This is the listen only command.
|
||||||
self.plugin_manager.run(packet)
|
self.plugin_manager.run(packet)
|
||||||
else:
|
else:
|
||||||
|
packet_log.log(packet)
|
||||||
if self.plugin_manager:
|
if self.plugin_manager:
|
||||||
# Don't do anything with the reply.
|
# Don't do anything with the reply.
|
||||||
# This is the listen only command.
|
# This is the listen only command.
|
||||||
self.plugin_manager.run(packet)
|
self.plugin_manager.run(packet)
|
||||||
else:
|
|
||||||
packet.log(header="RX")
|
|
||||||
|
|
||||||
packets.PacketList().rx(packet)
|
packet_collector.PacketCollector().rx(packet)
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
|
@ -96,11 +105,16 @@ class APRSDListenThread(rx.APRSDRXThread):
|
||||||
"--packet-filter",
|
"--packet-filter",
|
||||||
type=click.Choice(
|
type=click.Choice(
|
||||||
[
|
[
|
||||||
packets.Packet.__name__,
|
|
||||||
packets.AckPacket.__name__,
|
packets.AckPacket.__name__,
|
||||||
|
packets.BeaconPacket.__name__,
|
||||||
packets.GPSPacket.__name__,
|
packets.GPSPacket.__name__,
|
||||||
packets.MicEPacket.__name__,
|
packets.MicEPacket.__name__,
|
||||||
packets.MessagePacket.__name__,
|
packets.MessagePacket.__name__,
|
||||||
|
packets.ObjectPacket.__name__,
|
||||||
|
packets.RejectPacket.__name__,
|
||||||
|
packets.StatusPacket.__name__,
|
||||||
|
packets.ThirdPartyPacket.__name__,
|
||||||
|
packets.UnknownPacket.__name__,
|
||||||
packets.WeatherPacket.__name__,
|
packets.WeatherPacket.__name__,
|
||||||
],
|
],
|
||||||
case_sensitive=False,
|
case_sensitive=False,
|
||||||
|
@ -159,32 +173,32 @@ def listen(
|
||||||
LOG.info(f"APRSD Listen Started version: {aprsd.__version__}")
|
LOG.info(f"APRSD Listen Started version: {aprsd.__version__}")
|
||||||
|
|
||||||
CONF.log_opt_values(LOG, logging.DEBUG)
|
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||||
|
collector.Collector()
|
||||||
|
|
||||||
# Try and load saved MsgTrack list
|
# Try and load saved MsgTrack list
|
||||||
LOG.debug("Loading saved MsgTrack object.")
|
LOG.debug("Loading saved MsgTrack object.")
|
||||||
|
|
||||||
# Initialize the client factory and create
|
# Initialize the client factory and create
|
||||||
# The correct client object ready for use
|
# The correct client object ready for use
|
||||||
client.ClientFactory.setup()
|
|
||||||
# Make sure we have 1 client transport enabled
|
# Make sure we have 1 client transport enabled
|
||||||
if not client.factory.is_client_enabled():
|
if not client_factory.is_client_enabled():
|
||||||
LOG.error("No Clients are enabled in config.")
|
LOG.error("No Clients are enabled in config.")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
# Creates the client object
|
# Creates the client object
|
||||||
LOG.info("Creating client connection")
|
LOG.info("Creating client connection")
|
||||||
aprs_client = client.factory.create()
|
aprs_client = client_factory.create()
|
||||||
LOG.info(aprs_client)
|
LOG.info(aprs_client)
|
||||||
|
|
||||||
LOG.debug(f"Filter by '{filter}'")
|
LOG.debug(f"Filter by '{filter}'")
|
||||||
aprs_client.set_filter(filter)
|
aprs_client.set_filter(filter)
|
||||||
|
|
||||||
keepalive = threads.KeepAliveThread()
|
keepalive = keep_alive.KeepAliveThread()
|
||||||
keepalive.start()
|
# keepalive.start()
|
||||||
|
|
||||||
if CONF.rpc_settings.enabled:
|
if not CONF.enable_seen_list:
|
||||||
rpc = rpc_server.APRSDRPCThread()
|
# just deregister the class from the packet collector
|
||||||
rpc.start()
|
packet_collector.PacketCollector().unregister(seen_list.SeenList)
|
||||||
|
|
||||||
pm = None
|
pm = None
|
||||||
pm = plugin.PluginManager()
|
pm = plugin.PluginManager()
|
||||||
|
@ -196,6 +210,8 @@ def listen(
|
||||||
"Not Loading any plugins use --load-plugins to load what's "
|
"Not Loading any plugins use --load-plugins to load what's "
|
||||||
"defined in the config file.",
|
"defined in the config file.",
|
||||||
)
|
)
|
||||||
|
stats = stats_thread.APRSDStatsStoreThread()
|
||||||
|
stats.start()
|
||||||
|
|
||||||
LOG.debug("Create APRSDListenThread")
|
LOG.debug("Create APRSDListenThread")
|
||||||
listen_thread = APRSDListenThread(
|
listen_thread = APRSDListenThread(
|
||||||
|
@ -205,10 +221,10 @@ def listen(
|
||||||
)
|
)
|
||||||
LOG.debug("Start APRSDListenThread")
|
LOG.debug("Start APRSDListenThread")
|
||||||
listen_thread.start()
|
listen_thread.start()
|
||||||
|
|
||||||
|
keepalive.start()
|
||||||
LOG.debug("keepalive Join")
|
LOG.debug("keepalive Join")
|
||||||
keepalive.join()
|
keepalive.join()
|
||||||
LOG.debug("listen_thread Join")
|
LOG.debug("listen_thread Join")
|
||||||
listen_thread.join()
|
listen_thread.join()
|
||||||
|
stats.join()
|
||||||
if CONF.rpc_settings.enabled:
|
|
||||||
rpc.join()
|
|
||||||
|
|
|
@ -8,9 +8,11 @@ import click
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import cli_helper, client, packets
|
from aprsd import cli_helper, packets
|
||||||
from aprsd import conf # noqa : F401
|
from aprsd import conf # noqa : F401
|
||||||
|
from aprsd.client import client_factory
|
||||||
from aprsd.main import cli
|
from aprsd.main import cli
|
||||||
|
from aprsd.packets import collector
|
||||||
from aprsd.threads import tx
|
from aprsd.threads import tx
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,7 +78,6 @@ def send_message(
|
||||||
aprs_login = CONF.aprs_network.login
|
aprs_login = CONF.aprs_network.login
|
||||||
|
|
||||||
if not aprs_password:
|
if not aprs_password:
|
||||||
LOG.warning(CONF.aprs_network.password)
|
|
||||||
if not CONF.aprs_network.password:
|
if not CONF.aprs_network.password:
|
||||||
click.echo("Must set --aprs-password or APRS_PASSWORD")
|
click.echo("Must set --aprs-password or APRS_PASSWORD")
|
||||||
ctx.exit(-1)
|
ctx.exit(-1)
|
||||||
|
@ -102,9 +103,9 @@ def send_message(
|
||||||
|
|
||||||
def rx_packet(packet):
|
def rx_packet(packet):
|
||||||
global got_ack, got_response
|
global got_ack, got_response
|
||||||
cl = client.factory.create()
|
cl = client_factory.create()
|
||||||
packet = cl.decode_packet(packet)
|
packet = cl.decode_packet(packet)
|
||||||
packets.PacketList().rx(packet)
|
collector.PacketCollector().rx(packet)
|
||||||
packet.log("RX")
|
packet.log("RX")
|
||||||
# LOG.debug("Got packet back {}".format(packet))
|
# LOG.debug("Got packet back {}".format(packet))
|
||||||
if isinstance(packet, packets.AckPacket):
|
if isinstance(packet, packets.AckPacket):
|
||||||
|
@ -130,8 +131,7 @@ def send_message(
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
client.ClientFactory.setup()
|
client_factory.create().client
|
||||||
client.factory.create().client
|
|
||||||
except LoginError:
|
except LoginError:
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
|
@ -163,7 +163,7 @@ def send_message(
|
||||||
# This will register a packet consumer with aprslib
|
# This will register a packet consumer with aprslib
|
||||||
# When new packets come in the consumer will process
|
# When new packets come in the consumer will process
|
||||||
# the packet
|
# the packet
|
||||||
aprs_client = client.factory.create().client
|
aprs_client = client_factory.create().client
|
||||||
aprs_client.consumer(rx_packet, raw=False)
|
aprs_client.consumer(rx_packet, raw=False)
|
||||||
except aprslib.exceptions.ConnectionDrop:
|
except aprslib.exceptions.ConnectionDrop:
|
||||||
LOG.error("Connection dropped, reconnecting")
|
LOG.error("Connection dropped, reconnecting")
|
||||||
|
|
|
@ -6,12 +6,16 @@ import click
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import cli_helper, client
|
from aprsd import cli_helper
|
||||||
from aprsd import main as aprsd_main
|
from aprsd import main as aprsd_main
|
||||||
from aprsd import packets, plugin, threads, utils
|
from aprsd import packets, plugin, threads, utils
|
||||||
|
from aprsd.client import client_factory
|
||||||
from aprsd.main import cli
|
from aprsd.main import cli
|
||||||
from aprsd.rpc import server as rpc_server
|
from aprsd.packets import collector as packet_collector
|
||||||
from aprsd.threads import rx
|
from aprsd.packets import seen_list
|
||||||
|
from aprsd.threads import keep_alive, log_monitor, registry, rx
|
||||||
|
from aprsd.threads import stats as stats_thread
|
||||||
|
from aprsd.threads import tx
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
@ -46,7 +50,14 @@ def server(ctx, flush):
|
||||||
|
|
||||||
# Initialize the client factory and create
|
# Initialize the client factory and create
|
||||||
# The correct client object ready for use
|
# The correct client object ready for use
|
||||||
client.ClientFactory.setup()
|
if not client_factory.is_client_enabled():
|
||||||
|
LOG.error("No Clients are enabled in config.")
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
# Creates the client object
|
||||||
|
LOG.info("Creating client connection")
|
||||||
|
aprs_client = client_factory.create()
|
||||||
|
LOG.info(aprs_client)
|
||||||
|
|
||||||
# Create the initial PM singleton and Register plugins
|
# Create the initial PM singleton and Register plugins
|
||||||
# We register plugins first here so we can register each
|
# We register plugins first here so we can register each
|
||||||
|
@ -68,18 +79,14 @@ def server(ctx, flush):
|
||||||
LOG.info(p)
|
LOG.info(p)
|
||||||
|
|
||||||
# Make sure we have 1 client transport enabled
|
# Make sure we have 1 client transport enabled
|
||||||
if not client.factory.is_client_enabled():
|
if not client_factory.is_client_enabled():
|
||||||
LOG.error("No Clients are enabled in config.")
|
LOG.error("No Clients are enabled in config.")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
if not client.factory.is_client_configured():
|
if not client_factory.is_client_configured():
|
||||||
LOG.error("APRS client is not properly configured in config file.")
|
LOG.error("APRS client is not properly configured in config file.")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
# Creates the client object
|
|
||||||
# LOG.info("Creating client connection")
|
|
||||||
# client.factory.create().client
|
|
||||||
|
|
||||||
# Now load the msgTrack from disk if any
|
# Now load the msgTrack from disk if any
|
||||||
packets.PacketList()
|
packets.PacketList()
|
||||||
if flush:
|
if flush:
|
||||||
|
@ -87,16 +94,25 @@ def server(ctx, flush):
|
||||||
packets.PacketTrack().flush()
|
packets.PacketTrack().flush()
|
||||||
packets.WatchList().flush()
|
packets.WatchList().flush()
|
||||||
packets.SeenList().flush()
|
packets.SeenList().flush()
|
||||||
|
packets.PacketList().flush()
|
||||||
else:
|
else:
|
||||||
# Try and load saved MsgTrack list
|
# Try and load saved MsgTrack list
|
||||||
LOG.debug("Loading saved MsgTrack object.")
|
LOG.debug("Loading saved MsgTrack object.")
|
||||||
packets.PacketTrack().load()
|
packets.PacketTrack().load()
|
||||||
packets.WatchList().load()
|
packets.WatchList().load()
|
||||||
packets.SeenList().load()
|
packets.SeenList().load()
|
||||||
|
packets.PacketList().load()
|
||||||
|
|
||||||
keepalive = threads.KeepAliveThread()
|
keepalive = keep_alive.KeepAliveThread()
|
||||||
keepalive.start()
|
keepalive.start()
|
||||||
|
|
||||||
|
if not CONF.enable_seen_list:
|
||||||
|
# just deregister the class from the packet collector
|
||||||
|
packet_collector.PacketCollector().unregister(seen_list.SeenList)
|
||||||
|
|
||||||
|
stats_store_thread = stats_thread.APRSDStatsStoreThread()
|
||||||
|
stats_store_thread.start()
|
||||||
|
|
||||||
rx_thread = rx.APRSDPluginRXThread(
|
rx_thread = rx.APRSDPluginRXThread(
|
||||||
packet_queue=threads.packet_queue,
|
packet_queue=threads.packet_queue,
|
||||||
)
|
)
|
||||||
|
@ -106,13 +122,19 @@ def server(ctx, flush):
|
||||||
rx_thread.start()
|
rx_thread.start()
|
||||||
process_thread.start()
|
process_thread.start()
|
||||||
|
|
||||||
packets.PacketTrack().restart()
|
if CONF.enable_beacon:
|
||||||
|
LOG.info("Beacon Enabled. Starting Beacon thread.")
|
||||||
|
bcn_thread = tx.BeaconSendThread()
|
||||||
|
bcn_thread.start()
|
||||||
|
|
||||||
if CONF.rpc_settings.enabled:
|
if CONF.aprs_registry.enabled:
|
||||||
rpc = rpc_server.APRSDRPCThread()
|
LOG.info("Registry Enabled. Starting Registry thread.")
|
||||||
rpc.start()
|
registry_thread = registry.APRSRegistryThread()
|
||||||
log_monitor = threads.log_monitor.LogMonitorThread()
|
registry_thread.start()
|
||||||
log_monitor.start()
|
|
||||||
|
if CONF.admin.web_enabled:
|
||||||
|
log_monitor_thread = log_monitor.LogMonitorThread()
|
||||||
|
log_monitor_thread.start()
|
||||||
|
|
||||||
rx_thread.join()
|
rx_thread.join()
|
||||||
process_thread.join()
|
process_thread.join()
|
||||||
|
|
|
@ -1,37 +1,49 @@
|
||||||
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
|
||||||
import time
|
import time
|
||||||
|
|
||||||
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.client import client_factory, kiss
|
||||||
from aprsd.main import cli
|
from aprsd.main import cli
|
||||||
from aprsd.threads import rx, tx
|
from aprsd.threads import aprsd as aprsd_threads
|
||||||
|
from aprsd.threads import keep_alive, rx, tx
|
||||||
from aprsd.utils import trace
|
from aprsd.utils import trace
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger()
|
||||||
auth = HTTPBasicAuth()
|
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",
|
||||||
|
@ -52,7 +64,7 @@ def signal_handler(sig, frame):
|
||||||
time.sleep(1.5)
|
time.sleep(1.5)
|
||||||
# packets.WatchList().save()
|
# packets.WatchList().save()
|
||||||
# packets.SeenList().save()
|
# packets.SeenList().save()
|
||||||
LOG.info(stats.APRSDStats())
|
LOG.info(stats.stats_collector.collect())
|
||||||
LOG.info("Telling flask to bail.")
|
LOG.info("Telling flask to bail.")
|
||||||
signal.signal(signal.SIGTERM, sys.exit(0))
|
signal.signal(signal.SIGTERM, sys.exit(0))
|
||||||
|
|
||||||
|
@ -121,8 +133,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 +324,52 @@ 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):
|
||||||
LOG.info(f"process MessagePacket {repr(packet)}")
|
global callsign_locations
|
||||||
|
# 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
|
||||||
|
|
||||||
|
@ -155,10 +379,10 @@ def _get_transport(stats):
|
||||||
transport = "aprs-is"
|
transport = "aprs-is"
|
||||||
aprs_connection = (
|
aprs_connection = (
|
||||||
"APRS-IS Server: <a href='http://status.aprs2.net' >"
|
"APRS-IS Server: <a href='http://status.aprs2.net' >"
|
||||||
"{}</a>".format(stats["stats"]["aprs-is"]["server"])
|
"{}</a>".format(stats["APRSClientStats"]["server_string"])
|
||||||
)
|
)
|
||||||
elif client.KISSClient.is_enabled():
|
elif kiss.KISSClient.is_enabled():
|
||||||
transport = client.KISSClient.transport()
|
transport = kiss.KISSClient.transport()
|
||||||
if transport == client.TRANSPORT_TCPKISS:
|
if transport == client.TRANSPORT_TCPKISS:
|
||||||
aprs_connection = (
|
aprs_connection = (
|
||||||
"TCPKISS://{}:{}".format(
|
"TCPKISS://{}:{}".format(
|
||||||
|
@ -181,6 +405,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():
|
||||||
|
@ -190,7 +420,7 @@ def index():
|
||||||
html_template = "index.html"
|
html_template = "index.html"
|
||||||
LOG.debug(f"Template {html_template}")
|
LOG.debug(f"Template {html_template}")
|
||||||
|
|
||||||
transport, aprs_connection = _get_transport(stats)
|
transport, aprs_connection = _get_transport(stats["stats"])
|
||||||
LOG.debug(f"transport {transport} aprs_connection {aprs_connection}")
|
LOG.debug(f"transport {transport} aprs_connection {aprs_connection}")
|
||||||
|
|
||||||
stats["transport"] = transport
|
stats["transport"] = transport
|
||||||
|
@ -216,7 +446,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()
|
||||||
|
@ -225,27 +455,28 @@ def send_message_status():
|
||||||
|
|
||||||
|
|
||||||
def _stats():
|
def _stats():
|
||||||
stats_obj = stats.APRSDStats()
|
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
|
|
||||||
time_format = "%m-%d-%Y %H:%M:%S"
|
time_format = "%m-%d-%Y %H:%M:%S"
|
||||||
stats_dict = stats_obj.stats()
|
stats_dict = stats.stats_collector.collect(serializable=True)
|
||||||
# Webchat doesnt need these
|
# Webchat doesnt need these
|
||||||
if "watch_list" in stats_dict["aprsd"]:
|
if "WatchList" in stats_dict:
|
||||||
del stats_dict["aprsd"]["watch_list"]
|
del stats_dict["WatchList"]
|
||||||
if "seen_list" in stats_dict["aprsd"]:
|
if "SeenList" in stats_dict:
|
||||||
del stats_dict["aprsd"]["seen_list"]
|
del stats_dict["SeenList"]
|
||||||
if "threads" in stats_dict["aprsd"]:
|
if "APRSDThreadList" in stats_dict:
|
||||||
del stats_dict["aprsd"]["threads"]
|
del stats_dict["APRSDThreadList"]
|
||||||
# del stats_dict["email"]
|
if "PacketList" in stats_dict:
|
||||||
# del stats_dict["plugins"]
|
del stats_dict["PacketList"]
|
||||||
# del stats_dict["messages"]
|
if "EmailStats" in stats_dict:
|
||||||
|
del stats_dict["EmailStats"]
|
||||||
|
if "PluginManager" in stats_dict:
|
||||||
|
del stats_dict["PluginManager"]
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"time": now.strftime(time_format),
|
"time": now.strftime(time_format),
|
||||||
"stats": stats_dict,
|
"stats": stats_dict,
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@ -309,18 +540,27 @@ class SendMessageNamespace(Namespace):
|
||||||
|
|
||||||
def on_gps(self, data):
|
def on_gps(self, data):
|
||||||
LOG.debug(f"WS on_GPS: {data}")
|
LOG.debug(f"WS on_GPS: {data}")
|
||||||
lat = aprslib_util.latitude_to_ddm(data["latitude"])
|
lat = data["latitude"]
|
||||||
long = aprslib_util.longitude_to_ddm(data["longitude"])
|
long = data["longitude"]
|
||||||
LOG.debug(f"Lat DDM {lat}")
|
LOG.debug(f"Lat {lat}")
|
||||||
LOG.debug(f"Long DDM {long}")
|
LOG.debug(f"Long {long}")
|
||||||
|
path = data.get("path", None)
|
||||||
|
if not path:
|
||||||
|
path = []
|
||||||
|
elif "," in path:
|
||||||
|
path_opts = path.split(",")
|
||||||
|
path = [x.strip() for x in path_opts]
|
||||||
|
else:
|
||||||
|
path = [path]
|
||||||
|
|
||||||
tx.send(
|
tx.send(
|
||||||
packets.GPSPacket(
|
packets.BeaconPacket(
|
||||||
from_call=CONF.callsign,
|
from_call=CONF.callsign,
|
||||||
to_call="APDW16",
|
to_call="APDW16",
|
||||||
latitude=lat,
|
latitude=lat,
|
||||||
longitude=long,
|
longitude=long,
|
||||||
comment="APRSD WebChat Beacon",
|
comment="APRSD WebChat Beacon",
|
||||||
|
path=path,
|
||||||
),
|
),
|
||||||
direct=True,
|
direct=True,
|
||||||
)
|
)
|
||||||
|
@ -331,53 +571,19 @@ 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)
|
|
||||||
|
|
||||||
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(
|
||||||
|
@ -424,7 +630,7 @@ def webchat(ctx, flush, port):
|
||||||
LOG.info(msg)
|
LOG.info(msg)
|
||||||
LOG.info(f"APRSD Started version: {aprsd.__version__}")
|
LOG.info(f"APRSD Started version: {aprsd.__version__}")
|
||||||
|
|
||||||
CONF.log_opt_values(LOG, logging.DEBUG)
|
CONF.log_opt_values(logging.getLogger(), logging.DEBUG)
|
||||||
user = CONF.admin.user
|
user = CONF.admin.user
|
||||||
users[user] = generate_password_hash(CONF.admin.password)
|
users[user] = generate_password_hash(CONF.admin.password)
|
||||||
if not port:
|
if not port:
|
||||||
|
@ -432,13 +638,12 @@ def webchat(ctx, flush, port):
|
||||||
|
|
||||||
# Initialize the client factory and create
|
# Initialize the client factory and create
|
||||||
# The correct client object ready for use
|
# The correct client object ready for use
|
||||||
client.ClientFactory.setup()
|
|
||||||
# Make sure we have 1 client transport enabled
|
# Make sure we have 1 client transport enabled
|
||||||
if not client.factory.is_client_enabled():
|
if not client_factory.is_client_enabled():
|
||||||
LOG.error("No Clients are enabled in config.")
|
LOG.error("No Clients are enabled in config.")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
if not client.factory.is_client_configured():
|
if not client_factory.is_client_configured():
|
||||||
LOG.error("APRS client is not properly configured in config file.")
|
LOG.error("APRS client is not properly configured in config file.")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
|
@ -447,7 +652,7 @@ def webchat(ctx, flush, port):
|
||||||
packets.WatchList()
|
packets.WatchList()
|
||||||
packets.SeenList()
|
packets.SeenList()
|
||||||
|
|
||||||
keepalive = threads.KeepAliveThread()
|
keepalive = keep_alive.KeepAliveThread()
|
||||||
LOG.info("Start KeepAliveThread")
|
LOG.info("Start KeepAliveThread")
|
||||||
keepalive.start()
|
keepalive.start()
|
||||||
|
|
||||||
|
|
|
@ -15,15 +15,16 @@ watch_list_group = cfg.OptGroup(
|
||||||
name="watch_list",
|
name="watch_list",
|
||||||
title="Watch List settings",
|
title="Watch List settings",
|
||||||
)
|
)
|
||||||
rpc_group = cfg.OptGroup(
|
|
||||||
name="rpc_settings",
|
|
||||||
title="RPC Settings for admin <--> web",
|
|
||||||
)
|
|
||||||
webchat_group = cfg.OptGroup(
|
webchat_group = cfg.OptGroup(
|
||||||
name="webchat",
|
name="webchat",
|
||||||
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 +68,74 @@ 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.",
|
||||||
|
),
|
||||||
|
cfg.StrOpt(
|
||||||
|
"log_packet_format",
|
||||||
|
choices=["compact", "multiline", "both"],
|
||||||
|
default="compact",
|
||||||
|
help="When logging packets 'compact' will use a single line formatted for each packet."
|
||||||
|
"'multiline' will use multiple lines for each packet and is the traditional format."
|
||||||
|
"both will log both compact and multiline.",
|
||||||
|
),
|
||||||
|
cfg.IntOpt(
|
||||||
|
"default_packet_send_count",
|
||||||
|
default=3,
|
||||||
|
help="The number of times to send a non ack packet before giving up.",
|
||||||
|
),
|
||||||
|
cfg.IntOpt(
|
||||||
|
"default_ack_send_count",
|
||||||
|
default=3,
|
||||||
|
help="The number of times to send an ack packet in response to recieving a packet.",
|
||||||
|
),
|
||||||
|
cfg.IntOpt(
|
||||||
|
"packet_list_maxlen",
|
||||||
|
default=100,
|
||||||
|
help="The maximum number of packets to store in the packet list.",
|
||||||
|
),
|
||||||
|
cfg.IntOpt(
|
||||||
|
"packet_list_stats_maxlen",
|
||||||
|
default=20,
|
||||||
|
help="The maximum number of packets to send in the stats dict for admin ui.",
|
||||||
|
),
|
||||||
|
cfg.BoolOpt(
|
||||||
|
"enable_seen_list",
|
||||||
|
default=True,
|
||||||
|
help="Enable the Callsign seen list tracking feature. This allows aprsd to keep track of "
|
||||||
|
"callsigns that have been seen and when they were last seen.",
|
||||||
|
),
|
||||||
|
cfg.BoolOpt(
|
||||||
|
"enable_packet_logging",
|
||||||
|
default=True,
|
||||||
|
help="Set this to False, to disable logging of packets to the log file.",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
watch_list_opts = [
|
watch_list_opts = [
|
||||||
|
@ -107,7 +173,7 @@ admin_opts = [
|
||||||
default=False,
|
default=False,
|
||||||
help="Enable the Admin Web Interface",
|
help="Enable the Admin Web Interface",
|
||||||
),
|
),
|
||||||
cfg.IPOpt(
|
cfg.StrOpt(
|
||||||
"web_ip",
|
"web_ip",
|
||||||
default="0.0.0.0",
|
default="0.0.0.0",
|
||||||
help="The ip address to listen on",
|
help="The ip address to listen on",
|
||||||
|
@ -130,28 +196,6 @@ admin_opts = [
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
rpc_opts = [
|
|
||||||
cfg.BoolOpt(
|
|
||||||
"enabled",
|
|
||||||
default=True,
|
|
||||||
help="Enable RPC calls",
|
|
||||||
),
|
|
||||||
cfg.StrOpt(
|
|
||||||
"ip",
|
|
||||||
default="localhost",
|
|
||||||
help="The ip address to listen on",
|
|
||||||
),
|
|
||||||
cfg.PortOpt(
|
|
||||||
"port",
|
|
||||||
default=18861,
|
|
||||||
help="The port to listen on",
|
|
||||||
),
|
|
||||||
cfg.StrOpt(
|
|
||||||
"magic_word",
|
|
||||||
default=APRSD_DEFAULT_MAGIC_WORD,
|
|
||||||
help="Magic word to authenticate requests between client/server",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
enabled_plugins_opts = [
|
enabled_plugins_opts = [
|
||||||
cfg.ListOpt(
|
cfg.ListOpt(
|
||||||
|
@ -174,7 +218,7 @@ enabled_plugins_opts = [
|
||||||
]
|
]
|
||||||
|
|
||||||
webchat_opts = [
|
webchat_opts = [
|
||||||
cfg.IPOpt(
|
cfg.StrOpt(
|
||||||
"web_ip",
|
"web_ip",
|
||||||
default="0.0.0.0",
|
default="0.0.0.0",
|
||||||
help="The ip address to listen on",
|
help="The ip address to listen on",
|
||||||
|
@ -194,6 +238,44 @@ webchat_opts = [
|
||||||
default=None,
|
default=None,
|
||||||
help="Longitude for the GPS Beacon button. If not set, the button will not be enabled.",
|
help="Longitude for the GPS Beacon button. If not set, the button will not be enabled.",
|
||||||
),
|
),
|
||||||
|
cfg.BoolOpt(
|
||||||
|
"disable_url_request_logging",
|
||||||
|
default=False,
|
||||||
|
help="Disable the logging of url requests in the webchat command.",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
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.",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -204,10 +286,10 @@ def register_opts(config):
|
||||||
config.register_opts(admin_opts, group=admin_group)
|
config.register_opts(admin_opts, group=admin_group)
|
||||||
config.register_group(watch_list_group)
|
config.register_group(watch_list_group)
|
||||||
config.register_opts(watch_list_opts, group=watch_list_group)
|
config.register_opts(watch_list_opts, group=watch_list_group)
|
||||||
config.register_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():
|
||||||
|
@ -215,6 +297,6 @@ def list_opts():
|
||||||
"DEFAULT": (aprsd_opts + enabled_plugins_opts),
|
"DEFAULT": (aprsd_opts + enabled_plugins_opts),
|
||||||
admin_group.name: admin_opts,
|
admin_group.name: admin_opts,
|
||||||
watch_list_group.name: watch_list_opts,
|
watch_list_group.name: watch_list_opts,
|
||||||
rpc_group.name: rpc_opts,
|
|
||||||
webchat_group.name: webchat_opts,
|
webchat_group.name: webchat_opts,
|
||||||
|
registry_group.name: registry_opts,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
183
aprsd/log/log.py
183
aprsd/log/log.py
|
@ -1,89 +1,138 @@
|
||||||
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.conf import log as conf_log
|
||||||
from aprsd.log import rich as aprsd_logging
|
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
LOG = logging.getLogger("APRSD")
|
# LOG = logging.getLogger("APRSD")
|
||||||
logging_queue = queue.Queue()
|
LOG = logger
|
||||||
|
|
||||||
|
|
||||||
|
class QueueLatest(queue.Queue):
|
||||||
|
"""Custom Queue to keep only the latest N items.
|
||||||
|
|
||||||
|
This prevents the queue from blowing up in size.
|
||||||
|
"""
|
||||||
|
def put(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
super().put(*args, **kwargs)
|
||||||
|
except queue.Full:
|
||||||
|
self.queue.popleft()
|
||||||
|
super().put(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
logging_queue = QueueLatest(maxsize=200)
|
||||||
|
|
||||||
|
|
||||||
|
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",
|
||||||
|
]
|
||||||
|
webserver_list = [
|
||||||
|
"werkzeug",
|
||||||
|
"werkzeug._internal",
|
||||||
|
"socketio",
|
||||||
|
"urllib3.connectionpool",
|
||||||
|
"chardet",
|
||||||
|
"chardet.charsetgroupprober",
|
||||||
|
"chardet.eucjpprober",
|
||||||
|
"chardet.mbcharsetprober",
|
||||||
|
]
|
||||||
|
|
||||||
|
# We don't really want to see the aprslib parsing debug output.
|
||||||
|
disable_list = imap_list + aprslib_list + webserver_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
|
||||||
|
|
||||||
|
if CONF.webchat.disable_url_request_logging:
|
||||||
|
for name in webserver_list:
|
||||||
|
logging.getLogger(name).handlers = []
|
||||||
|
logging.getLogger(name).propagate = True
|
||||||
|
logging.getLogger(name).setLevel(logging.ERROR)
|
||||||
|
|
||||||
|
handlers = [
|
||||||
|
{
|
||||||
|
"sink": sys.stdout,
|
||||||
|
"serialize": False,
|
||||||
|
"format": CONF.logging.logformat,
|
||||||
|
"colorize": True,
|
||||||
|
"level": log_level,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
if CONF.logging.logfile:
|
||||||
|
handlers.append(
|
||||||
|
{
|
||||||
|
"sink": CONF.logging.logfile,
|
||||||
|
"serialize": False,
|
||||||
|
"format": CONF.logging.logformat,
|
||||||
|
"colorize": False,
|
||||||
|
"level": log_level,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
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,
|
||||||
|
"level": log_level,
|
||||||
|
"colorize": False,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
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)
|
logger.level("DEBUG", color="<fg #BABABA>")
|
||||||
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
|
|
|
@ -24,18 +24,17 @@ import datetime
|
||||||
import importlib.metadata as imp
|
import importlib.metadata as imp
|
||||||
from importlib.metadata import version as metadata_version
|
from importlib.metadata import version as metadata_version
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import click_completion
|
|
||||||
from oslo_config import cfg, generator
|
from oslo_config import cfg, generator
|
||||||
|
|
||||||
# local imports here
|
# local imports here
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import cli_helper, packets, stats, threads, utils
|
from aprsd import cli_helper, packets, threads, utils
|
||||||
|
from aprsd.stats import collector
|
||||||
|
|
||||||
|
|
||||||
# setup the global logger
|
# setup the global logger
|
||||||
|
@ -44,35 +43,27 @@ CONF = cfg.CONF
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||||
flask_enabled = False
|
flask_enabled = False
|
||||||
rpc_serv = None
|
|
||||||
|
|
||||||
|
|
||||||
def custom_startswith(string, incomplete):
|
@click.group(cls=cli_helper.AliasedGroup, context_settings=CONTEXT_SETTINGS)
|
||||||
"""A custom completion match that supports case insensitive matching."""
|
|
||||||
if os.environ.get("_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE"):
|
|
||||||
string = string.lower()
|
|
||||||
incomplete = incomplete.lower()
|
|
||||||
return string.startswith(incomplete)
|
|
||||||
|
|
||||||
|
|
||||||
click_completion.core.startswith = custom_startswith
|
|
||||||
click_completion.init()
|
|
||||||
|
|
||||||
|
|
||||||
@click.group(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")
|
||||||
|
|
||||||
|
|
||||||
|
@ -91,7 +82,8 @@ def signal_handler(sig, frame):
|
||||||
packets.PacketTrack().save()
|
packets.PacketTrack().save()
|
||||||
packets.WatchList().save()
|
packets.WatchList().save()
|
||||||
packets.SeenList().save()
|
packets.SeenList().save()
|
||||||
LOG.info(stats.APRSDStats())
|
packets.PacketList().save()
|
||||||
|
LOG.info(collector.Collector().collect())
|
||||||
# signal.signal(signal.SIGTERM, sys.exit(0))
|
# signal.signal(signal.SIGTERM, sys.exit(0))
|
||||||
# sys.exit(0)
|
# sys.exit(0)
|
||||||
|
|
||||||
|
@ -117,15 +109,25 @@ def check_version(ctx):
|
||||||
def sample_config(ctx):
|
def sample_config(ctx):
|
||||||
"""Generate a sample Config file from aprsd and all installed plugins."""
|
"""Generate a sample Config file from aprsd and all installed plugins."""
|
||||||
|
|
||||||
|
def _get_selected_entry_points():
|
||||||
|
import sys
|
||||||
|
if sys.version_info < (3, 10):
|
||||||
|
all = imp.entry_points()
|
||||||
|
selected = []
|
||||||
|
if "oslo.config.opts" in all:
|
||||||
|
for x in all["oslo.config.opts"]:
|
||||||
|
if x.group == "oslo.config.opts":
|
||||||
|
selected.append(x)
|
||||||
|
else:
|
||||||
|
selected = imp.entry_points(group="oslo.config.opts")
|
||||||
|
|
||||||
|
return selected
|
||||||
|
|
||||||
def get_namespaces():
|
def get_namespaces():
|
||||||
args = []
|
args = []
|
||||||
|
|
||||||
all = imp.entry_points()
|
# selected = imp.entry_points(group="oslo.config.opts")
|
||||||
selected = []
|
selected = _get_selected_entry_points()
|
||||||
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,7 +147,6 @@ def sample_config(ctx):
|
||||||
if not sys.argv[1:]:
|
if not sys.argv[1:]:
|
||||||
raise SystemExit
|
raise SystemExit
|
||||||
raise
|
raise
|
||||||
LOG.warning(conf.namespace)
|
|
||||||
generator.generate(conf)
|
generator.generate(conf)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from aprsd.packets.core import ( # noqa: F401
|
from aprsd.packets.core import ( # noqa: F401
|
||||||
AckPacket, GPSPacket, MessagePacket, MicEPacket, Packet, RejectPacket,
|
AckPacket, BeaconPacket, BulletinPacket, GPSPacket, MessagePacket,
|
||||||
StatusPacket, WeatherPacket,
|
MicEPacket, ObjectPacket, Packet, RejectPacket, StatusPacket,
|
||||||
|
ThirdPartyPacket, UnknownPacket, WeatherPacket, factory,
|
||||||
)
|
)
|
||||||
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
|
||||||
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
import logging
|
||||||
|
from typing import Callable, Protocol, runtime_checkable
|
||||||
|
|
||||||
|
from aprsd.packets import core
|
||||||
|
from aprsd.utils import singleton
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class PacketMonitor(Protocol):
|
||||||
|
"""Protocol for Monitoring packets in some way."""
|
||||||
|
|
||||||
|
def rx(self, packet: type[core.Packet]) -> None:
|
||||||
|
"""When we get a packet from the network."""
|
||||||
|
...
|
||||||
|
|
||||||
|
def tx(self, packet: type[core.Packet]) -> None:
|
||||||
|
"""When we send a packet out the network."""
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@singleton
|
||||||
|
class PacketCollector:
|
||||||
|
def __init__(self):
|
||||||
|
self.monitors: list[Callable] = []
|
||||||
|
|
||||||
|
def register(self, monitor: Callable) -> None:
|
||||||
|
self.monitors.append(monitor)
|
||||||
|
|
||||||
|
def unregister(self, monitor: Callable) -> None:
|
||||||
|
self.monitors.remove(monitor)
|
||||||
|
|
||||||
|
def rx(self, packet: type[core.Packet]) -> None:
|
||||||
|
for name in self.monitors:
|
||||||
|
cls = name()
|
||||||
|
if isinstance(cls, PacketMonitor):
|
||||||
|
try:
|
||||||
|
cls.rx(packet)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error(f"Error in monitor {name} (rx): {e}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise TypeError(f"Monitor {name} is not a PacketMonitor")
|
||||||
|
|
||||||
|
def tx(self, packet: type[core.Packet]) -> None:
|
||||||
|
for name in self.monitors:
|
||||||
|
cls = name()
|
||||||
|
if isinstance(cls, PacketMonitor):
|
||||||
|
try:
|
||||||
|
cls.tx(packet)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error(f"Error in monitor {name} (tx): {e}")
|
||||||
|
else:
|
||||||
|
raise TypeError(f"Monitor {name} is not a PacketMonitor")
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,143 @@
|
||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from aprsd.packets.core import AckPacket, RejectPacket
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger()
|
||||||
|
LOGU = logger
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
FROM_COLOR = "fg #C70039"
|
||||||
|
TO_COLOR = "fg #D033FF"
|
||||||
|
TX_COLOR = "red"
|
||||||
|
RX_COLOR = "green"
|
||||||
|
PACKET_COLOR = "cyan"
|
||||||
|
|
||||||
|
|
||||||
|
def log_multiline(packet, tx: Optional[bool] = False, header: Optional[bool] = True) -> None:
|
||||||
|
"""LOG a packet to the logfile."""
|
||||||
|
if not CONF.enable_packet_logging:
|
||||||
|
return
|
||||||
|
if CONF.log_packet_format == "compact":
|
||||||
|
return
|
||||||
|
|
||||||
|
# asdict(packet)
|
||||||
|
logit = ["\n"]
|
||||||
|
name = packet.__class__.__name__
|
||||||
|
|
||||||
|
if isinstance(packet, AckPacket):
|
||||||
|
pkt_max_send_count = CONF.default_ack_send_count
|
||||||
|
else:
|
||||||
|
pkt_max_send_count = CONF.default_packet_send_count
|
||||||
|
|
||||||
|
if header:
|
||||||
|
if tx:
|
||||||
|
header_str = f"<{TX_COLOR}>TX</{TX_COLOR}>"
|
||||||
|
logit.append(
|
||||||
|
f"{header_str}________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}> "
|
||||||
|
f"TX:{packet.send_count + 1} of {pkt_max_send_count}",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
header_str = f"<{RX_COLOR}>RX</{RX_COLOR}>"
|
||||||
|
logit.append(
|
||||||
|
f"{header_str}________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}>)",
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
header_str = ""
|
||||||
|
logit.append(f"__________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}>)")
|
||||||
|
# log_list.append(f" Packet : {packet.__class__.__name__}")
|
||||||
|
if packet.msgNo:
|
||||||
|
logit.append(f" Msg # : {packet.msgNo}")
|
||||||
|
if packet.from_call:
|
||||||
|
logit.append(f" From : <{FROM_COLOR}>{packet.from_call}</{FROM_COLOR}>")
|
||||||
|
if packet.to_call:
|
||||||
|
logit.append(f" To : <{TO_COLOR}>{packet.to_call}</{TO_COLOR}>")
|
||||||
|
if hasattr(packet, "path") and packet.path:
|
||||||
|
logit.append(f" Path : {'=>'.join(packet.path)}")
|
||||||
|
if hasattr(packet, "via") and packet.via:
|
||||||
|
logit.append(f" VIA : {packet.via}")
|
||||||
|
|
||||||
|
if not isinstance(packet, AckPacket) and not isinstance(packet, RejectPacket):
|
||||||
|
msg = packet.human_info
|
||||||
|
|
||||||
|
if msg:
|
||||||
|
msg = msg.replace("<", "\\<")
|
||||||
|
logit.append(f" Info : <light-yellow><b>{msg}</b></light-yellow>")
|
||||||
|
|
||||||
|
if hasattr(packet, "comment") and packet.comment:
|
||||||
|
logit.append(f" Comment : {packet.comment}")
|
||||||
|
|
||||||
|
raw = packet.raw.replace("<", "\\<")
|
||||||
|
logit.append(f" Raw : <fg #828282>{raw}</fg #828282>")
|
||||||
|
logit.append(f"{header_str}________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}>)")
|
||||||
|
|
||||||
|
LOGU.opt(colors=True).info("\n".join(logit))
|
||||||
|
LOG.debug(repr(packet))
|
||||||
|
|
||||||
|
|
||||||
|
def log(packet, tx: Optional[bool] = False, header: Optional[bool] = True) -> None:
|
||||||
|
if not CONF.enable_packet_logging:
|
||||||
|
return
|
||||||
|
if CONF.log_packet_format == "multiline":
|
||||||
|
log_multiline(packet, tx, header)
|
||||||
|
return
|
||||||
|
|
||||||
|
logit = []
|
||||||
|
name = packet.__class__.__name__
|
||||||
|
if isinstance(packet, AckPacket):
|
||||||
|
pkt_max_send_count = CONF.default_ack_send_count
|
||||||
|
else:
|
||||||
|
pkt_max_send_count = CONF.default_packet_send_count
|
||||||
|
|
||||||
|
if header:
|
||||||
|
if tx:
|
||||||
|
via_color = "red"
|
||||||
|
arrow = f"<{via_color}>-></{via_color}>"
|
||||||
|
logit.append(
|
||||||
|
f"<red>TX {arrow}</red> "
|
||||||
|
f"<cyan>{name}</cyan>"
|
||||||
|
f":{packet.msgNo}"
|
||||||
|
f" ({packet.send_count + 1} of {pkt_max_send_count})",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
via_color = "fg #828282"
|
||||||
|
arrow = f"<{via_color}>-></{via_color}>"
|
||||||
|
left_arrow = f"<{via_color}><-</{via_color}>"
|
||||||
|
logit.append(
|
||||||
|
f"<fg #1AA730>RX</fg #1AA730> {left_arrow} "
|
||||||
|
f"<cyan>{name}</cyan>"
|
||||||
|
f":{packet.msgNo}",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
via_color = "green"
|
||||||
|
arrow = f"<{via_color}>-></{via_color}>"
|
||||||
|
logit.append(
|
||||||
|
f"<cyan>{name}</cyan>"
|
||||||
|
f":{packet.msgNo}",
|
||||||
|
)
|
||||||
|
|
||||||
|
tmp = None
|
||||||
|
if packet.path:
|
||||||
|
tmp = f"{arrow}".join(packet.path) + f"{arrow} "
|
||||||
|
|
||||||
|
logit.append(
|
||||||
|
f"<{FROM_COLOR}>{packet.from_call}</{FROM_COLOR}> {arrow}"
|
||||||
|
f"{tmp if tmp else ' '}"
|
||||||
|
f"<{TO_COLOR}>{packet.to_call}</{TO_COLOR}>",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(packet, AckPacket) and not isinstance(packet, RejectPacket):
|
||||||
|
logit.append(":")
|
||||||
|
msg = packet.human_info
|
||||||
|
|
||||||
|
if msg:
|
||||||
|
msg = msg.replace("<", "\\<")
|
||||||
|
logit.append(f"<light-yellow><b>{msg}</b></light-yellow>")
|
||||||
|
|
||||||
|
LOGU.opt(colors=True).info(" ".join(logit))
|
||||||
|
log_multiline(packet, tx, header)
|
|
@ -1,99 +1,116 @@
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from collections.abc import MutableMapping
|
|
||||||
import logging
|
import logging
|
||||||
import threading
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
import wrapt
|
|
||||||
|
|
||||||
from aprsd import stats
|
from aprsd.packets import collector, core
|
||||||
from aprsd.packets import seen_list
|
from aprsd.utils import objectstore
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class PacketList(MutableMapping):
|
class PacketList(objectstore.ObjectStoreMixin):
|
||||||
|
"""Class to keep track of the packets we tx/rx."""
|
||||||
_instance = None
|
_instance = None
|
||||||
lock = threading.Lock()
|
|
||||||
_total_rx: int = 0
|
_total_rx: int = 0
|
||||||
_total_tx: int = 0
|
_total_tx: int = 0
|
||||||
types = {}
|
maxlen: int = 100
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super().__new__(cls)
|
cls._instance = super().__new__(cls)
|
||||||
cls._maxlen = 100
|
cls._instance.maxlen = CONF.packet_list_maxlen
|
||||||
cls.d = OrderedDict()
|
cls._instance._init_data()
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
def _init_data(self):
|
||||||
def rx(self, packet):
|
self.data = {
|
||||||
"""Add a packet that was received."""
|
"types": {},
|
||||||
self._total_rx += 1
|
"packets": OrderedDict(),
|
||||||
self._add(packet)
|
}
|
||||||
ptype = packet.__class__.__name__
|
|
||||||
if not ptype in self.types:
|
|
||||||
self.types[ptype] = {"tx": 0, "rx": 0}
|
|
||||||
self.types[ptype]["rx"] += 1
|
|
||||||
seen_list.SeenList().update_seen(packet)
|
|
||||||
stats.APRSDStats().rx(packet)
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
def rx(self, packet: type[core.Packet]):
|
||||||
def tx(self, packet):
|
|
||||||
"""Add a packet that was received."""
|
"""Add a packet that was received."""
|
||||||
self._total_tx += 1
|
with self.lock:
|
||||||
self._add(packet)
|
self._total_rx += 1
|
||||||
ptype = packet.__class__.__name__
|
self._add(packet)
|
||||||
if not ptype in self.types:
|
ptype = packet.__class__.__name__
|
||||||
self.types[ptype] = {"tx": 0, "rx": 0}
|
if not ptype in self.data["types"]:
|
||||||
self.types[ptype]["tx"] += 1
|
self.data["types"][ptype] = {"tx": 0, "rx": 0}
|
||||||
seen_list.SeenList().update_seen(packet)
|
self.data["types"][ptype]["rx"] += 1
|
||||||
stats.APRSDStats().tx(packet)
|
|
||||||
|
def tx(self, packet: type[core.Packet]):
|
||||||
|
"""Add a packet that was received."""
|
||||||
|
with self.lock:
|
||||||
|
self._total_tx += 1
|
||||||
|
self._add(packet)
|
||||||
|
ptype = packet.__class__.__name__
|
||||||
|
if not ptype in self.data["types"]:
|
||||||
|
self.data["types"][ptype] = {"tx": 0, "rx": 0}
|
||||||
|
self.data["types"][ptype]["tx"] += 1
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def add(self, packet):
|
def add(self, packet):
|
||||||
self._add(packet)
|
with self.lock:
|
||||||
|
self._add(packet)
|
||||||
|
|
||||||
def _add(self, packet):
|
def _add(self, packet):
|
||||||
self[packet.key] = packet
|
if not self.data.get("packets"):
|
||||||
|
self._init_data()
|
||||||
|
if packet.key in self.data["packets"]:
|
||||||
|
self.data["packets"].move_to_end(packet.key)
|
||||||
|
elif len(self.data["packets"]) == self.maxlen:
|
||||||
|
self.data["packets"].popitem(last=False)
|
||||||
|
self.data["packets"][packet.key] = packet
|
||||||
|
|
||||||
def copy(self):
|
|
||||||
return self.d.copy()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def maxlen(self):
|
|
||||||
return self._maxlen
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def find(self, packet):
|
def find(self, packet):
|
||||||
return self.get(packet.key)
|
with self.lock:
|
||||||
|
return self.data["packets"][packet.key]
|
||||||
def __getitem__(self, key):
|
|
||||||
# self.d.move_to_end(key)
|
|
||||||
return self.d[key]
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
if key in self.d:
|
|
||||||
self.d.move_to_end(key)
|
|
||||||
elif len(self.d) == self.maxlen:
|
|
||||||
self.d.popitem(last=False)
|
|
||||||
self.d[key] = value
|
|
||||||
|
|
||||||
def __delitem__(self, key):
|
|
||||||
del self.d[key]
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return self.d.__iter__()
|
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return len(self.d)
|
with self.lock:
|
||||||
|
return len(self.data["packets"])
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def total_rx(self):
|
def total_rx(self):
|
||||||
return self._total_rx
|
with self.lock:
|
||||||
|
return self._total_rx
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def total_tx(self):
|
def total_tx(self):
|
||||||
return self._total_tx
|
with self.lock:
|
||||||
|
return self._total_tx
|
||||||
|
|
||||||
|
def stats(self, serializable=False) -> dict:
|
||||||
|
# limit the number of packets to return to 50
|
||||||
|
with self.lock:
|
||||||
|
tmp = OrderedDict(
|
||||||
|
reversed(
|
||||||
|
list(
|
||||||
|
self.data.get("packets", OrderedDict()).items(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
pkts = []
|
||||||
|
count = 1
|
||||||
|
for packet in tmp:
|
||||||
|
pkts.append(tmp[packet])
|
||||||
|
count += 1
|
||||||
|
if count > CONF.packet_list_stats_maxlen:
|
||||||
|
break
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
"total_tracked": self._total_rx + self._total_rx,
|
||||||
|
"rx": self._total_rx,
|
||||||
|
"tx": self._total_tx,
|
||||||
|
"types": self.data.get("types", []),
|
||||||
|
"packet_count": len(self.data.get("packets", [])),
|
||||||
|
"maxlen": self.maxlen,
|
||||||
|
"packets": pkts,
|
||||||
|
}
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
# Now register the PacketList with the collector
|
||||||
|
# every packet we RX and TX goes through the collector
|
||||||
|
# for processing for whatever reason is needed.
|
||||||
|
collector.PacketCollector().register(PacketList)
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import threading
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
import wrapt
|
|
||||||
|
|
||||||
|
from aprsd.packets import collector, core
|
||||||
from aprsd.utils import objectstore
|
from aprsd.utils import objectstore
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,28 +15,40 @@ class SeenList(objectstore.ObjectStoreMixin):
|
||||||
"""Global callsign seen list."""
|
"""Global callsign seen list."""
|
||||||
|
|
||||||
_instance = None
|
_instance = None
|
||||||
lock = threading.Lock()
|
|
||||||
data: dict = {}
|
data: dict = {}
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super().__new__(cls)
|
cls._instance = super().__new__(cls)
|
||||||
cls._instance._init_store()
|
|
||||||
cls._instance.data = {}
|
cls._instance.data = {}
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
def stats(self, serializable=False):
|
||||||
def update_seen(self, packet):
|
"""Return the stats for the PacketTrack class."""
|
||||||
callsign = None
|
with self.lock:
|
||||||
if packet.from_call:
|
return self.data
|
||||||
callsign = packet.from_call
|
|
||||||
else:
|
def rx(self, packet: type[core.Packet]):
|
||||||
LOG.warning(f"Can't find FROM in packet {packet}")
|
"""When we get a packet from the network, update the seen list."""
|
||||||
return
|
with self.lock:
|
||||||
if callsign not in self.data:
|
callsign = None
|
||||||
self.data[callsign] = {
|
if packet.from_call:
|
||||||
"last": None,
|
callsign = packet.from_call
|
||||||
"count": 0,
|
else:
|
||||||
}
|
LOG.warning(f"Can't find FROM in packet {packet}")
|
||||||
self.data[callsign]["last"] = str(datetime.datetime.now())
|
return
|
||||||
self.data[callsign]["count"] += 1
|
if callsign not in self.data:
|
||||||
|
self.data[callsign] = {
|
||||||
|
"last": None,
|
||||||
|
"count": 0,
|
||||||
|
}
|
||||||
|
self.data[callsign]["last"] = datetime.datetime.now()
|
||||||
|
self.data[callsign]["count"] += 1
|
||||||
|
|
||||||
|
def tx(self, packet: type[core.Packet]):
|
||||||
|
"""We don't care about TX packets."""
|
||||||
|
|
||||||
|
|
||||||
|
# Register with the packet collector so we can process the packet
|
||||||
|
# when we get it off the client (network)
|
||||||
|
collector.PacketCollector().register(SeenList)
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import datetime
|
import datetime
|
||||||
import threading
|
import logging
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
import wrapt
|
|
||||||
|
|
||||||
from aprsd.threads import tx
|
from aprsd.packets import collector, core
|
||||||
from aprsd.utils import objectstore
|
from aprsd.utils import objectstore
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class PacketTrack(objectstore.ObjectStoreMixin):
|
class PacketTrack(objectstore.ObjectStoreMixin):
|
||||||
|
@ -26,7 +26,6 @@ class PacketTrack(objectstore.ObjectStoreMixin):
|
||||||
|
|
||||||
_instance = None
|
_instance = None
|
||||||
_start_time = None
|
_start_time = None
|
||||||
lock = threading.Lock()
|
|
||||||
|
|
||||||
data: dict = {}
|
data: dict = {}
|
||||||
total_tracked: int = 0
|
total_tracked: int = 0
|
||||||
|
@ -38,74 +37,73 @@ class PacketTrack(objectstore.ObjectStoreMixin):
|
||||||
cls._instance._init_store()
|
cls._instance._init_store()
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def __getitem__(self, name):
|
def __getitem__(self, name):
|
||||||
return self.data[name]
|
with self.lock:
|
||||||
|
return self.data[name]
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
return iter(self.data)
|
with self.lock:
|
||||||
|
return iter(self.data)
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def keys(self):
|
def keys(self):
|
||||||
return self.data.keys()
|
with self.lock:
|
||||||
|
return self.data.keys()
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def items(self):
|
def items(self):
|
||||||
return self.data.items()
|
with self.lock:
|
||||||
|
return self.data.items()
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def values(self):
|
def values(self):
|
||||||
return self.data.values()
|
with self.lock:
|
||||||
|
return self.data.values()
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
def stats(self, serializable=False):
|
||||||
def __len__(self):
|
with self.lock:
|
||||||
return len(self.data)
|
stats = {
|
||||||
|
"total_tracked": self.total_tracked,
|
||||||
|
}
|
||||||
|
pkts = {}
|
||||||
|
for key in self.data:
|
||||||
|
last_send_time = self.data[key].last_send_time
|
||||||
|
pkts[key] = {
|
||||||
|
"last_send_time": last_send_time,
|
||||||
|
"send_count": self.data[key].send_count,
|
||||||
|
"retry_count": self.data[key].retry_count,
|
||||||
|
"message": self.data[key].raw,
|
||||||
|
}
|
||||||
|
stats["packets"] = pkts
|
||||||
|
return stats
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
def rx(self, packet: type[core.Packet]) -> None:
|
||||||
def add(self, packet):
|
"""When we get a packet from the network, check if we should remove it."""
|
||||||
key = packet.msgNo
|
if isinstance(packet, core.AckPacket):
|
||||||
packet._last_send_attempt = 0
|
self._remove(packet.msgNo)
|
||||||
self.data[key] = packet
|
elif isinstance(packet, core.RejectPacket):
|
||||||
self.total_tracked += 1
|
self._remove(packet.msgNo)
|
||||||
|
elif hasattr(packet, "ackMsgNo"):
|
||||||
|
# Got a piggyback ack, so remove the original message
|
||||||
|
self._remove(packet.ackMsgNo)
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
def tx(self, packet: type[core.Packet]) -> None:
|
||||||
def get(self, key):
|
"""Add a packet that was sent."""
|
||||||
return self.data.get(key, None)
|
with self.lock:
|
||||||
|
key = packet.msgNo
|
||||||
|
packet.send_count = 0
|
||||||
|
self.data[key] = packet
|
||||||
|
self.total_tracked += 1
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def remove(self, key):
|
def remove(self, key):
|
||||||
try:
|
self._remove(key)
|
||||||
del self.data[key]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def restart(self):
|
def _remove(self, key):
|
||||||
"""Walk the list of messages and restart them if any."""
|
with self.lock:
|
||||||
for key in self.data.keys():
|
try:
|
||||||
pkt = self.data[key]
|
del self.data[key]
|
||||||
if pkt._last_send_attempt < pkt.retry_count:
|
except KeyError:
|
||||||
tx.send(pkt)
|
pass
|
||||||
|
|
||||||
def _resend(self, packet):
|
|
||||||
packet._last_send_attempt = 0
|
|
||||||
tx.send(packet)
|
|
||||||
|
|
||||||
def restart_delayed(self, count=None, most_recent=True):
|
# Now register the PacketList with the collector
|
||||||
"""Walk the list of delayed messages and restart them if any."""
|
# every packet we RX and TX goes through the collector
|
||||||
if not count:
|
# for processing for whatever reason is needed.
|
||||||
# Send all the delayed messages
|
collector.PacketCollector().register(PacketTrack)
|
||||||
for key in self.data.keys():
|
|
||||||
pkt = self.data[key]
|
|
||||||
if pkt._last_send_attempt == pkt._retry_count:
|
|
||||||
self._resend(pkt)
|
|
||||||
else:
|
|
||||||
# They want to resend <count> delayed messages
|
|
||||||
tmp = sorted(
|
|
||||||
self.data.items(),
|
|
||||||
reverse=most_recent,
|
|
||||||
key=lambda x: x[1].last_send_time,
|
|
||||||
)
|
|
||||||
pkt_list = tmp[:count]
|
|
||||||
for (_key, pkt) in pkt_list:
|
|
||||||
self._resend(pkt)
|
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import threading
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
import wrapt
|
|
||||||
|
|
||||||
from aprsd import utils
|
from aprsd import utils
|
||||||
|
from aprsd.packets import collector, core
|
||||||
from aprsd.utils import objectstore
|
from aprsd.utils import objectstore
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,56 +16,75 @@ class WatchList(objectstore.ObjectStoreMixin):
|
||||||
"""Global watch list and info for callsigns."""
|
"""Global watch list and info for callsigns."""
|
||||||
|
|
||||||
_instance = None
|
_instance = None
|
||||||
lock = threading.Lock()
|
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super().__new__(cls)
|
cls._instance = super().__new__(cls)
|
||||||
cls._instance._init_store()
|
|
||||||
cls._instance.data = {}
|
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
def __init__(self, config=None):
|
def __init__(self):
|
||||||
ring_size = CONF.watch_list.packet_keep_count
|
super().__init__()
|
||||||
|
self._update_from_conf()
|
||||||
|
|
||||||
if CONF.watch_list.callsigns:
|
def _update_from_conf(self, config=None):
|
||||||
for callsign in CONF.watch_list.callsigns:
|
with self.lock:
|
||||||
call = callsign.replace("*", "")
|
if CONF.watch_list.enabled and CONF.watch_list.callsigns:
|
||||||
# FIXME(waboring) - we should fetch the last time we saw
|
for callsign in CONF.watch_list.callsigns:
|
||||||
# a beacon from a callsign or some other mechanism to find
|
call = callsign.replace("*", "")
|
||||||
# last time a message was seen by aprs-is. For now this
|
# FIXME(waboring) - we should fetch the last time we saw
|
||||||
# is all we can do.
|
# a beacon from a callsign or some other mechanism to find
|
||||||
self.data[call] = {
|
# last time a message was seen by aprs-is. For now this
|
||||||
"last": datetime.datetime.now(),
|
# is all we can do.
|
||||||
"packets": utils.RingBuffer(
|
if call not in self.data:
|
||||||
ring_size,
|
self.data[call] = {
|
||||||
),
|
"last": None,
|
||||||
|
"packet": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
def stats(self, serializable=False) -> dict:
|
||||||
|
stats = {}
|
||||||
|
with self.lock:
|
||||||
|
for callsign in self.data:
|
||||||
|
stats[callsign] = {
|
||||||
|
"last": self.data[callsign]["last"],
|
||||||
|
"packet": self.data[callsign]["packet"],
|
||||||
|
"age": self.age(callsign),
|
||||||
|
"old": self.is_old(callsign),
|
||||||
}
|
}
|
||||||
|
return stats
|
||||||
|
|
||||||
def is_enabled(self):
|
def is_enabled(self):
|
||||||
return CONF.watch_list.enabled
|
return CONF.watch_list.enabled
|
||||||
|
|
||||||
def callsign_in_watchlist(self, callsign):
|
def callsign_in_watchlist(self, callsign):
|
||||||
return callsign in self.data
|
with self.lock:
|
||||||
|
return callsign in self.data
|
||||||
|
|
||||||
|
def rx(self, packet: type[core.Packet]) -> None:
|
||||||
|
"""Track when we got a packet from the network."""
|
||||||
|
callsign = packet.from_call
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def update_seen(self, packet):
|
|
||||||
if packet.addresse:
|
|
||||||
callsign = packet.addresse
|
|
||||||
else:
|
|
||||||
callsign = packet.from_call
|
|
||||||
if self.callsign_in_watchlist(callsign):
|
if self.callsign_in_watchlist(callsign):
|
||||||
self.data[callsign]["last"] = datetime.datetime.now()
|
with self.lock:
|
||||||
self.data[callsign]["packets"].append(packet)
|
self.data[callsign]["last"] = datetime.datetime.now()
|
||||||
|
self.data[callsign]["packet"] = packet
|
||||||
|
|
||||||
|
def tx(self, packet: type[core.Packet]) -> None:
|
||||||
|
"""We don't care about TX packets."""
|
||||||
|
|
||||||
def last_seen(self, callsign):
|
def last_seen(self, callsign):
|
||||||
if self.callsign_in_watchlist(callsign):
|
with self.lock:
|
||||||
return self.data[callsign]["last"]
|
if self.callsign_in_watchlist(callsign):
|
||||||
|
return self.data[callsign]["last"]
|
||||||
|
|
||||||
def age(self, callsign):
|
def age(self, callsign):
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
return str(now - self.last_seen(callsign))
|
last_seen_time = self.last_seen(callsign)
|
||||||
|
if last_seen_time:
|
||||||
|
return str(now - last_seen_time)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def max_delta(self, seconds=None):
|
def max_delta(self, seconds=None):
|
||||||
if not seconds:
|
if not seconds:
|
||||||
|
@ -83,14 +101,22 @@ class WatchList(objectstore.ObjectStoreMixin):
|
||||||
We put this here so any notification plugin can use this
|
We put this here so any notification plugin can use this
|
||||||
same test.
|
same test.
|
||||||
"""
|
"""
|
||||||
|
if not self.callsign_in_watchlist(callsign):
|
||||||
|
return False
|
||||||
|
|
||||||
age = self.age(callsign)
|
age = self.age(callsign)
|
||||||
|
if age:
|
||||||
|
delta = utils.parse_delta_str(age)
|
||||||
|
d = datetime.timedelta(**delta)
|
||||||
|
|
||||||
delta = utils.parse_delta_str(age)
|
max_delta = self.max_delta(seconds=seconds)
|
||||||
d = datetime.timedelta(**delta)
|
|
||||||
|
|
||||||
max_delta = self.max_delta(seconds=seconds)
|
if d > max_delta:
|
||||||
|
return True
|
||||||
if d > max_delta:
|
else:
|
||||||
return True
|
return False
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
collector.PacketCollector().register(WatchList)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
# The base plugin class
|
from __future__ import annotations
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
import importlib
|
import importlib
|
||||||
import inspect
|
import inspect
|
||||||
|
@ -42,7 +43,7 @@ class APRSDPluginSpec:
|
||||||
"""A hook specification namespace."""
|
"""A hook specification namespace."""
|
||||||
|
|
||||||
@hookspec
|
@hookspec
|
||||||
def filter(self, packet: packets.core.Packet):
|
def filter(self, packet: type[packets.Packet]):
|
||||||
"""My special little hook that you can customize."""
|
"""My special little hook that you can customize."""
|
||||||
|
|
||||||
|
|
||||||
|
@ -65,7 +66,7 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
||||||
self.threads = self.create_threads() or []
|
self.threads = self.create_threads() or []
|
||||||
self.start_threads()
|
self.start_threads()
|
||||||
|
|
||||||
def start_threads(self):
|
def start_threads(self) -> None:
|
||||||
if self.enabled and self.threads:
|
if self.enabled and self.threads:
|
||||||
if not isinstance(self.threads, list):
|
if not isinstance(self.threads, list):
|
||||||
self.threads = [self.threads]
|
self.threads = [self.threads]
|
||||||
|
@ -90,10 +91,10 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def message_count(self):
|
def message_count(self) -> int:
|
||||||
return self.message_counter
|
return self.message_counter
|
||||||
|
|
||||||
def help(self):
|
def help(self) -> str:
|
||||||
return "Help!"
|
return "Help!"
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
|
@ -118,11 +119,11 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
||||||
thread.stop()
|
thread.stop()
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def filter(self, packet: packets.core.Packet):
|
def filter(self, packet: type[packets.Packet]) -> str | packets.MessagePacket:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def process(self, packet: packets.core.Packet):
|
def process(self, packet: type[packets.Packet]):
|
||||||
"""This is called when the filter passes."""
|
"""This is called when the filter passes."""
|
||||||
|
|
||||||
|
|
||||||
|
@ -147,14 +148,14 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
||||||
watch_list = CONF.watch_list.callsigns
|
watch_list = CONF.watch_list.callsigns
|
||||||
# make sure the timeout is set or this doesn't work
|
# make sure the timeout is set or this doesn't work
|
||||||
if watch_list:
|
if watch_list:
|
||||||
aprs_client = client.factory.create().client
|
aprs_client = client.client_factory.create().client
|
||||||
filter_str = "b/{}".format("/".join(watch_list))
|
filter_str = "b/{}".format("/".join(watch_list))
|
||||||
aprs_client.set_filter(filter_str)
|
aprs_client.set_filter(filter_str)
|
||||||
else:
|
else:
|
||||||
LOG.warning("Watch list enabled, but no callsigns set.")
|
LOG.warning("Watch list enabled, but no callsigns set.")
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def filter(self, packet: packets.core.Packet):
|
def filter(self, packet: type[packets.Packet]) -> str | packets.MessagePacket:
|
||||||
result = packets.NULL_MESSAGE
|
result = packets.NULL_MESSAGE
|
||||||
if self.enabled:
|
if self.enabled:
|
||||||
wl = watch_list.WatchList()
|
wl = watch_list.WatchList()
|
||||||
|
@ -206,14 +207,14 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
||||||
self.enabled = True
|
self.enabled = True
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def filter(self, packet: packets.core.MessagePacket):
|
def filter(self, packet: packets.MessagePacket) -> str | packets.MessagePacket:
|
||||||
LOG.info(f"{self.__class__.__name__} called")
|
LOG.debug(f"{self.__class__.__name__} called")
|
||||||
if not self.enabled:
|
if not self.enabled:
|
||||||
result = f"{self.__class__.__name__} isn't enabled"
|
result = f"{self.__class__.__name__} isn't enabled"
|
||||||
LOG.warning(result)
|
LOG.warning(result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
if not isinstance(packet, packets.core.MessagePacket):
|
if not isinstance(packet, packets.MessagePacket):
|
||||||
LOG.warning(f"{self.__class__.__name__} Got a {packet.__class__.__name__} ignoring")
|
LOG.warning(f"{self.__class__.__name__} Got a {packet.__class__.__name__} ignoring")
|
||||||
return packets.NULL_MESSAGE
|
return packets.NULL_MESSAGE
|
||||||
|
|
||||||
|
@ -226,7 +227,7 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
||||||
# and is an APRS message format and has a message.
|
# and is an APRS message format and has a message.
|
||||||
if (
|
if (
|
||||||
tocall == CONF.callsign
|
tocall == CONF.callsign
|
||||||
and isinstance(packet, packets.core.MessagePacket)
|
and isinstance(packet, packets.MessagePacket)
|
||||||
and message
|
and message
|
||||||
):
|
):
|
||||||
if re.search(self.command_regex, message, re.IGNORECASE):
|
if re.search(self.command_regex, message, re.IGNORECASE):
|
||||||
|
@ -269,7 +270,7 @@ class HelpPlugin(APRSDRegexCommandPluginBase):
|
||||||
def help(self):
|
def help(self):
|
||||||
return "Help: send APRS help or help <plugin>"
|
return "Help: send APRS help or help <plugin>"
|
||||||
|
|
||||||
def process(self, packet: packets.core.MessagePacket):
|
def process(self, packet: packets.MessagePacket):
|
||||||
LOG.info("HelpPlugin")
|
LOG.info("HelpPlugin")
|
||||||
# fromcall = packet.get("from")
|
# fromcall = packet.get("from")
|
||||||
message = packet.message_text
|
message = packet.message_text
|
||||||
|
@ -343,6 +344,28 @@ class PluginManager:
|
||||||
self._watchlist_pm = pluggy.PluginManager("aprsd")
|
self._watchlist_pm = pluggy.PluginManager("aprsd")
|
||||||
self._watchlist_pm.add_hookspecs(APRSDPluginSpec)
|
self._watchlist_pm.add_hookspecs(APRSDPluginSpec)
|
||||||
|
|
||||||
|
def stats(self, serializable=False) -> dict:
|
||||||
|
"""Collect and return stats for all plugins."""
|
||||||
|
def full_name_with_qualname(obj):
|
||||||
|
return "{}.{}".format(
|
||||||
|
obj.__class__.__module__,
|
||||||
|
obj.__class__.__qualname__,
|
||||||
|
)
|
||||||
|
|
||||||
|
plugin_stats = {}
|
||||||
|
plugins = self.get_plugins()
|
||||||
|
if plugins:
|
||||||
|
|
||||||
|
for p in plugins:
|
||||||
|
plugin_stats[full_name_with_qualname(p)] = {
|
||||||
|
"enabled": p.enabled,
|
||||||
|
"rx": p.rx_count,
|
||||||
|
"tx": p.tx_count,
|
||||||
|
"version": p.version,
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugin_stats
|
||||||
|
|
||||||
def is_plugin(self, obj):
|
def is_plugin(self, obj):
|
||||||
for c in inspect.getmro(obj):
|
for c in inspect.getmro(obj):
|
||||||
if issubclass(c, APRSDPluginBase):
|
if issubclass(c, APRSDPluginBase):
|
||||||
|
@ -368,7 +391,9 @@ class PluginManager:
|
||||||
try:
|
try:
|
||||||
module_name, class_name = module_class_string.rsplit(".", 1)
|
module_name, class_name = module_class_string.rsplit(".", 1)
|
||||||
module = importlib.import_module(module_name)
|
module = importlib.import_module(module_name)
|
||||||
module = importlib.reload(module)
|
# Commented out because the email thread starts in a different context
|
||||||
|
# and hence gives a different singleton for the EmailStats
|
||||||
|
# module = importlib.reload(module)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
if not module_name:
|
if not module_name:
|
||||||
LOG.error(f"Failed to load Plugin {module_class_string}")
|
LOG.error(f"Failed to load Plugin {module_class_string}")
|
||||||
|
@ -469,12 +494,12 @@ class PluginManager:
|
||||||
|
|
||||||
LOG.info("Completed Plugin Loading.")
|
LOG.info("Completed Plugin Loading.")
|
||||||
|
|
||||||
def run(self, packet: packets.core.MessagePacket):
|
def run(self, packet: packets.MessagePacket):
|
||||||
"""Execute all the plugins run method."""
|
"""Execute all the plugins run method."""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
return self._pluggy_pm.hook.filter(packet=packet)
|
return self._pluggy_pm.hook.filter(packet=packet)
|
||||||
|
|
||||||
def run_watchlist(self, packet: packets.core.Packet):
|
def run_watchlist(self, packet: packets.Packet):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
return self._watchlist_pm.hook.filter(packet=packet)
|
return self._watchlist_pm.hook.filter(packet=packet)
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import time
|
||||||
import imapclient
|
import imapclient
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
from aprsd import packets, plugin, stats, threads
|
from aprsd import packets, plugin, threads, utils
|
||||||
from aprsd.threads import tx
|
from aprsd.threads import tx
|
||||||
from aprsd.utils import trace
|
from aprsd.utils import trace
|
||||||
|
|
||||||
|
@ -60,6 +60,38 @@ class EmailInfo:
|
||||||
self._delay = val
|
self._delay = val
|
||||||
|
|
||||||
|
|
||||||
|
@utils.singleton
|
||||||
|
class EmailStats:
|
||||||
|
"""Singleton object to store stats related to email."""
|
||||||
|
_instance = None
|
||||||
|
tx = 0
|
||||||
|
rx = 0
|
||||||
|
email_thread_last_time = None
|
||||||
|
|
||||||
|
def stats(self, serializable=False):
|
||||||
|
if CONF.email_plugin.enabled:
|
||||||
|
last_check_time = self.email_thread_last_time
|
||||||
|
if serializable and last_check_time:
|
||||||
|
last_check_time = last_check_time.isoformat()
|
||||||
|
stats = {
|
||||||
|
"tx": self.tx,
|
||||||
|
"rx": self.rx,
|
||||||
|
"last_check_time": last_check_time,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
stats = {}
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def tx_inc(self):
|
||||||
|
self.tx += 1
|
||||||
|
|
||||||
|
def rx_inc(self):
|
||||||
|
self.rx += 1
|
||||||
|
|
||||||
|
def email_thread_update(self):
|
||||||
|
self.email_thread_last_time = datetime.datetime.now()
|
||||||
|
|
||||||
|
|
||||||
class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||||
"""Email Plugin."""
|
"""Email Plugin."""
|
||||||
|
|
||||||
|
@ -190,10 +222,6 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||||
def _imap_connect():
|
def _imap_connect():
|
||||||
imap_port = CONF.email_plugin.imap_port
|
imap_port = CONF.email_plugin.imap_port
|
||||||
use_ssl = CONF.email_plugin.imap_use_ssl
|
use_ssl = CONF.email_plugin.imap_use_ssl
|
||||||
# host = CONFIG["aprsd"]["email"]["imap"]["host"]
|
|
||||||
# msg = "{}{}:{}".format("TLS " if use_ssl else "", host, imap_port)
|
|
||||||
# LOG.debug("Connect to IMAP host {} with user '{}'".
|
|
||||||
# format(msg, CONFIG['imap']['login']))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
server = imapclient.IMAPClient(
|
server = imapclient.IMAPClient(
|
||||||
|
@ -440,7 +468,7 @@ def send_email(to_addr, content):
|
||||||
[to_addr],
|
[to_addr],
|
||||||
msg.as_string(),
|
msg.as_string(),
|
||||||
)
|
)
|
||||||
stats.APRSDStats().email_tx_inc()
|
EmailStats().tx_inc()
|
||||||
except Exception:
|
except Exception:
|
||||||
LOG.exception("Sendmail Error!!!!")
|
LOG.exception("Sendmail Error!!!!")
|
||||||
server.quit()
|
server.quit()
|
||||||
|
@ -545,7 +573,7 @@ class APRSDEmailThread(threads.APRSDThread):
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
stats.APRSDStats().email_thread_update()
|
EmailStats().email_thread_update()
|
||||||
# always sleep for 5 seconds and see if we need to check email
|
# always sleep for 5 seconds and see if we need to check email
|
||||||
# This allows CTRL-C to stop the execution of this loop sooner
|
# This allows CTRL-C to stop the execution of this loop sooner
|
||||||
# than check_email_delay time
|
# than check_email_delay time
|
||||||
|
|
|
@ -1,81 +0,0 @@
|
||||||
import datetime
|
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
|
|
||||||
from aprsd import packets, plugin
|
|
||||||
from aprsd.packets import tracker
|
|
||||||
from aprsd.utils import trace
|
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
LOG = logging.getLogger("APRSD")
|
|
||||||
|
|
||||||
|
|
||||||
class QueryPlugin(plugin.APRSDRegexCommandPluginBase):
|
|
||||||
"""Query command."""
|
|
||||||
|
|
||||||
command_regex = r"^\!.*"
|
|
||||||
command_name = "query"
|
|
||||||
short_description = "APRSD Owner command to query messages in the MsgTrack"
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
"""Do any plugin setup here."""
|
|
||||||
if not CONF.query_plugin.callsign:
|
|
||||||
LOG.error("Config query_plugin.callsign not set. Disabling plugin")
|
|
||||||
self.enabled = False
|
|
||||||
self.enabled = True
|
|
||||||
|
|
||||||
@trace.trace
|
|
||||||
def process(self, packet: packets.MessagePacket):
|
|
||||||
LOG.info("Query COMMAND")
|
|
||||||
|
|
||||||
fromcall = packet.from_call
|
|
||||||
message = packet.get("message_text", None)
|
|
||||||
|
|
||||||
pkt_tracker = tracker.PacketTrack()
|
|
||||||
now = datetime.datetime.now()
|
|
||||||
reply = "Pending messages ({}) {}".format(
|
|
||||||
len(pkt_tracker),
|
|
||||||
now.strftime("%H:%M:%S"),
|
|
||||||
)
|
|
||||||
|
|
||||||
searchstring = "^" + CONF.query_plugin.callsign + ".*"
|
|
||||||
# only I can do admin commands
|
|
||||||
if re.search(searchstring, fromcall):
|
|
||||||
|
|
||||||
# resend last N most recent: "!3"
|
|
||||||
r = re.search(r"^\!([0-9]).*", message)
|
|
||||||
if r is not None:
|
|
||||||
if len(pkt_tracker) > 0:
|
|
||||||
last_n = r.group(1)
|
|
||||||
reply = packets.NULL_MESSAGE
|
|
||||||
LOG.debug(reply)
|
|
||||||
pkt_tracker.restart_delayed(count=int(last_n))
|
|
||||||
else:
|
|
||||||
reply = "No pending msgs to resend"
|
|
||||||
LOG.debug(reply)
|
|
||||||
return reply
|
|
||||||
|
|
||||||
# resend all: "!a"
|
|
||||||
r = re.search(r"^\![aA].*", message)
|
|
||||||
if r is not None:
|
|
||||||
if len(pkt_tracker) > 0:
|
|
||||||
reply = packets.NULL_MESSAGE
|
|
||||||
LOG.debug(reply)
|
|
||||||
pkt_tracker.restart_delayed()
|
|
||||||
else:
|
|
||||||
reply = "No pending msgs"
|
|
||||||
LOG.debug(reply)
|
|
||||||
return reply
|
|
||||||
|
|
||||||
# delete all: "!d"
|
|
||||||
r = re.search(r"^\![dD].*", message)
|
|
||||||
if r is not None:
|
|
||||||
reply = "Deleted ALL pending msgs."
|
|
||||||
LOG.debug(reply)
|
|
||||||
pkt_tracker.flush()
|
|
||||||
return reply
|
|
||||||
|
|
||||||
return reply
|
|
|
@ -1,9 +1,9 @@
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import time
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
import pytz
|
import pytz
|
||||||
|
from tzlocal import get_localzone
|
||||||
|
|
||||||
from aprsd import packets, plugin, plugin_utils
|
from aprsd import packets, plugin, plugin_utils
|
||||||
from aprsd.utils import fuzzy, trace
|
from aprsd.utils import fuzzy, trace
|
||||||
|
@ -22,7 +22,8 @@ class TimePlugin(plugin.APRSDRegexCommandPluginBase):
|
||||||
short_description = "What is the current local time."
|
short_description = "What is the current local time."
|
||||||
|
|
||||||
def _get_local_tz(self):
|
def _get_local_tz(self):
|
||||||
return pytz.timezone(time.strftime("%Z"))
|
lz = get_localzone()
|
||||||
|
return pytz.timezone(str(lz))
|
||||||
|
|
||||||
def _get_utcnow(self):
|
def _get_utcnow(self):
|
||||||
return pytz.datetime.datetime.utcnow()
|
return pytz.datetime.datetime.utcnow()
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import plugin, stats
|
from aprsd import plugin
|
||||||
|
from aprsd.stats import collector
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
@ -23,10 +24,8 @@ class VersionPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||||
# fromcall = packet.get("from")
|
# fromcall = packet.get("from")
|
||||||
# message = packet.get("message_text", None)
|
# message = packet.get("message_text", None)
|
||||||
# ack = packet.get("msgNo", "0")
|
# ack = packet.get("msgNo", "0")
|
||||||
stats_obj = stats.APRSDStats()
|
s = collector.Collector().collect()
|
||||||
s = stats_obj.stats()
|
|
||||||
print(s)
|
|
||||||
return "APRSD ver:{} uptime:{}".format(
|
return "APRSD ver:{} uptime:{}".format(
|
||||||
aprsd.__version__,
|
aprsd.__version__,
|
||||||
s["aprsd"]["uptime"],
|
s["APRSDStats"]["uptime"],
|
||||||
)
|
)
|
||||||
|
|
|
@ -110,7 +110,6 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def process(self, packet):
|
def process(self, packet):
|
||||||
print("FISTY")
|
|
||||||
fromcall = packet.get("from")
|
fromcall = packet.get("from")
|
||||||
message = packet.get("message_text", None)
|
message = packet.get("message_text", None)
|
||||||
# ack = packet.get("msgNo", "0")
|
# ack = packet.get("msgNo", "0")
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
import rpyc
|
|
||||||
|
|
||||||
|
|
||||||
class AuthSocketStream(rpyc.SocketStream):
|
|
||||||
"""Used to authenitcate the RPC stream to remote."""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def connect(cls, *args, authorizer=None, **kwargs):
|
|
||||||
stream_obj = super().connect(*args, **kwargs)
|
|
||||||
|
|
||||||
if callable(authorizer):
|
|
||||||
authorizer(stream_obj.sock)
|
|
||||||
|
|
||||||
return stream_obj
|
|
|
@ -1,165 +0,0 @@
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
import rpyc
|
|
||||||
|
|
||||||
from aprsd import conf # noqa
|
|
||||||
from aprsd import rpc
|
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
LOG = logging.getLogger("APRSD")
|
|
||||||
|
|
||||||
|
|
||||||
class RPCClient:
|
|
||||||
_instance = None
|
|
||||||
_rpc_client = None
|
|
||||||
|
|
||||||
ip = None
|
|
||||||
port = None
|
|
||||||
magic_word = None
|
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
|
||||||
if cls._instance is None:
|
|
||||||
cls._instance = super().__new__(cls)
|
|
||||||
return cls._instance
|
|
||||||
|
|
||||||
def __init__(self, ip=None, port=None, magic_word=None):
|
|
||||||
if ip:
|
|
||||||
self.ip = ip
|
|
||||||
else:
|
|
||||||
self.ip = CONF.rpc_settings.ip
|
|
||||||
if port:
|
|
||||||
self.port = int(port)
|
|
||||||
else:
|
|
||||||
self.port = CONF.rpc_settings.port
|
|
||||||
if magic_word:
|
|
||||||
self.magic_word = magic_word
|
|
||||||
else:
|
|
||||||
self.magic_word = CONF.rpc_settings.magic_word
|
|
||||||
self._check_settings()
|
|
||||||
self.get_rpc_client()
|
|
||||||
|
|
||||||
def _check_settings(self):
|
|
||||||
if not CONF.rpc_settings.enabled:
|
|
||||||
LOG.warning("RPC is not enabled, no way to get stats!!")
|
|
||||||
|
|
||||||
if self.magic_word == conf.common.APRSD_DEFAULT_MAGIC_WORD:
|
|
||||||
LOG.warning("You are using the default RPC magic word!!!")
|
|
||||||
LOG.warning("edit aprsd.conf and change rpc_settings.magic_word")
|
|
||||||
|
|
||||||
LOG.debug(f"RPC Client: {self.ip}:{self.port} {self.magic_word}")
|
|
||||||
|
|
||||||
def _rpyc_connect(
|
|
||||||
self, host, port, service=rpyc.VoidService,
|
|
||||||
config={}, ipv6=False,
|
|
||||||
keepalive=False, authorizer=None, ):
|
|
||||||
|
|
||||||
LOG.info(f"Connecting to RPC host '{host}:{port}'")
|
|
||||||
try:
|
|
||||||
s = rpc.AuthSocketStream.connect(
|
|
||||||
host, port, ipv6=ipv6, keepalive=keepalive,
|
|
||||||
authorizer=authorizer,
|
|
||||||
)
|
|
||||||
return rpyc.utils.factory.connect_stream(s, service, config=config)
|
|
||||||
except ConnectionRefusedError:
|
|
||||||
LOG.error(f"Failed to connect to RPC host '{host}:{port}'")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_rpc_client(self):
|
|
||||||
if not self._rpc_client:
|
|
||||||
self._rpc_client = self._rpyc_connect(
|
|
||||||
self.ip,
|
|
||||||
self.port,
|
|
||||||
authorizer=lambda sock: sock.send(self.magic_word.encode()),
|
|
||||||
)
|
|
||||||
return self._rpc_client
|
|
||||||
|
|
||||||
def get_stats_dict(self):
|
|
||||||
cl = self.get_rpc_client()
|
|
||||||
result = {}
|
|
||||||
if not cl:
|
|
||||||
return result
|
|
||||||
|
|
||||||
try:
|
|
||||||
rpc_stats_dict = cl.root.get_stats()
|
|
||||||
result = json.loads(rpc_stats_dict)
|
|
||||||
except EOFError:
|
|
||||||
LOG.error("Lost connection to RPC Host")
|
|
||||||
self._rpc_client = None
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_stats(self):
|
|
||||||
cl = self.get_rpc_client()
|
|
||||||
result = {}
|
|
||||||
if not cl:
|
|
||||||
return result
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = cl.root.get_stats_obj()
|
|
||||||
except EOFError:
|
|
||||||
LOG.error("Lost connection to RPC Host")
|
|
||||||
self._rpc_client = None
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_packet_track(self):
|
|
||||||
cl = self.get_rpc_client()
|
|
||||||
result = None
|
|
||||||
if not cl:
|
|
||||||
return result
|
|
||||||
try:
|
|
||||||
result = cl.root.get_packet_track()
|
|
||||||
except EOFError:
|
|
||||||
LOG.error("Lost connection to RPC Host")
|
|
||||||
self._rpc_client = None
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_packet_list(self):
|
|
||||||
cl = self.get_rpc_client()
|
|
||||||
result = None
|
|
||||||
if not cl:
|
|
||||||
return result
|
|
||||||
try:
|
|
||||||
result = cl.root.get_packet_list()
|
|
||||||
except EOFError:
|
|
||||||
LOG.error("Lost connection to RPC Host")
|
|
||||||
self._rpc_client = None
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_watch_list(self):
|
|
||||||
cl = self.get_rpc_client()
|
|
||||||
result = None
|
|
||||||
if not cl:
|
|
||||||
return result
|
|
||||||
try:
|
|
||||||
result = cl.root.get_watch_list()
|
|
||||||
except EOFError:
|
|
||||||
LOG.error("Lost connection to RPC Host")
|
|
||||||
self._rpc_client = None
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_seen_list(self):
|
|
||||||
cl = self.get_rpc_client()
|
|
||||||
result = None
|
|
||||||
if not cl:
|
|
||||||
return result
|
|
||||||
try:
|
|
||||||
result = cl.root.get_seen_list()
|
|
||||||
except EOFError:
|
|
||||||
LOG.error("Lost connection to RPC Host")
|
|
||||||
self._rpc_client = None
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_log_entries(self):
|
|
||||||
cl = self.get_rpc_client()
|
|
||||||
result = None
|
|
||||||
if not cl:
|
|
||||||
return result
|
|
||||||
try:
|
|
||||||
result_str = cl.root.get_log_entries()
|
|
||||||
result = json.loads(result_str)
|
|
||||||
except EOFError:
|
|
||||||
LOG.error("Lost connection to RPC Host")
|
|
||||||
self._rpc_client = None
|
|
||||||
return result
|
|
|
@ -1,99 +0,0 @@
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
import rpyc
|
|
||||||
from rpyc.utils.authenticators import AuthenticationError
|
|
||||||
from rpyc.utils.server import ThreadPoolServer
|
|
||||||
|
|
||||||
from aprsd import conf # noqa: F401
|
|
||||||
from aprsd import packets, stats, threads
|
|
||||||
from aprsd.threads import log_monitor
|
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
LOG = logging.getLogger("APRSD")
|
|
||||||
|
|
||||||
|
|
||||||
def magic_word_authenticator(sock):
|
|
||||||
client_ip = sock.getpeername()[0]
|
|
||||||
magic = sock.recv(len(CONF.rpc_settings.magic_word)).decode()
|
|
||||||
if magic != CONF.rpc_settings.magic_word:
|
|
||||||
LOG.error(
|
|
||||||
f"wrong magic word passed from {client_ip} "
|
|
||||||
"'{magic}' != '{CONF.rpc_settings.magic_word}'",
|
|
||||||
)
|
|
||||||
raise AuthenticationError(
|
|
||||||
f"wrong magic word passed in '{magic}'"
|
|
||||||
f" != '{CONF.rpc_settings.magic_word}'",
|
|
||||||
)
|
|
||||||
return sock, None
|
|
||||||
|
|
||||||
|
|
||||||
class APRSDRPCThread(threads.APRSDThread):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(name="RPCThread")
|
|
||||||
self.thread = ThreadPoolServer(
|
|
||||||
APRSDService,
|
|
||||||
port=CONF.rpc_settings.port,
|
|
||||||
protocol_config={"allow_public_attrs": True},
|
|
||||||
authenticator=magic_word_authenticator,
|
|
||||||
)
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
if self.thread:
|
|
||||||
self.thread.close()
|
|
||||||
self.thread_stop = True
|
|
||||||
|
|
||||||
def loop(self):
|
|
||||||
# there is no loop as run is blocked
|
|
||||||
if self.thread and not self.thread_stop:
|
|
||||||
# This is a blocking call
|
|
||||||
self.thread.start()
|
|
||||||
|
|
||||||
|
|
||||||
@rpyc.service
|
|
||||||
class APRSDService(rpyc.Service):
|
|
||||||
def on_connect(self, conn):
|
|
||||||
# code that runs when a connection is created
|
|
||||||
# (to init the service, if needed)
|
|
||||||
LOG.info("RPC Client Connected")
|
|
||||||
self._conn = conn
|
|
||||||
|
|
||||||
def on_disconnect(self, conn):
|
|
||||||
# code that runs after the connection has already closed
|
|
||||||
# (to finalize the service, if needed)
|
|
||||||
LOG.info("RPC Client Disconnected")
|
|
||||||
self._conn = None
|
|
||||||
|
|
||||||
@rpyc.exposed
|
|
||||||
def get_stats(self):
|
|
||||||
stat = stats.APRSDStats()
|
|
||||||
stats_dict = stat.stats()
|
|
||||||
return_str = json.dumps(stats_dict, indent=4, sort_keys=True, default=str)
|
|
||||||
return return_str
|
|
||||||
|
|
||||||
@rpyc.exposed
|
|
||||||
def get_stats_obj(self):
|
|
||||||
return stats.APRSDStats()
|
|
||||||
|
|
||||||
@rpyc.exposed
|
|
||||||
def get_packet_list(self):
|
|
||||||
return packets.PacketList()
|
|
||||||
|
|
||||||
@rpyc.exposed
|
|
||||||
def get_packet_track(self):
|
|
||||||
return packets.PacketTrack()
|
|
||||||
|
|
||||||
@rpyc.exposed
|
|
||||||
def get_watch_list(self):
|
|
||||||
return packets.WatchList()
|
|
||||||
|
|
||||||
@rpyc.exposed
|
|
||||||
def get_seen_list(self):
|
|
||||||
return packets.SeenList()
|
|
||||||
|
|
||||||
@rpyc.exposed
|
|
||||||
def get_log_entries(self):
|
|
||||||
entries = log_monitor.LogEntries().get_all_and_purge()
|
|
||||||
return json.dumps(entries, default=str)
|
|
266
aprsd/stats.py
266
aprsd/stats.py
|
@ -1,266 +0,0 @@
|
||||||
import datetime
|
|
||||||
import logging
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
import wrapt
|
|
||||||
|
|
||||||
import aprsd
|
|
||||||
from aprsd import packets, plugin, utils
|
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
LOG = logging.getLogger("APRSD")
|
|
||||||
|
|
||||||
|
|
||||||
class APRSDStats:
|
|
||||||
|
|
||||||
_instance = None
|
|
||||||
lock = threading.Lock()
|
|
||||||
|
|
||||||
start_time = None
|
|
||||||
_aprsis_server = None
|
|
||||||
_aprsis_keepalive = None
|
|
||||||
|
|
||||||
_email_thread_last_time = None
|
|
||||||
_email_tx = 0
|
|
||||||
_email_rx = 0
|
|
||||||
|
|
||||||
_mem_current = 0
|
|
||||||
_mem_peak = 0
|
|
||||||
|
|
||||||
_thread_info = {}
|
|
||||||
|
|
||||||
_pkt_cnt = {
|
|
||||||
"Packet": {
|
|
||||||
"tx": 0,
|
|
||||||
"rx": 0,
|
|
||||||
},
|
|
||||||
"AckPacket": {
|
|
||||||
"tx": 0,
|
|
||||||
"rx": 0,
|
|
||||||
},
|
|
||||||
"GPSPacket": {
|
|
||||||
"tx": 0,
|
|
||||||
"rx": 0,
|
|
||||||
},
|
|
||||||
"StatusPacket": {
|
|
||||||
"tx": 0,
|
|
||||||
"rx": 0,
|
|
||||||
},
|
|
||||||
"MicEPacket": {
|
|
||||||
"tx": 0,
|
|
||||||
"rx": 0,
|
|
||||||
},
|
|
||||||
"MessagePacket": {
|
|
||||||
"tx": 0,
|
|
||||||
"rx": 0,
|
|
||||||
},
|
|
||||||
"WeatherPacket": {
|
|
||||||
"tx": 0,
|
|
||||||
"rx": 0,
|
|
||||||
},
|
|
||||||
"ObjectPacket": {
|
|
||||||
"tx": 0,
|
|
||||||
"rx": 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
|
||||||
if cls._instance is None:
|
|
||||||
cls._instance = super().__new__(cls)
|
|
||||||
# any init here
|
|
||||||
cls._instance.start_time = datetime.datetime.now()
|
|
||||||
cls._instance._aprsis_keepalive = datetime.datetime.now()
|
|
||||||
return cls._instance
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
@property
|
|
||||||
def uptime(self):
|
|
||||||
return datetime.datetime.now() - self.start_time
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
@property
|
|
||||||
def memory(self):
|
|
||||||
return self._mem_current
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def set_memory(self, memory):
|
|
||||||
self._mem_current = memory
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
@property
|
|
||||||
def memory_peak(self):
|
|
||||||
return self._mem_peak
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def set_memory_peak(self, memory):
|
|
||||||
self._mem_peak = memory
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def set_thread_info(self, thread_info):
|
|
||||||
self._thread_info = thread_info
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
@property
|
|
||||||
def thread_info(self):
|
|
||||||
return self._thread_info
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
@property
|
|
||||||
def aprsis_server(self):
|
|
||||||
return self._aprsis_server
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def set_aprsis_server(self, server):
|
|
||||||
self._aprsis_server = server
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
@property
|
|
||||||
def aprsis_keepalive(self):
|
|
||||||
return self._aprsis_keepalive
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def set_aprsis_keepalive(self):
|
|
||||||
self._aprsis_keepalive = datetime.datetime.now()
|
|
||||||
|
|
||||||
def rx(self, packet):
|
|
||||||
pkt_type = packet.__class__.__name__
|
|
||||||
if pkt_type not in self._pkt_cnt:
|
|
||||||
self._pkt_cnt[pkt_type] = {
|
|
||||||
"tx": 0,
|
|
||||||
"rx": 0,
|
|
||||||
}
|
|
||||||
self._pkt_cnt[pkt_type]["rx"] += 1
|
|
||||||
|
|
||||||
def tx(self, packet):
|
|
||||||
pkt_type = packet.__class__.__name__
|
|
||||||
if pkt_type not in self._pkt_cnt:
|
|
||||||
self._pkt_cnt[pkt_type] = {
|
|
||||||
"tx": 0,
|
|
||||||
"rx": 0,
|
|
||||||
}
|
|
||||||
self._pkt_cnt[pkt_type]["tx"] += 1
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
@property
|
|
||||||
def msgs_tracked(self):
|
|
||||||
return packets.PacketTrack().total_tracked
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
@property
|
|
||||||
def email_tx(self):
|
|
||||||
return self._email_tx
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def email_tx_inc(self):
|
|
||||||
self._email_tx += 1
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
@property
|
|
||||||
def email_rx(self):
|
|
||||||
return self._email_rx
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def email_rx_inc(self):
|
|
||||||
self._email_rx += 1
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
@property
|
|
||||||
def email_thread_time(self):
|
|
||||||
return self._email_thread_last_time
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def email_thread_update(self):
|
|
||||||
self._email_thread_last_time = datetime.datetime.now()
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def stats(self):
|
|
||||||
now = datetime.datetime.now()
|
|
||||||
if self._email_thread_last_time:
|
|
||||||
last_update = str(now - self._email_thread_last_time)
|
|
||||||
else:
|
|
||||||
last_update = "never"
|
|
||||||
|
|
||||||
if self._aprsis_keepalive:
|
|
||||||
last_aprsis_keepalive = str(now - self._aprsis_keepalive)
|
|
||||||
else:
|
|
||||||
last_aprsis_keepalive = "never"
|
|
||||||
|
|
||||||
pm = plugin.PluginManager()
|
|
||||||
plugins = pm.get_plugins()
|
|
||||||
plugin_stats = {}
|
|
||||||
if plugins:
|
|
||||||
def full_name_with_qualname(obj):
|
|
||||||
return "{}.{}".format(
|
|
||||||
obj.__class__.__module__,
|
|
||||||
obj.__class__.__qualname__,
|
|
||||||
)
|
|
||||||
|
|
||||||
for p in plugins:
|
|
||||||
plugin_stats[full_name_with_qualname(p)] = {
|
|
||||||
"enabled": p.enabled,
|
|
||||||
"rx": p.rx_count,
|
|
||||||
"tx": p.tx_count,
|
|
||||||
"version": p.version,
|
|
||||||
}
|
|
||||||
|
|
||||||
wl = packets.WatchList()
|
|
||||||
sl = packets.SeenList()
|
|
||||||
pl = packets.PacketList()
|
|
||||||
|
|
||||||
stats = {
|
|
||||||
"aprsd": {
|
|
||||||
"version": aprsd.__version__,
|
|
||||||
"uptime": utils.strfdelta(self.uptime),
|
|
||||||
"callsign": CONF.callsign,
|
|
||||||
"memory_current": int(self.memory),
|
|
||||||
"memory_current_str": utils.human_size(self.memory),
|
|
||||||
"memory_peak": int(self.memory_peak),
|
|
||||||
"memory_peak_str": utils.human_size(self.memory_peak),
|
|
||||||
"threads": self._thread_info,
|
|
||||||
"watch_list": wl.get_all(),
|
|
||||||
"seen_list": sl.get_all(),
|
|
||||||
},
|
|
||||||
"aprs-is": {
|
|
||||||
"server": str(self.aprsis_server),
|
|
||||||
"callsign": CONF.aprs_network.login,
|
|
||||||
"last_update": last_aprsis_keepalive,
|
|
||||||
},
|
|
||||||
"packets": {
|
|
||||||
"total_tracked": int(pl.total_tx() + pl.total_rx()),
|
|
||||||
"total_sent": int(pl.total_tx()),
|
|
||||||
"total_received": int(pl.total_rx()),
|
|
||||||
"by_type": self._pkt_cnt,
|
|
||||||
},
|
|
||||||
"messages": {
|
|
||||||
"sent": self._pkt_cnt["MessagePacket"]["tx"],
|
|
||||||
"received": self._pkt_cnt["MessagePacket"]["tx"],
|
|
||||||
"ack_sent": self._pkt_cnt["AckPacket"]["tx"],
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"enabled": CONF.email_plugin.enabled,
|
|
||||||
"sent": int(self._email_tx),
|
|
||||||
"received": int(self._email_rx),
|
|
||||||
"thread_last_update": last_update,
|
|
||||||
},
|
|
||||||
"plugins": plugin_stats,
|
|
||||||
}
|
|
||||||
return stats
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
pl = packets.PacketList()
|
|
||||||
return (
|
|
||||||
"Uptime:{} Msgs TX:{} RX:{} "
|
|
||||||
"ACK: TX:{} RX:{} "
|
|
||||||
"Email TX:{} RX:{} LastLoop:{} ".format(
|
|
||||||
self.uptime,
|
|
||||||
pl.total_tx(),
|
|
||||||
pl.total_rx(),
|
|
||||||
self._pkt_cnt["AckPacket"]["tx"],
|
|
||||||
self._pkt_cnt["AckPacket"]["rx"],
|
|
||||||
self._email_tx,
|
|
||||||
self._email_rx,
|
|
||||||
self._email_thread_last_time,
|
|
||||||
)
|
|
||||||
)
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
from aprsd import plugin
|
||||||
|
from aprsd.client import stats as client_stats
|
||||||
|
from aprsd.packets import packet_list, seen_list, tracker, watch_list
|
||||||
|
from aprsd.plugins import email
|
||||||
|
from aprsd.stats import app, collector
|
||||||
|
from aprsd.threads import aprsd
|
||||||
|
|
||||||
|
|
||||||
|
# Create the collector and register all the objects
|
||||||
|
# that APRSD has that implement the stats protocol
|
||||||
|
stats_collector = collector.Collector()
|
||||||
|
stats_collector.register_producer(app.APRSDStats)
|
||||||
|
stats_collector.register_producer(packet_list.PacketList)
|
||||||
|
stats_collector.register_producer(watch_list.WatchList)
|
||||||
|
stats_collector.register_producer(tracker.PacketTrack)
|
||||||
|
stats_collector.register_producer(plugin.PluginManager)
|
||||||
|
stats_collector.register_producer(aprsd.APRSDThreadList)
|
||||||
|
stats_collector.register_producer(email.EmailStats)
|
||||||
|
stats_collector.register_producer(client_stats.APRSClientStats)
|
||||||
|
stats_collector.register_producer(seen_list.SeenList)
|
|
@ -0,0 +1,49 @@
|
||||||
|
import datetime
|
||||||
|
import tracemalloc
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
import aprsd
|
||||||
|
from aprsd import utils
|
||||||
|
from aprsd.log import log as aprsd_log
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
class APRSDStats:
|
||||||
|
"""The AppStats class is used to collect stats from the application."""
|
||||||
|
|
||||||
|
_instance = None
|
||||||
|
start_time = None
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
"""Have to override the new method to make this a singleton
|
||||||
|
|
||||||
|
instead of using @singletone decorator so the unit tests work.
|
||||||
|
"""
|
||||||
|
if not cls._instance:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
cls._instance.start_time = datetime.datetime.now()
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def uptime(self):
|
||||||
|
return datetime.datetime.now() - self.start_time
|
||||||
|
|
||||||
|
def stats(self, serializable=False) -> dict:
|
||||||
|
current, peak = tracemalloc.get_traced_memory()
|
||||||
|
uptime = self.uptime()
|
||||||
|
qsize = aprsd_log.logging_queue.qsize()
|
||||||
|
if serializable:
|
||||||
|
uptime = str(uptime)
|
||||||
|
stats = {
|
||||||
|
"version": aprsd.__version__,
|
||||||
|
"uptime": uptime,
|
||||||
|
"callsign": CONF.callsign,
|
||||||
|
"memory_current": int(current),
|
||||||
|
"memory_current_str": utils.human_size(current),
|
||||||
|
"memory_peak": int(peak),
|
||||||
|
"memory_peak_str": utils.human_size(peak),
|
||||||
|
"loging_queue": qsize,
|
||||||
|
}
|
||||||
|
return stats
|
|
@ -0,0 +1,38 @@
|
||||||
|
import logging
|
||||||
|
from typing import Callable, Protocol, runtime_checkable
|
||||||
|
|
||||||
|
from aprsd.utils import singleton
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class StatsProducer(Protocol):
|
||||||
|
"""The StatsProducer protocol is used to define the interface for collecting stats."""
|
||||||
|
def stats(self, serializeable=False) -> dict:
|
||||||
|
"""provide stats in a dictionary format."""
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@singleton
|
||||||
|
class Collector:
|
||||||
|
"""The Collector class is used to collect stats from multiple StatsProducer instances."""
|
||||||
|
def __init__(self):
|
||||||
|
self.producers: list[Callable] = []
|
||||||
|
|
||||||
|
def collect(self, serializable=False) -> dict:
|
||||||
|
stats = {}
|
||||||
|
for name in self.producers:
|
||||||
|
cls = name()
|
||||||
|
if isinstance(cls, StatsProducer):
|
||||||
|
try:
|
||||||
|
stats[cls.__class__.__name__] = cls.stats(serializable=serializable).copy()
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error(f"Error in producer {name} (stats): {e}")
|
||||||
|
else:
|
||||||
|
raise TypeError(f"{cls} is not an instance of StatsProducer")
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def register_producer(self, producer_name: Callable):
|
||||||
|
self.producers.append(producer_name)
|
|
@ -3,8 +3,9 @@ import queue
|
||||||
# Make these available to anyone importing
|
# Make these available to anyone importing
|
||||||
# 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 .rx import ( # noqa: F401
|
||||||
from .rx import APRSDRXThread # noqa: F401
|
APRSDDupeRXThread, APRSDProcessPacketThread, APRSDRXThread,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
packet_queue = queue.Queue(maxsize=20)
|
packet_queue = queue.Queue(maxsize=20)
|
||||||
|
|
|
@ -2,6 +2,7 @@ import abc
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
|
from typing import List
|
||||||
|
|
||||||
import wrapt
|
import wrapt
|
||||||
|
|
||||||
|
@ -9,43 +10,10 @@ import wrapt
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class APRSDThreadList:
|
|
||||||
"""Singleton class that keeps track of application wide threads."""
|
|
||||||
|
|
||||||
_instance = None
|
|
||||||
|
|
||||||
threads_list = []
|
|
||||||
lock = threading.Lock()
|
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
|
||||||
if cls._instance is None:
|
|
||||||
cls._instance = super().__new__(cls)
|
|
||||||
cls.threads_list = []
|
|
||||||
return cls._instance
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def add(self, thread_obj):
|
|
||||||
self.threads_list.append(thread_obj)
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def remove(self, thread_obj):
|
|
||||||
self.threads_list.remove(thread_obj)
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def stop_all(self):
|
|
||||||
"""Iterate over all threads and call stop on them."""
|
|
||||||
for th in self.threads_list:
|
|
||||||
LOG.info(f"Stopping Thread {th.name}")
|
|
||||||
if hasattr(th, "packet"):
|
|
||||||
LOG.info(F"{th.name} packet {th.packet}")
|
|
||||||
th.stop()
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def __len__(self):
|
|
||||||
return len(self.threads_list)
|
|
||||||
|
|
||||||
|
|
||||||
class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
||||||
|
"""Base class for all threads in APRSD."""
|
||||||
|
|
||||||
|
loop_count = 1
|
||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
super().__init__(name=name)
|
super().__init__(name=name)
|
||||||
|
@ -79,6 +47,7 @@ class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
||||||
def run(self):
|
def run(self):
|
||||||
LOG.debug("Starting")
|
LOG.debug("Starting")
|
||||||
while not self._should_quit():
|
while not self._should_quit():
|
||||||
|
self.loop_count += 1
|
||||||
can_loop = self.loop()
|
can_loop = self.loop()
|
||||||
self._last_loop = datetime.datetime.now()
|
self._last_loop = datetime.datetime.now()
|
||||||
if not can_loop:
|
if not can_loop:
|
||||||
|
@ -86,3 +55,65 @@ class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
||||||
self._cleanup()
|
self._cleanup()
|
||||||
APRSDThreadList().remove(self)
|
APRSDThreadList().remove(self)
|
||||||
LOG.debug("Exiting")
|
LOG.debug("Exiting")
|
||||||
|
|
||||||
|
|
||||||
|
class APRSDThreadList:
|
||||||
|
"""Singleton class that keeps track of application wide threads."""
|
||||||
|
|
||||||
|
_instance = None
|
||||||
|
|
||||||
|
threads_list: List[APRSDThread] = []
|
||||||
|
lock = threading.Lock()
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
cls.threads_list = []
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def stats(self, serializable=False) -> dict:
|
||||||
|
stats = {}
|
||||||
|
for th in self.threads_list:
|
||||||
|
age = th.loop_age()
|
||||||
|
if serializable:
|
||||||
|
age = str(age)
|
||||||
|
stats[th.name] = {
|
||||||
|
"name": th.name,
|
||||||
|
"class": th.__class__.__name__,
|
||||||
|
"alive": th.is_alive(),
|
||||||
|
"age": th.loop_age(),
|
||||||
|
"loop_count": th.loop_count,
|
||||||
|
}
|
||||||
|
return stats
|
||||||
|
|
||||||
|
@wrapt.synchronized(lock)
|
||||||
|
def add(self, thread_obj):
|
||||||
|
self.threads_list.append(thread_obj)
|
||||||
|
|
||||||
|
@wrapt.synchronized(lock)
|
||||||
|
def remove(self, thread_obj):
|
||||||
|
self.threads_list.remove(thread_obj)
|
||||||
|
|
||||||
|
@wrapt.synchronized(lock)
|
||||||
|
def stop_all(self):
|
||||||
|
"""Iterate over all threads and call stop on them."""
|
||||||
|
for th in self.threads_list:
|
||||||
|
LOG.info(f"Stopping Thread {th.name}")
|
||||||
|
if hasattr(th, "packet"):
|
||||||
|
LOG.info(F"{th.name} packet {th.packet}")
|
||||||
|
th.stop()
|
||||||
|
|
||||||
|
@wrapt.synchronized(lock)
|
||||||
|
def info(self):
|
||||||
|
"""Go through all the threads and collect info about each."""
|
||||||
|
info = {}
|
||||||
|
for thread in self.threads_list:
|
||||||
|
alive = thread.is_alive()
|
||||||
|
age = thread.loop_age()
|
||||||
|
key = thread.__class__.__name__
|
||||||
|
info[key] = {"alive": True if alive else False, "age": age, "name": thread.name}
|
||||||
|
return info
|
||||||
|
|
||||||
|
@wrapt.synchronized(lock)
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.threads_list)
|
||||||
|
|
|
@ -5,7 +5,10 @@ import tracemalloc
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
from aprsd import client, packets, stats, utils
|
from aprsd import packets, utils
|
||||||
|
from aprsd.client import client_factory
|
||||||
|
from aprsd.log import log as aprsd_log
|
||||||
|
from aprsd.stats import collector
|
||||||
from aprsd.threads import APRSDThread, APRSDThreadList
|
from aprsd.threads import APRSDThread, APRSDThreadList
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,64 +27,70 @@ class KeepAliveThread(APRSDThread):
|
||||||
self.max_delta = datetime.timedelta(**max_timeout)
|
self.max_delta = datetime.timedelta(**max_timeout)
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
if self.cntr % 60 == 0:
|
if self.loop_count % 60 == 0:
|
||||||
pkt_tracker = packets.PacketTrack()
|
stats_json = collector.Collector().collect()
|
||||||
stats_obj = stats.APRSDStats()
|
|
||||||
pl = packets.PacketList()
|
pl = packets.PacketList()
|
||||||
thread_list = APRSDThreadList()
|
thread_list = APRSDThreadList()
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
last_email = stats_obj.email_thread_time
|
|
||||||
if last_email:
|
if "EmailStats" in stats_json:
|
||||||
email_thread_time = utils.strfdelta(now - last_email)
|
email_stats = stats_json["EmailStats"]
|
||||||
|
if email_stats.get("last_check_time"):
|
||||||
|
email_thread_time = utils.strfdelta(now - email_stats["last_check_time"])
|
||||||
|
else:
|
||||||
|
email_thread_time = "N/A"
|
||||||
else:
|
else:
|
||||||
email_thread_time = "N/A"
|
email_thread_time = "N/A"
|
||||||
|
|
||||||
last_msg_time = utils.strfdelta(now - stats_obj.aprsis_keepalive)
|
if "APRSClientStats" in stats_json and stats_json["APRSClientStats"].get("transport") == "aprsis":
|
||||||
|
if stats_json["APRSClientStats"].get("server_keepalive"):
|
||||||
|
last_msg_time = utils.strfdelta(now - stats_json["APRSClientStats"]["server_keepalive"])
|
||||||
|
else:
|
||||||
|
last_msg_time = "N/A"
|
||||||
|
else:
|
||||||
|
last_msg_time = "N/A"
|
||||||
|
|
||||||
current, peak = tracemalloc.get_traced_memory()
|
tracked_packets = stats_json["PacketTrack"]["total_tracked"]
|
||||||
stats_obj.set_memory(current)
|
tx_msg = 0
|
||||||
stats_obj.set_memory_peak(peak)
|
rx_msg = 0
|
||||||
|
if "PacketList" in stats_json:
|
||||||
login = CONF.callsign
|
msg_packets = stats_json["PacketList"].get("MessagePacket")
|
||||||
|
if msg_packets:
|
||||||
tracked_packets = len(pkt_tracker)
|
tx_msg = msg_packets.get("tx", 0)
|
||||||
|
rx_msg = msg_packets.get("rx", 0)
|
||||||
|
|
||||||
keepalive = (
|
keepalive = (
|
||||||
"{} - Uptime {} RX:{} TX:{} Tracker:{} Msgs TX:{} RX:{} "
|
"{} - Uptime {} RX:{} TX:{} Tracker:{} Msgs TX:{} RX:{} "
|
||||||
"Last:{} Email: {} - RAM Current:{} Peak:{} Threads:{}"
|
"Last:{} Email: {} - RAM Current:{} Peak:{} Threads:{} LoggingQueue:{}"
|
||||||
).format(
|
).format(
|
||||||
login,
|
stats_json["APRSDStats"]["callsign"],
|
||||||
utils.strfdelta(stats_obj.uptime),
|
stats_json["APRSDStats"]["uptime"],
|
||||||
pl.total_rx(),
|
pl.total_rx(),
|
||||||
pl.total_tx(),
|
pl.total_tx(),
|
||||||
tracked_packets,
|
tracked_packets,
|
||||||
stats_obj._pkt_cnt["MessagePacket"]["tx"],
|
tx_msg,
|
||||||
stats_obj._pkt_cnt["MessagePacket"]["rx"],
|
rx_msg,
|
||||||
last_msg_time,
|
last_msg_time,
|
||||||
email_thread_time,
|
email_thread_time,
|
||||||
utils.human_size(current),
|
stats_json["APRSDStats"]["memory_current_str"],
|
||||||
utils.human_size(peak),
|
stats_json["APRSDStats"]["memory_peak_str"],
|
||||||
len(thread_list),
|
len(thread_list),
|
||||||
|
aprsd_log.logging_queue.qsize(),
|
||||||
)
|
)
|
||||||
LOG.info(keepalive)
|
LOG.info(keepalive)
|
||||||
thread_out = []
|
if "APRSDThreadList" in stats_json:
|
||||||
thread_info = {}
|
thread_list = stats_json["APRSDThreadList"]
|
||||||
for thread in thread_list.threads_list:
|
for thread_name in thread_list:
|
||||||
alive = thread.is_alive()
|
thread = thread_list[thread_name]
|
||||||
age = thread.loop_age()
|
alive = thread["alive"]
|
||||||
key = thread.__class__.__name__
|
age = thread["age"]
|
||||||
thread_out.append(f"{key}:{alive}:{age}")
|
key = thread["name"]
|
||||||
if key not in thread_info:
|
if not alive:
|
||||||
thread_info[key] = {}
|
LOG.error(f"Thread {thread}")
|
||||||
thread_info[key]["alive"] = alive
|
LOG.info(f"{key: <15} Alive? {str(alive): <5} {str(age): <20}")
|
||||||
thread_info[key]["age"] = age
|
|
||||||
if not alive:
|
|
||||||
LOG.error(f"Thread {thread}")
|
|
||||||
LOG.info(",".join(thread_out))
|
|
||||||
stats_obj.set_thread_info(thread_info)
|
|
||||||
|
|
||||||
# check the APRS connection
|
# check the APRS connection
|
||||||
cl = client.factory.create()
|
cl = client_factory.create()
|
||||||
# Reset the connection if it's dead and this isn't our
|
# Reset the connection if it's dead and this isn't our
|
||||||
# First time through the loop.
|
# First time through the loop.
|
||||||
# The first time through the loop can happen at startup where
|
# The first time through the loop can happen at startup where
|
||||||
|
@ -89,19 +98,19 @@ class KeepAliveThread(APRSDThread):
|
||||||
# to make it's connection the first time.
|
# to make it's connection the first time.
|
||||||
if not cl.is_alive() and self.cntr > 0:
|
if not cl.is_alive() and self.cntr > 0:
|
||||||
LOG.error(f"{cl.__class__.__name__} is not alive!!! Resetting")
|
LOG.error(f"{cl.__class__.__name__} is not alive!!! Resetting")
|
||||||
client.factory.create().reset()
|
client_factory.create().reset()
|
||||||
else:
|
# else:
|
||||||
# See if we should reset the aprs-is client
|
# # See if we should reset the aprs-is client
|
||||||
# Due to losing a keepalive from them
|
# # Due to losing a keepalive from them
|
||||||
delta_dict = utils.parse_delta_str(last_msg_time)
|
# delta_dict = utils.parse_delta_str(last_msg_time)
|
||||||
delta = datetime.timedelta(**delta_dict)
|
# delta = datetime.timedelta(**delta_dict)
|
||||||
|
#
|
||||||
if delta > self.max_delta:
|
# if delta > self.max_delta:
|
||||||
# We haven't gotten a keepalive from aprs-is in a while
|
# # We haven't gotten a keepalive from aprs-is in a while
|
||||||
# reset the connection.a
|
# # reset the connection.a
|
||||||
if not client.KISSClient.is_enabled():
|
# if not client.KISSClient.is_enabled():
|
||||||
LOG.warning(f"Resetting connection to APRS-IS {delta}")
|
# LOG.warning(f"Resetting connection to APRS-IS {delta}")
|
||||||
client.factory.create().reset()
|
# client.factory.create().reset()
|
||||||
|
|
||||||
# Check version every day
|
# Check version every day
|
||||||
delta = now - self.checker_time
|
delta = now - self.checker_time
|
||||||
|
@ -110,6 +119,6 @@ class KeepAliveThread(APRSDThread):
|
||||||
level, msg = utils._check_version()
|
level, msg = utils._check_version()
|
||||||
if level:
|
if level:
|
||||||
LOG.warning(msg)
|
LOG.warning(msg)
|
||||||
self.cntr += 1
|
self.cntr += 1
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -1,25 +1,54 @@
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
import requests
|
||||||
import wrapt
|
import wrapt
|
||||||
|
|
||||||
from aprsd import threads
|
from aprsd import threads
|
||||||
from aprsd.log import log
|
from aprsd.log import log
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
def send_log_entries(force=False):
|
||||||
|
"""Send all of the log entries to the web interface."""
|
||||||
|
if CONF.admin.web_enabled:
|
||||||
|
if force or LogEntries().is_purge_ready():
|
||||||
|
entries = LogEntries().get_all_and_purge()
|
||||||
|
if entries:
|
||||||
|
try:
|
||||||
|
requests.post(
|
||||||
|
f"http://{CONF.admin.web_ip}:{CONF.admin.web_port}/log_entries",
|
||||||
|
json=entries,
|
||||||
|
auth=(CONF.admin.user, CONF.admin.password),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
LOG.warning(f"Failed to send log entries. len={len(entries)}")
|
||||||
|
|
||||||
|
|
||||||
class LogEntries:
|
class LogEntries:
|
||||||
entries = []
|
entries = []
|
||||||
lock = threading.Lock()
|
lock = threading.Lock()
|
||||||
_instance = None
|
_instance = None
|
||||||
|
last_purge = datetime.datetime.now()
|
||||||
|
max_delta = datetime.timedelta(
|
||||||
|
hours=0.0, minutes=0, seconds=2,
|
||||||
|
)
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super().__new__(cls)
|
cls._instance = super().__new__(cls)
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
|
def stats(self) -> dict:
|
||||||
|
return {
|
||||||
|
"log_entries": self.entries,
|
||||||
|
}
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
@wrapt.synchronized(lock)
|
||||||
def add(self, entry):
|
def add(self, entry):
|
||||||
self.entries.append(entry)
|
self.entries.append(entry)
|
||||||
|
@ -28,8 +57,18 @@ class LogEntries:
|
||||||
def get_all_and_purge(self):
|
def get_all_and_purge(self):
|
||||||
entries = self.entries.copy()
|
entries = self.entries.copy()
|
||||||
self.entries = []
|
self.entries = []
|
||||||
|
self.last_purge = datetime.datetime.now()
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
|
def is_purge_ready(self):
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
if (
|
||||||
|
now - self.last_purge > self.max_delta
|
||||||
|
and len(self.entries) > 1
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
@wrapt.synchronized(lock)
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return len(self.entries)
|
return len(self.entries)
|
||||||
|
@ -40,6 +79,10 @@ class LogMonitorThread(threads.APRSDThread):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__("LogMonitorThread")
|
super().__init__("LogMonitorThread")
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
send_log_entries(force=True)
|
||||||
|
super().stop()
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
try:
|
try:
|
||||||
record = log.logging_queue.get(block=True, timeout=2)
|
record = log.logging_queue.get(block=True, timeout=2)
|
||||||
|
@ -54,6 +97,7 @@ class LogMonitorThread(threads.APRSDThread):
|
||||||
# Just ignore thi
|
# Just ignore thi
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
send_log_entries()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def json_record(self, record):
|
def json_record(self, record):
|
||||||
|
|
|
@ -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
|
|
@ -6,7 +6,10 @@ import time
|
||||||
import aprslib
|
import aprslib
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
from aprsd import client, packets, plugin
|
from aprsd import packets, plugin
|
||||||
|
from aprsd.client import client_factory
|
||||||
|
from aprsd.packets import collector
|
||||||
|
from aprsd.packets import log as packet_log
|
||||||
from aprsd.threads import APRSDThread, tx
|
from aprsd.threads import APRSDThread, tx
|
||||||
|
|
||||||
|
|
||||||
|
@ -16,15 +19,20 @@ LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
class APRSDRXThread(APRSDThread):
|
class APRSDRXThread(APRSDThread):
|
||||||
def __init__(self, packet_queue):
|
def __init__(self, packet_queue):
|
||||||
super().__init__("RX_MSG")
|
super().__init__("RX_PKT")
|
||||||
self.packet_queue = packet_queue
|
self.packet_queue = packet_queue
|
||||||
self._client = client.factory.create()
|
self._client = client_factory.create()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.thread_stop = True
|
self.thread_stop = True
|
||||||
client.factory.create().client.stop()
|
if self._client:
|
||||||
|
self._client.stop()
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
|
if not self._client:
|
||||||
|
self._client = client_factory.create()
|
||||||
|
time.sleep(1)
|
||||||
|
return True
|
||||||
# setup the consumer of messages and block until a messages
|
# setup the consumer of messages and block until a messages
|
||||||
try:
|
try:
|
||||||
# This will register a packet consumer with aprslib
|
# This will register a packet consumer with aprslib
|
||||||
|
@ -36,34 +44,44 @@ class APRSDRXThread(APRSDThread):
|
||||||
# and the aprslib developer didn't want to allow a PR to add
|
# and the aprslib developer didn't want to allow a PR to add
|
||||||
# kwargs. :(
|
# kwargs. :(
|
||||||
# https://github.com/rossengeorgiev/aprs-python/pull/56
|
# https://github.com/rossengeorgiev/aprs-python/pull/56
|
||||||
self._client.client.consumer(
|
self._client.consumer(
|
||||||
self.process_packet, raw=False, blocking=False,
|
self._process_packet, raw=False, blocking=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
except (
|
except (
|
||||||
aprslib.exceptions.ConnectionDrop,
|
aprslib.exceptions.ConnectionDrop,
|
||||||
aprslib.exceptions.ConnectionError,
|
aprslib.exceptions.ConnectionError,
|
||||||
):
|
):
|
||||||
LOG.error("Connection dropped, reconnecting")
|
LOG.error("Connection dropped, reconnecting")
|
||||||
time.sleep(5)
|
|
||||||
# Force the deletion of the client object connected to aprs
|
# Force the deletion of the client object connected to aprs
|
||||||
# This will cause a reconnect, next time client.get_client()
|
# This will cause a reconnect, next time client.get_client()
|
||||||
# is called
|
# is called
|
||||||
self._client.reset()
|
self._client.reset()
|
||||||
|
time.sleep(5)
|
||||||
|
except Exception:
|
||||||
|
# LOG.exception(ex)
|
||||||
|
LOG.error("Resetting connection and trying again.")
|
||||||
|
self._client.reset()
|
||||||
|
time.sleep(5)
|
||||||
# Continue to loop
|
# Continue to loop
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _process_packet(self, *args, **kwargs):
|
||||||
|
"""Intermediate callback so we can update the keepalive time."""
|
||||||
|
# Now call the 'real' packet processing for a RX'x packet
|
||||||
|
self.process_packet(*args, **kwargs)
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def process_packet(self, *args, **kwargs):
|
def process_packet(self, *args, **kwargs):
|
||||||
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):
|
||||||
|
@ -79,7 +97,8 @@ class APRSDPluginRXThread(APRSDRXThread):
|
||||||
"""
|
"""
|
||||||
packet = self._client.decode_packet(*args, **kwargs)
|
packet = self._client.decode_packet(*args, **kwargs)
|
||||||
# LOG.debug(raw)
|
# LOG.debug(raw)
|
||||||
packet.log(header="RX")
|
packet_log.log(packet)
|
||||||
|
pkt_list = packets.PacketList()
|
||||||
|
|
||||||
if isinstance(packet, packets.AckPacket):
|
if isinstance(packet, packets.AckPacket):
|
||||||
# We don't need to drop AckPackets, those should be
|
# We don't need to drop AckPackets, those should be
|
||||||
|
@ -90,7 +109,6 @@ class APRSDPluginRXThread(APRSDRXThread):
|
||||||
# For RF based APRS Clients we can get duplicate packets
|
# For RF based APRS Clients we can get duplicate packets
|
||||||
# So we need to track them and not process the dupes.
|
# So we need to track them and not process the dupes.
|
||||||
found = False
|
found = False
|
||||||
pkt_list = packets.PacketList()
|
|
||||||
try:
|
try:
|
||||||
# Find the packet in the list of already seen packets
|
# Find the packet in the list of already seen packets
|
||||||
# Based on the packet.key
|
# Based on the packet.key
|
||||||
|
@ -99,14 +117,11 @@ class APRSDPluginRXThread(APRSDRXThread):
|
||||||
found = False
|
found = False
|
||||||
|
|
||||||
if not found:
|
if not found:
|
||||||
# If we are in the process of already ack'ing
|
# We haven't seen this packet before, so we process it.
|
||||||
# a packet, we should drop the packet
|
collector.PacketCollector().rx(packet)
|
||||||
# because it's a dupe within the time that
|
|
||||||
# we send the 3 acks for the packet.
|
|
||||||
pkt_list.rx(packet)
|
|
||||||
self.packet_queue.put(packet)
|
self.packet_queue.put(packet)
|
||||||
elif packet.timestamp - found.timestamp < CONF.packet_dupe_timeout:
|
elif packet.timestamp - found.timestamp < CONF.packet_dupe_timeout:
|
||||||
# If the packet came in within 60 seconds of the
|
# If the packet came in within N seconds of the
|
||||||
# Last time seeing the packet, then we drop it as a dupe.
|
# Last time seeing the packet, then we drop it as a dupe.
|
||||||
LOG.warning(f"Packet {packet.from_call}:{packet.msgNo} already tracked, dropping.")
|
LOG.warning(f"Packet {packet.from_call}:{packet.msgNo} already tracked, dropping.")
|
||||||
else:
|
else:
|
||||||
|
@ -114,10 +129,17 @@ class APRSDPluginRXThread(APRSDRXThread):
|
||||||
f"Packet {packet.from_call}:{packet.msgNo} already tracked "
|
f"Packet {packet.from_call}:{packet.msgNo} already tracked "
|
||||||
f"but older than {CONF.packet_dupe_timeout} seconds. processing.",
|
f"but older than {CONF.packet_dupe_timeout} seconds. processing.",
|
||||||
)
|
)
|
||||||
pkt_list.rx(packet)
|
collector.PacketCollector().rx(packet)
|
||||||
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.
|
||||||
|
|
||||||
|
@ -129,21 +151,24 @@ class APRSDProcessPacketThread(APRSDThread):
|
||||||
def __init__(self, packet_queue):
|
def __init__(self, packet_queue):
|
||||||
self.packet_queue = packet_queue
|
self.packet_queue = packet_queue
|
||||||
super().__init__("ProcessPKT")
|
super().__init__("ProcessPKT")
|
||||||
self._loop_cnt = 1
|
|
||||||
|
|
||||||
def process_ack_packet(self, packet):
|
def process_ack_packet(self, packet):
|
||||||
"""We got an ack for a message, no need to resend it."""
|
"""We got an ack for a message, no need to resend it."""
|
||||||
ack_num = packet.msgNo
|
ack_num = packet.msgNo
|
||||||
LOG.info(f"Got ack for message {ack_num}")
|
LOG.debug(f"Got ack for message {ack_num}")
|
||||||
pkt_tracker = packets.PacketTrack()
|
collector.PacketCollector().rx(packet)
|
||||||
pkt_tracker.remove(ack_num)
|
|
||||||
|
def process_piggyback_ack(self, packet):
|
||||||
|
"""We got an ack embedded in a packet."""
|
||||||
|
ack_num = packet.ackMsgNo
|
||||||
|
LOG.debug(f"Got PiggyBackAck for message {ack_num}")
|
||||||
|
collector.PacketCollector().rx(packet)
|
||||||
|
|
||||||
def process_reject_packet(self, packet):
|
def process_reject_packet(self, packet):
|
||||||
"""We got a reject message for a packet. Stop sending the message."""
|
"""We got a reject message for a packet. Stop sending the message."""
|
||||||
ack_num = packet.msgNo
|
ack_num = packet.msgNo
|
||||||
LOG.info(f"Got REJECT for message {ack_num}")
|
LOG.debug(f"Got REJECT for message {ack_num}")
|
||||||
pkt_tracker = packets.PacketTrack()
|
collector.PacketCollector().rx(packet)
|
||||||
pkt_tracker.remove(ack_num)
|
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
try:
|
try:
|
||||||
|
@ -152,12 +177,11 @@ class APRSDProcessPacketThread(APRSDThread):
|
||||||
self.process_packet(packet)
|
self.process_packet(packet)
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
pass
|
pass
|
||||||
self._loop_cnt += 1
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
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_count}")
|
||||||
our_call = CONF.callsign.lower()
|
our_call = CONF.callsign.lower()
|
||||||
|
|
||||||
from_call = packet.from_call
|
from_call = packet.from_call
|
||||||
|
@ -180,6 +204,10 @@ class APRSDProcessPacketThread(APRSDThread):
|
||||||
):
|
):
|
||||||
self.process_reject_packet(packet)
|
self.process_reject_packet(packet)
|
||||||
else:
|
else:
|
||||||
|
if hasattr(packet, "ackMsgNo") and packet.ackMsgNo:
|
||||||
|
# we got an ack embedded in this packet
|
||||||
|
# we need to handle the ack
|
||||||
|
self.process_piggyback_ack(packet)
|
||||||
# Only ack messages that were sent directly to us
|
# Only ack messages that were sent directly to us
|
||||||
if isinstance(packet, packets.MessagePacket):
|
if isinstance(packet, packets.MessagePacket):
|
||||||
if to_call and to_call.lower() == our_call:
|
if to_call and to_call.lower() == our_call:
|
||||||
|
@ -202,7 +230,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
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
import wrapt
|
||||||
|
|
||||||
|
from aprsd.stats import collector
|
||||||
|
from aprsd.threads import APRSDThread
|
||||||
|
from aprsd.utils import objectstore
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
class StatsStore(objectstore.ObjectStoreMixin):
|
||||||
|
"""Container to save the stats from the collector."""
|
||||||
|
lock = threading.Lock()
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
@wrapt.synchronized(lock)
|
||||||
|
def add(self, stats: dict):
|
||||||
|
self.data = stats
|
||||||
|
|
||||||
|
|
||||||
|
class APRSDStatsStoreThread(APRSDThread):
|
||||||
|
"""Save APRSD Stats to disk periodically."""
|
||||||
|
|
||||||
|
# how often in seconds to write the file
|
||||||
|
save_interval = 10
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("StatsStore")
|
||||||
|
|
||||||
|
def loop(self):
|
||||||
|
if self.loop_count % self.save_interval == 0:
|
||||||
|
stats = collector.Collector().collect()
|
||||||
|
ss = StatsStore()
|
||||||
|
ss.add(stats)
|
||||||
|
ss.save()
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
return True
|
|
@ -1,4 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
@ -6,11 +7,14 @@ from rush import quota, throttle
|
||||||
from rush.contrib import decorator
|
from rush.contrib import decorator
|
||||||
from rush.limiters import periodic
|
from rush.limiters import periodic
|
||||||
from rush.stores import dictionary
|
from rush.stores import dictionary
|
||||||
|
import wrapt
|
||||||
|
|
||||||
from aprsd import client
|
|
||||||
from aprsd import conf # noqa
|
from aprsd import conf # noqa
|
||||||
from aprsd import threads as aprsd_threads
|
from aprsd import threads as aprsd_threads
|
||||||
from aprsd.packets import core, tracker
|
from aprsd.client import client_factory
|
||||||
|
from aprsd.packets import collector, core
|
||||||
|
from aprsd.packets import log as packet_log
|
||||||
|
from aprsd.packets import tracker
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
@ -35,13 +39,19 @@ ack_t = throttle.Throttle(
|
||||||
|
|
||||||
msg_throttle_decorator = decorator.ThrottleDecorator(throttle=msg_t)
|
msg_throttle_decorator = decorator.ThrottleDecorator(throttle=msg_t)
|
||||||
ack_throttle_decorator = decorator.ThrottleDecorator(throttle=ack_t)
|
ack_throttle_decorator = decorator.ThrottleDecorator(throttle=ack_t)
|
||||||
|
s_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
@wrapt.synchronized(s_lock)
|
||||||
|
@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.
|
||||||
# This constructs the packet.raw
|
# This constructs the packet.raw
|
||||||
packet.prepare()
|
packet.prepare()
|
||||||
|
# Have to call the collector to track the packet
|
||||||
|
# After prepare, as prepare assigns the msgNo
|
||||||
|
collector.PacketCollector().tx(packet)
|
||||||
if isinstance(packet, core.AckPacket):
|
if isinstance(packet, core.AckPacket):
|
||||||
_send_ack(packet, direct=direct, aprs_client=aprs_client)
|
_send_ack(packet, direct=direct, aprs_client=aprs_client)
|
||||||
else:
|
else:
|
||||||
|
@ -70,11 +80,15 @@ def _send_direct(packet, aprs_client=None):
|
||||||
if aprs_client:
|
if aprs_client:
|
||||||
cl = aprs_client
|
cl = aprs_client
|
||||||
else:
|
else:
|
||||||
cl = client.factory.create()
|
cl = client_factory.create()
|
||||||
|
|
||||||
packet.update_timestamp()
|
packet.update_timestamp()
|
||||||
packet.log(header="TX")
|
packet_log.log(packet, tx=True)
|
||||||
cl.send(packet)
|
try:
|
||||||
|
cl.send(packet)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error(f"Failed to send packet: {packet}")
|
||||||
|
LOG.error(e)
|
||||||
|
|
||||||
|
|
||||||
class SendPacketThread(aprsd_threads.APRSDThread):
|
class SendPacketThread(aprsd_threads.APRSDThread):
|
||||||
|
@ -82,10 +96,7 @@ class SendPacketThread(aprsd_threads.APRSDThread):
|
||||||
|
|
||||||
def __init__(self, packet):
|
def __init__(self, packet):
|
||||||
self.packet = packet
|
self.packet = packet
|
||||||
name = self.packet.raw[:5]
|
super().__init__(f"TX-{packet.to_call}-{self.packet.msgNo}")
|
||||||
super().__init__(f"TXPKT-{self.packet.msgNo}-{name}")
|
|
||||||
pkt_tracker = tracker.PacketTrack()
|
|
||||||
pkt_tracker.add(packet)
|
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
"""Loop until a message is acked or it gets delayed.
|
"""Loop until a message is acked or it gets delayed.
|
||||||
|
@ -111,7 +122,7 @@ class SendPacketThread(aprsd_threads.APRSDThread):
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
send_now = False
|
send_now = False
|
||||||
if packet.send_count == packet.retry_count:
|
if packet.send_count >= packet.retry_count:
|
||||||
# we reached the send limit, don't send again
|
# we reached the send limit, don't send again
|
||||||
# TODO(hemna) - Need to put this in a delayed queue?
|
# TODO(hemna) - Need to put this in a delayed queue?
|
||||||
LOG.info(
|
LOG.info(
|
||||||
|
@ -120,8 +131,7 @@ class SendPacketThread(aprsd_threads.APRSDThread):
|
||||||
"Message Send Complete. Max attempts reached"
|
"Message Send Complete. Max attempts reached"
|
||||||
f" {packet.retry_count}",
|
f" {packet.retry_count}",
|
||||||
)
|
)
|
||||||
if not packet.allow_delay:
|
pkt_tracker.remove(packet.msgNo)
|
||||||
pkt_tracker.remove(packet.msgNo)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Message is still outstanding and needs to be acked.
|
# Message is still outstanding and needs to be acked.
|
||||||
|
@ -140,7 +150,7 @@ class SendPacketThread(aprsd_threads.APRSDThread):
|
||||||
# no attempt time, so lets send it, and start
|
# no attempt time, so lets send it, and start
|
||||||
# tracking the time.
|
# tracking the time.
|
||||||
packet.last_send_time = int(round(time.time()))
|
packet.last_send_time = int(round(time.time()))
|
||||||
send(packet, direct=True)
|
_send_direct(packet)
|
||||||
packet.send_count += 1
|
packet.send_count += 1
|
||||||
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
@ -151,22 +161,24 @@ class SendPacketThread(aprsd_threads.APRSDThread):
|
||||||
|
|
||||||
class SendAckThread(aprsd_threads.APRSDThread):
|
class SendAckThread(aprsd_threads.APRSDThread):
|
||||||
loop_count: int = 1
|
loop_count: int = 1
|
||||||
|
max_retries = 3
|
||||||
|
|
||||||
def __init__(self, packet):
|
def __init__(self, packet):
|
||||||
self.packet = packet
|
self.packet = packet
|
||||||
super().__init__(f"SendAck-{self.packet.msgNo}")
|
super().__init__(f"TXAck-{packet.to_call}-{self.packet.msgNo}")
|
||||||
|
self.max_retries = CONF.default_ack_send_count
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
"""Separate thread to send acks with retries."""
|
"""Separate thread to send acks with retries."""
|
||||||
send_now = False
|
send_now = False
|
||||||
if self.packet.send_count == self.packet.retry_count:
|
if self.packet.send_count == self.max_retries:
|
||||||
# we reached the send limit, don't send again
|
# we reached the send limit, don't send again
|
||||||
# TODO(hemna) - Need to put this in a delayed queue?
|
# TODO(hemna) - Need to put this in a delayed queue?
|
||||||
LOG.info(
|
LOG.debug(
|
||||||
f"{self.packet.__class__.__name__}"
|
f"{self.packet.__class__.__name__}"
|
||||||
f"({self.packet.msgNo}) "
|
f"({self.packet.msgNo}) "
|
||||||
"Send Complete. Max attempts reached"
|
"Send Complete. Max attempts reached"
|
||||||
f" {self.packet.retry_count}",
|
f" {self.max_retries}",
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -187,10 +199,57 @@ class SendAckThread(aprsd_threads.APRSDThread):
|
||||||
send_now = True
|
send_now = True
|
||||||
|
|
||||||
if send_now:
|
if send_now:
|
||||||
send(self.packet, direct=True)
|
_send_direct(self.packet)
|
||||||
self.packet.send_count += 1
|
self.packet.send_count += 1
|
||||||
self.packet.last_send_time = int(round(time.time()))
|
self.packet.last_send_time = int(round(time.time()))
|
||||||
|
|
||||||
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,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
# Only send it once
|
||||||
|
pkt.retry_count = 1
|
||||||
|
send(pkt, direct=True)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error(f"Failed to send beacon: {e}")
|
||||||
|
client_factory.create().reset()
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
self._loop_cnt += 1
|
||||||
|
time.sleep(1)
|
||||||
|
return True
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
"""Utilities and helper functions."""
|
"""Utilities and helper functions."""
|
||||||
|
|
||||||
import errno
|
import errno
|
||||||
|
import functools
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
import update_checker
|
import update_checker
|
||||||
|
|
||||||
|
@ -18,7 +20,18 @@ 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 singleton(cls):
|
||||||
|
"""Make a class a Singleton class (only one instance)"""
|
||||||
|
@functools.wraps(cls)
|
||||||
|
def wrapper_singleton(*args, **kwargs):
|
||||||
|
if wrapper_singleton.instance is None:
|
||||||
|
wrapper_singleton.instance = cls(*args, **kwargs)
|
||||||
|
return wrapper_singleton.instance
|
||||||
|
wrapper_singleton.instance = None
|
||||||
|
return wrapper_singleton
|
||||||
|
|
||||||
|
|
||||||
def env(*vars, **kwargs):
|
def env(*vars, **kwargs):
|
||||||
|
@ -131,3 +144,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)
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
from multiprocessing import RawValue
|
from multiprocessing import RawValue
|
||||||
|
import random
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import wrapt
|
import wrapt
|
||||||
|
|
||||||
|
|
||||||
|
MAX_PACKET_ID = 9999
|
||||||
|
|
||||||
|
|
||||||
class PacketCounter:
|
class PacketCounter:
|
||||||
"""
|
"""
|
||||||
Global Packet id counter class.
|
Global Packet id counter class.
|
||||||
|
@ -17,19 +21,18 @@ class PacketCounter:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
_instance = None
|
_instance = None
|
||||||
max_count = 9999
|
|
||||||
lock = threading.Lock()
|
lock = threading.Lock()
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
"""Make this a singleton class."""
|
"""Make this a singleton class."""
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super().__new__(cls, *args, **kwargs)
|
cls._instance = super().__new__(cls, *args, **kwargs)
|
||||||
cls._instance.val = RawValue("i", 1)
|
cls._instance.val = RawValue("i", random.randint(1, MAX_PACKET_ID))
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
@wrapt.synchronized(lock)
|
||||||
def increment(self):
|
def increment(self):
|
||||||
if self.val.value == self.max_count:
|
if self.val.value == MAX_PACKET_ID:
|
||||||
self.val.value = 1
|
self.val.value = 1
|
||||||
else:
|
else:
|
||||||
self.val.value += 1
|
self.val.value += 1
|
||||||
|
|
|
@ -3,6 +3,8 @@ import decimal
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from aprsd.packets import core
|
||||||
|
|
||||||
|
|
||||||
class EnhancedJSONEncoder(json.JSONEncoder):
|
class EnhancedJSONEncoder(json.JSONEncoder):
|
||||||
def default(self, obj):
|
def default(self, obj):
|
||||||
|
@ -42,6 +44,24 @@ class EnhancedJSONEncoder(json.JSONEncoder):
|
||||||
return super().default(obj)
|
return super().default(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleJSONEncoder(json.JSONEncoder):
|
||||||
|
def default(self, obj):
|
||||||
|
if isinstance(obj, datetime.datetime):
|
||||||
|
return obj.isoformat()
|
||||||
|
elif isinstance(obj, datetime.date):
|
||||||
|
return str(obj)
|
||||||
|
elif isinstance(obj, datetime.time):
|
||||||
|
return str(obj)
|
||||||
|
elif isinstance(obj, datetime.timedelta):
|
||||||
|
return str(obj)
|
||||||
|
elif isinstance(obj, decimal.Decimal):
|
||||||
|
return str(obj)
|
||||||
|
elif isinstance(obj, core.Packet):
|
||||||
|
return obj.to_dict()
|
||||||
|
else:
|
||||||
|
return super().default(obj)
|
||||||
|
|
||||||
|
|
||||||
class EnhancedJSONDecoder(json.JSONDecoder):
|
class EnhancedJSONDecoder(json.JSONDecoder):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
|
@ -2,6 +2,7 @@ import logging
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import pickle
|
import pickle
|
||||||
|
import threading
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
@ -25,16 +26,28 @@ class ObjectStoreMixin:
|
||||||
aprsd server -f (flush) will wipe all saved objects.
|
aprsd server -f (flush) will wipe all saved objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.lock = threading.RLock()
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return len(self.data)
|
with self.lock:
|
||||||
|
return len(self.data)
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
with self.lock:
|
||||||
|
return iter(self.data)
|
||||||
|
|
||||||
def get_all(self):
|
def get_all(self):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
return self.data
|
return self.data
|
||||||
|
|
||||||
def get(self, id):
|
def get(self, key):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
return self.data[id]
|
return self.data.get(key)
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
with self.lock:
|
||||||
|
return self.data.copy()
|
||||||
|
|
||||||
def _init_store(self):
|
def _init_store(self):
|
||||||
if not CONF.enable_save:
|
if not CONF.enable_save:
|
||||||
|
@ -55,31 +68,26 @@ class ObjectStoreMixin:
|
||||||
self.__class__.__name__.lower(),
|
self.__class__.__name__.lower(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _dump(self):
|
|
||||||
dump = {}
|
|
||||||
with self.lock:
|
|
||||||
for key in self.data.keys():
|
|
||||||
dump[key] = self.data[key]
|
|
||||||
|
|
||||||
return dump
|
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""Save any queued to disk?"""
|
"""Save any queued to disk?"""
|
||||||
if not CONF.enable_save:
|
if not CONF.enable_save:
|
||||||
return
|
return
|
||||||
|
self._init_store()
|
||||||
|
save_filename = self._save_filename()
|
||||||
if len(self) > 0:
|
if len(self) > 0:
|
||||||
LOG.info(
|
LOG.info(
|
||||||
f"{self.__class__.__name__}::Saving"
|
f"{self.__class__.__name__}::Saving"
|
||||||
f" {len(self)} entries to disk at"
|
f" {len(self)} entries to disk at "
|
||||||
f"{CONF.save_location}",
|
f"{save_filename}",
|
||||||
)
|
)
|
||||||
with open(self._save_filename(), "wb+") as fp:
|
with self.lock:
|
||||||
pickle.dump(self._dump(), fp)
|
with open(save_filename, "wb+") as fp:
|
||||||
|
pickle.dump(self.data, fp)
|
||||||
else:
|
else:
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
"{} Nothing to save, flushing old save file '{}'".format(
|
"{} Nothing to save, flushing old save file '{}'".format(
|
||||||
self.__class__.__name__,
|
self.__class__.__name__,
|
||||||
self._save_filename(),
|
save_filename,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.flush()
|
self.flush()
|
||||||
|
@ -96,11 +104,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."""
|
||||||
|
|
|
@ -1,189 +1,4 @@
|
||||||
/* PrismJS 1.24.1
|
/* PrismJS 1.29.0
|
||||||
https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+log&plugins=show-language+toolbar */
|
https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+json+json5+log&plugins=show-language+toolbar */
|
||||||
/**
|
code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}
|
||||||
* prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML
|
div.code-toolbar{position:relative}div.code-toolbar>.toolbar{position:absolute;z-index:10;top:.3em;right:.2em;transition:opacity .3s ease-in-out;opacity:0}div.code-toolbar:hover>.toolbar{opacity:1}div.code-toolbar:focus-within>.toolbar{opacity:1}div.code-toolbar>.toolbar>.toolbar-item{display:inline-block}div.code-toolbar>.toolbar>.toolbar-item>a{cursor:pointer}div.code-toolbar>.toolbar>.toolbar-item>button{background:0 0;border:0;color:inherit;font:inherit;line-height:normal;overflow:visible;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}div.code-toolbar>.toolbar>.toolbar-item>a,div.code-toolbar>.toolbar>.toolbar-item>button,div.code-toolbar>.toolbar>.toolbar-item>span{color:#bbb;font-size:.8em;padding:0 .5em;background:#f5f2f0;background:rgba(224,224,224,.2);box-shadow:0 2px 0 0 rgba(0,0,0,.2);border-radius:.5em}div.code-toolbar>.toolbar>.toolbar-item>a:focus,div.code-toolbar>.toolbar>.toolbar-item>a:hover,div.code-toolbar>.toolbar>.toolbar-item>button:focus,div.code-toolbar>.toolbar>.toolbar-item>button:hover,div.code-toolbar>.toolbar>.toolbar-item>span:focus,div.code-toolbar>.toolbar>.toolbar-item>span:hover{color:inherit;text-decoration:none}
|
||||||
* Based on https://github.com/chriskempson/tomorrow-theme
|
|
||||||
* @author Rose Pritchard
|
|
||||||
*/
|
|
||||||
|
|
||||||
code[class*="language-"],
|
|
||||||
pre[class*="language-"] {
|
|
||||||
color: #ccc;
|
|
||||||
background: none;
|
|
||||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
|
||||||
font-size: 1em;
|
|
||||||
text-align: left;
|
|
||||||
white-space: pre;
|
|
||||||
word-spacing: normal;
|
|
||||||
word-break: normal;
|
|
||||||
word-wrap: normal;
|
|
||||||
line-height: 1.5;
|
|
||||||
|
|
||||||
-moz-tab-size: 4;
|
|
||||||
-o-tab-size: 4;
|
|
||||||
tab-size: 4;
|
|
||||||
|
|
||||||
-webkit-hyphens: none;
|
|
||||||
-moz-hyphens: none;
|
|
||||||
-ms-hyphens: none;
|
|
||||||
hyphens: none;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Code blocks */
|
|
||||||
pre[class*="language-"] {
|
|
||||||
padding: 1em;
|
|
||||||
margin: .5em 0;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:not(pre) > code[class*="language-"],
|
|
||||||
pre[class*="language-"] {
|
|
||||||
background: #2d2d2d;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Inline code */
|
|
||||||
:not(pre) > code[class*="language-"] {
|
|
||||||
padding: .1em;
|
|
||||||
border-radius: .3em;
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.comment,
|
|
||||||
.token.block-comment,
|
|
||||||
.token.prolog,
|
|
||||||
.token.doctype,
|
|
||||||
.token.cdata {
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.punctuation {
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.tag,
|
|
||||||
.token.attr-name,
|
|
||||||
.token.namespace,
|
|
||||||
.token.deleted {
|
|
||||||
color: #e2777a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.function-name {
|
|
||||||
color: #6196cc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.boolean,
|
|
||||||
.token.number,
|
|
||||||
.token.function {
|
|
||||||
color: #f08d49;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.property,
|
|
||||||
.token.class-name,
|
|
||||||
.token.constant,
|
|
||||||
.token.symbol {
|
|
||||||
color: #f8c555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.selector,
|
|
||||||
.token.important,
|
|
||||||
.token.atrule,
|
|
||||||
.token.keyword,
|
|
||||||
.token.builtin {
|
|
||||||
color: #cc99cd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.string,
|
|
||||||
.token.char,
|
|
||||||
.token.attr-value,
|
|
||||||
.token.regex,
|
|
||||||
.token.variable {
|
|
||||||
color: #7ec699;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.operator,
|
|
||||||
.token.entity,
|
|
||||||
.token.url {
|
|
||||||
color: #67cdcc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.important,
|
|
||||||
.token.bold {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.token.italic {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.entity {
|
|
||||||
cursor: help;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.inserted {
|
|
||||||
color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.code-toolbar {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.code-toolbar > .toolbar {
|
|
||||||
position: absolute;
|
|
||||||
top: .3em;
|
|
||||||
right: .2em;
|
|
||||||
transition: opacity 0.3s ease-in-out;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.code-toolbar:hover > .toolbar {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Separate line b/c rules are thrown out if selector is invalid.
|
|
||||||
IE11 and old Edge versions don't support :focus-within. */
|
|
||||||
div.code-toolbar:focus-within > .toolbar {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item > a {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item > button {
|
|
||||||
background: none;
|
|
||||||
border: 0;
|
|
||||||
color: inherit;
|
|
||||||
font: inherit;
|
|
||||||
line-height: normal;
|
|
||||||
overflow: visible;
|
|
||||||
padding: 0;
|
|
||||||
-webkit-user-select: none; /* for button */
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item > a,
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item > button,
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item > span {
|
|
||||||
color: #bbb;
|
|
||||||
font-size: .8em;
|
|
||||||
padding: 0 .5em;
|
|
||||||
background: #f5f2f0;
|
|
||||||
background: rgba(224, 224, 224, 0.2);
|
|
||||||
box-shadow: 0 2px 0 0 rgba(0,0,0,0.2);
|
|
||||||
border-radius: .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item > a:hover,
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item > a:focus,
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item > button:hover,
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item > button:focus,
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item > span:hover,
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item > span:focus {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
|
@ -219,15 +219,17 @@ function updateQuadData(chart, label, first, second, third, fourth) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function update_stats( data ) {
|
function update_stats( data ) {
|
||||||
our_callsign = data["stats"]["aprsd"]["callsign"];
|
our_callsign = data["APRSDStats"]["callsign"];
|
||||||
$("#version").text( data["stats"]["aprsd"]["version"] );
|
$("#version").text( data["APRSDStats"]["version"] );
|
||||||
$("#aprs_connection").html( data["aprs_connection"] );
|
$("#aprs_connection").html( data["aprs_connection"] );
|
||||||
$("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] );
|
$("#uptime").text( "uptime: " + data["APRSDStats"]["uptime"] );
|
||||||
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
|
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
|
||||||
$("#jsonstats").html(html_pretty);
|
$("#jsonstats").html(html_pretty);
|
||||||
short_time = data["time"].split(/\s(.+)/)[1];
|
short_time = data["time"].split(/\s(.+)/)[1];
|
||||||
updateDualData(packets_chart, short_time, data["stats"]["packets"]["sent"], data["stats"]["packets"]["received"]);
|
packet_list = data["PacketList"]["packets"];
|
||||||
updateQuadData(message_chart, short_time, data["stats"]["messages"]["sent"], data["stats"]["messages"]["received"], data["stats"]["messages"]["ack_sent"], data["stats"]["messages"]["ack_recieved"]);
|
updateDualData(packets_chart, short_time, data["PacketList"]["sent"], data["PacketList"]["received"]);
|
||||||
updateDualData(email_chart, short_time, data["stats"]["email"]["sent"], data["stats"]["email"]["recieved"]);
|
updateQuadData(message_chart, short_time, packet_list["MessagePacket"]["tx"], packet_list["MessagePacket"]["rx"],
|
||||||
updateDualData(memory_chart, short_time, data["stats"]["aprsd"]["memory_peak"], data["stats"]["aprsd"]["memory_current"]);
|
packet_list["AckPacket"]["tx"], packet_list["AckPacket"]["rx"]);
|
||||||
|
updateDualData(email_chart, short_time, data["EmailStats"]["sent"], data["EmailStats"]["recieved"]);
|
||||||
|
updateDualData(memory_chart, short_time, data["APRSDStats"]["memory_peak"], data["APRSDStats"]["memory_current"]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,8 @@ var packet_types_data = {};
|
||||||
var mem_current = []
|
var mem_current = []
|
||||||
var mem_peak = []
|
var mem_peak = []
|
||||||
|
|
||||||
|
var thread_current = []
|
||||||
|
|
||||||
|
|
||||||
function start_charts() {
|
function start_charts() {
|
||||||
console.log("start_charts() called");
|
console.log("start_charts() called");
|
||||||
|
@ -17,6 +19,7 @@ function start_charts() {
|
||||||
create_messages_chart();
|
create_messages_chart();
|
||||||
create_ack_chart();
|
create_ack_chart();
|
||||||
create_memory_chart();
|
create_memory_chart();
|
||||||
|
create_thread_chart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -258,6 +261,49 @@ function create_memory_chart() {
|
||||||
memory_chart.setOption(option);
|
memory_chart.setOption(option);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function create_thread_chart() {
|
||||||
|
thread_canvas = document.getElementById('threadChart');
|
||||||
|
thread_chart = echarts.init(thread_canvas);
|
||||||
|
|
||||||
|
// Specify the configuration items and data for the chart
|
||||||
|
var option = {
|
||||||
|
title: {
|
||||||
|
text: 'Active Threads'
|
||||||
|
},
|
||||||
|
legend: {},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis'
|
||||||
|
},
|
||||||
|
toolbox: {
|
||||||
|
show: true,
|
||||||
|
feature: {
|
||||||
|
mark : {show: true},
|
||||||
|
dataView : {show: true, readOnly: false},
|
||||||
|
magicType : {show: true, type: ['line', 'bar']},
|
||||||
|
restore : {show: true},
|
||||||
|
saveAsImage : {show: true}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
calculable: true,
|
||||||
|
xAxis: { type: 'time' },
|
||||||
|
yAxis: { },
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'current',
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
color: 'red',
|
||||||
|
encode: {
|
||||||
|
x: 'timestamp',
|
||||||
|
y: 'current' // refer sensor 1 value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
thread_chart.setOption(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -327,7 +373,6 @@ function updatePacketTypesChart() {
|
||||||
option = {
|
option = {
|
||||||
series: series
|
series: series
|
||||||
}
|
}
|
||||||
console.log(option)
|
|
||||||
packet_types_chart.setOption(option);
|
packet_types_chart.setOption(option);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -372,6 +417,21 @@ function updateMemChart(time, current, peak) {
|
||||||
memory_chart.setOption(option);
|
memory_chart.setOption(option);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateThreadChart(time, threads) {
|
||||||
|
keys = Object.keys(threads);
|
||||||
|
thread_count = keys.length;
|
||||||
|
thread_current.push([time, thread_count]);
|
||||||
|
option = {
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'current',
|
||||||
|
data: thread_current,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
thread_chart.setOption(option);
|
||||||
|
}
|
||||||
|
|
||||||
function updateMessagesChart() {
|
function updateMessagesChart() {
|
||||||
updateTypeChart(message_chart, "MessagePacket")
|
updateTypeChart(message_chart, "MessagePacket")
|
||||||
}
|
}
|
||||||
|
@ -381,22 +441,24 @@ function updateAcksChart() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function update_stats( data ) {
|
function update_stats( data ) {
|
||||||
console.log(data);
|
console.log("update_stats() echarts.js called")
|
||||||
our_callsign = data["stats"]["aprsd"]["callsign"];
|
stats = data["stats"];
|
||||||
$("#version").text( data["stats"]["aprsd"]["version"] );
|
our_callsign = stats["APRSDStats"]["callsign"];
|
||||||
$("#aprs_connection").html( data["aprs_connection"] );
|
$("#version").text( stats["APRSDStats"]["version"] );
|
||||||
$("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] );
|
$("#aprs_connection").html( stats["aprs_connection"] );
|
||||||
|
$("#uptime").text( "uptime: " + stats["APRSDStats"]["uptime"] );
|
||||||
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
|
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
|
||||||
$("#jsonstats").html(html_pretty);
|
$("#jsonstats").html(html_pretty);
|
||||||
|
|
||||||
t = Date.parse(data["time"]);
|
t = Date.parse(data["time"]);
|
||||||
ts = new Date(t);
|
ts = new Date(t);
|
||||||
updatePacketData(packets_chart, ts, data["stats"]["packets"]["sent"], data["stats"]["packets"]["received"]);
|
updatePacketData(packets_chart, ts, stats["PacketList"]["tx"], stats["PacketList"]["rx"]);
|
||||||
updatePacketTypesData(ts, data["stats"]["packets"]["types"]);
|
updatePacketTypesData(ts, stats["PacketList"]["types"]);
|
||||||
updatePacketTypesChart();
|
updatePacketTypesChart();
|
||||||
updateMessagesChart();
|
updateMessagesChart();
|
||||||
updateAcksChart();
|
updateAcksChart();
|
||||||
updateMemChart(ts, data["stats"]["aprsd"]["memory_current"], data["stats"]["aprsd"]["memory_peak"]);
|
updateMemChart(ts, stats["APRSDStats"]["memory_current"], stats["APRSDStats"]["memory_peak"]);
|
||||||
|
updateThreadChart(ts, stats["APRSDThreadList"]);
|
||||||
//updateQuadData(message_chart, short_time, data["stats"]["messages"]["sent"], data["stats"]["messages"]["received"], data["stats"]["messages"]["ack_sent"], data["stats"]["messages"]["ack_recieved"]);
|
//updateQuadData(message_chart, short_time, data["stats"]["messages"]["sent"], data["stats"]["messages"]["received"], data["stats"]["messages"]["ack_sent"], data["stats"]["messages"]["ack_recieved"]);
|
||||||
//updateDualData(email_chart, short_time, data["stats"]["email"]["sent"], data["stats"]["email"]["recieved"]);
|
//updateDualData(email_chart, short_time, data["stats"]["email"]["sent"], data["stats"]["email"]["recieved"]);
|
||||||
//updateDualData(memory_chart, short_time, data["stats"]["aprsd"]["memory_peak"], data["stats"]["aprsd"]["memory_current"]);
|
//updateDualData(memory_chart, short_time, data["stats"]["aprsd"]["memory_peak"], data["stats"]["aprsd"]["memory_current"]);
|
||||||
|
|
|
@ -24,11 +24,15 @@ function ord(str){return str.charCodeAt(0);}
|
||||||
|
|
||||||
|
|
||||||
function update_watchlist( data ) {
|
function update_watchlist( data ) {
|
||||||
// Update the watch list
|
// Update the watch list
|
||||||
|
stats = data["stats"];
|
||||||
|
if (stats.hasOwnProperty("WatchList") == false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
var watchdiv = $("#watchDiv");
|
var watchdiv = $("#watchDiv");
|
||||||
var html_str = '<table class="ui celled striped table"><thead><tr><th>HAM Callsign</th><th>Age since last seen by APRSD</th></tr></thead><tbody>'
|
var html_str = '<table class="ui celled striped table"><thead><tr><th>HAM Callsign</th><th>Age since last seen by APRSD</th></tr></thead><tbody>'
|
||||||
watchdiv.html('')
|
watchdiv.html('')
|
||||||
jQuery.each(data["stats"]["aprsd"]["watch_list"], function(i, val) {
|
jQuery.each(stats["WatchList"], function(i, val) {
|
||||||
html_str += '<tr><td class="collapsing"><img id="callsign_'+i+'" class="aprsd_1"></img>' + i + '</td><td>' + val["last"] + '</td></tr>'
|
html_str += '<tr><td class="collapsing"><img id="callsign_'+i+'" class="aprsd_1"></img>' + i + '</td><td>' + val["last"] + '</td></tr>'
|
||||||
});
|
});
|
||||||
html_str += "</tbody></table>";
|
html_str += "</tbody></table>";
|
||||||
|
@ -60,12 +64,16 @@ function update_watchlist_from_packet(callsign, val) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function update_seenlist( data ) {
|
function update_seenlist( data ) {
|
||||||
|
stats = data["stats"];
|
||||||
|
if (stats.hasOwnProperty("SeenList") == false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
var seendiv = $("#seenDiv");
|
var seendiv = $("#seenDiv");
|
||||||
var html_str = '<table class="ui celled striped table">'
|
var html_str = '<table class="ui celled striped table">'
|
||||||
html_str += '<thead><tr><th>HAM Callsign</th><th>Age since last seen by APRSD</th>'
|
html_str += '<thead><tr><th>HAM Callsign</th><th>Age since last seen by APRSD</th>'
|
||||||
html_str += '<th>Number of packets RX</th></tr></thead><tbody>'
|
html_str += '<th>Number of packets RX</th></tr></thead><tbody>'
|
||||||
seendiv.html('')
|
seendiv.html('')
|
||||||
var seen_list = data["stats"]["aprsd"]["seen_list"]
|
var seen_list = stats["SeenList"]
|
||||||
var len = Object.keys(seen_list).length
|
var len = Object.keys(seen_list).length
|
||||||
$('#seen_count').html(len)
|
$('#seen_count').html(len)
|
||||||
jQuery.each(seen_list, function(i, val) {
|
jQuery.each(seen_list, function(i, val) {
|
||||||
|
@ -79,6 +87,10 @@ function update_seenlist( data ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function update_plugins( data ) {
|
function update_plugins( data ) {
|
||||||
|
stats = data["stats"];
|
||||||
|
if (stats.hasOwnProperty("PluginManager") == false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
var plugindiv = $("#pluginDiv");
|
var plugindiv = $("#pluginDiv");
|
||||||
var html_str = '<table class="ui celled striped table"><thead><tr>'
|
var html_str = '<table class="ui celled striped table"><thead><tr>'
|
||||||
html_str += '<th>Plugin Name</th><th>Plugin Enabled?</th>'
|
html_str += '<th>Plugin Name</th><th>Plugin Enabled?</th>'
|
||||||
|
@ -87,7 +99,7 @@ function update_plugins( data ) {
|
||||||
html_str += '</tr></thead><tbody>'
|
html_str += '</tr></thead><tbody>'
|
||||||
plugindiv.html('')
|
plugindiv.html('')
|
||||||
|
|
||||||
var plugins = data["stats"]["plugins"];
|
var plugins = stats["PluginManager"];
|
||||||
var keys = Object.keys(plugins);
|
var keys = Object.keys(plugins);
|
||||||
keys.sort();
|
keys.sort();
|
||||||
for (var i=0; i<keys.length; i++) { // now lets iterate in sort order
|
for (var i=0; i<keys.length; i++) { // now lets iterate in sort order
|
||||||
|
@ -101,14 +113,42 @@ function update_plugins( data ) {
|
||||||
plugindiv.append(html_str);
|
plugindiv.append(html_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update_threads( data ) {
|
||||||
|
stats = data["stats"];
|
||||||
|
if (stats.hasOwnProperty("APRSDThreadList") == false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var threadsdiv = $("#threadsDiv");
|
||||||
|
var countdiv = $("#thread_count");
|
||||||
|
var html_str = '<table class="ui celled striped table"><thead><tr>'
|
||||||
|
html_str += '<th>Thread Name</th><th>Alive?</th>'
|
||||||
|
html_str += '<th>Age</th><th>Loop Count</th>'
|
||||||
|
html_str += '</tr></thead><tbody>'
|
||||||
|
threadsdiv.html('')
|
||||||
|
|
||||||
|
var threads = stats["APRSDThreadList"];
|
||||||
|
var keys = Object.keys(threads);
|
||||||
|
countdiv.html(keys.length);
|
||||||
|
keys.sort();
|
||||||
|
for (var i=0; i<keys.length; i++) { // now lets iterate in sort order
|
||||||
|
var key = keys[i];
|
||||||
|
var val = threads[key];
|
||||||
|
html_str += '<tr><td class="collapsing">' + key + '</td>';
|
||||||
|
html_str += '<td>' + val["alive"] + '</td><td>' + val["age"] + '</td>';
|
||||||
|
html_str += '<td>' + val["loop_count"] + '</td></tr>';
|
||||||
|
}
|
||||||
|
html_str += "</tbody></table>";
|
||||||
|
threadsdiv.append(html_str);
|
||||||
|
}
|
||||||
|
|
||||||
function update_packets( data ) {
|
function update_packets( data ) {
|
||||||
var packetsdiv = $("#packetsDiv");
|
var packetsdiv = $("#packetsDiv");
|
||||||
//nuke the contents first, then add to it.
|
//nuke the contents first, then add to it.
|
||||||
if (size_dict(packet_list) == 0 && size_dict(data) > 0) {
|
if (size_dict(packet_list) == 0 && size_dict(data) > 0) {
|
||||||
packetsdiv.html('')
|
packetsdiv.html('')
|
||||||
}
|
}
|
||||||
jQuery.each(data, function(i, val) {
|
jQuery.each(data.packets, function(i, val) {
|
||||||
pkt = JSON.parse(val);
|
pkt = val;
|
||||||
|
|
||||||
update_watchlist_from_packet(pkt['from_call'], pkt);
|
update_watchlist_from_packet(pkt['from_call'], pkt);
|
||||||
if ( packet_list.hasOwnProperty(pkt['timestamp']) == false ) {
|
if ( packet_list.hasOwnProperty(pkt['timestamp']) == false ) {
|
||||||
|
@ -167,6 +207,7 @@ function start_update() {
|
||||||
update_watchlist(data);
|
update_watchlist(data);
|
||||||
update_seenlist(data);
|
update_seenlist(data);
|
||||||
update_plugins(data);
|
update_plugins(data);
|
||||||
|
update_threads(data);
|
||||||
},
|
},
|
||||||
complete: function() {
|
complete: function() {
|
||||||
setTimeout(statsworker, 10000);
|
setTimeout(statsworker, 10000);
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1,57 +0,0 @@
|
||||||
/* Root element */
|
|
||||||
.json-document {
|
|
||||||
padding: 1em 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Syntax highlighting for JSON objects */
|
|
||||||
ul.json-dict, ol.json-array {
|
|
||||||
list-style-type: none;
|
|
||||||
margin: 0 0 0 1px;
|
|
||||||
border-left: 1px dotted #ccc;
|
|
||||||
padding-left: 2em;
|
|
||||||
}
|
|
||||||
.json-string {
|
|
||||||
color: #0B7500;
|
|
||||||
}
|
|
||||||
.json-literal {
|
|
||||||
color: #1A01CC;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toggle button */
|
|
||||||
a.json-toggle {
|
|
||||||
position: relative;
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
a.json-toggle:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
a.json-toggle:before {
|
|
||||||
font-size: 1.1em;
|
|
||||||
color: #c0c0c0;
|
|
||||||
content: "\25BC"; /* down arrow */
|
|
||||||
position: absolute;
|
|
||||||
display: inline-block;
|
|
||||||
width: 1em;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1em;
|
|
||||||
left: -1.2em;
|
|
||||||
}
|
|
||||||
a.json-toggle:hover:before {
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
a.json-toggle.collapsed:before {
|
|
||||||
/* Use rotated down arrow, prevents right arrow appearing smaller than down arrow in some browsers */
|
|
||||||
transform: rotate(-90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Collapsable placeholder links */
|
|
||||||
a.json-placeholder {
|
|
||||||
color: #aaa;
|
|
||||||
padding: 0 1em;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
a.json-placeholder:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
|
@ -1,158 +0,0 @@
|
||||||
/**
|
|
||||||
* jQuery json-viewer
|
|
||||||
* @author: Alexandre Bodelot <alexandre.bodelot@gmail.com>
|
|
||||||
* @link: https://github.com/abodelot/jquery.json-viewer
|
|
||||||
*/
|
|
||||||
(function($) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if arg is either an array with at least 1 element, or a dict with at least 1 key
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
function isCollapsable(arg) {
|
|
||||||
return arg instanceof Object && Object.keys(arg).length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a string represents a valid url
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
function isUrl(string) {
|
|
||||||
var urlRegexp = /^(https?:\/\/|ftps?:\/\/)?([a-z0-9%-]+\.){1,}([a-z0-9-]+)?(:(\d{1,5}))?(\/([a-z0-9\-._~:/?#[\]@!$&'()*+,;=%]+)?)?$/i;
|
|
||||||
return urlRegexp.test(string);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform a json object into html representation
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
function json2html(json, options) {
|
|
||||||
var html = '';
|
|
||||||
if (typeof json === 'string') {
|
|
||||||
// Escape tags and quotes
|
|
||||||
json = json
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/'/g, ''')
|
|
||||||
.replace(/"/g, '"');
|
|
||||||
|
|
||||||
if (options.withLinks && isUrl(json)) {
|
|
||||||
html += '<a href="' + json + '" class="json-string" target="_blank">' + json + '</a>';
|
|
||||||
} else {
|
|
||||||
// Escape double quotes in the rendered non-URL string.
|
|
||||||
json = json.replace(/"/g, '\\"');
|
|
||||||
html += '<span class="json-string">"' + json + '"</span>';
|
|
||||||
}
|
|
||||||
} else if (typeof json === 'number') {
|
|
||||||
html += '<span class="json-literal">' + json + '</span>';
|
|
||||||
} else if (typeof json === 'boolean') {
|
|
||||||
html += '<span class="json-literal">' + json + '</span>';
|
|
||||||
} else if (json === null) {
|
|
||||||
html += '<span class="json-literal">null</span>';
|
|
||||||
} else if (json instanceof Array) {
|
|
||||||
if (json.length > 0) {
|
|
||||||
html += '[<ol class="json-array">';
|
|
||||||
for (var i = 0; i < json.length; ++i) {
|
|
||||||
html += '<li>';
|
|
||||||
// Add toggle button if item is collapsable
|
|
||||||
if (isCollapsable(json[i])) {
|
|
||||||
html += '<a href class="json-toggle"></a>';
|
|
||||||
}
|
|
||||||
html += json2html(json[i], options);
|
|
||||||
// Add comma if item is not last
|
|
||||||
if (i < json.length - 1) {
|
|
||||||
html += ',';
|
|
||||||
}
|
|
||||||
html += '</li>';
|
|
||||||
}
|
|
||||||
html += '</ol>]';
|
|
||||||
} else {
|
|
||||||
html += '[]';
|
|
||||||
}
|
|
||||||
} else if (typeof json === 'object') {
|
|
||||||
var keyCount = Object.keys(json).length;
|
|
||||||
if (keyCount > 0) {
|
|
||||||
html += '{<ul class="json-dict">';
|
|
||||||
for (var key in json) {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(json, key)) {
|
|
||||||
html += '<li>';
|
|
||||||
var keyRepr = options.withQuotes ?
|
|
||||||
'<span class="json-string">"' + key + '"</span>' : key;
|
|
||||||
// Add toggle button if item is collapsable
|
|
||||||
if (isCollapsable(json[key])) {
|
|
||||||
html += '<a href class="json-toggle">' + keyRepr + '</a>';
|
|
||||||
} else {
|
|
||||||
html += keyRepr;
|
|
||||||
}
|
|
||||||
html += ': ' + json2html(json[key], options);
|
|
||||||
// Add comma if item is not last
|
|
||||||
if (--keyCount > 0) {
|
|
||||||
html += ',';
|
|
||||||
}
|
|
||||||
html += '</li>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
html += '</ul>}';
|
|
||||||
} else {
|
|
||||||
html += '{}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* jQuery plugin method
|
|
||||||
* @param json: a javascript object
|
|
||||||
* @param options: an optional options hash
|
|
||||||
*/
|
|
||||||
$.fn.jsonViewer = function(json, options) {
|
|
||||||
// Merge user options with default options
|
|
||||||
options = Object.assign({}, {
|
|
||||||
collapsed: false,
|
|
||||||
rootCollapsable: true,
|
|
||||||
withQuotes: false,
|
|
||||||
withLinks: true
|
|
||||||
}, options);
|
|
||||||
|
|
||||||
// jQuery chaining
|
|
||||||
return this.each(function() {
|
|
||||||
|
|
||||||
// Transform to HTML
|
|
||||||
var html = json2html(json, options);
|
|
||||||
if (options.rootCollapsable && isCollapsable(json)) {
|
|
||||||
html = '<a href class="json-toggle"></a>' + html;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert HTML in target DOM element
|
|
||||||
$(this).html(html);
|
|
||||||
$(this).addClass('json-document');
|
|
||||||
|
|
||||||
// Bind click on toggle buttons
|
|
||||||
$(this).off('click');
|
|
||||||
$(this).on('click', 'a.json-toggle', function() {
|
|
||||||
var target = $(this).toggleClass('collapsed').siblings('ul.json-dict, ol.json-array');
|
|
||||||
target.toggle();
|
|
||||||
if (target.is(':visible')) {
|
|
||||||
target.siblings('.json-placeholder').remove();
|
|
||||||
} else {
|
|
||||||
var count = target.children('li').length;
|
|
||||||
var placeholder = count + (count > 1 ? ' items' : ' item');
|
|
||||||
target.after('<a href class="json-placeholder">' + placeholder + '</a>');
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Simulate click on toggle button when placeholder is clicked
|
|
||||||
$(this).on('click', 'a.json-placeholder', function() {
|
|
||||||
$(this).siblings('a.json-toggle').click();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (options.collapsed == true) {
|
|
||||||
// Trigger click to collapse all nodes
|
|
||||||
$(this).find('a.json-toggle').click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
})(jQuery);
|
|
|
@ -30,7 +30,6 @@
|
||||||
var color = Chart.helpers.color;
|
var color = Chart.helpers.color;
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
console.log(initial_stats);
|
|
||||||
start_update();
|
start_update();
|
||||||
start_charts();
|
start_charts();
|
||||||
init_messages();
|
init_messages();
|
||||||
|
@ -82,6 +81,7 @@
|
||||||
<div class="item" data-tab="seen-tab">Seen List</div>
|
<div class="item" data-tab="seen-tab">Seen List</div>
|
||||||
<div class="item" data-tab="watch-tab">Watch List</div>
|
<div class="item" data-tab="watch-tab">Watch List</div>
|
||||||
<div class="item" data-tab="plugin-tab">Plugins</div>
|
<div class="item" data-tab="plugin-tab">Plugins</div>
|
||||||
|
<div class="item" data-tab="threads-tab">Threads</div>
|
||||||
<div class="item" data-tab="config-tab">Config</div>
|
<div class="item" data-tab="config-tab">Config</div>
|
||||||
<div class="item" data-tab="log-tab">LogFile</div>
|
<div class="item" data-tab="log-tab">LogFile</div>
|
||||||
<!-- <div class="item" data-tab="oslo-tab">OSLO CONFIG</div> //-->
|
<!-- <div class="item" data-tab="oslo-tab">OSLO CONFIG</div> //-->
|
||||||
|
@ -97,11 +97,6 @@
|
||||||
<div class="ui segment" style="height: 300px" id="packetsChart"></div>
|
<div class="ui segment" style="height: 300px" id="packetsChart"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
|
||||||
<div class="column">
|
|
||||||
<div class="ui segment" style="height: 300px" id="packetTypesChart"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="ui segment" style="height: 300px" id="messagesChart"></div>
|
<div class="ui segment" style="height: 300px" id="messagesChart"></div>
|
||||||
|
@ -112,8 +107,17 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="ui segment" style="height: 300px" id="memChart">
|
<div class="ui segment" style="height: 300px" id="packetTypesChart"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="column">
|
||||||
|
<div class="ui segment" style="height: 300px" id="threadChart"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="column">
|
||||||
|
<div class="ui segment" style="height: 300px" id="memChart"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="row">
|
<!-- <div class="row">
|
||||||
|
@ -156,6 +160,13 @@
|
||||||
<div id="pluginDiv" class="ui mini text">Loading</div>
|
<div id="pluginDiv" class="ui mini text">Loading</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="ui bottom attached tab segment" data-tab="threads-tab">
|
||||||
|
<h3 class="ui dividing header">
|
||||||
|
Threads Loaded (<span id="thread_count">{{ thread_count }}</span>)
|
||||||
|
</h3>
|
||||||
|
<div id="threadsDiv" class="ui mini text">Loading</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="ui bottom attached tab segment" data-tab="config-tab">
|
<div class="ui bottom attached tab segment" data-tab="config-tab">
|
||||||
<h3 class="ui dividing header">Config</h3>
|
<h3 class="ui dividing header">Config</h3>
|
||||||
<pre id="configjson" class="language-json">{{ config_json|safe }}</pre>
|
<pre id="configjson" class="language-json">{{ config_json|safe }}</pre>
|
||||||
|
@ -174,7 +185,7 @@
|
||||||
|
|
||||||
<div class="ui bottom attached tab segment" data-tab="raw-tab">
|
<div class="ui bottom attached tab segment" data-tab="raw-tab">
|
||||||
<h3 class="ui dividing header">Raw JSON</h3>
|
<h3 class="ui dividing header">Raw JSON</h3>
|
||||||
<pre id="jsonstats" class="language-yaml" style="height:600px;overflow-y:auto;">{{ stats|safe }}</pre>
|
<pre id="jsonstats" class="language-yaml" style="height:600px;overflow-y:auto;">{{ initial_stats|safe }}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ui text container">
|
<div class="ui text container">
|
||||||
|
|
|
@ -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 |
|
@ -64,9 +64,11 @@ function showError(error) {
|
||||||
|
|
||||||
function showPosition(position) {
|
function showPosition(position) {
|
||||||
console.log("showPosition Called");
|
console.log("showPosition Called");
|
||||||
|
path = $('#pkt_path option:selected').val();
|
||||||
msg = {
|
msg = {
|
||||||
'latitude': position.coords.latitude,
|
'latitude': position.coords.latitude,
|
||||||
'longitude': position.coords.longitude
|
'longitude': position.coords.longitude,
|
||||||
|
'path': path,
|
||||||
}
|
}
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
$.toast({
|
$.toast({
|
||||||
|
|
|
@ -19,9 +19,10 @@ function show_aprs_icon(item, symbol) {
|
||||||
function ord(str){return str.charCodeAt(0);}
|
function ord(str){return str.charCodeAt(0);}
|
||||||
|
|
||||||
function update_stats( data ) {
|
function update_stats( data ) {
|
||||||
$("#version").text( data["stats"]["aprsd"]["version"] );
|
console.log(data);
|
||||||
|
$("#version").text( data["stats"]["APRSDStats"]["version"] );
|
||||||
$("#aprs_connection").html( data["aprs_connection"] );
|
$("#aprs_connection").html( data["aprs_connection"] );
|
||||||
$("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] );
|
$("#uptime").text( "uptime: " + data["stats"]["APRSDStats"]["uptime"] );
|
||||||
short_time = data["time"].split(/\s(.+)/)[1];
|
short_time = data["time"].split(/\s(.+)/)[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +38,7 @@ function start_update() {
|
||||||
update_stats(data);
|
update_stats(data);
|
||||||
},
|
},
|
||||||
complete: function() {
|
complete: function() {
|
||||||
setTimeout(statsworker, 10000);
|
setTimeout(statsworker, 60000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -258,6 +316,7 @@ function create_callsign_tab(callsign, active=false) {
|
||||||
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 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 +332,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 +362,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 +387,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 +398,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);
|
||||||
|
@ -335,18 +408,21 @@ function append_message(callsign, msg, msg_html) {
|
||||||
tab_notify_id = tab_notification_id(callsign, true);
|
tab_notify_id = tab_notification_id(callsign, true);
|
||||||
// get the current count of notifications
|
// get the current count of notifications
|
||||||
count = parseInt($(tab_notify_id).text());
|
count = parseInt($(tab_notify_id).text());
|
||||||
|
if (isNaN(count)) {
|
||||||
|
count = 0;
|
||||||
|
}
|
||||||
count += 1;
|
count += 1;
|
||||||
$(tab_notify_id).text(count);
|
$(tab_notify_id).text(count);
|
||||||
$(tab_notify_id).removeClass('visually-hidden');
|
$(tab_notify_id).removeClass('visually-hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 +458,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 +483,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) {
|
||||||
|
@ -430,7 +505,7 @@ function sent_msg(msg) {
|
||||||
msg_html = create_message_html(d, t, msg['from_call'], msg['to_call'], msg['message_text'], ack_id, msg, false);
|
msg_html = create_message_html(d, t, msg['from_call'], msg['to_call'], msg['message_text'], ack_id, msg, false);
|
||||||
append_message(msg['to_call'], msg, msg_html);
|
append_message(msg['to_call'], msg, msg_html);
|
||||||
save_data();
|
save_data();
|
||||||
scroll_main_content(msg['from_call']);
|
scroll_main_content(msg['to_call']);
|
||||||
}
|
}
|
||||||
|
|
||||||
function from_msg(msg) {
|
function from_msg(msg) {
|
||||||
|
@ -440,12 +515,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 +576,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');
|
||||||
|
}
|
||||||
|
|
|
@ -1,57 +0,0 @@
|
||||||
/* Root element */
|
|
||||||
.json-document {
|
|
||||||
padding: 1em 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Syntax highlighting for JSON objects */
|
|
||||||
ul.json-dict, ol.json-array {
|
|
||||||
list-style-type: none;
|
|
||||||
margin: 0 0 0 1px;
|
|
||||||
border-left: 1px dotted #ccc;
|
|
||||||
padding-left: 2em;
|
|
||||||
}
|
|
||||||
.json-string {
|
|
||||||
color: #0B7500;
|
|
||||||
}
|
|
||||||
.json-literal {
|
|
||||||
color: #1A01CC;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toggle button */
|
|
||||||
a.json-toggle {
|
|
||||||
position: relative;
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
a.json-toggle:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
a.json-toggle:before {
|
|
||||||
font-size: 1.1em;
|
|
||||||
color: #c0c0c0;
|
|
||||||
content: "\25BC"; /* down arrow */
|
|
||||||
position: absolute;
|
|
||||||
display: inline-block;
|
|
||||||
width: 1em;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1em;
|
|
||||||
left: -1.2em;
|
|
||||||
}
|
|
||||||
a.json-toggle:hover:before {
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
a.json-toggle.collapsed:before {
|
|
||||||
/* Use rotated down arrow, prevents right arrow appearing smaller than down arrow in some browsers */
|
|
||||||
transform: rotate(-90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Collapsable placeholder links */
|
|
||||||
a.json-placeholder {
|
|
||||||
color: #aaa;
|
|
||||||
padding: 0 1em;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
a.json-placeholder:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
|
@ -1,158 +0,0 @@
|
||||||
/**
|
|
||||||
* jQuery json-viewer
|
|
||||||
* @author: Alexandre Bodelot <alexandre.bodelot@gmail.com>
|
|
||||||
* @link: https://github.com/abodelot/jquery.json-viewer
|
|
||||||
*/
|
|
||||||
(function($) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if arg is either an array with at least 1 element, or a dict with at least 1 key
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
function isCollapsable(arg) {
|
|
||||||
return arg instanceof Object && Object.keys(arg).length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a string represents a valid url
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
function isUrl(string) {
|
|
||||||
var urlRegexp = /^(https?:\/\/|ftps?:\/\/)?([a-z0-9%-]+\.){1,}([a-z0-9-]+)?(:(\d{1,5}))?(\/([a-z0-9\-._~:/?#[\]@!$&'()*+,;=%]+)?)?$/i;
|
|
||||||
return urlRegexp.test(string);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform a json object into html representation
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
function json2html(json, options) {
|
|
||||||
var html = '';
|
|
||||||
if (typeof json === 'string') {
|
|
||||||
// Escape tags and quotes
|
|
||||||
json = json
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/'/g, ''')
|
|
||||||
.replace(/"/g, '"');
|
|
||||||
|
|
||||||
if (options.withLinks && isUrl(json)) {
|
|
||||||
html += '<a href="' + json + '" class="json-string" target="_blank">' + json + '</a>';
|
|
||||||
} else {
|
|
||||||
// Escape double quotes in the rendered non-URL string.
|
|
||||||
json = json.replace(/"/g, '\\"');
|
|
||||||
html += '<span class="json-string">"' + json + '"</span>';
|
|
||||||
}
|
|
||||||
} else if (typeof json === 'number') {
|
|
||||||
html += '<span class="json-literal">' + json + '</span>';
|
|
||||||
} else if (typeof json === 'boolean') {
|
|
||||||
html += '<span class="json-literal">' + json + '</span>';
|
|
||||||
} else if (json === null) {
|
|
||||||
html += '<span class="json-literal">null</span>';
|
|
||||||
} else if (json instanceof Array) {
|
|
||||||
if (json.length > 0) {
|
|
||||||
html += '[<ol class="json-array">';
|
|
||||||
for (var i = 0; i < json.length; ++i) {
|
|
||||||
html += '<li>';
|
|
||||||
// Add toggle button if item is collapsable
|
|
||||||
if (isCollapsable(json[i])) {
|
|
||||||
html += '<a href class="json-toggle"></a>';
|
|
||||||
}
|
|
||||||
html += json2html(json[i], options);
|
|
||||||
// Add comma if item is not last
|
|
||||||
if (i < json.length - 1) {
|
|
||||||
html += ',';
|
|
||||||
}
|
|
||||||
html += '</li>';
|
|
||||||
}
|
|
||||||
html += '</ol>]';
|
|
||||||
} else {
|
|
||||||
html += '[]';
|
|
||||||
}
|
|
||||||
} else if (typeof json === 'object') {
|
|
||||||
var keyCount = Object.keys(json).length;
|
|
||||||
if (keyCount > 0) {
|
|
||||||
html += '{<ul class="json-dict">';
|
|
||||||
for (var key in json) {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(json, key)) {
|
|
||||||
html += '<li>';
|
|
||||||
var keyRepr = options.withQuotes ?
|
|
||||||
'<span class="json-string">"' + key + '"</span>' : key;
|
|
||||||
// Add toggle button if item is collapsable
|
|
||||||
if (isCollapsable(json[key])) {
|
|
||||||
html += '<a href class="json-toggle">' + keyRepr + '</a>';
|
|
||||||
} else {
|
|
||||||
html += keyRepr;
|
|
||||||
}
|
|
||||||
html += ': ' + json2html(json[key], options);
|
|
||||||
// Add comma if item is not last
|
|
||||||
if (--keyCount > 0) {
|
|
||||||
html += ',';
|
|
||||||
}
|
|
||||||
html += '</li>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
html += '</ul>}';
|
|
||||||
} else {
|
|
||||||
html += '{}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* jQuery plugin method
|
|
||||||
* @param json: a javascript object
|
|
||||||
* @param options: an optional options hash
|
|
||||||
*/
|
|
||||||
$.fn.jsonViewer = function(json, options) {
|
|
||||||
// Merge user options with default options
|
|
||||||
options = Object.assign({}, {
|
|
||||||
collapsed: false,
|
|
||||||
rootCollapsable: true,
|
|
||||||
withQuotes: false,
|
|
||||||
withLinks: true
|
|
||||||
}, options);
|
|
||||||
|
|
||||||
// jQuery chaining
|
|
||||||
return this.each(function() {
|
|
||||||
|
|
||||||
// Transform to HTML
|
|
||||||
var html = json2html(json, options);
|
|
||||||
if (options.rootCollapsable && isCollapsable(json)) {
|
|
||||||
html = '<a href class="json-toggle"></a>' + html;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert HTML in target DOM element
|
|
||||||
$(this).html(html);
|
|
||||||
$(this).addClass('json-document');
|
|
||||||
|
|
||||||
// Bind click on toggle buttons
|
|
||||||
$(this).off('click');
|
|
||||||
$(this).on('click', 'a.json-toggle', function() {
|
|
||||||
var target = $(this).toggleClass('collapsed').siblings('ul.json-dict, ol.json-array');
|
|
||||||
target.toggle();
|
|
||||||
if (target.is(':visible')) {
|
|
||||||
target.siblings('.json-placeholder').remove();
|
|
||||||
} else {
|
|
||||||
var count = target.children('li').length;
|
|
||||||
var placeholder = count + (count > 1 ? ' items' : ' item');
|
|
||||||
target.after('<a href class="json-placeholder">' + placeholder + '</a>');
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Simulate click on toggle button when placeholder is clicked
|
|
||||||
$(this).on('click', 'a.json-placeholder', function() {
|
|
||||||
$(this).siblings('a.json-toggle').click();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (options.collapsed == true) {
|
|
||||||
// Trigger click to collapse all nodes
|
|
||||||
$(this).find('a.json-toggle').click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
})(jQuery);
|
|
|
@ -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,10 +99,11 @@
|
||||||
<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>
|
||||||
|
<option value="GATE">GATE</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
|
|
226
aprsd/wsgi.py
226
aprsd/wsgi.py
|
@ -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 queue
|
||||||
import time
|
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from flask import Flask
|
from flask import Flask, request
|
||||||
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,15 +14,17 @@ 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.threads import stats as stats_threads
|
||||||
|
from aprsd.utils import json as aprsd_json
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
LOG = logging.getLogger("gunicorn.access")
|
LOG = logging.getLogger("gunicorn.access")
|
||||||
|
logging_queue = queue.Queue()
|
||||||
|
|
||||||
auth = HTTPBasicAuth()
|
auth = HTTPBasicAuth()
|
||||||
users = {}
|
users: dict[str, str] = {}
|
||||||
app = Flask(
|
app = Flask(
|
||||||
"aprsd",
|
"aprsd",
|
||||||
static_url_path="/static",
|
static_url_path="/static",
|
||||||
|
@ -47,114 +47,40 @@ def verify_password(username, password):
|
||||||
|
|
||||||
|
|
||||||
def _stats():
|
def _stats():
|
||||||
track = aprsd_rpc_client.RPCClient().get_packet_track()
|
stats_obj = stats_threads.StatsStore()
|
||||||
|
stats_obj.load()
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
|
|
||||||
time_format = "%m-%d-%Y %H:%M:%S"
|
time_format = "%m-%d-%Y %H:%M:%S"
|
||||||
|
stats = {
|
||||||
stats_dict = aprsd_rpc_client.RPCClient().get_stats_dict()
|
|
||||||
if not stats_dict:
|
|
||||||
stats_dict = {
|
|
||||||
"aprsd": {},
|
|
||||||
"aprs-is": {"server": ""},
|
|
||||||
"messages": {
|
|
||||||
"sent": 0,
|
|
||||||
"received": 0,
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"sent": 0,
|
|
||||||
"received": 0,
|
|
||||||
},
|
|
||||||
"seen_list": {
|
|
||||||
"sent": 0,
|
|
||||||
"received": 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
# Convert the watch_list entries to age
|
|
||||||
wl = aprsd_rpc_client.RPCClient().get_watch_list()
|
|
||||||
new_list = {}
|
|
||||||
if wl:
|
|
||||||
for call in wl.get_all():
|
|
||||||
# call_date = datetime.datetime.strptime(
|
|
||||||
# str(wl.last_seen(call)),
|
|
||||||
# "%Y-%m-%d %H:%M:%S.%f",
|
|
||||||
# )
|
|
||||||
|
|
||||||
# We have to convert the RingBuffer to a real list
|
|
||||||
# so that json.dumps works.
|
|
||||||
# pkts = []
|
|
||||||
# for pkt in wl.get(call)["packets"].get():
|
|
||||||
# pkts.append(pkt)
|
|
||||||
|
|
||||||
new_list[call] = {
|
|
||||||
"last": wl.age(call),
|
|
||||||
# "packets": pkts
|
|
||||||
}
|
|
||||||
|
|
||||||
stats_dict["aprsd"]["watch_list"] = new_list
|
|
||||||
packet_list = aprsd_rpc_client.RPCClient().get_packet_list()
|
|
||||||
rx = tx = 0
|
|
||||||
types = {}
|
|
||||||
if packet_list:
|
|
||||||
rx = packet_list.total_rx()
|
|
||||||
tx = packet_list.total_tx()
|
|
||||||
types_copy = packet_list.types.copy()
|
|
||||||
|
|
||||||
for key in types_copy:
|
|
||||||
types[str(key)] = dict(types_copy[key])
|
|
||||||
|
|
||||||
stats_dict["packets"] = {
|
|
||||||
"sent": tx,
|
|
||||||
"received": rx,
|
|
||||||
"types": types,
|
|
||||||
}
|
|
||||||
if track:
|
|
||||||
size_tracker = len(track)
|
|
||||||
else:
|
|
||||||
size_tracker = 0
|
|
||||||
|
|
||||||
result = {
|
|
||||||
"time": now.strftime(time_format),
|
"time": now.strftime(time_format),
|
||||||
"size_tracker": size_tracker,
|
"stats": stats_obj.data,
|
||||||
"stats": stats_dict,
|
|
||||||
}
|
}
|
||||||
|
return stats
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/stats")
|
@app.route("/stats")
|
||||||
def stats():
|
def stats():
|
||||||
LOG.debug("/stats called")
|
LOG.debug("/stats called")
|
||||||
return json.dumps(_stats())
|
return json.dumps(_stats(), cls=aprsd_json.SimpleJSONEncoder)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
stats = _stats()
|
stats = _stats()
|
||||||
wl = aprsd_rpc_client.RPCClient().get_watch_list()
|
|
||||||
if wl and wl.is_enabled():
|
|
||||||
watch_count = len(wl)
|
|
||||||
watch_age = wl.max_delta()
|
|
||||||
else:
|
|
||||||
watch_count = 0
|
|
||||||
watch_age = 0
|
|
||||||
|
|
||||||
sl = aprsd_rpc_client.RPCClient().get_seen_list()
|
|
||||||
if sl:
|
|
||||||
seen_count = len(sl)
|
|
||||||
else:
|
|
||||||
seen_count = 0
|
|
||||||
|
|
||||||
pm = plugin.PluginManager()
|
pm = plugin.PluginManager()
|
||||||
plugins = pm.get_plugins()
|
plugins = pm.get_plugins()
|
||||||
plugin_count = len(plugins)
|
plugin_count = len(plugins)
|
||||||
|
client_stats = stats["stats"].get("APRSClientStats", {})
|
||||||
|
|
||||||
if CONF.aprs_network.enabled:
|
if CONF.aprs_network.enabled:
|
||||||
transport = "aprs-is"
|
transport = "aprs-is"
|
||||||
|
if client_stats:
|
||||||
|
aprs_connection = client_stats.get("server_string", "")
|
||||||
|
else:
|
||||||
|
aprs_connection = "APRS-IS"
|
||||||
aprs_connection = (
|
aprs_connection = (
|
||||||
"APRS-IS Server: <a href='http://status.aprs2.net' >"
|
"APRS-IS Server: <a href='http://status.aprs2.net' >"
|
||||||
"{}</a>".format(stats["stats"]["aprs-is"]["server"])
|
"{}</a>".format(aprs_connection)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# We might be connected to a KISS socket?
|
# We might be connected to a KISS socket?
|
||||||
|
@ -175,13 +101,20 @@ def index():
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
stats["transport"] = transport
|
if client_stats:
|
||||||
stats["aprs_connection"] = aprs_connection
|
stats["stats"]["APRSClientStats"]["transport"] = transport
|
||||||
|
stats["stats"]["APRSClientStats"]["aprs_connection"] = aprs_connection
|
||||||
entries = conf.conf_to_dict()
|
entries = conf.conf_to_dict()
|
||||||
|
|
||||||
|
thread_info = stats["stats"].get("APRSDThreadList", {})
|
||||||
|
if thread_info:
|
||||||
|
thread_count = len(thread_info)
|
||||||
|
else:
|
||||||
|
thread_count = "unknown"
|
||||||
|
|
||||||
return flask.render_template(
|
return flask.render_template(
|
||||||
"index.html",
|
"index.html",
|
||||||
initial_stats=stats,
|
initial_stats=json.dumps(stats, cls=aprsd_json.SimpleJSONEncoder),
|
||||||
aprs_connection=aprs_connection,
|
aprs_connection=aprs_connection,
|
||||||
callsign=CONF.callsign,
|
callsign=CONF.callsign,
|
||||||
version=aprsd.__version__,
|
version=aprsd.__version__,
|
||||||
|
@ -189,10 +122,8 @@ def index():
|
||||||
entries, indent=4,
|
entries, indent=4,
|
||||||
sort_keys=True, default=str,
|
sort_keys=True, default=str,
|
||||||
),
|
),
|
||||||
watch_count=watch_count,
|
|
||||||
watch_age=watch_age,
|
|
||||||
seen_count=seen_count,
|
|
||||||
plugin_count=plugin_count,
|
plugin_count=plugin_count,
|
||||||
|
thread_count=thread_count,
|
||||||
# oslo_out=generate_oslo()
|
# oslo_out=generate_oslo()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -211,19 +142,10 @@ def messages():
|
||||||
@auth.login_required
|
@auth.login_required
|
||||||
@app.route("/packets")
|
@app.route("/packets")
|
||||||
def get_packets():
|
def get_packets():
|
||||||
LOG.debug("/packets called")
|
stats = _stats()
|
||||||
packet_list = aprsd_rpc_client.RPCClient().get_packet_list()
|
stats_dict = stats["stats"]
|
||||||
if packet_list:
|
packets = stats_dict.get("PacketList", {})
|
||||||
tmp_list = []
|
return json.dumps(packets, cls=aprsd_json.SimpleJSONEncoder)
|
||||||
pkts = packet_list.copy()
|
|
||||||
for key in pkts:
|
|
||||||
pkt = packet_list.get(key)
|
|
||||||
if pkt:
|
|
||||||
tmp_list.append(pkt.json)
|
|
||||||
|
|
||||||
return json.dumps(tmp_list)
|
|
||||||
else:
|
|
||||||
return json.dumps([])
|
|
||||||
|
|
||||||
|
|
||||||
@auth.login_required
|
@auth.login_required
|
||||||
|
@ -275,23 +197,34 @@ def save():
|
||||||
return json.dumps({"messages": "saved"})
|
return json.dumps({"messages": "saved"})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/log_entries", methods=["POST"])
|
||||||
|
def log_entries():
|
||||||
|
"""The url that the server can call to update the logs."""
|
||||||
|
entries = request.json
|
||||||
|
LOG.info(f"Log entries called {len(entries)}")
|
||||||
|
for entry in entries:
|
||||||
|
logging_queue.put(entry)
|
||||||
|
return json.dumps({"messages": "saved"})
|
||||||
|
|
||||||
|
|
||||||
class LogUpdateThread(threads.APRSDThread):
|
class LogUpdateThread(threads.APRSDThread):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, logging_queue=None):
|
||||||
super().__init__("LogUpdate")
|
super().__init__("LogUpdate")
|
||||||
|
self.logging_queue = logging_queue
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
if sio:
|
if sio:
|
||||||
log_entries = aprsd_rpc_client.RPCClient().get_log_entries()
|
try:
|
||||||
|
log_entry = self.logging_queue.get(block=True, timeout=1)
|
||||||
if log_entries:
|
if log_entry:
|
||||||
LOG.info(f"Sending log entries! {len(log_entries)}")
|
|
||||||
for entry in log_entries:
|
|
||||||
sio.emit(
|
sio.emit(
|
||||||
"log_entry", entry,
|
"log_entry",
|
||||||
|
log_entry,
|
||||||
namespace="/logs",
|
namespace="/logs",
|
||||||
)
|
)
|
||||||
time.sleep(5)
|
except queue.Empty:
|
||||||
|
pass
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -299,54 +232,21 @@ class LoggingNamespace(socketio.Namespace):
|
||||||
log_thread = None
|
log_thread = None
|
||||||
|
|
||||||
def on_connect(self, sid, environ):
|
def on_connect(self, sid, environ):
|
||||||
global sio
|
global sio, logging_queue
|
||||||
LOG.debug(f"LOG on_connect {sid}")
|
LOG.info(f"LOG on_connect {sid}")
|
||||||
sio.emit(
|
sio.emit(
|
||||||
"connected", {"data": "/logs Connected"},
|
"connected", {"data": "/logs Connected"},
|
||||||
namespace="/logs",
|
namespace="/logs",
|
||||||
)
|
)
|
||||||
self.log_thread = LogUpdateThread()
|
self.log_thread = LogUpdateThread(logging_queue=logging_queue)
|
||||||
self.log_thread.start()
|
self.log_thread.start()
|
||||||
|
|
||||||
def on_disconnect(self, sid):
|
def on_disconnect(self, sid):
|
||||||
LOG.debug(f"LOG Disconnected {sid}")
|
LOG.info(f"LOG Disconnected {sid}")
|
||||||
if self.log_thread:
|
if self.log_thread:
|
||||||
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:
|
||||||
|
@ -367,8 +267,8 @@ if __name__ == "__main__":
|
||||||
async_mode = "threading"
|
async_mode = "threading"
|
||||||
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()
|
||||||
setup_logging(app, log_level)
|
log.setup_logging(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(
|
||||||
|
@ -387,12 +287,12 @@ if __name__ == "uwsgi_file_aprsd_wsgi":
|
||||||
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 = init_app(
|
||||||
log_level="DEBUG",
|
# log_level="DEBUG",
|
||||||
config_file="/config/aprsd.conf",
|
config_file="/config/aprsd.conf",
|
||||||
# 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(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)
|
||||||
|
|
||||||
|
@ -406,10 +306,10 @@ if __name__ == "aprsd.wsgi":
|
||||||
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
|
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
|
||||||
|
|
||||||
log_level = init_app(
|
log_level = init_app(
|
||||||
log_level="DEBUG",
|
# log_level="DEBUG",
|
||||||
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(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)
|
||||||
|
|
|
@ -1,88 +0,0 @@
|
||||||
#
|
|
||||||
# This file is autogenerated by pip-compile with Python 3.10
|
|
||||||
# by the following command:
|
|
||||||
#
|
|
||||||
# pip-compile --annotation-style=line dev-requirements.in
|
|
||||||
#
|
|
||||||
add-trailing-comma==3.1.0 # via gray
|
|
||||||
alabaster==0.7.13 # via sphinx
|
|
||||||
attrs==23.1.0 # via jsonschema, referencing
|
|
||||||
autoflake==1.5.3 # via gray
|
|
||||||
babel==2.13.1 # via sphinx
|
|
||||||
black==23.11.0 # via gray
|
|
||||||
build==1.0.3 # via pip-tools
|
|
||||||
cachetools==5.3.2 # via tox
|
|
||||||
certifi==2023.7.22 # via requests
|
|
||||||
cfgv==3.4.0 # via pre-commit
|
|
||||||
chardet==5.2.0 # via tox
|
|
||||||
charset-normalizer==3.3.2 # via requests
|
|
||||||
click==8.1.7 # via black, pip-tools
|
|
||||||
colorama==0.4.6 # via tox
|
|
||||||
commonmark==0.9.1 # via rich
|
|
||||||
configargparse==1.7 # via gray
|
|
||||||
coverage[toml]==7.3.2 # via coverage, pytest-cov
|
|
||||||
distlib==0.3.7 # via virtualenv
|
|
||||||
docutils==0.20.1 # via sphinx
|
|
||||||
exceptiongroup==1.1.3 # via pytest
|
|
||||||
filelock==3.13.1 # via tox, virtualenv
|
|
||||||
fixit==0.1.4 # via gray
|
|
||||||
flake8==6.1.0 # via -r dev-requirements.in, fixit, pep8-naming
|
|
||||||
gray==0.13.0 # via -r dev-requirements.in
|
|
||||||
identify==2.5.31 # via pre-commit
|
|
||||||
idna==3.4 # via requests
|
|
||||||
imagesize==1.4.1 # via sphinx
|
|
||||||
importlib-resources==6.1.1 # via fixit
|
|
||||||
iniconfig==2.0.0 # via pytest
|
|
||||||
isort==5.12.0 # via -r dev-requirements.in, gray
|
|
||||||
jinja2==3.1.2 # via sphinx
|
|
||||||
jsonschema==4.20.0 # via fixit
|
|
||||||
jsonschema-specifications==2023.11.1 # via jsonschema
|
|
||||||
libcst==1.1.0 # via fixit
|
|
||||||
markupsafe==2.1.3 # via jinja2
|
|
||||||
mccabe==0.7.0 # via flake8
|
|
||||||
mypy==1.7.0 # via -r dev-requirements.in
|
|
||||||
mypy-extensions==1.0.0 # via black, mypy, typing-inspect
|
|
||||||
nodeenv==1.8.0 # via pre-commit
|
|
||||||
packaging==23.2 # via black, build, pyproject-api, pytest, sphinx, tox
|
|
||||||
pathspec==0.11.2 # via black
|
|
||||||
pep8-naming==0.13.3 # via -r dev-requirements.in
|
|
||||||
pip-tools==7.3.0 # via -r dev-requirements.in
|
|
||||||
platformdirs==3.11.0 # via black, tox, virtualenv
|
|
||||||
pluggy==1.3.0 # via pytest, tox
|
|
||||||
pre-commit==3.5.0 # via -r dev-requirements.in
|
|
||||||
pycodestyle==2.11.1 # via flake8
|
|
||||||
pyflakes==3.1.0 # via autoflake, flake8
|
|
||||||
pygments==2.16.1 # via rich, sphinx
|
|
||||||
pyproject-api==1.6.1 # via tox
|
|
||||||
pyproject-hooks==1.0.0 # via build
|
|
||||||
pytest==7.4.3 # via -r dev-requirements.in, pytest-cov
|
|
||||||
pytest-cov==4.1.0 # via -r dev-requirements.in
|
|
||||||
pyupgrade==3.15.0 # via gray
|
|
||||||
pyyaml==6.0.1 # via fixit, libcst, pre-commit
|
|
||||||
referencing==0.31.0 # via jsonschema, jsonschema-specifications
|
|
||||||
requests==2.31.0 # via sphinx
|
|
||||||
rich==12.6.0 # via gray
|
|
||||||
rpds-py==0.13.0 # via jsonschema, referencing
|
|
||||||
snowballstemmer==2.2.0 # via sphinx
|
|
||||||
sphinx==7.2.6 # via -r dev-requirements.in, sphinxcontrib-applehelp, sphinxcontrib-devhelp, sphinxcontrib-htmlhelp, sphinxcontrib-qthelp, sphinxcontrib-serializinghtml
|
|
||||||
sphinxcontrib-applehelp==1.0.7 # via sphinx
|
|
||||||
sphinxcontrib-devhelp==1.0.5 # via sphinx
|
|
||||||
sphinxcontrib-htmlhelp==2.0.4 # via sphinx
|
|
||||||
sphinxcontrib-jsmath==1.0.1 # via sphinx
|
|
||||||
sphinxcontrib-qthelp==1.0.6 # via sphinx
|
|
||||||
sphinxcontrib-serializinghtml==1.1.9 # via sphinx
|
|
||||||
tokenize-rt==5.2.0 # via add-trailing-comma, pyupgrade
|
|
||||||
toml==0.10.2 # via autoflake
|
|
||||||
tomli==2.0.1 # via black, build, coverage, mypy, pip-tools, pyproject-api, pyproject-hooks, pytest, tox
|
|
||||||
tox==4.11.3 # via -r dev-requirements.in
|
|
||||||
typing-extensions==4.8.0 # via black, libcst, mypy, typing-inspect
|
|
||||||
typing-inspect==0.9.0 # via libcst
|
|
||||||
unify==0.5 # via gray
|
|
||||||
untokenize==0.1.1 # via unify
|
|
||||||
urllib3==2.1.0 # via requests
|
|
||||||
virtualenv==20.24.6 # via pre-commit, tox
|
|
||||||
wheel==0.41.3 # via pip-tools
|
|
||||||
|
|
||||||
# The following packages are considered to be unsafe in a requirements file:
|
|
||||||
# pip
|
|
||||||
# setuptools
|
|
|
@ -1,10 +1,18 @@
|
||||||
FROM python:3.11-slim as build
|
FROM python:3.11-slim as build
|
||||||
|
|
||||||
ARG VERSION=3.1.0
|
ARG VERSION=3.4.0
|
||||||
|
# pass this in as 'dev' if you want to install from github repo vs pypi
|
||||||
|
ARG INSTALL_TYPE=pypi
|
||||||
|
|
||||||
|
ARG BRANCH=master
|
||||||
|
ARG BUILDX_QEMU_ENV
|
||||||
|
|
||||||
|
ENV APRSD_BRANCH=${BRANCH:-master}
|
||||||
ENV TZ=${TZ:-US/Eastern}
|
ENV TZ=${TZ:-US/Eastern}
|
||||||
ENV LC_ALL=C.UTF-8
|
ENV LC_ALL=C.UTF-8
|
||||||
ENV LANG=C.UTF-8
|
ENV LANG=C.UTF-8
|
||||||
ENV APRSD_PIP_VERSION=${VERSION}
|
ENV APRSD_PIP_VERSION=${VERSION}
|
||||||
|
ENV PATH="${PATH}:/app/.local/bin"
|
||||||
|
|
||||||
ENV PIP_DEFAULT_TIMEOUT=100 \
|
ENV PIP_DEFAULT_TIMEOUT=100 \
|
||||||
# Allow statements and log messages to immediately appear
|
# Allow statements and log messages to immediately appear
|
||||||
|
@ -34,26 +42,33 @@ RUN set -ex \
|
||||||
FROM build as final
|
FROM build as final
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN pip3 install aprsd==$APRSD_PIP_VERSION
|
RUN pip3 install -U pip
|
||||||
RUN pip install gevent uwsgi
|
|
||||||
RUN which aprsd
|
|
||||||
RUN mkdir /config
|
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 if [ "$INSTALL_TYPE" = "pypi" ]; then \
|
||||||
|
pip3 install aprsd==$APRSD_PIP_VERSION; \
|
||||||
|
elif [ "$INSTALL_TYPE" = "github" ]; then \
|
||||||
|
git clone -b $APRSD_BRANCH https://github.com/craigerl/aprsd; \
|
||||||
|
cd /app/aprsd && pip install -e .; \
|
||||||
|
ls -al /app/.local/lib/python3.11/site-packages/aprsd*; \
|
||||||
|
fi
|
||||||
|
RUN pip install gevent uwsgi
|
||||||
|
RUN echo "PATH=\$PATH:/usr/games:/app/.local/bin" >> /app/.bashrc
|
||||||
RUN which aprsd
|
RUN which aprsd
|
||||||
RUN aprsd sample-config > /config/aprsd.conf
|
RUN aprsd sample-config > /config/aprsd.conf
|
||||||
|
RUN aprsd --version
|
||||||
|
|
||||||
ADD bin/run.sh /app
|
ADD bin/setup.sh /app
|
||||||
ADD bin/listen.sh /app
|
|
||||||
ADD bin/admin.sh /app
|
ADD bin/admin.sh /app
|
||||||
|
|
||||||
# For the web admin interface
|
# For the web admin interface
|
||||||
EXPOSE 8001
|
EXPOSE 8001
|
||||||
|
|
||||||
ENTRYPOINT ["/app/run.sh"]
|
|
||||||
VOLUME ["/config"]
|
VOLUME ["/config"]
|
||||||
|
ENTRYPOINT ["/app/setup.sh"]
|
||||||
|
CMD ["server"]
|
||||||
|
|
||||||
# Set the user to run the application
|
# Set the user to run the application
|
||||||
USER appuser
|
USER appuser
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
FROM python:3.11-slim as build
|
|
||||||
|
|
||||||
ARG BRANCH=master
|
|
||||||
ARG BUILDX_QEMU_ENV
|
|
||||||
ENV APRSD_BRANCH=${BRANCH:-master}
|
|
||||||
|
|
||||||
ENV PIP_DEFAULT_TIMEOUT=100 \
|
|
||||||
# Allow statements and log messages to immediately appear
|
|
||||||
PYTHONUNBUFFERED=1 \
|
|
||||||
# disable a pip version check to reduce run-time & log-spam
|
|
||||||
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
|
||||||
# cache is useless in docker image, so disable to reduce image size
|
|
||||||
PIP_NO_CACHE_DIR=1
|
|
||||||
|
|
||||||
|
|
||||||
RUN set -ex \
|
|
||||||
# Create a non-root user
|
|
||||||
&& addgroup --system --gid 1001 appgroup \
|
|
||||||
&& useradd --uid 1001 --gid 1001 -s /usr/bin/bash -m -d /app appuser \
|
|
||||||
# Upgrade the package index and install security upgrades
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get upgrade -y \
|
|
||||||
&& apt-get install -y git build-essential curl libffi-dev fortune \
|
|
||||||
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo \
|
|
||||||
# Install dependencies
|
|
||||||
# Clean up
|
|
||||||
&& apt-get autoremove -y \
|
|
||||||
&& apt-get clean -y
|
|
||||||
|
|
||||||
|
|
||||||
### Final stage
|
|
||||||
FROM build as final
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
RUN git clone -b $APRSD_BRANCH https://github.com/craigerl/aprsd
|
|
||||||
RUN cd aprsd && pip install --no-cache-dir .
|
|
||||||
RUN pip install gevent uwsgi
|
|
||||||
RUN which aprsd
|
|
||||||
RUN mkdir /config
|
|
||||||
RUN chown -R appuser:appgroup /app
|
|
||||||
RUN chown -R appuser:appgroup /config
|
|
||||||
USER appuser
|
|
||||||
RUN echo "PATH=\$PATH:/usr/games" >> /app/.bashrc
|
|
||||||
RUN which aprsd
|
|
||||||
RUN aprsd sample-config > /config/aprsd.conf
|
|
||||||
|
|
||||||
ADD bin/run.sh /app
|
|
||||||
ADD bin/listen.sh /app
|
|
||||||
ADD bin/admin.sh /app
|
|
||||||
|
|
||||||
EXPOSE 8000
|
|
||||||
|
|
||||||
# CMD ["gunicorn", "aprsd.wsgi:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
||||||
ENTRYPOINT ["/app/run.sh"]
|
|
||||||
VOLUME ["/config"]
|
|
||||||
|
|
||||||
# Set the user to run the application
|
|
||||||
USER appuser
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -x
|
||||||
|
|
||||||
|
# The default command
|
||||||
|
# Override the command in docker-compose.yml to change
|
||||||
|
# what command you want to run in the container
|
||||||
|
COMMAND="server"
|
||||||
|
|
||||||
|
if [ ! -z "${@+x}" ]; then
|
||||||
|
COMMAND=$@
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "${APRSD_PLUGINS}" ]; then
|
||||||
|
OLDIFS=$IFS
|
||||||
|
IFS=','
|
||||||
|
echo "Installing pypi plugins '$APRSD_PLUGINS'";
|
||||||
|
for plugin in ${APRSD_PLUGINS}; do
|
||||||
|
IFS=$OLDIFS
|
||||||
|
# call your procedure/other scripts here below
|
||||||
|
echo "Installing '$plugin'"
|
||||||
|
pip3 install --user $plugin
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "${APRSD_EXTENSIONS}" ]; then
|
||||||
|
OLDIFS=$IFS
|
||||||
|
IFS=','
|
||||||
|
echo "Installing APRSD extensions from pypi '$APRSD_EXTENSIONS'";
|
||||||
|
for extension in ${APRSD_EXTENSIONS}; do
|
||||||
|
IFS=$OLDIFS
|
||||||
|
# call your procedure/other scripts here below
|
||||||
|
echo "Installing '$extension'"
|
||||||
|
pip3 install --user $extension
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${LOG_LEVEL}" ] || [[ ! "${LOG_LEVEL}" =~ ^(CRITICAL|ERROR|WARNING|INFO)$ ]]; then
|
||||||
|
LOG_LEVEL="DEBUG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Log level is set to ${LOG_LEVEL}";
|
||||||
|
|
||||||
|
# check to see if there is a config file
|
||||||
|
APRSD_CONFIG="/config/aprsd.conf"
|
||||||
|
if [ ! -e "$APRSD_CONFIG" ]; then
|
||||||
|
echo "'$APRSD_CONFIG' File does not exist. Creating."
|
||||||
|
aprsd sample-config > $APRSD_CONFIG
|
||||||
|
fi
|
||||||
|
|
||||||
|
aprsd ${COMMAND} --config ${APRSD_CONFIG} --loglevel ${LOG_LEVEL}
|
|
@ -26,7 +26,7 @@ DEV=0
|
||||||
REBUILD_BUILDX=0
|
REBUILD_BUILDX=0
|
||||||
TAG="latest"
|
TAG="latest"
|
||||||
BRANCH=${BRANCH:-master}
|
BRANCH=${BRANCH:-master}
|
||||||
VERSION="3.0.0"
|
VERSION="3.3.4"
|
||||||
|
|
||||||
while getopts “hdart:b:v:” OPTION
|
while getopts “hdart:b:v:” OPTION
|
||||||
do
|
do
|
||||||
|
@ -90,7 +90,8 @@ then
|
||||||
# Use this script to locally build the docker image
|
# Use this script to locally build the docker image
|
||||||
docker buildx build --push --platform $PLATFORMS \
|
docker buildx build --push --platform $PLATFORMS \
|
||||||
-t hemna6969/aprsd:$TAG \
|
-t hemna6969/aprsd:$TAG \
|
||||||
-f Dockerfile-dev --build-arg branch=$BRANCH \
|
--build-arg INSTALL_TYPE=github \
|
||||||
|
--build-arg branch=$BRANCH \
|
||||||
--build-arg BUILDX_QEMU_ENV=true \
|
--build-arg BUILDX_QEMU_ENV=true \
|
||||||
--no-cache .
|
--no-cache .
|
||||||
else
|
else
|
||||||
|
@ -101,6 +102,5 @@ else
|
||||||
--build-arg BUILDX_QEMU_ENV=true \
|
--build-arg BUILDX_QEMU_ENV=true \
|
||||||
-t hemna6969/aprsd:$VERSION \
|
-t hemna6969/aprsd:$VERSION \
|
||||||
-t hemna6969/aprsd:$TAG \
|
-t hemna6969/aprsd:$TAG \
|
||||||
-t hemna6969/aprsd:latest \
|
-t hemna6969/aprsd:latest .
|
||||||
-f Dockerfile .
|
|
||||||
fi
|
fi
|
||||||
|
|
161
pyproject.toml
161
pyproject.toml
|
@ -1,5 +1,162 @@
|
||||||
|
[project]
|
||||||
|
|
||||||
|
# This is the name of your project. The first time you publish this
|
||||||
|
# package, this name will be registered for you. It will determine how
|
||||||
|
# users can install this project, e.g.:
|
||||||
|
#
|
||||||
|
# $ pip install sampleproject
|
||||||
|
#
|
||||||
|
# And where it will live on PyPI: https://pypi.org/project/sampleproject/
|
||||||
|
#
|
||||||
|
# There are some restrictions on what makes a valid project name
|
||||||
|
# specification here:
|
||||||
|
# https://packaging.python.org/specifications/core-metadata/#name
|
||||||
|
name = "aprsd"
|
||||||
|
description = "APRSd is a APRS-IS server that can be used to connect to APRS-IS and send and receive APRS packets."
|
||||||
|
|
||||||
|
# Specify which Python versions you support. In contrast to the
|
||||||
|
# 'Programming Language' classifiers in this file, 'pip install' will check this
|
||||||
|
# and refuse to install the project if the version does not match. See
|
||||||
|
# https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
|
||||||
|
dynamic = ["version", "dependencies", "optional-dependencies"]
|
||||||
|
|
||||||
|
# This is an optional longer description of your project that represents
|
||||||
|
# the body of text which users will see when they visit PyPI.
|
||||||
|
#
|
||||||
|
# Often, this is the same as your README, so you can just read it in from
|
||||||
|
# that file directly.
|
||||||
|
#
|
||||||
|
# This field corresponds to the "Description" metadata field:
|
||||||
|
# https://packaging.python.org/specifications/core-metadata/#description-optional
|
||||||
|
readme = {file = "README.rst", content-type = "text/x-rst"}
|
||||||
|
|
||||||
|
|
||||||
|
# This is either text indicating the license for the distribution, or a file
|
||||||
|
# that contains the license.
|
||||||
|
# https://packaging.python.org/en/latest/specifications/core-metadata/#license
|
||||||
|
license = {file = "LICENSE"}
|
||||||
|
|
||||||
|
# This should be your name or the name of the organization who originally
|
||||||
|
# authored the project, and a valid email address corresponding to the name
|
||||||
|
# listed.
|
||||||
|
authors = [
|
||||||
|
{name = "Craig Lamparter", email = "craig@craiger.org"},
|
||||||
|
{name = "Walter A. Boring IV", email = "waboring@hemna.com"},
|
||||||
|
{name = "Emre Saglam", email = "emresaglam@gmail.com"},
|
||||||
|
{name = "Jason Martin", email= "jhmartin@toger.us"},
|
||||||
|
{name = "John", email="johng42@users.noreply.github.com"},
|
||||||
|
{name = "Martiros Shakhzadyan", email="vrzh@vrzh.net"},
|
||||||
|
{name = "Zoe Moore", email="zoenb@mailbox.org"},
|
||||||
|
{name = "ranguli", email="hello@joshmurphy.ca"},
|
||||||
|
]
|
||||||
|
|
||||||
|
# This should be your name or the names of the organization who currently
|
||||||
|
# maintains the project, and a valid email address corresponding to the name
|
||||||
|
# listed.
|
||||||
|
maintainers = [
|
||||||
|
{name = "Craig Lamparter", email = "craig@craiger.org"},
|
||||||
|
{name = "Walter A. Boring IV", email = "waboring@hemna.com"},
|
||||||
|
]
|
||||||
|
|
||||||
|
# This field adds keywords for your project which will appear on the
|
||||||
|
# project page. What does your project relate to?
|
||||||
|
#
|
||||||
|
# Note that this is a list of additional keywords, separated
|
||||||
|
# by commas, to be used to assist searching for the distribution in a
|
||||||
|
# larger catalog.
|
||||||
|
keywords = [
|
||||||
|
"aprs",
|
||||||
|
"aprs-is",
|
||||||
|
"aprsd",
|
||||||
|
"aprsd-server",
|
||||||
|
"aprsd-client",
|
||||||
|
"aprsd-socket",
|
||||||
|
"aprsd-socket-server",
|
||||||
|
"aprsd-socket-client",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Classifiers help users find your project by categorizing it.
|
||||||
|
#
|
||||||
|
# For a list of valid classifiers, see https://pypi.org/classifiers/
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 5 - Production/Stable",
|
||||||
|
"Environment :: Console",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Intended Audience :: End Users/Desktop",
|
||||||
|
"Intended Audience :: Information Technology",
|
||||||
|
"Topic :: Communications :: Ham Radio",
|
||||||
|
"Topic :: Communications :: Internet Relay Chat",
|
||||||
|
"Topic :: Internet",
|
||||||
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
]
|
||||||
|
|
||||||
|
# This field lists other packages that your project depends on to run.
|
||||||
|
# Any package you put here will be installed by pip when your project is
|
||||||
|
# installed, so they must be valid existing projects.
|
||||||
|
#
|
||||||
|
# For an analysis of this field vs pip's requirements files see:
|
||||||
|
# https://packaging.python.org/discussions/install-requires-vs-requirements/
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
dependencies = {file = ["./requirements.txt"]}
|
||||||
|
optional-dependencies.dev = {file = ["./requirements-dev.txt"]}
|
||||||
|
|
||||||
|
# List additional groups of dependencies here (e.g. development
|
||||||
|
# dependencies). Users will be able to install these using the "extras"
|
||||||
|
# syntax, for example:
|
||||||
|
#
|
||||||
|
# $ pip install sampleproject[dev]
|
||||||
|
#
|
||||||
|
# Optional dependencies the project provides. These are commonly
|
||||||
|
# referred to as "extras". For a more extensive definition see:
|
||||||
|
# https://packaging.python.org/en/latest/specifications/dependency-specifiers/#extras
|
||||||
|
# [project.optional-dependencies]
|
||||||
|
|
||||||
|
# List URLs that are relevant to your project
|
||||||
|
#
|
||||||
|
# This field corresponds to the "Project-URL" and "Home-Page" metadata fields:
|
||||||
|
# https://packaging.python.org/specifications/core-metadata/#project-url-multiple-use
|
||||||
|
# https://packaging.python.org/specifications/core-metadata/#home-page-optional
|
||||||
|
#
|
||||||
|
# Examples listed include a pattern for specifying where the package tracks
|
||||||
|
# issues, where the source is hosted, where to say thanks to the package
|
||||||
|
# maintainers, and where to support the project financially. The key is
|
||||||
|
# what's used to render the link text on PyPI.
|
||||||
|
[project.urls]
|
||||||
|
"Homepage" = "https://github.com/craigerl/aprsd"
|
||||||
|
"Bug Reports" = "https://github.com/craigerl/aprsd/issues"
|
||||||
|
"Source" = "https://github.com/craigerl/aprsd"
|
||||||
|
|
||||||
|
# The following would provide a command line executable called `sample`
|
||||||
|
# which executes the function `main` from this package when invoked.
|
||||||
|
[project.scripts]
|
||||||
|
aprsd = "aprsd.main:main"
|
||||||
|
|
||||||
|
[project.entry-points."oslo.config.opts"]
|
||||||
|
"aprsd.conf" = "aprsd.conf.opts:list_opts"
|
||||||
|
|
||||||
|
[project.entry-points."oslo.config.opts.defaults"]
|
||||||
|
"aprsd.conf" = "aprsd.conf:set_lib_defaults"
|
||||||
|
|
||||||
|
# If you are using a different build backend, you will need to change this.
|
||||||
|
[tool.setuptools]
|
||||||
|
# If there are data files included in your packages that need to be
|
||||||
|
# installed, specify them here.
|
||||||
|
py-modules = ["aprsd"]
|
||||||
|
package-data = {"sample" = ["*.dat"]}
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=46.0", "wheel"]
|
requires = [
|
||||||
|
"setuptools>=69.5.0",
|
||||||
|
"setuptools_scm>=0",
|
||||||
|
"wheel",
|
||||||
|
]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[tool.isort]
|
[tool.isort]
|
||||||
|
@ -14,3 +171,5 @@ skip_gitignore = true
|
||||||
|
|
||||||
[tool.coverage.run]
|
[tool.coverage.run]
|
||||||
branch = true
|
branch = true
|
||||||
|
|
||||||
|
[tool.setuptools_scm]
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
|
build
|
||||||
|
check-manifest
|
||||||
flake8
|
flake8
|
||||||
|
gray
|
||||||
isort
|
isort
|
||||||
mypy
|
mypy
|
||||||
pep8-naming
|
pep8-naming
|
||||||
|
pytest
|
||||||
|
pytest-cov
|
||||||
|
pip
|
||||||
|
pip-tools
|
||||||
|
pre-commit
|
||||||
Sphinx
|
Sphinx
|
||||||
tox
|
tox
|
||||||
|
wheel
|
||||||
|
|
||||||
# Twine is used for uploading packages to pypi
|
# Twine is used for uploading packages to pypi
|
||||||
# but it induces an install of cryptography
|
# but it induces an install of cryptography
|
||||||
# This is sucky for rpi systems.
|
# This is sucky for rpi systems.
|
||||||
# twine
|
# twine
|
||||||
pre-commit
|
|
||||||
pytest
|
|
||||||
pytest-cov
|
|
||||||
gray
|
|
||||||
pip
|
|
||||||
pip-tools
|
|
|
@ -0,0 +1,216 @@
|
||||||
|
#
|
||||||
|
# This file is autogenerated by pip-compile with Python 3.10
|
||||||
|
# by the following command:
|
||||||
|
#
|
||||||
|
# pip-compile --annotation-style=line requirements-dev.in
|
||||||
|
#
|
||||||
|
add-trailing-comma==3.1.0
|
||||||
|
# via gray
|
||||||
|
alabaster==0.7.16
|
||||||
|
# via sphinx
|
||||||
|
autoflake==1.5.3
|
||||||
|
# via gray
|
||||||
|
babel==2.15.0
|
||||||
|
# via sphinx
|
||||||
|
black==24.4.2
|
||||||
|
# via gray
|
||||||
|
build==1.2.1
|
||||||
|
# via
|
||||||
|
# -r requirements-dev.in
|
||||||
|
# check-manifest
|
||||||
|
# pip-tools
|
||||||
|
cachetools==5.3.3
|
||||||
|
# via tox
|
||||||
|
certifi==2024.2.2
|
||||||
|
# via requests
|
||||||
|
cfgv==3.4.0
|
||||||
|
# via pre-commit
|
||||||
|
chardet==5.2.0
|
||||||
|
# via tox
|
||||||
|
charset-normalizer==3.3.2
|
||||||
|
# via requests
|
||||||
|
check-manifest==0.49
|
||||||
|
# via -r requirements-dev.in
|
||||||
|
click==8.1.7
|
||||||
|
# via
|
||||||
|
# black
|
||||||
|
# fixit
|
||||||
|
# moreorless
|
||||||
|
# pip-tools
|
||||||
|
colorama==0.4.6
|
||||||
|
# via tox
|
||||||
|
commonmark==0.9.1
|
||||||
|
# via rich
|
||||||
|
configargparse==1.7
|
||||||
|
# via gray
|
||||||
|
coverage[toml]==7.5.1
|
||||||
|
# via pytest-cov
|
||||||
|
distlib==0.3.8
|
||||||
|
# via virtualenv
|
||||||
|
docutils==0.21.2
|
||||||
|
# via sphinx
|
||||||
|
exceptiongroup==1.2.1
|
||||||
|
# via pytest
|
||||||
|
filelock==3.14.0
|
||||||
|
# via
|
||||||
|
# tox
|
||||||
|
# virtualenv
|
||||||
|
fixit==2.1.0
|
||||||
|
# via gray
|
||||||
|
flake8==7.0.0
|
||||||
|
# via
|
||||||
|
# -r requirements-dev.in
|
||||||
|
# pep8-naming
|
||||||
|
gray==0.15.0
|
||||||
|
# via -r requirements-dev.in
|
||||||
|
identify==2.5.36
|
||||||
|
# via pre-commit
|
||||||
|
idna==3.7
|
||||||
|
# via requests
|
||||||
|
imagesize==1.4.1
|
||||||
|
# via sphinx
|
||||||
|
iniconfig==2.0.0
|
||||||
|
# via pytest
|
||||||
|
isort==5.13.2
|
||||||
|
# via
|
||||||
|
# -r requirements-dev.in
|
||||||
|
# gray
|
||||||
|
jinja2==3.1.4
|
||||||
|
# via sphinx
|
||||||
|
libcst==1.3.1
|
||||||
|
# via fixit
|
||||||
|
markupsafe==2.1.5
|
||||||
|
# via jinja2
|
||||||
|
mccabe==0.7.0
|
||||||
|
# via flake8
|
||||||
|
moreorless==0.4.0
|
||||||
|
# via fixit
|
||||||
|
mypy==1.10.0
|
||||||
|
# via -r requirements-dev.in
|
||||||
|
mypy-extensions==1.0.0
|
||||||
|
# via
|
||||||
|
# black
|
||||||
|
# mypy
|
||||||
|
nodeenv==1.8.0
|
||||||
|
# via pre-commit
|
||||||
|
packaging==24.0
|
||||||
|
# via
|
||||||
|
# black
|
||||||
|
# build
|
||||||
|
# fixit
|
||||||
|
# pyproject-api
|
||||||
|
# pytest
|
||||||
|
# sphinx
|
||||||
|
# tox
|
||||||
|
pathspec==0.12.1
|
||||||
|
# via
|
||||||
|
# black
|
||||||
|
# trailrunner
|
||||||
|
pep8-naming==0.14.1
|
||||||
|
# via -r requirements-dev.in
|
||||||
|
pip-tools==7.4.1
|
||||||
|
# via -r requirements-dev.in
|
||||||
|
platformdirs==4.2.2
|
||||||
|
# via
|
||||||
|
# black
|
||||||
|
# tox
|
||||||
|
# virtualenv
|
||||||
|
pluggy==1.5.0
|
||||||
|
# via
|
||||||
|
# pytest
|
||||||
|
# tox
|
||||||
|
pre-commit==3.7.1
|
||||||
|
# via -r requirements-dev.in
|
||||||
|
pycodestyle==2.11.1
|
||||||
|
# via flake8
|
||||||
|
pyflakes==3.2.0
|
||||||
|
# via
|
||||||
|
# autoflake
|
||||||
|
# flake8
|
||||||
|
pygments==2.18.0
|
||||||
|
# via
|
||||||
|
# rich
|
||||||
|
# sphinx
|
||||||
|
pyproject-api==1.6.1
|
||||||
|
# via tox
|
||||||
|
pyproject-hooks==1.1.0
|
||||||
|
# via
|
||||||
|
# build
|
||||||
|
# pip-tools
|
||||||
|
pytest==8.2.0
|
||||||
|
# via
|
||||||
|
# -r requirements-dev.in
|
||||||
|
# pytest-cov
|
||||||
|
pytest-cov==5.0.0
|
||||||
|
# via -r requirements-dev.in
|
||||||
|
pyupgrade==3.15.2
|
||||||
|
# via gray
|
||||||
|
pyyaml==6.0.1
|
||||||
|
# via
|
||||||
|
# libcst
|
||||||
|
# pre-commit
|
||||||
|
requests==2.32.0
|
||||||
|
# via sphinx
|
||||||
|
rich==12.6.0
|
||||||
|
# via gray
|
||||||
|
snowballstemmer==2.2.0
|
||||||
|
# via sphinx
|
||||||
|
sphinx==7.3.7
|
||||||
|
# via -r requirements-dev.in
|
||||||
|
sphinxcontrib-applehelp==1.0.8
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-devhelp==1.0.6
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-htmlhelp==2.0.5
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-jsmath==1.0.1
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-qthelp==1.0.7
|
||||||
|
# via sphinx
|
||||||
|
sphinxcontrib-serializinghtml==1.1.10
|
||||||
|
# via sphinx
|
||||||
|
tokenize-rt==5.2.0
|
||||||
|
# via
|
||||||
|
# add-trailing-comma
|
||||||
|
# pyupgrade
|
||||||
|
toml==0.10.2
|
||||||
|
# via autoflake
|
||||||
|
tomli==2.0.1
|
||||||
|
# via
|
||||||
|
# black
|
||||||
|
# build
|
||||||
|
# check-manifest
|
||||||
|
# coverage
|
||||||
|
# fixit
|
||||||
|
# mypy
|
||||||
|
# pip-tools
|
||||||
|
# pyproject-api
|
||||||
|
# pytest
|
||||||
|
# sphinx
|
||||||
|
# tox
|
||||||
|
tox==4.15.0
|
||||||
|
# via -r requirements-dev.in
|
||||||
|
trailrunner==1.4.0
|
||||||
|
# via fixit
|
||||||
|
typing-extensions==4.11.0
|
||||||
|
# via
|
||||||
|
# black
|
||||||
|
# mypy
|
||||||
|
unify==0.5
|
||||||
|
# via gray
|
||||||
|
untokenize==0.1.1
|
||||||
|
# via unify
|
||||||
|
urllib3==2.2.1
|
||||||
|
# via requests
|
||||||
|
virtualenv==20.26.2
|
||||||
|
# via
|
||||||
|
# pre-commit
|
||||||
|
# tox
|
||||||
|
wheel==0.43.0
|
||||||
|
# via
|
||||||
|
# -r requirements-dev.in
|
||||||
|
# pip-tools
|
||||||
|
|
||||||
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
|
# pip
|
||||||
|
# setuptools
|
|
@ -1,39 +1,32 @@
|
||||||
aprslib>=0.7.0
|
aprslib>=0.7.0
|
||||||
click
|
|
||||||
click-params
|
|
||||||
click-completion
|
|
||||||
flask
|
|
||||||
werkzeug
|
|
||||||
flask-httpauth
|
|
||||||
imapclient
|
|
||||||
pluggy
|
|
||||||
pbr
|
|
||||||
pyyaml
|
|
||||||
requests
|
|
||||||
pytz
|
|
||||||
six
|
|
||||||
thesmuggler
|
|
||||||
update_checker
|
|
||||||
flask-socketio
|
|
||||||
python-socketio
|
|
||||||
gevent
|
|
||||||
eventlet
|
|
||||||
tabulate
|
|
||||||
# Pinned due to gray needing 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
|
click
|
||||||
# kiss3 uses attrs
|
click-params
|
||||||
kiss3
|
|
||||||
attrs
|
|
||||||
dataclasses
|
dataclasses
|
||||||
dacite2
|
|
||||||
oslo.config
|
|
||||||
rpyc
|
|
||||||
# Pin this here so it doesn't require a compile on
|
|
||||||
# raspi
|
|
||||||
shellingham
|
|
||||||
geopy
|
|
||||||
rush
|
|
||||||
dataclasses-json
|
dataclasses-json
|
||||||
|
eventlet
|
||||||
|
flask
|
||||||
|
flask-httpauth
|
||||||
|
flask-socketio
|
||||||
|
geopy
|
||||||
|
gevent
|
||||||
|
imapclient
|
||||||
|
kiss3
|
||||||
|
loguru
|
||||||
|
oslo.config
|
||||||
|
pluggy
|
||||||
|
python-socketio
|
||||||
|
pyyaml
|
||||||
|
pytz
|
||||||
|
requests
|
||||||
|
# Pinned due to gray needing 12.6.0
|
||||||
|
rich~=12.6.0
|
||||||
|
rush
|
||||||
|
shellingham
|
||||||
|
six
|
||||||
|
tabulate
|
||||||
|
thesmuggler
|
||||||
|
tzlocal
|
||||||
|
update_checker
|
||||||
|
wrapt
|
||||||
|
|
247
requirements.txt
247
requirements.txt
|
@ -4,80 +4,179 @@
|
||||||
#
|
#
|
||||||
# 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
|
||||||
attrs==23.1.0 # via -r requirements.in, ax253, kiss3, rush
|
# via -r requirements.in
|
||||||
ax253==0.1.5.post1 # via kiss3
|
attrs==23.2.0
|
||||||
beautifulsoup4==4.12.2 # via -r requirements.in
|
# via
|
||||||
bidict==0.22.1 # via python-socketio
|
# ax253
|
||||||
bitarray==2.8.3 # via ax253, kiss3
|
# kiss3
|
||||||
blinker==1.7.0 # via flask
|
# rush
|
||||||
certifi==2023.7.22 # via requests
|
ax253==0.1.5.post1
|
||||||
charset-normalizer==3.3.2 # via requests
|
# via kiss3
|
||||||
click==8.1.7 # via -r requirements.in, click-completion, click-params, flask
|
beautifulsoup4==4.12.3
|
||||||
click-completion==0.5.2 # via -r requirements.in
|
# via -r requirements.in
|
||||||
click-params==0.4.1 # via -r requirements.in
|
bidict==0.23.1
|
||||||
commonmark==0.9.1 # via rich
|
# via python-socketio
|
||||||
dacite2==2.0.0 # via -r requirements.in
|
bitarray==2.9.2
|
||||||
dataclasses==0.6 # via -r requirements.in
|
# via
|
||||||
dataclasses-json==0.6.2 # via -r requirements.in
|
# ax253
|
||||||
debtcollector==2.5.0 # via oslo-config
|
# kiss3
|
||||||
decorator==5.1.1 # via validators
|
blinker==1.8.2
|
||||||
dnspython==2.4.2 # via eventlet
|
# via flask
|
||||||
eventlet==0.33.3 # via -r requirements.in
|
certifi==2024.2.2
|
||||||
flask==3.0.0 # via -r requirements.in, flask-httpauth, flask-socketio
|
# via requests
|
||||||
flask-httpauth==4.8.0 # via -r requirements.in
|
charset-normalizer==3.3.2
|
||||||
flask-socketio==5.3.6 # via -r requirements.in
|
# via requests
|
||||||
geographiclib==2.0 # via geopy
|
click==8.1.7
|
||||||
geopy==2.4.0 # via -r requirements.in
|
# via
|
||||||
gevent==23.9.1 # via -r requirements.in
|
# -r requirements.in
|
||||||
greenlet==3.0.1 # via eventlet, gevent
|
# click-params
|
||||||
h11==0.14.0 # via wsproto
|
# flask
|
||||||
idna==3.4 # via requests
|
click-params==0.5.0
|
||||||
imapclient==3.0.0 # via -r requirements.in
|
# via -r requirements.in
|
||||||
importlib-metadata==6.8.0 # via ax253, kiss3
|
commonmark==0.9.1
|
||||||
itsdangerous==2.1.2 # via flask
|
# via rich
|
||||||
jinja2==3.1.2 # via click-completion, flask
|
dataclasses==0.6
|
||||||
kiss3==8.0.0 # via -r requirements.in
|
# via -r requirements.in
|
||||||
markupsafe==2.1.3 # via jinja2, werkzeug
|
dataclasses-json==0.6.6
|
||||||
marshmallow==3.20.1 # via dataclasses-json
|
# via -r requirements.in
|
||||||
mypy-extensions==1.0.0 # via typing-inspect
|
debtcollector==3.0.0
|
||||||
netaddr==0.9.0 # via oslo-config
|
# via oslo-config
|
||||||
oslo-config==9.2.0 # via -r requirements.in
|
deprecated==1.2.14
|
||||||
oslo-i18n==6.2.0 # via oslo-config
|
# via click-params
|
||||||
packaging==23.2 # via marshmallow
|
dnspython==2.6.1
|
||||||
pbr==6.0.0 # via -r requirements.in, oslo-i18n, stevedore
|
# via eventlet
|
||||||
pluggy==1.3.0 # via -r requirements.in
|
eventlet==0.36.1
|
||||||
plumbum==1.8.2 # via rpyc
|
# via -r requirements.in
|
||||||
pygments==2.16.1 # via rich
|
flask==3.0.3
|
||||||
pyserial==3.5 # via pyserial-asyncio
|
# via
|
||||||
pyserial-asyncio==0.6 # via kiss3
|
# -r requirements.in
|
||||||
python-engineio==4.8.0 # via python-socketio
|
# flask-httpauth
|
||||||
python-socketio==5.10.0 # via -r requirements.in, flask-socketio
|
# flask-socketio
|
||||||
pytz==2023.3.post1 # via -r requirements.in
|
flask-httpauth==4.8.0
|
||||||
pyyaml==6.0.1 # via -r requirements.in, oslo-config
|
# via -r requirements.in
|
||||||
requests==2.31.0 # via -r requirements.in, oslo-config, update-checker
|
flask-socketio==5.3.6
|
||||||
rfc3986==2.0.0 # via oslo-config
|
# via -r requirements.in
|
||||||
rich==12.6.0 # via -r requirements.in
|
geographiclib==2.0
|
||||||
rpyc==5.3.1 # via -r requirements.in
|
# via geopy
|
||||||
rush==2021.4.0 # via -r requirements.in
|
geopy==2.4.1
|
||||||
shellingham==1.5.4 # via -r requirements.in, click-completion
|
# via -r requirements.in
|
||||||
simple-websocket==1.0.0 # via python-engineio
|
gevent==24.2.1
|
||||||
six==1.16.0 # via -r requirements.in, click-completion, eventlet
|
# via -r requirements.in
|
||||||
soupsieve==2.5 # via beautifulsoup4
|
greenlet==3.0.3
|
||||||
stevedore==5.1.0 # via oslo-config
|
# via
|
||||||
tabulate==0.9.0 # via -r requirements.in
|
# eventlet
|
||||||
thesmuggler==1.0.1 # via -r requirements.in
|
# gevent
|
||||||
typing-extensions==4.8.0 # via typing-inspect
|
h11==0.14.0
|
||||||
typing-inspect==0.9.0 # via dataclasses-json
|
# via wsproto
|
||||||
update-checker==0.18.0 # via -r requirements.in
|
idna==3.7
|
||||||
urllib3==2.1.0 # via requests
|
# via requests
|
||||||
validators==0.20.0 # via click-params
|
imapclient==3.0.1
|
||||||
werkzeug==3.0.1 # via -r requirements.in, flask
|
# via -r requirements.in
|
||||||
wrapt==1.16.0 # via -r requirements.in, debtcollector
|
importlib-metadata==7.1.0
|
||||||
wsproto==1.2.0 # via simple-websocket
|
# via
|
||||||
zipp==3.17.0 # via importlib-metadata
|
# ax253
|
||||||
zope-event==5.0 # via gevent
|
# kiss3
|
||||||
zope-interface==6.1 # via gevent
|
itsdangerous==2.2.0
|
||||||
|
# via flask
|
||||||
|
jinja2==3.1.4
|
||||||
|
# via flask
|
||||||
|
kiss3==8.0.0
|
||||||
|
# via -r requirements.in
|
||||||
|
loguru==0.7.2
|
||||||
|
# via -r requirements.in
|
||||||
|
markupsafe==2.1.5
|
||||||
|
# via
|
||||||
|
# jinja2
|
||||||
|
# werkzeug
|
||||||
|
marshmallow==3.21.2
|
||||||
|
# via dataclasses-json
|
||||||
|
mypy-extensions==1.0.0
|
||||||
|
# via typing-inspect
|
||||||
|
netaddr==1.2.1
|
||||||
|
# via oslo-config
|
||||||
|
oslo-config==9.4.0
|
||||||
|
# via -r requirements.in
|
||||||
|
oslo-i18n==6.3.0
|
||||||
|
# via oslo-config
|
||||||
|
packaging==24.0
|
||||||
|
# via marshmallow
|
||||||
|
pbr==6.0.0
|
||||||
|
# via
|
||||||
|
# oslo-i18n
|
||||||
|
# stevedore
|
||||||
|
pluggy==1.5.0
|
||||||
|
# via -r requirements.in
|
||||||
|
pygments==2.18.0
|
||||||
|
# via rich
|
||||||
|
pyserial==3.5
|
||||||
|
# via pyserial-asyncio
|
||||||
|
pyserial-asyncio==0.6
|
||||||
|
# via kiss3
|
||||||
|
python-engineio==4.9.0
|
||||||
|
# via python-socketio
|
||||||
|
python-socketio==5.11.2
|
||||||
|
# via
|
||||||
|
# -r requirements.in
|
||||||
|
# flask-socketio
|
||||||
|
pytz==2024.1
|
||||||
|
# via -r requirements.in
|
||||||
|
pyyaml==6.0.1
|
||||||
|
# via
|
||||||
|
# -r requirements.in
|
||||||
|
# oslo-config
|
||||||
|
requests==2.32.0
|
||||||
|
# via
|
||||||
|
# -r requirements.in
|
||||||
|
# oslo-config
|
||||||
|
# update-checker
|
||||||
|
rfc3986==2.0.0
|
||||||
|
# via oslo-config
|
||||||
|
rich==12.6.0
|
||||||
|
# via -r requirements.in
|
||||||
|
rush==2021.4.0
|
||||||
|
# via -r requirements.in
|
||||||
|
shellingham==1.5.4
|
||||||
|
# via -r requirements.in
|
||||||
|
simple-websocket==1.0.0
|
||||||
|
# via python-engineio
|
||||||
|
six==1.16.0
|
||||||
|
# via -r requirements.in
|
||||||
|
soupsieve==2.5
|
||||||
|
# via beautifulsoup4
|
||||||
|
stevedore==5.2.0
|
||||||
|
# via oslo-config
|
||||||
|
tabulate==0.9.0
|
||||||
|
# via -r requirements.in
|
||||||
|
thesmuggler==1.0.1
|
||||||
|
# via -r requirements.in
|
||||||
|
typing-extensions==4.11.0
|
||||||
|
# via typing-inspect
|
||||||
|
typing-inspect==0.9.0
|
||||||
|
# via dataclasses-json
|
||||||
|
tzlocal==5.2
|
||||||
|
# via -r requirements.in
|
||||||
|
update-checker==0.18.0
|
||||||
|
# via -r requirements.in
|
||||||
|
urllib3==2.2.1
|
||||||
|
# via requests
|
||||||
|
validators==0.22.0
|
||||||
|
# via click-params
|
||||||
|
werkzeug==3.0.3
|
||||||
|
# via flask
|
||||||
|
wrapt==1.16.0
|
||||||
|
# via
|
||||||
|
# -r requirements.in
|
||||||
|
# debtcollector
|
||||||
|
# deprecated
|
||||||
|
wsproto==1.2.0
|
||||||
|
# via simple-websocket
|
||||||
|
zipp==3.18.2
|
||||||
|
# via importlib-metadata
|
||||||
|
zope-event==5.0
|
||||||
|
# via gevent
|
||||||
|
zope-interface==6.4
|
||||||
|
# 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
|
||||||
|
|
51
setup.cfg
51
setup.cfg
|
@ -1,51 +0,0 @@
|
||||||
[metadata]
|
|
||||||
name = aprsd
|
|
||||||
long_description = file: README.rst
|
|
||||||
long_description_content_type = text/x-rst
|
|
||||||
url = http://aprsd.readthedocs.org
|
|
||||||
author = Craig Lamparter
|
|
||||||
author_email = something@somewhere.com
|
|
||||||
license = Apache
|
|
||||||
license_file = LICENSE
|
|
||||||
classifier =
|
|
||||||
License :: OSI Approved :: Apache Software License
|
|
||||||
Topic :: Communications :: Ham Radio
|
|
||||||
Operating System :: POSIX :: Linux
|
|
||||||
Programming Language :: Python :: 3 :: Only
|
|
||||||
Programming Language :: Python :: 3
|
|
||||||
Programming Language :: Python :: 3.7
|
|
||||||
Programming Language :: Python :: 3.8
|
|
||||||
Programming Language :: Python :: 3.9
|
|
||||||
description_file =
|
|
||||||
README.rst
|
|
||||||
project_urls =
|
|
||||||
Source=https://github.com/craigerl/aprsd
|
|
||||||
Tracker=https://github.com/craigerl/aprsd/issues
|
|
||||||
summary = Amateur radio APRS daemon which listens for messages and responds
|
|
||||||
|
|
||||||
[global]
|
|
||||||
setup-hooks =
|
|
||||||
pbr.hooks.setup_hook
|
|
||||||
|
|
||||||
[files]
|
|
||||||
packages =
|
|
||||||
aprsd
|
|
||||||
|
|
||||||
[entry_points]
|
|
||||||
console_scripts =
|
|
||||||
aprsd = aprsd.main:main
|
|
||||||
oslo.config.opts =
|
|
||||||
aprsd.conf = aprsd.conf.opts:list_opts
|
|
||||||
oslo.config.opts.defaults =
|
|
||||||
aprsd.conf = aprsd.conf:set_lib_defaults
|
|
||||||
|
|
||||||
[build_sphinx]
|
|
||||||
source-dir = docs
|
|
||||||
build-dir = docs/_build
|
|
||||||
all_files = 1
|
|
||||||
|
|
||||||
[upload_sphinx]
|
|
||||||
upload-dir = docs/_build
|
|
||||||
|
|
||||||
[bdist_wheel]
|
|
||||||
universal = 1
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue