mirror of https://github.com/craigerl/aprsd.git
Compare commits
355 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 | |
Hemna | 12a3113192 | |
Hemna | 026a64c003 | |
Hemna | 682e138ec2 | |
Walter A. Boring IV | e4e9c6e98b | |
Hemna | f02824b796 | |
Martiros Shakhzadyan | 530ac30a09 | |
Craig Lamparter | 9350cf6534 | |
Craig Lamparter | 651cf014b7 | |
Craig Lamparter | b6df9de8aa | |
Walter A. Boring IV | 0fd7daaae0 | |
Hemna | 0433768784 | |
Hemna | a8f73610fe | |
Hemna | c0e2ef1199 | |
Hemna | 809a41f123 | |
Hemna | b0bfdaa1fb | |
Walter A. Boring IV | b73373db3f | |
Hemna | 6b397cbdf1 | |
Hemna | 638128adf8 | |
Hemna | b9dd21bc14 | |
Hemna | fae7032346 | |
Hemna | 4b1214de74 | |
Hemna | 763c9ab897 | |
Hemna | fe1ebf2ec1 | |
Walter A. Boring IV | c01037d398 | |
Walter A. Boring IV | 072a1f4430 | |
Hemna | 8b2613ec47 | |
Jason Martin | d39ce76475 | |
Walter A. Boring IV | 3e9c3612ba | |
Walter A. Boring IV | 8746a9477c | |
dependabot[bot] | 7d0524cee5 | |
Jason Martin | 5828643f2e | |
Walter A. Boring IV | 313ea5b6a5 | |
dependabot[bot] | 7853e19c79 | |
Hemna | acf2b62bce | |
Craig Lamparter | 8e9a0213e9 | |
Hemna | bf905a0e9f | |
Hemna | 5ae45ce42f | |
Hemna | 0155923341 | |
Hemna | 156d9d9592 | |
Hemna | 81169600bd | |
Hemna | 746eeb81b0 | |
Hemna | f41488b48a | |
Walter A. Boring IV | 116f201394 | |
Hemna | ddd4d25e9d | |
Walter A. Boring IV | e2f89a6043 | |
Hemna | 544600a96b | |
dependabot[bot] | c16f3a0bb2 | |
Hemna | 59cec1317d | |
Hemna | 751bbc2514 | |
Hemna | 9bdfd166fd | |
Hemna | f79b88ec1b | |
Hemna | 99a0f877f4 | |
Hemna | 4f87d5da12 | |
Hemna | 0d7e50d2ba | |
Hemna | 1f6c55d2bf | |
Hemna | 740889426a | |
Hemna | c9dc4f67d4 | |
Hemna | 788a72c643 | |
Walter A. Boring IV | 1e3d0d4faf | |
Hemna | 82d25915fc | |
Hemna | 12dfdefb62 | |
Hemna | d63c6854af | |
Hemna | 6b083d4c4d | |
Hemna | ff358987a9 | |
dependabot[bot] | 412ab54303 | |
Hemna | 3f5dbe0a12 | |
Hemna | 9635893934 | |
Hemna | f151ae4348 | |
Hemna | 7130ca2fd9 | |
Hemna | b393060edb | |
Hemna | f770c5ffd5 | |
Hemna | ef206b1283 | |
Hemna | 140fa4ace4 | |
Hemna | 81a19dd101 | |
Walter A. Boring IV | 9985c8bf25 | |
Hemna | 1400e3e711 | |
Hemna | 8a90d5480a | |
Hemna | b4e02c760e | |
Hemna | ba6b410795 | |
Hemna | 70ddc44b5c | |
Hemna | 852760220f | |
Hemna | 14e984c9b4 | |
Hemna | 29f21a9469 | |
Hemna | 7292744a78 | |
Hemna | 619b1b708e | |
Hemna | 008b2ab09e | |
Hemna | 4b56e99689 | |
Hemna | 10bf04929e | |
Hemna | a9e8050ae6 | |
Hemna | 82f77b7a6a | |
Hemna | 570fdb98a7 | |
Hemna | 9582812041 | |
Hemna | 859f904602 | |
Hemna | 34311f0fbd | |
Hemna | 2416f0ea1a | |
Hemna | 377842c2ec | |
Hemna | a8dd9ce012 | |
Hemna | 1d6a667987 | |
Hemna | 2e9a204c74 | |
Hemna | f922b3f97b | |
Hemna | 8dd3b05bb1 | |
Hemna | e06305fceb | |
Hemna | 33c7871dbe | |
Hemna | b2f95b0f4e | |
Hemna | ae9e4d31ad | |
Hemna | 65a5a90458 | |
Hemna | 182887c20a | |
Hemna | f228144f4b | |
Hemna | db9e1d23d1 | |
Hemna | 986df391b2 | |
Walter A. Boring IV | 3994235380 | |
Hemna | 9ebf2f9a30 | |
Hemna | 011cfc55e1 | |
Hemna | e0c3c5cbbf | |
Hemna | 26f354b3a9 | |
Walter A. Boring IV | 922a6dbb35 | |
Hemna | d03c4fc096 | |
Hemna | dfd3688d8f | |
Hemna | c7d629f88a | |
Hemna | 099b87e250 | |
Hemna | 1ab9c3fee4 | |
Walter A. Boring IV | 8891cd3002 | |
Hemna | 4664ead9e7 | |
Hemna | e51a501544 | |
Hemna | 89576a3c43 | |
Hemna | 5383b698ea | |
Hemna | cbef93b327 | |
Hemna | 6ae55fc9a1 | |
Hemna | 588e140a7f | |
Walter A. Boring IV | d251a2727a | |
Hemna | d3a93b735d | |
Hemna | fa452cc773 | |
Hemna | 6a6e854caf | |
Hemna | e1183a7e30 | |
Hemna | 5723e3a77b | |
Hemna | dee73c1060 | |
Hemna | d8318f2ae2 | |
Walter A. Boring IV | 2825cac446 | |
Hemna | fa6e738a20 | |
Hemna | 0c179005ee | |
Hemna | ad004633de | |
Hemna | ccd564a52e | |
Hemna | 35d41582ee | |
Hemna | 565ffe3f72 | |
Hemna | 0bd11d05c6 | |
Walter A. Boring IV | 62eff8645d | |
Hemna | aa547cbef5 | |
Hemna | 7f2aba702a | |
Hemna | 63bf82aab5 | |
Hemna | bba7b68112 | |
Hemna | 005675cb46 | |
Hemna | 191e1ff552 | |
Hemna | 0a14b07fae | |
Hemna | b2e621da4b | |
Hemna | fe0d71de4d | |
Hemna | 9b944142bd | |
Hemna | b172c692a1 | |
Hemna | 311cebaf27 | |
Walter A. Boring IV | f4d60357ee | |
Hemna | 09a0c4cb02 | |
Hemna | 80b85e648f | |
Hemna | 9931c8a6c5 | |
Hemna | 319969cc08 | |
Hemna | da20ff038b | |
Hemna | 15bf3710d2 | |
Hemna | 5bc589f21f | |
Hemna | 8b73372b6e | |
Hemna | 26c1e7afbb | |
Walter A. Boring IV | c99d5b859e | |
Hemna | cad22e1744 | |
Hemna | 43d6b62760 | |
Jason Martin | 96fa4330ba | |
Hemna | 4e99e30f16 | |
Hemna | 00f1c3a2ba | |
Hemna | 0527ddfdba | |
Hemna | 5694cabd93 | |
Hemna | e21e2a7c50 | |
Hemna | 17d9c06b07 | |
Hemna | 66ebb286d8 | |
Hemna | 0ec41f7605 | |
Hemna | c353877321 | |
Hemna | 483afce5ad | |
Hemna | 8a456cac48 | |
Walter A. Boring IV | 62e1d69272 | |
Hemna | 840b0aba97 |
|
@ -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}}"
|
|
@ -0,0 +1,53 @@
|
|||
name: Manual Build docker container
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
logLevel:
|
||||
description: 'Log level'
|
||||
required: true
|
||||
default: 'warning'
|
||||
type: choice
|
||||
options:
|
||||
- info
|
||||
- warning
|
||||
- debug
|
||||
jobs:
|
||||
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Get Branch Name
|
||||
id: branch-name
|
||||
uses: tj-actions/branch-names@v8
|
||||
- name: Extract Branch
|
||||
id: extract_branch
|
||||
run: |
|
||||
echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
|
||||
- name: What is the selected branch?
|
||||
run: |
|
||||
echo "Selected Branch '${{ steps.extract_branch.outputs.branch }}'"
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Setup Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Login to Docker HUB
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build the Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: "{{defaultContext}}:docker"
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: ./Dockerfile
|
||||
build-args: |
|
||||
INSTALL_TYPE=github
|
||||
BRANCH=${{ steps.extract_branch.outputs.branch }}
|
||||
BUILDX_QEMU_ENV=true
|
||||
push: true
|
||||
tags: |
|
||||
hemna6969/aprsd:${{ steps.extract_branch.outputs.branch }}
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10"]
|
||||
python-version: ["3.9", "3.10", "3.11"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
|
@ -27,7 +27,7 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install tox tox-gh-actions
|
||||
pip install tox tox-gh>=1.2
|
||||
- name: Test with tox
|
||||
run: tox
|
||||
|
||||
|
@ -38,7 +38,7 @@ jobs:
|
|||
- uses: actions/checkout@v3
|
||||
- name: Get Branch Name
|
||||
id: branch-name
|
||||
uses: tj-actions/branch-names@v6
|
||||
uses: tj-actions/branch-names@v8
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Setup Docker Buildx
|
||||
|
@ -53,8 +53,9 @@ jobs:
|
|||
with:
|
||||
context: "{{defaultContext}}:docker"
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: ./Dockerfile-dev
|
||||
file: ./Dockerfile
|
||||
build-args: |
|
||||
INSTALL_TYPE=github
|
||||
BRANCH=${{ steps.branch-name.outputs.current_branch }}
|
||||
BUILDX_QEMU_ENV=true
|
||||
push: true
|
||||
|
|
|
@ -7,7 +7,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.8", "3.9", "3.10"]
|
||||
python-version: ["3.9", "3.10", "3.11"]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
|
@ -17,6 +17,6 @@ jobs:
|
|||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install tox tox-gh-actions
|
||||
pip install tox tox-gh>=1.2
|
||||
- name: Test with tox
|
||||
run: tox
|
||||
|
|
|
@ -6,8 +6,7 @@ on:
|
|||
aprsd_version:
|
||||
required: true
|
||||
options:
|
||||
- 2.5.9
|
||||
- 2.6.0
|
||||
- 3.0.0
|
||||
logLevel:
|
||||
description: 'Log level'
|
||||
required: true
|
||||
|
@ -25,7 +24,7 @@ jobs:
|
|||
- uses: actions/checkout@v3
|
||||
- name: Get Branch Name
|
||||
id: branch-name
|
||||
uses: tj-actions/branch-names@v6
|
||||
uses: tj-actions/branch-names@v8
|
||||
- name: Setup QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Setup Docker Buildx
|
||||
|
@ -39,7 +38,7 @@ jobs:
|
|||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: "{{defaultContext}}:docker"
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||
platforms: linux/amd64,linux/arm64
|
||||
file: ./Dockerfile
|
||||
build-args: |
|
||||
VERSION=${{ inputs.aprsd_version }}
|
||||
|
|
|
@ -58,3 +58,5 @@ AUTHORS
|
|||
.idea
|
||||
|
||||
Makefile.venv
|
||||
# Copilot
|
||||
.DS_Store
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.4.0
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
- id: detect-private-key
|
||||
- id: check-merge-conflict
|
||||
- id: check-case-conflict
|
||||
|
@ -13,11 +12,11 @@ repos:
|
|||
- id: check-builtin-literals
|
||||
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v1.16.0
|
||||
rev: v2.5.0
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
|
||||
- repo: https://github.com/dizballanze/gray
|
||||
rev: v0.10.1
|
||||
rev: v0.14.0
|
||||
hooks:
|
||||
- id: gray
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
# .readthedocs.yaml
|
||||
# Read the Docs configuration file
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
# Required
|
||||
version: 2
|
||||
|
||||
# Set the version of Python and other tools you might need
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.11"
|
||||
|
||||
# Build documentation in the docs/ directory with Sphinx
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
|
||||
# We recommend specifying your dependencies to enable reproducible builds:
|
||||
# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
|
||||
python:
|
||||
install:
|
||||
- requirements: dev-requirements.txt
|
372
ChangeLog
372
ChangeLog
|
@ -1,9 +1,381 @@
|
|||
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
|
||||
------
|
||||
|
||||
* Update for v3.2.2 release
|
||||
* Fix for types
|
||||
* Fix wsgi for prod
|
||||
* pep8 fixes
|
||||
* remove python 3.12 from github builds
|
||||
* Fixed datetime access in core.py
|
||||
* removed invalid reference to config.py
|
||||
* Updated requirements
|
||||
* Reworked the admin graphs
|
||||
* Test new packet serialization
|
||||
* Try to localize js libs and css for no internet
|
||||
* Normalize listen --aprs-login
|
||||
* Bump werkzeug from 2.3.7 to 3.0.1
|
||||
* Update INSTALL with new conf files
|
||||
* Bump urllib3 from 2.0.6 to 2.0.7
|
||||
|
||||
v3.2.1
|
||||
------
|
||||
|
||||
* Changelog for 3.2.1
|
||||
* Update index.html disable form autocomplete
|
||||
* Update the packet\_dupe\_timeout warning
|
||||
* Update the webchat paths
|
||||
* Changed the path option to a ListOpt
|
||||
* Fixed default path for tcp\_kiss client
|
||||
* Set a default password for admin
|
||||
* Fix path for KISS clients
|
||||
* Added packet\_dupe\_timeout conf
|
||||
* Add ability to change path on every TX packet
|
||||
* Make Packet objects hashable
|
||||
* Bump urllib3 from 2.0.4 to 2.0.6
|
||||
* Don't process AckPackets as dupes
|
||||
* Fixed another msgNo int issue
|
||||
* Fixed issue with packet tracker and msgNO Counter
|
||||
* Fixed import of Mutablemapping
|
||||
* pep8 fixes
|
||||
* rewrote packet\_list and drop dupe packets
|
||||
* Log a warning on dupe
|
||||
* Fix for dupe packets
|
||||
|
||||
v3.2.0
|
||||
------
|
||||
|
||||
* Update Changelog for 3.2.0
|
||||
* minor cleanup prior to release
|
||||
* Webchat: fix input maxlength
|
||||
* WebChat: cleanup some console.logs
|
||||
* WebChat: flash a dupe message
|
||||
* Webchat: Fix issue accessing msg.id
|
||||
* Webchat: Fix chat css on older browsers
|
||||
* WebChat: new tab should get focus
|
||||
* Bump gevent from 23.9.0.post1 to 23.9.1
|
||||
* Webchat: Fix pep8 errors
|
||||
* Webchat: Added tab notifications and raw packet
|
||||
* WebChat: Prevent sending message without callsign
|
||||
* WebChat: fixed content area scrolling
|
||||
* Webchat: tweaks to UI for expanding chat
|
||||
* Webchat: Fixed bug deleteing first tab
|
||||
* Ensure Keepalive doesn't reset client at startup
|
||||
* Ensure parse\_delta\_str doesn't puke
|
||||
* WebChat: Send GPS Beacon working
|
||||
* webchat: got active tab onclick working
|
||||
* webchat: set to\_call to value of tab when selected
|
||||
* Center the webchat input form
|
||||
* Update index.html to use chat.css
|
||||
* Deleted webchat mobile pages
|
||||
* Added close X on webchat tabs
|
||||
* Reworked webchat with new UI
|
||||
* Updated the webchat UI to look like iMessage
|
||||
* Restore previous conversations in webchat
|
||||
* Remove VIM from Dockerfile
|
||||
* recreate client during reset()
|
||||
* updated github workflows
|
||||
* Updated documentation build
|
||||
* Removed admin\_web.py
|
||||
* Removed some RPC server log noise
|
||||
* Fixed admin page packet date
|
||||
* RPC Server logs the client IP on failed auth
|
||||
* Start keepalive thread first
|
||||
* fixed an issue in the mobile webchat
|
||||
* Added dupe checkig code to webchat mobile
|
||||
* click on the div after added
|
||||
* Webchat suppress to display of dupe messages
|
||||
* Convert webchat internet urls to local static urls
|
||||
* Make use of webchat gps config options
|
||||
* Added new webchat config section
|
||||
* fixed webchat logging.logformat typeoh
|
||||
|
||||
v3.1.3
|
||||
------
|
||||
|
||||
* prep for 3.1.3
|
||||
* Forcefully allow development webchat flask
|
||||
|
||||
v3.1.2
|
||||
------
|
||||
|
||||
* Updated Changelog for 3.1.2
|
||||
* Added support for ThirdParty packet types
|
||||
* Disable the Send GPS Beacon button
|
||||
* Removed adhoc ssl support in webchat
|
||||
|
||||
v3.1.1
|
||||
------
|
||||
|
||||
* Updated Changelog for v3.1.1
|
||||
* Fixed pep8 failures
|
||||
* re-enable USWeatherPlugin to use mapClick
|
||||
* Fix sending packets over KISS interface
|
||||
* Use config web\_ip for running admin ui from module
|
||||
* remove loop log
|
||||
* Max out the client reconnect backoff to 5
|
||||
* Update the Dockerfile
|
||||
|
||||
v3.1.0
|
||||
------
|
||||
|
||||
* Changelog updates for v3.1.0
|
||||
* Use CONF.admin.web\_port for single launch web admin
|
||||
* Fixed sio namespace registration
|
||||
* Update Dockerfile-dev to include uwsgi
|
||||
* Fixed pep8
|
||||
* change port to 8000
|
||||
* replacement of flask-socketio with python-socketio
|
||||
* Change how fetch-stats gets it's defaults
|
||||
* Ensure fetch-stats ip is a string
|
||||
* Add info logging for rpc server calls
|
||||
* updated wsgi config default /config/aprsd.conf
|
||||
* Added timing after each thread loop
|
||||
* Update docker bin/admin.sh
|
||||
* Removed flask-classful from webchat
|
||||
* Remove flask pinning
|
||||
* removed linux/arm/v8
|
||||
* Update master build to include linux/arm/v8
|
||||
* Update Dockerfile-dev to fix plugin permissions
|
||||
* update manual build github
|
||||
* Update requirements for upgraded cryptography
|
||||
* Added more libs for Dockerfile-dev
|
||||
* Replace Dockerfile-dev with python3 slim
|
||||
* Moved logging to log for wsgi.py
|
||||
* Changed weather plugin regex pattern
|
||||
* Limit the float values to 3 decimal places
|
||||
* Fixed rain numbers from aprslib
|
||||
* Fixed rpc client initialization
|
||||
* Fix in for aprslib issue #80
|
||||
* Try and fix Dockerfile-dev
|
||||
* Fixed pep8 errors
|
||||
* Populate stats object with threads info
|
||||
* added counts to the fetch-stats table
|
||||
* Added the fetch-stats command
|
||||
* Replace ratelimiter with rush
|
||||
* Added some utilities to Dockerfile-dev
|
||||
* add arm64 for manual github build
|
||||
* Added manual master build
|
||||
* Update master-build.yml
|
||||
* Add github manual trigger for master build
|
||||
* Fixed unit tests for Location plugin
|
||||
* USe new tox and update githubworkflows
|
||||
* Updated requirements
|
||||
* force tox to 4.3.5
|
||||
* Update github workflows
|
||||
* Fixed pep8 violation
|
||||
* Added rpc server for listen
|
||||
* Update location plugin and reworked requirements
|
||||
* Fixed .readthedocs.yaml format
|
||||
* Add .readthedocs.yaml
|
||||
* Example plugin wrong function
|
||||
* Ensure conf is imported for threads/tx
|
||||
* Update Dockerfile to help build cryptography
|
||||
|
||||
v3.0.3
|
||||
------
|
||||
|
||||
* Update Changelog to 3.0.3
|
||||
* cleanup some debug messages
|
||||
* Fixed loading of plugins for server
|
||||
* Don't load help plugin for listen command
|
||||
* Added listen args
|
||||
* Change listen command plugins
|
||||
* Added listen.sh for docker
|
||||
* Update Listen command
|
||||
* Update Dockerfile
|
||||
* Add ratelimiting for acks and other packets
|
||||
|
||||
v3.0.2
|
||||
------
|
||||
|
||||
* Update Changelog for 3.0.2
|
||||
* Import RejectPacket
|
||||
|
||||
v3.0.1
|
||||
|
|
|
@ -27,9 +27,10 @@ pip install -e .
|
|||
|
||||
# CONFIGURE
|
||||
# Now configure aprsd HERE
|
||||
./aprsd sample-config # generates a config.yml template
|
||||
mkdir -p ~/.config/aprsd
|
||||
./aprsd sample-config > ~/.config/aprsd/aprsd.conf # generates a config template
|
||||
|
||||
vi ~/.config/aprsd/config.yml # copy/edit config here
|
||||
vi ~/.config/aprsd/aprsd.conf # copy/edit config here
|
||||
|
||||
aprsd server
|
||||
|
||||
|
|
17
Makefile
17
Makefile
|
@ -9,15 +9,15 @@ include Makefile.venv
|
|||
Makefile.venv:
|
||||
curl \
|
||||
-o Makefile.fetched \
|
||||
-L "https://github.com/sio/Makefile.venv/raw/v2022.07.20/Makefile.venv"
|
||||
echo "147b164f0cbbbe4a2740dcca6c9adb6e9d8d15b895be3998697aa6a821a277d8 *Makefile.fetched" \
|
||||
-L "https://raw.githubusercontent.com/sio/Makefile.venv/master/Makefile.venv"
|
||||
echo " fb48375ed1fd19e41e0cdcf51a4a0c6d1010dfe03b672ffc4c26a91878544f82 *Makefile.fetched" \
|
||||
| sha256sum --check - \
|
||||
&& mv Makefile.fetched Makefile.venv
|
||||
|
||||
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}'
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
find . -name '*.pyc' -exec rm -f {} +
|
||||
find . -name '*.pyo' -exec rm -f {} +
|
||||
find . -name '*~' -exec rm -f {} +
|
||||
find . -name '__pycache__' -exec rm -fr {} +
|
||||
|
||||
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
|
||||
$(VENV)/pip install twine
|
||||
$(VENV)/python3 setup.py sdist bdist_wheel
|
||||
$(VENV)/python3 -m build
|
||||
$(VENV)/twine check dist/*
|
||||
|
||||
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
|
||||
rm requirements.txt
|
||||
rm dev-requirements.txt
|
||||
rm requirements-dev.txt
|
||||
touch requirements.txt
|
||||
touch dev-requirements.txt
|
||||
$(VENV)/pip-compile --resolver backtracking --annotation-style line requirements.in
|
||||
$(VENV)/pip-compile --resolver backtracking --annotation-style line dev-requirements.in
|
||||
touch requirements-dev.txt
|
||||
$(VENV)/pip-compile --resolver backtracking --annotation-style=line requirements.in
|
||||
$(VENV)/pip-compile --resolver backtracking --annotation-style=line requirements-dev.in
|
||||
|
|
147
README.rst
147
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 listens on amateur radio aprs-is network for messages and respond to them.
|
||||
It has a plugin architecture for extensibility. Users of APRSD can write their own
|
||||
plugins that can respond to APRS-IS messages.
|
||||
|
||||
You must have an amateur radio callsign to use this software. APRSD gets
|
||||
messages for the configured HAM callsign, and sends those messages to a
|
||||
list of plugins for processing. There are a set of core plugins that
|
||||
provide responding to messages to check email, get location, ping,
|
||||
time of day, get weather, and fortune telling as well as version information
|
||||
of aprsd itself.
|
||||
What is APRSD
|
||||
=============
|
||||
APRSD is a python application for interacting with the APRS network and providing
|
||||
APRS services for HAM radio operators.
|
||||
|
||||
APRSD currently has 4 main commands to use.
|
||||
* server - Connect to APRS and listen/respond to APRS messages
|
||||
* webchat - web based chat program over APRS
|
||||
* send-message - Send a message to a callsign via APRS_IS.
|
||||
* listen - Listen to packets on the APRS-IS Network based on FILTER.
|
||||
|
||||
Each of those commands can connect to the APRS-IS network if internet connectivity
|
||||
is available. If internet is not available, then APRS can be configured to talk
|
||||
to a TCP KISS TNC for radio connectivity.
|
||||
|
||||
Please `read the docs`_ to learn more!
|
||||
|
||||
|
||||
.. contents:: :local:
|
||||
|
||||
|
||||
APRSD Overview Diagram
|
||||
======================
|
||||
|
||||
.. image:: https://raw.githubusercontent.com/craigerl/aprsd/master/docs/_static/aprsd_overview.svg?sanitize=true
|
||||
|
||||
|
||||
Typical use case
|
||||
================
|
||||
|
||||
APRSD's typical use case is that of providing an APRS wide service to all HAM
|
||||
radio operators. For example the callsign 'REPEAT' on the APRS network is actually
|
||||
an instance of APRSD that can provide a list of HAM repeaters in the area of the
|
||||
callsign that sent the message.
|
||||
|
||||
|
||||
Ham radio operator using an APRS enabled HAM radio sends a message to check
|
||||
the weather. An APRS message is sent, and then picked up by APRSD. The
|
||||
APRS packet is decoded, and the message is sent through the list of plugins
|
||||
|
@ -46,55 +52,6 @@ callsigns to look out for. The watch list can notify you when a HAM callsign
|
|||
in the list is seen and now available to message on the APRS network.
|
||||
|
||||
|
||||
Current list of built-in plugins
|
||||
======================================
|
||||
|
||||
::
|
||||
|
||||
└─> aprsd list-plugins
|
||||
🐍 APRSD Built-in Plugins 🐍
|
||||
┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Plugin Name ┃ Info ┃ Type ┃ Plugin Path ┃
|
||||
┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ AVWXWeatherPlugin │ AVWX weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.AVWXWeatherPlugin │
|
||||
│ EmailPlugin │ Send and Receive email │ RegexCommand │ aprsd.plugins.email.EmailPlugin │
|
||||
│ FortunePlugin │ Give me a fortune │ RegexCommand │ aprsd.plugins.fortune.FortunePlugin │
|
||||
│ LocationPlugin │ Where in the world is a CALLSIGN's last GPS beacon? │ RegexCommand │ aprsd.plugins.location.LocationPlugin │
|
||||
│ NotifySeenPlugin │ Notify me when a CALLSIGN is recently seen on APRS-IS │ WatchList │ aprsd.plugins.notify.NotifySeenPlugin │
|
||||
│ OWMWeatherPlugin │ OpenWeatherMap weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.OWMWeatherPlugin │
|
||||
│ PingPlugin │ reply with a Pong! │ RegexCommand │ aprsd.plugins.ping.PingPlugin │
|
||||
│ QueryPlugin │ APRSD Owner command to query messages in the MsgTrack │ RegexCommand │ aprsd.plugins.query.QueryPlugin │
|
||||
│ TimeOWMPlugin │ Current time of GPS beacon's timezone. Uses OpenWeatherMap │ RegexCommand │ aprsd.plugins.time.TimeOWMPlugin │
|
||||
│ TimePlugin │ What is the current local time. │ RegexCommand │ aprsd.plugins.time.TimePlugin │
|
||||
│ USMetarPlugin │ USA only METAR of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USMetarPlugin │
|
||||
│ USWeatherPlugin │ Provide USA only weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USWeatherPlugin │
|
||||
│ VersionPlugin │ What is the APRSD Version │ RegexCommand │ aprsd.plugins.version.VersionPlugin │
|
||||
└───────────────────┴────────────────────────────────────────────────────────────┴──────────────┴─────────────────────────────────────────┘
|
||||
|
||||
|
||||
Pypi.org APRSD Installable Plugin Packages
|
||||
|
||||
Install any of the following plugins with 'pip install <Plugin Package Name>'
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
|
||||
┃ Plugin Package Name ┃ Description ┃ Version ┃ Released ┃ Installed? ┃
|
||||
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
|
||||
│ 📂 aprsd-stock-plugin │ Ham Radio APRSD Plugin for fetching stock quotes │ 0.1.3 │ Dec 2, 2022 │ No │
|
||||
│ 📂 aprsd-sentry-plugin │ Ham radio APRSD plugin that does.... │ 0.1.2 │ Dec 2, 2022 │ No │
|
||||
│ 📂 aprsd-timeopencage-plugin │ APRSD plugin for fetching time based on GPS location │ 0.1.0 │ Dec 2, 2022 │ No │
|
||||
│ 📂 aprsd-weewx-plugin │ HAM Radio APRSD that reports weather from a weewx weather station. │ 0.1.4 │ Dec 7, 2021 │ Yes │
|
||||
│ 📂 aprsd-repeat-plugins │ APRSD Plugins for the REPEAT service │ 1.0.12 │ Dec 2, 2022 │ No │
|
||||
│ 📂 aprsd-telegram-plugin │ Ham Radio APRS APRSD plugin for Telegram IM service │ 0.1.3 │ Dec 2, 2022 │ No │
|
||||
│ 📂 aprsd-twitter-plugin │ Python APRSD plugin to send tweets │ 0.3.0 │ Dec 7, 2021 │ No │
|
||||
│ 📂 aprsd-slack-plugin │ Amateur radio APRS daemon which listens for messages and responds │ 1.0.5 │ Dec 18, 2022 │ No │
|
||||
└──────────────────────────────┴────────────────────────────────────────────────────────────────────┴─────────┴──────────────┴────────────┘
|
||||
|
||||
|
||||
🐍 APRSD Installed 3rd party Plugins 🐍
|
||||
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Package Name ┃ Plugin Name ┃ Version ┃ Type ┃ Plugin Path ┃
|
||||
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ aprsd-weewx-plugin │ WeewxMQTTPlugin │ 1.0 │ RegexCommand │ aprsd_weewx_plugin.weewx.WeewxMQTTPlugin │
|
||||
└────────────────────┴─────────────────┴─────────┴──────────────┴──────────────────────────────────────────┘
|
||||
|
||||
Installation
|
||||
=============
|
||||
|
@ -187,6 +144,56 @@ look for incomming commands to the callsign configured in the config file
|
|||
12/07/2021 03:16:17 PM MainThread INFO aprs.logfile = /tmp/aprsd.log server.py:60
|
||||
|
||||
|
||||
Current list of built-in plugins
|
||||
======================================
|
||||
|
||||
::
|
||||
|
||||
└─> aprsd list-plugins
|
||||
🐍 APRSD Built-in Plugins 🐍
|
||||
┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Plugin Name ┃ Info ┃ Type ┃ Plugin Path ┃
|
||||
┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ AVWXWeatherPlugin │ AVWX weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.AVWXWeatherPlugin │
|
||||
│ EmailPlugin │ Send and Receive email │ RegexCommand │ aprsd.plugins.email.EmailPlugin │
|
||||
│ FortunePlugin │ Give me a fortune │ RegexCommand │ aprsd.plugins.fortune.FortunePlugin │
|
||||
│ LocationPlugin │ Where in the world is a CALLSIGN's last GPS beacon? │ RegexCommand │ aprsd.plugins.location.LocationPlugin │
|
||||
│ NotifySeenPlugin │ Notify me when a CALLSIGN is recently seen on APRS-IS │ WatchList │ aprsd.plugins.notify.NotifySeenPlugin │
|
||||
│ OWMWeatherPlugin │ OpenWeatherMap weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.OWMWeatherPlugin │
|
||||
│ PingPlugin │ reply with a Pong! │ RegexCommand │ aprsd.plugins.ping.PingPlugin │
|
||||
│ QueryPlugin │ APRSD Owner command to query messages in the MsgTrack │ RegexCommand │ aprsd.plugins.query.QueryPlugin │
|
||||
│ TimeOWMPlugin │ Current time of GPS beacon's timezone. Uses OpenWeatherMap │ RegexCommand │ aprsd.plugins.time.TimeOWMPlugin │
|
||||
│ TimePlugin │ What is the current local time. │ RegexCommand │ aprsd.plugins.time.TimePlugin │
|
||||
│ USMetarPlugin │ USA only METAR of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USMetarPlugin │
|
||||
│ USWeatherPlugin │ Provide USA only weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USWeatherPlugin │
|
||||
│ VersionPlugin │ What is the APRSD Version │ RegexCommand │ aprsd.plugins.version.VersionPlugin │
|
||||
└───────────────────┴────────────────────────────────────────────────────────────┴──────────────┴─────────────────────────────────────────┘
|
||||
|
||||
|
||||
Pypi.org APRSD Installable Plugin Packages
|
||||
|
||||
Install any of the following plugins with 'pip install <Plugin Package Name>'
|
||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
|
||||
┃ Plugin Package Name ┃ Description ┃ Version ┃ Released ┃ Installed? ┃
|
||||
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
|
||||
│ 📂 aprsd-stock-plugin │ Ham Radio APRSD Plugin for fetching stock quotes │ 0.1.3 │ Dec 2, 2022 │ No │
|
||||
│ 📂 aprsd-sentry-plugin │ Ham radio APRSD plugin that does.... │ 0.1.2 │ Dec 2, 2022 │ No │
|
||||
│ 📂 aprsd-timeopencage-plugin │ APRSD plugin for fetching time based on GPS location │ 0.1.0 │ Dec 2, 2022 │ No │
|
||||
│ 📂 aprsd-weewx-plugin │ HAM Radio APRSD that reports weather from a weewx weather station. │ 0.1.4 │ Dec 7, 2021 │ Yes │
|
||||
│ 📂 aprsd-repeat-plugins │ APRSD Plugins for the REPEAT service │ 1.0.12 │ Dec 2, 2022 │ No │
|
||||
│ 📂 aprsd-telegram-plugin │ Ham Radio APRS APRSD plugin for Telegram IM service │ 0.1.3 │ Dec 2, 2022 │ No │
|
||||
│ 📂 aprsd-twitter-plugin │ Python APRSD plugin to send tweets │ 0.3.0 │ Dec 7, 2021 │ No │
|
||||
│ 📂 aprsd-slack-plugin │ Amateur radio APRS daemon which listens for messages and responds │ 1.0.5 │ Dec 18, 2022 │ No │
|
||||
└──────────────────────────────┴────────────────────────────────────────────────────────────────────┴─────────┴──────────────┴────────────┘
|
||||
|
||||
|
||||
🐍 APRSD Installed 3rd party Plugins 🐍
|
||||
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
┃ Package Name ┃ Plugin Name ┃ Version ┃ Type ┃ Plugin Path ┃
|
||||
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||
│ aprsd-weewx-plugin │ WeewxMQTTPlugin │ 1.0 │ RegexCommand │ aprsd_weewx_plugin.weewx.WeewxMQTTPlugin │
|
||||
└────────────────────┴─────────────────┴─────────┴──────────────┴──────────────────────────────────────────┘
|
||||
|
||||
|
||||
|
||||
send-message
|
||||
|
@ -289,6 +296,20 @@ LOCATION
|
|||
AND... ping, fortune, time.....
|
||||
|
||||
|
||||
Web Admin Interface
|
||||
===================
|
||||
To start the web admin interface, You have to install gunicorn in your virtualenv that already has aprsd installed.
|
||||
|
||||
::
|
||||
|
||||
source <path to APRSD's virtualenv>/bin/activate
|
||||
pip install gunicorn
|
||||
gunicorn --bind 0.0.0.0:8080 "aprsd.wsgi:app"
|
||||
|
||||
The web admin interface will be running on port 8080 on the local machine. http://localhost:8080
|
||||
|
||||
|
||||
|
||||
Development
|
||||
===========
|
||||
|
||||
|
|
|
@ -10,7 +10,10 @@
|
|||
# License for the specific language governing permissions and limitations
|
||||
# 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
|
||||
|
|
|
@ -8,7 +8,7 @@ from oslo_config import cfg
|
|||
|
||||
import aprsd
|
||||
from aprsd import conf # noqa: F401
|
||||
from aprsd.logging import log
|
||||
from aprsd.log import log
|
||||
from aprsd.utils import trace
|
||||
|
||||
|
||||
|
@ -50,6 +50,40 @@ common_options = [
|
|||
]
|
||||
|
||||
|
||||
class AliasedGroup(click.Group):
|
||||
def command(self, *args, **kwargs):
|
||||
"""A shortcut decorator for declaring and attaching a command to
|
||||
the group. This takes the same arguments as :func:`command` but
|
||||
immediately registers the created command with this instance by
|
||||
calling into :meth:`add_command`.
|
||||
Copied from `click` and extended for `aliases`.
|
||||
"""
|
||||
def decorator(f):
|
||||
aliases = kwargs.pop("aliases", [])
|
||||
cmd = click.decorators.command(*args, **kwargs)(f)
|
||||
self.add_command(cmd)
|
||||
for alias in aliases:
|
||||
self.add_command(cmd, name=alias)
|
||||
return cmd
|
||||
return decorator
|
||||
|
||||
def group(self, *args, **kwargs):
|
||||
"""A shortcut decorator for declaring and attaching a group to
|
||||
the group. This takes the same arguments as :func:`group` but
|
||||
immediately registers the created command with this instance by
|
||||
calling into :meth:`add_command`.
|
||||
Copied from `click` and extended for `aliases`.
|
||||
"""
|
||||
def decorator(f):
|
||||
aliases = kwargs.pop("aliases", [])
|
||||
cmd = click.decorators.group(*args, **kwargs)(f)
|
||||
self.add_command(cmd)
|
||||
for alias in aliases:
|
||||
self.add_command(cmd, name=alias)
|
||||
return cmd
|
||||
return decorator
|
||||
|
||||
|
||||
def add_options(options):
|
||||
def _add_options(func):
|
||||
for option in reversed(options):
|
||||
|
@ -104,7 +138,7 @@ def process_standard_options_no_config(f: F) -> F:
|
|||
ctx.obj["loglevel"] = kwargs["loglevel"]
|
||||
ctx.obj["config_file"] = kwargs["config_file"]
|
||||
ctx.obj["quiet"] = kwargs["quiet"]
|
||||
log.setup_logging_no_config(
|
||||
log.setup_logging(
|
||||
ctx.obj["loglevel"],
|
||||
ctx.obj["quiet"],
|
||||
)
|
||||
|
|
285
aprsd/client.py
285
aprsd/client.py
|
@ -1,285 +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, 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"
|
||||
|
||||
# Main must create this from the ClientFactory
|
||||
# object such that it's populated with the
|
||||
# Correct config
|
||||
factory = None
|
||||
|
||||
|
||||
class Client:
|
||||
"""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:
|
||||
self._client = self.setup_connection()
|
||||
if self.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
|
||||
|
||||
@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):
|
||||
|
||||
@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
|
||||
|
||||
@staticmethod
|
||||
def transport():
|
||||
return TRANSPORT_APRSIS
|
||||
|
||||
def decode_packet(self, *args, **kwargs):
|
||||
"""APRS lib already decodes this."""
|
||||
return core.Packet.factory(args[0])
|
||||
|
||||
@trace.trace
|
||||
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 logging 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
|
||||
raise e
|
||||
except Exception as e:
|
||||
LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
|
||||
time.sleep(backoff)
|
||||
backoff = backoff * 2
|
||||
continue
|
||||
LOG.debug(f"Logging in to APRS-IS with user '{user}'")
|
||||
self._client = aprs_client
|
||||
return aprs_client
|
||||
|
||||
|
||||
class KISSClient(Client):
|
||||
|
||||
@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
|
||||
|
||||
@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))
|
||||
return core.Packet.factory(raw)
|
||||
|
||||
@trace.trace
|
||||
def setup_connection(self):
|
||||
client = kiss.KISS3Client()
|
||||
return client
|
||||
|
||||
|
||||
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()
|
||||
|
||||
LOG.debug(f"GET client '{key}'")
|
||||
builder = self._builders.get(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)
|
|
@ -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 select
|
||||
import threading
|
||||
|
@ -11,7 +12,6 @@ from aprslib.exceptions import (
|
|||
import wrapt
|
||||
|
||||
import aprsd
|
||||
from aprsd import stats
|
||||
from aprsd.packets import core
|
||||
|
||||
|
||||
|
@ -24,6 +24,9 @@ class Aprsdis(aprslib.IS):
|
|||
# flag to tell us to stop
|
||||
thread_stop = False
|
||||
|
||||
# date for last time we heard from the server
|
||||
aprsd_keepalive = datetime.datetime.now()
|
||||
|
||||
# timeout in seconds
|
||||
select_timeout = 1
|
||||
lock = threading.Lock()
|
||||
|
@ -37,6 +40,10 @@ class Aprsdis(aprslib.IS):
|
|||
"""Send an APRS Message object."""
|
||||
self.sendall(packet.raw)
|
||||
|
||||
def is_alive(self):
|
||||
"""If the connection is alive or not."""
|
||||
return self._connected
|
||||
|
||||
def _socket_readlines(self, blocking=False):
|
||||
"""
|
||||
Generator for complete lines, received from the server
|
||||
|
@ -102,7 +109,7 @@ class Aprsdis(aprslib.IS):
|
|||
aprsd.__version__,
|
||||
)
|
||||
|
||||
self.logger.info("Sending login information")
|
||||
self.logger.debug("Sending login information")
|
||||
|
||||
try:
|
||||
self._sendall(login_str)
|
||||
|
@ -112,19 +119,18 @@ class Aprsdis(aprslib.IS):
|
|||
test = test.decode("latin-1")
|
||||
test = test.rstrip()
|
||||
|
||||
self.logger.debug("Server: %s", test)
|
||||
self.logger.debug("Server: '%s'", test)
|
||||
|
||||
a, b, callsign, status, e = test.split(" ", 4)
|
||||
if not test:
|
||||
raise LoginError(f"Server Response Empty: '{test}'")
|
||||
|
||||
_, _, callsign, status, e = test.split(" ", 4)
|
||||
s = e.split(",")
|
||||
if len(s):
|
||||
server_string = s[0].replace("server ", "")
|
||||
else:
|
||||
server_string = e.replace("server ", "")
|
||||
|
||||
self.logger.info(f"Connected to {server_string}")
|
||||
self.server_string = server_string
|
||||
stats.APRSDStats().set_aprsis_server(server_string)
|
||||
|
||||
if callsign == "":
|
||||
raise LoginError("Server responded with empty callsign???")
|
||||
if callsign != self.callsign:
|
||||
|
@ -137,6 +143,9 @@ class Aprsdis(aprslib.IS):
|
|||
else:
|
||||
self.logger.info("Login successful")
|
||||
|
||||
self.logger.info(f"Connected to {server_string}")
|
||||
self.server_string = server_string
|
||||
|
||||
except LoginError as e:
|
||||
self.logger.error(str(e))
|
||||
self.close()
|
||||
|
@ -144,6 +153,7 @@ class Aprsdis(aprslib.IS):
|
|||
except Exception as e:
|
||||
self.close()
|
||||
self.logger.error(f"Failed to login '{e}'")
|
||||
self.logger.exception(e)
|
||||
raise LoginError("Failed to login")
|
||||
|
||||
def consumer(self, callback, blocking=True, immortal=False, raw=False):
|
||||
|
@ -168,13 +178,14 @@ class Aprsdis(aprslib.IS):
|
|||
try:
|
||||
for line in self._socket_readlines(blocking):
|
||||
if line[0:1] != b"#":
|
||||
self.aprsd_keepalive = datetime.datetime.now()
|
||||
if raw:
|
||||
callback(line)
|
||||
else:
|
||||
callback(self._parse(line))
|
||||
else:
|
||||
self.logger.debug("Server: %s", line.decode("utf8"))
|
||||
stats.APRSDStats().set_aprsis_keepalive()
|
||||
self.aprsd_keepalive = datetime.datetime.now()
|
||||
except ParseError as exp:
|
||||
self.logger.log(
|
||||
11,
|
|
@ -0,0 +1,73 @@
|
|||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
import aprslib
|
||||
from oslo_config import cfg
|
||||
import wrapt
|
||||
|
||||
from aprsd import conf # noqa
|
||||
from aprsd.packets import core
|
||||
from aprsd.utils import trace
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
|
||||
'''Fake client for testing.'''
|
||||
|
||||
# flag to tell us to stop
|
||||
thread_stop = False
|
||||
|
||||
lock = threading.Lock()
|
||||
path = []
|
||||
|
||||
def __init__(self):
|
||||
LOG.info("Starting APRSDFakeClient client.")
|
||||
self.path = ["WIDE1-1", "WIDE2-1"]
|
||||
|
||||
def stop(self):
|
||||
self.thread_stop = True
|
||||
LOG.info("Shutdown APRSDFakeClient client.")
|
||||
|
||||
def is_alive(self):
|
||||
"""If the connection is alive or not."""
|
||||
return not self.thread_stop
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def send(self, packet: core.Packet):
|
||||
"""Send an APRS Message object."""
|
||||
LOG.info(f"Sending packet: {packet}")
|
||||
payload = None
|
||||
if isinstance(packet, core.Packet):
|
||||
packet.prepare()
|
||||
payload = packet.payload.encode("US-ASCII")
|
||||
if packet.path:
|
||||
packet.path
|
||||
else:
|
||||
self.path
|
||||
else:
|
||||
msg_payload = f"{packet.raw}{{{str(packet.msgNo)}"
|
||||
payload = (
|
||||
":{:<9}:{}".format(
|
||||
packet.to_call,
|
||||
msg_payload,
|
||||
)
|
||||
).encode("US-ASCII")
|
||||
|
||||
LOG.debug(
|
||||
f"FAKE::Send '{payload}' TO '{packet.to_call}' From "
|
||||
f"'{packet.from_call}' with PATH \"{self.path}\"",
|
||||
)
|
||||
|
||||
def consumer(self, callback, blocking=False, immortal=False, raw=False):
|
||||
LOG.debug("Start non blocking FAKE consumer")
|
||||
# Generate packets here?
|
||||
raw = "GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW"
|
||||
pkt_raw = aprslib.parse(raw)
|
||||
pkt = core.factory(pkt_raw)
|
||||
callback(packet=pkt)
|
||||
LOG.debug(f"END blocking FAKE consumer {self}")
|
||||
time.sleep(8)
|
|
@ -14,9 +14,14 @@ LOG = logging.getLogger("APRSD")
|
|||
|
||||
|
||||
class KISS3Client:
|
||||
path = []
|
||||
|
||||
def __init__(self):
|
||||
self.setup()
|
||||
|
||||
def is_alive(self):
|
||||
return True
|
||||
|
||||
def setup(self):
|
||||
# we can be TCP kiss or Serial kiss
|
||||
if CONF.kiss_serial.enabled:
|
||||
|
@ -31,6 +36,7 @@ class KISS3Client:
|
|||
speed=CONF.kiss_serial.baudrate,
|
||||
strip_df_start=True,
|
||||
)
|
||||
self.path = CONF.kiss_serial.path
|
||||
elif CONF.kiss_tcp.enabled:
|
||||
LOG.debug(
|
||||
"KISS({}) TCP Connection to {}:{}".format(
|
||||
|
@ -44,6 +50,7 @@ class KISS3Client:
|
|||
port=CONF.kiss_tcp.port,
|
||||
strip_df_start=True,
|
||||
)
|
||||
self.path = CONF.kiss_tcp.path
|
||||
|
||||
LOG.debug("Starting KISS interface connection")
|
||||
self.kiss.start()
|
||||
|
@ -74,31 +81,24 @@ class KISS3Client:
|
|||
LOG.error("Failed to parse bytes received from KISS interface.")
|
||||
LOG.exception(ex)
|
||||
|
||||
def consumer(self, callback, blocking=False, immortal=False, raw=False):
|
||||
def consumer(self, callback):
|
||||
LOG.debug("Start blocking KISS consumer")
|
||||
self._parse_callback = callback
|
||||
self.kiss.read(callback=self.parse_frame, min_frames=None)
|
||||
LOG.debug("END blocking KISS consumer")
|
||||
LOG.debug(f"END blocking KISS consumer {self.kiss}")
|
||||
|
||||
def send(self, packet):
|
||||
"""Send an APRS Message object."""
|
||||
|
||||
# payload = (':%-9s:%s' % (
|
||||
# msg.tocall,
|
||||
# payload
|
||||
# )).encode('US-ASCII'),
|
||||
# payload = str(msg).encode('US-ASCII')
|
||||
payload = None
|
||||
path = ["WIDE1-1", "WIDE2-1"]
|
||||
if isinstance(packet, core.AckPacket):
|
||||
msg_payload = f"ack{packet.msgNo}"
|
||||
elif isinstance(packet, core.Packet):
|
||||
payload = packet.raw.encode("US-ASCII")
|
||||
path = ["WIDE2-1"]
|
||||
path = self.path
|
||||
if isinstance(packet, core.Packet):
|
||||
packet.prepare()
|
||||
payload = packet.payload.encode("US-ASCII")
|
||||
if packet.path:
|
||||
path = packet.path
|
||||
else:
|
||||
msg_payload = f"{packet.raw}{{{str(packet.msgNo)}"
|
||||
|
||||
if not payload:
|
||||
payload = (
|
||||
":{:<9}:{}".format(
|
||||
packet.to_call,
|
||||
|
@ -106,9 +106,12 @@ class KISS3Client:
|
|||
)
|
||||
).encode("US-ASCII")
|
||||
|
||||
LOG.debug(f"Send '{payload}' TO KISS")
|
||||
LOG.debug(
|
||||
f"KISS Send '{payload}' TO '{packet.to_call}' From "
|
||||
f"'{packet.from_call}' with PATH '{path}'",
|
||||
)
|
||||
frame = Frame.ui(
|
||||
destination=packet.to_call,
|
||||
destination="APZ100",
|
||||
source=packet.from_call,
|
||||
path=path,
|
||||
info=payload,
|
|
@ -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,36 +1,22 @@
|
|||
import click
|
||||
import click_completion
|
||||
import click.shell_completion
|
||||
|
||||
from aprsd.aprsd import cli
|
||||
from aprsd.main import cli
|
||||
|
||||
|
||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
|
||||
|
||||
@cli.group(help="Click Completion subcommands", context_settings=CONTEXT_SETTINGS)
|
||||
@click.pass_context
|
||||
def completion(ctx):
|
||||
pass
|
||||
@cli.command()
|
||||
@click.argument("shell", type=click.Choice(list(click.shell_completion._available_shells)))
|
||||
def completion(shell):
|
||||
"""Show the shell completion code"""
|
||||
from click.utils import _detect_program_name
|
||||
|
||||
|
||||
# show dumps out the completion code for a particular shell
|
||||
@completion.command(help="Show completion code for shell", name="show")
|
||||
@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))
|
||||
def show(shell, case_insensitive):
|
||||
"""Show the click-completion-command completion code"""
|
||||
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}")
|
||||
cls = click.shell_completion.get_completion_class(shell)
|
||||
prog_name = _detect_program_name()
|
||||
complete_var = f"_{prog_name}_COMPLETE".replace("-", "_").upper()
|
||||
print(cls(cli, {}, prog_name, complete_var).source())
|
||||
print("# Add the following line to your shell configuration file to have aprsd command line completion")
|
||||
print("# but remove the leading '#' character.")
|
||||
print(f"# eval \"$(aprsd completion {shell})\"")
|
||||
|
|
|
@ -8,9 +8,10 @@ import logging
|
|||
import click
|
||||
from oslo_config import cfg
|
||||
|
||||
from aprsd import cli_helper, conf, packets, plugin
|
||||
# local imports here
|
||||
from aprsd import cli_helper, client, conf, packets, plugin
|
||||
from aprsd.aprsd import cli
|
||||
from aprsd.client import base
|
||||
from aprsd.main import cli
|
||||
from aprsd.utils import trace
|
||||
|
||||
|
||||
|
@ -96,13 +97,11 @@ def test_plugin(
|
|||
if CONF.trace_enabled:
|
||||
trace.setup_tracing(["method", "api"])
|
||||
|
||||
client.Client()
|
||||
base.APRSClient()
|
||||
|
||||
pm = plugin.PluginManager()
|
||||
if load_all:
|
||||
pm.setup_plugins()
|
||||
else:
|
||||
pm._init()
|
||||
obj = pm._create_class(plugin_path, plugin.APRSDPluginBase)
|
||||
if not obj:
|
||||
click.echo(ctx.get_help())
|
||||
|
@ -116,7 +115,7 @@ def test_plugin(
|
|||
obj.__class__, obj.version,
|
||||
),
|
||||
)
|
||||
pm._pluggy_pm.register(obj)
|
||||
pm.register_msg(obj)
|
||||
|
||||
packet = packets.MessagePacket(
|
||||
from_call=fromcall,
|
||||
|
@ -127,8 +126,37 @@ def test_plugin(
|
|||
LOG.info(f"P'{plugin_path}' F'{fromcall}' C'{message}'")
|
||||
|
||||
for x in range(number):
|
||||
reply = pm.run(packet)
|
||||
replies = pm.run(packet)
|
||||
# Plugin might have threads, so lets stop them so we can exit.
|
||||
# obj.stop_threads()
|
||||
LOG.info(f"Result{x} = '{reply}'")
|
||||
for reply in replies:
|
||||
if isinstance(reply, list):
|
||||
# one of the plugins wants to send multiple messages
|
||||
for subreply in reply:
|
||||
if isinstance(subreply, packets.Packet):
|
||||
LOG.info(subreply)
|
||||
else:
|
||||
LOG.info(
|
||||
packets.MessagePacket(
|
||||
from_call=CONF.callsign,
|
||||
to_call=fromcall,
|
||||
message_text=subreply,
|
||||
),
|
||||
)
|
||||
elif isinstance(reply, packets.Packet):
|
||||
# We have a message based object.
|
||||
LOG.info(reply)
|
||||
else:
|
||||
# A plugin can return a null message flag which signals
|
||||
# us that they processed the message correctly, but have
|
||||
# nothing to reply with, so we avoid replying with a
|
||||
# usage string
|
||||
if reply is not packets.NULL_MESSAGE:
|
||||
LOG.info(
|
||||
packets.MessagePacket(
|
||||
from_call=CONF.callsign,
|
||||
to_call=fromcall,
|
||||
message_text=reply,
|
||||
),
|
||||
)
|
||||
pm.stop()
|
||||
|
|
|
@ -0,0 +1,156 @@
|
|||
# Fetch active stats from a remote running instance of aprsd admin web interface.
|
||||
import logging
|
||||
|
||||
import click
|
||||
from oslo_config import cfg
|
||||
import requests
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
# local imports here
|
||||
import aprsd
|
||||
from aprsd import cli_helper
|
||||
from aprsd.main import cli
|
||||
|
||||
|
||||
# setup the global logger
|
||||
# log.basicConfig(level=log.DEBUG) # level=10
|
||||
LOG = logging.getLogger("APRSD")
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
@cli.command()
|
||||
@cli_helper.add_options(cli_helper.common_options)
|
||||
@click.option(
|
||||
"--host", type=str,
|
||||
default=None,
|
||||
help="IP address of the remote aprsd admin web ui fetch stats from.",
|
||||
)
|
||||
@click.option(
|
||||
"--port", type=int,
|
||||
default=None,
|
||||
help="Port of the remote aprsd web admin interface to fetch stats from.",
|
||||
)
|
||||
@click.pass_context
|
||||
@cli_helper.process_standard_options
|
||||
def fetch_stats(ctx, host, port):
|
||||
"""Fetch stats from a APRSD admin web interface."""
|
||||
console = Console()
|
||||
console.print(f"APRSD Fetch-Stats started version: {aprsd.__version__}")
|
||||
|
||||
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||
if not host:
|
||||
host = CONF.admin.web_ip
|
||||
if not port:
|
||||
port = CONF.admin.web_port
|
||||
|
||||
msg = f"Fetching stats from {host}:{port}"
|
||||
console.print(msg)
|
||||
with console.status(msg):
|
||||
response = requests.get(f"http://{host}:{port}/stats", timeout=120)
|
||||
if not response:
|
||||
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 "
|
||||
f"[bold cyan]v{stats['APRSDStats']['version']}[/] "
|
||||
f"Callsign [bold green]{stats['APRSDStats']['callsign']}[/] "
|
||||
f"Uptime [bold yellow]{stats['APRSDStats']['uptime']}[/]"
|
||||
)
|
||||
|
||||
console.rule(f"Stats from {host}:{port}")
|
||||
console.print("\n\n")
|
||||
console.rule(aprsd_title)
|
||||
|
||||
# Show the connection to APRS
|
||||
# It can be a connection to an APRS-IS server or a local TNC via KISS or KISSTCP
|
||||
if "aprs-is" in stats:
|
||||
title = f"APRS-IS Connection {stats['APRSClientStats']['server_string']}"
|
||||
table = Table(title=title)
|
||||
table.add_column("Key")
|
||||
table.add_column("Value")
|
||||
for key, value in stats["APRSClientStats"].items():
|
||||
table.add_row(key, value)
|
||||
console.print(table)
|
||||
|
||||
threads_table = Table(title="Threads")
|
||||
threads_table.add_column("Name")
|
||||
threads_table.add_column("Alive?")
|
||||
for name, alive in stats["APRSDThreadList"].items():
|
||||
threads_table.add_row(name, str(alive))
|
||||
|
||||
console.print(threads_table)
|
||||
|
||||
packet_totals = Table(title="Packet Totals")
|
||||
packet_totals.add_column("Key")
|
||||
packet_totals.add_column("Value")
|
||||
packet_totals.add_row("Total Received", str(stats["PacketList"]["rx"]))
|
||||
packet_totals.add_row("Total Sent", str(stats["PacketList"]["tx"]))
|
||||
console.print(packet_totals)
|
||||
|
||||
# Show each of the packet types
|
||||
packets_table = Table(title="Packets By Type")
|
||||
packets_table.add_column("Packet Type")
|
||||
packets_table.add_column("TX")
|
||||
packets_table.add_column("RX")
|
||||
for key, value in stats["PacketList"]["packets"].items():
|
||||
packets_table.add_row(key, str(value["tx"]), str(value["rx"]))
|
||||
|
||||
console.print(packets_table)
|
||||
|
||||
if "plugins" in stats:
|
||||
count = len(stats["PluginManager"])
|
||||
plugins_table = Table(title=f"Plugins ({count})")
|
||||
plugins_table.add_column("Plugin")
|
||||
plugins_table.add_column("Enabled")
|
||||
plugins_table.add_column("Version")
|
||||
plugins_table.add_column("TX")
|
||||
plugins_table.add_column("RX")
|
||||
plugins = stats["PluginManager"]
|
||||
for key, value in plugins.items():
|
||||
plugins_table.add_row(
|
||||
key,
|
||||
str(plugins[key]["enabled"]),
|
||||
plugins[key]["version"],
|
||||
str(plugins[key]["tx"]),
|
||||
str(plugins[key]["rx"]),
|
||||
)
|
||||
|
||||
console.print(plugins_table)
|
||||
|
||||
seen_list = stats.get("SeenList")
|
||||
|
||||
if seen_list:
|
||||
count = len(seen_list)
|
||||
seen_table = Table(title=f"Seen List ({count})")
|
||||
seen_table.add_column("Callsign")
|
||||
seen_table.add_column("Message Count")
|
||||
seen_table.add_column("Last Heard")
|
||||
for key, value in seen_list.items():
|
||||
seen_table.add_row(key, str(value["count"]), value["last"])
|
||||
|
||||
console.print(seen_table)
|
||||
|
||||
watch_list = stats.get("WatchList")
|
||||
|
||||
if watch_list:
|
||||
count = len(watch_list)
|
||||
watch_table = Table(title=f"Watch List ({count})")
|
||||
watch_table.add_column("Callsign")
|
||||
watch_table.add_column("Last Heard")
|
||||
for key, value in watch_list.items():
|
||||
watch_table.add_row(key, value["last"])
|
||||
|
||||
console.print(watch_table)
|
|
@ -13,15 +13,15 @@ from oslo_config import cfg
|
|||
from rich.console import Console
|
||||
|
||||
import aprsd
|
||||
from aprsd import cli_helper, utils
|
||||
from aprsd import cli_helper
|
||||
from aprsd import conf # noqa
|
||||
# local imports here
|
||||
from aprsd.aprsd import cli
|
||||
from aprsd.rpc import client as aprsd_rpc_client
|
||||
from aprsd.main import cli
|
||||
from aprsd.threads import stats as stats_threads
|
||||
|
||||
|
||||
# setup the global logger
|
||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||
# log.basicConfig(level=log.DEBUG) # level=10
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger("APRSD")
|
||||
console = Console()
|
||||
|
@ -39,46 +39,48 @@ console = Console()
|
|||
@cli_helper.process_standard_options
|
||||
def healthcheck(ctx, timeout):
|
||||
"""Check the health of the running aprsd server."""
|
||||
console.log(f"APRSD HealthCheck version: {aprsd.__version__}")
|
||||
if not CONF.rpc_settings.enabled:
|
||||
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)
|
||||
ver_str = f"APRSD HealthCheck version: {aprsd.__version__}"
|
||||
console.log(ver_str)
|
||||
|
||||
with console.status(f"APRSD HealthCheck version: {aprsd.__version__}") as status:
|
||||
with console.status(ver_str):
|
||||
try:
|
||||
status.update(f"Contacting APRSD via RPC {CONF.rpc_settings.ip}")
|
||||
stats = aprsd_rpc_client.RPCClient().get_stats_dict()
|
||||
stats_obj = stats_threads.StatsStore()
|
||||
stats_obj.load()
|
||||
stats = stats_obj.data
|
||||
# console.print(stats)
|
||||
except Exception as ex:
|
||||
console.log(f"Failed to fetch healthcheck : '{ex}'")
|
||||
console.log(f"Failed to load stats: '{ex}'")
|
||||
sys.exit(-1)
|
||||
else:
|
||||
now = datetime.datetime.now()
|
||||
if not stats:
|
||||
console.log("No stats from aprsd")
|
||||
sys.exit(-1)
|
||||
email_thread_last_update = stats["email"]["thread_last_update"]
|
||||
|
||||
if email_thread_last_update != "never":
|
||||
delta = utils.parse_delta_str(email_thread_last_update)
|
||||
d = datetime.timedelta(**delta)
|
||||
email_stats = stats.get("EmailStats")
|
||||
if email_stats:
|
||||
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_delta = datetime.timedelta(**max_timeout)
|
||||
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)
|
||||
|
||||
aprsis_last_update = stats["aprs-is"]["last_update"]
|
||||
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)
|
||||
|
||||
console.log("OK")
|
||||
sys.exit(0)
|
||||
|
|
|
@ -19,13 +19,14 @@ from thesmuggler import smuggle
|
|||
|
||||
from aprsd import cli_helper
|
||||
from aprsd import plugin as aprsd_plugin
|
||||
from aprsd.aprsd import cli
|
||||
from aprsd.main import cli
|
||||
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")
|
||||
PYPI_URL = "https://pypi.org/search/"
|
||||
|
||||
|
||||
def onerror(name):
|
||||
|
@ -89,22 +90,39 @@ def get_module_info(package_name, module_name, module_path):
|
|||
return obj_list
|
||||
|
||||
|
||||
def get_installed_plugins():
|
||||
def _get_installed_aprsd_items():
|
||||
# installed plugins
|
||||
ip = {}
|
||||
plugins = {}
|
||||
extensions = {}
|
||||
for finder, name, ispkg in pkgutil.iter_modules():
|
||||
if name.startswith("aprsd_"):
|
||||
print(f"Found aprsd_ module: {name}")
|
||||
if ispkg:
|
||||
module = importlib.import_module(name)
|
||||
pkgs = walk_package(module)
|
||||
for pkg in pkgs:
|
||||
pkg_info = get_module_info(module.__name__, pkg.name, module.__path__[0])
|
||||
ip[name] = pkg_info
|
||||
return ip
|
||||
if "plugin" in name:
|
||||
plugins[name] = pkg_info
|
||||
elif "extension" in name:
|
||||
extensions[name] = pkg_info
|
||||
return plugins, extensions
|
||||
|
||||
|
||||
def get_installed_plugins():
|
||||
# installed plugins
|
||||
plugins, extensions = _get_installed_aprsd_items()
|
||||
return plugins
|
||||
|
||||
|
||||
def get_installed_extensions():
|
||||
# installed plugins
|
||||
plugins, extensions = _get_installed_aprsd_items()
|
||||
return extensions
|
||||
|
||||
|
||||
def show_built_in_plugins(console):
|
||||
modules = [email, fortune, location, notify, ping, query, time, version, weather]
|
||||
modules = [email, fortune, location, notify, ping, time, version, weather]
|
||||
plugins = []
|
||||
|
||||
for module in modules:
|
||||
|
@ -144,22 +162,27 @@ def show_built_in_plugins(console):
|
|||
console.print(table)
|
||||
|
||||
|
||||
def show_pypi_plugins(installed_plugins, console):
|
||||
def _get_pypi_packages():
|
||||
query = "aprsd"
|
||||
api_url = "https://pypi.org/search/"
|
||||
snippets = []
|
||||
s = requests.Session()
|
||||
for page in range(1, 3):
|
||||
params = {"q": query, "page": page}
|
||||
r = s.get(api_url, params=params)
|
||||
r = s.get(PYPI_URL, params=params)
|
||||
soup = BeautifulSoup(r.text, "html.parser")
|
||||
snippets += soup.select('a[class*="snippet"]')
|
||||
if not hasattr(s, "start_url"):
|
||||
s.start_url = r.url.rsplit("&page", maxsplit=1).pop(0)
|
||||
|
||||
return snippets
|
||||
|
||||
|
||||
def show_pypi_plugins(installed_plugins, console):
|
||||
snippets = _get_pypi_packages()
|
||||
|
||||
title = Text.assemble(
|
||||
("Pypi.org APRSD Installable Plugin Packages\n\n", "bold magenta"),
|
||||
("Install any of the following plugins with ", "bold yellow"),
|
||||
("Install any of the following plugins with\n", "bold yellow"),
|
||||
("'pip install ", "bold white"),
|
||||
("<Plugin Package Name>'", "cyan"),
|
||||
)
|
||||
|
@ -171,7 +194,7 @@ def show_pypi_plugins(installed_plugins, console):
|
|||
table.add_column("Released", style="bold green", justify="center")
|
||||
table.add_column("Installed?", style="red", justify="center")
|
||||
for snippet in snippets:
|
||||
link = urljoin(api_url, snippet.get("href"))
|
||||
link = urljoin(PYPI_URL, snippet.get("href"))
|
||||
package = re.sub(r"\s+", " ", snippet.select_one('span[class*="name"]').text.strip())
|
||||
version = re.sub(r"\s+", " ", snippet.select_one('span[class*="version"]').text.strip())
|
||||
created = re.sub(r"\s+", " ", snippet.select_one('span[class*="created"]').text.strip())
|
||||
|
@ -194,7 +217,47 @@ def show_pypi_plugins(installed_plugins, console):
|
|||
|
||||
console.print("\n")
|
||||
console.print(table)
|
||||
return
|
||||
|
||||
|
||||
def show_pypi_extensions(installed_extensions, console):
|
||||
snippets = _get_pypi_packages()
|
||||
|
||||
title = Text.assemble(
|
||||
("Pypi.org APRSD Installable Extension Packages\n\n", "bold magenta"),
|
||||
("Install any of the following extensions by running\n", "bold yellow"),
|
||||
("'pip install ", "bold white"),
|
||||
("<Plugin Package Name>'", "cyan"),
|
||||
)
|
||||
table = Table(title=title)
|
||||
table.add_column("Extension Package Name", style="cyan", no_wrap=True)
|
||||
table.add_column("Description", style="yellow")
|
||||
table.add_column("Version", style="yellow", justify="center")
|
||||
table.add_column("Released", style="bold green", justify="center")
|
||||
table.add_column("Installed?", style="red", justify="center")
|
||||
for snippet in snippets:
|
||||
link = urljoin(PYPI_URL, snippet.get("href"))
|
||||
package = re.sub(r"\s+", " ", snippet.select_one('span[class*="name"]').text.strip())
|
||||
version = re.sub(r"\s+", " ", snippet.select_one('span[class*="version"]').text.strip())
|
||||
created = re.sub(r"\s+", " ", snippet.select_one('span[class*="created"]').text.strip())
|
||||
description = re.sub(r"\s+", " ", snippet.select_one('p[class*="description"]').text.strip())
|
||||
emoji = ":open_file_folder:"
|
||||
|
||||
if "aprsd-" not in package or "-extension" not in package:
|
||||
continue
|
||||
|
||||
under = package.replace("-", "_")
|
||||
if under in installed_extensions:
|
||||
installed = "Yes"
|
||||
else:
|
||||
installed = "No"
|
||||
|
||||
table.add_row(
|
||||
f"[link={link}]{emoji}[/link] {package}",
|
||||
description, version, created, installed,
|
||||
)
|
||||
|
||||
console.print("\n")
|
||||
console.print(table)
|
||||
|
||||
|
||||
def show_installed_plugins(installed_plugins, console):
|
||||
|
@ -240,3 +303,17 @@ def list_plugins(ctx):
|
|||
|
||||
status.update("Looking for installed APRSD plugins")
|
||||
show_installed_plugins(installed_plugins, console)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@cli_helper.add_options(cli_helper.common_options)
|
||||
@click.pass_context
|
||||
@cli_helper.process_standard_options_no_config
|
||||
def list_extensions(ctx):
|
||||
"""List the built in plugins available to APRSD."""
|
||||
console = Console()
|
||||
|
||||
with console.status("Show APRSD Extensions") as status:
|
||||
status.update("Fetching pypi.org APRSD Extensions")
|
||||
installed_extensions = get_installed_extensions()
|
||||
show_pypi_extensions(installed_extensions, console)
|
||||
|
|
|
@ -15,13 +15,19 @@ from rich.console import Console
|
|||
|
||||
# local imports here
|
||||
import aprsd
|
||||
from aprsd import cli_helper, client, packets, stats, threads
|
||||
from aprsd.aprsd import cli
|
||||
from aprsd.threads import rx
|
||||
from aprsd import cli_helper, packets, plugin, threads
|
||||
from aprsd.client import client_factory
|
||||
from aprsd.main import cli
|
||||
from aprsd.packets import collector as packet_collector
|
||||
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
|
||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||
# log.basicConfig(level=log.DEBUG) # level=10
|
||||
LOG = logging.getLogger("APRSD")
|
||||
CONF = cfg.CONF
|
||||
console = Console()
|
||||
|
@ -36,33 +42,49 @@ def signal_handler(sig, frame):
|
|||
),
|
||||
)
|
||||
time.sleep(5)
|
||||
LOG.info(stats.APRSDStats())
|
||||
LOG.info(collector.Collector().collect())
|
||||
|
||||
|
||||
class APRSDListenThread(rx.APRSDRXThread):
|
||||
def __init__(self, packet_queue, packet_filter=None):
|
||||
def __init__(self, packet_queue, packet_filter=None, plugin_manager=None):
|
||||
super().__init__(packet_queue)
|
||||
self.packet_filter = packet_filter
|
||||
self.plugin_manager = plugin_manager
|
||||
if self.plugin_manager:
|
||||
LOG.info(f"Plugins {self.plugin_manager.get_message_plugins()}")
|
||||
|
||||
def process_packet(self, *args, **kwargs):
|
||||
packet = self._client.decode_packet(*args, **kwargs)
|
||||
filters = {
|
||||
packets.Packet.__name__: packets.Packet,
|
||||
packets.AckPacket.__name__: packets.AckPacket,
|
||||
packets.BeaconPacket.__name__: packets.BeaconPacket,
|
||||
packets.GPSPacket.__name__: packets.GPSPacket,
|
||||
packets.MessagePacket.__name__: packets.MessagePacket,
|
||||
packets.MicEPacket.__name__: packets.MicEPacket,
|
||||
packets.ObjectPacket.__name__: packets.ObjectPacket,
|
||||
packets.StatusPacket.__name__: packets.StatusPacket,
|
||||
packets.ThirdPartyPacket.__name__: packets.ThirdPartyPacket,
|
||||
packets.WeatherPacket.__name__: packets.WeatherPacket,
|
||||
packets.UnknownPacket.__name__: packets.UnknownPacket,
|
||||
}
|
||||
|
||||
if self.packet_filter:
|
||||
filter_class = filters[self.packet_filter]
|
||||
if isinstance(packet, filter_class):
|
||||
packet.log(header="RX")
|
||||
packet_log.log(packet)
|
||||
if self.plugin_manager:
|
||||
# Don't do anything with the reply
|
||||
# This is the listen only command.
|
||||
self.plugin_manager.run(packet)
|
||||
else:
|
||||
packet.log(header="RX")
|
||||
packet_log.log(packet)
|
||||
if self.plugin_manager:
|
||||
# Don't do anything with the reply.
|
||||
# This is the listen only command.
|
||||
self.plugin_manager.run(packet)
|
||||
|
||||
packets.PacketList().rx(packet)
|
||||
packet_collector.PacketCollector().rx(packet)
|
||||
|
||||
|
||||
@cli.command()
|
||||
|
@ -83,17 +105,28 @@ class APRSDListenThread(rx.APRSDRXThread):
|
|||
"--packet-filter",
|
||||
type=click.Choice(
|
||||
[
|
||||
packets.Packet.__name__,
|
||||
packets.AckPacket.__name__,
|
||||
packets.BeaconPacket.__name__,
|
||||
packets.GPSPacket.__name__,
|
||||
packets.MicEPacket.__name__,
|
||||
packets.MessagePacket.__name__,
|
||||
packets.ObjectPacket.__name__,
|
||||
packets.RejectPacket.__name__,
|
||||
packets.StatusPacket.__name__,
|
||||
packets.ThirdPartyPacket.__name__,
|
||||
packets.UnknownPacket.__name__,
|
||||
packets.WeatherPacket.__name__,
|
||||
],
|
||||
case_sensitive=False,
|
||||
),
|
||||
help="Filter by packet type",
|
||||
)
|
||||
@click.option(
|
||||
"--load-plugins",
|
||||
default=False,
|
||||
is_flag=True,
|
||||
help="Load plugins as enabled in aprsd.conf ?",
|
||||
)
|
||||
@click.argument(
|
||||
"filter",
|
||||
nargs=-1,
|
||||
|
@ -106,6 +139,7 @@ def listen(
|
|||
aprs_login,
|
||||
aprs_password,
|
||||
packet_filter,
|
||||
load_plugins,
|
||||
filter,
|
||||
):
|
||||
"""Listen to packets on the APRS-IS Network based on FILTER.
|
||||
|
@ -124,7 +158,7 @@ def listen(
|
|||
if not aprs_login:
|
||||
click.echo(ctx.get_help())
|
||||
click.echo("")
|
||||
ctx.fail("Must set --aprs_login or APRS_LOGIN")
|
||||
ctx.fail("Must set --aprs-login or APRS_LOGIN")
|
||||
ctx.exit()
|
||||
|
||||
if not aprs_password:
|
||||
|
@ -139,37 +173,58 @@ def listen(
|
|||
LOG.info(f"APRSD Listen Started version: {aprsd.__version__}")
|
||||
|
||||
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||
collector.Collector()
|
||||
|
||||
# Try and load saved MsgTrack list
|
||||
LOG.debug("Loading saved MsgTrack object.")
|
||||
|
||||
# Initialize the client factory and create
|
||||
# The correct client object ready for use
|
||||
client.ClientFactory.setup()
|
||||
# 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.")
|
||||
sys.exit(-1)
|
||||
|
||||
# Creates the client object
|
||||
LOG.info("Creating client connection")
|
||||
aprs_client = client.factory.create()
|
||||
aprs_client = client_factory.create()
|
||||
LOG.info(aprs_client)
|
||||
|
||||
LOG.debug(f"Filter by '{filter}'")
|
||||
aprs_client.set_filter(filter)
|
||||
|
||||
keepalive = threads.KeepAliveThread()
|
||||
keepalive.start()
|
||||
keepalive = keep_alive.KeepAliveThread()
|
||||
# keepalive.start()
|
||||
|
||||
if not CONF.enable_seen_list:
|
||||
# just deregister the class from the packet collector
|
||||
packet_collector.PacketCollector().unregister(seen_list.SeenList)
|
||||
|
||||
pm = None
|
||||
pm = plugin.PluginManager()
|
||||
if load_plugins:
|
||||
LOG.info("Loading plugins")
|
||||
pm.setup_plugins(load_help_plugin=False)
|
||||
else:
|
||||
LOG.warning(
|
||||
"Not Loading any plugins use --load-plugins to load what's "
|
||||
"defined in the config file.",
|
||||
)
|
||||
stats = stats_thread.APRSDStatsStoreThread()
|
||||
stats.start()
|
||||
|
||||
LOG.debug("Create APRSDListenThread")
|
||||
listen_thread = APRSDListenThread(
|
||||
packet_queue=threads.packet_queue,
|
||||
packet_filter=packet_filter,
|
||||
plugin_manager=pm,
|
||||
)
|
||||
LOG.debug("Start APRSDListenThread")
|
||||
listen_thread.start()
|
||||
|
||||
keepalive.start()
|
||||
LOG.debug("keepalive Join")
|
||||
keepalive.join()
|
||||
LOG.debug("listen_thread Join")
|
||||
listen_thread.join()
|
||||
stats.join()
|
||||
|
|
|
@ -8,9 +8,11 @@ import click
|
|||
from oslo_config import cfg
|
||||
|
||||
import aprsd
|
||||
from aprsd import cli_helper, client, packets
|
||||
from aprsd import cli_helper, packets
|
||||
from aprsd import conf # noqa : F401
|
||||
from aprsd.aprsd import cli
|
||||
from aprsd.client import client_factory
|
||||
from aprsd.main import cli
|
||||
from aprsd.packets import collector
|
||||
from aprsd.threads import tx
|
||||
|
||||
|
||||
|
@ -76,7 +78,6 @@ def send_message(
|
|||
aprs_login = CONF.aprs_network.login
|
||||
|
||||
if not aprs_password:
|
||||
LOG.warning(CONF.aprs_network.password)
|
||||
if not CONF.aprs_network.password:
|
||||
click.echo("Must set --aprs-password or APRS_PASSWORD")
|
||||
ctx.exit(-1)
|
||||
|
@ -102,9 +103,9 @@ def send_message(
|
|||
|
||||
def rx_packet(packet):
|
||||
global got_ack, got_response
|
||||
cl = client.factory.create()
|
||||
cl = client_factory.create()
|
||||
packet = cl.decode_packet(packet)
|
||||
packets.PacketList().rx(packet)
|
||||
collector.PacketCollector().rx(packet)
|
||||
packet.log("RX")
|
||||
# LOG.debug("Got packet back {}".format(packet))
|
||||
if isinstance(packet, packets.AckPacket):
|
||||
|
@ -130,8 +131,7 @@ def send_message(
|
|||
sys.exit(0)
|
||||
|
||||
try:
|
||||
client.ClientFactory.setup()
|
||||
client.factory.create().client
|
||||
client_factory.create().client
|
||||
except LoginError:
|
||||
sys.exit(-1)
|
||||
|
||||
|
@ -163,7 +163,7 @@ def send_message(
|
|||
# This will register a packet consumer with aprslib
|
||||
# When new packets come in the consumer will process
|
||||
# the packet
|
||||
aprs_client = client.factory.create().client
|
||||
aprs_client = client_factory.create().client
|
||||
aprs_client.consumer(rx_packet, raw=False)
|
||||
except aprslib.exceptions.ConnectionDrop:
|
||||
LOG.error("Connection dropped, reconnecting")
|
||||
|
|
|
@ -6,11 +6,16 @@ import click
|
|||
from oslo_config import cfg
|
||||
|
||||
import aprsd
|
||||
from aprsd import aprsd as aprsd_main
|
||||
from aprsd import cli_helper, client, packets, plugin, threads, utils
|
||||
from aprsd.aprsd import cli
|
||||
from aprsd.rpc import server as rpc_server
|
||||
from aprsd.threads import rx
|
||||
from aprsd import cli_helper
|
||||
from aprsd import main as aprsd_main
|
||||
from aprsd import packets, plugin, threads, utils
|
||||
from aprsd.client import client_factory
|
||||
from aprsd.main import cli
|
||||
from aprsd.packets import collector as packet_collector
|
||||
from aprsd.packets import 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
|
||||
|
@ -45,7 +50,14 @@ def server(ctx, flush):
|
|||
|
||||
# Initialize the client factory and create
|
||||
# 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
|
||||
# We register plugins first here so we can register each
|
||||
|
@ -57,20 +69,24 @@ def server(ctx, flush):
|
|||
|
||||
# Dump all the config options now.
|
||||
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||
message_plugins = plugin_manager.get_message_plugins()
|
||||
watchlist_plugins = plugin_manager.get_watchlist_plugins()
|
||||
LOG.info("Message Plugins enabled and running:")
|
||||
for p in message_plugins:
|
||||
LOG.info(p)
|
||||
LOG.info("Watchlist Plugins enabled and running:")
|
||||
for p in watchlist_plugins:
|
||||
LOG.info(p)
|
||||
|
||||
# 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.")
|
||||
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.")
|
||||
sys.exit(-1)
|
||||
|
||||
# Creates the client object
|
||||
# LOG.info("Creating client connection")
|
||||
# client.factory.create().client
|
||||
|
||||
# Now load the msgTrack from disk if any
|
||||
packets.PacketList()
|
||||
if flush:
|
||||
|
@ -78,12 +94,24 @@ def server(ctx, flush):
|
|||
packets.PacketTrack().flush()
|
||||
packets.WatchList().flush()
|
||||
packets.SeenList().flush()
|
||||
packets.PacketList().flush()
|
||||
else:
|
||||
# Try and load saved MsgTrack list
|
||||
LOG.debug("Loading saved MsgTrack object.")
|
||||
packets.PacketTrack().load()
|
||||
packets.WatchList().load()
|
||||
packets.SeenList().load()
|
||||
packets.PacketList().load()
|
||||
|
||||
keepalive = keep_alive.KeepAliveThread()
|
||||
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(
|
||||
packet_queue=threads.packet_queue,
|
||||
|
@ -94,16 +122,19 @@ def server(ctx, flush):
|
|||
rx_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()
|
||||
|
||||
keepalive = threads.KeepAliveThread()
|
||||
keepalive.start()
|
||||
if CONF.aprs_registry.enabled:
|
||||
LOG.info("Registry Enabled. Starting Registry thread.")
|
||||
registry_thread = registry.APRSRegistryThread()
|
||||
registry_thread.start()
|
||||
|
||||
if CONF.rpc_settings.enabled:
|
||||
rpc = rpc_server.APRSDRPCThread()
|
||||
rpc.start()
|
||||
log_monitor = threads.log_monitor.LogMonitorThread()
|
||||
log_monitor.start()
|
||||
if CONF.admin.web_enabled:
|
||||
log_monitor_thread = log_monitor.LogMonitorThread()
|
||||
log_monitor_thread.start()
|
||||
|
||||
rx_thread.join()
|
||||
process_thread.join()
|
||||
|
|
|
@ -1,39 +1,56 @@
|
|||
import datetime
|
||||
import json
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import math
|
||||
import signal
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
from aprslib import util as aprslib_util
|
||||
import click
|
||||
import flask
|
||||
from flask import request
|
||||
from flask.logging import default_handler
|
||||
import flask_classful
|
||||
from flask_httpauth import HTTPBasicAuth
|
||||
from flask_socketio import Namespace, SocketIO
|
||||
from geopy.distance import geodesic
|
||||
from oslo_config import cfg
|
||||
from user_agents import parse as ua_parse
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
import wrapt
|
||||
|
||||
import aprsd
|
||||
from aprsd import cli_helper, client, conf, packets, stats, threads, utils
|
||||
from aprsd.aprsd import cli
|
||||
from aprsd.logging import rich as aprsd_logging
|
||||
from aprsd.threads import rx, tx
|
||||
from aprsd.utils import objectstore, trace
|
||||
from aprsd import (
|
||||
cli_helper, client, packets, plugin_utils, stats, threads, utils,
|
||||
)
|
||||
from aprsd.client import client_factory, kiss
|
||||
from aprsd.main import cli
|
||||
from aprsd.threads import aprsd as aprsd_threads
|
||||
from aprsd.threads import keep_alive, rx, tx
|
||||
from aprsd.utils import trace
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger("APRSD")
|
||||
LOG = logging.getLogger()
|
||||
auth = HTTPBasicAuth()
|
||||
users = None
|
||||
users = {}
|
||||
socketio = None
|
||||
|
||||
# List of callsigns that we don't want to track/fetch their location
|
||||
callsign_no_track = [
|
||||
"REPEAT", "WB4BOR-11", "APDW16", "WXNOW", "WXBOT", "BLN0", "BLN1", "BLN2",
|
||||
"BLN3", "BLN4", "BLN5", "BLN6", "BLN7", "BLN8", "BLN9",
|
||||
]
|
||||
|
||||
# Callsign location information
|
||||
# callsign: {lat: 0.0, long: 0.0, last_update: datetime}
|
||||
callsign_locations = {}
|
||||
|
||||
flask_app = flask.Flask(
|
||||
"aprsd",
|
||||
static_url_path="/static",
|
||||
static_folder="web/chat/static",
|
||||
template_folder="web/chat/templates",
|
||||
)
|
||||
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
|
||||
|
@ -47,12 +64,13 @@ def signal_handler(sig, frame):
|
|||
time.sleep(1.5)
|
||||
# packets.WatchList().save()
|
||||
# packets.SeenList().save()
|
||||
LOG.info(stats.APRSDStats())
|
||||
LOG.info(stats.stats_collector.collect())
|
||||
LOG.info("Telling flask to bail.")
|
||||
signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
|
||||
|
||||
class SentMessages(objectstore.ObjectStoreMixin):
|
||||
class SentMessages:
|
||||
|
||||
_instance = None
|
||||
lock = threading.Lock()
|
||||
|
||||
|
@ -69,25 +87,7 @@ class SentMessages(objectstore.ObjectStoreMixin):
|
|||
|
||||
@wrapt.synchronized(lock)
|
||||
def add(self, msg):
|
||||
self.data[msg.msgNo] = self.create(msg.msgNo)
|
||||
self.data[msg.msgNo]["from"] = msg.from_call
|
||||
self.data[msg.msgNo]["to"] = msg.to_call
|
||||
self.data[msg.msgNo]["message"] = msg.message_text.rstrip("\n")
|
||||
self.data[msg.msgNo]["raw"] = msg.message_text.rstrip("\n")
|
||||
|
||||
def create(self, id):
|
||||
return {
|
||||
"id": id,
|
||||
"ts": time.time(),
|
||||
"ack": False,
|
||||
"from": None,
|
||||
"to": None,
|
||||
"raw": None,
|
||||
"message": None,
|
||||
"status": None,
|
||||
"last_update": None,
|
||||
"reply": None,
|
||||
}
|
||||
self.data[msg.msgNo] = msg.__dict__
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def __len__(self):
|
||||
|
@ -133,8 +133,188 @@ def verify_password(username, password):
|
|||
return username
|
||||
|
||||
|
||||
def calculate_initial_compass_bearing(point_a, point_b):
|
||||
"""
|
||||
Calculates the bearing between two points.
|
||||
The formulae used is the following:
|
||||
θ = atan2(sin(Δlong).cos(lat2),
|
||||
cos(lat1).sin(lat2) − sin(lat1).cos(lat2).cos(Δlong))
|
||||
:Parameters:
|
||||
- `pointA: The tuple representing the latitude/longitude for the
|
||||
first point. Latitude and longitude must be in decimal degrees
|
||||
- `pointB: The tuple representing the latitude/longitude for the
|
||||
second point. Latitude and longitude must be in decimal degrees
|
||||
:Returns:
|
||||
The bearing in degrees
|
||||
:Returns Type:
|
||||
float
|
||||
"""
|
||||
if (type(point_a) is not tuple) or (type(point_b) is not tuple):
|
||||
raise TypeError("Only tuples are supported as arguments")
|
||||
|
||||
lat1 = math.radians(point_a[0])
|
||||
lat2 = math.radians(point_b[0])
|
||||
|
||||
diff_long = math.radians(point_b[1] - point_a[1])
|
||||
|
||||
x = math.sin(diff_long) * math.cos(lat2)
|
||||
y = math.cos(lat1) * math.sin(lat2) - (
|
||||
math.sin(lat1)
|
||||
* math.cos(lat2) * math.cos(diff_long)
|
||||
)
|
||||
|
||||
initial_bearing = math.atan2(x, y)
|
||||
|
||||
# Now we have the initial bearing but math.atan2 return values
|
||||
# from -180° to + 180° which is not what we want for a compass bearing
|
||||
# The solution is to normalize the initial bearing as shown below
|
||||
initial_bearing = math.degrees(initial_bearing)
|
||||
compass_bearing = (initial_bearing + 360) % 360
|
||||
|
||||
return compass_bearing
|
||||
|
||||
|
||||
def _build_location_from_repeat(message):
|
||||
# This is a location message Format is
|
||||
# ^ld^callsign:latitude,longitude,altitude,course,speed,timestamp
|
||||
a = message.split(":")
|
||||
LOG.warning(a)
|
||||
if len(a) == 2:
|
||||
callsign = a[0].replace("^ld^", "")
|
||||
b = a[1].split(",")
|
||||
LOG.warning(b)
|
||||
if len(b) == 6:
|
||||
lat = float(b[0])
|
||||
lon = float(b[1])
|
||||
alt = float(b[2])
|
||||
course = float(b[3])
|
||||
speed = float(b[4])
|
||||
time = int(b[5])
|
||||
data = {
|
||||
"callsign": callsign,
|
||||
"lat": lat,
|
||||
"lon": lon,
|
||||
"altitude": alt,
|
||||
"course": course,
|
||||
"speed": speed,
|
||||
"lasttime": time,
|
||||
}
|
||||
LOG.warning(f"Location data from REPEAT {data}")
|
||||
return data
|
||||
|
||||
|
||||
def _calculate_location_data(location_data):
|
||||
"""Calculate all of the location data from data from aprs.fi or REPEAT."""
|
||||
lat = location_data["lat"]
|
||||
lon = location_data["lon"]
|
||||
alt = location_data["altitude"]
|
||||
speed = location_data["speed"]
|
||||
lasttime = location_data["lasttime"]
|
||||
# now calculate distance from our own location
|
||||
distance = 0
|
||||
if CONF.webchat.latitude and CONF.webchat.longitude:
|
||||
our_lat = float(CONF.webchat.latitude)
|
||||
our_lon = float(CONF.webchat.longitude)
|
||||
distance = geodesic((our_lat, our_lon), (lat, lon)).kilometers
|
||||
bearing = calculate_initial_compass_bearing(
|
||||
(our_lat, our_lon),
|
||||
(lat, lon),
|
||||
)
|
||||
return {
|
||||
"callsign": location_data["callsign"],
|
||||
"lat": lat,
|
||||
"lon": lon,
|
||||
"altitude": alt,
|
||||
"course": f"{bearing:0.1f}",
|
||||
"speed": speed,
|
||||
"lasttime": lasttime,
|
||||
"distance": f"{distance:0.3f}",
|
||||
}
|
||||
|
||||
|
||||
def send_location_data_to_browser(location_data):
|
||||
global socketio
|
||||
callsign = location_data["callsign"]
|
||||
LOG.info(f"Got location for {callsign} {callsign_locations[callsign]}")
|
||||
socketio.emit(
|
||||
"callsign_location", callsign_locations[callsign],
|
||||
namespace="/sendmsg",
|
||||
)
|
||||
|
||||
|
||||
def populate_callsign_location(callsign, data=None):
|
||||
"""Populate the location for the callsign.
|
||||
|
||||
if data is passed in, then we have the location already from
|
||||
an APRS packet. If data is None, then we need to fetch the
|
||||
location from aprs.fi or REPEAT.
|
||||
"""
|
||||
global socketio
|
||||
"""Fetch the location for the callsign."""
|
||||
LOG.debug(f"populate_callsign_location {callsign}")
|
||||
if data:
|
||||
location_data = _calculate_location_data(data)
|
||||
callsign_locations[callsign] = location_data
|
||||
send_location_data_to_browser(location_data)
|
||||
return
|
||||
|
||||
# First we are going to try to get the location from aprs.fi
|
||||
# if there is no internets, then this will fail and we will
|
||||
# fallback to calling REPEAT for the location for the callsign.
|
||||
fallback = False
|
||||
if not CONF.aprs_fi.apiKey:
|
||||
LOG.warning(
|
||||
"Config aprs_fi.apiKey is not set. Can't get location from aprs.fi "
|
||||
" falling back to sending REPEAT to get location.",
|
||||
)
|
||||
fallback = True
|
||||
else:
|
||||
try:
|
||||
aprs_data = plugin_utils.get_aprs_fi(CONF.aprs_fi.apiKey, callsign)
|
||||
if not len(aprs_data["entries"]):
|
||||
LOG.error("Didn't get any entries from aprs.fi")
|
||||
return
|
||||
lat = float(aprs_data["entries"][0]["lat"])
|
||||
lon = float(aprs_data["entries"][0]["lng"])
|
||||
try: # altitude not always provided
|
||||
alt = float(aprs_data["entries"][0]["altitude"])
|
||||
except Exception:
|
||||
alt = 0
|
||||
location_data = {
|
||||
"callsign": callsign,
|
||||
"lat": lat,
|
||||
"lon": lon,
|
||||
"altitude": alt,
|
||||
"lasttime": int(aprs_data["entries"][0]["lasttime"]),
|
||||
"course": float(aprs_data["entries"][0].get("course", 0)),
|
||||
"speed": float(aprs_data["entries"][0].get("speed", 0)),
|
||||
}
|
||||
location_data = _calculate_location_data(location_data)
|
||||
callsign_locations[callsign] = location_data
|
||||
send_location_data_to_browser(location_data)
|
||||
return
|
||||
except Exception as ex:
|
||||
LOG.error(f"Failed to fetch aprs.fi '{ex}'")
|
||||
LOG.error(ex)
|
||||
fallback = True
|
||||
|
||||
if fallback:
|
||||
# We don't have the location data
|
||||
# and we can't get it from aprs.fi
|
||||
# Send a special message to REPEAT to get the location data
|
||||
LOG.info(f"Sending REPEAT to get location for callsign {callsign}.")
|
||||
tx.send(
|
||||
packets.MessagePacket(
|
||||
from_call=CONF.callsign,
|
||||
to_call="REPEAT",
|
||||
message_text=f"ld {callsign}",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
|
||||
"""Class that handles packets being sent to us."""
|
||||
|
||||
def __init__(self, packet_queue, socketio):
|
||||
self.socketio = socketio
|
||||
self.connected = False
|
||||
|
@ -143,137 +323,166 @@ class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
|
|||
def process_ack_packet(self, packet: packets.AckPacket):
|
||||
super().process_ack_packet(packet)
|
||||
ack_num = packet.get("msgNo")
|
||||
SentMessages().ack(int(ack_num))
|
||||
self.socketio.emit(
|
||||
"ack", SentMessages().get(int(ack_num)),
|
||||
namespace="/sendmsg",
|
||||
)
|
||||
SentMessages().ack(ack_num)
|
||||
msg = SentMessages().get(ack_num)
|
||||
if msg:
|
||||
self.socketio.emit(
|
||||
"ack", msg,
|
||||
namespace="/sendmsg",
|
||||
)
|
||||
self.got_ack = True
|
||||
|
||||
def process_our_message_packet(self, packet: packets.MessagePacket):
|
||||
LOG.info(f"process MessagePacket {repr(packet)}")
|
||||
packet.get("addresse", None)
|
||||
fromcall = packet.from_call
|
||||
|
||||
message = packet.get("message_text", None)
|
||||
msg = {
|
||||
"id": 0,
|
||||
"ts": packet.get("timestamp", time.time()),
|
||||
"ack": False,
|
||||
"from": fromcall,
|
||||
"to": packet.to_call,
|
||||
"raw": packet.raw,
|
||||
"message": message,
|
||||
"status": None,
|
||||
"last_update": None,
|
||||
"reply": None,
|
||||
}
|
||||
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(
|
||||
"new", msg,
|
||||
"new", packet.__dict__,
|
||||
namespace="/sendmsg",
|
||||
)
|
||||
|
||||
|
||||
class WebChatFlask(flask_classful.FlaskView):
|
||||
class LocationProcessingThread(aprsd_threads.APRSDThread):
|
||||
"""Class to handle the location processing."""
|
||||
def __init__(self):
|
||||
super().__init__("LocationProcessingThread")
|
||||
|
||||
def set_config(self):
|
||||
global users
|
||||
self.users = {}
|
||||
user = CONF.admin.user
|
||||
self.users[user] = generate_password_hash(CONF.admin.password)
|
||||
users = self.users
|
||||
def loop(self):
|
||||
pass
|
||||
|
||||
def _get_transport(self, stats):
|
||||
if CONF.aprs_network.enabled:
|
||||
transport = "aprs-is"
|
||||
aprs_connection = (
|
||||
"APRS-IS Server: <a href='http://status.aprs2.net' >"
|
||||
"{}</a>".format(stats["stats"]["aprs-is"]["server"])
|
||||
)
|
||||
else:
|
||||
# We might be connected to a KISS socket?
|
||||
if client.KISSClient.is_enabled():
|
||||
transport = client.KISSClient.transport()
|
||||
if transport == client.TRANSPORT_TCPKISS:
|
||||
aprs_connection = (
|
||||
"TCPKISS://{}:{}".format(
|
||||
CONF.kiss_tcp.host,
|
||||
CONF.kiss_tcp.port,
|
||||
)
|
||||
)
|
||||
elif transport == client.TRANSPORT_SERIALKISS:
|
||||
# for pep8 violation
|
||||
aprs_connection = (
|
||||
"SerialKISS://{}@{} baud".format(
|
||||
CONF.kiss_serial.device,
|
||||
CONF.kiss_serial.baudrate,
|
||||
),
|
||||
)
|
||||
|
||||
return transport, aprs_connection
|
||||
def set_config():
|
||||
global users
|
||||
|
||||
@auth.login_required
|
||||
def index(self):
|
||||
ua_str = request.headers.get("User-Agent")
|
||||
# this takes about 2 seconds :(
|
||||
user_agent = ua_parse(ua_str)
|
||||
LOG.debug(f"Is mobile? {user_agent.is_mobile}")
|
||||
stats = self._stats()
|
||||
|
||||
if user_agent.is_mobile:
|
||||
html_template = "mobile.html"
|
||||
else:
|
||||
html_template = "index.html"
|
||||
|
||||
# For development
|
||||
# html_template = "mobile.html"
|
||||
|
||||
LOG.debug(f"Template {html_template}")
|
||||
|
||||
transport, aprs_connection = self._get_transport(stats)
|
||||
LOG.debug(f"transport {transport} aprs_connection {aprs_connection}")
|
||||
|
||||
stats["transport"] = transport
|
||||
stats["aprs_connection"] = aprs_connection
|
||||
LOG.debug(f"initial stats = {stats}")
|
||||
|
||||
return flask.render_template(
|
||||
html_template,
|
||||
initial_stats=stats,
|
||||
aprs_connection=aprs_connection,
|
||||
callsign=CONF.callsign,
|
||||
version=aprsd.__version__,
|
||||
def _get_transport(stats):
|
||||
if CONF.aprs_network.enabled:
|
||||
transport = "aprs-is"
|
||||
aprs_connection = (
|
||||
"APRS-IS Server: <a href='http://status.aprs2.net' >"
|
||||
"{}</a>".format(stats["APRSClientStats"]["server_string"])
|
||||
)
|
||||
elif kiss.KISSClient.is_enabled():
|
||||
transport = kiss.KISSClient.transport()
|
||||
if transport == client.TRANSPORT_TCPKISS:
|
||||
aprs_connection = (
|
||||
"TCPKISS://{}:{}".format(
|
||||
CONF.kiss_tcp.host,
|
||||
CONF.kiss_tcp.port,
|
||||
)
|
||||
)
|
||||
elif transport == client.TRANSPORT_SERIALKISS:
|
||||
# for pep8 violation
|
||||
aprs_connection = (
|
||||
"SerialKISS://{}@{} baud".format(
|
||||
CONF.kiss_serial.device,
|
||||
CONF.kiss_serial.baudrate,
|
||||
),
|
||||
)
|
||||
elif CONF.fake_client.enabled:
|
||||
transport = client.TRANSPORT_FAKE
|
||||
aprs_connection = "Fake Client"
|
||||
|
||||
@auth.login_required
|
||||
def send_message_status(self):
|
||||
LOG.debug(request)
|
||||
msgs = SentMessages()
|
||||
info = msgs.get_all()
|
||||
return json.dumps(info)
|
||||
return transport, aprs_connection
|
||||
|
||||
def _stats(self):
|
||||
stats_obj = stats.APRSDStats()
|
||||
now = datetime.datetime.now()
|
||||
|
||||
time_format = "%m-%d-%Y %H:%M:%S"
|
||||
stats_dict = stats_obj.stats()
|
||||
# Webchat doesnt need these
|
||||
del stats_dict["aprsd"]["watch_list"]
|
||||
del stats_dict["aprsd"]["seen_list"]
|
||||
# del stats_dict["email"]
|
||||
# del stats_dict["plugins"]
|
||||
# del stats_dict["messages"]
|
||||
@flask_app.route("/location/<callsign>", methods=["POST"])
|
||||
def location(callsign):
|
||||
LOG.debug(f"Fetch location for callsign {callsign}")
|
||||
populate_callsign_location(callsign)
|
||||
|
||||
result = {
|
||||
"time": now.strftime(time_format),
|
||||
"stats": stats_dict,
|
||||
}
|
||||
|
||||
return result
|
||||
@auth.login_required
|
||||
@flask_app.route("/")
|
||||
def index():
|
||||
stats = _stats()
|
||||
|
||||
def stats(self):
|
||||
return json.dumps(self._stats())
|
||||
# For development
|
||||
html_template = "index.html"
|
||||
LOG.debug(f"Template {html_template}")
|
||||
|
||||
transport, aprs_connection = _get_transport(stats["stats"])
|
||||
LOG.debug(f"transport {transport} aprs_connection {aprs_connection}")
|
||||
|
||||
stats["transport"] = transport
|
||||
stats["aprs_connection"] = aprs_connection
|
||||
LOG.debug(f"initial stats = {stats}")
|
||||
latitude = CONF.webchat.latitude
|
||||
if latitude:
|
||||
latitude = float(CONF.webchat.latitude)
|
||||
|
||||
longitude = CONF.webchat.longitude
|
||||
if longitude:
|
||||
longitude = float(longitude)
|
||||
|
||||
return flask.render_template(
|
||||
html_template,
|
||||
initial_stats=stats,
|
||||
aprs_connection=aprs_connection,
|
||||
callsign=CONF.callsign,
|
||||
version=aprsd.__version__,
|
||||
latitude=latitude,
|
||||
longitude=longitude,
|
||||
)
|
||||
|
||||
|
||||
@auth.login_required
|
||||
@flask_app.route("/send-message-status")
|
||||
def send_message_status():
|
||||
LOG.debug(request)
|
||||
msgs = SentMessages()
|
||||
info = msgs.get_all()
|
||||
return json.dumps(info)
|
||||
|
||||
|
||||
def _stats():
|
||||
now = datetime.datetime.now()
|
||||
|
||||
time_format = "%m-%d-%Y %H:%M:%S"
|
||||
stats_dict = stats.stats_collector.collect(serializable=True)
|
||||
# Webchat doesnt need these
|
||||
if "WatchList" in stats_dict:
|
||||
del stats_dict["WatchList"]
|
||||
if "SeenList" in stats_dict:
|
||||
del stats_dict["SeenList"]
|
||||
if "APRSDThreadList" in stats_dict:
|
||||
del stats_dict["APRSDThreadList"]
|
||||
if "PacketList" in stats_dict:
|
||||
del stats_dict["PacketList"]
|
||||
if "EmailStats" in stats_dict:
|
||||
del stats_dict["EmailStats"]
|
||||
if "PluginManager" in stats_dict:
|
||||
del stats_dict["PluginManager"]
|
||||
|
||||
result = {
|
||||
"time": now.strftime(time_format),
|
||||
"stats": stats_dict,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
@flask_app.route("/stats")
|
||||
def get_stats():
|
||||
return json.dumps(_stats())
|
||||
|
||||
|
||||
class SendMessageNamespace(Namespace):
|
||||
|
@ -302,11 +511,22 @@ class SendMessageNamespace(Namespace):
|
|||
LOG.debug(f"WS: on_send {data}")
|
||||
self.request = data
|
||||
data["from"] = CONF.callsign
|
||||
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]
|
||||
|
||||
pkt = packets.MessagePacket(
|
||||
from_call=data["from"],
|
||||
to_call=data["to"].upper(),
|
||||
message_text=data["message"],
|
||||
path=path,
|
||||
)
|
||||
pkt.prepare()
|
||||
self.msg = pkt
|
||||
msgs = SentMessages()
|
||||
msgs.add(pkt)
|
||||
|
@ -320,18 +540,27 @@ class SendMessageNamespace(Namespace):
|
|||
|
||||
def on_gps(self, data):
|
||||
LOG.debug(f"WS on_GPS: {data}")
|
||||
lat = aprslib_util.latitude_to_ddm(data["latitude"])
|
||||
long = aprslib_util.longitude_to_ddm(data["longitude"])
|
||||
LOG.debug(f"Lat DDM {lat}")
|
||||
LOG.debug(f"Long DDM {long}")
|
||||
lat = data["latitude"]
|
||||
long = data["longitude"]
|
||||
LOG.debug(f"Lat {lat}")
|
||||
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(
|
||||
packets.GPSPacket(
|
||||
packets.BeaconPacket(
|
||||
from_call=CONF.callsign,
|
||||
to_call="APDW16",
|
||||
latitude=lat,
|
||||
longitude=long,
|
||||
comment="APRSD WebChat Beacon",
|
||||
path=path,
|
||||
),
|
||||
direct=True,
|
||||
)
|
||||
|
@ -342,72 +571,26 @@ class SendMessageNamespace(Namespace):
|
|||
def handle_json(self, data):
|
||||
LOG.debug(f"WS json {data}")
|
||||
|
||||
|
||||
def setup_logging(flask_app, loglevel, quiet):
|
||||
flask_log = logging.getLogger("werkzeug")
|
||||
flask_app.logger.removeHandler(default_handler)
|
||||
flask_log.removeHandler(default_handler)
|
||||
|
||||
log_level = conf.log.LOG_LEVELS[loglevel]
|
||||
flask_log.setLevel(log_level)
|
||||
date_format = CONF.logging.date_format
|
||||
|
||||
if CONF.logging.rich_logging and not quiet:
|
||||
log_format = "%(message)s"
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
rh = aprsd_logging.APRSDRichHandler(
|
||||
show_thread=True, thread_width=15,
|
||||
rich_tracebacks=True, omit_repeated_times=False,
|
||||
)
|
||||
rh.setFormatter(log_formatter)
|
||||
flask_log.addHandler(rh)
|
||||
|
||||
log_file = CONF.logging.logfile
|
||||
|
||||
if log_file:
|
||||
log_format = CONF.loging.logformat
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
fh = RotatingFileHandler(
|
||||
log_file, maxBytes=(10248576 * 5),
|
||||
backupCount=4,
|
||||
)
|
||||
fh.setFormatter(log_formatter)
|
||||
flask_log.addHandler(fh)
|
||||
def on_get_callsign_location(self, data):
|
||||
LOG.debug(f"on_callsign_location {data}")
|
||||
populate_callsign_location(data["callsign"])
|
||||
|
||||
|
||||
@trace.trace
|
||||
def init_flask(loglevel, quiet):
|
||||
global socketio
|
||||
|
||||
flask_app = flask.Flask(
|
||||
"aprsd",
|
||||
static_url_path="/static",
|
||||
static_folder="web/chat/static",
|
||||
template_folder="web/chat/templates",
|
||||
)
|
||||
setup_logging(flask_app, loglevel, quiet)
|
||||
server = WebChatFlask()
|
||||
server.set_config()
|
||||
flask_app.route("/", methods=["GET"])(server.index)
|
||||
flask_app.route("/stats", methods=["GET"])(server.stats)
|
||||
# flask_app.route("/send-message", methods=["GET"])(server.send_message)
|
||||
flask_app.route("/send-message-status", methods=["GET"])(server.send_message_status)
|
||||
global socketio, flask_app
|
||||
|
||||
socketio = SocketIO(
|
||||
flask_app, logger=False, engineio_logger=False,
|
||||
async_mode="threading",
|
||||
)
|
||||
# async_mode="gevent",
|
||||
# async_mode="eventlet",
|
||||
# import eventlet
|
||||
# eventlet.monkey_patch()
|
||||
|
||||
socketio.on_namespace(
|
||||
SendMessageNamespace(
|
||||
"/sendmsg",
|
||||
),
|
||||
)
|
||||
return socketio, flask_app
|
||||
return socketio
|
||||
|
||||
|
||||
# main() ###
|
||||
|
@ -427,8 +610,8 @@ def init_flask(loglevel, quiet):
|
|||
"--port",
|
||||
"port",
|
||||
show_default=True,
|
||||
default=80,
|
||||
help="Port to listen to web requests",
|
||||
default=None,
|
||||
help="Port to listen to web requests. This overrides the config.webchat.web_port setting.",
|
||||
)
|
||||
@click.pass_context
|
||||
@cli_helper.process_standard_options
|
||||
|
@ -447,17 +630,20 @@ def webchat(ctx, flush, port):
|
|||
LOG.info(msg)
|
||||
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
|
||||
users[user] = generate_password_hash(CONF.admin.password)
|
||||
if not port:
|
||||
port = CONF.webchat.web_port
|
||||
|
||||
# Initialize the client factory and create
|
||||
# The correct client object ready for use
|
||||
client.ClientFactory.setup()
|
||||
# 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.")
|
||||
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.")
|
||||
sys.exit(-1)
|
||||
|
||||
|
@ -466,7 +652,11 @@ def webchat(ctx, flush, port):
|
|||
packets.WatchList()
|
||||
packets.SeenList()
|
||||
|
||||
(socketio, app) = init_flask(loglevel, quiet)
|
||||
keepalive = keep_alive.KeepAliveThread()
|
||||
LOG.info("Start KeepAliveThread")
|
||||
keepalive.start()
|
||||
|
||||
socketio = init_flask(loglevel, quiet)
|
||||
rx_thread = rx.APRSDPluginRXThread(
|
||||
packet_queue=threads.packet_queue,
|
||||
)
|
||||
|
@ -477,15 +667,15 @@ def webchat(ctx, flush, port):
|
|||
)
|
||||
process_thread.start()
|
||||
|
||||
keepalive = threads.KeepAliveThread()
|
||||
LOG.info("Start KeepAliveThread")
|
||||
keepalive.start()
|
||||
LOG.info("Start socketio.run()")
|
||||
socketio.run(
|
||||
app,
|
||||
ssl_context="adhoc",
|
||||
host=CONF.admin.web_ip,
|
||||
flask_app,
|
||||
# This is broken for now after removing cryptography
|
||||
# and pyopenssl
|
||||
# ssl_context="adhoc",
|
||||
host=CONF.webchat.web_ip,
|
||||
port=port,
|
||||
allow_unsafe_werkzeug=True,
|
||||
)
|
||||
|
||||
LOG.info("WebChat exiting!!!! Bye.")
|
||||
|
|
|
@ -28,7 +28,7 @@ def set_lib_defaults():
|
|||
|
||||
|
||||
def set_log_defaults():
|
||||
# logging.set_defaults(default_log_levels=logging.get_default_log_levels())
|
||||
# log.set_defaults(default_log_levels=log.get_default_log_levels())
|
||||
pass
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
The options for logging setup
|
||||
The options for log setup
|
||||
"""
|
||||
|
||||
from oslo_config import cfg
|
||||
|
@ -11,14 +11,21 @@ aprs_group = cfg.OptGroup(
|
|||
name="aprs_network",
|
||||
title="APRS-IS Network settings",
|
||||
)
|
||||
|
||||
kiss_serial_group = cfg.OptGroup(
|
||||
name="kiss_serial",
|
||||
title="KISS Serial device connection",
|
||||
)
|
||||
|
||||
kiss_tcp_group = cfg.OptGroup(
|
||||
name="kiss_tcp",
|
||||
title="KISS TCP/IP Device connection",
|
||||
)
|
||||
|
||||
fake_client_group = cfg.OptGroup(
|
||||
name="fake_client",
|
||||
title="Fake Client settings",
|
||||
)
|
||||
aprs_opts = [
|
||||
cfg.BoolOpt(
|
||||
"enabled",
|
||||
|
@ -65,6 +72,11 @@ kiss_serial_opts = [
|
|||
default=9600,
|
||||
help="The Serial device baud rate for communication",
|
||||
),
|
||||
cfg.ListOpt(
|
||||
"path",
|
||||
default=["WIDE1-1", "WIDE2-1"],
|
||||
help="The APRS path to use for wide area coverage.",
|
||||
),
|
||||
]
|
||||
|
||||
kiss_tcp_opts = [
|
||||
|
@ -82,6 +94,19 @@ kiss_tcp_opts = [
|
|||
default=8001,
|
||||
help="The KISS TCP/IP network port",
|
||||
),
|
||||
cfg.ListOpt(
|
||||
"path",
|
||||
default=["WIDE1-1", "WIDE2-1"],
|
||||
help="The APRS path to use for wide area coverage.",
|
||||
),
|
||||
]
|
||||
|
||||
fake_client_opts = [
|
||||
cfg.BoolOpt(
|
||||
"enabled",
|
||||
default=False,
|
||||
help="Enable fake client connection.",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
@ -93,10 +118,14 @@ def register_opts(config):
|
|||
config.register_opts(kiss_serial_opts, group=kiss_serial_group)
|
||||
config.register_opts(kiss_tcp_opts, group=kiss_tcp_group)
|
||||
|
||||
config.register_group(fake_client_group)
|
||||
config.register_opts(fake_client_opts, group=fake_client_group)
|
||||
|
||||
|
||||
def list_opts():
|
||||
return {
|
||||
aprs_group.name: aprs_opts,
|
||||
kiss_serial_group.name: kiss_serial_opts,
|
||||
kiss_tcp_group.name: kiss_tcp_opts,
|
||||
fake_client_group.name: fake_client_opts,
|
||||
}
|
||||
|
|
|
@ -15,9 +15,14 @@ watch_list_group = cfg.OptGroup(
|
|||
name="watch_list",
|
||||
title="Watch List settings",
|
||||
)
|
||||
rpc_group = cfg.OptGroup(
|
||||
name="rpc_settings",
|
||||
title="RPC Settings for admin <--> web",
|
||||
webchat_group = cfg.OptGroup(
|
||||
name="webchat",
|
||||
title="Settings specific to the webchat command",
|
||||
)
|
||||
|
||||
registry_group = cfg.OptGroup(
|
||||
name="aprs_registry",
|
||||
title="APRS Registry settings",
|
||||
)
|
||||
|
||||
|
||||
|
@ -47,6 +52,90 @@ aprsd_opts = [
|
|||
default="imperial",
|
||||
help="Units for display, imperial or metric",
|
||||
),
|
||||
cfg.IntOpt(
|
||||
"ack_rate_limit_period",
|
||||
default=1,
|
||||
help="The wait period in seconds per Ack packet being sent."
|
||||
"1 means 1 ack packet per second allowed."
|
||||
"2 means 1 pack packet every 2 seconds allowed",
|
||||
),
|
||||
cfg.IntOpt(
|
||||
"msg_rate_limit_period",
|
||||
default=2,
|
||||
help="Wait period in seconds per non AckPacket being sent."
|
||||
"2 means 1 packet every 2 seconds allowed."
|
||||
"5 means 1 pack packet every 5 seconds allowed",
|
||||
),
|
||||
cfg.IntOpt(
|
||||
"packet_dupe_timeout",
|
||||
default=300,
|
||||
help="The number of seconds before a packet is not considered a duplicate.",
|
||||
),
|
||||
cfg.BoolOpt(
|
||||
"enable_beacon",
|
||||
default=False,
|
||||
help="Enable sending of a GPS Beacon packet to locate this service. "
|
||||
"Requires latitude and longitude to be set.",
|
||||
),
|
||||
cfg.IntOpt(
|
||||
"beacon_interval",
|
||||
default=1800,
|
||||
help="The number of seconds between beacon packets.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"beacon_symbol",
|
||||
default="/",
|
||||
help="The symbol to use for the GPS Beacon packet. See: http://www.aprs.net/vm/DOS/SYMBOLS.HTM",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"latitude",
|
||||
default=None,
|
||||
help="Latitude for the GPS Beacon button. If not set, the button will not be enabled.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"longitude",
|
||||
default=None,
|
||||
help="Longitude for the GPS Beacon button. If not set, the button will not be enabled.",
|
||||
),
|
||||
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 = [
|
||||
|
@ -84,7 +173,7 @@ admin_opts = [
|
|||
default=False,
|
||||
help="Enable the Admin Web Interface",
|
||||
),
|
||||
cfg.IPOpt(
|
||||
cfg.StrOpt(
|
||||
"web_ip",
|
||||
default="0.0.0.0",
|
||||
help="The ip address to listen on",
|
||||
|
@ -101,33 +190,12 @@ admin_opts = [
|
|||
),
|
||||
cfg.StrOpt(
|
||||
"password",
|
||||
default="password",
|
||||
secret=True,
|
||||
help="Admin interface password",
|
||||
),
|
||||
]
|
||||
|
||||
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 = [
|
||||
cfg.ListOpt(
|
||||
|
@ -149,6 +217,67 @@ enabled_plugins_opts = [
|
|||
),
|
||||
]
|
||||
|
||||
webchat_opts = [
|
||||
cfg.StrOpt(
|
||||
"web_ip",
|
||||
default="0.0.0.0",
|
||||
help="The ip address to listen on",
|
||||
),
|
||||
cfg.PortOpt(
|
||||
"web_port",
|
||||
default=8001,
|
||||
help="The port to listen on",
|
||||
),
|
||||
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.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.",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def register_opts(config):
|
||||
config.register_opts(aprsd_opts)
|
||||
|
@ -157,8 +286,10 @@ def register_opts(config):
|
|||
config.register_opts(admin_opts, group=admin_group)
|
||||
config.register_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_opts(webchat_opts, group=webchat_group)
|
||||
config.register_group(registry_group)
|
||||
config.register_opts(registry_opts, group=registry_group)
|
||||
|
||||
|
||||
def list_opts():
|
||||
|
@ -166,5 +297,6 @@ def list_opts():
|
|||
"DEFAULT": (aprsd_opts + enabled_plugins_opts),
|
||||
admin_group.name: admin_opts,
|
||||
watch_list_group.name: watch_list_opts,
|
||||
rpc_group.name: rpc_opts,
|
||||
webchat_group.name: webchat_opts,
|
||||
registry_group.name: registry_opts,
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
The options for logging setup
|
||||
The options for log setup
|
||||
"""
|
||||
import logging
|
||||
|
||||
|
@ -20,21 +20,19 @@ DEFAULT_LOG_FORMAT = (
|
|||
" %(message)s - [%(pathname)s:%(lineno)d]"
|
||||
)
|
||||
|
||||
DEFAULT_LOG_FORMAT = (
|
||||
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
|
||||
"<yellow>{thread.name: <18}</yellow> | "
|
||||
"<level>{level: <8}</level> | "
|
||||
"<level>{message}</level> | "
|
||||
"<cyan>{name}</cyan>:<cyan>{function:}</cyan>:<magenta>{line:}</magenta>"
|
||||
)
|
||||
|
||||
logging_group = cfg.OptGroup(
|
||||
name="logging",
|
||||
title="Logging options",
|
||||
)
|
||||
logging_opts = [
|
||||
cfg.StrOpt(
|
||||
"date_format",
|
||||
default=DEFAULT_DATE_FORMAT,
|
||||
help="Date format for log entries",
|
||||
),
|
||||
cfg.BoolOpt(
|
||||
"rich_logging",
|
||||
default=True,
|
||||
help="Enable Rich logging",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"logfile",
|
||||
default=None,
|
||||
|
@ -45,6 +43,12 @@ logging_opts = [
|
|||
default=DEFAULT_LOG_FORMAT,
|
||||
help="Log file format, unless rich_logging enabled.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"log_level",
|
||||
default="INFO",
|
||||
choices=LOG_LEVELS.keys(),
|
||||
help="Log level for logging of events.",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -18,6 +18,11 @@ owm_wx_group = cfg.OptGroup(
|
|||
title="Options for the OWMWeatherPlugin",
|
||||
)
|
||||
|
||||
location_group = cfg.OptGroup(
|
||||
name="location_plugin",
|
||||
title="Options for the LocationPlugin",
|
||||
)
|
||||
|
||||
aprsfi_opts = [
|
||||
cfg.StrOpt(
|
||||
"apiKey",
|
||||
|
@ -62,6 +67,106 @@ avwx_opts = [
|
|||
),
|
||||
]
|
||||
|
||||
location_opts = [
|
||||
cfg.StrOpt(
|
||||
"geopy_geocoder",
|
||||
choices=[
|
||||
"ArcGIS", "AzureMaps", "Baidu", "Bing", "GoogleV3", "HERE",
|
||||
"Nominatim", "OpenCage", "TomTom", "USGov", "What3Words", "Woosmap",
|
||||
],
|
||||
default="Nominatim",
|
||||
help="The geopy geocoder to use. Default is Nominatim."
|
||||
"See https://geopy.readthedocs.io/en/stable/#module-geopy.geocoders"
|
||||
"for more information.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"user_agent",
|
||||
default="APRSD",
|
||||
help="The user agent to use for the Nominatim geocoder."
|
||||
"See https://geopy.readthedocs.io/en/stable/#module-geopy.geocoders"
|
||||
"for more information.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"arcgis_username",
|
||||
default=None,
|
||||
help="The username to use for the ArcGIS geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#arcgis"
|
||||
"for more information."
|
||||
"Only used for the ArcGIS geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"arcgis_password",
|
||||
default=None,
|
||||
help="The password to use for the ArcGIS geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#arcgis"
|
||||
"for more information."
|
||||
"Only used for the ArcGIS geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"azuremaps_subscription_key",
|
||||
help="The subscription key to use for the AzureMaps geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#azuremaps"
|
||||
"for more information."
|
||||
"Only used for the AzureMaps geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"baidu_api_key",
|
||||
help="The API key to use for the Baidu geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#baidu"
|
||||
"for more information."
|
||||
"Only used for the Baidu geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"bing_api_key",
|
||||
help="The API key to use for the Bing geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#bing"
|
||||
"for more information."
|
||||
"Only used for the Bing geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"google_api_key",
|
||||
help="The API key to use for the Google geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#googlev3"
|
||||
"for more information."
|
||||
"Only used for the Google geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"here_api_key",
|
||||
help="The API key to use for the HERE geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#here"
|
||||
"for more information."
|
||||
"Only used for the HERE geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"opencage_api_key",
|
||||
help="The API key to use for the OpenCage geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#opencage"
|
||||
"for more information."
|
||||
"Only used for the OpenCage geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"tomtom_api_key",
|
||||
help="The API key to use for the TomTom geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#tomtom"
|
||||
"for more information."
|
||||
"Only used for the TomTom geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"what3words_api_key",
|
||||
help="The API key to use for the What3Words geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#what3words"
|
||||
"for more information."
|
||||
"Only used for the What3Words geocoder.",
|
||||
),
|
||||
cfg.StrOpt(
|
||||
"woosmap_api_key",
|
||||
help="The API key to use for the Woosmap geocoder."
|
||||
"See https://geopy.readthedocs.io/en/latest/#woosmap"
|
||||
"for more information."
|
||||
"Only used for the Woosmap geocoder.",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def register_opts(config):
|
||||
config.register_group(aprsfi_group)
|
||||
|
@ -72,6 +177,8 @@ def register_opts(config):
|
|||
config.register_opts(owm_wx_opts, group=owm_wx_group)
|
||||
config.register_group(avwx_group)
|
||||
config.register_opts(avwx_opts, group=avwx_group)
|
||||
config.register_group(location_group)
|
||||
config.register_opts(location_opts, group=location_group)
|
||||
|
||||
|
||||
def list_opts():
|
||||
|
@ -80,4 +187,5 @@ def list_opts():
|
|||
query_group.name: query_plugin_opts,
|
||||
owm_wx_group.name: owm_wx_opts,
|
||||
avwx_group.name: avwx_opts,
|
||||
location_group.name: location_opts,
|
||||
}
|
||||
|
|
347
aprsd/flask.py
347
aprsd/flask.py
|
@ -1,347 +0,0 @@
|
|||
import datetime
|
||||
import json
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import time
|
||||
|
||||
import flask
|
||||
from flask.logging import default_handler
|
||||
import flask_classful
|
||||
from flask_httpauth import HTTPBasicAuth
|
||||
from flask_socketio import Namespace, SocketIO
|
||||
from oslo_config import cfg
|
||||
from werkzeug.security import check_password_hash, generate_password_hash
|
||||
|
||||
import aprsd
|
||||
from aprsd import cli_helper, client, conf, packets, plugin, threads
|
||||
from aprsd.logging import rich as aprsd_logging
|
||||
from aprsd.rpc import client as aprsd_rpc_client
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
auth = HTTPBasicAuth()
|
||||
users = None
|
||||
app = None
|
||||
|
||||
|
||||
# HTTPBasicAuth doesn't work on a class method.
|
||||
# This has to be out here. Rely on the APRSDFlask
|
||||
# class to initialize the users from the config
|
||||
@auth.verify_password
|
||||
def verify_password(username, password):
|
||||
global users
|
||||
|
||||
if username in users and check_password_hash(users.get(username), password):
|
||||
return username
|
||||
|
||||
|
||||
class APRSDFlask(flask_classful.FlaskView):
|
||||
|
||||
def set_config(self):
|
||||
global users
|
||||
self.users = {}
|
||||
user = CONF.admin.user
|
||||
self.users[user] = generate_password_hash(CONF.admin.password)
|
||||
users = self.users
|
||||
|
||||
@auth.login_required
|
||||
def index(self):
|
||||
stats = self._stats()
|
||||
print(stats)
|
||||
LOG.debug(
|
||||
"watch list? {}".format(
|
||||
CONF.watch_list.callsigns,
|
||||
),
|
||||
)
|
||||
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()
|
||||
plugins = pm.get_plugins()
|
||||
plugin_count = len(plugins)
|
||||
|
||||
if CONF.aprs_network.enabled:
|
||||
transport = "aprs-is"
|
||||
aprs_connection = (
|
||||
"APRS-IS Server: <a href='http://status.aprs2.net' >"
|
||||
"{}</a>".format(stats["stats"]["aprs-is"]["server"])
|
||||
)
|
||||
else:
|
||||
# We might be connected to a KISS socket?
|
||||
if client.KISSClient.kiss_enabled():
|
||||
transport = client.KISSClient.transport()
|
||||
if transport == client.TRANSPORT_TCPKISS:
|
||||
aprs_connection = (
|
||||
"TCPKISS://{}:{}".format(
|
||||
CONF.kiss_tcp.host,
|
||||
CONF.kiss_tcp.port,
|
||||
)
|
||||
)
|
||||
elif transport == client.TRANSPORT_SERIALKISS:
|
||||
aprs_connection = (
|
||||
"SerialKISS://{}@{} baud".format(
|
||||
CONF.kiss_serial.device,
|
||||
CONF.kiss_serial.baudrate,
|
||||
)
|
||||
)
|
||||
|
||||
stats["transport"] = transport
|
||||
stats["aprs_connection"] = aprs_connection
|
||||
entries = conf.conf_to_dict()
|
||||
|
||||
return flask.render_template(
|
||||
"index.html",
|
||||
initial_stats=stats,
|
||||
aprs_connection=aprs_connection,
|
||||
callsign=CONF.callsign,
|
||||
version=aprsd.__version__,
|
||||
config_json=json.dumps(
|
||||
entries, indent=4,
|
||||
sort_keys=True, default=str,
|
||||
),
|
||||
watch_count=watch_count,
|
||||
watch_age=watch_age,
|
||||
seen_count=seen_count,
|
||||
plugin_count=plugin_count,
|
||||
)
|
||||
|
||||
@auth.login_required
|
||||
def messages(self):
|
||||
track = packets.PacketTrack()
|
||||
msgs = []
|
||||
for id in track:
|
||||
LOG.info(track[id].dict())
|
||||
msgs.append(track[id].dict())
|
||||
|
||||
return flask.render_template("messages.html", messages=json.dumps(msgs))
|
||||
|
||||
@auth.login_required
|
||||
def packets(self):
|
||||
packet_list = aprsd_rpc_client.RPCClient().get_packet_list()
|
||||
if packet_list:
|
||||
packets = packet_list.get()
|
||||
tmp_list = []
|
||||
for pkt in packets:
|
||||
tmp_list.append(pkt.json)
|
||||
|
||||
return json.dumps(tmp_list)
|
||||
else:
|
||||
return json.dumps([])
|
||||
|
||||
@auth.login_required
|
||||
def plugins(self):
|
||||
pm = plugin.PluginManager()
|
||||
pm.reload_plugins()
|
||||
|
||||
return "reloaded"
|
||||
|
||||
@auth.login_required
|
||||
def save(self):
|
||||
"""Save the existing queue to disk."""
|
||||
track = packets.PacketTrack()
|
||||
track.save()
|
||||
return json.dumps({"messages": "saved"})
|
||||
|
||||
def _stats(self):
|
||||
track = aprsd_rpc_client.RPCClient().get_packet_track()
|
||||
now = datetime.datetime.now()
|
||||
|
||||
time_format = "%m-%d-%Y %H:%M:%S"
|
||||
|
||||
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
|
||||
if packet_list:
|
||||
rx = packet_list.total_rx()
|
||||
tx = packet_list.total_tx()
|
||||
stats_dict["packets"] = {
|
||||
"sent": tx,
|
||||
"received": rx,
|
||||
}
|
||||
if track:
|
||||
size_tracker = len(track)
|
||||
else:
|
||||
size_tracker = 0
|
||||
|
||||
result = {
|
||||
"time": now.strftime(time_format),
|
||||
"size_tracker": size_tracker,
|
||||
"stats": stats_dict,
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def stats(self):
|
||||
return json.dumps(self._stats())
|
||||
|
||||
|
||||
class LogUpdateThread(threads.APRSDThread):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("LogUpdate")
|
||||
|
||||
def loop(self):
|
||||
global socketio
|
||||
|
||||
if socketio:
|
||||
log_entries = aprsd_rpc_client.RPCClient().get_log_entries()
|
||||
|
||||
if log_entries:
|
||||
for entry in log_entries:
|
||||
socketio.emit(
|
||||
"log_entry", entry,
|
||||
namespace="/logs",
|
||||
)
|
||||
|
||||
time.sleep(5)
|
||||
return True
|
||||
|
||||
|
||||
class LoggingNamespace(Namespace):
|
||||
log_thread = None
|
||||
|
||||
def on_connect(self):
|
||||
global socketio
|
||||
socketio.emit(
|
||||
"connected", {"data": "/logs Connected"},
|
||||
namespace="/logs",
|
||||
)
|
||||
self.log_thread = LogUpdateThread()
|
||||
self.log_thread.start()
|
||||
|
||||
def on_disconnect(self):
|
||||
LOG.debug("LOG Disconnected")
|
||||
if self.log_thread:
|
||||
self.log_thread.stop()
|
||||
|
||||
|
||||
def setup_logging(flask_app, loglevel, quiet):
|
||||
flask_log = logging.getLogger("werkzeug")
|
||||
flask_app.logger.removeHandler(default_handler)
|
||||
flask_log.removeHandler(default_handler)
|
||||
|
||||
log_level = conf.log.LOG_LEVELS[loglevel]
|
||||
flask_log.setLevel(log_level)
|
||||
date_format = CONF.logging.date_format
|
||||
flask_log.disabled = True
|
||||
flask_app.logger.disabled = True
|
||||
|
||||
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)
|
||||
flask_log.addHandler(rh)
|
||||
|
||||
log_file = CONF.logging.logfile
|
||||
|
||||
if log_file:
|
||||
log_format = CONF.logging.logformat
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
fh = RotatingFileHandler(
|
||||
log_file, maxBytes=(10248576 * 5),
|
||||
backupCount=4,
|
||||
)
|
||||
fh.setFormatter(log_formatter)
|
||||
flask_log.addHandler(fh)
|
||||
|
||||
|
||||
def init_flask(loglevel, quiet):
|
||||
global socketio
|
||||
|
||||
flask_app = flask.Flask(
|
||||
"aprsd",
|
||||
static_url_path="/static",
|
||||
static_folder="web/admin/static",
|
||||
template_folder="web/admin/templates",
|
||||
)
|
||||
setup_logging(flask_app, loglevel, quiet)
|
||||
server = APRSDFlask()
|
||||
server.set_config()
|
||||
flask_app.route("/", methods=["GET"])(server.index)
|
||||
flask_app.route("/stats", methods=["GET"])(server.stats)
|
||||
flask_app.route("/messages", methods=["GET"])(server.messages)
|
||||
flask_app.route("/packets", methods=["GET"])(server.packets)
|
||||
flask_app.route("/save", methods=["GET"])(server.save)
|
||||
flask_app.route("/plugins", methods=["GET"])(server.plugins)
|
||||
|
||||
socketio = SocketIO(
|
||||
flask_app, logger=False, engineio_logger=False,
|
||||
# async_mode="threading",
|
||||
)
|
||||
# import eventlet
|
||||
# eventlet.monkey_patch()
|
||||
gunicorn_logger = logging.getLogger("gunicorn.error")
|
||||
flask_app.logger.handlers = gunicorn_logger.handlers
|
||||
flask_app.logger.setLevel(gunicorn_logger.level)
|
||||
|
||||
socketio.on_namespace(LoggingNamespace("/logs"))
|
||||
return socketio, flask_app
|
||||
|
||||
|
||||
if __name__ == "aprsd.flask":
|
||||
try:
|
||||
default_config_file = cli_helper.DEFAULT_CONFIG_FILE
|
||||
CONF(
|
||||
[], project="aprsd", version=aprsd.__version__,
|
||||
default_config_files=[default_config_file],
|
||||
)
|
||||
except cfg.ConfigFilesNotFoundError:
|
||||
pass
|
||||
sio, app = init_flask("DEBUG", False)
|
|
@ -0,0 +1,138 @@
|
|||
import logging
|
||||
from logging.handlers import QueueHandler
|
||||
import queue
|
||||
import sys
|
||||
|
||||
from loguru import logger
|
||||
from oslo_config import cfg
|
||||
|
||||
from aprsd.conf import log as conf_log
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
# LOG = logging.getLogger("APRSD")
|
||||
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
|
||||
# to disable log to stdout, but still log to file
|
||||
# use the --quiet option on the cmdln
|
||||
def setup_logging(loglevel=None, quiet=False):
|
||||
if not loglevel:
|
||||
log_level = CONF.logging.log_level
|
||||
else:
|
||||
log_level = conf_log.LOG_LEVELS[loglevel]
|
||||
|
||||
# intercept everything at the root logger
|
||||
logging.root.handlers = [InterceptHandler()]
|
||||
logging.root.setLevel(log_level)
|
||||
|
||||
imap_list = [
|
||||
"imapclient.imaplib", "imaplib", "imapclient",
|
||||
"imapclient.util",
|
||||
]
|
||||
aprslib_list = [
|
||||
"aprslib",
|
||||
"aprslib.parsing",
|
||||
"aprslib.exceptions",
|
||||
]
|
||||
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,
|
||||
},
|
||||
)
|
||||
|
||||
if CONF.email_plugin.enabled and CONF.email_plugin.debug:
|
||||
for name in imap_list:
|
||||
logging.getLogger(name).propagate = True
|
||||
|
||||
if CONF.admin.web_enabled:
|
||||
qh = QueueHandler(logging_queue)
|
||||
handlers.append(
|
||||
{
|
||||
"sink": qh, "serialize": False,
|
||||
"format": CONF.logging.logformat,
|
||||
"level": log_level,
|
||||
"colorize": False,
|
||||
},
|
||||
)
|
||||
|
||||
# configure loguru
|
||||
logger.configure(handlers=handlers)
|
||||
logger.level("DEBUG", color="<fg #BABABA>")
|
|
@ -1,89 +0,0 @@
|
|||
import logging
|
||||
from logging import NullHandler
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import queue
|
||||
import sys
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from aprsd import conf
|
||||
from aprsd.logging import rich as aprsd_logging
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger("APRSD")
|
||||
logging_queue = queue.Queue()
|
||||
|
||||
|
||||
# Setup the logging faciility
|
||||
# to disable logging to stdout, but still log to file
|
||||
# use the --quiet option on the cmdln
|
||||
def setup_logging(loglevel, quiet):
|
||||
log_level = conf.log.LOG_LEVELS[loglevel]
|
||||
LOG.setLevel(log_level)
|
||||
date_format = CONF.logging.date_format
|
||||
rh = None
|
||||
fh = None
|
||||
|
||||
rich_logging = False
|
||||
if CONF.logging.get("rich_logging", False) and not quiet:
|
||||
log_format = "%(message)s"
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
rh = aprsd_logging.APRSDRichHandler(
|
||||
show_thread=True, thread_width=20,
|
||||
rich_tracebacks=True, omit_repeated_times=False,
|
||||
)
|
||||
rh.setFormatter(log_formatter)
|
||||
LOG.addHandler(rh)
|
||||
rich_logging = True
|
||||
|
||||
log_file = CONF.logging.logfile
|
||||
log_format = CONF.logging.logformat
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
|
||||
if log_file:
|
||||
fh = RotatingFileHandler(log_file, maxBytes=(10248576 * 5), backupCount=4)
|
||||
fh.setFormatter(log_formatter)
|
||||
LOG.addHandler(fh)
|
||||
|
||||
imap_logger = None
|
||||
if CONF.email_plugin.enabled and CONF.email_plugin.debug:
|
||||
imap_logger = logging.getLogger("imapclient.imaplib")
|
||||
imap_logger.setLevel(log_level)
|
||||
if rh:
|
||||
imap_logger.addHandler(rh)
|
||||
if fh:
|
||||
imap_logger.addHandler(fh)
|
||||
|
||||
if CONF.admin.web_enabled:
|
||||
qh = logging.handlers.QueueHandler(logging_queue)
|
||||
q_log_formatter = logging.Formatter(
|
||||
fmt=CONF.logging.logformat,
|
||||
datefmt=CONF.logging.date_format,
|
||||
)
|
||||
qh.setFormatter(q_log_formatter)
|
||||
LOG.addHandler(qh)
|
||||
|
||||
if not quiet and not rich_logging:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
LOG.addHandler(sh)
|
||||
if imap_logger:
|
||||
imap_logger.addHandler(sh)
|
||||
|
||||
|
||||
def setup_logging_no_config(loglevel, quiet):
|
||||
log_level = conf.log.LOG_LEVELS[loglevel]
|
||||
LOG.setLevel(log_level)
|
||||
log_format = CONF.logging.logformat
|
||||
date_format = CONF.logging.date_format
|
||||
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||
fh = NullHandler()
|
||||
|
||||
fh.setFormatter(log_formatter)
|
||||
LOG.addHandler(fh)
|
||||
|
||||
if not quiet:
|
||||
sh = logging.StreamHandler(sys.stdout)
|
||||
sh.setFormatter(log_formatter)
|
||||
LOG.addHandler(sh)
|
|
@ -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): logging 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,55 +24,46 @@ import datetime
|
|||
import importlib.metadata as imp
|
||||
from importlib.metadata import version as metadata_version
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
import click
|
||||
import click_completion
|
||||
from oslo_config import cfg, generator
|
||||
|
||||
# local imports here
|
||||
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
|
||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
||||
# log.basicConfig(level=log.DEBUG) # level=10
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger("APRSD")
|
||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
flask_enabled = False
|
||||
rpc_serv = None
|
||||
|
||||
|
||||
def custom_startswith(string, incomplete):
|
||||
"""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.group(cls=cli_helper.AliasedGroup, context_settings=CONTEXT_SETTINGS)
|
||||
@click.version_option()
|
||||
@click.pass_context
|
||||
def cli(ctx):
|
||||
pass
|
||||
|
||||
|
||||
def load_commands():
|
||||
from .cmds import ( # noqa
|
||||
completion, dev, fetch_stats, healthcheck, list_plugins, listen,
|
||||
send_message, server, webchat,
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
# First import all the possible commands for the CLI
|
||||
# The commands themselves live in the cmds directory
|
||||
from .cmds import ( # noqa
|
||||
completion, dev, healthcheck, list_plugins, listen, send_message,
|
||||
server, webchat,
|
||||
)
|
||||
load_commands()
|
||||
utils.load_entry_points("aprsd.extension")
|
||||
cli(auto_envvar_prefix="APRSD")
|
||||
|
||||
|
||||
|
@ -91,7 +82,8 @@ def signal_handler(sig, frame):
|
|||
packets.PacketTrack().save()
|
||||
packets.WatchList().save()
|
||||
packets.SeenList().save()
|
||||
LOG.info(stats.APRSDStats())
|
||||
packets.PacketList().save()
|
||||
LOG.info(collector.Collector().collect())
|
||||
# signal.signal(signal.SIGTERM, sys.exit(0))
|
||||
# sys.exit(0)
|
||||
|
||||
|
@ -117,15 +109,25 @@ def check_version(ctx):
|
|||
def sample_config(ctx):
|
||||
"""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():
|
||||
args = []
|
||||
|
||||
all = imp.entry_points()
|
||||
selected = []
|
||||
if "oslo.config.opts" in all:
|
||||
for x in all["oslo.config.opts"]:
|
||||
if x.group == "oslo.config.opts":
|
||||
selected.append(x)
|
||||
# selected = imp.entry_points(group="oslo.config.opts")
|
||||
selected = _get_selected_entry_points()
|
||||
for entry in selected:
|
||||
if "aprsd" in entry.name:
|
||||
args.append("--namespace")
|
||||
|
@ -146,6 +148,7 @@ def sample_config(ctx):
|
|||
raise SystemExit
|
||||
raise
|
||||
generator.generate(conf)
|
||||
return
|
||||
|
||||
|
||||
@cli.command()
|
|
@ -1,6 +1,7 @@
|
|||
from aprsd.packets.core import ( # noqa: F401
|
||||
AckPacket, GPSPacket, MessagePacket, MicEPacket, Packet, PathPacket,
|
||||
RejectPacket, StatusPacket, WeatherPacket,
|
||||
AckPacket, BeaconPacket, BulletinPacket, GPSPacket, MessagePacket,
|
||||
MicEPacket, ObjectPacket, Packet, RejectPacket, StatusPacket,
|
||||
ThirdPartyPacket, UnknownPacket, WeatherPacket, factory,
|
||||
)
|
||||
from aprsd.packets.packet_list import PacketList # 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,61 +1,116 @@
|
|||
from collections import OrderedDict
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from oslo_config import cfg
|
||||
import wrapt
|
||||
|
||||
from aprsd import stats, utils
|
||||
from aprsd.packets import seen_list
|
||||
from aprsd.packets import collector, core
|
||||
from aprsd.utils import objectstore
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class PacketList:
|
||||
"""Class to track all of the packets rx'd and tx'd by aprsd."""
|
||||
|
||||
class PacketList(objectstore.ObjectStoreMixin):
|
||||
"""Class to keep track of the packets we tx/rx."""
|
||||
_instance = None
|
||||
lock = threading.Lock()
|
||||
|
||||
packet_list: utils.RingBuffer = utils.RingBuffer(1000)
|
||||
|
||||
_total_rx: int = 0
|
||||
_total_tx: int = 0
|
||||
maxlen: int = 100
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance.maxlen = CONF.packet_list_maxlen
|
||||
cls._instance._init_data()
|
||||
return cls._instance
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def __iter__(self):
|
||||
return iter(self.packet_list)
|
||||
def _init_data(self):
|
||||
self.data = {
|
||||
"types": {},
|
||||
"packets": OrderedDict(),
|
||||
}
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def rx(self, packet):
|
||||
def rx(self, packet: type[core.Packet]):
|
||||
"""Add a packet that was received."""
|
||||
self._total_rx += 1
|
||||
self.packet_list.append(packet)
|
||||
seen_list.SeenList().update_seen(packet)
|
||||
stats.APRSDStats().rx(packet)
|
||||
with self.lock:
|
||||
self._total_rx += 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]["rx"] += 1
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def tx(self, packet):
|
||||
def tx(self, packet: type[core.Packet]):
|
||||
"""Add a packet that was received."""
|
||||
self._total_tx += 1
|
||||
self.packet_list.append(packet)
|
||||
seen_list.SeenList().update_seen(packet)
|
||||
stats.APRSDStats().tx(packet)
|
||||
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 get(self):
|
||||
return self.packet_list.get()
|
||||
def add(self, packet):
|
||||
with self.lock:
|
||||
self._add(packet)
|
||||
|
||||
def _add(self, 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 find(self, packet):
|
||||
with self.lock:
|
||||
return self.data["packets"][packet.key]
|
||||
|
||||
def __len__(self):
|
||||
with self.lock:
|
||||
return len(self.data["packets"])
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def total_rx(self):
|
||||
return self._total_rx
|
||||
with self.lock:
|
||||
return self._total_rx
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
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 logging
|
||||
import threading
|
||||
|
||||
from oslo_config import cfg
|
||||
import wrapt
|
||||
|
||||
from aprsd.packets import collector, core
|
||||
from aprsd.utils import objectstore
|
||||
|
||||
|
||||
|
@ -16,28 +15,40 @@ class SeenList(objectstore.ObjectStoreMixin):
|
|||
"""Global callsign seen list."""
|
||||
|
||||
_instance = None
|
||||
lock = threading.Lock()
|
||||
data: dict = {}
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance._init_store()
|
||||
cls._instance.data = {}
|
||||
return cls._instance
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def update_seen(self, packet):
|
||||
callsign = None
|
||||
if packet.from_call:
|
||||
callsign = packet.from_call
|
||||
else:
|
||||
LOG.warning(f"Can't find FROM in packet {packet}")
|
||||
return
|
||||
if callsign not in self.data:
|
||||
self.data[callsign] = {
|
||||
"last": None,
|
||||
"count": 0,
|
||||
}
|
||||
self.data[callsign]["last"] = str(datetime.datetime.now())
|
||||
self.data[callsign]["count"] += 1
|
||||
def stats(self, serializable=False):
|
||||
"""Return the stats for the PacketTrack class."""
|
||||
with self.lock:
|
||||
return self.data
|
||||
|
||||
def rx(self, packet: type[core.Packet]):
|
||||
"""When we get a packet from the network, update the seen list."""
|
||||
with self.lock:
|
||||
callsign = None
|
||||
if packet.from_call:
|
||||
callsign = packet.from_call
|
||||
else:
|
||||
LOG.warning(f"Can't find FROM in packet {packet}")
|
||||
return
|
||||
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 threading
|
||||
import logging
|
||||
|
||||
from oslo_config import cfg
|
||||
import wrapt
|
||||
|
||||
from aprsd.threads import tx
|
||||
from aprsd.packets import collector, core
|
||||
from aprsd.utils import objectstore
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class PacketTrack(objectstore.ObjectStoreMixin):
|
||||
|
@ -26,7 +26,6 @@ class PacketTrack(objectstore.ObjectStoreMixin):
|
|||
|
||||
_instance = None
|
||||
_start_time = None
|
||||
lock = threading.Lock()
|
||||
|
||||
data: dict = {}
|
||||
total_tracked: int = 0
|
||||
|
@ -38,81 +37,73 @@ class PacketTrack(objectstore.ObjectStoreMixin):
|
|||
cls._instance._init_store()
|
||||
return cls._instance
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def __getitem__(self, name):
|
||||
return self.data[name]
|
||||
with self.lock:
|
||||
return self.data[name]
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def __iter__(self):
|
||||
return iter(self.data)
|
||||
with self.lock:
|
||||
return iter(self.data)
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def keys(self):
|
||||
return self.data.keys()
|
||||
with self.lock:
|
||||
return self.data.keys()
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def items(self):
|
||||
return self.data.items()
|
||||
with self.lock:
|
||||
return self.data.items()
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def values(self):
|
||||
return self.data.values()
|
||||
with self.lock:
|
||||
return self.data.values()
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def __len__(self):
|
||||
return len(self.data)
|
||||
def stats(self, serializable=False):
|
||||
with self.lock:
|
||||
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 __str__(self):
|
||||
result = "{"
|
||||
for key in self.data.keys():
|
||||
result += f"{key}: {str(self.data[key])}, "
|
||||
result += "}"
|
||||
return result
|
||||
def rx(self, packet: type[core.Packet]) -> None:
|
||||
"""When we get a packet from the network, check if we should remove it."""
|
||||
if isinstance(packet, core.AckPacket):
|
||||
self._remove(packet.msgNo)
|
||||
elif isinstance(packet, core.RejectPacket):
|
||||
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 add(self, packet):
|
||||
key = int(packet.msgNo)
|
||||
self.data[key] = packet
|
||||
self.total_tracked += 1
|
||||
def tx(self, packet: type[core.Packet]) -> None:
|
||||
"""Add a packet that was sent."""
|
||||
with self.lock:
|
||||
key = packet.msgNo
|
||||
packet.send_count = 0
|
||||
self.data[key] = packet
|
||||
self.total_tracked += 1
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def get(self, id):
|
||||
if id in self.data:
|
||||
return self.data[id]
|
||||
def remove(self, key):
|
||||
self._remove(key)
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def remove(self, id):
|
||||
key = int(id)
|
||||
if key in self.data.keys():
|
||||
del self.data[key]
|
||||
def _remove(self, key):
|
||||
with self.lock:
|
||||
try:
|
||||
del self.data[key]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def restart(self):
|
||||
"""Walk the list of messages and restart them if any."""
|
||||
for key in self.data.keys():
|
||||
pkt = self.data[key]
|
||||
if pkt.last_send_attempt < pkt.retry_count:
|
||||
tx.send(pkt)
|
||||
|
||||
def _resend(self, packet):
|
||||
packet._last_send_attempt = 0
|
||||
tx.send(packet)
|
||||
|
||||
def restart_delayed(self, count=None, most_recent=True):
|
||||
"""Walk the list of delayed messages and restart them if any."""
|
||||
if not count:
|
||||
# Send all the delayed messages
|
||||
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)
|
||||
# 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(PacketTrack)
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import datetime
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from oslo_config import cfg
|
||||
import wrapt
|
||||
|
||||
from aprsd import utils
|
||||
from aprsd.packets import collector, core
|
||||
from aprsd.utils import objectstore
|
||||
|
||||
|
||||
|
@ -17,56 +16,75 @@ class WatchList(objectstore.ObjectStoreMixin):
|
|||
"""Global watch list and info for callsigns."""
|
||||
|
||||
_instance = None
|
||||
lock = threading.Lock()
|
||||
data = {}
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance._init_store()
|
||||
cls._instance.data = {}
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, config=None):
|
||||
ring_size = CONF.watch_list.packet_keep_count
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._update_from_conf()
|
||||
|
||||
if CONF.watch_list.callsigns:
|
||||
for callsign in CONF.watch_list.callsigns:
|
||||
call = callsign.replace("*", "")
|
||||
# FIXME(waboring) - we should fetch the last time we saw
|
||||
# a beacon from a callsign or some other mechanism to find
|
||||
# last time a message was seen by aprs-is. For now this
|
||||
# is all we can do.
|
||||
self.data[call] = {
|
||||
"last": datetime.datetime.now(),
|
||||
"packets": utils.RingBuffer(
|
||||
ring_size,
|
||||
),
|
||||
def _update_from_conf(self, config=None):
|
||||
with self.lock:
|
||||
if CONF.watch_list.enabled and CONF.watch_list.callsigns:
|
||||
for callsign in CONF.watch_list.callsigns:
|
||||
call = callsign.replace("*", "")
|
||||
# FIXME(waboring) - we should fetch the last time we saw
|
||||
# a beacon from a callsign or some other mechanism to find
|
||||
# last time a message was seen by aprs-is. For now this
|
||||
# is all we can do.
|
||||
if call not in self.data:
|
||||
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):
|
||||
return CONF.watch_list.enabled
|
||||
|
||||
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):
|
||||
self.data[callsign]["last"] = datetime.datetime.now()
|
||||
self.data[callsign]["packets"].append(packet)
|
||||
with self.lock:
|
||||
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):
|
||||
if self.callsign_in_watchlist(callsign):
|
||||
return self.data[callsign]["last"]
|
||||
with self.lock:
|
||||
if self.callsign_in_watchlist(callsign):
|
||||
return self.data[callsign]["last"]
|
||||
|
||||
def age(self, callsign):
|
||||
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):
|
||||
if not seconds:
|
||||
|
@ -83,14 +101,22 @@ class WatchList(objectstore.ObjectStoreMixin):
|
|||
We put this here so any notification plugin can use this
|
||||
same test.
|
||||
"""
|
||||
if not self.callsign_in_watchlist(callsign):
|
||||
return False
|
||||
|
||||
age = self.age(callsign)
|
||||
if age:
|
||||
delta = utils.parse_delta_str(age)
|
||||
d = datetime.timedelta(**delta)
|
||||
|
||||
delta = utils.parse_delta_str(age)
|
||||
d = datetime.timedelta(**delta)
|
||||
max_delta = self.max_delta(seconds=seconds)
|
||||
|
||||
max_delta = self.max_delta(seconds=seconds)
|
||||
|
||||
if d > max_delta:
|
||||
return True
|
||||
if d > max_delta:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
collector.PacketCollector().register(WatchList)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
# The base plugin class
|
||||
from __future__ import annotations
|
||||
|
||||
import abc
|
||||
import importlib
|
||||
import inspect
|
||||
|
@ -42,7 +43,7 @@ class APRSDPluginSpec:
|
|||
"""A hook specification namespace."""
|
||||
|
||||
@hookspec
|
||||
def filter(self, packet: packets.core.Packet):
|
||||
def filter(self, packet: type[packets.Packet]):
|
||||
"""My special little hook that you can customize."""
|
||||
|
||||
|
||||
|
@ -65,7 +66,7 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
|||
self.threads = self.create_threads() or []
|
||||
self.start_threads()
|
||||
|
||||
def start_threads(self):
|
||||
def start_threads(self) -> None:
|
||||
if self.enabled and self.threads:
|
||||
if not isinstance(self.threads, list):
|
||||
self.threads = [self.threads]
|
||||
|
@ -90,10 +91,10 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
|||
)
|
||||
|
||||
@property
|
||||
def message_count(self):
|
||||
def message_count(self) -> int:
|
||||
return self.message_counter
|
||||
|
||||
def help(self):
|
||||
def help(self) -> str:
|
||||
return "Help!"
|
||||
|
||||
@abc.abstractmethod
|
||||
|
@ -118,11 +119,11 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
|||
thread.stop()
|
||||
|
||||
@abc.abstractmethod
|
||||
def filter(self, packet: packets.core.Packet):
|
||||
def filter(self, packet: type[packets.Packet]) -> str | packets.MessagePacket:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def process(self, packet: packets.core.Packet):
|
||||
def process(self, packet: type[packets.Packet]):
|
||||
"""This is called when the filter passes."""
|
||||
|
||||
|
||||
|
@ -147,14 +148,14 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
|||
watch_list = CONF.watch_list.callsigns
|
||||
# make sure the timeout is set or this doesn't work
|
||||
if watch_list:
|
||||
aprs_client = client.factory.create().client
|
||||
aprs_client = client.client_factory.create().client
|
||||
filter_str = "b/{}".format("/".join(watch_list))
|
||||
aprs_client.set_filter(filter_str)
|
||||
else:
|
||||
LOG.warning("Watch list enabled, but no callsigns set.")
|
||||
|
||||
@hookimpl
|
||||
def filter(self, packet: packets.core.Packet):
|
||||
def filter(self, packet: type[packets.Packet]) -> str | packets.MessagePacket:
|
||||
result = packets.NULL_MESSAGE
|
||||
if self.enabled:
|
||||
wl = watch_list.WatchList()
|
||||
|
@ -206,14 +207,15 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
|||
self.enabled = True
|
||||
|
||||
@hookimpl
|
||||
def filter(self, packet: packets.core.MessagePacket):
|
||||
def filter(self, packet: packets.MessagePacket) -> str | packets.MessagePacket:
|
||||
LOG.debug(f"{self.__class__.__name__} called")
|
||||
if not self.enabled:
|
||||
result = f"{self.__class__.__name__} isn't enabled"
|
||||
LOG.warning(result)
|
||||
return result
|
||||
|
||||
if not isinstance(packet, packets.core.MessagePacket):
|
||||
LOG.warning(f"Got a {packet.__class__.__name__} ignoring")
|
||||
if not isinstance(packet, packets.MessagePacket):
|
||||
LOG.warning(f"{self.__class__.__name__} Got a {packet.__class__.__name__} ignoring")
|
||||
return packets.NULL_MESSAGE
|
||||
|
||||
result = None
|
||||
|
@ -225,7 +227,7 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
|||
# and is an APRS message format and has a message.
|
||||
if (
|
||||
tocall == CONF.callsign
|
||||
and isinstance(packet, packets.core.MessagePacket)
|
||||
and isinstance(packet, packets.MessagePacket)
|
||||
and message
|
||||
):
|
||||
if re.search(self.command_regex, message, re.IGNORECASE):
|
||||
|
@ -268,7 +270,7 @@ class HelpPlugin(APRSDRegexCommandPluginBase):
|
|||
def help(self):
|
||||
return "Help: send APRS help or help <plugin>"
|
||||
|
||||
def process(self, packet: packets.core.MessagePacket):
|
||||
def process(self, packet: packets.MessagePacket):
|
||||
LOG.info("HelpPlugin")
|
||||
# fromcall = packet.get("from")
|
||||
message = packet.message_text
|
||||
|
@ -324,9 +326,6 @@ class PluginManager:
|
|||
# the pluggy PluginManager for all WatchList plugins
|
||||
_watchlist_pm = None
|
||||
|
||||
# aprsd config dict
|
||||
config = None
|
||||
|
||||
lock = None
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
|
@ -335,13 +334,9 @@ class PluginManager:
|
|||
cls._instance = super().__new__(cls)
|
||||
# Put any initialization here.
|
||||
cls._instance.lock = threading.Lock()
|
||||
cls._instance._init()
|
||||
return cls._instance
|
||||
|
||||
def __init__(self, config=None):
|
||||
self.obj_list = []
|
||||
if config:
|
||||
self.config = config
|
||||
|
||||
def _init(self):
|
||||
self._pluggy_pm = pluggy.PluginManager("aprsd")
|
||||
self._pluggy_pm.add_hookspecs(APRSDPluginSpec)
|
||||
|
@ -349,6 +344,28 @@ class PluginManager:
|
|||
self._watchlist_pm = pluggy.PluginManager("aprsd")
|
||||
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):
|
||||
for c in inspect.getmro(obj):
|
||||
if issubclass(c, APRSDPluginBase):
|
||||
|
@ -374,7 +391,9 @@ class PluginManager:
|
|||
try:
|
||||
module_name, class_name = module_class_string.rsplit(".", 1)
|
||||
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:
|
||||
if not module_name:
|
||||
LOG.error(f"Failed to load Plugin {module_class_string}")
|
||||
|
@ -422,10 +441,10 @@ class PluginManager:
|
|||
self._watchlist_pm.register(plugin_obj)
|
||||
else:
|
||||
LOG.warning(f"Plugin {plugin_obj.__class__.__name__} is disabled")
|
||||
else:
|
||||
elif isinstance(plugin_obj, APRSDRegexCommandPluginBase):
|
||||
if plugin_obj.enabled:
|
||||
LOG.info(
|
||||
"Registering plugin '{}'({}) -- {}".format(
|
||||
"Registering Regex plugin '{}'({}) -- {}".format(
|
||||
plugin_name,
|
||||
plugin_obj.version,
|
||||
plugin_obj.command_regex,
|
||||
|
@ -434,6 +453,17 @@ class PluginManager:
|
|||
self._pluggy_pm.register(plugin_obj)
|
||||
else:
|
||||
LOG.warning(f"Plugin {plugin_obj.__class__.__name__} is disabled")
|
||||
elif isinstance(plugin_obj, APRSDPluginBase):
|
||||
if plugin_obj.enabled:
|
||||
LOG.info(
|
||||
"Registering Base plugin '{}'({})".format(
|
||||
plugin_name,
|
||||
plugin_obj.version,
|
||||
),
|
||||
)
|
||||
self._pluggy_pm.register(plugin_obj)
|
||||
else:
|
||||
LOG.warning(f"Plugin {plugin_obj.__class__.__name__} is disabled")
|
||||
except Exception as ex:
|
||||
LOG.error(f"Couldn't load plugin '{plugin_name}'")
|
||||
LOG.exception(ex)
|
||||
|
@ -443,14 +473,14 @@ class PluginManager:
|
|||
del self._pluggy_pm
|
||||
self.setup_plugins()
|
||||
|
||||
def setup_plugins(self):
|
||||
def setup_plugins(self, load_help_plugin=True):
|
||||
"""Create the plugin manager and register plugins."""
|
||||
|
||||
LOG.info("Loading APRSD Plugins")
|
||||
self._init()
|
||||
# Help plugin is always enabled.
|
||||
_help = HelpPlugin()
|
||||
self._pluggy_pm.register(_help)
|
||||
if load_help_plugin:
|
||||
_help = HelpPlugin()
|
||||
self._pluggy_pm.register(_help)
|
||||
|
||||
enabled_plugins = CONF.enabled_plugins
|
||||
if enabled_plugins:
|
||||
|
@ -464,12 +494,12 @@ class PluginManager:
|
|||
|
||||
LOG.info("Completed Plugin Loading.")
|
||||
|
||||
def run(self, packet: packets.core.MessagePacket):
|
||||
"""Execute all the pluguns run method."""
|
||||
def run(self, packet: packets.MessagePacket):
|
||||
"""Execute all the plugins run method."""
|
||||
with self.lock:
|
||||
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:
|
||||
return self._watchlist_pm.hook.filter(packet=packet)
|
||||
|
||||
|
@ -482,7 +512,8 @@ class PluginManager:
|
|||
|
||||
def register_msg(self, obj):
|
||||
"""Register the plugin."""
|
||||
self._pluggy_pm.register(obj)
|
||||
with self.lock:
|
||||
self._pluggy_pm.register(obj)
|
||||
|
||||
def get_plugins(self):
|
||||
plugin_list = []
|
||||
|
|
|
@ -33,9 +33,9 @@ def get_weather_gov_for_gps(lat, lon):
|
|||
)
|
||||
try:
|
||||
url2 = (
|
||||
# "https://forecast.weather.gov/MapClick.php?lat=%s"
|
||||
# "&lon=%s&FcstType=json" % (lat, lon)
|
||||
f"https://api.weather.gov/points/{lat},{lon}"
|
||||
"https://forecast.weather.gov/MapClick.php?lat=%s"
|
||||
"&lon=%s&FcstType=json" % (lat, lon)
|
||||
# f"https://api.weather.gov/points/{lat},{lon}"
|
||||
)
|
||||
LOG.debug(f"Fetching weather '{url2}'")
|
||||
response = requests.get(url2, headers=headers)
|
||||
|
@ -76,6 +76,7 @@ def fetch_openweathermap(api_key, lat, lon, units="metric", exclude=None):
|
|||
exclude,
|
||||
)
|
||||
)
|
||||
LOG.debug(f"Fetching OWM weather '{url}'")
|
||||
response = requests.get(url)
|
||||
except Exception as e:
|
||||
LOG.error(e)
|
||||
|
|
|
@ -11,7 +11,7 @@ import time
|
|||
import imapclient
|
||||
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.utils import trace
|
||||
|
||||
|
@ -60,6 +60,38 @@ class EmailInfo:
|
|||
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):
|
||||
"""Email Plugin."""
|
||||
|
||||
|
@ -190,10 +222,6 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
|||
def _imap_connect():
|
||||
imap_port = CONF.email_plugin.imap_port
|
||||
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:
|
||||
server = imapclient.IMAPClient(
|
||||
|
@ -440,7 +468,7 @@ def send_email(to_addr, content):
|
|||
[to_addr],
|
||||
msg.as_string(),
|
||||
)
|
||||
stats.APRSDStats().email_tx_inc()
|
||||
EmailStats().tx_inc()
|
||||
except Exception:
|
||||
LOG.exception("Sendmail Error!!!!")
|
||||
server.quit()
|
||||
|
@ -545,7 +573,7 @@ class APRSDEmailThread(threads.APRSDThread):
|
|||
|
||||
def loop(self):
|
||||
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
|
||||
# This allows CTRL-C to stop the execution of this loop sooner
|
||||
# than check_email_delay time
|
||||
|
@ -666,7 +694,7 @@ class APRSDEmailThread(threads.APRSDThread):
|
|||
EmailInfo().delay = 60
|
||||
|
||||
# reset clock
|
||||
LOG.debug("Done looping over Server.fetch, logging out.")
|
||||
LOG.debug("Done looping over Server.fetch, log out.")
|
||||
self.past = datetime.datetime.now()
|
||||
try:
|
||||
server.logout()
|
||||
|
|
|
@ -8,6 +8,8 @@ from aprsd.utils import trace
|
|||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
DEFAULT_FORTUNE_PATH = '/usr/games/fortune'
|
||||
|
||||
|
||||
class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
|
||||
"""Fortune."""
|
||||
|
@ -19,7 +21,8 @@ class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
|
|||
fortune_path = None
|
||||
|
||||
def setup(self):
|
||||
self.fortune_path = shutil.which("fortune")
|
||||
self.fortune_path = shutil.which(DEFAULT_FORTUNE_PATH)
|
||||
LOG.info(f"Fortune path {self.fortune_path}")
|
||||
if not self.fortune_path:
|
||||
self.enabled = False
|
||||
else:
|
||||
|
|
|
@ -2,6 +2,8 @@ import logging
|
|||
import re
|
||||
import time
|
||||
|
||||
from geopy.geocoders import ArcGIS, AzureMaps, Baidu, Bing, GoogleV3
|
||||
from geopy.geocoders import HereV7, Nominatim, OpenCage, TomTom, What3WordsV3, Woosmap
|
||||
from oslo_config import cfg
|
||||
|
||||
from aprsd import packets, plugin, plugin_utils
|
||||
|
@ -12,6 +14,82 @@ CONF = cfg.CONF
|
|||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class UsLocation:
|
||||
raw = {}
|
||||
|
||||
def __init__(self, info):
|
||||
self.info = info
|
||||
|
||||
def __str__(self):
|
||||
return self.info
|
||||
|
||||
|
||||
class USGov:
|
||||
"""US Government geocoder that uses the geopy API.
|
||||
|
||||
This is a dummy class the implements the geopy reverse API,
|
||||
so the factory can return an object that conforms to the API.
|
||||
"""
|
||||
def reverse(self, coordinates):
|
||||
"""Reverse geocode a coordinate."""
|
||||
LOG.info(f"USGov reverse geocode {coordinates}")
|
||||
coords = coordinates.split(",")
|
||||
lat = float(coords[0])
|
||||
lon = float(coords[1])
|
||||
result = plugin_utils.get_weather_gov_for_gps(lat, lon)
|
||||
# LOG.info(f"WEATHER: {result}")
|
||||
# LOG.info(f"area description {result['location']['areaDescription']}")
|
||||
if 'location' in result:
|
||||
loc = UsLocation(result['location']['areaDescription'])
|
||||
else:
|
||||
loc = UsLocation("Unknown Location")
|
||||
|
||||
LOG.info(f"USGov reverse geocode LOC {loc}")
|
||||
return loc
|
||||
|
||||
|
||||
def geopy_factory():
|
||||
"""Factory function for geopy geocoders."""
|
||||
geocoder = CONF.location_plugin.geopy_geocoder
|
||||
LOG.info(f"Using geocoder: {geocoder}")
|
||||
user_agent = CONF.location_plugin.user_agent
|
||||
LOG.info(f"Using user_agent: {user_agent}")
|
||||
|
||||
if geocoder == "Nominatim":
|
||||
return Nominatim(user_agent=user_agent)
|
||||
elif geocoder == "USGov":
|
||||
return USGov()
|
||||
elif geocoder == "ArcGIS":
|
||||
return ArcGIS(
|
||||
username=CONF.location_plugin.arcgis_username,
|
||||
password=CONF.location_plugin.arcgis_password,
|
||||
user_agent=user_agent,
|
||||
)
|
||||
elif geocoder == "AzureMaps":
|
||||
return AzureMaps(
|
||||
user_agent=user_agent,
|
||||
subscription_key=CONF.location_plugin.azuremaps_subscription_key,
|
||||
)
|
||||
elif geocoder == "Baidu":
|
||||
return Baidu(user_agent=user_agent, api_key=CONF.location_plugin.baidu_api_key)
|
||||
elif geocoder == "Bing":
|
||||
return Bing(user_agent=user_agent, api_key=CONF.location_plugin.bing_api_key)
|
||||
elif geocoder == "GoogleV3":
|
||||
return GoogleV3(user_agent=user_agent, api_key=CONF.location_plugin.google_api_key)
|
||||
elif geocoder == "HERE":
|
||||
return HereV7(user_agent=user_agent, api_key=CONF.location_plugin.here_api_key)
|
||||
elif geocoder == "OpenCage":
|
||||
return OpenCage(user_agent=user_agent, api_key=CONF.location_plugin.opencage_api_key)
|
||||
elif geocoder == "TomTom":
|
||||
return TomTom(user_agent=user_agent, api_key=CONF.location_plugin.tomtom_api_key)
|
||||
elif geocoder == "What3Words":
|
||||
return What3WordsV3(user_agent=user_agent, api_key=CONF.location_plugin.what3words_api_key)
|
||||
elif geocoder == "Woosmap":
|
||||
return Woosmap(user_agent=user_agent, api_key=CONF.location_plugin.woosmap_api_key)
|
||||
else:
|
||||
raise ValueError(f"Unknown geocoder: {geocoder}")
|
||||
|
||||
|
||||
class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
||||
"""Location!"""
|
||||
|
||||
|
@ -50,8 +128,33 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
|||
LOG.error("Didn't get any entries from aprs.fi")
|
||||
return "Failed to fetch aprs.fi location"
|
||||
|
||||
lat = aprs_data["entries"][0]["lat"]
|
||||
lon = aprs_data["entries"][0]["lng"]
|
||||
lat = float(aprs_data["entries"][0]["lat"])
|
||||
lon = float(aprs_data["entries"][0]["lng"])
|
||||
|
||||
# Get some information about their location
|
||||
try:
|
||||
tic = time.perf_counter()
|
||||
geolocator = geopy_factory()
|
||||
LOG.info(f"Using GEOLOCATOR: {geolocator}")
|
||||
coordinates = f"{lat:0.6f}, {lon:0.6f}"
|
||||
location = geolocator.reverse(coordinates)
|
||||
address = location.raw.get("address")
|
||||
LOG.debug(f"GEOLOCATOR address: {address}")
|
||||
toc = time.perf_counter()
|
||||
if address:
|
||||
LOG.info(f"Geopy address {address} took {toc - tic:0.4f}")
|
||||
if address.get("country_code") == "us":
|
||||
area_info = f"{address.get('county')}, {address.get('state')}"
|
||||
else:
|
||||
# what to do for address for non US?
|
||||
area_info = f"{address.get('country'), 'Unknown'}"
|
||||
else:
|
||||
area_info = str(location)
|
||||
except Exception as ex:
|
||||
LOG.error(ex)
|
||||
LOG.error(f"Failed to fetch Geopy address {ex}")
|
||||
area_info = "Unknown Location"
|
||||
|
||||
try: # altitude not always provided
|
||||
alt = float(aprs_data["entries"][0]["altitude"])
|
||||
except Exception:
|
||||
|
@ -64,22 +167,12 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
|||
delta_seconds = time.time() - int(aprs_lasttime_seconds)
|
||||
delta_hours = delta_seconds / 60 / 60
|
||||
|
||||
try:
|
||||
wx_data = plugin_utils.get_weather_gov_for_gps(lat, lon)
|
||||
except Exception as ex:
|
||||
LOG.error(f"Couldn't fetch forecast.weather.gov '{ex}'")
|
||||
wx_data = {"location": {"areaDescription": "Unknown Location"}}
|
||||
|
||||
if "location" not in wx_data:
|
||||
LOG.error(f"Couldn't fetch forecast.weather.gov '{wx_data}'")
|
||||
wx_data = {"location": {"areaDescription": "Unknown Location"}}
|
||||
|
||||
reply = "{}: {} {}' {},{} {}h ago".format(
|
||||
searchcall,
|
||||
wx_data["location"]["areaDescription"],
|
||||
area_info,
|
||||
str(altfeet),
|
||||
str(lat),
|
||||
str(lon),
|
||||
f"{lat:0.2f}",
|
||||
f"{lon:0.2f}",
|
||||
str("%.1f" % round(delta_hours, 1)),
|
||||
).rstrip()
|
||||
|
||||
|
|
|
@ -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 re
|
||||
import time
|
||||
|
||||
from oslo_config import cfg
|
||||
import pytz
|
||||
from tzlocal import get_localzone
|
||||
|
||||
from aprsd import packets, plugin, plugin_utils
|
||||
from aprsd.utils import fuzzy, trace
|
||||
|
@ -22,7 +22,8 @@ class TimePlugin(plugin.APRSDRegexCommandPluginBase):
|
|||
short_description = "What is the current local time."
|
||||
|
||||
def _get_local_tz(self):
|
||||
return pytz.timezone(time.strftime("%Z"))
|
||||
lz = get_localzone()
|
||||
return pytz.timezone(str(lz))
|
||||
|
||||
def _get_utcnow(self):
|
||||
return pytz.datetime.datetime.utcnow()
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import logging
|
||||
|
||||
import aprsd
|
||||
from aprsd import plugin, stats
|
||||
from aprsd import plugin
|
||||
from aprsd.stats import collector
|
||||
|
||||
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
@ -23,10 +24,8 @@ class VersionPlugin(plugin.APRSDRegexCommandPluginBase):
|
|||
# fromcall = packet.get("from")
|
||||
# message = packet.get("message_text", None)
|
||||
# ack = packet.get("msgNo", "0")
|
||||
stats_obj = stats.APRSDStats()
|
||||
s = stats_obj.stats()
|
||||
print(s)
|
||||
s = collector.Collector().collect()
|
||||
return "APRSD ver:{} uptime:{}".format(
|
||||
aprsd.__version__,
|
||||
s["aprsd"]["uptime"],
|
||||
s["APRSDStats"]["uptime"],
|
||||
)
|
||||
|
|
|
@ -26,7 +26,9 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
|
|||
"weather" - returns weather near the calling callsign
|
||||
"""
|
||||
|
||||
command_regex = r"^([w]|[w]\s|[w][x]|[w][x]\s|weather)"
|
||||
# command_regex = r"^([w][x]|[w][x]\s|weather)"
|
||||
command_regex = r"^[wW]"
|
||||
|
||||
command_name = "USWeather"
|
||||
short_description = "Provide USA only weather of GPS Beacon location"
|
||||
|
||||
|
@ -37,11 +39,18 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
|
|||
def process(self, packet):
|
||||
LOG.info("Weather Plugin")
|
||||
fromcall = packet.from_call
|
||||
message = packet.get("message_text", None)
|
||||
# message = packet.get("message_text", None)
|
||||
# ack = packet.get("msgNo", "0")
|
||||
a = re.search(r"^.*\s+(.*)", message)
|
||||
if a is not None:
|
||||
searchcall = a.group(1)
|
||||
searchcall = searchcall.upper()
|
||||
else:
|
||||
searchcall = fromcall
|
||||
api_key = CONF.aprs_fi.apiKey
|
||||
try:
|
||||
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
|
||||
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
|
||||
except Exception as ex:
|
||||
LOG.error(f"Failed to fetch aprs.fi data {ex}")
|
||||
return "Failed to fetch aprs.fi location"
|
||||
|
@ -92,7 +101,7 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
|||
|
||||
"""
|
||||
|
||||
command_regex = r"^([m]|[m]|[m]\s|metar)"
|
||||
command_regex = r"^([m]|[M]|[m]\s|metar)"
|
||||
command_name = "USMetar"
|
||||
short_description = "USA only METAR of GPS Beacon location"
|
||||
|
||||
|
@ -101,7 +110,6 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
|||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
print("FISTY")
|
||||
fromcall = packet.get("from")
|
||||
message = packet.get("message_text", None)
|
||||
# ack = packet.get("msgNo", "0")
|
||||
|
@ -182,7 +190,9 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
|||
|
||||
"""
|
||||
|
||||
command_regex = r"^([w]|[w]\s|[w][x]|[w][x]\s|weather)"
|
||||
# command_regex = r"^([w][x]|[w][x]\s|weather)"
|
||||
command_regex = r"^[wW]"
|
||||
|
||||
command_name = "OpenWeatherMap"
|
||||
short_description = "OpenWeatherMap weather of GPS Beacon location"
|
||||
|
||||
|
@ -204,7 +214,7 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
|||
|
||||
@trace.trace
|
||||
def process(self, packet):
|
||||
fromcall = packet.get("from")
|
||||
fromcall = packet.get("from_call")
|
||||
message = packet.get("message_text", None)
|
||||
# ack = packet.get("msgNo", "0")
|
||||
LOG.info(f"OWMWeather Plugin '{message}'")
|
||||
|
|
|
@ -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,150 +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
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
self._check_settings()
|
||||
self.get_rpc_client()
|
||||
|
||||
def _check_settings(self):
|
||||
if not CONF.rpc_settings.enabled:
|
||||
LOG.error("RPC is not enabled, no way to get stats!!")
|
||||
|
||||
if CONF.rpc_settings.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")
|
||||
|
||||
def _rpyc_connect(
|
||||
self, host, port,
|
||||
service=rpyc.VoidService,
|
||||
config={}, ipv6=False,
|
||||
keepalive=False, authorizer=None,
|
||||
):
|
||||
|
||||
print(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}")
|
||||
return None
|
||||
|
||||
def get_rpc_client(self):
|
||||
if not self._rpc_client:
|
||||
magic = CONF.rpc_settings.magic_word
|
||||
self._rpc_client = self._rpyc_connect(
|
||||
CONF.rpc_settings.ip,
|
||||
CONF.rpc_settings.port,
|
||||
authorizer=lambda sock: sock.send(magic.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,90 +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):
|
||||
magic = sock.recv(len(CONF.rpc_settings.magic_word)).decode()
|
||||
if magic != CONF.rpc_settings.magic_word:
|
||||
raise AuthenticationError(f"wrong magic word {magic}")
|
||||
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("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("Disconnected")
|
||||
self._conn = None
|
||||
|
||||
@rpyc.exposed
|
||||
def get_stats(self):
|
||||
stat = stats.APRSDStats()
|
||||
stats_dict = stat.stats()
|
||||
return json.dumps(stats_dict, indent=4, sort_keys=True, default=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)
|
253
aprsd/stats.py
253
aprsd/stats.py
|
@ -1,253 +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
|
||||
|
||||
_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)
|
||||
@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),
|
||||
"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": {
|
||||
"tracked": int(pl.total_tx() + pl.total_rx()),
|
||||
"sent": int(pl.total_tx()),
|
||||
"received": int(pl.total_rx()),
|
||||
},
|
||||
"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
|
||||
# aprsd.threads
|
||||
from .aprsd import APRSDThread, APRSDThreadList # noqa: F401
|
||||
from .keep_alive import KeepAliveThread # noqa: F401
|
||||
from .rx import APRSDRXThread # noqa: F401
|
||||
from .rx import ( # noqa: F401
|
||||
APRSDDupeRXThread, APRSDProcessPacketThread, APRSDRXThread,
|
||||
)
|
||||
|
||||
|
||||
packet_queue = queue.Queue(maxsize=20)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import abc
|
||||
import datetime
|
||||
import logging
|
||||
import threading
|
||||
from typing import List
|
||||
|
||||
import wrapt
|
||||
|
||||
|
@ -8,12 +10,59 @@ import wrapt
|
|||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
|
||||
class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
||||
"""Base class for all threads in APRSD."""
|
||||
|
||||
loop_count = 1
|
||||
|
||||
def __init__(self, name):
|
||||
super().__init__(name=name)
|
||||
self.thread_stop = False
|
||||
APRSDThreadList().add(self)
|
||||
self._last_loop = datetime.datetime.now()
|
||||
|
||||
def _should_quit(self):
|
||||
""" see if we have a quit message from the global queue."""
|
||||
if self.thread_stop:
|
||||
return True
|
||||
|
||||
def stop(self):
|
||||
self.thread_stop = True
|
||||
|
||||
@abc.abstractmethod
|
||||
def loop(self):
|
||||
pass
|
||||
|
||||
def _cleanup(self):
|
||||
"""Add code to subclass to do any cleanup"""
|
||||
|
||||
def __str__(self):
|
||||
out = f"Thread <{self.__class__.__name__}({self.name}) Alive? {self.is_alive()}>"
|
||||
return out
|
||||
|
||||
def loop_age(self):
|
||||
"""How old is the last loop call?"""
|
||||
return datetime.datetime.now() - self._last_loop
|
||||
|
||||
def run(self):
|
||||
LOG.debug("Starting")
|
||||
while not self._should_quit():
|
||||
self.loop_count += 1
|
||||
can_loop = self.loop()
|
||||
self._last_loop = datetime.datetime.now()
|
||||
if not can_loop:
|
||||
self.stop()
|
||||
self._cleanup()
|
||||
APRSDThreadList().remove(self)
|
||||
LOG.debug("Exiting")
|
||||
|
||||
|
||||
class APRSDThreadList:
|
||||
"""Singleton class that keeps track of application wide threads."""
|
||||
|
||||
_instance = None
|
||||
|
||||
threads_list = []
|
||||
threads_list: List[APRSDThread] = []
|
||||
lock = threading.Lock()
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
|
@ -22,6 +71,21 @@ class APRSDThreadList:
|
|||
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)
|
||||
|
@ -39,39 +103,17 @@ class APRSDThreadList:
|
|||
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)
|
||||
|
||||
|
||||
class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
||||
|
||||
def __init__(self, name):
|
||||
super().__init__(name=name)
|
||||
self.thread_stop = False
|
||||
APRSDThreadList().add(self)
|
||||
|
||||
def _should_quit(self):
|
||||
""" see if we have a quit message from the global queue."""
|
||||
if self.thread_stop:
|
||||
return True
|
||||
|
||||
def stop(self):
|
||||
self.thread_stop = True
|
||||
|
||||
@abc.abstractmethod
|
||||
def loop(self):
|
||||
pass
|
||||
|
||||
def _cleanup(self):
|
||||
"""Add code to subclass to do any cleanup"""
|
||||
|
||||
def run(self):
|
||||
LOG.debug("Starting")
|
||||
while not self._should_quit():
|
||||
can_loop = self.loop()
|
||||
if not can_loop:
|
||||
self.stop()
|
||||
self._cleanup()
|
||||
APRSDThreadList().remove(self)
|
||||
LOG.debug("Exiting")
|
||||
|
|
|
@ -5,7 +5,10 @@ import tracemalloc
|
|||
|
||||
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
|
||||
|
||||
|
||||
|
@ -24,66 +27,98 @@ class KeepAliveThread(APRSDThread):
|
|||
self.max_delta = datetime.timedelta(**max_timeout)
|
||||
|
||||
def loop(self):
|
||||
if self.cntr % 60 == 0:
|
||||
pkt_tracker = packets.PacketTrack()
|
||||
stats_obj = stats.APRSDStats()
|
||||
if self.loop_count % 60 == 0:
|
||||
stats_json = collector.Collector().collect()
|
||||
pl = packets.PacketList()
|
||||
thread_list = APRSDThreadList()
|
||||
now = datetime.datetime.now()
|
||||
last_email = stats_obj.email_thread_time
|
||||
if last_email:
|
||||
email_thread_time = utils.strfdelta(now - last_email)
|
||||
|
||||
if "EmailStats" in stats_json:
|
||||
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:
|
||||
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()
|
||||
stats_obj.set_memory(current)
|
||||
stats_obj.set_memory_peak(peak)
|
||||
|
||||
login = CONF.callsign
|
||||
|
||||
tracked_packets = len(pkt_tracker)
|
||||
tracked_packets = stats_json["PacketTrack"]["total_tracked"]
|
||||
tx_msg = 0
|
||||
rx_msg = 0
|
||||
if "PacketList" in stats_json:
|
||||
msg_packets = stats_json["PacketList"].get("MessagePacket")
|
||||
if msg_packets:
|
||||
tx_msg = msg_packets.get("tx", 0)
|
||||
rx_msg = msg_packets.get("rx", 0)
|
||||
|
||||
keepalive = (
|
||||
"{} - Uptime {} RX:{} TX:{} Tracker:{} Msgs TX:{} RX:{} "
|
||||
"Last:{} Email: {} - RAM Current:{} Peak:{} Threads:{}"
|
||||
"Last:{} Email: {} - RAM Current:{} Peak:{} Threads:{} LoggingQueue:{}"
|
||||
).format(
|
||||
login,
|
||||
utils.strfdelta(stats_obj.uptime),
|
||||
stats_json["APRSDStats"]["callsign"],
|
||||
stats_json["APRSDStats"]["uptime"],
|
||||
pl.total_rx(),
|
||||
pl.total_tx(),
|
||||
tracked_packets,
|
||||
stats_obj._pkt_cnt["MessagePacket"]["tx"],
|
||||
stats_obj._pkt_cnt["MessagePacket"]["rx"],
|
||||
tx_msg,
|
||||
rx_msg,
|
||||
last_msg_time,
|
||||
email_thread_time,
|
||||
utils.human_size(current),
|
||||
utils.human_size(peak),
|
||||
stats_json["APRSDStats"]["memory_current_str"],
|
||||
stats_json["APRSDStats"]["memory_peak_str"],
|
||||
len(thread_list),
|
||||
aprsd_log.logging_queue.qsize(),
|
||||
)
|
||||
LOG.info(keepalive)
|
||||
if "APRSDThreadList" in stats_json:
|
||||
thread_list = stats_json["APRSDThreadList"]
|
||||
for thread_name in thread_list:
|
||||
thread = thread_list[thread_name]
|
||||
alive = thread["alive"]
|
||||
age = thread["age"]
|
||||
key = thread["name"]
|
||||
if not alive:
|
||||
LOG.error(f"Thread {thread}")
|
||||
LOG.info(f"{key: <15} Alive? {str(alive): <5} {str(age): <20}")
|
||||
|
||||
# See if we should reset the aprs-is client
|
||||
# Due to losing a keepalive from them
|
||||
delta_dict = utils.parse_delta_str(last_msg_time)
|
||||
delta = datetime.timedelta(**delta_dict)
|
||||
# check the APRS connection
|
||||
cl = client_factory.create()
|
||||
# Reset the connection if it's dead and this isn't our
|
||||
# First time through the loop.
|
||||
# The first time through the loop can happen at startup where
|
||||
# The keepalive thread starts before the client has a chance
|
||||
# to make it's connection the first time.
|
||||
if not cl.is_alive() and self.cntr > 0:
|
||||
LOG.error(f"{cl.__class__.__name__} is not alive!!! Resetting")
|
||||
client_factory.create().reset()
|
||||
# else:
|
||||
# # See if we should reset the aprs-is client
|
||||
# # Due to losing a keepalive from them
|
||||
# delta_dict = utils.parse_delta_str(last_msg_time)
|
||||
# delta = datetime.timedelta(**delta_dict)
|
||||
#
|
||||
# if delta > self.max_delta:
|
||||
# # We haven't gotten a keepalive from aprs-is in a while
|
||||
# # reset the connection.a
|
||||
# if not client.KISSClient.is_enabled():
|
||||
# LOG.warning(f"Resetting connection to APRS-IS {delta}")
|
||||
# client.factory.create().reset()
|
||||
|
||||
if delta > self.max_delta:
|
||||
# We haven't gotten a keepalive from aprs-is in a while
|
||||
# reset the connection.a
|
||||
if not client.KISSClient.is_enabled():
|
||||
LOG.warning(f"Resetting connection to APRS-IS {delta}")
|
||||
client.factory.create().reset()
|
||||
|
||||
# Check version every hour
|
||||
# Check version every day
|
||||
delta = now - self.checker_time
|
||||
if delta > datetime.timedelta(hours=1):
|
||||
if delta > datetime.timedelta(hours=24):
|
||||
self.checker_time = now
|
||||
level, msg = utils._check_version()
|
||||
if level:
|
||||
LOG.warning(msg)
|
||||
self.cntr += 1
|
||||
self.cntr += 1
|
||||
time.sleep(1)
|
||||
return True
|
||||
|
|
|
@ -1,25 +1,54 @@
|
|||
import datetime
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from oslo_config import cfg
|
||||
import requests
|
||||
import wrapt
|
||||
|
||||
from aprsd import threads
|
||||
from aprsd.logging import log
|
||||
from aprsd.log import log
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
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:
|
||||
entries = []
|
||||
lock = threading.Lock()
|
||||
_instance = None
|
||||
last_purge = datetime.datetime.now()
|
||||
max_delta = datetime.timedelta(
|
||||
hours=0.0, minutes=0, seconds=2,
|
||||
)
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def stats(self) -> dict:
|
||||
return {
|
||||
"log_entries": self.entries,
|
||||
}
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def add(self, entry):
|
||||
self.entries.append(entry)
|
||||
|
@ -28,8 +57,18 @@ class LogEntries:
|
|||
def get_all_and_purge(self):
|
||||
entries = self.entries.copy()
|
||||
self.entries = []
|
||||
self.last_purge = datetime.datetime.now()
|
||||
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)
|
||||
def __len__(self):
|
||||
return len(self.entries)
|
||||
|
@ -40,6 +79,10 @@ class LogMonitorThread(threads.APRSDThread):
|
|||
def __init__(self):
|
||||
super().__init__("LogMonitorThread")
|
||||
|
||||
def stop(self):
|
||||
send_log_entries(force=True)
|
||||
super().stop()
|
||||
|
||||
def loop(self):
|
||||
try:
|
||||
record = log.logging_queue.get(block=True, timeout=2)
|
||||
|
@ -54,6 +97,7 @@ class LogMonitorThread(threads.APRSDThread):
|
|||
# Just ignore thi
|
||||
pass
|
||||
|
||||
send_log_entries()
|
||||
return True
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
|
||||
|
@ -16,15 +19,20 @@ LOG = logging.getLogger("APRSD")
|
|||
|
||||
class APRSDRXThread(APRSDThread):
|
||||
def __init__(self, packet_queue):
|
||||
super().__init__("RX_MSG")
|
||||
super().__init__("RX_PKT")
|
||||
self.packet_queue = packet_queue
|
||||
self._client = client.factory.create()
|
||||
self._client = client_factory.create()
|
||||
|
||||
def stop(self):
|
||||
self.thread_stop = True
|
||||
client.factory.create().client.stop()
|
||||
if self._client:
|
||||
self._client.stop()
|
||||
|
||||
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
|
||||
try:
|
||||
# This will register a packet consumer with aprslib
|
||||
|
@ -36,41 +44,100 @@ class APRSDRXThread(APRSDThread):
|
|||
# and the aprslib developer didn't want to allow a PR to add
|
||||
# kwargs. :(
|
||||
# https://github.com/rossengeorgiev/aprs-python/pull/56
|
||||
self._client.client.consumer(
|
||||
self.process_packet, raw=False, blocking=False,
|
||||
self._client.consumer(
|
||||
self._process_packet, raw=False, blocking=False,
|
||||
)
|
||||
|
||||
except (
|
||||
aprslib.exceptions.ConnectionDrop,
|
||||
aprslib.exceptions.ConnectionError,
|
||||
):
|
||||
LOG.error("Connection dropped, reconnecting")
|
||||
time.sleep(5)
|
||||
# Force the deletion of the client object connected to aprs
|
||||
# This will cause a reconnect, next time client.get_client()
|
||||
# is called
|
||||
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
|
||||
return True
|
||||
|
||||
def _process_packet(self, *args, **kwargs):
|
||||
"""Intermediate callback so we can update the keepalive time."""
|
||||
# Now call the 'real' packet processing for a RX'x packet
|
||||
self.process_packet(*args, **kwargs)
|
||||
|
||||
@abc.abstractmethod
|
||||
def process_packet(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class APRSDPluginRXThread(APRSDRXThread):
|
||||
class APRSDDupeRXThread(APRSDRXThread):
|
||||
"""Process received packets.
|
||||
|
||||
This is the main APRSD Server command thread that
|
||||
receives packets from APRIS and then sends them for
|
||||
processing in the PluginProcessPacketThread.
|
||||
receives packets and makes sure the packet
|
||||
hasn't been seen previously before sending it on
|
||||
to be processed.
|
||||
"""
|
||||
|
||||
def process_packet(self, *args, **kwargs):
|
||||
"""This handles the processing of an inbound packet.
|
||||
|
||||
When a packet is received by the connected client object,
|
||||
it sends the raw packet into this function. This function then
|
||||
decodes the packet via the client, and then processes the packet.
|
||||
Ack Packets are sent to the PluginProcessPacketThread for processing.
|
||||
All other packets have to be checked as a dupe, and then only after
|
||||
we haven't seen this packet before, do we send it to the
|
||||
PluginProcessPacketThread for processing.
|
||||
"""
|
||||
packet = self._client.decode_packet(*args, **kwargs)
|
||||
# LOG.debug(raw)
|
||||
packet.log(header="RX")
|
||||
packets.PacketList().rx(packet)
|
||||
self.packet_queue.put(packet)
|
||||
packet_log.log(packet)
|
||||
pkt_list = packets.PacketList()
|
||||
|
||||
if isinstance(packet, packets.AckPacket):
|
||||
# We don't need to drop AckPackets, those should be
|
||||
# processed.
|
||||
self.packet_queue.put(packet)
|
||||
else:
|
||||
# Make sure we aren't re-processing the same packet
|
||||
# For RF based APRS Clients we can get duplicate packets
|
||||
# So we need to track them and not process the dupes.
|
||||
found = False
|
||||
try:
|
||||
# Find the packet in the list of already seen packets
|
||||
# Based on the packet.key
|
||||
found = pkt_list.find(packet)
|
||||
except KeyError:
|
||||
found = False
|
||||
|
||||
if not found:
|
||||
# We haven't seen this packet before, so we process it.
|
||||
collector.PacketCollector().rx(packet)
|
||||
self.packet_queue.put(packet)
|
||||
elif packet.timestamp - found.timestamp < CONF.packet_dupe_timeout:
|
||||
# If the packet came in within N seconds of the
|
||||
# Last time seeing the packet, then we drop it as a dupe.
|
||||
LOG.warning(f"Packet {packet.from_call}:{packet.msgNo} already tracked, dropping.")
|
||||
else:
|
||||
LOG.warning(
|
||||
f"Packet {packet.from_call}:{packet.msgNo} already tracked "
|
||||
f"but older than {CONF.packet_dupe_timeout} seconds. processing.",
|
||||
)
|
||||
collector.PacketCollector().rx(packet)
|
||||
self.packet_queue.put(packet)
|
||||
|
||||
|
||||
class APRSDPluginRXThread(APRSDDupeRXThread):
|
||||
""""Process received packets.
|
||||
|
||||
For backwards compatibility, we keep the APRSDPluginRXThread.
|
||||
"""
|
||||
|
||||
|
||||
class APRSDProcessPacketThread(APRSDThread):
|
||||
|
@ -84,21 +151,24 @@ class APRSDProcessPacketThread(APRSDThread):
|
|||
def __init__(self, packet_queue):
|
||||
self.packet_queue = packet_queue
|
||||
super().__init__("ProcessPKT")
|
||||
self._loop_cnt = 1
|
||||
|
||||
def process_ack_packet(self, packet):
|
||||
"""We got an ack for a message, no need to resend it."""
|
||||
ack_num = packet.msgNo
|
||||
LOG.info(f"Got ack for message {ack_num}")
|
||||
pkt_tracker = packets.PacketTrack()
|
||||
pkt_tracker.remove(ack_num)
|
||||
LOG.debug(f"Got ack for message {ack_num}")
|
||||
collector.PacketCollector().rx(packet)
|
||||
|
||||
def process_piggyback_ack(self, packet):
|
||||
"""We got an ack embedded in a packet."""
|
||||
ack_num = packet.ackMsgNo
|
||||
LOG.debug(f"Got PiggyBackAck for message {ack_num}")
|
||||
collector.PacketCollector().rx(packet)
|
||||
|
||||
def process_reject_packet(self, packet):
|
||||
"""We got a reject message for a packet. Stop sending the message."""
|
||||
ack_num = packet.msgNo
|
||||
LOG.info(f"Got REJECT for message {ack_num}")
|
||||
pkt_tracker = packets.PacketTrack()
|
||||
pkt_tracker.remove(ack_num)
|
||||
LOG.debug(f"Got REJECT for message {ack_num}")
|
||||
collector.PacketCollector().rx(packet)
|
||||
|
||||
def loop(self):
|
||||
try:
|
||||
|
@ -107,12 +177,11 @@ class APRSDProcessPacketThread(APRSDThread):
|
|||
self.process_packet(packet)
|
||||
except queue.Empty:
|
||||
pass
|
||||
self._loop_cnt += 1
|
||||
return True
|
||||
|
||||
def process_packet(self, packet):
|
||||
"""Process a packet received from aprs-is server."""
|
||||
LOG.debug(f"RXPKT-LOOP {self._loop_cnt}")
|
||||
LOG.debug(f"ProcessPKT-LOOP {self.loop_count}")
|
||||
our_call = CONF.callsign.lower()
|
||||
|
||||
from_call = packet.from_call
|
||||
|
@ -124,8 +193,6 @@ class APRSDProcessPacketThread(APRSDThread):
|
|||
|
||||
# We don't put ack packets destined for us through the
|
||||
# plugins.
|
||||
wl = packets.WatchList()
|
||||
wl.update_seen(packet)
|
||||
if (
|
||||
isinstance(packet, packets.AckPacket)
|
||||
and packet.addresse.lower() == our_call
|
||||
|
@ -137,6 +204,10 @@ class APRSDProcessPacketThread(APRSDThread):
|
|||
):
|
||||
self.process_reject_packet(packet)
|
||||
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
|
||||
if isinstance(packet, packets.MessagePacket):
|
||||
if to_call and to_call.lower() == our_call:
|
||||
|
@ -159,7 +230,7 @@ class APRSDProcessPacketThread(APRSDThread):
|
|||
self.process_other_packet(
|
||||
packet, for_us=(to_call.lower() == our_call),
|
||||
)
|
||||
LOG.debug("Packet processing complete")
|
||||
LOG.debug(f"Packet processing complete for pkt '{packet.key}'")
|
||||
return False
|
||||
|
||||
@abc.abstractmethod
|
||||
|
@ -213,9 +284,7 @@ class APRSDPluginProcessPacketThread(APRSDProcessPacketThread):
|
|||
to_call = packet.addresse
|
||||
else:
|
||||
to_call = None
|
||||
# msg = packet.get("message_text", None)
|
||||
# packet.get("msgNo", "0")
|
||||
# packet.get("response", None)
|
||||
|
||||
pm = plugin.PluginManager()
|
||||
try:
|
||||
results = pm.run(packet)
|
||||
|
|
|
@ -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,35 +1,94 @@
|
|||
import datetime
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
from aprsd import client
|
||||
from oslo_config import cfg
|
||||
from rush import quota, throttle
|
||||
from rush.contrib import decorator
|
||||
from rush.limiters import periodic
|
||||
from rush.stores import dictionary
|
||||
import wrapt
|
||||
|
||||
from aprsd import conf # noqa
|
||||
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
|
||||
LOG = logging.getLogger("APRSD")
|
||||
|
||||
msg_t = throttle.Throttle(
|
||||
limiter=periodic.PeriodicLimiter(
|
||||
store=dictionary.DictionaryStore(),
|
||||
),
|
||||
rate=quota.Quota.per_second(
|
||||
count=CONF.msg_rate_limit_period,
|
||||
),
|
||||
)
|
||||
ack_t = throttle.Throttle(
|
||||
limiter=periodic.PeriodicLimiter(
|
||||
store=dictionary.DictionaryStore(),
|
||||
),
|
||||
rate=quota.Quota.per_second(
|
||||
count=CONF.ack_rate_limit_period,
|
||||
),
|
||||
)
|
||||
|
||||
msg_throttle_decorator = decorator.ThrottleDecorator(throttle=msg_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):
|
||||
"""Send a packet either in a thread or directly to the client."""
|
||||
# prepare the packet for sending.
|
||||
# This constructs the packet.raw
|
||||
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):
|
||||
_send_ack(packet, direct=direct, aprs_client=aprs_client)
|
||||
else:
|
||||
_send_packet(packet, direct=direct, aprs_client=aprs_client)
|
||||
|
||||
|
||||
@msg_throttle_decorator.sleep_and_retry
|
||||
def _send_packet(packet: core.Packet, direct=False, aprs_client=None):
|
||||
if not direct:
|
||||
if isinstance(packet, core.AckPacket):
|
||||
thread = SendAckThread(packet=packet)
|
||||
else:
|
||||
thread = SendPacketThread(packet=packet)
|
||||
thread = SendPacketThread(packet=packet)
|
||||
thread.start()
|
||||
else:
|
||||
if aprs_client:
|
||||
cl = aprs_client
|
||||
else:
|
||||
cl = client.factory.create()
|
||||
_send_direct(packet, aprs_client=aprs_client)
|
||||
|
||||
packet.update_timestamp()
|
||||
packet.log(header="TX")
|
||||
|
||||
@ack_throttle_decorator.sleep_and_retry
|
||||
def _send_ack(packet: core.AckPacket, direct=False, aprs_client=None):
|
||||
if not direct:
|
||||
thread = SendAckThread(packet=packet)
|
||||
thread.start()
|
||||
else:
|
||||
_send_direct(packet, aprs_client=aprs_client)
|
||||
|
||||
|
||||
def _send_direct(packet, aprs_client=None):
|
||||
if aprs_client:
|
||||
cl = aprs_client
|
||||
else:
|
||||
cl = client_factory.create()
|
||||
|
||||
packet.update_timestamp()
|
||||
packet_log.log(packet, tx=True)
|
||||
try:
|
||||
cl.send(packet)
|
||||
except Exception as e:
|
||||
LOG.error(f"Failed to send packet: {packet}")
|
||||
LOG.error(e)
|
||||
|
||||
|
||||
class SendPacketThread(aprsd_threads.APRSDThread):
|
||||
|
@ -37,10 +96,7 @@ class SendPacketThread(aprsd_threads.APRSDThread):
|
|||
|
||||
def __init__(self, packet):
|
||||
self.packet = packet
|
||||
name = self.packet.raw[:5]
|
||||
super().__init__(f"TXPKT-{self.packet.msgNo}-{name}")
|
||||
pkt_tracker = tracker.PacketTrack()
|
||||
pkt_tracker.add(packet)
|
||||
super().__init__(f"TX-{packet.to_call}-{self.packet.msgNo}")
|
||||
|
||||
def loop(self):
|
||||
"""Loop until a message is acked or it gets delayed.
|
||||
|
@ -66,7 +122,7 @@ class SendPacketThread(aprsd_threads.APRSDThread):
|
|||
return False
|
||||
else:
|
||||
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
|
||||
# TODO(hemna) - Need to put this in a delayed queue?
|
||||
LOG.info(
|
||||
|
@ -75,17 +131,16 @@ class SendPacketThread(aprsd_threads.APRSDThread):
|
|||
"Message Send Complete. Max attempts reached"
|
||||
f" {packet.retry_count}",
|
||||
)
|
||||
if not packet.allow_delay:
|
||||
pkt_tracker.remove(packet.msgNo)
|
||||
pkt_tracker.remove(packet.msgNo)
|
||||
return False
|
||||
|
||||
# Message is still outstanding and needs to be acked.
|
||||
if packet.last_send_time:
|
||||
# Message has a last send time tracking
|
||||
now = datetime.datetime.now()
|
||||
now = int(round(time.time()))
|
||||
sleeptime = (packet.send_count + 1) * 31
|
||||
delta = now - packet.last_send_time
|
||||
if delta > datetime.timedelta(seconds=sleeptime):
|
||||
if delta > sleeptime:
|
||||
# It's time to try to send it again
|
||||
send_now = True
|
||||
else:
|
||||
|
@ -94,8 +149,8 @@ class SendPacketThread(aprsd_threads.APRSDThread):
|
|||
if send_now:
|
||||
# no attempt time, so lets send it, and start
|
||||
# tracking the time.
|
||||
packet.last_send_time = datetime.datetime.now()
|
||||
send(packet, direct=True)
|
||||
packet.last_send_time = int(round(time.time()))
|
||||
_send_direct(packet)
|
||||
packet.send_count += 1
|
||||
|
||||
time.sleep(1)
|
||||
|
@ -106,34 +161,36 @@ class SendPacketThread(aprsd_threads.APRSDThread):
|
|||
|
||||
class SendAckThread(aprsd_threads.APRSDThread):
|
||||
loop_count: int = 1
|
||||
max_retries = 3
|
||||
|
||||
def __init__(self, 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):
|
||||
"""Separate thread to send acks with retries."""
|
||||
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
|
||||
# TODO(hemna) - Need to put this in a delayed queue?
|
||||
LOG.info(
|
||||
LOG.debug(
|
||||
f"{self.packet.__class__.__name__}"
|
||||
f"({self.packet.msgNo}) "
|
||||
"Send Complete. Max attempts reached"
|
||||
f" {self.packet.retry_count}",
|
||||
f" {self.max_retries}",
|
||||
)
|
||||
return False
|
||||
|
||||
if self.packet.last_send_time:
|
||||
# Message has a last send time tracking
|
||||
now = datetime.datetime.now()
|
||||
now = int(round(time.time()))
|
||||
|
||||
# aprs duplicate detection is 30 secs?
|
||||
# (21 only sends first, 28 skips middle)
|
||||
sleep_time = 31
|
||||
delta = now - self.packet.last_send_time
|
||||
if delta > datetime.timedelta(seconds=sleep_time):
|
||||
if delta > sleep_time:
|
||||
# It's time to try to send it again
|
||||
send_now = True
|
||||
elif self.loop_count % 10 == 0:
|
||||
|
@ -142,10 +199,57 @@ class SendAckThread(aprsd_threads.APRSDThread):
|
|||
send_now = True
|
||||
|
||||
if send_now:
|
||||
send(self.packet, direct=True)
|
||||
_send_direct(self.packet)
|
||||
self.packet.send_count += 1
|
||||
self.packet.last_send_time = datetime.datetime.now()
|
||||
self.packet.last_send_time = int(round(time.time()))
|
||||
|
||||
time.sleep(1)
|
||||
self.loop_count += 1
|
||||
return True
|
||||
|
||||
|
||||
class BeaconSendThread(aprsd_threads.APRSDThread):
|
||||
"""Thread that sends a GPS beacon packet periodically.
|
||||
|
||||
Settings are in the [DEFAULT] section of the config file.
|
||||
"""
|
||||
_loop_cnt: int = 1
|
||||
|
||||
def __init__(self):
|
||||
super().__init__("BeaconSendThread")
|
||||
self._loop_cnt = 1
|
||||
# Make sure Latitude and Longitude are set.
|
||||
if not CONF.latitude or not CONF.longitude:
|
||||
LOG.error(
|
||||
"Latitude and Longitude are not set in the config file."
|
||||
"Beacon will not be sent and thread is STOPPED.",
|
||||
)
|
||||
self.stop()
|
||||
LOG.info(
|
||||
"Beacon thread is running and will send "
|
||||
f"beacons every {CONF.beacon_interval} seconds.",
|
||||
)
|
||||
|
||||
def loop(self):
|
||||
# Only dump out the stats every N seconds
|
||||
if self._loop_cnt % CONF.beacon_interval == 0:
|
||||
pkt = core.BeaconPacket(
|
||||
from_call=CONF.callsign,
|
||||
to_call="APRS",
|
||||
latitude=float(CONF.latitude),
|
||||
longitude=float(CONF.longitude),
|
||||
comment="APRSD GPS Beacon",
|
||||
symbol=CONF.beacon_symbol,
|
||||
)
|
||||
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."""
|
||||
|
||||
import errno
|
||||
import functools
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
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:
|
||||
from collections.abc import MutableMapping
|
||||
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):
|
||||
|
@ -126,4 +139,25 @@ def parse_delta_str(s):
|
|||
)
|
||||
else:
|
||||
m = re.match(r"(?P<hours>\d+):(?P<minutes>\d+):(?P<seconds>\d[\.\d+]*)", s)
|
||||
return {key: float(val) for key, val in m.groupdict().items()}
|
||||
|
||||
if m:
|
||||
return {key: float(val) for key, val in m.groupdict().items()}
|
||||
else:
|
||||
return {}
|
||||
|
||||
|
||||
def load_entry_points(group):
|
||||
"""Load all extensions registered to the given entry point group"""
|
||||
try:
|
||||
import importlib_metadata
|
||||
except ImportError:
|
||||
# For python 3.10 and later
|
||||
import importlib.metadata as importlib_metadata
|
||||
|
||||
eps = importlib_metadata.entry_points(group=group)
|
||||
for ep in eps:
|
||||
try:
|
||||
ep.load()
|
||||
except Exception as e:
|
||||
print(f"Extension {ep.name} of group {group} failed to load with {e}", file=sys.stderr)
|
||||
print(traceback.format_exc(), file=sys.stderr)
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
from multiprocessing import RawValue
|
||||
import random
|
||||
import threading
|
||||
|
||||
import wrapt
|
||||
|
||||
|
||||
MAX_PACKET_ID = 9999
|
||||
|
||||
|
||||
class PacketCounter:
|
||||
"""
|
||||
Global Packet id counter class.
|
||||
|
@ -17,19 +21,18 @@ class PacketCounter:
|
|||
"""
|
||||
|
||||
_instance = None
|
||||
max_count = 9999
|
||||
lock = threading.Lock()
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
"""Make this a singleton class."""
|
||||
if cls._instance is None:
|
||||
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
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def increment(self):
|
||||
if self.val.value == self.max_count:
|
||||
if self.val.value == MAX_PACKET_ID:
|
||||
self.val.value = 1
|
||||
else:
|
||||
self.val.value += 1
|
||||
|
@ -37,7 +40,7 @@ class PacketCounter:
|
|||
@property
|
||||
@wrapt.synchronized(lock)
|
||||
def value(self):
|
||||
return self.val.value
|
||||
return str(self.val.value)
|
||||
|
||||
@wrapt.synchronized(lock)
|
||||
def __repr__(self):
|
||||
|
|
|
@ -3,6 +3,8 @@ import decimal
|
|||
import json
|
||||
import sys
|
||||
|
||||
from aprsd.packets import core
|
||||
|
||||
|
||||
class EnhancedJSONEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
|
@ -42,6 +44,24 @@ class EnhancedJSONEncoder(json.JSONEncoder):
|
|||
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):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
|
|
@ -2,6 +2,7 @@ import logging
|
|||
import os
|
||||
import pathlib
|
||||
import pickle
|
||||
import threading
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
|
@ -25,16 +26,28 @@ class ObjectStoreMixin:
|
|||
aprsd server -f (flush) will wipe all saved objects.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.lock = threading.RLock()
|
||||
|
||||
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):
|
||||
with self.lock:
|
||||
return self.data
|
||||
|
||||
def get(self, id):
|
||||
def get(self, key):
|
||||
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):
|
||||
if not CONF.enable_save:
|
||||
|
@ -55,31 +68,26 @@ class ObjectStoreMixin:
|
|||
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):
|
||||
"""Save any queued to disk?"""
|
||||
if not CONF.enable_save:
|
||||
return
|
||||
self._init_store()
|
||||
save_filename = self._save_filename()
|
||||
if len(self) > 0:
|
||||
LOG.info(
|
||||
f"{self.__class__.__name__}::Saving"
|
||||
f" {len(self)} entries to disk at"
|
||||
f"{CONF.save_location}",
|
||||
f" {len(self)} entries to disk at "
|
||||
f"{save_filename}",
|
||||
)
|
||||
with open(self._save_filename(), "wb+") as fp:
|
||||
pickle.dump(self._dump(), fp)
|
||||
with self.lock:
|
||||
with open(save_filename, "wb+") as fp:
|
||||
pickle.dump(self.data, fp)
|
||||
else:
|
||||
LOG.debug(
|
||||
"{} Nothing to save, flushing old save file '{}'".format(
|
||||
self.__class__.__name__,
|
||||
self._save_filename(),
|
||||
save_filename,
|
||||
),
|
||||
)
|
||||
self.flush()
|
||||
|
@ -96,11 +104,14 @@ class ObjectStoreMixin:
|
|||
LOG.debug(
|
||||
f"{self.__class__.__name__}::Loaded {len(self)} entries from disk.",
|
||||
)
|
||||
LOG.debug(f"{self.data}")
|
||||
else:
|
||||
LOG.debug(f"{self.__class__.__name__}::No data to load.")
|
||||
except (pickle.UnpicklingError, Exception) as ex:
|
||||
LOG.error(f"Failed to UnPickle {self._save_filename()}")
|
||||
LOG.error(ex)
|
||||
self.data = {}
|
||||
else:
|
||||
LOG.debug(f"{self.__class__.__name__}::No save file found.")
|
||||
|
||||
def flush(self):
|
||||
"""Nuke the old pickle file that stored the old results from last aprsd run."""
|
||||
|
|
|
@ -1,189 +1,4 @@
|
|||
/* PrismJS 1.24.1
|
||||
https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+log&plugins=show-language+toolbar */
|
||||
/**
|
||||
* prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML
|
||||
* 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;
|
||||
}
|
||||
/* PrismJS 1.29.0
|
||||
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}
|
||||
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}
|
||||
|
|
|
@ -219,15 +219,17 @@ function updateQuadData(chart, label, first, second, third, fourth) {
|
|||
}
|
||||
|
||||
function update_stats( data ) {
|
||||
our_callsign = data["stats"]["aprsd"]["callsign"];
|
||||
$("#version").text( data["stats"]["aprsd"]["version"] );
|
||||
our_callsign = data["APRSDStats"]["callsign"];
|
||||
$("#version").text( data["APRSDStats"]["version"] );
|
||||
$("#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');
|
||||
$("#jsonstats").html(html_pretty);
|
||||
short_time = data["time"].split(/\s(.+)/)[1];
|
||||
updateDualData(packets_chart, short_time, data["stats"]["packets"]["sent"], data["stats"]["packets"]["received"]);
|
||||
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(memory_chart, short_time, data["stats"]["aprsd"]["memory_peak"], data["stats"]["aprsd"]["memory_current"]);
|
||||
packet_list = data["PacketList"]["packets"];
|
||||
updateDualData(packets_chart, short_time, data["PacketList"]["sent"], data["PacketList"]["received"]);
|
||||
updateQuadData(message_chart, short_time, packet_list["MessagePacket"]["tx"], packet_list["MessagePacket"]["rx"],
|
||||
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"]);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,465 @@
|
|||
var packet_list = {};
|
||||
|
||||
var tx_data = [];
|
||||
var rx_data = [];
|
||||
|
||||
var packet_types_data = {};
|
||||
|
||||
var mem_current = []
|
||||
var mem_peak = []
|
||||
|
||||
var thread_current = []
|
||||
|
||||
|
||||
function start_charts() {
|
||||
console.log("start_charts() called");
|
||||
// Initialize the echarts instance based on the prepared dom
|
||||
create_packets_chart();
|
||||
create_packets_types_chart();
|
||||
create_messages_chart();
|
||||
create_ack_chart();
|
||||
create_memory_chart();
|
||||
create_thread_chart();
|
||||
}
|
||||
|
||||
|
||||
function create_packets_chart() {
|
||||
// The packets totals TX/RX chart.
|
||||
pkt_c_canvas = document.getElementById('packetsChart');
|
||||
packets_chart = echarts.init(pkt_c_canvas);
|
||||
|
||||
// Specify the configuration items and data for the chart
|
||||
var option = {
|
||||
title: {
|
||||
text: 'APRS Packet totals'
|
||||
},
|
||||
legend: {},
|
||||
tooltip : {
|
||||
trigger: 'axis'
|
||||
},
|
||||
toolbox: {
|
||||
show : true,
|
||||
feature : {
|
||||
mark : {show: true},
|
||||
dataView : {show: true, readOnly: true},
|
||||
magicType : {show: true, type: ['line', 'bar']},
|
||||
restore : {show: true},
|
||||
saveAsImage : {show: true}
|
||||
}
|
||||
},
|
||||
calculable : true,
|
||||
xAxis: { type: 'time' },
|
||||
yAxis: { },
|
||||
series: [
|
||||
{
|
||||
name: 'tx',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
color: 'red',
|
||||
encode: {
|
||||
x: 'timestamp',
|
||||
y: 'tx' // refer sensor 1 value
|
||||
}
|
||||
},{
|
||||
name: 'rx',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
encode: {
|
||||
x: 'timestamp',
|
||||
y: 'rx'
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
// Display the chart using the configuration items and data just specified.
|
||||
packets_chart.setOption(option);
|
||||
}
|
||||
|
||||
|
||||
function create_packets_types_chart() {
|
||||
// The packets types chart
|
||||
pkt_types_canvas = document.getElementById('packetTypesChart');
|
||||
packet_types_chart = echarts.init(pkt_types_canvas);
|
||||
|
||||
// The series and data are built and updated on the fly
|
||||
// as packets come in.
|
||||
var option = {
|
||||
title: {
|
||||
text: 'Packet Types'
|
||||
},
|
||||
legend: {},
|
||||
tooltip : {
|
||||
trigger: 'axis'
|
||||
},
|
||||
toolbox: {
|
||||
show : true,
|
||||
feature : {
|
||||
mark : {show: true},
|
||||
dataView : {show: true, readOnly: true},
|
||||
magicType : {show: true, type: ['line', 'bar']},
|
||||
restore : {show: true},
|
||||
saveAsImage : {show: true}
|
||||
}
|
||||
},
|
||||
calculable : true,
|
||||
xAxis: { type: 'time' },
|
||||
yAxis: { },
|
||||
}
|
||||
|
||||
packet_types_chart.setOption(option);
|
||||
}
|
||||
|
||||
|
||||
function create_messages_chart() {
|
||||
msg_c_canvas = document.getElementById('messagesChart');
|
||||
message_chart = echarts.init(msg_c_canvas);
|
||||
|
||||
// Specify the configuration items and data for the chart
|
||||
var option = {
|
||||
title: {
|
||||
text: 'Message Packets'
|
||||
},
|
||||
legend: {},
|
||||
tooltip: {
|
||||
trigger: 'axis'
|
||||
},
|
||||
toolbox: {
|
||||
show: true,
|
||||
feature: {
|
||||
mark : {show: true},
|
||||
dataView : {show: true, readOnly: true},
|
||||
magicType : {show: true, type: ['line', 'bar']},
|
||||
restore : {show: true},
|
||||
saveAsImage : {show: true}
|
||||
}
|
||||
},
|
||||
calculable: true,
|
||||
xAxis: { type: 'time' },
|
||||
yAxis: { },
|
||||
series: [
|
||||
{
|
||||
name: 'tx',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
color: 'red',
|
||||
encode: {
|
||||
x: 'timestamp',
|
||||
y: 'tx' // refer sensor 1 value
|
||||
}
|
||||
},{
|
||||
name: 'rx',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
encode: {
|
||||
x: 'timestamp',
|
||||
y: 'rx'
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
// Display the chart using the configuration items and data just specified.
|
||||
message_chart.setOption(option);
|
||||
}
|
||||
|
||||
function create_ack_chart() {
|
||||
ack_canvas = document.getElementById('acksChart');
|
||||
ack_chart = echarts.init(ack_canvas);
|
||||
|
||||
// Specify the configuration items and data for the chart
|
||||
var option = {
|
||||
title: {
|
||||
text: 'Ack Packets'
|
||||
},
|
||||
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: 'tx',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
color: 'red',
|
||||
encode: {
|
||||
x: 'timestamp',
|
||||
y: 'tx' // refer sensor 1 value
|
||||
}
|
||||
},{
|
||||
name: 'rx',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
encode: {
|
||||
x: 'timestamp',
|
||||
y: 'rx'
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
ack_chart.setOption(option);
|
||||
}
|
||||
|
||||
function create_memory_chart() {
|
||||
ack_canvas = document.getElementById('memChart');
|
||||
memory_chart = echarts.init(ack_canvas);
|
||||
|
||||
// Specify the configuration items and data for the chart
|
||||
var option = {
|
||||
title: {
|
||||
text: 'Memory Usage'
|
||||
},
|
||||
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
|
||||
}
|
||||
},{
|
||||
name: 'peak',
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
encode: {
|
||||
x: 'timestamp',
|
||||
y: 'peak'
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function updatePacketData(chart, time, first, second) {
|
||||
tx_data.push([time, first]);
|
||||
rx_data.push([time, second]);
|
||||
option = {
|
||||
series: [
|
||||
{
|
||||
name: 'tx',
|
||||
data: tx_data,
|
||||
},
|
||||
{
|
||||
name: 'rx',
|
||||
data: rx_data,
|
||||
}
|
||||
]
|
||||
}
|
||||
chart.setOption(option);
|
||||
}
|
||||
|
||||
function updatePacketTypesData(time, typesdata) {
|
||||
//The options series is created on the fly each time based on
|
||||
//the packet types we have in the data
|
||||
var series = []
|
||||
|
||||
for (const k in typesdata) {
|
||||
tx = [time, typesdata[k]["tx"]]
|
||||
rx = [time, typesdata[k]["rx"]]
|
||||
|
||||
if (packet_types_data.hasOwnProperty(k)) {
|
||||
packet_types_data[k]["tx"].push(tx)
|
||||
packet_types_data[k]["rx"].push(rx)
|
||||
} else {
|
||||
packet_types_data[k] = {'tx': [tx], 'rx': [rx]}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updatePacketTypesChart() {
|
||||
series = []
|
||||
for (const k in packet_types_data) {
|
||||
entry = {
|
||||
name: k+"tx",
|
||||
data: packet_types_data[k]["tx"],
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
encode: {
|
||||
x: 'timestamp',
|
||||
y: k+'tx' // refer sensor 1 value
|
||||
}
|
||||
}
|
||||
series.push(entry)
|
||||
entry = {
|
||||
name: k+"rx",
|
||||
data: packet_types_data[k]["rx"],
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
encode: {
|
||||
x: 'timestamp',
|
||||
y: k+'rx' // refer sensor 1 value
|
||||
}
|
||||
}
|
||||
series.push(entry)
|
||||
}
|
||||
|
||||
option = {
|
||||
series: series
|
||||
}
|
||||
packet_types_chart.setOption(option);
|
||||
}
|
||||
|
||||
function updateTypeChart(chart, key) {
|
||||
//Generic function to update a packet type chart
|
||||
if (! packet_types_data.hasOwnProperty(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! packet_types_data[key].hasOwnProperty('tx')) {
|
||||
return;
|
||||
}
|
||||
var option = {
|
||||
series: [{
|
||||
name: "tx",
|
||||
data: packet_types_data[key]["tx"],
|
||||
},
|
||||
{
|
||||
name: "rx",
|
||||
data: packet_types_data[key]["rx"]
|
||||
}]
|
||||
}
|
||||
|
||||
chart.setOption(option);
|
||||
}
|
||||
|
||||
function updateMemChart(time, current, peak) {
|
||||
mem_current.push([time, current]);
|
||||
mem_peak.push([time, peak]);
|
||||
option = {
|
||||
series: [
|
||||
{
|
||||
name: 'current',
|
||||
data: mem_current,
|
||||
},
|
||||
{
|
||||
name: 'peak',
|
||||
data: mem_peak,
|
||||
}
|
||||
]
|
||||
}
|
||||
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() {
|
||||
updateTypeChart(message_chart, "MessagePacket")
|
||||
}
|
||||
|
||||
function updateAcksChart() {
|
||||
updateTypeChart(ack_chart, "AckPacket")
|
||||
}
|
||||
|
||||
function update_stats( data ) {
|
||||
console.log("update_stats() echarts.js called")
|
||||
stats = data["stats"];
|
||||
our_callsign = stats["APRSDStats"]["callsign"];
|
||||
$("#version").text( stats["APRSDStats"]["version"] );
|
||||
$("#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');
|
||||
$("#jsonstats").html(html_pretty);
|
||||
|
||||
t = Date.parse(data["time"]);
|
||||
ts = new Date(t);
|
||||
updatePacketData(packets_chart, ts, stats["PacketList"]["tx"], stats["PacketList"]["rx"]);
|
||||
updatePacketTypesData(ts, stats["PacketList"]["types"]);
|
||||
updatePacketTypesChart();
|
||||
updateMessagesChart();
|
||||
updateAcksChart();
|
||||
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"]);
|
||||
//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"]);
|
||||
}
|
|
@ -24,11 +24,15 @@ function ord(str){return str.charCodeAt(0);}
|
|||
|
||||
|
||||
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 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('')
|
||||
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 += "</tbody></table>";
|
||||
|
@ -60,12 +64,16 @@ function update_watchlist_from_packet(callsign, val) {
|
|||
}
|
||||
|
||||
function update_seenlist( data ) {
|
||||
stats = data["stats"];
|
||||
if (stats.hasOwnProperty("SeenList") == false) {
|
||||
return
|
||||
}
|
||||
var seendiv = $("#seenDiv");
|
||||
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 += '<th>Number of packets RX</th></tr></thead><tbody>'
|
||||
seendiv.html('')
|
||||
var seen_list = data["stats"]["aprsd"]["seen_list"]
|
||||
var seen_list = stats["SeenList"]
|
||||
var len = Object.keys(seen_list).length
|
||||
$('#seen_count').html(len)
|
||||
jQuery.each(seen_list, function(i, val) {
|
||||
|
@ -79,6 +87,10 @@ function update_seenlist( data ) {
|
|||
}
|
||||
|
||||
function update_plugins( data ) {
|
||||
stats = data["stats"];
|
||||
if (stats.hasOwnProperty("PluginManager") == false) {
|
||||
return
|
||||
}
|
||||
var plugindiv = $("#pluginDiv");
|
||||
var html_str = '<table class="ui celled striped table"><thead><tr>'
|
||||
html_str += '<th>Plugin Name</th><th>Plugin Enabled?</th>'
|
||||
|
@ -87,7 +99,7 @@ function update_plugins( data ) {
|
|||
html_str += '</tr></thead><tbody>'
|
||||
plugindiv.html('')
|
||||
|
||||
var plugins = data["stats"]["plugins"];
|
||||
var plugins = stats["PluginManager"];
|
||||
var keys = Object.keys(plugins);
|
||||
keys.sort();
|
||||
for (var i=0; i<keys.length; i++) { // now lets iterate in sort order
|
||||
|
@ -101,24 +113,52 @@ function update_plugins( data ) {
|
|||
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 ) {
|
||||
var packetsdiv = $("#packetsDiv");
|
||||
//nuke the contents first, then add to it.
|
||||
if (size_dict(packet_list) == 0 && size_dict(data) > 0) {
|
||||
packetsdiv.html('')
|
||||
}
|
||||
jQuery.each(data, function(i, val) {
|
||||
pkt = JSON.parse(val);
|
||||
jQuery.each(data.packets, function(i, val) {
|
||||
pkt = val;
|
||||
|
||||
update_watchlist_from_packet(pkt['from_call'], pkt);
|
||||
if ( packet_list.hasOwnProperty(pkt.timestamp) == false ) {
|
||||
if ( packet_list.hasOwnProperty(pkt['timestamp']) == false ) {
|
||||
// Store the packet
|
||||
packet_list[pkt.timestamp] = pkt;
|
||||
packet_list[pkt['timestamp']] = pkt;
|
||||
//ts_str = val["timestamp"].toString();
|
||||
//ts = ts_str.split(".")[0]*1000;
|
||||
ts = pkt.timestamp
|
||||
var d = new Date(ts).toLocaleDateString("en-US");
|
||||
var t = new Date(ts).toLocaleTimeString("en-US");
|
||||
ts = pkt['timestamp'] * 1000;
|
||||
var d = new Date(ts).toLocaleDateString();
|
||||
var t = new Date(ts).toLocaleTimeString();
|
||||
var from_call = pkt.from_call;
|
||||
if (from_call == our_callsign) {
|
||||
title_id = 'title_tx';
|
||||
|
@ -167,6 +207,7 @@ function start_update() {
|
|||
update_watchlist(data);
|
||||
update_seenlist(data);
|
||||
update_plugins(data);
|
||||
update_threads(data);
|
||||
},
|
||||
complete: function() {
|
||||
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);
|
|
@ -3,9 +3,10 @@
|
|||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
||||
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
|
||||
<script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.socket.io/4.7.1/socket.io.min.js" integrity="sha512-+NaO7d6gQ1YPxvc/qHIqZEchjGm207SszoNeMgppoqD/67fEqmc1edS8zrbxPD+4RQI3gDgT/83ihpFW61TG/Q==" crossorigin="anonymous"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.bundle.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.js"></script>
|
||||
|
@ -15,7 +16,7 @@
|
|||
<link rel="stylesheet" href="/static/css/prism.css">
|
||||
<script src="/static/js/prism.js"></script>
|
||||
<script src="/static/js/main.js"></script>
|
||||
<script src="/static/js/charts.js"></script>
|
||||
<script src="/static/js/echarts.js"></script>
|
||||
<script src="/static/js/tabs.js"></script>
|
||||
<script src="/static/js/send-message.js"></script>
|
||||
<script src="/static/js/logs.js"></script>
|
||||
|
@ -29,7 +30,6 @@
|
|||
var color = Chart.helpers.color;
|
||||
|
||||
$(document).ready(function() {
|
||||
console.log(initial_stats);
|
||||
start_update();
|
||||
start_charts();
|
||||
init_messages();
|
||||
|
@ -81,8 +81,10 @@
|
|||
<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="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="log-tab">LogFile</div>
|
||||
<!-- <div class="item" data-tab="oslo-tab">OSLO CONFIG</div> //-->
|
||||
<div class="item" data-tab="raw-tab">Raw JSON</div>
|
||||
</div>
|
||||
|
||||
|
@ -92,33 +94,37 @@
|
|||
<div class="ui equal width relaxed grid">
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<div class="ui segment" style="height: 300px">
|
||||
<canvas id="packetsChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="ui segment" style="height: 300px">
|
||||
<canvas id="messageChart"></canvas>
|
||||
</div>
|
||||
<div class="ui segment" style="height: 300px" id="packetsChart"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
<div class="ui segment" style="height: 300px">
|
||||
<canvas id="emailChart"></canvas>
|
||||
</div>
|
||||
<div class="ui segment" style="height: 300px" id="messagesChart"></div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="ui segment" style="height: 300px">
|
||||
<canvas id="memChart"></canvas>
|
||||
</div>
|
||||
<div class="ui segment" style="height: 300px" id="acksChart"></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="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 class="row">
|
||||
<div id="stats" class="two column">
|
||||
<button class="ui button" id="toggleStats">Toggle raw json</button>
|
||||
<pre id="jsonstats" class="language-json">{{ stats }}</pre>
|
||||
</div> --!>
|
||||
</div> //-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -154,6 +160,13 @@
|
|||
<div id="pluginDiv" class="ui mini text">Loading</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">
|
||||
<h3 class="ui dividing header">Config</h3>
|
||||
<pre id="configjson" class="language-json">{{ config_json|safe }}</pre>
|
||||
|
@ -164,9 +177,15 @@
|
|||
<pre id="logContainer" style="height: 600px;overflow-y:auto;overflow-x:auto;"><code id="logtext" class="language-log" ></code></pre>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
<div class="ui bottom attached tab segment" data-tab="oslo-tab">
|
||||
<h3 class="ui dividing header">OSLO</h3>
|
||||
<pre id="osloContainer" style="height:600px;overflow-y:auto;" class="language-json">{{ oslo_out|safe }}</pre>
|
||||
</div> //-->
|
||||
|
||||
<div class="ui bottom attached tab segment" data-tab="raw-tab">
|
||||
<h3 class="ui dividing header">Raw JSON</h3>
|
||||
<pre id="jsonstats" class="language-json">{{ stats|safe }}</pre>
|
||||
<pre id="jsonstats" class="language-yaml" style="height:600px;overflow-y:auto;">{{ initial_stats|safe }}</pre>
|
||||
</div>
|
||||
|
||||
<div class="ui text container">
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
input[type=search]::-webkit-search-cancel-button {
|
||||
-webkit-appearance: searchfield-cancel-button;
|
||||
}
|
||||
|
||||
.speech-wrapper {
|
||||
padding-top: 0px;
|
||||
padding: 5px 30px;
|
||||
background-color: #CCCCCC;
|
||||
}
|
||||
|
||||
.bubble-row {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.bubble-row.alt {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
/*width: 350px; */
|
||||
height: auto;
|
||||
display: block;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
box-shadow: 2px 8px 5px #555;
|
||||
position: relative;
|
||||
margin: 0 0 15px;
|
||||
}
|
||||
|
||||
.bubble.alt {
|
||||
margin: 0 0 15px;
|
||||
}
|
||||
|
||||
.bubble-text {
|
||||
padding: 5px 5px 0px 8px;
|
||||
}
|
||||
|
||||
.bubble-name {
|
||||
width: 280px;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
margin: 0 0 0px;
|
||||
color: #3498db;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.material-symbols-rounded {
|
||||
margin-left: auto;
|
||||
font-weight: normal;
|
||||
color: #808080;
|
||||
}
|
||||
}
|
||||
.bubble-name.alt {
|
||||
color: #2ecc71;
|
||||
}
|
||||
|
||||
.bubble-timestamp {
|
||||
margin-right: auto;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
color: #bbb
|
||||
}
|
||||
|
||||
.bubble-message {
|
||||
font-size: 16px;
|
||||
margin: 0px;
|
||||
padding: 0px 0px 0px 0px;
|
||||
color: #2b2b2b;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.bubble-arrow {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
bottom:30px;
|
||||
left: -16px;
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
.bubble-arrow.alt {
|
||||
right: -2px;
|
||||
bottom: 30px;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
.bubble-arrow:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border: 0 solid transparent;
|
||||
border-top: 9px solid #f5f5f5;
|
||||
border-radius: 0 20px 0;
|
||||
width: 15px;
|
||||
height: 30px;
|
||||
transform: rotate(145deg);
|
||||
}
|
||||
.bubble-arrow.alt:after {
|
||||
transform: rotate(45deg) scaleY(-1);
|
||||
}
|
||||
|
||||
.popover {
|
||||
max-width: 400px;
|
||||
}
|
||||
.popover-header {
|
||||
font-size: 8pt;
|
||||
max-width: 400px;
|
||||
padding: 5px;
|
||||
background-color: #ee;
|
||||
}
|
||||
|
||||
.popover-body {
|
||||
white-space: pre-line;
|
||||
max-width: 400px;
|
||||
padding: 5px;
|
||||
}
|
|
@ -1,36 +1,9 @@
|
|||
body {
|
||||
background: #eeeeee;
|
||||
margin: 2em;
|
||||
/*margin: 1em;*/
|
||||
text-align: center;
|
||||
font-family: system-ui, sans-serif;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 2em;
|
||||
text-align: center;
|
||||
height: 10vh;
|
||||
}
|
||||
|
||||
.ui.segment {
|
||||
background: #eeeeee;
|
||||
}
|
||||
|
||||
ul.list {
|
||||
list-style-type: disc;
|
||||
}
|
||||
ul.list li {
|
||||
list-style-position: outside;
|
||||
}
|
||||
|
||||
#left {
|
||||
margin-right: 2px;
|
||||
height: 300px;
|
||||
}
|
||||
#right {
|
||||
height: 300px;
|
||||
}
|
||||
#center {
|
||||
height: 300px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#title {
|
||||
|
@ -39,6 +12,7 @@ ul.list li {
|
|||
#version{
|
||||
font-size: .5em;
|
||||
}
|
||||
|
||||
#uptime, #aprsis {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
@ -67,28 +41,26 @@ ul.list li {
|
|||
height: 16px;
|
||||
}
|
||||
|
||||
#msgsTabsDiv .ui.tab {
|
||||
margin:0px;
|
||||
padding:0px;
|
||||
display: block;
|
||||
.wc-container {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
height: 100%;
|
||||
}
|
||||
.wc-container .wc-row {
|
||||
/*border: 1px dotted #0313fc;*/
|
||||
padding: 2px;
|
||||
}
|
||||
.wc-container .wc-row.header {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
.wc-container .wc-row.content {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.wc-container .wc-row.footer {
|
||||
flex: 0 1 0px;
|
||||
}
|
||||
|
||||
#msgsTabsDiv .header, .tiny.text, .content, .break,
|
||||
.thumbs.down.outline.icon,
|
||||
.phone.volume.icon
|
||||
{
|
||||
display: inline-block;
|
||||
float: left;
|
||||
position: relative;
|
||||
}
|
||||
#msgsTabsDiv .tiny.text {
|
||||
width:100px;
|
||||
}
|
||||
#msgsTabsDiv .tiny.header {
|
||||
width:100px;
|
||||
text-align: left;
|
||||
}
|
||||
#msgsTabsDiv .break {
|
||||
margin: 2px;
|
||||
text-align: left;
|
||||
.material-symbols-rounded.md-10 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
|
|
@ -37,5 +37,5 @@
|
|||
border: 1px solid #ccc;
|
||||
height: 450px;
|
||||
overflow-y: scroll;
|
||||
background-color: white;
|
||||
background-color: #CCCCCC;
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
Binary file not shown.
|
@ -0,0 +1,23 @@
|
|||
/* fallback */
|
||||
@font-face {
|
||||
font-family: 'Material Symbols Rounded';
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
src: url(/static/css/upstream/font.woff2) format('woff2');
|
||||
}
|
||||
|
||||
.material-symbols-rounded {
|
||||
font-family: 'Material Symbols Rounded';
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
letter-spacing: normal;
|
||||
text-transform: none;
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
word-wrap: normal;
|
||||
direction: ltr;
|
||||
-webkit-font-feature-settings: 'liga';
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* jQuery toast plugin created by Kamran Ahmed copyright MIT license 2014
|
||||
*/
|
||||
.jq-toast-wrap { display: block; position: fixed; width: 250px; pointer-events: none !important; margin: 0; padding: 0; letter-spacing: normal; z-index: 9000 !important; }
|
||||
.jq-toast-wrap * { margin: 0; padding: 0; }
|
||||
|
||||
.jq-toast-wrap.bottom-left { bottom: 20px; left: 20px; }
|
||||
.jq-toast-wrap.bottom-right { bottom: 20px; right: 40px; }
|
||||
.jq-toast-wrap.top-left { top: 20px; left: 20px; }
|
||||
.jq-toast-wrap.top-right { top: 20px; right: 40px; }
|
||||
|
||||
.jq-toast-single { display: block; width: 100%; padding: 10px; margin: 0px 0px 5px; border-radius: 4px; font-size: 12px; font-family: arial, sans-serif; line-height: 17px; position: relative; pointer-events: all !important; background-color: #444444; color: white; }
|
||||
|
||||
.jq-toast-single h2 { font-family: arial, sans-serif; font-size: 14px; margin: 0px 0px 7px; background: none; color: inherit; line-height: inherit; letter-spacing: normal; }
|
||||
.jq-toast-single a { color: #eee; text-decoration: none; font-weight: bold; border-bottom: 1px solid white; padding-bottom: 3px; font-size: 12px; }
|
||||
|
||||
.jq-toast-single ul { margin: 0px 0px 0px 15px; background: none; padding:0px; }
|
||||
.jq-toast-single ul li { list-style-type: disc !important; line-height: 17px; background: none; margin: 0; padding: 0; letter-spacing: normal; }
|
||||
|
||||
.close-jq-toast-single { position: absolute; top: 3px; right: 7px; font-size: 14px; cursor: pointer; }
|
||||
|
||||
.jq-toast-loader { display: block; position: absolute; top: -2px; height: 5px; width: 0%; left: 0; border-radius: 5px; background: red; }
|
||||
.jq-toast-loaded { width: 100%; }
|
||||
.jq-has-icon { padding: 10px 10px 10px 50px; background-repeat: no-repeat; background-position: 10px; }
|
||||
.jq-icon-info { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGwSURBVEhLtZa9SgNBEMc9sUxxRcoUKSzSWIhXpFMhhYWFhaBg4yPYiWCXZxBLERsLRS3EQkEfwCKdjWJAwSKCgoKCcudv4O5YLrt7EzgXhiU3/4+b2ckmwVjJSpKkQ6wAi4gwhT+z3wRBcEz0yjSseUTrcRyfsHsXmD0AmbHOC9Ii8VImnuXBPglHpQ5wwSVM7sNnTG7Za4JwDdCjxyAiH3nyA2mtaTJufiDZ5dCaqlItILh1NHatfN5skvjx9Z38m69CgzuXmZgVrPIGE763Jx9qKsRozWYw6xOHdER+nn2KkO+Bb+UV5CBN6WC6QtBgbRVozrahAbmm6HtUsgtPC19tFdxXZYBOfkbmFJ1VaHA1VAHjd0pp70oTZzvR+EVrx2Ygfdsq6eu55BHYR8hlcki+n+kERUFG8BrA0BwjeAv2M8WLQBtcy+SD6fNsmnB3AlBLrgTtVW1c2QN4bVWLATaIS60J2Du5y1TiJgjSBvFVZgTmwCU+dAZFoPxGEEs8nyHC9Bwe2GvEJv2WXZb0vjdyFT4Cxk3e/kIqlOGoVLwwPevpYHT+00T+hWwXDf4AJAOUqWcDhbwAAAAASUVORK5CYII='); background-color: #31708f; color: #d9edf7; border-color: #bce8f1; }
|
||||
.jq-icon-warning { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAGYSURBVEhL5ZSvTsNQFMbXZGICMYGYmJhAQIJAICYQPAACiSDB8AiICQQJT4CqQEwgJvYASAQCiZiYmJhAIBATCARJy+9rTsldd8sKu1M0+dLb057v6/lbq/2rK0mS/TRNj9cWNAKPYIJII7gIxCcQ51cvqID+GIEX8ASG4B1bK5gIZFeQfoJdEXOfgX4QAQg7kH2A65yQ87lyxb27sggkAzAuFhbbg1K2kgCkB1bVwyIR9m2L7PRPIhDUIXgGtyKw575yz3lTNs6X4JXnjV+LKM/m3MydnTbtOKIjtz6VhCBq4vSm3ncdrD2lk0VgUXSVKjVDJXJzijW1RQdsU7F77He8u68koNZTz8Oz5yGa6J3H3lZ0xYgXBK2QymlWWA+RWnYhskLBv2vmE+hBMCtbA7KX5drWyRT/2JsqZ2IvfB9Y4bWDNMFbJRFmC9E74SoS0CqulwjkC0+5bpcV1CZ8NMej4pjy0U+doDQsGyo1hzVJttIjhQ7GnBtRFN1UarUlH8F3xict+HY07rEzoUGPlWcjRFRr4/gChZgc3ZL2d8oAAAAASUVORK5CYII='); background-color: #8a6d3b; color: #fcf8e3; border-color: #faebcc; }
|
||||
.jq-icon-error { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAHOSURBVEhLrZa/SgNBEMZzh0WKCClSCKaIYOED+AAKeQQLG8HWztLCImBrYadgIdY+gIKNYkBFSwu7CAoqCgkkoGBI/E28PdbLZmeDLgzZzcx83/zZ2SSXC1j9fr+I1Hq93g2yxH4iwM1vkoBWAdxCmpzTxfkN2RcyZNaHFIkSo10+8kgxkXIURV5HGxTmFuc75B2RfQkpxHG8aAgaAFa0tAHqYFfQ7Iwe2yhODk8+J4C7yAoRTWI3w/4klGRgR4lO7Rpn9+gvMyWp+uxFh8+H+ARlgN1nJuJuQAYvNkEnwGFck18Er4q3egEc/oO+mhLdKgRyhdNFiacC0rlOCbhNVz4H9FnAYgDBvU3QIioZlJFLJtsoHYRDfiZoUyIxqCtRpVlANq0EU4dApjrtgezPFad5S19Wgjkc0hNVnuF4HjVA6C7QrSIbylB+oZe3aHgBsqlNqKYH48jXyJKMuAbiyVJ8KzaB3eRc0pg9VwQ4niFryI68qiOi3AbjwdsfnAtk0bCjTLJKr6mrD9g8iq/S/B81hguOMlQTnVyG40wAcjnmgsCNESDrjme7wfftP4P7SP4N3CJZdvzoNyGq2c/HWOXJGsvVg+RA/k2MC/wN6I2YA2Pt8GkAAAAASUVORK5CYII='); background-color: #a94442; color: #f2dede; border-color: #ebccd1; }
|
||||
.jq-icon-success { background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAADsSURBVEhLY2AYBfQMgf///3P8+/evAIgvA/FsIF+BavYDDWMBGroaSMMBiE8VC7AZDrIFaMFnii3AZTjUgsUUWUDA8OdAH6iQbQEhw4HyGsPEcKBXBIC4ARhex4G4BsjmweU1soIFaGg/WtoFZRIZdEvIMhxkCCjXIVsATV6gFGACs4Rsw0EGgIIH3QJYJgHSARQZDrWAB+jawzgs+Q2UO49D7jnRSRGoEFRILcdmEMWGI0cm0JJ2QpYA1RDvcmzJEWhABhD/pqrL0S0CWuABKgnRki9lLseS7g2AlqwHWQSKH4oKLrILpRGhEQCw2LiRUIa4lwAAAABJRU5ErkJggg=='); color: #dff0d8; background-color: #3c763d; border-color: #d6e9c6; }
|
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue