mirror of
https://github.com/craigerl/aprsd.git
synced 2024-09-27 15:46:53 -04:00
Compare commits
146 Commits
Author | SHA1 | Date | |
---|---|---|---|
a74a66d9c3 | |||
a5dc322066 | |||
9b843eead9 | |||
e5662b95f8 | |||
a6f84e42bc | |||
e3ab6e7f59 | |||
af3d741833 | |||
b172c6dbde | |||
9d3f45ac30 | |||
49e8a622a7 | |||
92cb92f89c | |||
37415557b5 | |||
5ebbb52a2c | |||
673b34c78b | |||
ffa28fa28a | |||
93f752cd6d | |||
b5aa187d54 | |||
616cd69a2d | |||
4b26e2b7f7 | |||
f07ef71ce0 | |||
|
ee0c546231 | ||
|
ba4d9bb565 | ||
|
6d294113f8 | ||
8f1733e493 | |||
f7a9f7aaab | |||
1828342ef2 | |||
b317d0eb63 | |||
63962acfe6 | |||
44a72e813e | |||
afeb11a085 | |||
|
18fb2a9e2b | ||
fa2d2d965d | |||
2abf8bc750 | |||
f15974131c | |||
4d1dfadbde | |||
93a9cce0c0 | |||
|
321260ff7a | ||
cb2a3441b4 | |||
fc9ab4aa74 | |||
a5680a7cbb | |||
c4b17eee9d | |||
63f3de47b7 | |||
c206f52a76 | |||
2b2bf6c92d | |||
992485e9c7 | |||
f02db20c3e | |||
09b97086bc | |||
c43652dbea | |||
29d97d9f0c | |||
813bc7ea29 | |||
bef32059f4 | |||
717db6083e | |||
4c7e27c88b | |||
88d26241f5 | |||
27359d61aa | |||
7541f13174 | |||
a656d93263 | |||
cb0cfeea0b | |||
8d86764c23 | |||
dc4879a367 | |||
4542c0a643 | |||
3e8716365e | |||
758ea432ed | |||
1c9f25a3b3 | |||
7c935345e5 | |||
c2f8af06bc | |||
5b2a59fae3 | |||
8392d6b8ef | |||
1a7694e7e2 | |||
f2d39e5fd2 | |||
3bd7adda44 | |||
91ba6d10ce | |||
c6079f897d | |||
66e4850353 | |||
40c028c844 | |||
4c2a40b7a7 | |||
f682890ef0 | |||
026dc6e376 | |||
f59b65d13c | |||
5ff62c9bdf | |||
5fa4eaf909 | |||
f34120c2df | |||
3bef1314f8 | |||
94f36e0aad | |||
|
886ad9be09 | ||
|
aa6e732935 | ||
b3889896b9 | |||
8f6f8007f4 | |||
2e9cf3ce88 | |||
8728926bf4 | |||
2c5bc6c1f7 | |||
80705cb341 | |||
a839dbd3c5 | |||
1267a53ec8 | |||
da882b4f9b | |||
6845d266f2 | |||
db2fbce079 | |||
bc3bdc48d2 | |||
7114269cee | |||
fcc02f29af | |||
0ca9072c97 | |||
333feee805 | |||
a8d56a9967 | |||
50e491bab4 | |||
71d72adf06 | |||
e2e58530b2 | |||
01cd0a0327 | |||
f92b2ee364 | |||
a270c75263 | |||
bd005f628d | |||
200944f37a | |||
a62e490353 | |||
428edaced9 | |||
8f588e653d | |||
144ad34ae5 | |||
0321cb6cf1 | |||
c0623596cd | |||
f400c6004e | |||
873fc06608 | |||
f53df24988 | |||
f4356e4a20 | |||
c581dc5020 | |||
da7b7124d7 | |||
9e26df26d6 | |||
b461231c00 | |||
1e6c483002 | |||
127d3b3f26 | |||
f450238348 | |||
9858955d34 | |||
e386e91f6e | |||
386d2bea62 | |||
eada5e9ce2 | |||
00e185b4e7 | |||
1477e61b0f | |||
6f1d6b4122 | |||
90f212e6dc | |||
9c77ca26be | |||
d80277c9d8 | |||
29b4b04eee | |||
12dab284cb | |||
d0f53c563f | |||
24830ae810 | |||
|
52896a1c6f | ||
82b3761628 | |||
8797dfd072 | |||
c1acdc2510 |
3
.github/workflows/manual_build.yml
vendored
3
.github/workflows/manual_build.yml
vendored
@ -43,8 +43,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: "{{defaultContext}}:docker"
|
context: "{{defaultContext}}:docker"
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
file: ./Dockerfile-dev
|
file: ./Dockerfile
|
||||||
build-args: |
|
build-args: |
|
||||||
|
INSTALL_TYPE=github
|
||||||
BRANCH=${{ steps.extract_branch.outputs.branch }}
|
BRANCH=${{ steps.extract_branch.outputs.branch }}
|
||||||
BUILDX_QEMU_ENV=true
|
BUILDX_QEMU_ENV=true
|
||||||
push: true
|
push: true
|
||||||
|
5
.github/workflows/master-build.yml
vendored
5
.github/workflows/master-build.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.9", "3.10", "3.11"]
|
python-version: ["3.10", "3.11"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
@ -53,8 +53,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
context: "{{defaultContext}}:docker"
|
context: "{{defaultContext}}:docker"
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
file: ./Dockerfile-dev
|
file: ./Dockerfile
|
||||||
build-args: |
|
build-args: |
|
||||||
|
INSTALL_TYPE=github
|
||||||
BRANCH=${{ steps.branch-name.outputs.current_branch }}
|
BRANCH=${{ steps.branch-name.outputs.current_branch }}
|
||||||
BUILDX_QEMU_ENV=true
|
BUILDX_QEMU_ENV=true
|
||||||
push: true
|
push: true
|
||||||
|
2
.github/workflows/python.yml
vendored
2
.github/workflows/python.yml
vendored
@ -7,7 +7,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.9", "3.10", "3.11"]
|
python-version: ["3.10", "3.11"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python ${{ matrix.python-version }}
|
- name: Set up Python ${{ matrix.python-version }}
|
||||||
|
951
ChangeLog
951
ChangeLog
@ -1,951 +0,0 @@
|
|||||||
CHANGES
|
|
||||||
=======
|
|
||||||
|
|
||||||
v3.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
|
|
||||||
------
|
|
||||||
|
|
||||||
* 3.0.1
|
|
||||||
* Add support to Reject messages
|
|
||||||
* Update Docker builds for 3.0.0
|
|
||||||
|
|
||||||
v3.0.0
|
|
||||||
------
|
|
||||||
|
|
||||||
* Update Changelog for 3.0.0
|
|
||||||
* Ensure server command main thread doesn't exit
|
|
||||||
* Fixed save directory default
|
|
||||||
* Fixed pep8 failure
|
|
||||||
* Cleaned up KISS interfaces use of old config
|
|
||||||
* reworked usage of importlib.metadata
|
|
||||||
* Added new docs files for 3.0.0
|
|
||||||
* Removed url option from healthcheck in dev
|
|
||||||
* Updated Healthcheck to use rpc to call aprsd
|
|
||||||
* Updated docker/bin/run.sh to use new conf
|
|
||||||
* Added ObjectPacket
|
|
||||||
* Update regex processing and regex for plugins
|
|
||||||
* Change ordering of starting up of server command
|
|
||||||
* Update documentation and README
|
|
||||||
* Decouple admin web interface from server command
|
|
||||||
* Dockerfile now produces aprsd.conf
|
|
||||||
* Fix some unit tests and loading of CONF w/o file
|
|
||||||
* Added missing conf
|
|
||||||
* Removed references to old custom config
|
|
||||||
* Convert config to oslo\_config
|
|
||||||
* Added rain formatting unit tests to WeatherPacket
|
|
||||||
* Fix Rain reporting in WeatherPacket send
|
|
||||||
* Removed Packet.send()
|
|
||||||
* Removed watchlist plugins
|
|
||||||
* Fix PluginManager.get\_plugins
|
|
||||||
* Cleaned up PluginManager
|
|
||||||
* Cleaned up PluginManager
|
|
||||||
* Update routing for weatherpacket
|
|
||||||
* Fix some WeatherPacket formatting
|
|
||||||
* Fix pep8 violation
|
|
||||||
* Add packet filtering for aprsd listen
|
|
||||||
* Added WeatherPacket encoding
|
|
||||||
* Updated webchat and listen for queue based RX
|
|
||||||
* reworked collecting and reporting stats
|
|
||||||
* Removed unused threading code
|
|
||||||
* Change RX packet processing to enqueu
|
|
||||||
* Make tracking objectstores work w/o initializing
|
|
||||||
* Cleaned up packet transmit class attributes
|
|
||||||
* Fix packets timestamp to int
|
|
||||||
* More messaging -> packets cleanup
|
|
||||||
* Cleaned out all references to messaging
|
|
||||||
* Added contructing a GPSPacket for sending
|
|
||||||
* cleanup webchat
|
|
||||||
* Reworked all packet processing
|
|
||||||
* Updated plugins and plugin interfaces for Packet
|
|
||||||
* Started using dataclasses to describe packets
|
|
||||||
|
|
||||||
v2.6.1
|
|
||||||
------
|
|
||||||
|
|
||||||
* v2.6.1
|
|
||||||
* Fixed position report for webchat beacon
|
|
||||||
* Try and fix broken 32bit qemu builds on 64bit system
|
|
||||||
* Add unit tests for webchat
|
|
||||||
* remove armv7 build RUST sucks
|
|
||||||
* Fix for Collections change in 3.10
|
|
||||||
|
|
||||||
v2.6.0
|
|
||||||
------
|
|
||||||
|
|
||||||
* Update workflow again
|
|
||||||
* Update Dockerfile to 22.04
|
|
||||||
* Update Dockerfile and build.sh
|
|
||||||
* Update workflow
|
|
||||||
* Prep for 2.6.0 release
|
|
||||||
* Update requirements
|
|
||||||
* Removed Makefile comment
|
|
||||||
* Update Makefile for dev vs. run environments
|
|
||||||
* Added pyopenssl for https for webchat
|
|
||||||
* change from device-detector to user-agents
|
|
||||||
* Remove twine from dev-requirements
|
|
||||||
* Update to latest Makefile.venv
|
|
||||||
* Refactored threads a bit
|
|
||||||
* Mark packets as acked in MsgTracker
|
|
||||||
* remove dev setting for template
|
|
||||||
* Add GPS beacon to mobile page
|
|
||||||
* Allow werkzeug for admin interface
|
|
||||||
* Allow werkzeug for admin interface
|
|
||||||
* Add support for mobile browsers for webchat
|
|
||||||
* Ignore callsign case while processing packets
|
|
||||||
* remove linux/arm/v7 for official builds for now
|
|
||||||
* added workflow for building specific version
|
|
||||||
* Allow passing in version to the Dockerfile
|
|
||||||
* Send GPS Beacon from webchat interface
|
|
||||||
* specify Dockerfile-dev
|
|
||||||
* Fixed build.sh
|
|
||||||
* Build on the source not released aprsd
|
|
||||||
* Remove email validation
|
|
||||||
* Add support for building linux/arm/v7
|
|
||||||
* Remove python 3.7 from docker build github
|
|
||||||
* Fixed failing unit tests
|
|
||||||
* change github workflow
|
|
||||||
* Removed TimeOpenCageDataPlugin
|
|
||||||
* Dump config with aprsd dev test-plugin
|
|
||||||
* Updated requirements
|
|
||||||
* Got webchat working with KISS tcp
|
|
||||||
* Added click auto\_envvar\_prefix
|
|
||||||
* Update aprsd thread base class to use queue
|
|
||||||
* Update packets to use wrapt
|
|
||||||
* Add remving existing requirements
|
|
||||||
* Try sending raw APRSFrames to aioax25
|
|
||||||
* Use new aprsd.callsign as the main callsign
|
|
||||||
* Fixed access to threads refactor
|
|
||||||
* Added webchat command
|
|
||||||
* Moved log.py to logging
|
|
||||||
* Moved trace.py to utils
|
|
||||||
* Fixed pep8 errors
|
|
||||||
* Refactored threads.py
|
|
||||||
* Refactor utils to directory
|
|
||||||
* remove arm build for now
|
|
||||||
* Added rustc and cargo to Dockerfile
|
|
||||||
* remove linux/arm/v6 from docker platform build
|
|
||||||
* Only tag master build as master
|
|
||||||
* Remove docker build from test
|
|
||||||
* create master-build.yml
|
|
||||||
* Added container build action
|
|
||||||
* Update docs on using Docker
|
|
||||||
* Update dev-requirements pip-tools
|
|
||||||
* Fix typo in docker-compose.yml
|
|
||||||
* Fix PyPI scraping
|
|
||||||
* Allow web interface when running in Docker
|
|
||||||
* Fix typo on exception
|
|
||||||
* README formatting fixes
|
|
||||||
* Bump dependencies to fix python 3.10
|
|
||||||
* Fixed up config option checking for KISS
|
|
||||||
* Fix logging issue with log messages
|
|
||||||
* for 2.5.9
|
|
||||||
|
|
||||||
v2.5.9
|
|
||||||
------
|
|
||||||
|
|
||||||
* FIX: logging exceptions
|
|
||||||
* Updated build and run for rich lib
|
|
||||||
* update build for 2.5.8
|
|
||||||
|
|
||||||
v2.5.8
|
|
||||||
------
|
|
||||||
|
|
||||||
* For 2.5.8
|
|
||||||
* Removed debug code
|
|
||||||
* Updated list-plugins
|
|
||||||
* Renamed virtualenv dir to .aprsd-venv
|
|
||||||
* Added unit tests for dev test-plugin
|
|
||||||
* Send Message command defaults to config
|
|
||||||
|
|
||||||
v2.5.7
|
|
||||||
------
|
|
||||||
|
|
||||||
* Updated Changelog
|
|
||||||
* Fixed an KISS config disabled issue
|
|
||||||
* Fixed a bug with multiple notify plugins enabled
|
|
||||||
* Unify the logging to file and stdout
|
|
||||||
* Added new feature to list-plugins command
|
|
||||||
* more README.rst cleanup
|
|
||||||
* Updated README examples
|
|
||||||
|
|
||||||
v2.5.6
|
|
||||||
------
|
|
||||||
|
|
||||||
* Changelog
|
|
||||||
* Tightened up the packet logging
|
|
||||||
* Added unit tests for USWeatherPlugin, USMetarPlugin
|
|
||||||
* Added test\_location to test LocationPlugin
|
|
||||||
* Updated pytest output
|
|
||||||
* Added py39 to tox for tests
|
|
||||||
* Added NotifyPlugin unit tests and more
|
|
||||||
* Small cleanup on packet logging
|
|
||||||
* Reduced the APRSIS connection reset to 2 minutes
|
|
||||||
* Fixed the NotifyPlugin
|
|
||||||
* Fixed some pep8 errors
|
|
||||||
* Add tracing for dev command
|
|
||||||
* Added python rich library based logging
|
|
||||||
* Added LOG\_LEVEL env variable for the docker
|
|
||||||
|
|
||||||
v2.5.5
|
|
||||||
------
|
|
||||||
|
|
||||||
* Update requirements to use aprslib 0.7.0
|
|
||||||
* fixed the failure during loading for objectstore
|
|
||||||
* updated docker build
|
|
||||||
|
|
||||||
v2.5.4
|
|
||||||
------
|
|
||||||
|
|
||||||
* Updated Changelog
|
|
||||||
* Fixed dev command missing initialization
|
|
||||||
|
|
||||||
v2.5.3
|
|
||||||
------
|
|
||||||
|
|
||||||
* Fix admin logging tab
|
|
||||||
|
|
||||||
v2.5.2
|
|
||||||
------
|
|
||||||
|
|
||||||
* Added new list-plugins command
|
|
||||||
* Don't require check-version command to have a config
|
|
||||||
* Healthcheck command doesn't need the aprsd.yml config
|
|
||||||
* Fix test failures
|
|
||||||
* Removed requirement for aprs.fi key
|
|
||||||
* Updated Changelog
|
|
||||||
|
|
||||||
v2.5.1
|
|
||||||
------
|
|
||||||
|
|
||||||
* Removed stock plugin
|
|
||||||
* Removed the stock plugin
|
|
||||||
|
|
||||||
v2.5.0
|
|
||||||
------
|
|
||||||
|
|
||||||
* Updated for v2.5.0
|
|
||||||
* Updated Dockerfile's and build script for docker
|
|
||||||
* Cleaned up some verbose output & colorized output
|
|
||||||
* Reworked all the common arguments
|
|
||||||
* Fixed test-plugin
|
|
||||||
* Ensure common params are honored
|
|
||||||
* pep8
|
|
||||||
* Added healthcheck to the cmds
|
|
||||||
* Removed the need for FROMCALL in dev test-plugin
|
|
||||||
* Pep8 failures
|
|
||||||
* Refactor the cli
|
|
||||||
* Updated Changelog for 4.2.3
|
|
||||||
* Fixed a problem with send-message command
|
|
||||||
|
|
||||||
v2.4.2
|
|
||||||
------
|
|
||||||
|
|
||||||
* Updated Changelog
|
|
||||||
* Be more careful picking data to/from disk
|
|
||||||
* Updated Changelog
|
|
||||||
|
|
||||||
v2.4.1
|
|
||||||
------
|
|
||||||
|
|
||||||
* Ensure plugins are last to be loaded
|
|
||||||
* Fixed email connecting to smtp server
|
|
||||||
|
|
||||||
v2.4.0
|
|
||||||
------
|
|
||||||
|
|
||||||
* Updated Changelog for 2.4.0 release
|
|
||||||
* Converted MsgTrack to ObjectStoreMixin
|
|
||||||
* Fixed unit tests
|
|
||||||
* Make sure SeenList update has a from in packet
|
|
||||||
* Ensure PacketList is initialized
|
|
||||||
* Added SIGTERM to signal\_handler
|
|
||||||
* Enable configuring where to save the objectstore data
|
|
||||||
* PEP8 cleanup
|
|
||||||
* Added objectstore Mixin
|
|
||||||
* Added -num option to aprsd-dev test-plugin
|
|
||||||
* Only call stop\_threads if it exists
|
|
||||||
* Added new SeenList
|
|
||||||
* Added plugin version to stats reporting
|
|
||||||
* Added new HelpPlugin
|
|
||||||
* Updated aprsd-dev to use config for logfile format
|
|
||||||
* Updated build.sh
|
|
||||||
* removed usage of config.check\_config\_option
|
|
||||||
* Fixed send-message after config/client rework
|
|
||||||
* Fixed issue with flask config
|
|
||||||
* Added some server startup info logs
|
|
||||||
* Increase email delay to +10
|
|
||||||
* Updated dev to use plugin manager
|
|
||||||
* Fixed notify plugins
|
|
||||||
* Added new Config object
|
|
||||||
* Fixed email plugin's use of globals
|
|
||||||
* Refactored client classes
|
|
||||||
* Refactor utils usage
|
|
||||||
* 2.3.1 Changelog
|
|
||||||
|
|
||||||
v2.3.1
|
|
||||||
------
|
|
||||||
|
|
||||||
* Fixed issue of aprs-is missing keepalive
|
|
||||||
* Fixed packet processing issue with aprsd send-message
|
|
||||||
|
|
||||||
v2.3.0
|
|
||||||
------
|
|
||||||
|
|
||||||
* Prep 2.3.0
|
|
||||||
* Enable plugins to return message object
|
|
||||||
* Added enabled flag for every plugin object
|
|
||||||
* Ensure plugin threads are valid
|
|
||||||
* Updated Dockerfile to use v2.3.0
|
|
||||||
* Removed fixed size on logging queue
|
|
||||||
* Added Logfile tab in Admin ui
|
|
||||||
* Updated Makefile clean target
|
|
||||||
* Added self creating Makefile help target
|
|
||||||
* Update dev.py
|
|
||||||
* Allow passing in aprsis\_client
|
|
||||||
* Fixed a problem with the AVWX plugin not working
|
|
||||||
* Remove some noisy trace in email plugin
|
|
||||||
* Fixed issue at startup with notify plugin
|
|
||||||
* Fixed email validation
|
|
||||||
* Removed values from forms
|
|
||||||
* Added send-message to the main admin UI
|
|
||||||
* Updated requirements
|
|
||||||
* Cleaned up some pep8 failures
|
|
||||||
* Upgraded the send-message POC to use websockets
|
|
||||||
* New Admin ui send message page working
|
|
||||||
* Send Message via admin Web interface
|
|
||||||
* Updated Admin UI to show KISS connections
|
|
||||||
* Got TX/RX working with aioax25+direwolf over TCP
|
|
||||||
* Rebased from master
|
|
||||||
* Added the ability to use direwolf KISS socket
|
|
||||||
* Update Dockerfile to use 2.2.1
|
|
||||||
|
|
||||||
v2.2.1
|
|
||||||
------
|
|
||||||
|
|
||||||
* Update Changelog for 2.2.1
|
|
||||||
* Silence some log noise
|
|
||||||
|
|
||||||
v2.2.0
|
|
||||||
------
|
|
||||||
|
|
||||||
* Updated Changelog for v2.2.0
|
|
||||||
* Updated overview image
|
|
||||||
* Removed Black code style reference
|
|
||||||
* Removed TXThread
|
|
||||||
* Added days to uptime string formatting
|
|
||||||
* Updated select timeouts
|
|
||||||
* Rebase from master and run gray
|
|
||||||
* Added tracking plugin processing
|
|
||||||
* Added threads functions to APRSDPluginBase
|
|
||||||
* Refactor Message processing and MORE
|
|
||||||
* Use Gray instead of Black for code formatting
|
|
||||||
* Updated tox.ini
|
|
||||||
* Fixed LOG.debug issue in weather plugin
|
|
||||||
* Updated slack channel link
|
|
||||||
* Cleanup of the README.rst
|
|
||||||
* Fixed aprsd-dev
|
|
||||||
|
|
||||||
v2.1.0
|
|
||||||
------
|
|
||||||
|
|
||||||
* Prep for v2.1.0
|
|
||||||
* Enable multiple replies for plugins
|
|
||||||
* Put in a fix for aprslib parse exceptions
|
|
||||||
* Fixed time plugin
|
|
||||||
* Updated the charts Added the packets chart
|
|
||||||
* Added showing symbol images to watch list
|
|
||||||
|
|
||||||
v2.0.0
|
|
||||||
------
|
|
||||||
|
|
||||||
* Updated docs for 2.0.0
|
|
||||||
* Reworked the notification threads and admin ui
|
|
||||||
* Fixed small bug with packets get\_packet\_type
|
|
||||||
* Updated overview images
|
|
||||||
* Move version string output to top of log
|
|
||||||
* Add new watchlist feature
|
|
||||||
* Fixed the Ack thread not resending acks
|
|
||||||
* reworked the admin ui to use semenatic ui more
|
|
||||||
* Added messages count to admin messages list
|
|
||||||
* Add admin UI tabs for charts, messages, config
|
|
||||||
* Removed a noisy debug log
|
|
||||||
* Dump out the config during startup
|
|
||||||
* Added message counts for each plugin
|
|
||||||
* Bump urllib3 from 1.26.4 to 1.26.5
|
|
||||||
* Added aprsd version checking
|
|
||||||
* Updated INSTALL.txt
|
|
||||||
* Update my callsign
|
|
||||||
* Update README.rst
|
|
||||||
* Update README.rst
|
|
||||||
* Bump urllib3 from 1.26.3 to 1.26.4
|
|
||||||
* Prep for v1.6.1 release
|
|
||||||
|
|
||||||
v1.6.1
|
|
||||||
------
|
|
||||||
|
|
||||||
* Removed debug log for KeepAlive thread
|
|
||||||
* ignore Makefile.venv
|
|
||||||
* Reworked Makefile to use Makefile.venv
|
|
||||||
* Fixed version unit tests
|
|
||||||
* Updated stats output for KeepAlive thread
|
|
||||||
* Update Dockerfile-dev to work with startup
|
|
||||||
* Force all the graphs to 0 minimum
|
|
||||||
* Added email messages graphs
|
|
||||||
* Reworked the stats dict output and healthcheck
|
|
||||||
* Added callsign to the web index page
|
|
||||||
* Added log config for flask and lnav config file
|
|
||||||
* Added showing APRS-IS server to stats
|
|
||||||
* Provide an initial datapoint on rendering index
|
|
||||||
* Make the index page behind auth
|
|
||||||
* Bump pygments from 2.7.3 to 2.7.4
|
|
||||||
* Added acks with messages graphs
|
|
||||||
* Updated web stats index to show messages and ram usage
|
|
||||||
* Added aprsd web index page
|
|
||||||
* Bump lxml from 4.6.2 to 4.6.3
|
|
||||||
* Bump jinja2 from 2.11.2 to 2.11.3
|
|
||||||
* Bump urllib3 from 1.26.2 to 1.26.3
|
|
||||||
* Added log format and dateformat to config file
|
|
||||||
* Added Dockerfile-dev and updated build.sh
|
|
||||||
* Require python 3.7 and >
|
|
||||||
* Added plugin live reload and StockPlugin
|
|
||||||
* Updated Dockerfile and build.sh
|
|
||||||
* Updated Dockerfile for multiplatform builds
|
|
||||||
* Updated Dockerfile for multiplatform builds
|
|
||||||
* Dockerfile: Make creation of /config quiet failure
|
|
||||||
* Updated README docs
|
|
||||||
|
|
||||||
v1.6.0
|
|
||||||
------
|
|
||||||
|
|
||||||
* 1.6.0 release prep
|
|
||||||
* Updated path of run.sh for docker build
|
|
||||||
* Moved docker related stuffs to docker dir
|
|
||||||
* Removed some noisy debug log
|
|
||||||
* Bump cryptography from 3.3.1 to 3.3.2
|
|
||||||
* Wrap another server call with try except
|
|
||||||
* Wrap all imap calls with try except blocks
|
|
||||||
* Bump bleach from 3.2.1 to 3.3.0
|
|
||||||
* EmailThread was exiting because of IMAP timeout, added exceptions for this
|
|
||||||
* Added memory tracing in keeplive
|
|
||||||
* Fixed tox pep8 failure for trace
|
|
||||||
* Added tracing facility
|
|
||||||
* Fixed email login issue
|
|
||||||
* duplicate email messages from RF would generate usage response
|
|
||||||
* Enable debug logging for smtp and imap
|
|
||||||
* more debug around email thread
|
|
||||||
* debug around EmailThread hanging or vanishing
|
|
||||||
* Fixed resend email after config rework
|
|
||||||
* Added flask messages web UI and basic auth
|
|
||||||
* Fixed an issue with LocationPlugin
|
|
||||||
* Cleaned up the KeepAlive output
|
|
||||||
* updated .gitignore
|
|
||||||
* Added healthcheck app
|
|
||||||
* Add flask and flask\_classful reqs
|
|
||||||
* Added Flask web thread and stats collection
|
|
||||||
* First hack at flask
|
|
||||||
* Allow email to be disabled
|
|
||||||
* Reworked the config file and options
|
|
||||||
* Updated documentation and config output
|
|
||||||
* Fixed extracting lat/lon
|
|
||||||
* Added openweathermap weather plugin
|
|
||||||
* Added new time plugins
|
|
||||||
* Fixed TimePlugin timezone issue
|
|
||||||
* remove fortune white space
|
|
||||||
* fix git with install.txt
|
|
||||||
* change query char from ? to !
|
|
||||||
* Updated readme to include readthedocs link
|
|
||||||
* Added aprsd-dev plugin test cli and WxPlugin
|
|
||||||
|
|
||||||
v1.5.1
|
|
||||||
------
|
|
||||||
|
|
||||||
* Updated Changelog for v1.5.1
|
|
||||||
* Updated README to fix pypi page
|
|
||||||
* Update INSTALL.txt
|
|
||||||
|
|
||||||
v1.5.0
|
|
||||||
------
|
|
||||||
|
|
||||||
* Updated Changelog for v1.5.0 release
|
|
||||||
* Fix tox tests
|
|
||||||
* fix usage statement
|
|
||||||
* Enabled some emailthread messages and added timestamp
|
|
||||||
* Fixed main server client initialization
|
|
||||||
* test plugin expect responses update to match query output
|
|
||||||
* Fixed the queryPlugin unit test
|
|
||||||
* Removed flask code
|
|
||||||
* Changed default log level to INFO
|
|
||||||
* fix plugin tests to expect new strings
|
|
||||||
* fix query command syntax ?, ?3, ?d(elete), ?a(ll)
|
|
||||||
* Fixed latitude reporting in locationPlugin
|
|
||||||
* get rid of some debug noise from tracker and email delay
|
|
||||||
* fixed sample-config double print
|
|
||||||
* make sample config easier to interpret
|
|
||||||
* Fixed comments
|
|
||||||
* Added the ability to add comments to the config file
|
|
||||||
* Updated docker run.sh script
|
|
||||||
* Added --raw format for sending messages
|
|
||||||
* Fixed --quiet option
|
|
||||||
* Added send-message login checking and --no-ack
|
|
||||||
* Added new config for aprs.fi API Key
|
|
||||||
* Added a fix for failed logins to APRS-IS
|
|
||||||
* Fixed unit test for fortune plugin
|
|
||||||
* Fixed fortune plugin failures
|
|
||||||
* getting out of git hell with client.py problems
|
|
||||||
* Extend APRS.IS object to change login string
|
|
||||||
* Extend APRS.IS object to change login string
|
|
||||||
* expect different reply from query plugin
|
|
||||||
* update query plugin to resend last N messages. syntax: ?rN
|
|
||||||
* Added unit test for QueryPlugin
|
|
||||||
* Updated MsgTrack restart\_delayed
|
|
||||||
* refactor Plugin objects to plugins directory
|
|
||||||
* Updated README with more workflow details
|
|
||||||
* change query character syntax, don't reply that we're resending stuff
|
|
||||||
* Added APRSD system diagram to docs
|
|
||||||
* Disable MX record validation
|
|
||||||
* Added some more badges to readme files
|
|
||||||
* Updated build for docs tox -edocs
|
|
||||||
* switch command characters for query plugin
|
|
||||||
* Fix broken test
|
|
||||||
* undo git disaster
|
|
||||||
* swap Query command characters a bit
|
|
||||||
* Added Sphinx based documentation
|
|
||||||
* refactor Plugin objects to plugins directory
|
|
||||||
* Updated Makefile
|
|
||||||
* removed double-quote-string-fixer
|
|
||||||
* Lots of fixes
|
|
||||||
* Added more pre-commit hook tests
|
|
||||||
* Fixed email shortcut lookup
|
|
||||||
* Added Makefile for easy dev setup
|
|
||||||
* Added Makefile for easy dev setup
|
|
||||||
* Cleaned out old ack\_dict
|
|
||||||
* add null reply for send\_email
|
|
||||||
* Updated README with more workflow details
|
|
||||||
* backout my patch that broke tox, trying to push to craiger-test branch
|
|
||||||
* Fixed failures caused by last commit
|
|
||||||
* don't tell radio emails were sent, ack is enuf
|
|
||||||
* Updated README to include development env
|
|
||||||
* Added pre-commit hooks
|
|
||||||
* Update Changelog for v1.5.0
|
|
||||||
* Added QueryPlugin resend all delayed msgs or Flush
|
|
||||||
* Added QueryPlugin
|
|
||||||
* Added support to save/load MsgTrack on exit/start
|
|
||||||
* Creation of MsgTrack object and other stuff
|
|
||||||
* Added FortunePlugin unit test
|
|
||||||
* Added some plugin unit tests
|
|
||||||
* reworked threading
|
|
||||||
* Reworked messaging lib
|
|
||||||
|
|
||||||
v1.1.0
|
|
||||||
------
|
|
||||||
|
|
||||||
* Refactored the main process\_packet method
|
|
||||||
* Update README with version 1.1.0 related info
|
|
||||||
* Added fix for an unknown packet type
|
|
||||||
* Ensure fortune is installed
|
|
||||||
* Updated docker-compose
|
|
||||||
* Added Changelog
|
|
||||||
* Fixed issue when RX ack
|
|
||||||
* Updated the aprsd-slack-plugin required version
|
|
||||||
* Updated README.rst
|
|
||||||
* Fixed send-message with email command and others
|
|
||||||
* Update .gitignore
|
|
||||||
* Big patch
|
|
||||||
* Major refactor
|
|
||||||
* Updated the Dockerfile to use alpine
|
|
||||||
|
|
||||||
v1.0.1
|
|
||||||
------
|
|
||||||
|
|
||||||
* Fix unknown characterset emails
|
|
||||||
* Updated loggin timestamp to include []
|
|
||||||
* Updated README with a TOC
|
|
||||||
* Updates for building containers
|
|
||||||
* Don't use the dirname for the plugin path search
|
|
||||||
* Reworked Plugin loading
|
|
||||||
* Updated README with development information
|
|
||||||
* Fixed an issue with weather plugin
|
|
||||||
|
|
||||||
v1.0.0
|
|
||||||
------
|
|
||||||
|
|
||||||
* Rewrote the README.md to README.rst
|
|
||||||
* Fixed the usage string after plugins introduced
|
|
||||||
* Created plugin.py for Command Plugins
|
|
||||||
* Refactor networking and commands
|
|
||||||
* get rid of some debug statements
|
|
||||||
* yet another unicode problem, in resend\_email fixed
|
|
||||||
* reset default email check delay to 60, fix a few comments
|
|
||||||
* Update tox environment to fix formatting python errors
|
|
||||||
* fixed fortune. yet another unicode issue, tested in py3 and py2
|
|
||||||
* lose some logging statements
|
|
||||||
* completely off urllib now, tested locate/weather in py2 and py3
|
|
||||||
* add urllib import back until i replace all calls with requests
|
|
||||||
* cleaned up weather code after switch to requests ... from urllib. works on py2 and py3
|
|
||||||
* switch from urlib to requests for weather, tested in py3 and py2. still need to update locate, and all other http calls
|
|
||||||
* imap tags are unicode in py3. .decode tags
|
|
||||||
* Update INSTALL.txt
|
|
||||||
* Initial conversion to click
|
|
||||||
* Reconnect on socket timeout
|
|
||||||
* clean up code around closed\_socket and reconnect
|
|
||||||
* Update INSTALL.txt
|
|
||||||
* Fixed all pep8 errors and some py3 errors
|
|
||||||
* fix check\_email\_thread to do proper threading, take delay as arg
|
|
||||||
* found another .decode that didn't include errors='ignore'
|
|
||||||
* some failed attempts at getting the first txt or html from a multipart message, currently sends the last
|
|
||||||
* fix parse\_email unicode probs by using body.decode(errors='ignore').. again
|
|
||||||
* fix parse\_email unicode probs by using body.decode(errors='ignore')
|
|
||||||
* clean up code around closed\_socket and reconnect
|
|
||||||
* socket timeout 5 minutes
|
|
||||||
* Detect closed socket, reconnect, with a bit more grace
|
|
||||||
* can detect closed socket and reconnect now
|
|
||||||
* Update INSTALL.txt
|
|
||||||
* more debugging messages trying to find rare tight loop in main
|
|
||||||
* Update INSTALL.txt
|
|
||||||
* main loop went into tight loop, more debug prints
|
|
||||||
* main loop went into tight loop, added debug print before every continue
|
|
||||||
* Update INSTALL.txt
|
|
||||||
* Update INSTALL.txt
|
|
||||||
* George Carlin profanity filter
|
|
||||||
* added decaying email check timer which resets with activity
|
|
||||||
* Fixed all pep8 errors and some py3 errors
|
|
||||||
* Fixed all pep8 errors and some py3 errors
|
|
||||||
* Reconnect on socket timeout
|
|
||||||
* socket reconnect on timeout testing
|
|
||||||
* socket timeout of 300 instead of 60
|
|
||||||
* Reconnect on socket timeout
|
|
||||||
* socket reconnect on timeout testing
|
|
||||||
* Fixed all pep8 errors and some py3 errors
|
|
||||||
* fix check\_email\_thread to do proper threading, take delay as arg
|
|
||||||
* INSTALL.txt for the average person
|
|
||||||
* fix bugs after beautification and yaml config additions. Convert to sockets. case insensitive commands
|
|
||||||
* fix INBOX
|
|
||||||
* Update README.md
|
|
||||||
* Added tox support
|
|
||||||
* Fixed SMTP settings
|
|
||||||
* Created fake\_aprs.py
|
|
||||||
* select inbox if gmail server
|
|
||||||
* removed ASS
|
|
||||||
* Added a try block around imap login
|
|
||||||
* Added port and fixed telnet user
|
|
||||||
* Require ~/.aprsd/config.yml
|
|
||||||
* updated README for install and usage instructions
|
|
||||||
* added test to ensure shortcuts in config.yml
|
|
||||||
* added exit if missing config file
|
|
||||||
* Added reading of a config file
|
|
||||||
* update readme
|
|
||||||
* update readme
|
|
||||||
* sanitize readme
|
|
||||||
* readme again again
|
|
||||||
* readme again again
|
|
||||||
* readme again
|
|
||||||
* readme
|
|
||||||
* readme update
|
|
||||||
* First stab at migrating this to a pytpi repo
|
|
||||||
* First stab at migrating this to a pytpi repo
|
|
||||||
* Added password, callsign and host
|
|
||||||
* Added argparse for cli options
|
|
||||||
* comments
|
|
||||||
* Cleaned up trailing whitespace
|
|
||||||
* add tweaked fuzzyclock
|
|
||||||
* make tn a global
|
|
||||||
* Added standard python main()
|
|
||||||
* tweaks to readme
|
|
||||||
* drop virtenv on first line
|
|
||||||
* sanitize readme a bit more
|
|
||||||
* sanitize readme a bit more
|
|
||||||
* sanitize readme
|
|
||||||
* added weather and location 3
|
|
||||||
* added weather and location 2
|
|
||||||
* added weather and location
|
|
||||||
* mapme
|
|
||||||
* de-localize
|
|
||||||
* Update README.md
|
|
||||||
* Update README.md
|
|
||||||
* Update README.md
|
|
||||||
* Update README.md
|
|
||||||
* de-localize
|
|
||||||
* Update README.md
|
|
||||||
* Update README.md
|
|
||||||
* Update aprsd.py
|
|
||||||
* Add files via upload
|
|
||||||
* Update README.md
|
|
||||||
* Update aprsd.py
|
|
||||||
* Update README.md
|
|
||||||
* Update README.md
|
|
||||||
* Update README.md
|
|
||||||
* Update README.md
|
|
||||||
* Update README.md
|
|
||||||
* Update README.md
|
|
||||||
* Update README.md
|
|
||||||
* Update README.md
|
|
||||||
* Update README.md
|
|
||||||
* Update README.md
|
|
||||||
* Update README.md
|
|
||||||
* Update README.md
|
|
||||||
* Add files via upload
|
|
||||||
* Initial commit
|
|
1171
ChangeLog.md
Normal file
1171
ChangeLog.md
Normal file
File diff suppressed because it is too large
Load Diff
22
Makefile
22
Makefile
@ -17,14 +17,19 @@ Makefile.venv:
|
|||||||
help: # Help for the Makefile
|
help: # Help for the Makefile
|
||||||
@egrep -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
@egrep -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||||
|
|
||||||
dev: REQUIREMENTS_TXT = requirements.txt dev-requirements.txt
|
dev: REQUIREMENTS_TXT = requirements.txt requirements-dev.txt
|
||||||
dev: venv ## Create a python virtual environment for development of aprsd
|
dev: venv ## Create a python virtual environment for development of aprsd
|
||||||
|
|
||||||
run: venv ## Create a virtual environment for running aprsd commands
|
run: venv ## Create a virtual environment for running aprsd commands
|
||||||
|
|
||||||
docs: dev
|
changelog: dev
|
||||||
|
npm i -g auto-changelog
|
||||||
|
auto-changelog -l false -o ChangeLog.md
|
||||||
|
|
||||||
|
docs: changelog
|
||||||
|
m2r --overwrite ChangeLog.md
|
||||||
cp README.rst docs/readme.rst
|
cp README.rst docs/readme.rst
|
||||||
cp Changelog docs/changelog.rst
|
mv ChangeLog.rst docs/changelog.rst
|
||||||
tox -edocs
|
tox -edocs
|
||||||
|
|
||||||
clean: clean-build clean-pyc clean-test clean-dev ## remove all build, test, coverage and Python artifacts
|
clean: clean-build clean-pyc clean-test clean-dev ## remove all build, test, coverage and Python artifacts
|
||||||
@ -39,7 +44,6 @@ clean-build: ## remove build artifacts
|
|||||||
clean-pyc: ## remove Python file artifacts
|
clean-pyc: ## remove Python file artifacts
|
||||||
find . -name '*.pyc' -exec rm -f {} +
|
find . -name '*.pyc' -exec rm -f {} +
|
||||||
find . -name '*.pyo' -exec rm -f {} +
|
find . -name '*.pyo' -exec rm -f {} +
|
||||||
find . -name '*~' -exec rm -f {} +
|
|
||||||
find . -name '__pycache__' -exec rm -fr {} +
|
find . -name '__pycache__' -exec rm -fr {} +
|
||||||
|
|
||||||
clean-test: ## remove test and coverage artifacts
|
clean-test: ## remove test and coverage artifacts
|
||||||
@ -55,9 +59,9 @@ clean-dev:
|
|||||||
test: dev ## Run all the tox tests
|
test: dev ## Run all the tox tests
|
||||||
tox -p all
|
tox -p all
|
||||||
|
|
||||||
build: test ## Make the build artifact prior to doing an upload
|
build: test changelog ## Make the build artifact prior to doing an upload
|
||||||
$(VENV)/pip install twine
|
$(VENV)/pip install twine
|
||||||
$(VENV)/python3 setup.py sdist bdist_wheel
|
$(VENV)/python3 -m build
|
||||||
$(VENV)/twine check dist/*
|
$(VENV)/twine check dist/*
|
||||||
|
|
||||||
upload: build ## Upload a new version of the plugin
|
upload: build ## Upload a new version of the plugin
|
||||||
@ -81,8 +85,8 @@ docker-dev: test ## Make a development docker container tagged with hemna6969/a
|
|||||||
|
|
||||||
update-requirements: dev ## Update the requirements.txt and dev-requirements.txt files
|
update-requirements: dev ## Update the requirements.txt and dev-requirements.txt files
|
||||||
rm requirements.txt
|
rm requirements.txt
|
||||||
rm dev-requirements.txt
|
rm requirements-dev.txt
|
||||||
touch requirements.txt
|
touch requirements.txt
|
||||||
touch dev-requirements.txt
|
touch requirements-dev.txt
|
||||||
$(VENV)/pip-compile --resolver backtracking --annotation-style=line requirements.in
|
$(VENV)/pip-compile --resolver backtracking --annotation-style=line requirements.in
|
||||||
$(VENV)/pip-compile --resolver backtracking --annotation-style=line dev-requirements.in
|
$(VENV)/pip-compile --resolver backtracking --annotation-style=line requirements-dev.in
|
||||||
|
@ -69,6 +69,7 @@ Help
|
|||||||
====
|
====
|
||||||
::
|
::
|
||||||
|
|
||||||
|
|
||||||
└─> aprsd -h
|
└─> aprsd -h
|
||||||
Usage: aprsd [OPTIONS] COMMAND [ARGS]...
|
Usage: aprsd [OPTIONS] COMMAND [ARGS]...
|
||||||
|
|
||||||
@ -78,9 +79,11 @@ Help
|
|||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
check-version Check this version against the latest in pypi.org.
|
check-version Check this version against the latest in pypi.org.
|
||||||
completion Click Completion subcommands
|
completion Show the shell completion code
|
||||||
dev Development type subcommands
|
dev Development type subcommands
|
||||||
|
fetch-stats Fetch stats from a APRSD admin web interface.
|
||||||
healthcheck Check the health of the running aprsd server.
|
healthcheck Check the health of the running aprsd server.
|
||||||
|
list-extensions List the built in plugins available to APRSD.
|
||||||
list-plugins List the built in plugins available to APRSD.
|
list-plugins List the built in plugins available to APRSD.
|
||||||
listen Listen to packets on the APRS-IS Network based on FILTER.
|
listen Listen to packets on the APRS-IS Network based on FILTER.
|
||||||
sample-config Generate a sample Config file from aprsd and all...
|
sample-config Generate a sample Config file from aprsd and all...
|
||||||
@ -90,7 +93,6 @@ Help
|
|||||||
webchat Web based HAM Radio chat program!
|
webchat Web based HAM Radio chat program!
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Commands
|
Commands
|
||||||
========
|
========
|
||||||
|
|
||||||
|
@ -10,7 +10,10 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import pbr.version
|
from importlib.metadata import PackageNotFoundError, version
|
||||||
|
|
||||||
|
|
||||||
__version__ = pbr.version.VersionInfo("aprsd").version_string()
|
try:
|
||||||
|
__version__ = version("aprsd")
|
||||||
|
except PackageNotFoundError:
|
||||||
|
pass
|
||||||
|
348
aprsd/client.py
348
aprsd/client.py
@ -1,348 +0,0 @@
|
|||||||
import abc
|
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
|
|
||||||
import aprslib
|
|
||||||
from aprslib.exceptions import LoginError
|
|
||||||
from oslo_config import cfg
|
|
||||||
|
|
||||||
from aprsd import exception
|
|
||||||
from aprsd.clients import aprsis, fake, kiss
|
|
||||||
from aprsd.packets import core, packet_list
|
|
||||||
from aprsd.utils import trace
|
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
LOG = logging.getLogger("APRSD")
|
|
||||||
TRANSPORT_APRSIS = "aprsis"
|
|
||||||
TRANSPORT_TCPKISS = "tcpkiss"
|
|
||||||
TRANSPORT_SERIALKISS = "serialkiss"
|
|
||||||
TRANSPORT_FAKE = "fake"
|
|
||||||
|
|
||||||
# Main must create this from the ClientFactory
|
|
||||||
# object such that it's populated with the
|
|
||||||
# Correct config
|
|
||||||
factory = None
|
|
||||||
|
|
||||||
|
|
||||||
class Client:
|
|
||||||
"""Singleton client class that constructs the aprslib connection."""
|
|
||||||
|
|
||||||
_instance = None
|
|
||||||
_client = None
|
|
||||||
|
|
||||||
connected = False
|
|
||||||
server_string = None
|
|
||||||
filter = None
|
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
|
||||||
"""This magic turns this into a singleton."""
|
|
||||||
if cls._instance is None:
|
|
||||||
cls._instance = super().__new__(cls)
|
|
||||||
# Put any initialization here.
|
|
||||||
return cls._instance
|
|
||||||
|
|
||||||
def set_filter(self, filter):
|
|
||||||
self.filter = filter
|
|
||||||
if self._client:
|
|
||||||
self._client.set_filter(filter)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def client(self):
|
|
||||||
if not self._client:
|
|
||||||
LOG.info("Creating APRS client")
|
|
||||||
self._client = self.setup_connection()
|
|
||||||
if self.filter:
|
|
||||||
LOG.info("Creating APRS client filter")
|
|
||||||
self._client.set_filter(self.filter)
|
|
||||||
return self._client
|
|
||||||
|
|
||||||
def send(self, packet: core.Packet):
|
|
||||||
packet_list.PacketList().tx(packet)
|
|
||||||
self.client.send(packet)
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
"""Call this to force a rebuild/reconnect."""
|
|
||||||
if self._client:
|
|
||||||
del self._client
|
|
||||||
else:
|
|
||||||
LOG.warning("Client not initialized, nothing to reset.")
|
|
||||||
|
|
||||||
# Recreate the client
|
|
||||||
LOG.info(f"Creating new client {self.client}")
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def setup_connection(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
@abc.abstractmethod
|
|
||||||
def is_enabled():
|
|
||||||
pass
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
@abc.abstractmethod
|
|
||||||
def transport():
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def decode_packet(self, *args, **kwargs):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class APRSISClient(Client):
|
|
||||||
|
|
||||||
_client = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_enabled():
|
|
||||||
# Defaults to True if the enabled flag is non existent
|
|
||||||
try:
|
|
||||||
return CONF.aprs_network.enabled
|
|
||||||
except KeyError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_configured():
|
|
||||||
if APRSISClient.is_enabled():
|
|
||||||
# Ensure that the config vars are correctly set
|
|
||||||
if not CONF.aprs_network.login:
|
|
||||||
LOG.error("Config aprs_network.login not set.")
|
|
||||||
raise exception.MissingConfigOptionException(
|
|
||||||
"aprs_network.login is not set.",
|
|
||||||
)
|
|
||||||
if not CONF.aprs_network.password:
|
|
||||||
LOG.error("Config aprs_network.password not set.")
|
|
||||||
raise exception.MissingConfigOptionException(
|
|
||||||
"aprs_network.password is not set.",
|
|
||||||
)
|
|
||||||
if not CONF.aprs_network.host:
|
|
||||||
LOG.error("Config aprs_network.host not set.")
|
|
||||||
raise exception.MissingConfigOptionException(
|
|
||||||
"aprs_network.host is not set.",
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
|
||||||
return True
|
|
||||||
|
|
||||||
def is_alive(self):
|
|
||||||
if self._client:
|
|
||||||
return self._client.is_alive()
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def transport():
|
|
||||||
return TRANSPORT_APRSIS
|
|
||||||
|
|
||||||
def decode_packet(self, *args, **kwargs):
|
|
||||||
"""APRS lib already decodes this."""
|
|
||||||
return core.Packet.factory(args[0])
|
|
||||||
|
|
||||||
def setup_connection(self):
|
|
||||||
user = CONF.aprs_network.login
|
|
||||||
password = CONF.aprs_network.password
|
|
||||||
host = CONF.aprs_network.host
|
|
||||||
port = CONF.aprs_network.port
|
|
||||||
connected = False
|
|
||||||
backoff = 1
|
|
||||||
aprs_client = None
|
|
||||||
while not connected:
|
|
||||||
try:
|
|
||||||
LOG.info("Creating aprslib client")
|
|
||||||
aprs_client = aprsis.Aprsdis(user, passwd=password, host=host, port=port)
|
|
||||||
# Force the log to be the same
|
|
||||||
aprs_client.logger = LOG
|
|
||||||
aprs_client.connect()
|
|
||||||
connected = True
|
|
||||||
backoff = 1
|
|
||||||
except LoginError as e:
|
|
||||||
LOG.error(f"Failed to login to APRS-IS Server '{e}'")
|
|
||||||
connected = False
|
|
||||||
time.sleep(backoff)
|
|
||||||
except Exception as e:
|
|
||||||
LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
|
|
||||||
connected = False
|
|
||||||
time.sleep(backoff)
|
|
||||||
# Don't allow the backoff to go to inifinity.
|
|
||||||
if backoff > 5:
|
|
||||||
backoff = 5
|
|
||||||
else:
|
|
||||||
backoff += 1
|
|
||||||
continue
|
|
||||||
LOG.debug(f"Logging in to APRS-IS with user '{user}'")
|
|
||||||
self._client = aprs_client
|
|
||||||
return aprs_client
|
|
||||||
|
|
||||||
|
|
||||||
class KISSClient(Client):
|
|
||||||
|
|
||||||
_client = None
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_enabled():
|
|
||||||
"""Return if tcp or serial KISS is enabled."""
|
|
||||||
if CONF.kiss_serial.enabled:
|
|
||||||
return True
|
|
||||||
|
|
||||||
if CONF.kiss_tcp.enabled:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_configured():
|
|
||||||
# Ensure that the config vars are correctly set
|
|
||||||
if KISSClient.is_enabled():
|
|
||||||
transport = KISSClient.transport()
|
|
||||||
if transport == TRANSPORT_SERIALKISS:
|
|
||||||
if not CONF.kiss_serial.device:
|
|
||||||
LOG.error("KISS serial enabled, but no device is set.")
|
|
||||||
raise exception.MissingConfigOptionException(
|
|
||||||
"kiss_serial.device is not set.",
|
|
||||||
)
|
|
||||||
elif transport == TRANSPORT_TCPKISS:
|
|
||||||
if not CONF.kiss_tcp.host:
|
|
||||||
LOG.error("KISS TCP enabled, but no host is set.")
|
|
||||||
raise exception.MissingConfigOptionException(
|
|
||||||
"kiss_tcp.host is not set.",
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def is_alive(self):
|
|
||||||
if self._client:
|
|
||||||
return self._client.is_alive()
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def transport():
|
|
||||||
if CONF.kiss_serial.enabled:
|
|
||||||
return TRANSPORT_SERIALKISS
|
|
||||||
|
|
||||||
if CONF.kiss_tcp.enabled:
|
|
||||||
return TRANSPORT_TCPKISS
|
|
||||||
|
|
||||||
def decode_packet(self, *args, **kwargs):
|
|
||||||
"""We get a frame, which has to be decoded."""
|
|
||||||
LOG.debug(f"kwargs {kwargs}")
|
|
||||||
frame = kwargs["frame"]
|
|
||||||
LOG.debug(f"Got an APRS Frame '{frame}'")
|
|
||||||
# try and nuke the * from the fromcall sign.
|
|
||||||
# frame.header._source._ch = False
|
|
||||||
# payload = str(frame.payload.decode())
|
|
||||||
# msg = f"{str(frame.header)}:{payload}"
|
|
||||||
# msg = frame.tnc2
|
|
||||||
# LOG.debug(f"Decoding {msg}")
|
|
||||||
|
|
||||||
raw = aprslib.parse(str(frame))
|
|
||||||
packet = core.Packet.factory(raw)
|
|
||||||
if isinstance(packet, core.ThirdParty):
|
|
||||||
return packet.subpacket
|
|
||||||
else:
|
|
||||||
return packet
|
|
||||||
|
|
||||||
def setup_connection(self):
|
|
||||||
self._client = kiss.KISS3Client()
|
|
||||||
return self._client
|
|
||||||
|
|
||||||
|
|
||||||
class APRSDFakeClient(Client, metaclass=trace.TraceWrapperMetaclass):
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_enabled():
|
|
||||||
if CONF.fake_client.enabled:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def is_configured():
|
|
||||||
return APRSDFakeClient.is_enabled()
|
|
||||||
|
|
||||||
def is_alive(self):
|
|
||||||
return True
|
|
||||||
|
|
||||||
def setup_connection(self):
|
|
||||||
return fake.APRSDFakeClient()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def transport():
|
|
||||||
return TRANSPORT_FAKE
|
|
||||||
|
|
||||||
def decode_packet(self, *args, **kwargs):
|
|
||||||
LOG.debug(f"kwargs {kwargs}")
|
|
||||||
pkt = kwargs["packet"]
|
|
||||||
LOG.debug(f"Got an APRS Fake Packet '{pkt}'")
|
|
||||||
return pkt
|
|
||||||
|
|
||||||
|
|
||||||
class ClientFactory:
|
|
||||||
_instance = None
|
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
|
||||||
"""This magic turns this into a singleton."""
|
|
||||||
if cls._instance is None:
|
|
||||||
cls._instance = super().__new__(cls)
|
|
||||||
# Put any initialization here.
|
|
||||||
return cls._instance
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self._builders = {}
|
|
||||||
|
|
||||||
def register(self, key, builder):
|
|
||||||
self._builders[key] = builder
|
|
||||||
|
|
||||||
def create(self, key=None):
|
|
||||||
if not key:
|
|
||||||
if APRSISClient.is_enabled():
|
|
||||||
key = TRANSPORT_APRSIS
|
|
||||||
elif KISSClient.is_enabled():
|
|
||||||
key = KISSClient.transport()
|
|
||||||
elif APRSDFakeClient.is_enabled():
|
|
||||||
key = TRANSPORT_FAKE
|
|
||||||
|
|
||||||
builder = self._builders.get(key)
|
|
||||||
LOG.debug(f"Creating client {key}")
|
|
||||||
if not builder:
|
|
||||||
raise ValueError(key)
|
|
||||||
return builder()
|
|
||||||
|
|
||||||
def is_client_enabled(self):
|
|
||||||
"""Make sure at least one client is enabled."""
|
|
||||||
enabled = False
|
|
||||||
for key in self._builders.keys():
|
|
||||||
try:
|
|
||||||
enabled |= self._builders[key].is_enabled()
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return enabled
|
|
||||||
|
|
||||||
def is_client_configured(self):
|
|
||||||
enabled = False
|
|
||||||
for key in self._builders.keys():
|
|
||||||
try:
|
|
||||||
enabled |= self._builders[key].is_configured()
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
except exception.MissingConfigOptionException as ex:
|
|
||||||
LOG.error(ex.message)
|
|
||||||
return False
|
|
||||||
except exception.ConfigOptionBogusDefaultException as ex:
|
|
||||||
LOG.error(ex.message)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return enabled
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def setup():
|
|
||||||
"""Create and register all possible client objects."""
|
|
||||||
global factory
|
|
||||||
|
|
||||||
factory = ClientFactory()
|
|
||||||
factory.register(TRANSPORT_APRSIS, APRSISClient)
|
|
||||||
factory.register(TRANSPORT_TCPKISS, KISSClient)
|
|
||||||
factory.register(TRANSPORT_SERIALKISS, KISSClient)
|
|
||||||
factory.register(TRANSPORT_FAKE, APRSDFakeClient)
|
|
13
aprsd/client/__init__.py
Normal file
13
aprsd/client/__init__.py
Normal file
@ -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)
|
132
aprsd/client/aprsis.py
Normal file
132
aprsd/client/aprsis.py
Normal file
@ -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,
|
||||||
|
)
|
105
aprsd/client/base.py
Normal file
105
aprsd/client/base.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import abc
|
||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
import wrapt
|
||||||
|
|
||||||
|
from aprsd.packets import core
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
class APRSClient:
|
||||||
|
"""Singleton client class that constructs the aprslib connection."""
|
||||||
|
|
||||||
|
_instance = None
|
||||||
|
_client = None
|
||||||
|
|
||||||
|
connected = False
|
||||||
|
filter = None
|
||||||
|
lock = threading.Lock()
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
"""This magic turns this into a singleton."""
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
# Put any initialization here.
|
||||||
|
cls._instance._create_client()
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def stats(self) -> dict:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def set_filter(self, filter):
|
||||||
|
self.filter = filter
|
||||||
|
if self._client:
|
||||||
|
self._client.set_filter(filter)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self):
|
||||||
|
if not self._client:
|
||||||
|
self._create_client()
|
||||||
|
return self._client
|
||||||
|
|
||||||
|
def _create_client(self):
|
||||||
|
self._client = self.setup_connection()
|
||||||
|
if self.filter:
|
||||||
|
LOG.info("Creating APRS client filter")
|
||||||
|
self._client.set_filter(self.filter)
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if self._client:
|
||||||
|
LOG.info("Stopping client connection.")
|
||||||
|
self._client.stop()
|
||||||
|
|
||||||
|
def send(self, packet: core.Packet):
|
||||||
|
"""Send a packet to the network."""
|
||||||
|
self.client.send(packet)
|
||||||
|
|
||||||
|
@wrapt.synchronized(lock)
|
||||||
|
def reset(self):
|
||||||
|
"""Call this to force a rebuild/reconnect."""
|
||||||
|
LOG.info("Resetting client connection.")
|
||||||
|
if self._client:
|
||||||
|
self._client.close()
|
||||||
|
del self._client
|
||||||
|
self._create_client()
|
||||||
|
else:
|
||||||
|
LOG.warning("Client not initialized, nothing to reset.")
|
||||||
|
|
||||||
|
# Recreate the client
|
||||||
|
LOG.info(f"Creating new client {self.client}")
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def setup_connection(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abc.abstractmethod
|
||||||
|
def is_enabled():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@abc.abstractmethod
|
||||||
|
def transport():
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def decode_packet(self, *args, **kwargs):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def consumer(self, callback, blocking=False, immortal=False, raw=False):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def is_alive(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def close(self):
|
||||||
|
pass
|
@ -1,3 +1,4 @@
|
|||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import select
|
import select
|
||||||
import threading
|
import threading
|
||||||
@ -11,7 +12,6 @@ from aprslib.exceptions import (
|
|||||||
import wrapt
|
import wrapt
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import stats
|
|
||||||
from aprsd.packets import core
|
from aprsd.packets import core
|
||||||
|
|
||||||
|
|
||||||
@ -24,6 +24,9 @@ class Aprsdis(aprslib.IS):
|
|||||||
# flag to tell us to stop
|
# flag to tell us to stop
|
||||||
thread_stop = False
|
thread_stop = False
|
||||||
|
|
||||||
|
# date for last time we heard from the server
|
||||||
|
aprsd_keepalive = datetime.datetime.now()
|
||||||
|
|
||||||
# timeout in seconds
|
# timeout in seconds
|
||||||
select_timeout = 1
|
select_timeout = 1
|
||||||
lock = threading.Lock()
|
lock = threading.Lock()
|
||||||
@ -142,7 +145,6 @@ class Aprsdis(aprslib.IS):
|
|||||||
|
|
||||||
self.logger.info(f"Connected to {server_string}")
|
self.logger.info(f"Connected to {server_string}")
|
||||||
self.server_string = server_string
|
self.server_string = server_string
|
||||||
stats.APRSDStats().set_aprsis_server(server_string)
|
|
||||||
|
|
||||||
except LoginError as e:
|
except LoginError as e:
|
||||||
self.logger.error(str(e))
|
self.logger.error(str(e))
|
||||||
@ -176,13 +178,14 @@ class Aprsdis(aprslib.IS):
|
|||||||
try:
|
try:
|
||||||
for line in self._socket_readlines(blocking):
|
for line in self._socket_readlines(blocking):
|
||||||
if line[0:1] != b"#":
|
if line[0:1] != b"#":
|
||||||
|
self.aprsd_keepalive = datetime.datetime.now()
|
||||||
if raw:
|
if raw:
|
||||||
callback(line)
|
callback(line)
|
||||||
else:
|
else:
|
||||||
callback(self._parse(line))
|
callback(self._parse(line))
|
||||||
else:
|
else:
|
||||||
self.logger.debug("Server: %s", line.decode("utf8"))
|
self.logger.debug("Server: %s", line.decode("utf8"))
|
||||||
stats.APRSDStats().set_aprsis_keepalive()
|
self.aprsd_keepalive = datetime.datetime.now()
|
||||||
except ParseError as exp:
|
except ParseError as exp:
|
||||||
self.logger.log(
|
self.logger.log(
|
||||||
11,
|
11,
|
@ -67,7 +67,7 @@ class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
|
|||||||
# Generate packets here?
|
# Generate packets here?
|
||||||
raw = "GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW"
|
raw = "GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW"
|
||||||
pkt_raw = aprslib.parse(raw)
|
pkt_raw = aprslib.parse(raw)
|
||||||
pkt = core.Packet.factory(pkt_raw)
|
pkt = core.factory(pkt_raw)
|
||||||
callback(packet=pkt)
|
callback(packet=pkt)
|
||||||
LOG.debug(f"END blocking FAKE consumer {self}")
|
LOG.debug(f"END blocking FAKE consumer {self}")
|
||||||
time.sleep(8)
|
time.sleep(8)
|
@ -81,7 +81,7 @@ class KISS3Client:
|
|||||||
LOG.error("Failed to parse bytes received from KISS interface.")
|
LOG.error("Failed to parse bytes received from KISS interface.")
|
||||||
LOG.exception(ex)
|
LOG.exception(ex)
|
||||||
|
|
||||||
def consumer(self, callback, blocking=False, immortal=False, raw=False):
|
def consumer(self, callback):
|
||||||
LOG.debug("Start blocking KISS consumer")
|
LOG.debug("Start blocking KISS consumer")
|
||||||
self._parse_callback = callback
|
self._parse_callback = callback
|
||||||
self.kiss.read(callback=self.parse_frame, min_frames=None)
|
self.kiss.read(callback=self.parse_frame, min_frames=None)
|
88
aprsd/client/factory.py
Normal file
88
aprsd/client/factory.py
Normal file
@ -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
|
48
aprsd/client/fake.py
Normal file
48
aprsd/client/fake.py
Normal file
@ -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
|
103
aprsd/client/kiss.py
Normal file
103
aprsd/client/kiss.py
Normal file
@ -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.ThirdPartyPacket):
|
||||||
|
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)
|
38
aprsd/client/stats.py
Normal file
38
aprsd/client/stats.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import threading
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
import wrapt
|
||||||
|
|
||||||
|
from aprsd import client
|
||||||
|
from aprsd.utils import singleton
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
@singleton
|
||||||
|
class APRSClientStats:
|
||||||
|
|
||||||
|
lock = threading.Lock()
|
||||||
|
|
||||||
|
@wrapt.synchronized(lock)
|
||||||
|
def stats(self, serializable=False):
|
||||||
|
cl = client.client_factory.create()
|
||||||
|
stats = {
|
||||||
|
"transport": cl.transport(),
|
||||||
|
"filter": cl.filter,
|
||||||
|
"connected": cl.connected,
|
||||||
|
}
|
||||||
|
|
||||||
|
if cl.transport() == client.TRANSPORT_APRSIS:
|
||||||
|
stats["server_string"] = cl.client.server_string
|
||||||
|
keepalive = cl.client.aprsd_keepalive
|
||||||
|
if serializable:
|
||||||
|
keepalive = keepalive.isoformat()
|
||||||
|
stats["server_keepalive"] = keepalive
|
||||||
|
elif cl.transport() == client.TRANSPORT_TCPKISS:
|
||||||
|
stats["host"] = CONF.kiss_tcp.host
|
||||||
|
stats["port"] = CONF.kiss_tcp.port
|
||||||
|
elif cl.transport() == client.TRANSPORT_SERIALKISS:
|
||||||
|
stats["device"] = CONF.kiss_serial.device
|
||||||
|
return stats
|
@ -1,5 +1,5 @@
|
|||||||
import click
|
import click
|
||||||
import click_completion
|
import click.shell_completion
|
||||||
|
|
||||||
from aprsd.main import cli
|
from aprsd.main import cli
|
||||||
|
|
||||||
@ -7,30 +7,16 @@ from aprsd.main import cli
|
|||||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||||
|
|
||||||
|
|
||||||
@cli.group(help="Click Completion subcommands", context_settings=CONTEXT_SETTINGS)
|
@cli.command()
|
||||||
@click.pass_context
|
@click.argument("shell", type=click.Choice(list(click.shell_completion._available_shells)))
|
||||||
def completion(ctx):
|
def completion(shell):
|
||||||
pass
|
"""Show the shell completion code"""
|
||||||
|
from click.utils import _detect_program_name
|
||||||
|
|
||||||
|
cls = click.shell_completion.get_completion_class(shell)
|
||||||
# show dumps out the completion code for a particular shell
|
prog_name = _detect_program_name()
|
||||||
@completion.command(help="Show completion code for shell", name="show")
|
complete_var = f"_{prog_name}_COMPLETE".replace("-", "_").upper()
|
||||||
@click.option("-i", "--case-insensitive/--no-case-insensitive", help="Case insensitive completion")
|
print(cls(cli, {}, prog_name, complete_var).source())
|
||||||
@click.argument("shell", required=False, type=click_completion.DocumentedChoice(click_completion.core.shells))
|
print("# Add the following line to your shell configuration file to have aprsd command line completion")
|
||||||
def show(shell, case_insensitive):
|
print("# but remove the leading '#' character.")
|
||||||
"""Show the click-completion-command completion code"""
|
print(f"# eval \"$(aprsd completion {shell})\"")
|
||||||
extra_env = {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} if case_insensitive else {}
|
|
||||||
click.echo(click_completion.core.get_code(shell, extra_env=extra_env))
|
|
||||||
|
|
||||||
|
|
||||||
# install will install the completion code for a particular shell
|
|
||||||
@completion.command(help="Install completion code for a shell", name="install")
|
|
||||||
@click.option("--append/--overwrite", help="Append the completion code to the file", default=None)
|
|
||||||
@click.option("-i", "--case-insensitive/--no-case-insensitive", help="Case insensitive completion")
|
|
||||||
@click.argument("shell", required=False, type=click_completion.DocumentedChoice(click_completion.core.shells))
|
|
||||||
@click.argument("path", required=False)
|
|
||||||
def install(append, case_insensitive, shell, path):
|
|
||||||
"""Install the click-completion-command completion"""
|
|
||||||
extra_env = {"_CLICK_COMPLETION_COMMAND_CASE_INSENSITIVE_COMPLETE": "ON"} if case_insensitive else {}
|
|
||||||
shell, path = click_completion.core.install(shell=shell, path=path, append=append, extra_env=extra_env)
|
|
||||||
click.echo(f"{shell} completion installed in {path}")
|
|
||||||
|
@ -8,8 +8,9 @@ import logging
|
|||||||
import click
|
import click
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from aprsd import cli_helper, conf, packets, plugin
|
||||||
# local imports here
|
# local imports here
|
||||||
from aprsd import cli_helper, client, conf, packets, plugin
|
from aprsd.client import base
|
||||||
from aprsd.main import cli
|
from aprsd.main import cli
|
||||||
from aprsd.utils import trace
|
from aprsd.utils import trace
|
||||||
|
|
||||||
@ -96,7 +97,7 @@ def test_plugin(
|
|||||||
if CONF.trace_enabled:
|
if CONF.trace_enabled:
|
||||||
trace.setup_tracing(["method", "api"])
|
trace.setup_tracing(["method", "api"])
|
||||||
|
|
||||||
client.Client()
|
base.APRSClient()
|
||||||
|
|
||||||
pm = plugin.PluginManager()
|
pm = plugin.PluginManager()
|
||||||
if load_all:
|
if load_all:
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
# Fetch active stats from a remote running instance of aprsd server
|
# Fetch active stats from a remote running instance of aprsd admin web interface.
|
||||||
# This uses the RPC server to fetch the stats from the remote server.
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
import requests
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
|
|
||||||
@ -12,7 +11,6 @@ from rich.table import Table
|
|||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import cli_helper
|
from aprsd import cli_helper
|
||||||
from aprsd.main import cli
|
from aprsd.main import cli
|
||||||
from aprsd.rpc import client as rpc_client
|
|
||||||
|
|
||||||
|
|
||||||
# setup the global logger
|
# setup the global logger
|
||||||
@ -26,87 +24,80 @@ CONF = cfg.CONF
|
|||||||
@click.option(
|
@click.option(
|
||||||
"--host", type=str,
|
"--host", type=str,
|
||||||
default=None,
|
default=None,
|
||||||
help="IP address of the remote aprsd server to fetch stats from.",
|
help="IP address of the remote aprsd admin web ui fetch stats from.",
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--port", type=int,
|
"--port", type=int,
|
||||||
default=None,
|
default=None,
|
||||||
help="Port of the remote aprsd server rpc port to fetch stats from.",
|
help="Port of the remote aprsd web admin interface to fetch stats from.",
|
||||||
)
|
|
||||||
@click.option(
|
|
||||||
"--magic-word", type=str,
|
|
||||||
default=None,
|
|
||||||
help="Magic word of the remote aprsd server rpc port to fetch stats from.",
|
|
||||||
)
|
)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
@cli_helper.process_standard_options
|
@cli_helper.process_standard_options
|
||||||
def fetch_stats(ctx, host, port, magic_word):
|
def fetch_stats(ctx, host, port):
|
||||||
"""Fetch stats from a remote running instance of aprsd server."""
|
"""Fetch stats from a APRSD admin web interface."""
|
||||||
LOG.info(f"APRSD Fetch-Stats started version: {aprsd.__version__}")
|
console = Console()
|
||||||
|
console.print(f"APRSD Fetch-Stats started version: {aprsd.__version__}")
|
||||||
|
|
||||||
CONF.log_opt_values(LOG, logging.DEBUG)
|
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||||
if not host:
|
if not host:
|
||||||
host = CONF.rpc_settings.ip
|
host = CONF.admin.web_ip
|
||||||
if not port:
|
if not port:
|
||||||
port = CONF.rpc_settings.port
|
port = CONF.admin.web_port
|
||||||
if not magic_word:
|
|
||||||
magic_word = CONF.rpc_settings.magic_word
|
|
||||||
|
|
||||||
msg = f"Fetching stats from {host}:{port} with magic word '{magic_word}'"
|
msg = f"Fetching stats from {host}:{port}"
|
||||||
console = Console()
|
|
||||||
console.print(msg)
|
console.print(msg)
|
||||||
with console.status(msg):
|
with console.status(msg):
|
||||||
client = rpc_client.RPCClient(host, port, magic_word)
|
response = requests.get(f"http://{host}:{port}/stats", timeout=120)
|
||||||
stats = client.get_stats_dict()
|
if not response:
|
||||||
if stats:
|
console.print(
|
||||||
console.print_json(data=stats)
|
f"Failed to fetch stats from {host}:{port}?",
|
||||||
else:
|
style="bold red",
|
||||||
LOG.error(f"Failed to fetch stats via RPC aprsd server at {host}:{port}")
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
stats = response.json()
|
||||||
|
if not stats:
|
||||||
|
console.print(
|
||||||
|
f"Failed to fetch stats from aprsd admin ui at {host}:{port}",
|
||||||
|
style="bold red",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
aprsd_title = (
|
aprsd_title = (
|
||||||
"APRSD "
|
"APRSD "
|
||||||
f"[bold cyan]v{stats['aprsd']['version']}[/] "
|
f"[bold cyan]v{stats['APRSDStats']['version']}[/] "
|
||||||
f"Callsign [bold green]{stats['aprsd']['callsign']}[/] "
|
f"Callsign [bold green]{stats['APRSDStats']['callsign']}[/] "
|
||||||
f"Uptime [bold yellow]{stats['aprsd']['uptime']}[/]"
|
f"Uptime [bold yellow]{stats['APRSDStats']['uptime']}[/]"
|
||||||
)
|
)
|
||||||
|
|
||||||
console.rule(f"Stats from {host}:{port} with magic word '{magic_word}'")
|
console.rule(f"Stats from {host}:{port}")
|
||||||
console.print("\n\n")
|
console.print("\n\n")
|
||||||
console.rule(aprsd_title)
|
console.rule(aprsd_title)
|
||||||
|
|
||||||
# Show the connection to APRS
|
# Show the connection to APRS
|
||||||
# It can be a connection to an APRS-IS server or a local TNC via KISS or KISSTCP
|
# It can be a connection to an APRS-IS server or a local TNC via KISS or KISSTCP
|
||||||
if "aprs-is" in stats:
|
if "aprs-is" in stats:
|
||||||
title = f"APRS-IS Connection {stats['aprs-is']['server']}"
|
title = f"APRS-IS Connection {stats['APRSClientStats']['server_string']}"
|
||||||
table = Table(title=title)
|
table = Table(title=title)
|
||||||
table.add_column("Key")
|
table.add_column("Key")
|
||||||
table.add_column("Value")
|
table.add_column("Value")
|
||||||
for key, value in stats["aprs-is"].items():
|
for key, value in stats["APRSClientStats"].items():
|
||||||
table.add_row(key, value)
|
table.add_row(key, value)
|
||||||
console.print(table)
|
console.print(table)
|
||||||
|
|
||||||
threads_table = Table(title="Threads")
|
threads_table = Table(title="Threads")
|
||||||
threads_table.add_column("Name")
|
threads_table.add_column("Name")
|
||||||
threads_table.add_column("Alive?")
|
threads_table.add_column("Alive?")
|
||||||
for name, alive in stats["aprsd"]["threads"].items():
|
for name, alive in stats["APRSDThreadList"].items():
|
||||||
threads_table.add_row(name, str(alive))
|
threads_table.add_row(name, str(alive))
|
||||||
|
|
||||||
console.print(threads_table)
|
console.print(threads_table)
|
||||||
|
|
||||||
msgs_table = Table(title="Messages")
|
|
||||||
msgs_table.add_column("Key")
|
|
||||||
msgs_table.add_column("Value")
|
|
||||||
for key, value in stats["messages"].items():
|
|
||||||
msgs_table.add_row(key, str(value))
|
|
||||||
|
|
||||||
console.print(msgs_table)
|
|
||||||
|
|
||||||
packet_totals = Table(title="Packet Totals")
|
packet_totals = Table(title="Packet Totals")
|
||||||
packet_totals.add_column("Key")
|
packet_totals.add_column("Key")
|
||||||
packet_totals.add_column("Value")
|
packet_totals.add_column("Value")
|
||||||
packet_totals.add_row("Total Received", str(stats["packets"]["total_received"]))
|
packet_totals.add_row("Total Received", str(stats["PacketList"]["rx"]))
|
||||||
packet_totals.add_row("Total Sent", str(stats["packets"]["total_sent"]))
|
packet_totals.add_row("Total Sent", str(stats["PacketList"]["tx"]))
|
||||||
packet_totals.add_row("Total Tracked", str(stats["packets"]["total_tracked"]))
|
|
||||||
console.print(packet_totals)
|
console.print(packet_totals)
|
||||||
|
|
||||||
# Show each of the packet types
|
# Show each of the packet types
|
||||||
@ -114,47 +105,52 @@ def fetch_stats(ctx, host, port, magic_word):
|
|||||||
packets_table.add_column("Packet Type")
|
packets_table.add_column("Packet Type")
|
||||||
packets_table.add_column("TX")
|
packets_table.add_column("TX")
|
||||||
packets_table.add_column("RX")
|
packets_table.add_column("RX")
|
||||||
for key, value in stats["packets"]["by_type"].items():
|
for key, value in stats["PacketList"]["packets"].items():
|
||||||
packets_table.add_row(key, str(value["tx"]), str(value["rx"]))
|
packets_table.add_row(key, str(value["tx"]), str(value["rx"]))
|
||||||
|
|
||||||
console.print(packets_table)
|
console.print(packets_table)
|
||||||
|
|
||||||
if "plugins" in stats:
|
if "plugins" in stats:
|
||||||
count = len(stats["plugins"])
|
count = len(stats["PluginManager"])
|
||||||
plugins_table = Table(title=f"Plugins ({count})")
|
plugins_table = Table(title=f"Plugins ({count})")
|
||||||
plugins_table.add_column("Plugin")
|
plugins_table.add_column("Plugin")
|
||||||
plugins_table.add_column("Enabled")
|
plugins_table.add_column("Enabled")
|
||||||
plugins_table.add_column("Version")
|
plugins_table.add_column("Version")
|
||||||
plugins_table.add_column("TX")
|
plugins_table.add_column("TX")
|
||||||
plugins_table.add_column("RX")
|
plugins_table.add_column("RX")
|
||||||
for key, value in stats["plugins"].items():
|
plugins = stats["PluginManager"]
|
||||||
|
for key, value in plugins.items():
|
||||||
plugins_table.add_row(
|
plugins_table.add_row(
|
||||||
key,
|
key,
|
||||||
str(stats["plugins"][key]["enabled"]),
|
str(plugins[key]["enabled"]),
|
||||||
stats["plugins"][key]["version"],
|
plugins[key]["version"],
|
||||||
str(stats["plugins"][key]["tx"]),
|
str(plugins[key]["tx"]),
|
||||||
str(stats["plugins"][key]["rx"]),
|
str(plugins[key]["rx"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
console.print(plugins_table)
|
console.print(plugins_table)
|
||||||
|
|
||||||
if "seen_list" in stats["aprsd"]:
|
seen_list = stats.get("SeenList")
|
||||||
count = len(stats["aprsd"]["seen_list"])
|
|
||||||
|
if seen_list:
|
||||||
|
count = len(seen_list)
|
||||||
seen_table = Table(title=f"Seen List ({count})")
|
seen_table = Table(title=f"Seen List ({count})")
|
||||||
seen_table.add_column("Callsign")
|
seen_table.add_column("Callsign")
|
||||||
seen_table.add_column("Message Count")
|
seen_table.add_column("Message Count")
|
||||||
seen_table.add_column("Last Heard")
|
seen_table.add_column("Last Heard")
|
||||||
for key, value in stats["aprsd"]["seen_list"].items():
|
for key, value in seen_list.items():
|
||||||
seen_table.add_row(key, str(value["count"]), value["last"])
|
seen_table.add_row(key, str(value["count"]), value["last"])
|
||||||
|
|
||||||
console.print(seen_table)
|
console.print(seen_table)
|
||||||
|
|
||||||
if "watch_list" in stats["aprsd"]:
|
watch_list = stats.get("WatchList")
|
||||||
count = len(stats["aprsd"]["watch_list"])
|
|
||||||
|
if watch_list:
|
||||||
|
count = len(watch_list)
|
||||||
watch_table = Table(title=f"Watch List ({count})")
|
watch_table = Table(title=f"Watch List ({count})")
|
||||||
watch_table.add_column("Callsign")
|
watch_table.add_column("Callsign")
|
||||||
watch_table.add_column("Last Heard")
|
watch_table.add_column("Last Heard")
|
||||||
for key, value in stats["aprsd"]["watch_list"].items():
|
for key, value in watch_list.items():
|
||||||
watch_table.add_row(key, value["last"])
|
watch_table.add_row(key, value["last"])
|
||||||
|
|
||||||
console.print(watch_table)
|
console.print(watch_table)
|
||||||
|
@ -13,11 +13,11 @@ from oslo_config import cfg
|
|||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import cli_helper, utils
|
from aprsd import cli_helper
|
||||||
from aprsd import conf # noqa
|
from aprsd import conf # noqa
|
||||||
# local imports here
|
# local imports here
|
||||||
from aprsd.main import cli
|
from aprsd.main import cli
|
||||||
from aprsd.rpc import client as aprsd_rpc_client
|
from aprsd.threads import stats as stats_threads
|
||||||
|
|
||||||
|
|
||||||
# setup the global logger
|
# setup the global logger
|
||||||
@ -39,46 +39,48 @@ console = Console()
|
|||||||
@cli_helper.process_standard_options
|
@cli_helper.process_standard_options
|
||||||
def healthcheck(ctx, timeout):
|
def healthcheck(ctx, timeout):
|
||||||
"""Check the health of the running aprsd server."""
|
"""Check the health of the running aprsd server."""
|
||||||
console.log(f"APRSD HealthCheck version: {aprsd.__version__}")
|
ver_str = f"APRSD HealthCheck version: {aprsd.__version__}"
|
||||||
if not CONF.rpc_settings.enabled:
|
console.log(ver_str)
|
||||||
LOG.error("Must enable rpc_settings.enabled to use healthcheck")
|
|
||||||
sys.exit(-1)
|
|
||||||
if not CONF.rpc_settings.ip:
|
|
||||||
LOG.error("Must enable rpc_settings.ip to use healthcheck")
|
|
||||||
sys.exit(-1)
|
|
||||||
if not CONF.rpc_settings.magic_word:
|
|
||||||
LOG.error("Must enable rpc_settings.magic_word to use healthcheck")
|
|
||||||
sys.exit(-1)
|
|
||||||
|
|
||||||
with console.status(f"APRSD HealthCheck version: {aprsd.__version__}") as status:
|
with console.status(ver_str):
|
||||||
try:
|
try:
|
||||||
status.update(f"Contacting APRSD via RPC {CONF.rpc_settings.ip}")
|
stats_obj = stats_threads.StatsStore()
|
||||||
stats = aprsd_rpc_client.RPCClient().get_stats_dict()
|
stats_obj.load()
|
||||||
|
stats = stats_obj.data
|
||||||
|
# console.print(stats)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
console.log(f"Failed to fetch healthcheck : '{ex}'")
|
console.log(f"Failed to load stats: '{ex}'")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
else:
|
else:
|
||||||
|
now = datetime.datetime.now()
|
||||||
if not stats:
|
if not stats:
|
||||||
console.log("No stats from aprsd")
|
console.log("No stats from aprsd")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
email_thread_last_update = stats["email"]["thread_last_update"]
|
|
||||||
|
email_stats = stats.get("EmailStats")
|
||||||
|
if email_stats:
|
||||||
|
email_thread_last_update = email_stats["last_check_time"]
|
||||||
|
|
||||||
if email_thread_last_update != "never":
|
if email_thread_last_update != "never":
|
||||||
delta = utils.parse_delta_str(email_thread_last_update)
|
d = now - email_thread_last_update
|
||||||
d = datetime.timedelta(**delta)
|
|
||||||
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
||||||
max_delta = datetime.timedelta(**max_timeout)
|
max_delta = datetime.timedelta(**max_timeout)
|
||||||
if d > max_delta:
|
if d > max_delta:
|
||||||
console.log(f"Email thread is very old! {d}")
|
console.log(f"Email thread is very old! {d}")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
aprsis_last_update = stats["aprs-is"]["last_update"]
|
client_stats = stats.get("APRSClientStats")
|
||||||
delta = utils.parse_delta_str(aprsis_last_update)
|
if not client_stats:
|
||||||
d = datetime.timedelta(**delta)
|
console.log("No APRSClientStats")
|
||||||
|
sys.exit(-1)
|
||||||
|
else:
|
||||||
|
aprsis_last_update = client_stats["server_keepalive"]
|
||||||
|
d = now - aprsis_last_update
|
||||||
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
max_timeout = {"hours": 0.0, "minutes": 5, "seconds": 0}
|
||||||
max_delta = datetime.timedelta(**max_timeout)
|
max_delta = datetime.timedelta(**max_timeout)
|
||||||
if d > max_delta:
|
if d > max_delta:
|
||||||
LOG.error(f"APRS-IS last update is very old! {d}")
|
LOG.error(f"APRS-IS last update is very old! {d}")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
|
console.log("OK")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
@ -21,7 +21,7 @@ from aprsd import cli_helper
|
|||||||
from aprsd import plugin as aprsd_plugin
|
from aprsd import plugin as aprsd_plugin
|
||||||
from aprsd.main import cli
|
from aprsd.main import cli
|
||||||
from aprsd.plugins import (
|
from aprsd.plugins import (
|
||||||
email, fortune, location, notify, ping, query, time, version, weather,
|
email, fortune, location, notify, ping, time, version, weather,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -122,7 +122,7 @@ def get_installed_extensions():
|
|||||||
|
|
||||||
|
|
||||||
def show_built_in_plugins(console):
|
def show_built_in_plugins(console):
|
||||||
modules = [email, fortune, location, notify, ping, query, time, version, weather]
|
modules = [email, fortune, location, notify, ping, time, version, weather]
|
||||||
plugins = []
|
plugins = []
|
||||||
|
|
||||||
for module in modules:
|
for module in modules:
|
||||||
|
@ -15,10 +15,15 @@ from rich.console import Console
|
|||||||
|
|
||||||
# local imports here
|
# local imports here
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import cli_helper, client, packets, plugin, stats, threads
|
from aprsd import cli_helper, packets, plugin, threads
|
||||||
|
from aprsd.client import client_factory
|
||||||
from aprsd.main import cli
|
from aprsd.main import cli
|
||||||
from aprsd.rpc import server as rpc_server
|
from aprsd.packets import collector as packet_collector
|
||||||
from aprsd.threads import rx
|
from aprsd.packets import log as packet_log
|
||||||
|
from aprsd.packets import seen_list
|
||||||
|
from aprsd.stats import collector
|
||||||
|
from aprsd.threads import keep_alive, rx
|
||||||
|
from aprsd.threads import stats as stats_thread
|
||||||
|
|
||||||
|
|
||||||
# setup the global logger
|
# setup the global logger
|
||||||
@ -37,7 +42,7 @@ def signal_handler(sig, frame):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
LOG.info(stats.APRSDStats())
|
LOG.info(collector.Collector().collect())
|
||||||
|
|
||||||
|
|
||||||
class APRSDListenThread(rx.APRSDRXThread):
|
class APRSDListenThread(rx.APRSDRXThread):
|
||||||
@ -53,29 +58,33 @@ class APRSDListenThread(rx.APRSDRXThread):
|
|||||||
filters = {
|
filters = {
|
||||||
packets.Packet.__name__: packets.Packet,
|
packets.Packet.__name__: packets.Packet,
|
||||||
packets.AckPacket.__name__: packets.AckPacket,
|
packets.AckPacket.__name__: packets.AckPacket,
|
||||||
|
packets.BeaconPacket.__name__: packets.BeaconPacket,
|
||||||
packets.GPSPacket.__name__: packets.GPSPacket,
|
packets.GPSPacket.__name__: packets.GPSPacket,
|
||||||
packets.MessagePacket.__name__: packets.MessagePacket,
|
packets.MessagePacket.__name__: packets.MessagePacket,
|
||||||
packets.MicEPacket.__name__: packets.MicEPacket,
|
packets.MicEPacket.__name__: packets.MicEPacket,
|
||||||
|
packets.ObjectPacket.__name__: packets.ObjectPacket,
|
||||||
|
packets.StatusPacket.__name__: packets.StatusPacket,
|
||||||
|
packets.ThirdPartyPacket.__name__: packets.ThirdPartyPacket,
|
||||||
packets.WeatherPacket.__name__: packets.WeatherPacket,
|
packets.WeatherPacket.__name__: packets.WeatherPacket,
|
||||||
|
packets.UnknownPacket.__name__: packets.UnknownPacket,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.packet_filter:
|
if self.packet_filter:
|
||||||
filter_class = filters[self.packet_filter]
|
filter_class = filters[self.packet_filter]
|
||||||
if isinstance(packet, filter_class):
|
if isinstance(packet, filter_class):
|
||||||
packet.log(header="RX")
|
packet_log.log(packet)
|
||||||
if self.plugin_manager:
|
if self.plugin_manager:
|
||||||
# Don't do anything with the reply
|
# Don't do anything with the reply
|
||||||
# This is the listen only command.
|
# This is the listen only command.
|
||||||
self.plugin_manager.run(packet)
|
self.plugin_manager.run(packet)
|
||||||
else:
|
else:
|
||||||
|
packet_log.log(packet)
|
||||||
if self.plugin_manager:
|
if self.plugin_manager:
|
||||||
# Don't do anything with the reply.
|
# Don't do anything with the reply.
|
||||||
# This is the listen only command.
|
# This is the listen only command.
|
||||||
self.plugin_manager.run(packet)
|
self.plugin_manager.run(packet)
|
||||||
else:
|
|
||||||
packet.log(header="RX")
|
|
||||||
|
|
||||||
packets.PacketList().rx(packet)
|
packet_collector.PacketCollector().rx(packet)
|
||||||
|
|
||||||
|
|
||||||
@cli.command()
|
@cli.command()
|
||||||
@ -96,11 +105,16 @@ class APRSDListenThread(rx.APRSDRXThread):
|
|||||||
"--packet-filter",
|
"--packet-filter",
|
||||||
type=click.Choice(
|
type=click.Choice(
|
||||||
[
|
[
|
||||||
packets.Packet.__name__,
|
|
||||||
packets.AckPacket.__name__,
|
packets.AckPacket.__name__,
|
||||||
|
packets.BeaconPacket.__name__,
|
||||||
packets.GPSPacket.__name__,
|
packets.GPSPacket.__name__,
|
||||||
packets.MicEPacket.__name__,
|
packets.MicEPacket.__name__,
|
||||||
packets.MessagePacket.__name__,
|
packets.MessagePacket.__name__,
|
||||||
|
packets.ObjectPacket.__name__,
|
||||||
|
packets.RejectPacket.__name__,
|
||||||
|
packets.StatusPacket.__name__,
|
||||||
|
packets.ThirdPartyPacket.__name__,
|
||||||
|
packets.UnknownPacket.__name__,
|
||||||
packets.WeatherPacket.__name__,
|
packets.WeatherPacket.__name__,
|
||||||
],
|
],
|
||||||
case_sensitive=False,
|
case_sensitive=False,
|
||||||
@ -159,32 +173,32 @@ def listen(
|
|||||||
LOG.info(f"APRSD Listen Started version: {aprsd.__version__}")
|
LOG.info(f"APRSD Listen Started version: {aprsd.__version__}")
|
||||||
|
|
||||||
CONF.log_opt_values(LOG, logging.DEBUG)
|
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||||
|
collector.Collector()
|
||||||
|
|
||||||
# Try and load saved MsgTrack list
|
# Try and load saved MsgTrack list
|
||||||
LOG.debug("Loading saved MsgTrack object.")
|
LOG.debug("Loading saved MsgTrack object.")
|
||||||
|
|
||||||
# Initialize the client factory and create
|
# Initialize the client factory and create
|
||||||
# The correct client object ready for use
|
# The correct client object ready for use
|
||||||
client.ClientFactory.setup()
|
|
||||||
# Make sure we have 1 client transport enabled
|
# Make sure we have 1 client transport enabled
|
||||||
if not client.factory.is_client_enabled():
|
if not client_factory.is_client_enabled():
|
||||||
LOG.error("No Clients are enabled in config.")
|
LOG.error("No Clients are enabled in config.")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
# Creates the client object
|
# Creates the client object
|
||||||
LOG.info("Creating client connection")
|
LOG.info("Creating client connection")
|
||||||
aprs_client = client.factory.create()
|
aprs_client = client_factory.create()
|
||||||
LOG.info(aprs_client)
|
LOG.info(aprs_client)
|
||||||
|
|
||||||
LOG.debug(f"Filter by '{filter}'")
|
LOG.debug(f"Filter by '{filter}'")
|
||||||
aprs_client.set_filter(filter)
|
aprs_client.set_filter(filter)
|
||||||
|
|
||||||
keepalive = threads.KeepAliveThread()
|
keepalive = keep_alive.KeepAliveThread()
|
||||||
keepalive.start()
|
# keepalive.start()
|
||||||
|
|
||||||
if CONF.rpc_settings.enabled:
|
if not CONF.enable_seen_list:
|
||||||
rpc = rpc_server.APRSDRPCThread()
|
# just deregister the class from the packet collector
|
||||||
rpc.start()
|
packet_collector.PacketCollector().unregister(seen_list.SeenList)
|
||||||
|
|
||||||
pm = None
|
pm = None
|
||||||
pm = plugin.PluginManager()
|
pm = plugin.PluginManager()
|
||||||
@ -196,6 +210,8 @@ def listen(
|
|||||||
"Not Loading any plugins use --load-plugins to load what's "
|
"Not Loading any plugins use --load-plugins to load what's "
|
||||||
"defined in the config file.",
|
"defined in the config file.",
|
||||||
)
|
)
|
||||||
|
stats = stats_thread.APRSDStatsStoreThread()
|
||||||
|
stats.start()
|
||||||
|
|
||||||
LOG.debug("Create APRSDListenThread")
|
LOG.debug("Create APRSDListenThread")
|
||||||
listen_thread = APRSDListenThread(
|
listen_thread = APRSDListenThread(
|
||||||
@ -205,10 +221,10 @@ def listen(
|
|||||||
)
|
)
|
||||||
LOG.debug("Start APRSDListenThread")
|
LOG.debug("Start APRSDListenThread")
|
||||||
listen_thread.start()
|
listen_thread.start()
|
||||||
|
|
||||||
|
keepalive.start()
|
||||||
LOG.debug("keepalive Join")
|
LOG.debug("keepalive Join")
|
||||||
keepalive.join()
|
keepalive.join()
|
||||||
LOG.debug("listen_thread Join")
|
LOG.debug("listen_thread Join")
|
||||||
listen_thread.join()
|
listen_thread.join()
|
||||||
|
stats.join()
|
||||||
if CONF.rpc_settings.enabled:
|
|
||||||
rpc.join()
|
|
||||||
|
@ -8,9 +8,11 @@ import click
|
|||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import cli_helper, client, packets
|
from aprsd import cli_helper, packets
|
||||||
from aprsd import conf # noqa : F401
|
from aprsd import conf # noqa : F401
|
||||||
|
from aprsd.client import client_factory
|
||||||
from aprsd.main import cli
|
from aprsd.main import cli
|
||||||
|
from aprsd.packets import collector
|
||||||
from aprsd.threads import tx
|
from aprsd.threads import tx
|
||||||
|
|
||||||
|
|
||||||
@ -76,7 +78,6 @@ def send_message(
|
|||||||
aprs_login = CONF.aprs_network.login
|
aprs_login = CONF.aprs_network.login
|
||||||
|
|
||||||
if not aprs_password:
|
if not aprs_password:
|
||||||
LOG.warning(CONF.aprs_network.password)
|
|
||||||
if not CONF.aprs_network.password:
|
if not CONF.aprs_network.password:
|
||||||
click.echo("Must set --aprs-password or APRS_PASSWORD")
|
click.echo("Must set --aprs-password or APRS_PASSWORD")
|
||||||
ctx.exit(-1)
|
ctx.exit(-1)
|
||||||
@ -102,9 +103,9 @@ def send_message(
|
|||||||
|
|
||||||
def rx_packet(packet):
|
def rx_packet(packet):
|
||||||
global got_ack, got_response
|
global got_ack, got_response
|
||||||
cl = client.factory.create()
|
cl = client_factory.create()
|
||||||
packet = cl.decode_packet(packet)
|
packet = cl.decode_packet(packet)
|
||||||
packets.PacketList().rx(packet)
|
collector.PacketCollector().rx(packet)
|
||||||
packet.log("RX")
|
packet.log("RX")
|
||||||
# LOG.debug("Got packet back {}".format(packet))
|
# LOG.debug("Got packet back {}".format(packet))
|
||||||
if isinstance(packet, packets.AckPacket):
|
if isinstance(packet, packets.AckPacket):
|
||||||
@ -130,8 +131,7 @@ def send_message(
|
|||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
client.ClientFactory.setup()
|
client_factory.create().client
|
||||||
client.factory.create().client
|
|
||||||
except LoginError:
|
except LoginError:
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
@ -163,7 +163,7 @@ def send_message(
|
|||||||
# This will register a packet consumer with aprslib
|
# This will register a packet consumer with aprslib
|
||||||
# When new packets come in the consumer will process
|
# When new packets come in the consumer will process
|
||||||
# the packet
|
# the packet
|
||||||
aprs_client = client.factory.create().client
|
aprs_client = client_factory.create().client
|
||||||
aprs_client.consumer(rx_packet, raw=False)
|
aprs_client.consumer(rx_packet, raw=False)
|
||||||
except aprslib.exceptions.ConnectionDrop:
|
except aprslib.exceptions.ConnectionDrop:
|
||||||
LOG.error("Connection dropped, reconnecting")
|
LOG.error("Connection dropped, reconnecting")
|
||||||
|
@ -6,12 +6,16 @@ import click
|
|||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import cli_helper, client
|
from aprsd import cli_helper
|
||||||
from aprsd import main as aprsd_main
|
from aprsd import main as aprsd_main
|
||||||
from aprsd import packets, plugin, threads, utils
|
from aprsd import packets, plugin, threads, utils
|
||||||
|
from aprsd.client import client_factory
|
||||||
from aprsd.main import cli
|
from aprsd.main import cli
|
||||||
from aprsd.rpc import server as rpc_server
|
from aprsd.packets import collector as packet_collector
|
||||||
from aprsd.threads import registry, rx, tx
|
from aprsd.packets import seen_list
|
||||||
|
from aprsd.threads import keep_alive, log_monitor, registry, rx
|
||||||
|
from aprsd.threads import stats as stats_thread
|
||||||
|
from aprsd.threads import tx
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@ -46,7 +50,14 @@ def server(ctx, flush):
|
|||||||
|
|
||||||
# Initialize the client factory and create
|
# Initialize the client factory and create
|
||||||
# The correct client object ready for use
|
# The correct client object ready for use
|
||||||
client.ClientFactory.setup()
|
if not client_factory.is_client_enabled():
|
||||||
|
LOG.error("No Clients are enabled in config.")
|
||||||
|
sys.exit(-1)
|
||||||
|
|
||||||
|
# Creates the client object
|
||||||
|
LOG.info("Creating client connection")
|
||||||
|
aprs_client = client_factory.create()
|
||||||
|
LOG.info(aprs_client)
|
||||||
|
|
||||||
# Create the initial PM singleton and Register plugins
|
# Create the initial PM singleton and Register plugins
|
||||||
# We register plugins first here so we can register each
|
# We register plugins first here so we can register each
|
||||||
@ -68,18 +79,14 @@ def server(ctx, flush):
|
|||||||
LOG.info(p)
|
LOG.info(p)
|
||||||
|
|
||||||
# Make sure we have 1 client transport enabled
|
# Make sure we have 1 client transport enabled
|
||||||
if not client.factory.is_client_enabled():
|
if not client_factory.is_client_enabled():
|
||||||
LOG.error("No Clients are enabled in config.")
|
LOG.error("No Clients are enabled in config.")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
if not client.factory.is_client_configured():
|
if not client_factory.is_client_configured():
|
||||||
LOG.error("APRS client is not properly configured in config file.")
|
LOG.error("APRS client is not properly configured in config file.")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
# Creates the client object
|
|
||||||
# LOG.info("Creating client connection")
|
|
||||||
# client.factory.create().client
|
|
||||||
|
|
||||||
# Now load the msgTrack from disk if any
|
# Now load the msgTrack from disk if any
|
||||||
packets.PacketList()
|
packets.PacketList()
|
||||||
if flush:
|
if flush:
|
||||||
@ -87,16 +94,25 @@ def server(ctx, flush):
|
|||||||
packets.PacketTrack().flush()
|
packets.PacketTrack().flush()
|
||||||
packets.WatchList().flush()
|
packets.WatchList().flush()
|
||||||
packets.SeenList().flush()
|
packets.SeenList().flush()
|
||||||
|
packets.PacketList().flush()
|
||||||
else:
|
else:
|
||||||
# Try and load saved MsgTrack list
|
# Try and load saved MsgTrack list
|
||||||
LOG.debug("Loading saved MsgTrack object.")
|
LOG.debug("Loading saved MsgTrack object.")
|
||||||
packets.PacketTrack().load()
|
packets.PacketTrack().load()
|
||||||
packets.WatchList().load()
|
packets.WatchList().load()
|
||||||
packets.SeenList().load()
|
packets.SeenList().load()
|
||||||
|
packets.PacketList().load()
|
||||||
|
|
||||||
keepalive = threads.KeepAliveThread()
|
keepalive = keep_alive.KeepAliveThread()
|
||||||
keepalive.start()
|
keepalive.start()
|
||||||
|
|
||||||
|
if not CONF.enable_seen_list:
|
||||||
|
# just deregister the class from the packet collector
|
||||||
|
packet_collector.PacketCollector().unregister(seen_list.SeenList)
|
||||||
|
|
||||||
|
stats_store_thread = stats_thread.APRSDStatsStoreThread()
|
||||||
|
stats_store_thread.start()
|
||||||
|
|
||||||
rx_thread = rx.APRSDPluginRXThread(
|
rx_thread = rx.APRSDPluginRXThread(
|
||||||
packet_queue=threads.packet_queue,
|
packet_queue=threads.packet_queue,
|
||||||
)
|
)
|
||||||
@ -106,7 +122,6 @@ def server(ctx, flush):
|
|||||||
rx_thread.start()
|
rx_thread.start()
|
||||||
process_thread.start()
|
process_thread.start()
|
||||||
|
|
||||||
packets.PacketTrack().restart()
|
|
||||||
if CONF.enable_beacon:
|
if CONF.enable_beacon:
|
||||||
LOG.info("Beacon Enabled. Starting Beacon thread.")
|
LOG.info("Beacon Enabled. Starting Beacon thread.")
|
||||||
bcn_thread = tx.BeaconSendThread()
|
bcn_thread = tx.BeaconSendThread()
|
||||||
@ -117,11 +132,9 @@ def server(ctx, flush):
|
|||||||
registry_thread = registry.APRSRegistryThread()
|
registry_thread = registry.APRSRegistryThread()
|
||||||
registry_thread.start()
|
registry_thread.start()
|
||||||
|
|
||||||
if CONF.rpc_settings.enabled:
|
if CONF.admin.web_enabled:
|
||||||
rpc = rpc_server.APRSDRPCThread()
|
log_monitor_thread = log_monitor.LogMonitorThread()
|
||||||
rpc.start()
|
log_monitor_thread.start()
|
||||||
log_monitor = threads.log_monitor.LogMonitorThread()
|
|
||||||
log_monitor.start()
|
|
||||||
|
|
||||||
rx_thread.join()
|
rx_thread.join()
|
||||||
process_thread.join()
|
process_thread.join()
|
||||||
|
@ -7,7 +7,6 @@ import sys
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from aprslib import util as aprslib_util
|
|
||||||
import click
|
import click
|
||||||
import flask
|
import flask
|
||||||
from flask import request
|
from flask import request
|
||||||
@ -22,15 +21,15 @@ import aprsd
|
|||||||
from aprsd import (
|
from aprsd import (
|
||||||
cli_helper, client, packets, plugin_utils, stats, threads, utils,
|
cli_helper, client, packets, plugin_utils, stats, threads, utils,
|
||||||
)
|
)
|
||||||
from aprsd.log import log
|
from aprsd.client import client_factory, kiss
|
||||||
from aprsd.main import cli
|
from aprsd.main import cli
|
||||||
from aprsd.threads import aprsd as aprsd_threads
|
from aprsd.threads import aprsd as aprsd_threads
|
||||||
from aprsd.threads import rx, tx
|
from aprsd.threads import keep_alive, rx, tx
|
||||||
from aprsd.utils import trace
|
from aprsd.utils import trace
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger()
|
||||||
auth = HTTPBasicAuth()
|
auth = HTTPBasicAuth()
|
||||||
users = {}
|
users = {}
|
||||||
socketio = None
|
socketio = None
|
||||||
@ -65,7 +64,7 @@ def signal_handler(sig, frame):
|
|||||||
time.sleep(1.5)
|
time.sleep(1.5)
|
||||||
# packets.WatchList().save()
|
# packets.WatchList().save()
|
||||||
# packets.SeenList().save()
|
# packets.SeenList().save()
|
||||||
LOG.info(stats.APRSDStats())
|
LOG.info(stats.stats_collector.collect())
|
||||||
LOG.info("Telling flask to bail.")
|
LOG.info("Telling flask to bail.")
|
||||||
signal.signal(signal.SIGTERM, sys.exit(0))
|
signal.signal(signal.SIGTERM, sys.exit(0))
|
||||||
|
|
||||||
@ -335,7 +334,6 @@ class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
|
|||||||
|
|
||||||
def process_our_message_packet(self, packet: packets.MessagePacket):
|
def process_our_message_packet(self, packet: packets.MessagePacket):
|
||||||
global callsign_locations
|
global callsign_locations
|
||||||
LOG.info(f"process MessagePacket {repr(packet)}")
|
|
||||||
# ok lets see if we have the location for the
|
# ok lets see if we have the location for the
|
||||||
# person we just sent a message to.
|
# person we just sent a message to.
|
||||||
from_call = packet.get("from_call").upper()
|
from_call = packet.get("from_call").upper()
|
||||||
@ -381,10 +379,10 @@ def _get_transport(stats):
|
|||||||
transport = "aprs-is"
|
transport = "aprs-is"
|
||||||
aprs_connection = (
|
aprs_connection = (
|
||||||
"APRS-IS Server: <a href='http://status.aprs2.net' >"
|
"APRS-IS Server: <a href='http://status.aprs2.net' >"
|
||||||
"{}</a>".format(stats["stats"]["aprs-is"]["server"])
|
"{}</a>".format(stats["APRSClientStats"]["server_string"])
|
||||||
)
|
)
|
||||||
elif client.KISSClient.is_enabled():
|
elif kiss.KISSClient.is_enabled():
|
||||||
transport = client.KISSClient.transport()
|
transport = kiss.KISSClient.transport()
|
||||||
if transport == client.TRANSPORT_TCPKISS:
|
if transport == client.TRANSPORT_TCPKISS:
|
||||||
aprs_connection = (
|
aprs_connection = (
|
||||||
"TCPKISS://{}:{}".format(
|
"TCPKISS://{}:{}".format(
|
||||||
@ -422,7 +420,7 @@ def index():
|
|||||||
html_template = "index.html"
|
html_template = "index.html"
|
||||||
LOG.debug(f"Template {html_template}")
|
LOG.debug(f"Template {html_template}")
|
||||||
|
|
||||||
transport, aprs_connection = _get_transport(stats)
|
transport, aprs_connection = _get_transport(stats["stats"])
|
||||||
LOG.debug(f"transport {transport} aprs_connection {aprs_connection}")
|
LOG.debug(f"transport {transport} aprs_connection {aprs_connection}")
|
||||||
|
|
||||||
stats["transport"] = transport
|
stats["transport"] = transport
|
||||||
@ -457,27 +455,28 @@ def send_message_status():
|
|||||||
|
|
||||||
|
|
||||||
def _stats():
|
def _stats():
|
||||||
stats_obj = stats.APRSDStats()
|
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
|
|
||||||
time_format = "%m-%d-%Y %H:%M:%S"
|
time_format = "%m-%d-%Y %H:%M:%S"
|
||||||
stats_dict = stats_obj.stats()
|
stats_dict = stats.stats_collector.collect(serializable=True)
|
||||||
# Webchat doesnt need these
|
# Webchat doesnt need these
|
||||||
if "watch_list" in stats_dict["aprsd"]:
|
if "WatchList" in stats_dict:
|
||||||
del stats_dict["aprsd"]["watch_list"]
|
del stats_dict["WatchList"]
|
||||||
if "seen_list" in stats_dict["aprsd"]:
|
if "SeenList" in stats_dict:
|
||||||
del stats_dict["aprsd"]["seen_list"]
|
del stats_dict["SeenList"]
|
||||||
if "threads" in stats_dict["aprsd"]:
|
if "APRSDThreadList" in stats_dict:
|
||||||
del stats_dict["aprsd"]["threads"]
|
del stats_dict["APRSDThreadList"]
|
||||||
# del stats_dict["email"]
|
if "PacketList" in stats_dict:
|
||||||
# del stats_dict["plugins"]
|
del stats_dict["PacketList"]
|
||||||
# del stats_dict["messages"]
|
if "EmailStats" in stats_dict:
|
||||||
|
del stats_dict["EmailStats"]
|
||||||
|
if "PluginManager" in stats_dict:
|
||||||
|
del stats_dict["PluginManager"]
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"time": now.strftime(time_format),
|
"time": now.strftime(time_format),
|
||||||
"stats": stats_dict,
|
"stats": stats_dict,
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@ -541,18 +540,27 @@ class SendMessageNamespace(Namespace):
|
|||||||
|
|
||||||
def on_gps(self, data):
|
def on_gps(self, data):
|
||||||
LOG.debug(f"WS on_GPS: {data}")
|
LOG.debug(f"WS on_GPS: {data}")
|
||||||
lat = aprslib_util.latitude_to_ddm(data["latitude"])
|
lat = data["latitude"]
|
||||||
long = aprslib_util.longitude_to_ddm(data["longitude"])
|
long = data["longitude"]
|
||||||
LOG.debug(f"Lat DDM {lat}")
|
LOG.debug(f"Lat {lat}")
|
||||||
LOG.debug(f"Long DDM {long}")
|
LOG.debug(f"Long {long}")
|
||||||
|
path = data.get("path", None)
|
||||||
|
if not path:
|
||||||
|
path = []
|
||||||
|
elif "," in path:
|
||||||
|
path_opts = path.split(",")
|
||||||
|
path = [x.strip() for x in path_opts]
|
||||||
|
else:
|
||||||
|
path = [path]
|
||||||
|
|
||||||
tx.send(
|
tx.send(
|
||||||
packets.GPSPacket(
|
packets.BeaconPacket(
|
||||||
from_call=CONF.callsign,
|
from_call=CONF.callsign,
|
||||||
to_call="APDW16",
|
to_call="APDW16",
|
||||||
latitude=lat,
|
latitude=lat,
|
||||||
longitude=long,
|
longitude=long,
|
||||||
comment="APRSD WebChat Beacon",
|
comment="APRSD WebChat Beacon",
|
||||||
|
path=path,
|
||||||
),
|
),
|
||||||
direct=True,
|
direct=True,
|
||||||
)
|
)
|
||||||
@ -572,8 +580,6 @@ class SendMessageNamespace(Namespace):
|
|||||||
def init_flask(loglevel, quiet):
|
def init_flask(loglevel, quiet):
|
||||||
global socketio, flask_app
|
global socketio, flask_app
|
||||||
|
|
||||||
log.setup_logging(loglevel, quiet)
|
|
||||||
|
|
||||||
socketio = SocketIO(
|
socketio = SocketIO(
|
||||||
flask_app, logger=False, engineio_logger=False,
|
flask_app, logger=False, engineio_logger=False,
|
||||||
async_mode="threading",
|
async_mode="threading",
|
||||||
@ -624,7 +630,7 @@ def webchat(ctx, flush, port):
|
|||||||
LOG.info(msg)
|
LOG.info(msg)
|
||||||
LOG.info(f"APRSD Started version: {aprsd.__version__}")
|
LOG.info(f"APRSD Started version: {aprsd.__version__}")
|
||||||
|
|
||||||
CONF.log_opt_values(LOG, logging.DEBUG)
|
CONF.log_opt_values(logging.getLogger(), logging.DEBUG)
|
||||||
user = CONF.admin.user
|
user = CONF.admin.user
|
||||||
users[user] = generate_password_hash(CONF.admin.password)
|
users[user] = generate_password_hash(CONF.admin.password)
|
||||||
if not port:
|
if not port:
|
||||||
@ -632,13 +638,12 @@ def webchat(ctx, flush, port):
|
|||||||
|
|
||||||
# Initialize the client factory and create
|
# Initialize the client factory and create
|
||||||
# The correct client object ready for use
|
# The correct client object ready for use
|
||||||
client.ClientFactory.setup()
|
|
||||||
# Make sure we have 1 client transport enabled
|
# Make sure we have 1 client transport enabled
|
||||||
if not client.factory.is_client_enabled():
|
if not client_factory.is_client_enabled():
|
||||||
LOG.error("No Clients are enabled in config.")
|
LOG.error("No Clients are enabled in config.")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
if not client.factory.is_client_configured():
|
if not client_factory.is_client_configured():
|
||||||
LOG.error("APRS client is not properly configured in config file.")
|
LOG.error("APRS client is not properly configured in config file.")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
|
|
||||||
@ -647,7 +652,7 @@ def webchat(ctx, flush, port):
|
|||||||
packets.WatchList()
|
packets.WatchList()
|
||||||
packets.SeenList()
|
packets.SeenList()
|
||||||
|
|
||||||
keepalive = threads.KeepAliveThread()
|
keepalive = keep_alive.KeepAliveThread()
|
||||||
LOG.info("Start KeepAliveThread")
|
LOG.info("Start KeepAliveThread")
|
||||||
keepalive.start()
|
keepalive.start()
|
||||||
|
|
||||||
|
@ -15,10 +15,6 @@ watch_list_group = cfg.OptGroup(
|
|||||||
name="watch_list",
|
name="watch_list",
|
||||||
title="Watch List settings",
|
title="Watch List settings",
|
||||||
)
|
)
|
||||||
rpc_group = cfg.OptGroup(
|
|
||||||
name="rpc_settings",
|
|
||||||
title="RPC Settings for admin <--> web",
|
|
||||||
)
|
|
||||||
webchat_group = cfg.OptGroup(
|
webchat_group = cfg.OptGroup(
|
||||||
name="webchat",
|
name="webchat",
|
||||||
title="Settings specific to the webchat command",
|
title="Settings specific to the webchat command",
|
||||||
@ -101,6 +97,45 @@ aprsd_opts = [
|
|||||||
default=None,
|
default=None,
|
||||||
help="Longitude for the GPS Beacon button. If not set, the button will not be enabled.",
|
help="Longitude for the GPS Beacon button. If not set, the button will not be enabled.",
|
||||||
),
|
),
|
||||||
|
cfg.StrOpt(
|
||||||
|
"log_packet_format",
|
||||||
|
choices=["compact", "multiline", "both"],
|
||||||
|
default="compact",
|
||||||
|
help="When logging packets 'compact' will use a single line formatted for each packet."
|
||||||
|
"'multiline' will use multiple lines for each packet and is the traditional format."
|
||||||
|
"both will log both compact and multiline.",
|
||||||
|
),
|
||||||
|
cfg.IntOpt(
|
||||||
|
"default_packet_send_count",
|
||||||
|
default=3,
|
||||||
|
help="The number of times to send a non ack packet before giving up.",
|
||||||
|
),
|
||||||
|
cfg.IntOpt(
|
||||||
|
"default_ack_send_count",
|
||||||
|
default=3,
|
||||||
|
help="The number of times to send an ack packet in response to recieving a packet.",
|
||||||
|
),
|
||||||
|
cfg.IntOpt(
|
||||||
|
"packet_list_maxlen",
|
||||||
|
default=100,
|
||||||
|
help="The maximum number of packets to store in the packet list.",
|
||||||
|
),
|
||||||
|
cfg.IntOpt(
|
||||||
|
"packet_list_stats_maxlen",
|
||||||
|
default=20,
|
||||||
|
help="The maximum number of packets to send in the stats dict for admin ui.",
|
||||||
|
),
|
||||||
|
cfg.BoolOpt(
|
||||||
|
"enable_seen_list",
|
||||||
|
default=True,
|
||||||
|
help="Enable the Callsign seen list tracking feature. This allows aprsd to keep track of "
|
||||||
|
"callsigns that have been seen and when they were last seen.",
|
||||||
|
),
|
||||||
|
cfg.BoolOpt(
|
||||||
|
"enable_packet_logging",
|
||||||
|
default=True,
|
||||||
|
help="Set this to False, to disable logging of packets to the log file.",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
watch_list_opts = [
|
watch_list_opts = [
|
||||||
@ -138,7 +173,7 @@ admin_opts = [
|
|||||||
default=False,
|
default=False,
|
||||||
help="Enable the Admin Web Interface",
|
help="Enable the Admin Web Interface",
|
||||||
),
|
),
|
||||||
cfg.IPOpt(
|
cfg.StrOpt(
|
||||||
"web_ip",
|
"web_ip",
|
||||||
default="0.0.0.0",
|
default="0.0.0.0",
|
||||||
help="The ip address to listen on",
|
help="The ip address to listen on",
|
||||||
@ -161,28 +196,6 @@ admin_opts = [
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
rpc_opts = [
|
|
||||||
cfg.BoolOpt(
|
|
||||||
"enabled",
|
|
||||||
default=True,
|
|
||||||
help="Enable RPC calls",
|
|
||||||
),
|
|
||||||
cfg.StrOpt(
|
|
||||||
"ip",
|
|
||||||
default="localhost",
|
|
||||||
help="The ip address to listen on",
|
|
||||||
),
|
|
||||||
cfg.PortOpt(
|
|
||||||
"port",
|
|
||||||
default=18861,
|
|
||||||
help="The port to listen on",
|
|
||||||
),
|
|
||||||
cfg.StrOpt(
|
|
||||||
"magic_word",
|
|
||||||
default=APRSD_DEFAULT_MAGIC_WORD,
|
|
||||||
help="Magic word to authenticate requests between client/server",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
enabled_plugins_opts = [
|
enabled_plugins_opts = [
|
||||||
cfg.ListOpt(
|
cfg.ListOpt(
|
||||||
@ -205,7 +218,7 @@ enabled_plugins_opts = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
webchat_opts = [
|
webchat_opts = [
|
||||||
cfg.IPOpt(
|
cfg.StrOpt(
|
||||||
"web_ip",
|
"web_ip",
|
||||||
default="0.0.0.0",
|
default="0.0.0.0",
|
||||||
help="The ip address to listen on",
|
help="The ip address to listen on",
|
||||||
@ -225,10 +238,15 @@ webchat_opts = [
|
|||||||
default=None,
|
default=None,
|
||||||
help="Longitude for the GPS Beacon button. If not set, the button will not be enabled.",
|
help="Longitude for the GPS Beacon button. If not set, the button will not be enabled.",
|
||||||
),
|
),
|
||||||
|
cfg.BoolOpt(
|
||||||
|
"disable_url_request_logging",
|
||||||
|
default=False,
|
||||||
|
help="Disable the logging of url requests in the webchat command.",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
registry_opts = [
|
registry_opts = [
|
||||||
cfg.StrOpt(
|
cfg.BoolOpt(
|
||||||
"enabled",
|
"enabled",
|
||||||
default=False,
|
default=False,
|
||||||
help="Enable sending aprs registry information. This will let the "
|
help="Enable sending aprs registry information. This will let the "
|
||||||
@ -268,8 +286,6 @@ def register_opts(config):
|
|||||||
config.register_opts(admin_opts, group=admin_group)
|
config.register_opts(admin_opts, group=admin_group)
|
||||||
config.register_group(watch_list_group)
|
config.register_group(watch_list_group)
|
||||||
config.register_opts(watch_list_opts, group=watch_list_group)
|
config.register_opts(watch_list_opts, group=watch_list_group)
|
||||||
config.register_group(rpc_group)
|
|
||||||
config.register_opts(rpc_opts, group=rpc_group)
|
|
||||||
config.register_group(webchat_group)
|
config.register_group(webchat_group)
|
||||||
config.register_opts(webchat_opts, group=webchat_group)
|
config.register_opts(webchat_opts, group=webchat_group)
|
||||||
config.register_group(registry_group)
|
config.register_group(registry_group)
|
||||||
@ -281,7 +297,6 @@ def list_opts():
|
|||||||
"DEFAULT": (aprsd_opts + enabled_plugins_opts),
|
"DEFAULT": (aprsd_opts + enabled_plugins_opts),
|
||||||
admin_group.name: admin_opts,
|
admin_group.name: admin_opts,
|
||||||
watch_list_group.name: watch_list_opts,
|
watch_list_group.name: watch_list_opts,
|
||||||
rpc_group.name: rpc_opts,
|
|
||||||
webchat_group.name: webchat_opts,
|
webchat_group.name: webchat_opts,
|
||||||
registry_group.name: registry_opts,
|
registry_group.name: registry_opts,
|
||||||
}
|
}
|
||||||
|
@ -6,12 +6,28 @@ import sys
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
from aprsd import conf
|
from aprsd.conf import log as conf_log
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
LOG = logging.getLogger("APRSD")
|
# LOG = logging.getLogger("APRSD")
|
||||||
logging_queue = queue.Queue()
|
LOG = logger
|
||||||
|
|
||||||
|
|
||||||
|
class QueueLatest(queue.Queue):
|
||||||
|
"""Custom Queue to keep only the latest N items.
|
||||||
|
|
||||||
|
This prevents the queue from blowing up in size.
|
||||||
|
"""
|
||||||
|
def put(self, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
super().put(*args, **kwargs)
|
||||||
|
except queue.Full:
|
||||||
|
self.queue.popleft()
|
||||||
|
super().put(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
logging_queue = QueueLatest(maxsize=200)
|
||||||
|
|
||||||
|
|
||||||
class InterceptHandler(logging.Handler):
|
class InterceptHandler(logging.Handler):
|
||||||
@ -38,7 +54,7 @@ def setup_logging(loglevel=None, quiet=False):
|
|||||||
if not loglevel:
|
if not loglevel:
|
||||||
log_level = CONF.logging.log_level
|
log_level = CONF.logging.log_level
|
||||||
else:
|
else:
|
||||||
log_level = conf.log.LOG_LEVELS[loglevel]
|
log_level = conf_log.LOG_LEVELS[loglevel]
|
||||||
|
|
||||||
# intercept everything at the root logger
|
# intercept everything at the root logger
|
||||||
logging.root.handlers = [InterceptHandler()]
|
logging.root.handlers = [InterceptHandler()]
|
||||||
@ -53,9 +69,19 @@ def setup_logging(loglevel=None, quiet=False):
|
|||||||
"aprslib.parsing",
|
"aprslib.parsing",
|
||||||
"aprslib.exceptions",
|
"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.
|
# We don't really want to see the aprslib parsing debug output.
|
||||||
disable_list = imap_list + aprslib_list
|
disable_list = imap_list + aprslib_list + webserver_list
|
||||||
|
|
||||||
# remove every other logger's handlers
|
# remove every other logger's handlers
|
||||||
# and propagate to root logger
|
# and propagate to root logger
|
||||||
@ -66,17 +92,29 @@ def setup_logging(loglevel=None, quiet=False):
|
|||||||
else:
|
else:
|
||||||
logging.getLogger(name).propagate = True
|
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 = [
|
handlers = [
|
||||||
{
|
{
|
||||||
"sink": sys.stdout, "serialize": False,
|
"sink": sys.stdout,
|
||||||
|
"serialize": False,
|
||||||
"format": CONF.logging.logformat,
|
"format": CONF.logging.logformat,
|
||||||
|
"colorize": True,
|
||||||
|
"level": log_level,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
if CONF.logging.logfile:
|
if CONF.logging.logfile:
|
||||||
handlers.append(
|
handlers.append(
|
||||||
{
|
{
|
||||||
"sink": CONF.logging.logfile, "serialize": False,
|
"sink": CONF.logging.logfile,
|
||||||
|
"serialize": False,
|
||||||
"format": CONF.logging.logformat,
|
"format": CONF.logging.logformat,
|
||||||
|
"colorize": False,
|
||||||
|
"level": log_level,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -90,8 +128,11 @@ def setup_logging(loglevel=None, quiet=False):
|
|||||||
{
|
{
|
||||||
"sink": qh, "serialize": False,
|
"sink": qh, "serialize": False,
|
||||||
"format": CONF.logging.logformat,
|
"format": CONF.logging.logformat,
|
||||||
|
"level": log_level,
|
||||||
|
"colorize": False,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
# configure loguru
|
# configure loguru
|
||||||
logger.configure(handlers=handlers)
|
logger.configure(handlers=handlers)
|
||||||
|
logger.level("DEBUG", color="<fg #BABABA>")
|
||||||
|
@ -24,18 +24,17 @@ import datetime
|
|||||||
import importlib.metadata as imp
|
import importlib.metadata as imp
|
||||||
from importlib.metadata import version as metadata_version
|
from importlib.metadata import version as metadata_version
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import click_completion
|
|
||||||
from oslo_config import cfg, generator
|
from oslo_config import cfg, generator
|
||||||
|
|
||||||
# local imports here
|
# local imports here
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import cli_helper, packets, stats, threads, utils
|
from aprsd import cli_helper, packets, threads, utils
|
||||||
|
from aprsd.stats import collector
|
||||||
|
|
||||||
|
|
||||||
# setup the global logger
|
# setup the global logger
|
||||||
@ -44,19 +43,6 @@ CONF = cfg.CONF
|
|||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||||
flask_enabled = False
|
flask_enabled = False
|
||||||
rpc_serv = None
|
|
||||||
|
|
||||||
|
|
||||||
def custom_startswith(string, incomplete):
|
|
||||||
"""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(cls=cli_helper.AliasedGroup, context_settings=CONTEXT_SETTINGS)
|
@click.group(cls=cli_helper.AliasedGroup, context_settings=CONTEXT_SETTINGS)
|
||||||
@ -96,7 +82,8 @@ def signal_handler(sig, frame):
|
|||||||
packets.PacketTrack().save()
|
packets.PacketTrack().save()
|
||||||
packets.WatchList().save()
|
packets.WatchList().save()
|
||||||
packets.SeenList().save()
|
packets.SeenList().save()
|
||||||
LOG.info(stats.APRSDStats())
|
packets.PacketList().save()
|
||||||
|
LOG.info(collector.Collector().collect())
|
||||||
# signal.signal(signal.SIGTERM, sys.exit(0))
|
# signal.signal(signal.SIGTERM, sys.exit(0))
|
||||||
# sys.exit(0)
|
# sys.exit(0)
|
||||||
|
|
||||||
@ -122,10 +109,25 @@ def check_version(ctx):
|
|||||||
def sample_config(ctx):
|
def sample_config(ctx):
|
||||||
"""Generate a sample Config file from aprsd and all installed plugins."""
|
"""Generate a sample Config file from aprsd and all installed plugins."""
|
||||||
|
|
||||||
|
def _get_selected_entry_points():
|
||||||
|
import sys
|
||||||
|
if sys.version_info < (3, 10):
|
||||||
|
all = imp.entry_points()
|
||||||
|
selected = []
|
||||||
|
if "oslo.config.opts" in all:
|
||||||
|
for x in all["oslo.config.opts"]:
|
||||||
|
if x.group == "oslo.config.opts":
|
||||||
|
selected.append(x)
|
||||||
|
else:
|
||||||
|
selected = imp.entry_points(group="oslo.config.opts")
|
||||||
|
|
||||||
|
return selected
|
||||||
|
|
||||||
def get_namespaces():
|
def get_namespaces():
|
||||||
args = []
|
args = []
|
||||||
|
|
||||||
selected = imp.entry_points(group="oslo.config.opts")
|
# selected = imp.entry_points(group="oslo.config.opts")
|
||||||
|
selected = _get_selected_entry_points()
|
||||||
for entry in selected:
|
for entry in selected:
|
||||||
if "aprsd" in entry.name:
|
if "aprsd" in entry.name:
|
||||||
args.append("--namespace")
|
args.append("--namespace")
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from aprsd.packets.core import ( # noqa: F401
|
from aprsd.packets.core import ( # noqa: F401
|
||||||
AckPacket, BeaconPacket, GPSPacket, MessagePacket, MicEPacket, Packet,
|
AckPacket, BeaconPacket, BulletinPacket, GPSPacket, MessagePacket,
|
||||||
RejectPacket, StatusPacket, WeatherPacket,
|
MicEPacket, ObjectPacket, Packet, RejectPacket, StatusPacket,
|
||||||
|
ThirdPartyPacket, UnknownPacket, WeatherPacket, factory,
|
||||||
)
|
)
|
||||||
from aprsd.packets.packet_list import PacketList # noqa: F401
|
from aprsd.packets.packet_list import PacketList # noqa: F401
|
||||||
from aprsd.packets.seen_list import SeenList # noqa: F401
|
from aprsd.packets.seen_list import SeenList # noqa: F401
|
||||||
|
56
aprsd/packets/collector.py
Normal file
56
aprsd/packets/collector.py
Normal file
@ -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
143
aprsd/packets/log.py
Normal file
143
aprsd/packets/log.py
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from aprsd.packets.core import AckPacket, RejectPacket
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger()
|
||||||
|
LOGU = logger
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
FROM_COLOR = "fg #C70039"
|
||||||
|
TO_COLOR = "fg #D033FF"
|
||||||
|
TX_COLOR = "red"
|
||||||
|
RX_COLOR = "green"
|
||||||
|
PACKET_COLOR = "cyan"
|
||||||
|
|
||||||
|
|
||||||
|
def log_multiline(packet, tx: Optional[bool] = False, header: Optional[bool] = True) -> None:
|
||||||
|
"""LOG a packet to the logfile."""
|
||||||
|
if not CONF.enable_packet_logging:
|
||||||
|
return
|
||||||
|
if CONF.log_packet_format == "compact":
|
||||||
|
return
|
||||||
|
|
||||||
|
# asdict(packet)
|
||||||
|
logit = ["\n"]
|
||||||
|
name = packet.__class__.__name__
|
||||||
|
|
||||||
|
if isinstance(packet, AckPacket):
|
||||||
|
pkt_max_send_count = CONF.default_ack_send_count
|
||||||
|
else:
|
||||||
|
pkt_max_send_count = CONF.default_packet_send_count
|
||||||
|
|
||||||
|
if header:
|
||||||
|
if tx:
|
||||||
|
header_str = f"<{TX_COLOR}>TX</{TX_COLOR}>"
|
||||||
|
logit.append(
|
||||||
|
f"{header_str}________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}> "
|
||||||
|
f"TX:{packet.send_count + 1} of {pkt_max_send_count}",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
header_str = f"<{RX_COLOR}>RX</{RX_COLOR}>"
|
||||||
|
logit.append(
|
||||||
|
f"{header_str}________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}>)",
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
header_str = ""
|
||||||
|
logit.append(f"__________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}>)")
|
||||||
|
# log_list.append(f" Packet : {packet.__class__.__name__}")
|
||||||
|
if packet.msgNo:
|
||||||
|
logit.append(f" Msg # : {packet.msgNo}")
|
||||||
|
if packet.from_call:
|
||||||
|
logit.append(f" From : <{FROM_COLOR}>{packet.from_call}</{FROM_COLOR}>")
|
||||||
|
if packet.to_call:
|
||||||
|
logit.append(f" To : <{TO_COLOR}>{packet.to_call}</{TO_COLOR}>")
|
||||||
|
if hasattr(packet, "path") and packet.path:
|
||||||
|
logit.append(f" Path : {'=>'.join(packet.path)}")
|
||||||
|
if hasattr(packet, "via") and packet.via:
|
||||||
|
logit.append(f" VIA : {packet.via}")
|
||||||
|
|
||||||
|
if not isinstance(packet, AckPacket) and not isinstance(packet, RejectPacket):
|
||||||
|
msg = packet.human_info
|
||||||
|
|
||||||
|
if msg:
|
||||||
|
msg = msg.replace("<", "\\<")
|
||||||
|
logit.append(f" Info : <light-yellow><b>{msg}</b></light-yellow>")
|
||||||
|
|
||||||
|
if hasattr(packet, "comment") and packet.comment:
|
||||||
|
logit.append(f" Comment : {packet.comment}")
|
||||||
|
|
||||||
|
raw = packet.raw.replace("<", "\\<")
|
||||||
|
logit.append(f" Raw : <fg #828282>{raw}</fg #828282>")
|
||||||
|
logit.append(f"{header_str}________(<{PACKET_COLOR}>{name}</{PACKET_COLOR}>)")
|
||||||
|
|
||||||
|
LOGU.opt(colors=True).info("\n".join(logit))
|
||||||
|
LOG.debug(repr(packet))
|
||||||
|
|
||||||
|
|
||||||
|
def log(packet, tx: Optional[bool] = False, header: Optional[bool] = True) -> None:
|
||||||
|
if not CONF.enable_packet_logging:
|
||||||
|
return
|
||||||
|
if CONF.log_packet_format == "multiline":
|
||||||
|
log_multiline(packet, tx, header)
|
||||||
|
return
|
||||||
|
|
||||||
|
logit = []
|
||||||
|
name = packet.__class__.__name__
|
||||||
|
if isinstance(packet, AckPacket):
|
||||||
|
pkt_max_send_count = CONF.default_ack_send_count
|
||||||
|
else:
|
||||||
|
pkt_max_send_count = CONF.default_packet_send_count
|
||||||
|
|
||||||
|
if header:
|
||||||
|
if tx:
|
||||||
|
via_color = "red"
|
||||||
|
arrow = f"<{via_color}>-></{via_color}>"
|
||||||
|
logit.append(
|
||||||
|
f"<red>TX {arrow}</red> "
|
||||||
|
f"<cyan>{name}</cyan>"
|
||||||
|
f":{packet.msgNo}"
|
||||||
|
f" ({packet.send_count + 1} of {pkt_max_send_count})",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
via_color = "fg #828282"
|
||||||
|
arrow = f"<{via_color}>-></{via_color}>"
|
||||||
|
left_arrow = f"<{via_color}><-</{via_color}>"
|
||||||
|
logit.append(
|
||||||
|
f"<fg #1AA730>RX</fg #1AA730> {left_arrow} "
|
||||||
|
f"<cyan>{name}</cyan>"
|
||||||
|
f":{packet.msgNo}",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
via_color = "green"
|
||||||
|
arrow = f"<{via_color}>-></{via_color}>"
|
||||||
|
logit.append(
|
||||||
|
f"<cyan>{name}</cyan>"
|
||||||
|
f":{packet.msgNo}",
|
||||||
|
)
|
||||||
|
|
||||||
|
tmp = None
|
||||||
|
if packet.path:
|
||||||
|
tmp = f"{arrow}".join(packet.path) + f"{arrow} "
|
||||||
|
|
||||||
|
logit.append(
|
||||||
|
f"<{FROM_COLOR}>{packet.from_call}</{FROM_COLOR}> {arrow}"
|
||||||
|
f"{tmp if tmp else ' '}"
|
||||||
|
f"<{TO_COLOR}>{packet.to_call}</{TO_COLOR}>",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not isinstance(packet, AckPacket) and not isinstance(packet, RejectPacket):
|
||||||
|
logit.append(":")
|
||||||
|
msg = packet.human_info
|
||||||
|
|
||||||
|
if msg:
|
||||||
|
msg = msg.replace("<", "\\<")
|
||||||
|
logit.append(f"<light-yellow><b>{msg}</b></light-yellow>")
|
||||||
|
|
||||||
|
LOGU.opt(colors=True).info(" ".join(logit))
|
||||||
|
log_multiline(packet, tx, header)
|
@ -1,99 +1,116 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from collections.abc import MutableMapping
|
|
||||||
import logging
|
import logging
|
||||||
import threading
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
import wrapt
|
|
||||||
|
|
||||||
from aprsd import stats
|
from aprsd.packets import collector, core
|
||||||
from aprsd.packets import seen_list
|
from aprsd.utils import objectstore
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class PacketList(MutableMapping):
|
class PacketList(objectstore.ObjectStoreMixin):
|
||||||
|
"""Class to keep track of the packets we tx/rx."""
|
||||||
_instance = None
|
_instance = None
|
||||||
lock = threading.Lock()
|
|
||||||
_total_rx: int = 0
|
_total_rx: int = 0
|
||||||
_total_tx: int = 0
|
_total_tx: int = 0
|
||||||
types = {}
|
maxlen: int = 100
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super().__new__(cls)
|
cls._instance = super().__new__(cls)
|
||||||
cls._maxlen = 100
|
cls._instance.maxlen = CONF.packet_list_maxlen
|
||||||
cls.d = OrderedDict()
|
cls._instance._init_data()
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
def _init_data(self):
|
||||||
def rx(self, packet):
|
self.data = {
|
||||||
|
"types": {},
|
||||||
|
"packets": OrderedDict(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def rx(self, packet: type[core.Packet]):
|
||||||
"""Add a packet that was received."""
|
"""Add a packet that was received."""
|
||||||
|
with self.lock:
|
||||||
self._total_rx += 1
|
self._total_rx += 1
|
||||||
self._add(packet)
|
self._add(packet)
|
||||||
ptype = packet.__class__.__name__
|
ptype = packet.__class__.__name__
|
||||||
if not ptype in self.types:
|
if ptype not in self.data["types"]:
|
||||||
self.types[ptype] = {"tx": 0, "rx": 0}
|
self.data["types"][ptype] = {"tx": 0, "rx": 0}
|
||||||
self.types[ptype]["rx"] += 1
|
self.data["types"][ptype]["rx"] += 1
|
||||||
seen_list.SeenList().update_seen(packet)
|
|
||||||
stats.APRSDStats().rx(packet)
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
def tx(self, packet: type[core.Packet]):
|
||||||
def tx(self, packet):
|
|
||||||
"""Add a packet that was received."""
|
"""Add a packet that was received."""
|
||||||
|
with self.lock:
|
||||||
self._total_tx += 1
|
self._total_tx += 1
|
||||||
self._add(packet)
|
self._add(packet)
|
||||||
ptype = packet.__class__.__name__
|
ptype = packet.__class__.__name__
|
||||||
if not ptype in self.types:
|
if ptype not in self.data["types"]:
|
||||||
self.types[ptype] = {"tx": 0, "rx": 0}
|
self.data["types"][ptype] = {"tx": 0, "rx": 0}
|
||||||
self.types[ptype]["tx"] += 1
|
self.data["types"][ptype]["tx"] += 1
|
||||||
seen_list.SeenList().update_seen(packet)
|
|
||||||
stats.APRSDStats().tx(packet)
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def add(self, packet):
|
def add(self, packet):
|
||||||
|
with self.lock:
|
||||||
self._add(packet)
|
self._add(packet)
|
||||||
|
|
||||||
def _add(self, packet):
|
def _add(self, packet):
|
||||||
self[packet.key] = packet
|
if not self.data.get("packets"):
|
||||||
|
self._init_data()
|
||||||
|
if packet.key in self.data["packets"]:
|
||||||
|
self.data["packets"].move_to_end(packet.key)
|
||||||
|
elif len(self.data["packets"]) == self.maxlen:
|
||||||
|
self.data["packets"].popitem(last=False)
|
||||||
|
self.data["packets"][packet.key] = packet
|
||||||
|
|
||||||
def copy(self):
|
|
||||||
return self.d.copy()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def maxlen(self):
|
|
||||||
return self._maxlen
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def find(self, packet):
|
def find(self, packet):
|
||||||
return self.get(packet.key)
|
with self.lock:
|
||||||
|
return self.data["packets"][packet.key]
|
||||||
def __getitem__(self, key):
|
|
||||||
# self.d.move_to_end(key)
|
|
||||||
return self.d[key]
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
if key in self.d:
|
|
||||||
self.d.move_to_end(key)
|
|
||||||
elif len(self.d) == self.maxlen:
|
|
||||||
self.d.popitem(last=False)
|
|
||||||
self.d[key] = value
|
|
||||||
|
|
||||||
def __delitem__(self, key):
|
|
||||||
del self.d[key]
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return self.d.__iter__()
|
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return len(self.d)
|
with self.lock:
|
||||||
|
return len(self.data["packets"])
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def total_rx(self):
|
def total_rx(self):
|
||||||
|
with self.lock:
|
||||||
return self._total_rx
|
return self._total_rx
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def total_tx(self):
|
def total_tx(self):
|
||||||
|
with self.lock:
|
||||||
return self._total_tx
|
return self._total_tx
|
||||||
|
|
||||||
|
def stats(self, serializable=False) -> dict:
|
||||||
|
# limit the number of packets to return to 50
|
||||||
|
with self.lock:
|
||||||
|
tmp = OrderedDict(
|
||||||
|
reversed(
|
||||||
|
list(
|
||||||
|
self.data.get("packets", OrderedDict()).items(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
pkts = []
|
||||||
|
count = 1
|
||||||
|
for packet in tmp:
|
||||||
|
pkts.append(tmp[packet])
|
||||||
|
count += 1
|
||||||
|
if count > CONF.packet_list_stats_maxlen:
|
||||||
|
break
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
"total_tracked": self._total_rx + self._total_rx,
|
||||||
|
"rx": self._total_rx,
|
||||||
|
"tx": self._total_tx,
|
||||||
|
"types": self.data.get("types", []),
|
||||||
|
"packet_count": len(self.data.get("packets", [])),
|
||||||
|
"maxlen": self.maxlen,
|
||||||
|
"packets": pkts,
|
||||||
|
}
|
||||||
|
return stats
|
||||||
|
|
||||||
|
|
||||||
|
# Now register the PacketList with the collector
|
||||||
|
# every packet we RX and TX goes through the collector
|
||||||
|
# for processing for whatever reason is needed.
|
||||||
|
collector.PacketCollector().register(PacketList)
|
||||||
|
@ -1,10 +1,9 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import threading
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
import wrapt
|
|
||||||
|
|
||||||
|
from aprsd.packets import collector, core
|
||||||
from aprsd.utils import objectstore
|
from aprsd.utils import objectstore
|
||||||
|
|
||||||
|
|
||||||
@ -16,18 +15,22 @@ class SeenList(objectstore.ObjectStoreMixin):
|
|||||||
"""Global callsign seen list."""
|
"""Global callsign seen list."""
|
||||||
|
|
||||||
_instance = None
|
_instance = None
|
||||||
lock = threading.Lock()
|
|
||||||
data: dict = {}
|
data: dict = {}
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super().__new__(cls)
|
cls._instance = super().__new__(cls)
|
||||||
cls._instance._init_store()
|
|
||||||
cls._instance.data = {}
|
cls._instance.data = {}
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
def stats(self, serializable=False):
|
||||||
def update_seen(self, packet):
|
"""Return the stats for the PacketTrack class."""
|
||||||
|
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
|
callsign = None
|
||||||
if packet.from_call:
|
if packet.from_call:
|
||||||
callsign = packet.from_call
|
callsign = packet.from_call
|
||||||
@ -39,5 +42,13 @@ class SeenList(objectstore.ObjectStoreMixin):
|
|||||||
"last": None,
|
"last": None,
|
||||||
"count": 0,
|
"count": 0,
|
||||||
}
|
}
|
||||||
self.data[callsign]["last"] = str(datetime.datetime.now())
|
self.data[callsign]["last"] = datetime.datetime.now()
|
||||||
self.data[callsign]["count"] += 1
|
self.data[callsign]["count"] += 1
|
||||||
|
|
||||||
|
def tx(self, packet: type[core.Packet]):
|
||||||
|
"""We don't care about TX packets."""
|
||||||
|
|
||||||
|
|
||||||
|
# Register with the packet collector so we can process the packet
|
||||||
|
# when we get it off the client (network)
|
||||||
|
collector.PacketCollector().register(SeenList)
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import threading
|
import logging
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
import wrapt
|
|
||||||
|
|
||||||
from aprsd.threads import tx
|
from aprsd.packets import collector, core
|
||||||
from aprsd.utils import objectstore
|
from aprsd.utils import objectstore
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class PacketTrack(objectstore.ObjectStoreMixin):
|
class PacketTrack(objectstore.ObjectStoreMixin):
|
||||||
@ -26,7 +26,6 @@ class PacketTrack(objectstore.ObjectStoreMixin):
|
|||||||
|
|
||||||
_instance = None
|
_instance = None
|
||||||
_start_time = None
|
_start_time = None
|
||||||
lock = threading.Lock()
|
|
||||||
|
|
||||||
data: dict = {}
|
data: dict = {}
|
||||||
total_tracked: int = 0
|
total_tracked: int = 0
|
||||||
@ -38,74 +37,73 @@ class PacketTrack(objectstore.ObjectStoreMixin):
|
|||||||
cls._instance._init_store()
|
cls._instance._init_store()
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def __getitem__(self, name):
|
def __getitem__(self, name):
|
||||||
|
with self.lock:
|
||||||
return self.data[name]
|
return self.data[name]
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
|
with self.lock:
|
||||||
return iter(self.data)
|
return iter(self.data)
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def keys(self):
|
def keys(self):
|
||||||
|
with self.lock:
|
||||||
return self.data.keys()
|
return self.data.keys()
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def items(self):
|
def items(self):
|
||||||
|
with self.lock:
|
||||||
return self.data.items()
|
return self.data.items()
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def values(self):
|
def values(self):
|
||||||
|
with self.lock:
|
||||||
return self.data.values()
|
return self.data.values()
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
def stats(self, serializable=False):
|
||||||
def __len__(self):
|
with self.lock:
|
||||||
return len(self.data)
|
stats = {
|
||||||
|
"total_tracked": self.total_tracked,
|
||||||
|
}
|
||||||
|
pkts = {}
|
||||||
|
for key in self.data:
|
||||||
|
last_send_time = self.data[key].last_send_time
|
||||||
|
pkts[key] = {
|
||||||
|
"last_send_time": last_send_time,
|
||||||
|
"send_count": self.data[key].send_count,
|
||||||
|
"retry_count": self.data[key].retry_count,
|
||||||
|
"message": self.data[key].raw,
|
||||||
|
}
|
||||||
|
stats["packets"] = pkts
|
||||||
|
return stats
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
def rx(self, packet: type[core.Packet]) -> None:
|
||||||
def add(self, packet):
|
"""When we get a packet from the network, check if we should remove it."""
|
||||||
|
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)
|
||||||
|
|
||||||
|
def tx(self, packet: type[core.Packet]) -> None:
|
||||||
|
"""Add a packet that was sent."""
|
||||||
|
with self.lock:
|
||||||
key = packet.msgNo
|
key = packet.msgNo
|
||||||
packet._last_send_attempt = 0
|
packet.send_count = 0
|
||||||
self.data[key] = packet
|
self.data[key] = packet
|
||||||
self.total_tracked += 1
|
self.total_tracked += 1
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def get(self, key):
|
|
||||||
return self.data.get(key, None)
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def remove(self, key):
|
def remove(self, key):
|
||||||
|
self._remove(key)
|
||||||
|
|
||||||
|
def _remove(self, key):
|
||||||
|
with self.lock:
|
||||||
try:
|
try:
|
||||||
del self.data[key]
|
del self.data[key]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass
|
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):
|
# Now register the PacketList with the collector
|
||||||
packet._last_send_attempt = 0
|
# every packet we RX and TX goes through the collector
|
||||||
tx.send(packet)
|
# for processing for whatever reason is needed.
|
||||||
|
collector.PacketCollector().register(PacketTrack)
|
||||||
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)
|
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import threading
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
import wrapt
|
|
||||||
|
|
||||||
from aprsd import utils
|
from aprsd import utils
|
||||||
|
from aprsd.packets import collector, core
|
||||||
from aprsd.utils import objectstore
|
from aprsd.utils import objectstore
|
||||||
|
|
||||||
|
|
||||||
@ -17,56 +16,75 @@ class WatchList(objectstore.ObjectStoreMixin):
|
|||||||
"""Global watch list and info for callsigns."""
|
"""Global watch list and info for callsigns."""
|
||||||
|
|
||||||
_instance = None
|
_instance = None
|
||||||
lock = threading.Lock()
|
|
||||||
data = {}
|
data = {}
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super().__new__(cls)
|
cls._instance = super().__new__(cls)
|
||||||
cls._instance._init_store()
|
|
||||||
cls._instance.data = {}
|
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
def __init__(self, config=None):
|
def __init__(self):
|
||||||
ring_size = CONF.watch_list.packet_keep_count
|
super().__init__()
|
||||||
|
self._update_from_conf()
|
||||||
|
|
||||||
if CONF.watch_list.callsigns:
|
def _update_from_conf(self, config=None):
|
||||||
|
with self.lock:
|
||||||
|
if CONF.watch_list.enabled and CONF.watch_list.callsigns:
|
||||||
for callsign in CONF.watch_list.callsigns:
|
for callsign in CONF.watch_list.callsigns:
|
||||||
call = callsign.replace("*", "")
|
call = callsign.replace("*", "")
|
||||||
# FIXME(waboring) - we should fetch the last time we saw
|
# FIXME(waboring) - we should fetch the last time we saw
|
||||||
# a beacon from a callsign or some other mechanism to find
|
# a beacon from a callsign or some other mechanism to find
|
||||||
# last time a message was seen by aprs-is. For now this
|
# last time a message was seen by aprs-is. For now this
|
||||||
# is all we can do.
|
# is all we can do.
|
||||||
|
if call not in self.data:
|
||||||
self.data[call] = {
|
self.data[call] = {
|
||||||
"last": datetime.datetime.now(),
|
"last": None,
|
||||||
"packets": utils.RingBuffer(
|
"packet": None,
|
||||||
ring_size,
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def stats(self, serializable=False) -> dict:
|
||||||
|
stats = {}
|
||||||
|
with self.lock:
|
||||||
|
for callsign in self.data:
|
||||||
|
stats[callsign] = {
|
||||||
|
"last": self.data[callsign]["last"],
|
||||||
|
"packet": self.data[callsign]["packet"],
|
||||||
|
"age": self.age(callsign),
|
||||||
|
"old": self.is_old(callsign),
|
||||||
|
}
|
||||||
|
return stats
|
||||||
|
|
||||||
def is_enabled(self):
|
def is_enabled(self):
|
||||||
return CONF.watch_list.enabled
|
return CONF.watch_list.enabled
|
||||||
|
|
||||||
def callsign_in_watchlist(self, callsign):
|
def callsign_in_watchlist(self, callsign):
|
||||||
|
with self.lock:
|
||||||
return callsign in self.data
|
return callsign in self.data
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
def rx(self, packet: type[core.Packet]) -> None:
|
||||||
def update_seen(self, packet):
|
"""Track when we got a packet from the network."""
|
||||||
if packet.addresse:
|
|
||||||
callsign = packet.addresse
|
|
||||||
else:
|
|
||||||
callsign = packet.from_call
|
callsign = packet.from_call
|
||||||
|
|
||||||
if self.callsign_in_watchlist(callsign):
|
if self.callsign_in_watchlist(callsign):
|
||||||
|
with self.lock:
|
||||||
self.data[callsign]["last"] = datetime.datetime.now()
|
self.data[callsign]["last"] = datetime.datetime.now()
|
||||||
self.data[callsign]["packets"].append(packet)
|
self.data[callsign]["packet"] = packet
|
||||||
|
|
||||||
|
def tx(self, packet: type[core.Packet]) -> None:
|
||||||
|
"""We don't care about TX packets."""
|
||||||
|
|
||||||
def last_seen(self, callsign):
|
def last_seen(self, callsign):
|
||||||
|
with self.lock:
|
||||||
if self.callsign_in_watchlist(callsign):
|
if self.callsign_in_watchlist(callsign):
|
||||||
return self.data[callsign]["last"]
|
return self.data[callsign]["last"]
|
||||||
|
|
||||||
def age(self, callsign):
|
def age(self, callsign):
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
return str(now - self.last_seen(callsign))
|
last_seen_time = self.last_seen(callsign)
|
||||||
|
if last_seen_time:
|
||||||
|
return str(now - last_seen_time)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def max_delta(self, seconds=None):
|
def max_delta(self, seconds=None):
|
||||||
if not seconds:
|
if not seconds:
|
||||||
@ -83,8 +101,11 @@ class WatchList(objectstore.ObjectStoreMixin):
|
|||||||
We put this here so any notification plugin can use this
|
We put this here so any notification plugin can use this
|
||||||
same test.
|
same test.
|
||||||
"""
|
"""
|
||||||
age = self.age(callsign)
|
if not self.callsign_in_watchlist(callsign):
|
||||||
|
return False
|
||||||
|
|
||||||
|
age = self.age(callsign)
|
||||||
|
if age:
|
||||||
delta = utils.parse_delta_str(age)
|
delta = utils.parse_delta_str(age)
|
||||||
d = datetime.timedelta(**delta)
|
d = datetime.timedelta(**delta)
|
||||||
|
|
||||||
@ -94,3 +115,8 @@ class WatchList(objectstore.ObjectStoreMixin):
|
|||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
collector.PacketCollector().register(WatchList)
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
# The base plugin class
|
from __future__ import annotations
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
import importlib
|
import importlib
|
||||||
import inspect
|
import inspect
|
||||||
@ -42,7 +43,7 @@ class APRSDPluginSpec:
|
|||||||
"""A hook specification namespace."""
|
"""A hook specification namespace."""
|
||||||
|
|
||||||
@hookspec
|
@hookspec
|
||||||
def filter(self, packet: packets.core.Packet):
|
def filter(self, packet: type[packets.Packet]):
|
||||||
"""My special little hook that you can customize."""
|
"""My special little hook that you can customize."""
|
||||||
|
|
||||||
|
|
||||||
@ -65,7 +66,7 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
|||||||
self.threads = self.create_threads() or []
|
self.threads = self.create_threads() or []
|
||||||
self.start_threads()
|
self.start_threads()
|
||||||
|
|
||||||
def start_threads(self):
|
def start_threads(self) -> None:
|
||||||
if self.enabled and self.threads:
|
if self.enabled and self.threads:
|
||||||
if not isinstance(self.threads, list):
|
if not isinstance(self.threads, list):
|
||||||
self.threads = [self.threads]
|
self.threads = [self.threads]
|
||||||
@ -90,10 +91,10 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def message_count(self):
|
def message_count(self) -> int:
|
||||||
return self.message_counter
|
return self.message_counter
|
||||||
|
|
||||||
def help(self):
|
def help(self) -> str:
|
||||||
return "Help!"
|
return "Help!"
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
@ -118,11 +119,11 @@ class APRSDPluginBase(metaclass=abc.ABCMeta):
|
|||||||
thread.stop()
|
thread.stop()
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def filter(self, packet: packets.core.Packet):
|
def filter(self, packet: type[packets.Packet]) -> str | packets.MessagePacket:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def process(self, packet: packets.core.Packet):
|
def process(self, packet: type[packets.Packet]):
|
||||||
"""This is called when the filter passes."""
|
"""This is called when the filter passes."""
|
||||||
|
|
||||||
|
|
||||||
@ -147,14 +148,14 @@ class APRSDWatchListPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
|||||||
watch_list = CONF.watch_list.callsigns
|
watch_list = CONF.watch_list.callsigns
|
||||||
# make sure the timeout is set or this doesn't work
|
# make sure the timeout is set or this doesn't work
|
||||||
if watch_list:
|
if watch_list:
|
||||||
aprs_client = client.factory.create().client
|
aprs_client = client.client_factory.create().client
|
||||||
filter_str = "b/{}".format("/".join(watch_list))
|
filter_str = "b/{}".format("/".join(watch_list))
|
||||||
aprs_client.set_filter(filter_str)
|
aprs_client.set_filter(filter_str)
|
||||||
else:
|
else:
|
||||||
LOG.warning("Watch list enabled, but no callsigns set.")
|
LOG.warning("Watch list enabled, but no callsigns set.")
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def filter(self, packet: packets.core.Packet):
|
def filter(self, packet: type[packets.Packet]) -> str | packets.MessagePacket:
|
||||||
result = packets.NULL_MESSAGE
|
result = packets.NULL_MESSAGE
|
||||||
if self.enabled:
|
if self.enabled:
|
||||||
wl = watch_list.WatchList()
|
wl = watch_list.WatchList()
|
||||||
@ -206,14 +207,14 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
|||||||
self.enabled = True
|
self.enabled = True
|
||||||
|
|
||||||
@hookimpl
|
@hookimpl
|
||||||
def filter(self, packet: packets.core.MessagePacket):
|
def filter(self, packet: packets.MessagePacket) -> str | packets.MessagePacket:
|
||||||
LOG.info(f"{self.__class__.__name__} called")
|
LOG.debug(f"{self.__class__.__name__} called")
|
||||||
if not self.enabled:
|
if not self.enabled:
|
||||||
result = f"{self.__class__.__name__} isn't enabled"
|
result = f"{self.__class__.__name__} isn't enabled"
|
||||||
LOG.warning(result)
|
LOG.warning(result)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
if not isinstance(packet, packets.core.MessagePacket):
|
if not isinstance(packet, packets.MessagePacket):
|
||||||
LOG.warning(f"{self.__class__.__name__} Got a {packet.__class__.__name__} ignoring")
|
LOG.warning(f"{self.__class__.__name__} Got a {packet.__class__.__name__} ignoring")
|
||||||
return packets.NULL_MESSAGE
|
return packets.NULL_MESSAGE
|
||||||
|
|
||||||
@ -226,7 +227,7 @@ class APRSDRegexCommandPluginBase(APRSDPluginBase, metaclass=abc.ABCMeta):
|
|||||||
# and is an APRS message format and has a message.
|
# and is an APRS message format and has a message.
|
||||||
if (
|
if (
|
||||||
tocall == CONF.callsign
|
tocall == CONF.callsign
|
||||||
and isinstance(packet, packets.core.MessagePacket)
|
and isinstance(packet, packets.MessagePacket)
|
||||||
and message
|
and message
|
||||||
):
|
):
|
||||||
if re.search(self.command_regex, message, re.IGNORECASE):
|
if re.search(self.command_regex, message, re.IGNORECASE):
|
||||||
@ -269,7 +270,7 @@ class HelpPlugin(APRSDRegexCommandPluginBase):
|
|||||||
def help(self):
|
def help(self):
|
||||||
return "Help: send APRS help or help <plugin>"
|
return "Help: send APRS help or help <plugin>"
|
||||||
|
|
||||||
def process(self, packet: packets.core.MessagePacket):
|
def process(self, packet: packets.MessagePacket):
|
||||||
LOG.info("HelpPlugin")
|
LOG.info("HelpPlugin")
|
||||||
# fromcall = packet.get("from")
|
# fromcall = packet.get("from")
|
||||||
message = packet.message_text
|
message = packet.message_text
|
||||||
@ -343,6 +344,28 @@ class PluginManager:
|
|||||||
self._watchlist_pm = pluggy.PluginManager("aprsd")
|
self._watchlist_pm = pluggy.PluginManager("aprsd")
|
||||||
self._watchlist_pm.add_hookspecs(APRSDPluginSpec)
|
self._watchlist_pm.add_hookspecs(APRSDPluginSpec)
|
||||||
|
|
||||||
|
def stats(self, serializable=False) -> dict:
|
||||||
|
"""Collect and return stats for all plugins."""
|
||||||
|
def full_name_with_qualname(obj):
|
||||||
|
return "{}.{}".format(
|
||||||
|
obj.__class__.__module__,
|
||||||
|
obj.__class__.__qualname__,
|
||||||
|
)
|
||||||
|
|
||||||
|
plugin_stats = {}
|
||||||
|
plugins = self.get_plugins()
|
||||||
|
if plugins:
|
||||||
|
|
||||||
|
for p in plugins:
|
||||||
|
plugin_stats[full_name_with_qualname(p)] = {
|
||||||
|
"enabled": p.enabled,
|
||||||
|
"rx": p.rx_count,
|
||||||
|
"tx": p.tx_count,
|
||||||
|
"version": p.version,
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugin_stats
|
||||||
|
|
||||||
def is_plugin(self, obj):
|
def is_plugin(self, obj):
|
||||||
for c in inspect.getmro(obj):
|
for c in inspect.getmro(obj):
|
||||||
if issubclass(c, APRSDPluginBase):
|
if issubclass(c, APRSDPluginBase):
|
||||||
@ -368,7 +391,9 @@ class PluginManager:
|
|||||||
try:
|
try:
|
||||||
module_name, class_name = module_class_string.rsplit(".", 1)
|
module_name, class_name = module_class_string.rsplit(".", 1)
|
||||||
module = importlib.import_module(module_name)
|
module = importlib.import_module(module_name)
|
||||||
module = importlib.reload(module)
|
# Commented out because the email thread starts in a different context
|
||||||
|
# and hence gives a different singleton for the EmailStats
|
||||||
|
# module = importlib.reload(module)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
if not module_name:
|
if not module_name:
|
||||||
LOG.error(f"Failed to load Plugin {module_class_string}")
|
LOG.error(f"Failed to load Plugin {module_class_string}")
|
||||||
@ -469,12 +494,12 @@ class PluginManager:
|
|||||||
|
|
||||||
LOG.info("Completed Plugin Loading.")
|
LOG.info("Completed Plugin Loading.")
|
||||||
|
|
||||||
def run(self, packet: packets.core.MessagePacket):
|
def run(self, packet: packets.MessagePacket):
|
||||||
"""Execute all the plugins run method."""
|
"""Execute all the plugins run method."""
|
||||||
with self.lock:
|
with self.lock:
|
||||||
return self._pluggy_pm.hook.filter(packet=packet)
|
return self._pluggy_pm.hook.filter(packet=packet)
|
||||||
|
|
||||||
def run_watchlist(self, packet: packets.core.Packet):
|
def run_watchlist(self, packet: packets.Packet):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
return self._watchlist_pm.hook.filter(packet=packet)
|
return self._watchlist_pm.hook.filter(packet=packet)
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import time
|
|||||||
import imapclient
|
import imapclient
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
from aprsd import packets, plugin, stats, threads
|
from aprsd import packets, plugin, threads, utils
|
||||||
from aprsd.threads import tx
|
from aprsd.threads import tx
|
||||||
from aprsd.utils import trace
|
from aprsd.utils import trace
|
||||||
|
|
||||||
@ -60,6 +60,38 @@ class EmailInfo:
|
|||||||
self._delay = val
|
self._delay = val
|
||||||
|
|
||||||
|
|
||||||
|
@utils.singleton
|
||||||
|
class EmailStats:
|
||||||
|
"""Singleton object to store stats related to email."""
|
||||||
|
_instance = None
|
||||||
|
tx = 0
|
||||||
|
rx = 0
|
||||||
|
email_thread_last_time = None
|
||||||
|
|
||||||
|
def stats(self, serializable=False):
|
||||||
|
if CONF.email_plugin.enabled:
|
||||||
|
last_check_time = self.email_thread_last_time
|
||||||
|
if serializable and last_check_time:
|
||||||
|
last_check_time = last_check_time.isoformat()
|
||||||
|
stats = {
|
||||||
|
"tx": self.tx,
|
||||||
|
"rx": self.rx,
|
||||||
|
"last_check_time": last_check_time,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
stats = {}
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def tx_inc(self):
|
||||||
|
self.tx += 1
|
||||||
|
|
||||||
|
def rx_inc(self):
|
||||||
|
self.rx += 1
|
||||||
|
|
||||||
|
def email_thread_update(self):
|
||||||
|
self.email_thread_last_time = datetime.datetime.now()
|
||||||
|
|
||||||
|
|
||||||
class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
||||||
"""Email Plugin."""
|
"""Email Plugin."""
|
||||||
|
|
||||||
@ -190,10 +222,6 @@ class EmailPlugin(plugin.APRSDRegexCommandPluginBase):
|
|||||||
def _imap_connect():
|
def _imap_connect():
|
||||||
imap_port = CONF.email_plugin.imap_port
|
imap_port = CONF.email_plugin.imap_port
|
||||||
use_ssl = CONF.email_plugin.imap_use_ssl
|
use_ssl = CONF.email_plugin.imap_use_ssl
|
||||||
# host = CONFIG["aprsd"]["email"]["imap"]["host"]
|
|
||||||
# msg = "{}{}:{}".format("TLS " if use_ssl else "", host, imap_port)
|
|
||||||
# LOG.debug("Connect to IMAP host {} with user '{}'".
|
|
||||||
# format(msg, CONFIG['imap']['login']))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
server = imapclient.IMAPClient(
|
server = imapclient.IMAPClient(
|
||||||
@ -440,7 +468,7 @@ def send_email(to_addr, content):
|
|||||||
[to_addr],
|
[to_addr],
|
||||||
msg.as_string(),
|
msg.as_string(),
|
||||||
)
|
)
|
||||||
stats.APRSDStats().email_tx_inc()
|
EmailStats().tx_inc()
|
||||||
except Exception:
|
except Exception:
|
||||||
LOG.exception("Sendmail Error!!!!")
|
LOG.exception("Sendmail Error!!!!")
|
||||||
server.quit()
|
server.quit()
|
||||||
@ -545,7 +573,7 @@ class APRSDEmailThread(threads.APRSDThread):
|
|||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
stats.APRSDStats().email_thread_update()
|
EmailStats().email_thread_update()
|
||||||
# always sleep for 5 seconds and see if we need to check email
|
# always sleep for 5 seconds and see if we need to check email
|
||||||
# This allows CTRL-C to stop the execution of this loop sooner
|
# This allows CTRL-C to stop the execution of this loop sooner
|
||||||
# than check_email_delay time
|
# than check_email_delay time
|
||||||
|
@ -1,81 +0,0 @@
|
|||||||
import datetime
|
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
|
|
||||||
from aprsd import packets, plugin
|
|
||||||
from aprsd.packets import tracker
|
|
||||||
from aprsd.utils import trace
|
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
LOG = logging.getLogger("APRSD")
|
|
||||||
|
|
||||||
|
|
||||||
class QueryPlugin(plugin.APRSDRegexCommandPluginBase):
|
|
||||||
"""Query command."""
|
|
||||||
|
|
||||||
command_regex = r"^\!.*"
|
|
||||||
command_name = "query"
|
|
||||||
short_description = "APRSD Owner command to query messages in the MsgTrack"
|
|
||||||
|
|
||||||
def setup(self):
|
|
||||||
"""Do any plugin setup here."""
|
|
||||||
if not CONF.query_plugin.callsign:
|
|
||||||
LOG.error("Config query_plugin.callsign not set. Disabling plugin")
|
|
||||||
self.enabled = False
|
|
||||||
self.enabled = True
|
|
||||||
|
|
||||||
@trace.trace
|
|
||||||
def process(self, packet: packets.MessagePacket):
|
|
||||||
LOG.info("Query COMMAND")
|
|
||||||
|
|
||||||
fromcall = packet.from_call
|
|
||||||
message = packet.get("message_text", None)
|
|
||||||
|
|
||||||
pkt_tracker = tracker.PacketTrack()
|
|
||||||
now = datetime.datetime.now()
|
|
||||||
reply = "Pending messages ({}) {}".format(
|
|
||||||
len(pkt_tracker),
|
|
||||||
now.strftime("%H:%M:%S"),
|
|
||||||
)
|
|
||||||
|
|
||||||
searchstring = "^" + CONF.query_plugin.callsign + ".*"
|
|
||||||
# only I can do admin commands
|
|
||||||
if re.search(searchstring, fromcall):
|
|
||||||
|
|
||||||
# resend last N most recent: "!3"
|
|
||||||
r = re.search(r"^\!([0-9]).*", message)
|
|
||||||
if r is not None:
|
|
||||||
if len(pkt_tracker) > 0:
|
|
||||||
last_n = r.group(1)
|
|
||||||
reply = packets.NULL_MESSAGE
|
|
||||||
LOG.debug(reply)
|
|
||||||
pkt_tracker.restart_delayed(count=int(last_n))
|
|
||||||
else:
|
|
||||||
reply = "No pending msgs to resend"
|
|
||||||
LOG.debug(reply)
|
|
||||||
return reply
|
|
||||||
|
|
||||||
# resend all: "!a"
|
|
||||||
r = re.search(r"^\![aA].*", message)
|
|
||||||
if r is not None:
|
|
||||||
if len(pkt_tracker) > 0:
|
|
||||||
reply = packets.NULL_MESSAGE
|
|
||||||
LOG.debug(reply)
|
|
||||||
pkt_tracker.restart_delayed()
|
|
||||||
else:
|
|
||||||
reply = "No pending msgs"
|
|
||||||
LOG.debug(reply)
|
|
||||||
return reply
|
|
||||||
|
|
||||||
# delete all: "!d"
|
|
||||||
r = re.search(r"^\![dD].*", message)
|
|
||||||
if r is not None:
|
|
||||||
reply = "Deleted ALL pending msgs."
|
|
||||||
LOG.debug(reply)
|
|
||||||
pkt_tracker.flush()
|
|
||||||
return reply
|
|
||||||
|
|
||||||
return reply
|
|
@ -1,9 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
import time
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
import pytz
|
import pytz
|
||||||
|
from tzlocal import get_localzone
|
||||||
|
|
||||||
from aprsd import packets, plugin, plugin_utils
|
from aprsd import packets, plugin, plugin_utils
|
||||||
from aprsd.utils import fuzzy, trace
|
from aprsd.utils import fuzzy, trace
|
||||||
@ -22,7 +22,8 @@ class TimePlugin(plugin.APRSDRegexCommandPluginBase):
|
|||||||
short_description = "What is the current local time."
|
short_description = "What is the current local time."
|
||||||
|
|
||||||
def _get_local_tz(self):
|
def _get_local_tz(self):
|
||||||
return pytz.timezone(time.strftime("%Z"))
|
lz = get_localzone()
|
||||||
|
return pytz.timezone(str(lz))
|
||||||
|
|
||||||
def _get_utcnow(self):
|
def _get_utcnow(self):
|
||||||
return pytz.datetime.datetime.utcnow()
|
return pytz.datetime.datetime.utcnow()
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import plugin, stats
|
from aprsd import plugin
|
||||||
|
from aprsd.stats import collector
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
@ -23,10 +24,8 @@ class VersionPlugin(plugin.APRSDRegexCommandPluginBase):
|
|||||||
# fromcall = packet.get("from")
|
# fromcall = packet.get("from")
|
||||||
# message = packet.get("message_text", None)
|
# message = packet.get("message_text", None)
|
||||||
# ack = packet.get("msgNo", "0")
|
# ack = packet.get("msgNo", "0")
|
||||||
stats_obj = stats.APRSDStats()
|
s = collector.Collector().collect()
|
||||||
s = stats_obj.stats()
|
|
||||||
print(s)
|
|
||||||
return "APRSD ver:{} uptime:{}".format(
|
return "APRSD ver:{} uptime:{}".format(
|
||||||
aprsd.__version__,
|
aprsd.__version__,
|
||||||
s["aprsd"]["uptime"],
|
s["APRSDStats"]["uptime"],
|
||||||
)
|
)
|
||||||
|
@ -110,7 +110,6 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
|||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def process(self, packet):
|
def process(self, packet):
|
||||||
print("FISTY")
|
|
||||||
fromcall = packet.get("from")
|
fromcall = packet.get("from")
|
||||||
message = packet.get("message_text", None)
|
message = packet.get("message_text", None)
|
||||||
# ack = packet.get("msgNo", "0")
|
# ack = packet.get("msgNo", "0")
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
import rpyc
|
|
||||||
|
|
||||||
|
|
||||||
class AuthSocketStream(rpyc.SocketStream):
|
|
||||||
"""Used to authenitcate the RPC stream to remote."""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def connect(cls, *args, authorizer=None, **kwargs):
|
|
||||||
stream_obj = super().connect(*args, **kwargs)
|
|
||||||
|
|
||||||
if callable(authorizer):
|
|
||||||
authorizer(stream_obj.sock)
|
|
||||||
|
|
||||||
return stream_obj
|
|
@ -1,165 +0,0 @@
|
|||||||
import json
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
import rpyc
|
|
||||||
|
|
||||||
from aprsd import conf # noqa
|
|
||||||
from aprsd import rpc
|
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
LOG = logging.getLogger("APRSD")
|
|
||||||
|
|
||||||
|
|
||||||
class RPCClient:
|
|
||||||
_instance = None
|
|
||||||
_rpc_client = None
|
|
||||||
|
|
||||||
ip = None
|
|
||||||
port = None
|
|
||||||
magic_word = None
|
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
|
||||||
if cls._instance is None:
|
|
||||||
cls._instance = super().__new__(cls)
|
|
||||||
return cls._instance
|
|
||||||
|
|
||||||
def __init__(self, ip=None, port=None, magic_word=None):
|
|
||||||
if ip:
|
|
||||||
self.ip = ip
|
|
||||||
else:
|
|
||||||
self.ip = CONF.rpc_settings.ip
|
|
||||||
if port:
|
|
||||||
self.port = int(port)
|
|
||||||
else:
|
|
||||||
self.port = CONF.rpc_settings.port
|
|
||||||
if magic_word:
|
|
||||||
self.magic_word = magic_word
|
|
||||||
else:
|
|
||||||
self.magic_word = CONF.rpc_settings.magic_word
|
|
||||||
self._check_settings()
|
|
||||||
self.get_rpc_client()
|
|
||||||
|
|
||||||
def _check_settings(self):
|
|
||||||
if not CONF.rpc_settings.enabled:
|
|
||||||
LOG.warning("RPC is not enabled, no way to get stats!!")
|
|
||||||
|
|
||||||
if self.magic_word == conf.common.APRSD_DEFAULT_MAGIC_WORD:
|
|
||||||
LOG.warning("You are using the default RPC magic word!!!")
|
|
||||||
LOG.warning("edit aprsd.conf and change rpc_settings.magic_word")
|
|
||||||
|
|
||||||
LOG.debug(f"RPC Client: {self.ip}:{self.port} {self.magic_word}")
|
|
||||||
|
|
||||||
def _rpyc_connect(
|
|
||||||
self, host, port, service=rpyc.VoidService,
|
|
||||||
config={}, ipv6=False,
|
|
||||||
keepalive=False, authorizer=None, ):
|
|
||||||
|
|
||||||
LOG.info(f"Connecting to RPC host '{host}:{port}'")
|
|
||||||
try:
|
|
||||||
s = rpc.AuthSocketStream.connect(
|
|
||||||
host, port, ipv6=ipv6, keepalive=keepalive,
|
|
||||||
authorizer=authorizer,
|
|
||||||
)
|
|
||||||
return rpyc.utils.factory.connect_stream(s, service, config=config)
|
|
||||||
except ConnectionRefusedError:
|
|
||||||
LOG.error(f"Failed to connect to RPC host '{host}:{port}'")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_rpc_client(self):
|
|
||||||
if not self._rpc_client:
|
|
||||||
self._rpc_client = self._rpyc_connect(
|
|
||||||
self.ip,
|
|
||||||
self.port,
|
|
||||||
authorizer=lambda sock: sock.send(self.magic_word.encode()),
|
|
||||||
)
|
|
||||||
return self._rpc_client
|
|
||||||
|
|
||||||
def get_stats_dict(self):
|
|
||||||
cl = self.get_rpc_client()
|
|
||||||
result = {}
|
|
||||||
if not cl:
|
|
||||||
return result
|
|
||||||
|
|
||||||
try:
|
|
||||||
rpc_stats_dict = cl.root.get_stats()
|
|
||||||
result = json.loads(rpc_stats_dict)
|
|
||||||
except EOFError:
|
|
||||||
LOG.error("Lost connection to RPC Host")
|
|
||||||
self._rpc_client = None
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_stats(self):
|
|
||||||
cl = self.get_rpc_client()
|
|
||||||
result = {}
|
|
||||||
if not cl:
|
|
||||||
return result
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = cl.root.get_stats_obj()
|
|
||||||
except EOFError:
|
|
||||||
LOG.error("Lost connection to RPC Host")
|
|
||||||
self._rpc_client = None
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_packet_track(self):
|
|
||||||
cl = self.get_rpc_client()
|
|
||||||
result = None
|
|
||||||
if not cl:
|
|
||||||
return result
|
|
||||||
try:
|
|
||||||
result = cl.root.get_packet_track()
|
|
||||||
except EOFError:
|
|
||||||
LOG.error("Lost connection to RPC Host")
|
|
||||||
self._rpc_client = None
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_packet_list(self):
|
|
||||||
cl = self.get_rpc_client()
|
|
||||||
result = None
|
|
||||||
if not cl:
|
|
||||||
return result
|
|
||||||
try:
|
|
||||||
result = cl.root.get_packet_list()
|
|
||||||
except EOFError:
|
|
||||||
LOG.error("Lost connection to RPC Host")
|
|
||||||
self._rpc_client = None
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_watch_list(self):
|
|
||||||
cl = self.get_rpc_client()
|
|
||||||
result = None
|
|
||||||
if not cl:
|
|
||||||
return result
|
|
||||||
try:
|
|
||||||
result = cl.root.get_watch_list()
|
|
||||||
except EOFError:
|
|
||||||
LOG.error("Lost connection to RPC Host")
|
|
||||||
self._rpc_client = None
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_seen_list(self):
|
|
||||||
cl = self.get_rpc_client()
|
|
||||||
result = None
|
|
||||||
if not cl:
|
|
||||||
return result
|
|
||||||
try:
|
|
||||||
result = cl.root.get_seen_list()
|
|
||||||
except EOFError:
|
|
||||||
LOG.error("Lost connection to RPC Host")
|
|
||||||
self._rpc_client = None
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_log_entries(self):
|
|
||||||
cl = self.get_rpc_client()
|
|
||||||
result = None
|
|
||||||
if not cl:
|
|
||||||
return result
|
|
||||||
try:
|
|
||||||
result_str = cl.root.get_log_entries()
|
|
||||||
result = json.loads(result_str)
|
|
||||||
except EOFError:
|
|
||||||
LOG.error("Lost connection to RPC Host")
|
|
||||||
self._rpc_client = None
|
|
||||||
return result
|
|
@ -1,99 +0,0 @@
|
|||||||
import json
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
import rpyc
|
|
||||||
from rpyc.utils.authenticators import AuthenticationError
|
|
||||||
from rpyc.utils.server import ThreadPoolServer
|
|
||||||
|
|
||||||
from aprsd import conf # noqa: F401
|
|
||||||
from aprsd import packets, stats, threads
|
|
||||||
from aprsd.threads import log_monitor
|
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
LOG = logging.getLogger("APRSD")
|
|
||||||
|
|
||||||
|
|
||||||
def magic_word_authenticator(sock):
|
|
||||||
client_ip = sock.getpeername()[0]
|
|
||||||
magic = sock.recv(len(CONF.rpc_settings.magic_word)).decode()
|
|
||||||
if magic != CONF.rpc_settings.magic_word:
|
|
||||||
LOG.error(
|
|
||||||
f"wrong magic word passed from {client_ip} "
|
|
||||||
"'{magic}' != '{CONF.rpc_settings.magic_word}'",
|
|
||||||
)
|
|
||||||
raise AuthenticationError(
|
|
||||||
f"wrong magic word passed in '{magic}'"
|
|
||||||
f" != '{CONF.rpc_settings.magic_word}'",
|
|
||||||
)
|
|
||||||
return sock, None
|
|
||||||
|
|
||||||
|
|
||||||
class APRSDRPCThread(threads.APRSDThread):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(name="RPCThread")
|
|
||||||
self.thread = ThreadPoolServer(
|
|
||||||
APRSDService,
|
|
||||||
port=CONF.rpc_settings.port,
|
|
||||||
protocol_config={"allow_public_attrs": True},
|
|
||||||
authenticator=magic_word_authenticator,
|
|
||||||
)
|
|
||||||
|
|
||||||
def stop(self):
|
|
||||||
if self.thread:
|
|
||||||
self.thread.close()
|
|
||||||
self.thread_stop = True
|
|
||||||
|
|
||||||
def loop(self):
|
|
||||||
# there is no loop as run is blocked
|
|
||||||
if self.thread and not self.thread_stop:
|
|
||||||
# This is a blocking call
|
|
||||||
self.thread.start()
|
|
||||||
|
|
||||||
|
|
||||||
@rpyc.service
|
|
||||||
class APRSDService(rpyc.Service):
|
|
||||||
def on_connect(self, conn):
|
|
||||||
# code that runs when a connection is created
|
|
||||||
# (to init the service, if needed)
|
|
||||||
LOG.info("RPC Client Connected")
|
|
||||||
self._conn = conn
|
|
||||||
|
|
||||||
def on_disconnect(self, conn):
|
|
||||||
# code that runs after the connection has already closed
|
|
||||||
# (to finalize the service, if needed)
|
|
||||||
LOG.info("RPC Client Disconnected")
|
|
||||||
self._conn = None
|
|
||||||
|
|
||||||
@rpyc.exposed
|
|
||||||
def get_stats(self):
|
|
||||||
stat = stats.APRSDStats()
|
|
||||||
stats_dict = stat.stats()
|
|
||||||
return_str = json.dumps(stats_dict, indent=4, sort_keys=True, default=str)
|
|
||||||
return return_str
|
|
||||||
|
|
||||||
@rpyc.exposed
|
|
||||||
def get_stats_obj(self):
|
|
||||||
return stats.APRSDStats()
|
|
||||||
|
|
||||||
@rpyc.exposed
|
|
||||||
def get_packet_list(self):
|
|
||||||
return packets.PacketList()
|
|
||||||
|
|
||||||
@rpyc.exposed
|
|
||||||
def get_packet_track(self):
|
|
||||||
return packets.PacketTrack()
|
|
||||||
|
|
||||||
@rpyc.exposed
|
|
||||||
def get_watch_list(self):
|
|
||||||
return packets.WatchList()
|
|
||||||
|
|
||||||
@rpyc.exposed
|
|
||||||
def get_seen_list(self):
|
|
||||||
return packets.SeenList()
|
|
||||||
|
|
||||||
@rpyc.exposed
|
|
||||||
def get_log_entries(self):
|
|
||||||
entries = log_monitor.LogEntries().get_all_and_purge()
|
|
||||||
return json.dumps(entries, default=str)
|
|
266
aprsd/stats.py
266
aprsd/stats.py
@ -1,266 +0,0 @@
|
|||||||
import datetime
|
|
||||||
import logging
|
|
||||||
import threading
|
|
||||||
|
|
||||||
from oslo_config import cfg
|
|
||||||
import wrapt
|
|
||||||
|
|
||||||
import aprsd
|
|
||||||
from aprsd import packets, plugin, utils
|
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
|
||||||
LOG = logging.getLogger("APRSD")
|
|
||||||
|
|
||||||
|
|
||||||
class APRSDStats:
|
|
||||||
|
|
||||||
_instance = None
|
|
||||||
lock = threading.Lock()
|
|
||||||
|
|
||||||
start_time = None
|
|
||||||
_aprsis_server = None
|
|
||||||
_aprsis_keepalive = None
|
|
||||||
|
|
||||||
_email_thread_last_time = None
|
|
||||||
_email_tx = 0
|
|
||||||
_email_rx = 0
|
|
||||||
|
|
||||||
_mem_current = 0
|
|
||||||
_mem_peak = 0
|
|
||||||
|
|
||||||
_thread_info = {}
|
|
||||||
|
|
||||||
_pkt_cnt = {
|
|
||||||
"Packet": {
|
|
||||||
"tx": 0,
|
|
||||||
"rx": 0,
|
|
||||||
},
|
|
||||||
"AckPacket": {
|
|
||||||
"tx": 0,
|
|
||||||
"rx": 0,
|
|
||||||
},
|
|
||||||
"GPSPacket": {
|
|
||||||
"tx": 0,
|
|
||||||
"rx": 0,
|
|
||||||
},
|
|
||||||
"StatusPacket": {
|
|
||||||
"tx": 0,
|
|
||||||
"rx": 0,
|
|
||||||
},
|
|
||||||
"MicEPacket": {
|
|
||||||
"tx": 0,
|
|
||||||
"rx": 0,
|
|
||||||
},
|
|
||||||
"MessagePacket": {
|
|
||||||
"tx": 0,
|
|
||||||
"rx": 0,
|
|
||||||
},
|
|
||||||
"WeatherPacket": {
|
|
||||||
"tx": 0,
|
|
||||||
"rx": 0,
|
|
||||||
},
|
|
||||||
"ObjectPacket": {
|
|
||||||
"tx": 0,
|
|
||||||
"rx": 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
|
||||||
if cls._instance is None:
|
|
||||||
cls._instance = super().__new__(cls)
|
|
||||||
# any init here
|
|
||||||
cls._instance.start_time = datetime.datetime.now()
|
|
||||||
cls._instance._aprsis_keepalive = datetime.datetime.now()
|
|
||||||
return cls._instance
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
@property
|
|
||||||
def uptime(self):
|
|
||||||
return datetime.datetime.now() - self.start_time
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
@property
|
|
||||||
def memory(self):
|
|
||||||
return self._mem_current
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def set_memory(self, memory):
|
|
||||||
self._mem_current = memory
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
@property
|
|
||||||
def memory_peak(self):
|
|
||||||
return self._mem_peak
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def set_memory_peak(self, memory):
|
|
||||||
self._mem_peak = memory
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def set_thread_info(self, thread_info):
|
|
||||||
self._thread_info = thread_info
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
@property
|
|
||||||
def thread_info(self):
|
|
||||||
return self._thread_info
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
@property
|
|
||||||
def aprsis_server(self):
|
|
||||||
return self._aprsis_server
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def set_aprsis_server(self, server):
|
|
||||||
self._aprsis_server = server
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
@property
|
|
||||||
def aprsis_keepalive(self):
|
|
||||||
return self._aprsis_keepalive
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def set_aprsis_keepalive(self):
|
|
||||||
self._aprsis_keepalive = datetime.datetime.now()
|
|
||||||
|
|
||||||
def rx(self, packet):
|
|
||||||
pkt_type = packet.__class__.__name__
|
|
||||||
if pkt_type not in self._pkt_cnt:
|
|
||||||
self._pkt_cnt[pkt_type] = {
|
|
||||||
"tx": 0,
|
|
||||||
"rx": 0,
|
|
||||||
}
|
|
||||||
self._pkt_cnt[pkt_type]["rx"] += 1
|
|
||||||
|
|
||||||
def tx(self, packet):
|
|
||||||
pkt_type = packet.__class__.__name__
|
|
||||||
if pkt_type not in self._pkt_cnt:
|
|
||||||
self._pkt_cnt[pkt_type] = {
|
|
||||||
"tx": 0,
|
|
||||||
"rx": 0,
|
|
||||||
}
|
|
||||||
self._pkt_cnt[pkt_type]["tx"] += 1
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
@property
|
|
||||||
def msgs_tracked(self):
|
|
||||||
return packets.PacketTrack().total_tracked
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
@property
|
|
||||||
def email_tx(self):
|
|
||||||
return self._email_tx
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def email_tx_inc(self):
|
|
||||||
self._email_tx += 1
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
@property
|
|
||||||
def email_rx(self):
|
|
||||||
return self._email_rx
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def email_rx_inc(self):
|
|
||||||
self._email_rx += 1
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
@property
|
|
||||||
def email_thread_time(self):
|
|
||||||
return self._email_thread_last_time
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def email_thread_update(self):
|
|
||||||
self._email_thread_last_time = datetime.datetime.now()
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def stats(self):
|
|
||||||
now = datetime.datetime.now()
|
|
||||||
if self._email_thread_last_time:
|
|
||||||
last_update = str(now - self._email_thread_last_time)
|
|
||||||
else:
|
|
||||||
last_update = "never"
|
|
||||||
|
|
||||||
if self._aprsis_keepalive:
|
|
||||||
last_aprsis_keepalive = str(now - self._aprsis_keepalive)
|
|
||||||
else:
|
|
||||||
last_aprsis_keepalive = "never"
|
|
||||||
|
|
||||||
pm = plugin.PluginManager()
|
|
||||||
plugins = pm.get_plugins()
|
|
||||||
plugin_stats = {}
|
|
||||||
if plugins:
|
|
||||||
def full_name_with_qualname(obj):
|
|
||||||
return "{}.{}".format(
|
|
||||||
obj.__class__.__module__,
|
|
||||||
obj.__class__.__qualname__,
|
|
||||||
)
|
|
||||||
|
|
||||||
for p in plugins:
|
|
||||||
plugin_stats[full_name_with_qualname(p)] = {
|
|
||||||
"enabled": p.enabled,
|
|
||||||
"rx": p.rx_count,
|
|
||||||
"tx": p.tx_count,
|
|
||||||
"version": p.version,
|
|
||||||
}
|
|
||||||
|
|
||||||
wl = packets.WatchList()
|
|
||||||
sl = packets.SeenList()
|
|
||||||
pl = packets.PacketList()
|
|
||||||
|
|
||||||
stats = {
|
|
||||||
"aprsd": {
|
|
||||||
"version": aprsd.__version__,
|
|
||||||
"uptime": utils.strfdelta(self.uptime),
|
|
||||||
"callsign": CONF.callsign,
|
|
||||||
"memory_current": int(self.memory),
|
|
||||||
"memory_current_str": utils.human_size(self.memory),
|
|
||||||
"memory_peak": int(self.memory_peak),
|
|
||||||
"memory_peak_str": utils.human_size(self.memory_peak),
|
|
||||||
"threads": self._thread_info,
|
|
||||||
"watch_list": wl.get_all(),
|
|
||||||
"seen_list": sl.get_all(),
|
|
||||||
},
|
|
||||||
"aprs-is": {
|
|
||||||
"server": str(self.aprsis_server),
|
|
||||||
"callsign": CONF.aprs_network.login,
|
|
||||||
"last_update": last_aprsis_keepalive,
|
|
||||||
},
|
|
||||||
"packets": {
|
|
||||||
"total_tracked": int(pl.total_tx() + pl.total_rx()),
|
|
||||||
"total_sent": int(pl.total_tx()),
|
|
||||||
"total_received": int(pl.total_rx()),
|
|
||||||
"by_type": self._pkt_cnt,
|
|
||||||
},
|
|
||||||
"messages": {
|
|
||||||
"sent": self._pkt_cnt["MessagePacket"]["tx"],
|
|
||||||
"received": self._pkt_cnt["MessagePacket"]["tx"],
|
|
||||||
"ack_sent": self._pkt_cnt["AckPacket"]["tx"],
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"enabled": CONF.email_plugin.enabled,
|
|
||||||
"sent": int(self._email_tx),
|
|
||||||
"received": int(self._email_rx),
|
|
||||||
"thread_last_update": last_update,
|
|
||||||
},
|
|
||||||
"plugins": plugin_stats,
|
|
||||||
}
|
|
||||||
return stats
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
pl = packets.PacketList()
|
|
||||||
return (
|
|
||||||
"Uptime:{} Msgs TX:{} RX:{} "
|
|
||||||
"ACK: TX:{} RX:{} "
|
|
||||||
"Email TX:{} RX:{} LastLoop:{} ".format(
|
|
||||||
self.uptime,
|
|
||||||
pl.total_tx(),
|
|
||||||
pl.total_rx(),
|
|
||||||
self._pkt_cnt["AckPacket"]["tx"],
|
|
||||||
self._pkt_cnt["AckPacket"]["rx"],
|
|
||||||
self._email_tx,
|
|
||||||
self._email_rx,
|
|
||||||
self._email_thread_last_time,
|
|
||||||
)
|
|
||||||
)
|
|
20
aprsd/stats/__init__.py
Normal file
20
aprsd/stats/__init__.py
Normal file
@ -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)
|
49
aprsd/stats/app.py
Normal file
49
aprsd/stats/app.py
Normal file
@ -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
|
38
aprsd/stats/collector.py
Normal file
38
aprsd/stats/collector.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import logging
|
||||||
|
from typing import Callable, Protocol, runtime_checkable
|
||||||
|
|
||||||
|
from aprsd.utils import singleton
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class StatsProducer(Protocol):
|
||||||
|
"""The StatsProducer protocol is used to define the interface for collecting stats."""
|
||||||
|
def stats(self, serializeable=False) -> dict:
|
||||||
|
"""provide stats in a dictionary format."""
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@singleton
|
||||||
|
class Collector:
|
||||||
|
"""The Collector class is used to collect stats from multiple StatsProducer instances."""
|
||||||
|
def __init__(self):
|
||||||
|
self.producers: list[Callable] = []
|
||||||
|
|
||||||
|
def collect(self, serializable=False) -> dict:
|
||||||
|
stats = {}
|
||||||
|
for name in self.producers:
|
||||||
|
cls = name()
|
||||||
|
if isinstance(cls, StatsProducer):
|
||||||
|
try:
|
||||||
|
stats[cls.__class__.__name__] = cls.stats(serializable=serializable).copy()
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error(f"Error in producer {name} (stats): {e}")
|
||||||
|
else:
|
||||||
|
raise TypeError(f"{cls} is not an instance of StatsProducer")
|
||||||
|
return stats
|
||||||
|
|
||||||
|
def register_producer(self, producer_name: Callable):
|
||||||
|
self.producers.append(producer_name)
|
@ -3,8 +3,9 @@ import queue
|
|||||||
# Make these available to anyone importing
|
# Make these available to anyone importing
|
||||||
# aprsd.threads
|
# aprsd.threads
|
||||||
from .aprsd import APRSDThread, APRSDThreadList # noqa: F401
|
from .aprsd import APRSDThread, APRSDThreadList # noqa: F401
|
||||||
from .keep_alive import KeepAliveThread # noqa: F401
|
from .rx import ( # noqa: F401
|
||||||
from .rx import APRSDRXThread, APRSDDupeRXThread, APRSDProcessPacketThread # noqa: F401
|
APRSDDupeRXThread, APRSDProcessPacketThread, APRSDRXThread,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
packet_queue = queue.Queue(maxsize=20)
|
packet_queue = queue.Queue(maxsize=20)
|
||||||
|
@ -2,6 +2,7 @@ import abc
|
|||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
|
from typing import List
|
||||||
|
|
||||||
import wrapt
|
import wrapt
|
||||||
|
|
||||||
@ -9,43 +10,10 @@ import wrapt
|
|||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
class APRSDThreadList:
|
|
||||||
"""Singleton class that keeps track of application wide threads."""
|
|
||||||
|
|
||||||
_instance = None
|
|
||||||
|
|
||||||
threads_list = []
|
|
||||||
lock = threading.Lock()
|
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
|
||||||
if cls._instance is None:
|
|
||||||
cls._instance = super().__new__(cls)
|
|
||||||
cls.threads_list = []
|
|
||||||
return cls._instance
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def add(self, thread_obj):
|
|
||||||
self.threads_list.append(thread_obj)
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def remove(self, thread_obj):
|
|
||||||
self.threads_list.remove(thread_obj)
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def stop_all(self):
|
|
||||||
"""Iterate over all threads and call stop on them."""
|
|
||||||
for th in self.threads_list:
|
|
||||||
LOG.info(f"Stopping Thread {th.name}")
|
|
||||||
if hasattr(th, "packet"):
|
|
||||||
LOG.info(F"{th.name} packet {th.packet}")
|
|
||||||
th.stop()
|
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
|
||||||
def __len__(self):
|
|
||||||
return len(self.threads_list)
|
|
||||||
|
|
||||||
|
|
||||||
class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
||||||
|
"""Base class for all threads in APRSD."""
|
||||||
|
|
||||||
|
loop_count = 1
|
||||||
|
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
super().__init__(name=name)
|
super().__init__(name=name)
|
||||||
@ -79,6 +47,7 @@ class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
|||||||
def run(self):
|
def run(self):
|
||||||
LOG.debug("Starting")
|
LOG.debug("Starting")
|
||||||
while not self._should_quit():
|
while not self._should_quit():
|
||||||
|
self.loop_count += 1
|
||||||
can_loop = self.loop()
|
can_loop = self.loop()
|
||||||
self._last_loop = datetime.datetime.now()
|
self._last_loop = datetime.datetime.now()
|
||||||
if not can_loop:
|
if not can_loop:
|
||||||
@ -86,3 +55,65 @@ class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
|||||||
self._cleanup()
|
self._cleanup()
|
||||||
APRSDThreadList().remove(self)
|
APRSDThreadList().remove(self)
|
||||||
LOG.debug("Exiting")
|
LOG.debug("Exiting")
|
||||||
|
|
||||||
|
|
||||||
|
class APRSDThreadList:
|
||||||
|
"""Singleton class that keeps track of application wide threads."""
|
||||||
|
|
||||||
|
_instance = None
|
||||||
|
|
||||||
|
threads_list: List[APRSDThread] = []
|
||||||
|
lock = threading.Lock()
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
cls.threads_list = []
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def stats(self, serializable=False) -> dict:
|
||||||
|
stats = {}
|
||||||
|
for th in self.threads_list:
|
||||||
|
age = th.loop_age()
|
||||||
|
if serializable:
|
||||||
|
age = str(age)
|
||||||
|
stats[th.name] = {
|
||||||
|
"name": th.name,
|
||||||
|
"class": th.__class__.__name__,
|
||||||
|
"alive": th.is_alive(),
|
||||||
|
"age": th.loop_age(),
|
||||||
|
"loop_count": th.loop_count,
|
||||||
|
}
|
||||||
|
return stats
|
||||||
|
|
||||||
|
@wrapt.synchronized(lock)
|
||||||
|
def add(self, thread_obj):
|
||||||
|
self.threads_list.append(thread_obj)
|
||||||
|
|
||||||
|
@wrapt.synchronized(lock)
|
||||||
|
def remove(self, thread_obj):
|
||||||
|
self.threads_list.remove(thread_obj)
|
||||||
|
|
||||||
|
@wrapt.synchronized(lock)
|
||||||
|
def stop_all(self):
|
||||||
|
"""Iterate over all threads and call stop on them."""
|
||||||
|
for th in self.threads_list:
|
||||||
|
LOG.info(f"Stopping Thread {th.name}")
|
||||||
|
if hasattr(th, "packet"):
|
||||||
|
LOG.info(F"{th.name} packet {th.packet}")
|
||||||
|
th.stop()
|
||||||
|
|
||||||
|
@wrapt.synchronized(lock)
|
||||||
|
def info(self):
|
||||||
|
"""Go through all the threads and collect info about each."""
|
||||||
|
info = {}
|
||||||
|
for thread in self.threads_list:
|
||||||
|
alive = thread.is_alive()
|
||||||
|
age = thread.loop_age()
|
||||||
|
key = thread.__class__.__name__
|
||||||
|
info[key] = {"alive": True if alive else False, "age": age, "name": thread.name}
|
||||||
|
return info
|
||||||
|
|
||||||
|
@wrapt.synchronized(lock)
|
||||||
|
def __len__(self):
|
||||||
|
return len(self.threads_list)
|
||||||
|
@ -5,7 +5,10 @@ import tracemalloc
|
|||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
from aprsd import client, packets, stats, utils
|
from aprsd import packets, utils
|
||||||
|
from aprsd.client import client_factory
|
||||||
|
from aprsd.log import log as aprsd_log
|
||||||
|
from aprsd.stats import collector
|
||||||
from aprsd.threads import APRSDThread, APRSDThreadList
|
from aprsd.threads import APRSDThread, APRSDThreadList
|
||||||
|
|
||||||
|
|
||||||
@ -24,64 +27,70 @@ class KeepAliveThread(APRSDThread):
|
|||||||
self.max_delta = datetime.timedelta(**max_timeout)
|
self.max_delta = datetime.timedelta(**max_timeout)
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
if self.cntr % 60 == 0:
|
if self.loop_count % 60 == 0:
|
||||||
pkt_tracker = packets.PacketTrack()
|
stats_json = collector.Collector().collect()
|
||||||
stats_obj = stats.APRSDStats()
|
|
||||||
pl = packets.PacketList()
|
pl = packets.PacketList()
|
||||||
thread_list = APRSDThreadList()
|
thread_list = APRSDThreadList()
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
last_email = stats_obj.email_thread_time
|
|
||||||
if last_email:
|
if "EmailStats" in stats_json:
|
||||||
email_thread_time = utils.strfdelta(now - last_email)
|
email_stats = stats_json["EmailStats"]
|
||||||
|
if email_stats.get("last_check_time"):
|
||||||
|
email_thread_time = utils.strfdelta(now - email_stats["last_check_time"])
|
||||||
|
else:
|
||||||
|
email_thread_time = "N/A"
|
||||||
else:
|
else:
|
||||||
email_thread_time = "N/A"
|
email_thread_time = "N/A"
|
||||||
|
|
||||||
last_msg_time = utils.strfdelta(now - stats_obj.aprsis_keepalive)
|
if "APRSClientStats" in stats_json and stats_json["APRSClientStats"].get("transport") == "aprsis":
|
||||||
|
if stats_json["APRSClientStats"].get("server_keepalive"):
|
||||||
|
last_msg_time = utils.strfdelta(now - stats_json["APRSClientStats"]["server_keepalive"])
|
||||||
|
else:
|
||||||
|
last_msg_time = "N/A"
|
||||||
|
else:
|
||||||
|
last_msg_time = "N/A"
|
||||||
|
|
||||||
current, peak = tracemalloc.get_traced_memory()
|
tracked_packets = stats_json["PacketTrack"]["total_tracked"]
|
||||||
stats_obj.set_memory(current)
|
tx_msg = 0
|
||||||
stats_obj.set_memory_peak(peak)
|
rx_msg = 0
|
||||||
|
if "PacketList" in stats_json:
|
||||||
login = CONF.callsign
|
msg_packets = stats_json["PacketList"].get("MessagePacket")
|
||||||
|
if msg_packets:
|
||||||
tracked_packets = len(pkt_tracker)
|
tx_msg = msg_packets.get("tx", 0)
|
||||||
|
rx_msg = msg_packets.get("rx", 0)
|
||||||
|
|
||||||
keepalive = (
|
keepalive = (
|
||||||
"{} - Uptime {} RX:{} TX:{} Tracker:{} Msgs TX:{} RX:{} "
|
"{} - Uptime {} RX:{} TX:{} Tracker:{} Msgs TX:{} RX:{} "
|
||||||
"Last:{} Email: {} - RAM Current:{} Peak:{} Threads:{}"
|
"Last:{} Email: {} - RAM Current:{} Peak:{} Threads:{} LoggingQueue:{}"
|
||||||
).format(
|
).format(
|
||||||
login,
|
stats_json["APRSDStats"]["callsign"],
|
||||||
utils.strfdelta(stats_obj.uptime),
|
stats_json["APRSDStats"]["uptime"],
|
||||||
pl.total_rx(),
|
pl.total_rx(),
|
||||||
pl.total_tx(),
|
pl.total_tx(),
|
||||||
tracked_packets,
|
tracked_packets,
|
||||||
stats_obj._pkt_cnt["MessagePacket"]["tx"],
|
tx_msg,
|
||||||
stats_obj._pkt_cnt["MessagePacket"]["rx"],
|
rx_msg,
|
||||||
last_msg_time,
|
last_msg_time,
|
||||||
email_thread_time,
|
email_thread_time,
|
||||||
utils.human_size(current),
|
stats_json["APRSDStats"]["memory_current_str"],
|
||||||
utils.human_size(peak),
|
stats_json["APRSDStats"]["memory_peak_str"],
|
||||||
len(thread_list),
|
len(thread_list),
|
||||||
|
aprsd_log.logging_queue.qsize(),
|
||||||
)
|
)
|
||||||
LOG.info(keepalive)
|
LOG.info(keepalive)
|
||||||
thread_out = []
|
if "APRSDThreadList" in stats_json:
|
||||||
thread_info = {}
|
thread_list = stats_json["APRSDThreadList"]
|
||||||
for thread in thread_list.threads_list:
|
for thread_name in thread_list:
|
||||||
alive = thread.is_alive()
|
thread = thread_list[thread_name]
|
||||||
age = thread.loop_age()
|
alive = thread["alive"]
|
||||||
key = thread.__class__.__name__
|
age = thread["age"]
|
||||||
thread_out.append(f"{key}:{alive}:{age}")
|
key = thread["name"]
|
||||||
if key not in thread_info:
|
|
||||||
thread_info[key] = {}
|
|
||||||
thread_info[key]["alive"] = alive
|
|
||||||
thread_info[key]["age"] = age
|
|
||||||
if not alive:
|
if not alive:
|
||||||
LOG.error(f"Thread {thread}")
|
LOG.error(f"Thread {thread}")
|
||||||
LOG.info(",".join(thread_out))
|
LOG.info(f"{key: <15} Alive? {str(alive): <5} {str(age): <20}")
|
||||||
stats_obj.set_thread_info(thread_info)
|
|
||||||
|
|
||||||
# check the APRS connection
|
# check the APRS connection
|
||||||
cl = client.factory.create()
|
cl = client_factory.create()
|
||||||
# Reset the connection if it's dead and this isn't our
|
# Reset the connection if it's dead and this isn't our
|
||||||
# First time through the loop.
|
# First time through the loop.
|
||||||
# The first time through the loop can happen at startup where
|
# The first time through the loop can happen at startup where
|
||||||
@ -89,19 +98,19 @@ class KeepAliveThread(APRSDThread):
|
|||||||
# to make it's connection the first time.
|
# to make it's connection the first time.
|
||||||
if not cl.is_alive() and self.cntr > 0:
|
if not cl.is_alive() and self.cntr > 0:
|
||||||
LOG.error(f"{cl.__class__.__name__} is not alive!!! Resetting")
|
LOG.error(f"{cl.__class__.__name__} is not alive!!! Resetting")
|
||||||
client.factory.create().reset()
|
client_factory.create().reset()
|
||||||
else:
|
# else:
|
||||||
# See if we should reset the aprs-is client
|
# # See if we should reset the aprs-is client
|
||||||
# Due to losing a keepalive from them
|
# # Due to losing a keepalive from them
|
||||||
delta_dict = utils.parse_delta_str(last_msg_time)
|
# delta_dict = utils.parse_delta_str(last_msg_time)
|
||||||
delta = datetime.timedelta(**delta_dict)
|
# delta = datetime.timedelta(**delta_dict)
|
||||||
|
#
|
||||||
if delta > self.max_delta:
|
# if delta > self.max_delta:
|
||||||
# We haven't gotten a keepalive from aprs-is in a while
|
# # We haven't gotten a keepalive from aprs-is in a while
|
||||||
# reset the connection.a
|
# # reset the connection.a
|
||||||
if not client.KISSClient.is_enabled():
|
# if not client.KISSClient.is_enabled():
|
||||||
LOG.warning(f"Resetting connection to APRS-IS {delta}")
|
# LOG.warning(f"Resetting connection to APRS-IS {delta}")
|
||||||
client.factory.create().reset()
|
# client.factory.create().reset()
|
||||||
|
|
||||||
# Check version every day
|
# Check version every day
|
||||||
delta = now - self.checker_time
|
delta = now - self.checker_time
|
||||||
|
@ -1,25 +1,54 @@
|
|||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
import requests
|
||||||
import wrapt
|
import wrapt
|
||||||
|
|
||||||
from aprsd import threads
|
from aprsd import threads
|
||||||
from aprsd.log import log
|
from aprsd.log import log
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
def send_log_entries(force=False):
|
||||||
|
"""Send all of the log entries to the web interface."""
|
||||||
|
if CONF.admin.web_enabled:
|
||||||
|
if force or LogEntries().is_purge_ready():
|
||||||
|
entries = LogEntries().get_all_and_purge()
|
||||||
|
if entries:
|
||||||
|
try:
|
||||||
|
requests.post(
|
||||||
|
f"http://{CONF.admin.web_ip}:{CONF.admin.web_port}/log_entries",
|
||||||
|
json=entries,
|
||||||
|
auth=(CONF.admin.user, CONF.admin.password),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
LOG.warning(f"Failed to send log entries. len={len(entries)}")
|
||||||
|
|
||||||
|
|
||||||
class LogEntries:
|
class LogEntries:
|
||||||
entries = []
|
entries = []
|
||||||
lock = threading.Lock()
|
lock = threading.Lock()
|
||||||
_instance = None
|
_instance = None
|
||||||
|
last_purge = datetime.datetime.now()
|
||||||
|
max_delta = datetime.timedelta(
|
||||||
|
hours=0.0, minutes=0, seconds=2,
|
||||||
|
)
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super().__new__(cls)
|
cls._instance = super().__new__(cls)
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
|
def stats(self) -> dict:
|
||||||
|
return {
|
||||||
|
"log_entries": self.entries,
|
||||||
|
}
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
@wrapt.synchronized(lock)
|
||||||
def add(self, entry):
|
def add(self, entry):
|
||||||
self.entries.append(entry)
|
self.entries.append(entry)
|
||||||
@ -28,8 +57,18 @@ class LogEntries:
|
|||||||
def get_all_and_purge(self):
|
def get_all_and_purge(self):
|
||||||
entries = self.entries.copy()
|
entries = self.entries.copy()
|
||||||
self.entries = []
|
self.entries = []
|
||||||
|
self.last_purge = datetime.datetime.now()
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
|
def is_purge_ready(self):
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
if (
|
||||||
|
now - self.last_purge > self.max_delta
|
||||||
|
and len(self.entries) > 1
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
@wrapt.synchronized(lock)
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return len(self.entries)
|
return len(self.entries)
|
||||||
@ -40,6 +79,10 @@ class LogMonitorThread(threads.APRSDThread):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__("LogMonitorThread")
|
super().__init__("LogMonitorThread")
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
send_log_entries(force=True)
|
||||||
|
super().stop()
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
try:
|
try:
|
||||||
record = log.logging_queue.get(block=True, timeout=2)
|
record = log.logging_queue.get(block=True, timeout=2)
|
||||||
@ -54,6 +97,7 @@ class LogMonitorThread(threads.APRSDThread):
|
|||||||
# Just ignore thi
|
# Just ignore thi
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
send_log_entries()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def json_record(self, record):
|
def json_record(self, record):
|
||||||
|
@ -6,7 +6,10 @@ import time
|
|||||||
import aprslib
|
import aprslib
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
from aprsd import client, packets, plugin
|
from aprsd import packets, plugin
|
||||||
|
from aprsd.client import client_factory
|
||||||
|
from aprsd.packets import collector
|
||||||
|
from aprsd.packets import log as packet_log
|
||||||
from aprsd.threads import APRSDThread, tx
|
from aprsd.threads import APRSDThread, tx
|
||||||
|
|
||||||
|
|
||||||
@ -16,15 +19,20 @@ LOG = logging.getLogger("APRSD")
|
|||||||
|
|
||||||
class APRSDRXThread(APRSDThread):
|
class APRSDRXThread(APRSDThread):
|
||||||
def __init__(self, packet_queue):
|
def __init__(self, packet_queue):
|
||||||
super().__init__("RX_MSG")
|
super().__init__("RX_PKT")
|
||||||
self.packet_queue = packet_queue
|
self.packet_queue = packet_queue
|
||||||
self._client = client.factory.create()
|
self._client = client_factory.create()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.thread_stop = True
|
self.thread_stop = True
|
||||||
client.factory.create().client.stop()
|
if self._client:
|
||||||
|
self._client.stop()
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
|
if not self._client:
|
||||||
|
self._client = client_factory.create()
|
||||||
|
time.sleep(1)
|
||||||
|
return True
|
||||||
# setup the consumer of messages and block until a messages
|
# setup the consumer of messages and block until a messages
|
||||||
try:
|
try:
|
||||||
# This will register a packet consumer with aprslib
|
# This will register a packet consumer with aprslib
|
||||||
@ -36,23 +44,32 @@ class APRSDRXThread(APRSDThread):
|
|||||||
# and the aprslib developer didn't want to allow a PR to add
|
# and the aprslib developer didn't want to allow a PR to add
|
||||||
# kwargs. :(
|
# kwargs. :(
|
||||||
# https://github.com/rossengeorgiev/aprs-python/pull/56
|
# https://github.com/rossengeorgiev/aprs-python/pull/56
|
||||||
self._client.client.consumer(
|
self._client.consumer(
|
||||||
self.process_packet, raw=False, blocking=False,
|
self._process_packet, raw=False, blocking=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
except (
|
except (
|
||||||
aprslib.exceptions.ConnectionDrop,
|
aprslib.exceptions.ConnectionDrop,
|
||||||
aprslib.exceptions.ConnectionError,
|
aprslib.exceptions.ConnectionError,
|
||||||
):
|
):
|
||||||
LOG.error("Connection dropped, reconnecting")
|
LOG.error("Connection dropped, reconnecting")
|
||||||
time.sleep(5)
|
|
||||||
# Force the deletion of the client object connected to aprs
|
# Force the deletion of the client object connected to aprs
|
||||||
# This will cause a reconnect, next time client.get_client()
|
# This will cause a reconnect, next time client.get_client()
|
||||||
# is called
|
# is called
|
||||||
self._client.reset()
|
self._client.reset()
|
||||||
|
time.sleep(5)
|
||||||
|
except Exception:
|
||||||
|
# LOG.exception(ex)
|
||||||
|
LOG.error("Resetting connection and trying again.")
|
||||||
|
self._client.reset()
|
||||||
|
time.sleep(5)
|
||||||
# Continue to loop
|
# Continue to loop
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def _process_packet(self, *args, **kwargs):
|
||||||
|
"""Intermediate callback so we can update the keepalive time."""
|
||||||
|
# Now call the 'real' packet processing for a RX'x packet
|
||||||
|
self.process_packet(*args, **kwargs)
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def process_packet(self, *args, **kwargs):
|
def process_packet(self, *args, **kwargs):
|
||||||
pass
|
pass
|
||||||
@ -80,7 +97,8 @@ class APRSDDupeRXThread(APRSDRXThread):
|
|||||||
"""
|
"""
|
||||||
packet = self._client.decode_packet(*args, **kwargs)
|
packet = self._client.decode_packet(*args, **kwargs)
|
||||||
# LOG.debug(raw)
|
# LOG.debug(raw)
|
||||||
packet.log(header="RX")
|
packet_log.log(packet)
|
||||||
|
pkt_list = packets.PacketList()
|
||||||
|
|
||||||
if isinstance(packet, packets.AckPacket):
|
if isinstance(packet, packets.AckPacket):
|
||||||
# We don't need to drop AckPackets, those should be
|
# We don't need to drop AckPackets, those should be
|
||||||
@ -91,7 +109,6 @@ class APRSDDupeRXThread(APRSDRXThread):
|
|||||||
# For RF based APRS Clients we can get duplicate packets
|
# For RF based APRS Clients we can get duplicate packets
|
||||||
# So we need to track them and not process the dupes.
|
# So we need to track them and not process the dupes.
|
||||||
found = False
|
found = False
|
||||||
pkt_list = packets.PacketList()
|
|
||||||
try:
|
try:
|
||||||
# Find the packet in the list of already seen packets
|
# Find the packet in the list of already seen packets
|
||||||
# Based on the packet.key
|
# Based on the packet.key
|
||||||
@ -100,14 +117,11 @@ class APRSDDupeRXThread(APRSDRXThread):
|
|||||||
found = False
|
found = False
|
||||||
|
|
||||||
if not found:
|
if not found:
|
||||||
# If we are in the process of already ack'ing
|
# We haven't seen this packet before, so we process it.
|
||||||
# a packet, we should drop the packet
|
collector.PacketCollector().rx(packet)
|
||||||
# because it's a dupe within the time that
|
|
||||||
# we send the 3 acks for the packet.
|
|
||||||
pkt_list.rx(packet)
|
|
||||||
self.packet_queue.put(packet)
|
self.packet_queue.put(packet)
|
||||||
elif packet.timestamp - found.timestamp < CONF.packet_dupe_timeout:
|
elif packet.timestamp - found.timestamp < CONF.packet_dupe_timeout:
|
||||||
# If the packet came in within 60 seconds of the
|
# If the packet came in within N seconds of the
|
||||||
# Last time seeing the packet, then we drop it as a dupe.
|
# Last time seeing the packet, then we drop it as a dupe.
|
||||||
LOG.warning(f"Packet {packet.from_call}:{packet.msgNo} already tracked, dropping.")
|
LOG.warning(f"Packet {packet.from_call}:{packet.msgNo} already tracked, dropping.")
|
||||||
else:
|
else:
|
||||||
@ -115,7 +129,7 @@ class APRSDDupeRXThread(APRSDRXThread):
|
|||||||
f"Packet {packet.from_call}:{packet.msgNo} already tracked "
|
f"Packet {packet.from_call}:{packet.msgNo} already tracked "
|
||||||
f"but older than {CONF.packet_dupe_timeout} seconds. processing.",
|
f"but older than {CONF.packet_dupe_timeout} seconds. processing.",
|
||||||
)
|
)
|
||||||
pkt_list.rx(packet)
|
collector.PacketCollector().rx(packet)
|
||||||
self.packet_queue.put(packet)
|
self.packet_queue.put(packet)
|
||||||
|
|
||||||
|
|
||||||
@ -137,21 +151,24 @@ class APRSDProcessPacketThread(APRSDThread):
|
|||||||
def __init__(self, packet_queue):
|
def __init__(self, packet_queue):
|
||||||
self.packet_queue = packet_queue
|
self.packet_queue = packet_queue
|
||||||
super().__init__("ProcessPKT")
|
super().__init__("ProcessPKT")
|
||||||
self._loop_cnt = 1
|
|
||||||
|
|
||||||
def process_ack_packet(self, packet):
|
def process_ack_packet(self, packet):
|
||||||
"""We got an ack for a message, no need to resend it."""
|
"""We got an ack for a message, no need to resend it."""
|
||||||
ack_num = packet.msgNo
|
ack_num = packet.msgNo
|
||||||
LOG.info(f"Got ack for message {ack_num}")
|
LOG.debug(f"Got ack for message {ack_num}")
|
||||||
pkt_tracker = packets.PacketTrack()
|
collector.PacketCollector().rx(packet)
|
||||||
pkt_tracker.remove(ack_num)
|
|
||||||
|
def process_piggyback_ack(self, packet):
|
||||||
|
"""We got an ack embedded in a packet."""
|
||||||
|
ack_num = packet.ackMsgNo
|
||||||
|
LOG.debug(f"Got PiggyBackAck for message {ack_num}")
|
||||||
|
collector.PacketCollector().rx(packet)
|
||||||
|
|
||||||
def process_reject_packet(self, packet):
|
def process_reject_packet(self, packet):
|
||||||
"""We got a reject message for a packet. Stop sending the message."""
|
"""We got a reject message for a packet. Stop sending the message."""
|
||||||
ack_num = packet.msgNo
|
ack_num = packet.msgNo
|
||||||
LOG.info(f"Got REJECT for message {ack_num}")
|
LOG.debug(f"Got REJECT for message {ack_num}")
|
||||||
pkt_tracker = packets.PacketTrack()
|
collector.PacketCollector().rx(packet)
|
||||||
pkt_tracker.remove(ack_num)
|
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
try:
|
try:
|
||||||
@ -160,12 +177,11 @@ class APRSDProcessPacketThread(APRSDThread):
|
|||||||
self.process_packet(packet)
|
self.process_packet(packet)
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
pass
|
pass
|
||||||
self._loop_cnt += 1
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def process_packet(self, packet):
|
def process_packet(self, packet):
|
||||||
"""Process a packet received from aprs-is server."""
|
"""Process a packet received from aprs-is server."""
|
||||||
LOG.debug(f"ProcessPKT-LOOP {self._loop_cnt}")
|
LOG.debug(f"ProcessPKT-LOOP {self.loop_count}")
|
||||||
our_call = CONF.callsign.lower()
|
our_call = CONF.callsign.lower()
|
||||||
|
|
||||||
from_call = packet.from_call
|
from_call = packet.from_call
|
||||||
@ -188,6 +204,10 @@ class APRSDProcessPacketThread(APRSDThread):
|
|||||||
):
|
):
|
||||||
self.process_reject_packet(packet)
|
self.process_reject_packet(packet)
|
||||||
else:
|
else:
|
||||||
|
if hasattr(packet, "ackMsgNo") and packet.ackMsgNo:
|
||||||
|
# we got an ack embedded in this packet
|
||||||
|
# we need to handle the ack
|
||||||
|
self.process_piggyback_ack(packet)
|
||||||
# Only ack messages that were sent directly to us
|
# Only ack messages that were sent directly to us
|
||||||
if isinstance(packet, packets.MessagePacket):
|
if isinstance(packet, packets.MessagePacket):
|
||||||
if to_call and to_call.lower() == our_call:
|
if to_call and to_call.lower() == our_call:
|
||||||
|
44
aprsd/threads/stats.py
Normal file
44
aprsd/threads/stats.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import logging
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
import wrapt
|
||||||
|
|
||||||
|
from aprsd.stats import collector
|
||||||
|
from aprsd.threads import APRSDThread
|
||||||
|
from aprsd.utils import objectstore
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
|
||||||
|
class StatsStore(objectstore.ObjectStoreMixin):
|
||||||
|
"""Container to save the stats from the collector."""
|
||||||
|
lock = threading.Lock()
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
@wrapt.synchronized(lock)
|
||||||
|
def add(self, stats: dict):
|
||||||
|
self.data = stats
|
||||||
|
|
||||||
|
|
||||||
|
class APRSDStatsStoreThread(APRSDThread):
|
||||||
|
"""Save APRSD Stats to disk periodically."""
|
||||||
|
|
||||||
|
# how often in seconds to write the file
|
||||||
|
save_interval = 10
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("StatsStore")
|
||||||
|
|
||||||
|
def loop(self):
|
||||||
|
if self.loop_count % self.save_interval == 0:
|
||||||
|
stats = collector.Collector().collect()
|
||||||
|
ss = StatsStore()
|
||||||
|
ss.add(stats)
|
||||||
|
ss.save()
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
return True
|
@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
@ -6,11 +7,14 @@ from rush import quota, throttle
|
|||||||
from rush.contrib import decorator
|
from rush.contrib import decorator
|
||||||
from rush.limiters import periodic
|
from rush.limiters import periodic
|
||||||
from rush.stores import dictionary
|
from rush.stores import dictionary
|
||||||
|
import wrapt
|
||||||
|
|
||||||
from aprsd import client
|
|
||||||
from aprsd import conf # noqa
|
from aprsd import conf # noqa
|
||||||
from aprsd import threads as aprsd_threads
|
from aprsd import threads as aprsd_threads
|
||||||
from aprsd.packets import core, tracker
|
from aprsd.client import client_factory
|
||||||
|
from aprsd.packets import collector, core
|
||||||
|
from aprsd.packets import log as packet_log
|
||||||
|
from aprsd.packets import tracker
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@ -35,14 +39,19 @@ ack_t = throttle.Throttle(
|
|||||||
|
|
||||||
msg_throttle_decorator = decorator.ThrottleDecorator(throttle=msg_t)
|
msg_throttle_decorator = decorator.ThrottleDecorator(throttle=msg_t)
|
||||||
ack_throttle_decorator = decorator.ThrottleDecorator(throttle=ack_t)
|
ack_throttle_decorator = decorator.ThrottleDecorator(throttle=ack_t)
|
||||||
|
s_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
@wrapt.synchronized(s_lock)
|
||||||
@msg_throttle_decorator.sleep_and_retry
|
@msg_throttle_decorator.sleep_and_retry
|
||||||
def send(packet: core.Packet, direct=False, aprs_client=None):
|
def send(packet: core.Packet, direct=False, aprs_client=None):
|
||||||
"""Send a packet either in a thread or directly to the client."""
|
"""Send a packet either in a thread or directly to the client."""
|
||||||
# prepare the packet for sending.
|
# prepare the packet for sending.
|
||||||
# This constructs the packet.raw
|
# This constructs the packet.raw
|
||||||
packet.prepare()
|
packet.prepare()
|
||||||
|
# Have to call the collector to track the packet
|
||||||
|
# After prepare, as prepare assigns the msgNo
|
||||||
|
collector.PacketCollector().tx(packet)
|
||||||
if isinstance(packet, core.AckPacket):
|
if isinstance(packet, core.AckPacket):
|
||||||
_send_ack(packet, direct=direct, aprs_client=aprs_client)
|
_send_ack(packet, direct=direct, aprs_client=aprs_client)
|
||||||
else:
|
else:
|
||||||
@ -71,11 +80,15 @@ def _send_direct(packet, aprs_client=None):
|
|||||||
if aprs_client:
|
if aprs_client:
|
||||||
cl = aprs_client
|
cl = aprs_client
|
||||||
else:
|
else:
|
||||||
cl = client.factory.create()
|
cl = client_factory.create()
|
||||||
|
|
||||||
packet.update_timestamp()
|
packet.update_timestamp()
|
||||||
packet.log(header="TX")
|
packet_log.log(packet, tx=True)
|
||||||
|
try:
|
||||||
cl.send(packet)
|
cl.send(packet)
|
||||||
|
except Exception as e:
|
||||||
|
LOG.error(f"Failed to send packet: {packet}")
|
||||||
|
LOG.error(e)
|
||||||
|
|
||||||
|
|
||||||
class SendPacketThread(aprsd_threads.APRSDThread):
|
class SendPacketThread(aprsd_threads.APRSDThread):
|
||||||
@ -83,10 +96,7 @@ class SendPacketThread(aprsd_threads.APRSDThread):
|
|||||||
|
|
||||||
def __init__(self, packet):
|
def __init__(self, packet):
|
||||||
self.packet = packet
|
self.packet = packet
|
||||||
name = self.packet.raw[:5]
|
super().__init__(f"TX-{packet.to_call}-{self.packet.msgNo}")
|
||||||
super().__init__(f"TXPKT-{self.packet.msgNo}-{name}")
|
|
||||||
pkt_tracker = tracker.PacketTrack()
|
|
||||||
pkt_tracker.add(packet)
|
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
"""Loop until a message is acked or it gets delayed.
|
"""Loop until a message is acked or it gets delayed.
|
||||||
@ -112,7 +122,7 @@ class SendPacketThread(aprsd_threads.APRSDThread):
|
|||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
send_now = False
|
send_now = False
|
||||||
if packet.send_count == packet.retry_count:
|
if packet.send_count >= packet.retry_count:
|
||||||
# we reached the send limit, don't send again
|
# we reached the send limit, don't send again
|
||||||
# TODO(hemna) - Need to put this in a delayed queue?
|
# TODO(hemna) - Need to put this in a delayed queue?
|
||||||
LOG.info(
|
LOG.info(
|
||||||
@ -121,7 +131,6 @@ class SendPacketThread(aprsd_threads.APRSDThread):
|
|||||||
"Message Send Complete. Max attempts reached"
|
"Message Send Complete. Max attempts reached"
|
||||||
f" {packet.retry_count}",
|
f" {packet.retry_count}",
|
||||||
)
|
)
|
||||||
if not packet.allow_delay:
|
|
||||||
pkt_tracker.remove(packet.msgNo)
|
pkt_tracker.remove(packet.msgNo)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -141,7 +150,7 @@ class SendPacketThread(aprsd_threads.APRSDThread):
|
|||||||
# no attempt time, so lets send it, and start
|
# no attempt time, so lets send it, and start
|
||||||
# tracking the time.
|
# tracking the time.
|
||||||
packet.last_send_time = int(round(time.time()))
|
packet.last_send_time = int(round(time.time()))
|
||||||
send(packet, direct=True)
|
_send_direct(packet)
|
||||||
packet.send_count += 1
|
packet.send_count += 1
|
||||||
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
@ -152,22 +161,24 @@ class SendPacketThread(aprsd_threads.APRSDThread):
|
|||||||
|
|
||||||
class SendAckThread(aprsd_threads.APRSDThread):
|
class SendAckThread(aprsd_threads.APRSDThread):
|
||||||
loop_count: int = 1
|
loop_count: int = 1
|
||||||
|
max_retries = 3
|
||||||
|
|
||||||
def __init__(self, packet):
|
def __init__(self, packet):
|
||||||
self.packet = packet
|
self.packet = packet
|
||||||
super().__init__(f"SendAck-{self.packet.msgNo}")
|
super().__init__(f"TXAck-{packet.to_call}-{self.packet.msgNo}")
|
||||||
|
self.max_retries = CONF.default_ack_send_count
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
"""Separate thread to send acks with retries."""
|
"""Separate thread to send acks with retries."""
|
||||||
send_now = False
|
send_now = False
|
||||||
if self.packet.send_count == self.packet.retry_count:
|
if self.packet.send_count == self.max_retries:
|
||||||
# we reached the send limit, don't send again
|
# we reached the send limit, don't send again
|
||||||
# TODO(hemna) - Need to put this in a delayed queue?
|
# TODO(hemna) - Need to put this in a delayed queue?
|
||||||
LOG.info(
|
LOG.debug(
|
||||||
f"{self.packet.__class__.__name__}"
|
f"{self.packet.__class__.__name__}"
|
||||||
f"({self.packet.msgNo}) "
|
f"({self.packet.msgNo}) "
|
||||||
"Send Complete. Max attempts reached"
|
"Send Complete. Max attempts reached"
|
||||||
f" {self.packet.retry_count}",
|
f" {self.max_retries}",
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -188,7 +199,7 @@ class SendAckThread(aprsd_threads.APRSDThread):
|
|||||||
send_now = True
|
send_now = True
|
||||||
|
|
||||||
if send_now:
|
if send_now:
|
||||||
send(self.packet, direct=True)
|
_send_direct(self.packet)
|
||||||
self.packet.send_count += 1
|
self.packet.send_count += 1
|
||||||
self.packet.last_send_time = int(round(time.time()))
|
self.packet.last_send_time = int(round(time.time()))
|
||||||
|
|
||||||
@ -230,7 +241,15 @@ class BeaconSendThread(aprsd_threads.APRSDThread):
|
|||||||
comment="APRSD GPS Beacon",
|
comment="APRSD GPS Beacon",
|
||||||
symbol=CONF.beacon_symbol,
|
symbol=CONF.beacon_symbol,
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
|
# Only send it once
|
||||||
|
pkt.retry_count = 1
|
||||||
send(pkt, direct=True)
|
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
|
self._loop_cnt += 1
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
return True
|
return True
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""Utilities and helper functions."""
|
"""Utilities and helper functions."""
|
||||||
|
|
||||||
import errno
|
import errno
|
||||||
|
import functools
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
@ -22,6 +23,17 @@ else:
|
|||||||
from collections.abc import MutableMapping
|
from collections.abc import MutableMapping
|
||||||
|
|
||||||
|
|
||||||
|
def singleton(cls):
|
||||||
|
"""Make a class a Singleton class (only one instance)"""
|
||||||
|
@functools.wraps(cls)
|
||||||
|
def wrapper_singleton(*args, **kwargs):
|
||||||
|
if wrapper_singleton.instance is None:
|
||||||
|
wrapper_singleton.instance = cls(*args, **kwargs)
|
||||||
|
return wrapper_singleton.instance
|
||||||
|
wrapper_singleton.instance = None
|
||||||
|
return wrapper_singleton
|
||||||
|
|
||||||
|
|
||||||
def env(*vars, **kwargs):
|
def env(*vars, **kwargs):
|
||||||
"""This returns the first environment variable set.
|
"""This returns the first environment variable set.
|
||||||
if none are non-empty, defaults to '' or keyword arg default
|
if none are non-empty, defaults to '' or keyword arg default
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
from multiprocessing import RawValue
|
from multiprocessing import RawValue
|
||||||
|
import random
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
import wrapt
|
import wrapt
|
||||||
|
|
||||||
|
|
||||||
|
MAX_PACKET_ID = 9999
|
||||||
|
|
||||||
|
|
||||||
class PacketCounter:
|
class PacketCounter:
|
||||||
"""
|
"""
|
||||||
Global Packet id counter class.
|
Global Packet id counter class.
|
||||||
@ -17,19 +21,18 @@ class PacketCounter:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_instance = None
|
_instance = None
|
||||||
max_count = 9999
|
|
||||||
lock = threading.Lock()
|
lock = threading.Lock()
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
"""Make this a singleton class."""
|
"""Make this a singleton class."""
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super().__new__(cls, *args, **kwargs)
|
cls._instance = super().__new__(cls, *args, **kwargs)
|
||||||
cls._instance.val = RawValue("i", 1)
|
cls._instance.val = RawValue("i", random.randint(1, MAX_PACKET_ID))
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
@wrapt.synchronized(lock)
|
@wrapt.synchronized(lock)
|
||||||
def increment(self):
|
def increment(self):
|
||||||
if self.val.value == self.max_count:
|
if self.val.value == MAX_PACKET_ID:
|
||||||
self.val.value = 1
|
self.val.value = 1
|
||||||
else:
|
else:
|
||||||
self.val.value += 1
|
self.val.value += 1
|
||||||
|
@ -3,6 +3,8 @@ import decimal
|
|||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from aprsd.packets import core
|
||||||
|
|
||||||
|
|
||||||
class EnhancedJSONEncoder(json.JSONEncoder):
|
class EnhancedJSONEncoder(json.JSONEncoder):
|
||||||
def default(self, obj):
|
def default(self, obj):
|
||||||
@ -42,6 +44,24 @@ class EnhancedJSONEncoder(json.JSONEncoder):
|
|||||||
return super().default(obj)
|
return super().default(obj)
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleJSONEncoder(json.JSONEncoder):
|
||||||
|
def default(self, obj):
|
||||||
|
if isinstance(obj, datetime.datetime):
|
||||||
|
return obj.isoformat()
|
||||||
|
elif isinstance(obj, datetime.date):
|
||||||
|
return str(obj)
|
||||||
|
elif isinstance(obj, datetime.time):
|
||||||
|
return str(obj)
|
||||||
|
elif isinstance(obj, datetime.timedelta):
|
||||||
|
return str(obj)
|
||||||
|
elif isinstance(obj, decimal.Decimal):
|
||||||
|
return str(obj)
|
||||||
|
elif isinstance(obj, core.Packet):
|
||||||
|
return obj.to_dict()
|
||||||
|
else:
|
||||||
|
return super().default(obj)
|
||||||
|
|
||||||
|
|
||||||
class EnhancedJSONDecoder(json.JSONDecoder):
|
class EnhancedJSONDecoder(json.JSONDecoder):
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
@ -2,6 +2,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import pickle
|
import pickle
|
||||||
|
import threading
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
@ -25,19 +26,28 @@ class ObjectStoreMixin:
|
|||||||
aprsd server -f (flush) will wipe all saved objects.
|
aprsd server -f (flush) will wipe all saved objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.lock = threading.RLock()
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
|
with self.lock:
|
||||||
return len(self.data)
|
return len(self.data)
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
|
with self.lock:
|
||||||
return iter(self.data)
|
return iter(self.data)
|
||||||
|
|
||||||
def get_all(self):
|
def get_all(self):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
return self.data
|
return self.data
|
||||||
|
|
||||||
def get(self, id):
|
def get(self, key):
|
||||||
with self.lock:
|
with self.lock:
|
||||||
return self.data[id]
|
return self.data.get(key)
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
with self.lock:
|
||||||
|
return self.data.copy()
|
||||||
|
|
||||||
def _init_store(self):
|
def _init_store(self):
|
||||||
if not CONF.enable_save:
|
if not CONF.enable_save:
|
||||||
@ -58,31 +68,26 @@ class ObjectStoreMixin:
|
|||||||
self.__class__.__name__.lower(),
|
self.__class__.__name__.lower(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _dump(self):
|
|
||||||
dump = {}
|
|
||||||
with self.lock:
|
|
||||||
for key in self.data.keys():
|
|
||||||
dump[key] = self.data[key]
|
|
||||||
|
|
||||||
return dump
|
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""Save any queued to disk?"""
|
"""Save any queued to disk?"""
|
||||||
if not CONF.enable_save:
|
if not CONF.enable_save:
|
||||||
return
|
return
|
||||||
|
self._init_store()
|
||||||
|
save_filename = self._save_filename()
|
||||||
if len(self) > 0:
|
if len(self) > 0:
|
||||||
LOG.info(
|
LOG.info(
|
||||||
f"{self.__class__.__name__}::Saving"
|
f"{self.__class__.__name__}::Saving"
|
||||||
f" {len(self)} entries to disk at"
|
f" {len(self)} entries to disk at "
|
||||||
f"{CONF.save_location}",
|
f"{save_filename}",
|
||||||
)
|
)
|
||||||
with open(self._save_filename(), "wb+") as fp:
|
with self.lock:
|
||||||
pickle.dump(self._dump(), fp)
|
with open(save_filename, "wb+") as fp:
|
||||||
|
pickle.dump(self.data, fp)
|
||||||
else:
|
else:
|
||||||
LOG.debug(
|
LOG.debug(
|
||||||
"{} Nothing to save, flushing old save file '{}'".format(
|
"{} Nothing to save, flushing old save file '{}'".format(
|
||||||
self.__class__.__name__,
|
self.__class__.__name__,
|
||||||
self._save_filename(),
|
save_filename,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.flush()
|
self.flush()
|
||||||
|
@ -1,189 +1,4 @@
|
|||||||
/* PrismJS 1.24.1
|
/* PrismJS 1.29.0
|
||||||
https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+log&plugins=show-language+toolbar */
|
https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+json+json5+log&plugins=show-language+toolbar */
|
||||||
/**
|
code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#2d2d2d}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}
|
||||||
* prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML
|
div.code-toolbar{position:relative}div.code-toolbar>.toolbar{position:absolute;z-index:10;top:.3em;right:.2em;transition:opacity .3s ease-in-out;opacity:0}div.code-toolbar:hover>.toolbar{opacity:1}div.code-toolbar:focus-within>.toolbar{opacity:1}div.code-toolbar>.toolbar>.toolbar-item{display:inline-block}div.code-toolbar>.toolbar>.toolbar-item>a{cursor:pointer}div.code-toolbar>.toolbar>.toolbar-item>button{background:0 0;border:0;color:inherit;font:inherit;line-height:normal;overflow:visible;padding:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none}div.code-toolbar>.toolbar>.toolbar-item>a,div.code-toolbar>.toolbar>.toolbar-item>button,div.code-toolbar>.toolbar>.toolbar-item>span{color:#bbb;font-size:.8em;padding:0 .5em;background:#f5f2f0;background:rgba(224,224,224,.2);box-shadow:0 2px 0 0 rgba(0,0,0,.2);border-radius:.5em}div.code-toolbar>.toolbar>.toolbar-item>a:focus,div.code-toolbar>.toolbar>.toolbar-item>a:hover,div.code-toolbar>.toolbar>.toolbar-item>button:focus,div.code-toolbar>.toolbar>.toolbar-item>button:hover,div.code-toolbar>.toolbar>.toolbar-item>span:focus,div.code-toolbar>.toolbar>.toolbar-item>span:hover{color:inherit;text-decoration:none}
|
||||||
* Based on https://github.com/chriskempson/tomorrow-theme
|
|
||||||
* @author Rose Pritchard
|
|
||||||
*/
|
|
||||||
|
|
||||||
code[class*="language-"],
|
|
||||||
pre[class*="language-"] {
|
|
||||||
color: #ccc;
|
|
||||||
background: none;
|
|
||||||
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
|
|
||||||
font-size: 1em;
|
|
||||||
text-align: left;
|
|
||||||
white-space: pre;
|
|
||||||
word-spacing: normal;
|
|
||||||
word-break: normal;
|
|
||||||
word-wrap: normal;
|
|
||||||
line-height: 1.5;
|
|
||||||
|
|
||||||
-moz-tab-size: 4;
|
|
||||||
-o-tab-size: 4;
|
|
||||||
tab-size: 4;
|
|
||||||
|
|
||||||
-webkit-hyphens: none;
|
|
||||||
-moz-hyphens: none;
|
|
||||||
-ms-hyphens: none;
|
|
||||||
hyphens: none;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Code blocks */
|
|
||||||
pre[class*="language-"] {
|
|
||||||
padding: 1em;
|
|
||||||
margin: .5em 0;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
:not(pre) > code[class*="language-"],
|
|
||||||
pre[class*="language-"] {
|
|
||||||
background: #2d2d2d;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Inline code */
|
|
||||||
:not(pre) > code[class*="language-"] {
|
|
||||||
padding: .1em;
|
|
||||||
border-radius: .3em;
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.comment,
|
|
||||||
.token.block-comment,
|
|
||||||
.token.prolog,
|
|
||||||
.token.doctype,
|
|
||||||
.token.cdata {
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.punctuation {
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.tag,
|
|
||||||
.token.attr-name,
|
|
||||||
.token.namespace,
|
|
||||||
.token.deleted {
|
|
||||||
color: #e2777a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.function-name {
|
|
||||||
color: #6196cc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.boolean,
|
|
||||||
.token.number,
|
|
||||||
.token.function {
|
|
||||||
color: #f08d49;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.property,
|
|
||||||
.token.class-name,
|
|
||||||
.token.constant,
|
|
||||||
.token.symbol {
|
|
||||||
color: #f8c555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.selector,
|
|
||||||
.token.important,
|
|
||||||
.token.atrule,
|
|
||||||
.token.keyword,
|
|
||||||
.token.builtin {
|
|
||||||
color: #cc99cd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.string,
|
|
||||||
.token.char,
|
|
||||||
.token.attr-value,
|
|
||||||
.token.regex,
|
|
||||||
.token.variable {
|
|
||||||
color: #7ec699;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.operator,
|
|
||||||
.token.entity,
|
|
||||||
.token.url {
|
|
||||||
color: #67cdcc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.important,
|
|
||||||
.token.bold {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.token.italic {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.entity {
|
|
||||||
cursor: help;
|
|
||||||
}
|
|
||||||
|
|
||||||
.token.inserted {
|
|
||||||
color: green;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.code-toolbar {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.code-toolbar > .toolbar {
|
|
||||||
position: absolute;
|
|
||||||
top: .3em;
|
|
||||||
right: .2em;
|
|
||||||
transition: opacity 0.3s ease-in-out;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.code-toolbar:hover > .toolbar {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Separate line b/c rules are thrown out if selector is invalid.
|
|
||||||
IE11 and old Edge versions don't support :focus-within. */
|
|
||||||
div.code-toolbar:focus-within > .toolbar {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item > a {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item > button {
|
|
||||||
background: none;
|
|
||||||
border: 0;
|
|
||||||
color: inherit;
|
|
||||||
font: inherit;
|
|
||||||
line-height: normal;
|
|
||||||
overflow: visible;
|
|
||||||
padding: 0;
|
|
||||||
-webkit-user-select: none; /* for button */
|
|
||||||
-moz-user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item > a,
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item > button,
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item > span {
|
|
||||||
color: #bbb;
|
|
||||||
font-size: .8em;
|
|
||||||
padding: 0 .5em;
|
|
||||||
background: #f5f2f0;
|
|
||||||
background: rgba(224, 224, 224, 0.2);
|
|
||||||
box-shadow: 0 2px 0 0 rgba(0,0,0,0.2);
|
|
||||||
border-radius: .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item > a:hover,
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item > a:focus,
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item > button:hover,
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item > button:focus,
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item > span:hover,
|
|
||||||
div.code-toolbar > .toolbar > .toolbar-item > span:focus {
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
@ -219,15 +219,17 @@ function updateQuadData(chart, label, first, second, third, fourth) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function update_stats( data ) {
|
function update_stats( data ) {
|
||||||
our_callsign = data["stats"]["aprsd"]["callsign"];
|
our_callsign = data["APRSDStats"]["callsign"];
|
||||||
$("#version").text( data["stats"]["aprsd"]["version"] );
|
$("#version").text( data["APRSDStats"]["version"] );
|
||||||
$("#aprs_connection").html( data["aprs_connection"] );
|
$("#aprs_connection").html( data["aprs_connection"] );
|
||||||
$("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] );
|
$("#uptime").text( "uptime: " + data["APRSDStats"]["uptime"] );
|
||||||
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
|
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
|
||||||
$("#jsonstats").html(html_pretty);
|
$("#jsonstats").html(html_pretty);
|
||||||
short_time = data["time"].split(/\s(.+)/)[1];
|
short_time = data["time"].split(/\s(.+)/)[1];
|
||||||
updateDualData(packets_chart, short_time, data["stats"]["packets"]["sent"], data["stats"]["packets"]["received"]);
|
packet_list = data["PacketList"]["packets"];
|
||||||
updateQuadData(message_chart, short_time, data["stats"]["messages"]["sent"], data["stats"]["messages"]["received"], data["stats"]["messages"]["ack_sent"], data["stats"]["messages"]["ack_recieved"]);
|
updateDualData(packets_chart, short_time, data["PacketList"]["sent"], data["PacketList"]["received"]);
|
||||||
updateDualData(email_chart, short_time, data["stats"]["email"]["sent"], data["stats"]["email"]["recieved"]);
|
updateQuadData(message_chart, short_time, packet_list["MessagePacket"]["tx"], packet_list["MessagePacket"]["rx"],
|
||||||
updateDualData(memory_chart, short_time, data["stats"]["aprsd"]["memory_peak"], data["stats"]["aprsd"]["memory_current"]);
|
packet_list["AckPacket"]["tx"], packet_list["AckPacket"]["rx"]);
|
||||||
|
updateDualData(email_chart, short_time, data["EmailStats"]["sent"], data["EmailStats"]["recieved"]);
|
||||||
|
updateDualData(memory_chart, short_time, data["APRSDStats"]["memory_peak"], data["APRSDStats"]["memory_current"]);
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ var packet_types_data = {};
|
|||||||
var mem_current = []
|
var mem_current = []
|
||||||
var mem_peak = []
|
var mem_peak = []
|
||||||
|
|
||||||
|
var thread_current = []
|
||||||
|
|
||||||
|
|
||||||
function start_charts() {
|
function start_charts() {
|
||||||
console.log("start_charts() called");
|
console.log("start_charts() called");
|
||||||
@ -17,6 +19,7 @@ function start_charts() {
|
|||||||
create_messages_chart();
|
create_messages_chart();
|
||||||
create_ack_chart();
|
create_ack_chart();
|
||||||
create_memory_chart();
|
create_memory_chart();
|
||||||
|
create_thread_chart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -258,6 +261,49 @@ function create_memory_chart() {
|
|||||||
memory_chart.setOption(option);
|
memory_chart.setOption(option);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function create_thread_chart() {
|
||||||
|
thread_canvas = document.getElementById('threadChart');
|
||||||
|
thread_chart = echarts.init(thread_canvas);
|
||||||
|
|
||||||
|
// Specify the configuration items and data for the chart
|
||||||
|
var option = {
|
||||||
|
title: {
|
||||||
|
text: 'Active Threads'
|
||||||
|
},
|
||||||
|
legend: {},
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis'
|
||||||
|
},
|
||||||
|
toolbox: {
|
||||||
|
show: true,
|
||||||
|
feature: {
|
||||||
|
mark : {show: true},
|
||||||
|
dataView : {show: true, readOnly: false},
|
||||||
|
magicType : {show: true, type: ['line', 'bar']},
|
||||||
|
restore : {show: true},
|
||||||
|
saveAsImage : {show: true}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
calculable: true,
|
||||||
|
xAxis: { type: 'time' },
|
||||||
|
yAxis: { },
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'current',
|
||||||
|
type: 'line',
|
||||||
|
smooth: true,
|
||||||
|
color: 'red',
|
||||||
|
encode: {
|
||||||
|
x: 'timestamp',
|
||||||
|
y: 'current' // refer sensor 1 value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
thread_chart.setOption(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -327,7 +373,6 @@ function updatePacketTypesChart() {
|
|||||||
option = {
|
option = {
|
||||||
series: series
|
series: series
|
||||||
}
|
}
|
||||||
console.log(option)
|
|
||||||
packet_types_chart.setOption(option);
|
packet_types_chart.setOption(option);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -372,6 +417,21 @@ function updateMemChart(time, current, peak) {
|
|||||||
memory_chart.setOption(option);
|
memory_chart.setOption(option);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateThreadChart(time, threads) {
|
||||||
|
keys = Object.keys(threads);
|
||||||
|
thread_count = keys.length;
|
||||||
|
thread_current.push([time, thread_count]);
|
||||||
|
option = {
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: 'current',
|
||||||
|
data: thread_current,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
thread_chart.setOption(option);
|
||||||
|
}
|
||||||
|
|
||||||
function updateMessagesChart() {
|
function updateMessagesChart() {
|
||||||
updateTypeChart(message_chart, "MessagePacket")
|
updateTypeChart(message_chart, "MessagePacket")
|
||||||
}
|
}
|
||||||
@ -381,22 +441,24 @@ function updateAcksChart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function update_stats( data ) {
|
function update_stats( data ) {
|
||||||
console.log(data);
|
console.log("update_stats() echarts.js called")
|
||||||
our_callsign = data["stats"]["aprsd"]["callsign"];
|
stats = data["stats"];
|
||||||
$("#version").text( data["stats"]["aprsd"]["version"] );
|
our_callsign = stats["APRSDStats"]["callsign"];
|
||||||
$("#aprs_connection").html( data["aprs_connection"] );
|
$("#version").text( stats["APRSDStats"]["version"] );
|
||||||
$("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] );
|
$("#aprs_connection").html( stats["aprs_connection"] );
|
||||||
|
$("#uptime").text( "uptime: " + stats["APRSDStats"]["uptime"] );
|
||||||
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
|
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
|
||||||
$("#jsonstats").html(html_pretty);
|
$("#jsonstats").html(html_pretty);
|
||||||
|
|
||||||
t = Date.parse(data["time"]);
|
t = Date.parse(data["time"]);
|
||||||
ts = new Date(t);
|
ts = new Date(t);
|
||||||
updatePacketData(packets_chart, ts, data["stats"]["packets"]["sent"], data["stats"]["packets"]["received"]);
|
updatePacketData(packets_chart, ts, stats["PacketList"]["tx"], stats["PacketList"]["rx"]);
|
||||||
updatePacketTypesData(ts, data["stats"]["packets"]["types"]);
|
updatePacketTypesData(ts, stats["PacketList"]["types"]);
|
||||||
updatePacketTypesChart();
|
updatePacketTypesChart();
|
||||||
updateMessagesChart();
|
updateMessagesChart();
|
||||||
updateAcksChart();
|
updateAcksChart();
|
||||||
updateMemChart(ts, data["stats"]["aprsd"]["memory_current"], data["stats"]["aprsd"]["memory_peak"]);
|
updateMemChart(ts, stats["APRSDStats"]["memory_current"], stats["APRSDStats"]["memory_peak"]);
|
||||||
|
updateThreadChart(ts, stats["APRSDThreadList"]);
|
||||||
//updateQuadData(message_chart, short_time, data["stats"]["messages"]["sent"], data["stats"]["messages"]["received"], data["stats"]["messages"]["ack_sent"], data["stats"]["messages"]["ack_recieved"]);
|
//updateQuadData(message_chart, short_time, data["stats"]["messages"]["sent"], data["stats"]["messages"]["received"], data["stats"]["messages"]["ack_sent"], data["stats"]["messages"]["ack_recieved"]);
|
||||||
//updateDualData(email_chart, short_time, data["stats"]["email"]["sent"], data["stats"]["email"]["recieved"]);
|
//updateDualData(email_chart, short_time, data["stats"]["email"]["sent"], data["stats"]["email"]["recieved"]);
|
||||||
//updateDualData(memory_chart, short_time, data["stats"]["aprsd"]["memory_peak"], data["stats"]["aprsd"]["memory_current"]);
|
//updateDualData(memory_chart, short_time, data["stats"]["aprsd"]["memory_peak"], data["stats"]["aprsd"]["memory_current"]);
|
||||||
|
@ -25,10 +25,14 @@ function ord(str){return str.charCodeAt(0);}
|
|||||||
|
|
||||||
function update_watchlist( data ) {
|
function update_watchlist( data ) {
|
||||||
// Update the watch list
|
// Update the watch list
|
||||||
|
stats = data["stats"];
|
||||||
|
if (stats.hasOwnProperty("WatchList") == false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
var watchdiv = $("#watchDiv");
|
var watchdiv = $("#watchDiv");
|
||||||
var html_str = '<table class="ui celled striped table"><thead><tr><th>HAM Callsign</th><th>Age since last seen by APRSD</th></tr></thead><tbody>'
|
var html_str = '<table class="ui celled striped table"><thead><tr><th>HAM Callsign</th><th>Age since last seen by APRSD</th></tr></thead><tbody>'
|
||||||
watchdiv.html('')
|
watchdiv.html('')
|
||||||
jQuery.each(data["stats"]["aprsd"]["watch_list"], function(i, val) {
|
jQuery.each(stats["WatchList"], function(i, val) {
|
||||||
html_str += '<tr><td class="collapsing"><img id="callsign_'+i+'" class="aprsd_1"></img>' + i + '</td><td>' + val["last"] + '</td></tr>'
|
html_str += '<tr><td class="collapsing"><img id="callsign_'+i+'" class="aprsd_1"></img>' + i + '</td><td>' + val["last"] + '</td></tr>'
|
||||||
});
|
});
|
||||||
html_str += "</tbody></table>";
|
html_str += "</tbody></table>";
|
||||||
@ -60,12 +64,16 @@ function update_watchlist_from_packet(callsign, val) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function update_seenlist( data ) {
|
function update_seenlist( data ) {
|
||||||
|
stats = data["stats"];
|
||||||
|
if (stats.hasOwnProperty("SeenList") == false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
var seendiv = $("#seenDiv");
|
var seendiv = $("#seenDiv");
|
||||||
var html_str = '<table class="ui celled striped table">'
|
var html_str = '<table class="ui celled striped table">'
|
||||||
html_str += '<thead><tr><th>HAM Callsign</th><th>Age since last seen by APRSD</th>'
|
html_str += '<thead><tr><th>HAM Callsign</th><th>Age since last seen by APRSD</th>'
|
||||||
html_str += '<th>Number of packets RX</th></tr></thead><tbody>'
|
html_str += '<th>Number of packets RX</th></tr></thead><tbody>'
|
||||||
seendiv.html('')
|
seendiv.html('')
|
||||||
var seen_list = data["stats"]["aprsd"]["seen_list"]
|
var seen_list = stats["SeenList"]
|
||||||
var len = Object.keys(seen_list).length
|
var len = Object.keys(seen_list).length
|
||||||
$('#seen_count').html(len)
|
$('#seen_count').html(len)
|
||||||
jQuery.each(seen_list, function(i, val) {
|
jQuery.each(seen_list, function(i, val) {
|
||||||
@ -79,6 +87,10 @@ function update_seenlist( data ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function update_plugins( data ) {
|
function update_plugins( data ) {
|
||||||
|
stats = data["stats"];
|
||||||
|
if (stats.hasOwnProperty("PluginManager") == false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
var plugindiv = $("#pluginDiv");
|
var plugindiv = $("#pluginDiv");
|
||||||
var html_str = '<table class="ui celled striped table"><thead><tr>'
|
var html_str = '<table class="ui celled striped table"><thead><tr>'
|
||||||
html_str += '<th>Plugin Name</th><th>Plugin Enabled?</th>'
|
html_str += '<th>Plugin Name</th><th>Plugin Enabled?</th>'
|
||||||
@ -87,7 +99,7 @@ function update_plugins( data ) {
|
|||||||
html_str += '</tr></thead><tbody>'
|
html_str += '</tr></thead><tbody>'
|
||||||
plugindiv.html('')
|
plugindiv.html('')
|
||||||
|
|
||||||
var plugins = data["stats"]["plugins"];
|
var plugins = stats["PluginManager"];
|
||||||
var keys = Object.keys(plugins);
|
var keys = Object.keys(plugins);
|
||||||
keys.sort();
|
keys.sort();
|
||||||
for (var i=0; i<keys.length; i++) { // now lets iterate in sort order
|
for (var i=0; i<keys.length; i++) { // now lets iterate in sort order
|
||||||
@ -101,14 +113,42 @@ function update_plugins( data ) {
|
|||||||
plugindiv.append(html_str);
|
plugindiv.append(html_str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update_threads( data ) {
|
||||||
|
stats = data["stats"];
|
||||||
|
if (stats.hasOwnProperty("APRSDThreadList") == false) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var threadsdiv = $("#threadsDiv");
|
||||||
|
var countdiv = $("#thread_count");
|
||||||
|
var html_str = '<table class="ui celled striped table"><thead><tr>'
|
||||||
|
html_str += '<th>Thread Name</th><th>Alive?</th>'
|
||||||
|
html_str += '<th>Age</th><th>Loop Count</th>'
|
||||||
|
html_str += '</tr></thead><tbody>'
|
||||||
|
threadsdiv.html('')
|
||||||
|
|
||||||
|
var threads = stats["APRSDThreadList"];
|
||||||
|
var keys = Object.keys(threads);
|
||||||
|
countdiv.html(keys.length);
|
||||||
|
keys.sort();
|
||||||
|
for (var i=0; i<keys.length; i++) { // now lets iterate in sort order
|
||||||
|
var key = keys[i];
|
||||||
|
var val = threads[key];
|
||||||
|
html_str += '<tr><td class="collapsing">' + key + '</td>';
|
||||||
|
html_str += '<td>' + val["alive"] + '</td><td>' + val["age"] + '</td>';
|
||||||
|
html_str += '<td>' + val["loop_count"] + '</td></tr>';
|
||||||
|
}
|
||||||
|
html_str += "</tbody></table>";
|
||||||
|
threadsdiv.append(html_str);
|
||||||
|
}
|
||||||
|
|
||||||
function update_packets( data ) {
|
function update_packets( data ) {
|
||||||
var packetsdiv = $("#packetsDiv");
|
var packetsdiv = $("#packetsDiv");
|
||||||
//nuke the contents first, then add to it.
|
//nuke the contents first, then add to it.
|
||||||
if (size_dict(packet_list) == 0 && size_dict(data) > 0) {
|
if (size_dict(packet_list) == 0 && size_dict(data) > 0) {
|
||||||
packetsdiv.html('')
|
packetsdiv.html('')
|
||||||
}
|
}
|
||||||
jQuery.each(data, function(i, val) {
|
jQuery.each(data.packets, function(i, val) {
|
||||||
pkt = JSON.parse(val);
|
pkt = val;
|
||||||
|
|
||||||
update_watchlist_from_packet(pkt['from_call'], pkt);
|
update_watchlist_from_packet(pkt['from_call'], pkt);
|
||||||
if ( packet_list.hasOwnProperty(pkt['timestamp']) == false ) {
|
if ( packet_list.hasOwnProperty(pkt['timestamp']) == false ) {
|
||||||
@ -167,6 +207,7 @@ function start_update() {
|
|||||||
update_watchlist(data);
|
update_watchlist(data);
|
||||||
update_seenlist(data);
|
update_seenlist(data);
|
||||||
update_plugins(data);
|
update_plugins(data);
|
||||||
|
update_threads(data);
|
||||||
},
|
},
|
||||||
complete: function() {
|
complete: function() {
|
||||||
setTimeout(statsworker, 10000);
|
setTimeout(statsworker, 10000);
|
||||||
|
File diff suppressed because one or more lines are too long
@ -1,57 +0,0 @@
|
|||||||
/* Root element */
|
|
||||||
.json-document {
|
|
||||||
padding: 1em 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Syntax highlighting for JSON objects */
|
|
||||||
ul.json-dict, ol.json-array {
|
|
||||||
list-style-type: none;
|
|
||||||
margin: 0 0 0 1px;
|
|
||||||
border-left: 1px dotted #ccc;
|
|
||||||
padding-left: 2em;
|
|
||||||
}
|
|
||||||
.json-string {
|
|
||||||
color: #0B7500;
|
|
||||||
}
|
|
||||||
.json-literal {
|
|
||||||
color: #1A01CC;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Toggle button */
|
|
||||||
a.json-toggle {
|
|
||||||
position: relative;
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
a.json-toggle:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
a.json-toggle:before {
|
|
||||||
font-size: 1.1em;
|
|
||||||
color: #c0c0c0;
|
|
||||||
content: "\25BC"; /* down arrow */
|
|
||||||
position: absolute;
|
|
||||||
display: inline-block;
|
|
||||||
width: 1em;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1em;
|
|
||||||
left: -1.2em;
|
|
||||||
}
|
|
||||||
a.json-toggle:hover:before {
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
a.json-toggle.collapsed:before {
|
|
||||||
/* Use rotated down arrow, prevents right arrow appearing smaller than down arrow in some browsers */
|
|
||||||
transform: rotate(-90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Collapsable placeholder links */
|
|
||||||
a.json-placeholder {
|
|
||||||
color: #aaa;
|
|
||||||
padding: 0 1em;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
a.json-placeholder:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
@ -1,158 +0,0 @@
|
|||||||
/**
|
|
||||||
* jQuery json-viewer
|
|
||||||
* @author: Alexandre Bodelot <alexandre.bodelot@gmail.com>
|
|
||||||
* @link: https://github.com/abodelot/jquery.json-viewer
|
|
||||||
*/
|
|
||||||
(function($) {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if arg is either an array with at least 1 element, or a dict with at least 1 key
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
function isCollapsable(arg) {
|
|
||||||
return arg instanceof Object && Object.keys(arg).length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a string represents a valid url
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
function isUrl(string) {
|
|
||||||
var urlRegexp = /^(https?:\/\/|ftps?:\/\/)?([a-z0-9%-]+\.){1,}([a-z0-9-]+)?(:(\d{1,5}))?(\/([a-z0-9\-._~:/?#[\]@!$&'()*+,;=%]+)?)?$/i;
|
|
||||||
return urlRegexp.test(string);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform a json object into html representation
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
function json2html(json, options) {
|
|
||||||
var html = '';
|
|
||||||
if (typeof json === 'string') {
|
|
||||||
// Escape tags and quotes
|
|
||||||
json = json
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/'/g, ''')
|
|
||||||
.replace(/"/g, '"');
|
|
||||||
|
|
||||||
if (options.withLinks && isUrl(json)) {
|
|
||||||
html += '<a href="' + json + '" class="json-string" target="_blank">' + json + '</a>';
|
|
||||||
} else {
|
|
||||||
// Escape double quotes in the rendered non-URL string.
|
|
||||||
json = json.replace(/"/g, '\\"');
|
|
||||||
html += '<span class="json-string">"' + json + '"</span>';
|
|
||||||
}
|
|
||||||
} else if (typeof json === 'number') {
|
|
||||||
html += '<span class="json-literal">' + json + '</span>';
|
|
||||||
} else if (typeof json === 'boolean') {
|
|
||||||
html += '<span class="json-literal">' + json + '</span>';
|
|
||||||
} else if (json === null) {
|
|
||||||
html += '<span class="json-literal">null</span>';
|
|
||||||
} else if (json instanceof Array) {
|
|
||||||
if (json.length > 0) {
|
|
||||||
html += '[<ol class="json-array">';
|
|
||||||
for (var i = 0; i < json.length; ++i) {
|
|
||||||
html += '<li>';
|
|
||||||
// Add toggle button if item is collapsable
|
|
||||||
if (isCollapsable(json[i])) {
|
|
||||||
html += '<a href class="json-toggle"></a>';
|
|
||||||
}
|
|
||||||
html += json2html(json[i], options);
|
|
||||||
// Add comma if item is not last
|
|
||||||
if (i < json.length - 1) {
|
|
||||||
html += ',';
|
|
||||||
}
|
|
||||||
html += '</li>';
|
|
||||||
}
|
|
||||||
html += '</ol>]';
|
|
||||||
} else {
|
|
||||||
html += '[]';
|
|
||||||
}
|
|
||||||
} else if (typeof json === 'object') {
|
|
||||||
var keyCount = Object.keys(json).length;
|
|
||||||
if (keyCount > 0) {
|
|
||||||
html += '{<ul class="json-dict">';
|
|
||||||
for (var key in json) {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(json, key)) {
|
|
||||||
html += '<li>';
|
|
||||||
var keyRepr = options.withQuotes ?
|
|
||||||
'<span class="json-string">"' + key + '"</span>' : key;
|
|
||||||
// Add toggle button if item is collapsable
|
|
||||||
if (isCollapsable(json[key])) {
|
|
||||||
html += '<a href class="json-toggle">' + keyRepr + '</a>';
|
|
||||||
} else {
|
|
||||||
html += keyRepr;
|
|
||||||
}
|
|
||||||
html += ': ' + json2html(json[key], options);
|
|
||||||
// Add comma if item is not last
|
|
||||||
if (--keyCount > 0) {
|
|
||||||
html += ',';
|
|
||||||
}
|
|
||||||
html += '</li>';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
html += '</ul>}';
|
|
||||||
} else {
|
|
||||||
html += '{}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* jQuery plugin method
|
|
||||||
* @param json: a javascript object
|
|
||||||
* @param options: an optional options hash
|
|
||||||
*/
|
|
||||||
$.fn.jsonViewer = function(json, options) {
|
|
||||||
// Merge user options with default options
|
|
||||||
options = Object.assign({}, {
|
|
||||||
collapsed: false,
|
|
||||||
rootCollapsable: true,
|
|
||||||
withQuotes: false,
|
|
||||||
withLinks: true
|
|
||||||
}, options);
|
|
||||||
|
|
||||||
// jQuery chaining
|
|
||||||
return this.each(function() {
|
|
||||||
|
|
||||||
// Transform to HTML
|
|
||||||
var html = json2html(json, options);
|
|
||||||
if (options.rootCollapsable && isCollapsable(json)) {
|
|
||||||
html = '<a href class="json-toggle"></a>' + html;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert HTML in target DOM element
|
|
||||||
$(this).html(html);
|
|
||||||
$(this).addClass('json-document');
|
|
||||||
|
|
||||||
// Bind click on toggle buttons
|
|
||||||
$(this).off('click');
|
|
||||||
$(this).on('click', 'a.json-toggle', function() {
|
|
||||||
var target = $(this).toggleClass('collapsed').siblings('ul.json-dict, ol.json-array');
|
|
||||||
target.toggle();
|
|
||||||
if (target.is(':visible')) {
|
|
||||||
target.siblings('.json-placeholder').remove();
|
|
||||||
} else {
|
|
||||||
var count = target.children('li').length;
|
|
||||||
var placeholder = count + (count > 1 ? ' items' : ' item');
|
|
||||||
target.after('<a href class="json-placeholder">' + placeholder + '</a>');
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Simulate click on toggle button when placeholder is clicked
|
|
||||||
$(this).on('click', 'a.json-placeholder', function() {
|
|
||||||
$(this).siblings('a.json-toggle').click();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (options.collapsed == true) {
|
|
||||||
// Trigger click to collapse all nodes
|
|
||||||
$(this).find('a.json-toggle').click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
})(jQuery);
|
|
@ -30,7 +30,6 @@
|
|||||||
var color = Chart.helpers.color;
|
var color = Chart.helpers.color;
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
console.log(initial_stats);
|
|
||||||
start_update();
|
start_update();
|
||||||
start_charts();
|
start_charts();
|
||||||
init_messages();
|
init_messages();
|
||||||
@ -82,6 +81,7 @@
|
|||||||
<div class="item" data-tab="seen-tab">Seen List</div>
|
<div class="item" data-tab="seen-tab">Seen List</div>
|
||||||
<div class="item" data-tab="watch-tab">Watch List</div>
|
<div class="item" data-tab="watch-tab">Watch List</div>
|
||||||
<div class="item" data-tab="plugin-tab">Plugins</div>
|
<div class="item" data-tab="plugin-tab">Plugins</div>
|
||||||
|
<div class="item" data-tab="threads-tab">Threads</div>
|
||||||
<div class="item" data-tab="config-tab">Config</div>
|
<div class="item" data-tab="config-tab">Config</div>
|
||||||
<div class="item" data-tab="log-tab">LogFile</div>
|
<div class="item" data-tab="log-tab">LogFile</div>
|
||||||
<!-- <div class="item" data-tab="oslo-tab">OSLO CONFIG</div> //-->
|
<!-- <div class="item" data-tab="oslo-tab">OSLO CONFIG</div> //-->
|
||||||
@ -97,11 +97,6 @@
|
|||||||
<div class="ui segment" style="height: 300px" id="packetsChart"></div>
|
<div class="ui segment" style="height: 300px" id="packetsChart"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
|
||||||
<div class="column">
|
|
||||||
<div class="ui segment" style="height: 300px" id="packetTypesChart"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="ui segment" style="height: 300px" id="messagesChart"></div>
|
<div class="ui segment" style="height: 300px" id="messagesChart"></div>
|
||||||
@ -112,9 +107,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<div class="ui segment" style="height: 300px" id="memChart">
|
<div class="ui segment" style="height: 300px" id="packetTypesChart"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="column">
|
||||||
|
<div class="ui segment" style="height: 300px" id="threadChart"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="column">
|
||||||
|
<div class="ui segment" style="height: 300px" id="memChart"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="row">
|
<!-- <div class="row">
|
||||||
<div id="stats" class="two column">
|
<div id="stats" class="two column">
|
||||||
@ -156,6 +160,13 @@
|
|||||||
<div id="pluginDiv" class="ui mini text">Loading</div>
|
<div id="pluginDiv" class="ui mini text">Loading</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="ui bottom attached tab segment" data-tab="threads-tab">
|
||||||
|
<h3 class="ui dividing header">
|
||||||
|
Threads Loaded (<span id="thread_count">{{ thread_count }}</span>)
|
||||||
|
</h3>
|
||||||
|
<div id="threadsDiv" class="ui mini text">Loading</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="ui bottom attached tab segment" data-tab="config-tab">
|
<div class="ui bottom attached tab segment" data-tab="config-tab">
|
||||||
<h3 class="ui dividing header">Config</h3>
|
<h3 class="ui dividing header">Config</h3>
|
||||||
<pre id="configjson" class="language-json">{{ config_json|safe }}</pre>
|
<pre id="configjson" class="language-json">{{ config_json|safe }}</pre>
|
||||||
@ -174,7 +185,7 @@
|
|||||||
|
|
||||||
<div class="ui bottom attached tab segment" data-tab="raw-tab">
|
<div class="ui bottom attached tab segment" data-tab="raw-tab">
|
||||||
<h3 class="ui dividing header">Raw JSON</h3>
|
<h3 class="ui dividing header">Raw JSON</h3>
|
||||||
<pre id="jsonstats" class="language-yaml" style="height:600px;overflow-y:auto;">{{ stats|safe }}</pre>
|
<pre id="jsonstats" class="language-yaml" style="height:600px;overflow-y:auto;">{{ initial_stats|safe }}</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ui text container">
|
<div class="ui text container">
|
||||||
|
@ -64,9 +64,11 @@ function showError(error) {
|
|||||||
|
|
||||||
function showPosition(position) {
|
function showPosition(position) {
|
||||||
console.log("showPosition Called");
|
console.log("showPosition Called");
|
||||||
|
path = $('#pkt_path option:selected').val();
|
||||||
msg = {
|
msg = {
|
||||||
'latitude': position.coords.latitude,
|
'latitude': position.coords.latitude,
|
||||||
'longitude': position.coords.longitude
|
'longitude': position.coords.longitude,
|
||||||
|
'path': path,
|
||||||
}
|
}
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
$.toast({
|
$.toast({
|
||||||
|
@ -19,9 +19,10 @@ function show_aprs_icon(item, symbol) {
|
|||||||
function ord(str){return str.charCodeAt(0);}
|
function ord(str){return str.charCodeAt(0);}
|
||||||
|
|
||||||
function update_stats( data ) {
|
function update_stats( data ) {
|
||||||
$("#version").text( data["stats"]["aprsd"]["version"] );
|
console.log(data);
|
||||||
|
$("#version").text( data["stats"]["APRSDStats"]["version"] );
|
||||||
$("#aprs_connection").html( data["aprs_connection"] );
|
$("#aprs_connection").html( data["aprs_connection"] );
|
||||||
$("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] );
|
$("#uptime").text( "uptime: " + data["stats"]["APRSDStats"]["uptime"] );
|
||||||
short_time = data["time"].split(/\s(.+)/)[1];
|
short_time = data["time"].split(/\s(.+)/)[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,7 +38,7 @@ function start_update() {
|
|||||||
update_stats(data);
|
update_stats(data);
|
||||||
},
|
},
|
||||||
complete: function() {
|
complete: function() {
|
||||||
setTimeout(statsworker, 10000);
|
setTimeout(statsworker, 60000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
@ -313,6 +313,7 @@ function create_callsign_tab(callsign, active=false) {
|
|||||||
//item_html += '<button onClick="callsign_select(\''+callsign+'\');" callsign="'+callsign+'" class="nav-link '+active_str+'" id="'+tab_id+'" data-bs-toggle="tab" data-bs-target="#'+tab_content+'" type="button" role="tab" aria-controls="'+callsign+'" aria-selected="true">';
|
//item_html += '<button onClick="callsign_select(\''+callsign+'\');" callsign="'+callsign+'" class="nav-link '+active_str+'" id="'+tab_id+'" data-bs-toggle="tab" data-bs-target="#'+tab_content+'" type="button" role="tab" aria-controls="'+callsign+'" aria-selected="true">';
|
||||||
item_html += '<button onClick="callsign_select(\''+callsign+'\');" callsign="'+callsign+'" class="nav-link position-relative '+active_str+'" id="'+tab_id+'" data-bs-toggle="tab" data-bs-target="#'+tab_content+'" type="button" role="tab" aria-controls="'+callsign+'" aria-selected="true">';
|
item_html += '<button onClick="callsign_select(\''+callsign+'\');" callsign="'+callsign+'" class="nav-link position-relative '+active_str+'" id="'+tab_id+'" data-bs-toggle="tab" data-bs-target="#'+tab_content+'" type="button" role="tab" aria-controls="'+callsign+'" aria-selected="true">';
|
||||||
item_html += callsign+' ';
|
item_html += callsign+' ';
|
||||||
|
item_html += '<span id="'+tab_notify_id+'" class="position-absolute top-0 start-80 translate-middle badge bg-danger border border-light rounded-pill visually-hidden">0</span>';
|
||||||
item_html += '<span onclick="delete_tab(\''+callsign+'\');">×</span>';
|
item_html += '<span onclick="delete_tab(\''+callsign+'\');">×</span>';
|
||||||
item_html += '</button></li>'
|
item_html += '</button></li>'
|
||||||
|
|
||||||
@ -407,13 +408,15 @@ function append_message(callsign, msg, msg_html) {
|
|||||||
tab_notify_id = tab_notification_id(callsign, true);
|
tab_notify_id = tab_notification_id(callsign, true);
|
||||||
// get the current count of notifications
|
// get the current count of notifications
|
||||||
count = parseInt($(tab_notify_id).text());
|
count = parseInt($(tab_notify_id).text());
|
||||||
|
if (isNaN(count)) {
|
||||||
|
count = 0;
|
||||||
|
}
|
||||||
count += 1;
|
count += 1;
|
||||||
$(tab_notify_id).text(count);
|
$(tab_notify_id).text(count);
|
||||||
$(tab_notify_id).removeClass('visually-hidden');
|
$(tab_notify_id).removeClass('visually-hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the right div to place the html
|
// Find the right div to place the html
|
||||||
|
|
||||||
new_callsign = add_callsign(callsign, msg);
|
new_callsign = add_callsign(callsign, msg);
|
||||||
update_callsign_path(callsign, msg);
|
update_callsign_path(callsign, msg);
|
||||||
append_message_html(callsign, msg_html, new_callsign);
|
append_message_html(callsign, msg_html, new_callsign);
|
||||||
@ -502,7 +505,7 @@ function sent_msg(msg) {
|
|||||||
msg_html = create_message_html(d, t, msg['from_call'], msg['to_call'], msg['message_text'], ack_id, msg, false);
|
msg_html = create_message_html(d, t, msg['from_call'], msg['to_call'], msg['message_text'], ack_id, msg, false);
|
||||||
append_message(msg['to_call'], msg, msg_html);
|
append_message(msg['to_call'], msg, msg_html);
|
||||||
save_data();
|
save_data();
|
||||||
scroll_main_content(msg['from_call']);
|
scroll_main_content(msg['to_call']);
|
||||||
}
|
}
|
||||||
|
|
||||||
function from_msg(msg) {
|
function from_msg(msg) {
|
||||||
|
@ -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);
|
|
@ -103,6 +103,7 @@
|
|||||||
<option value="WIDE1-1">WIDE1-1</option>
|
<option value="WIDE1-1">WIDE1-1</option>
|
||||||
<option value="WIDE1-1,WIDE2-1">WIDE1-1,WIDE2-1</option>
|
<option value="WIDE1-1,WIDE2-1">WIDE1-1,WIDE2-1</option>
|
||||||
<option value="ARISS">ARISS</option>
|
<option value="ARISS">ARISS</option>
|
||||||
|
<option value="GATE">GATE</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
|
189
aprsd/wsgi.py
189
aprsd/wsgi.py
@ -3,10 +3,10 @@ import importlib.metadata as imp
|
|||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
import queue
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from flask import Flask
|
from flask import Flask, request
|
||||||
from flask_httpauth import HTTPBasicAuth
|
from flask_httpauth import HTTPBasicAuth
|
||||||
from oslo_config import cfg, generator
|
from oslo_config import cfg, generator
|
||||||
import socketio
|
import socketio
|
||||||
@ -15,14 +15,16 @@ from werkzeug.security import check_password_hash
|
|||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import cli_helper, client, conf, packets, plugin, threads
|
from aprsd import cli_helper, client, conf, packets, plugin, threads
|
||||||
from aprsd.log import log
|
from aprsd.log import log
|
||||||
from aprsd.rpc import client as aprsd_rpc_client
|
from aprsd.threads import stats as stats_threads
|
||||||
|
from aprsd.utils import json as aprsd_json
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
LOG = logging.getLogger("gunicorn.access")
|
LOG = logging.getLogger("gunicorn.access")
|
||||||
|
logging_queue = queue.Queue()
|
||||||
|
|
||||||
auth = HTTPBasicAuth()
|
auth = HTTPBasicAuth()
|
||||||
users = {}
|
users: dict[str, str] = {}
|
||||||
app = Flask(
|
app = Flask(
|
||||||
"aprsd",
|
"aprsd",
|
||||||
static_url_path="/static",
|
static_url_path="/static",
|
||||||
@ -45,114 +47,40 @@ def verify_password(username, password):
|
|||||||
|
|
||||||
|
|
||||||
def _stats():
|
def _stats():
|
||||||
track = aprsd_rpc_client.RPCClient().get_packet_track()
|
stats_obj = stats_threads.StatsStore()
|
||||||
|
stats_obj.load()
|
||||||
now = datetime.datetime.now()
|
now = datetime.datetime.now()
|
||||||
|
|
||||||
time_format = "%m-%d-%Y %H:%M:%S"
|
time_format = "%m-%d-%Y %H:%M:%S"
|
||||||
|
stats = {
|
||||||
stats_dict = aprsd_rpc_client.RPCClient().get_stats_dict()
|
|
||||||
if not stats_dict:
|
|
||||||
stats_dict = {
|
|
||||||
"aprsd": {},
|
|
||||||
"aprs-is": {"server": ""},
|
|
||||||
"messages": {
|
|
||||||
"sent": 0,
|
|
||||||
"received": 0,
|
|
||||||
},
|
|
||||||
"email": {
|
|
||||||
"sent": 0,
|
|
||||||
"received": 0,
|
|
||||||
},
|
|
||||||
"seen_list": {
|
|
||||||
"sent": 0,
|
|
||||||
"received": 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
# Convert the watch_list entries to age
|
|
||||||
wl = aprsd_rpc_client.RPCClient().get_watch_list()
|
|
||||||
new_list = {}
|
|
||||||
if wl:
|
|
||||||
for call in wl.get_all():
|
|
||||||
# call_date = datetime.datetime.strptime(
|
|
||||||
# str(wl.last_seen(call)),
|
|
||||||
# "%Y-%m-%d %H:%M:%S.%f",
|
|
||||||
# )
|
|
||||||
|
|
||||||
# We have to convert the RingBuffer to a real list
|
|
||||||
# so that json.dumps works.
|
|
||||||
# pkts = []
|
|
||||||
# for pkt in wl.get(call)["packets"].get():
|
|
||||||
# pkts.append(pkt)
|
|
||||||
|
|
||||||
new_list[call] = {
|
|
||||||
"last": wl.age(call),
|
|
||||||
# "packets": pkts
|
|
||||||
}
|
|
||||||
|
|
||||||
stats_dict["aprsd"]["watch_list"] = new_list
|
|
||||||
packet_list = aprsd_rpc_client.RPCClient().get_packet_list()
|
|
||||||
rx = tx = 0
|
|
||||||
types = {}
|
|
||||||
if packet_list:
|
|
||||||
rx = packet_list.total_rx()
|
|
||||||
tx = packet_list.total_tx()
|
|
||||||
types_copy = packet_list.types.copy()
|
|
||||||
|
|
||||||
for key in types_copy:
|
|
||||||
types[str(key)] = dict(types_copy[key])
|
|
||||||
|
|
||||||
stats_dict["packets"] = {
|
|
||||||
"sent": tx,
|
|
||||||
"received": rx,
|
|
||||||
"types": types,
|
|
||||||
}
|
|
||||||
if track:
|
|
||||||
size_tracker = len(track)
|
|
||||||
else:
|
|
||||||
size_tracker = 0
|
|
||||||
|
|
||||||
result = {
|
|
||||||
"time": now.strftime(time_format),
|
"time": now.strftime(time_format),
|
||||||
"size_tracker": size_tracker,
|
"stats": stats_obj.data,
|
||||||
"stats": stats_dict,
|
|
||||||
}
|
}
|
||||||
|
return stats
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/stats")
|
@app.route("/stats")
|
||||||
def stats():
|
def stats():
|
||||||
LOG.debug("/stats called")
|
LOG.debug("/stats called")
|
||||||
return json.dumps(_stats())
|
return json.dumps(_stats(), cls=aprsd_json.SimpleJSONEncoder)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
stats = _stats()
|
stats = _stats()
|
||||||
wl = aprsd_rpc_client.RPCClient().get_watch_list()
|
|
||||||
if wl and wl.is_enabled():
|
|
||||||
watch_count = len(wl)
|
|
||||||
watch_age = wl.max_delta()
|
|
||||||
else:
|
|
||||||
watch_count = 0
|
|
||||||
watch_age = 0
|
|
||||||
|
|
||||||
sl = aprsd_rpc_client.RPCClient().get_seen_list()
|
|
||||||
if sl:
|
|
||||||
seen_count = len(sl)
|
|
||||||
else:
|
|
||||||
seen_count = 0
|
|
||||||
|
|
||||||
pm = plugin.PluginManager()
|
pm = plugin.PluginManager()
|
||||||
plugins = pm.get_plugins()
|
plugins = pm.get_plugins()
|
||||||
plugin_count = len(plugins)
|
plugin_count = len(plugins)
|
||||||
|
client_stats = stats["stats"].get("APRSClientStats", {})
|
||||||
|
|
||||||
if CONF.aprs_network.enabled:
|
if CONF.aprs_network.enabled:
|
||||||
transport = "aprs-is"
|
transport = "aprs-is"
|
||||||
|
if client_stats:
|
||||||
|
aprs_connection = client_stats.get("server_string", "")
|
||||||
|
else:
|
||||||
|
aprs_connection = "APRS-IS"
|
||||||
aprs_connection = (
|
aprs_connection = (
|
||||||
"APRS-IS Server: <a href='http://status.aprs2.net' >"
|
"APRS-IS Server: <a href='http://status.aprs2.net' >"
|
||||||
"{}</a>".format(stats["stats"]["aprs-is"]["server"])
|
"{}</a>".format(aprs_connection)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# We might be connected to a KISS socket?
|
# We might be connected to a KISS socket?
|
||||||
@ -173,13 +101,20 @@ def index():
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
stats["transport"] = transport
|
if client_stats:
|
||||||
stats["aprs_connection"] = aprs_connection
|
stats["stats"]["APRSClientStats"]["transport"] = transport
|
||||||
|
stats["stats"]["APRSClientStats"]["aprs_connection"] = aprs_connection
|
||||||
entries = conf.conf_to_dict()
|
entries = conf.conf_to_dict()
|
||||||
|
|
||||||
|
thread_info = stats["stats"].get("APRSDThreadList", {})
|
||||||
|
if thread_info:
|
||||||
|
thread_count = len(thread_info)
|
||||||
|
else:
|
||||||
|
thread_count = "unknown"
|
||||||
|
|
||||||
return flask.render_template(
|
return flask.render_template(
|
||||||
"index.html",
|
"index.html",
|
||||||
initial_stats=stats,
|
initial_stats=json.dumps(stats, cls=aprsd_json.SimpleJSONEncoder),
|
||||||
aprs_connection=aprs_connection,
|
aprs_connection=aprs_connection,
|
||||||
callsign=CONF.callsign,
|
callsign=CONF.callsign,
|
||||||
version=aprsd.__version__,
|
version=aprsd.__version__,
|
||||||
@ -187,10 +122,8 @@ def index():
|
|||||||
entries, indent=4,
|
entries, indent=4,
|
||||||
sort_keys=True, default=str,
|
sort_keys=True, default=str,
|
||||||
),
|
),
|
||||||
watch_count=watch_count,
|
|
||||||
watch_age=watch_age,
|
|
||||||
seen_count=seen_count,
|
|
||||||
plugin_count=plugin_count,
|
plugin_count=plugin_count,
|
||||||
|
thread_count=thread_count,
|
||||||
# oslo_out=generate_oslo()
|
# oslo_out=generate_oslo()
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -209,19 +142,10 @@ def messages():
|
|||||||
@auth.login_required
|
@auth.login_required
|
||||||
@app.route("/packets")
|
@app.route("/packets")
|
||||||
def get_packets():
|
def get_packets():
|
||||||
LOG.debug("/packets called")
|
stats = _stats()
|
||||||
packet_list = aprsd_rpc_client.RPCClient().get_packet_list()
|
stats_dict = stats["stats"]
|
||||||
if packet_list:
|
packets = stats_dict.get("PacketList", {})
|
||||||
tmp_list = []
|
return json.dumps(packets, cls=aprsd_json.SimpleJSONEncoder)
|
||||||
pkts = packet_list.copy()
|
|
||||||
for key in pkts:
|
|
||||||
pkt = packet_list.get(key)
|
|
||||||
if pkt:
|
|
||||||
tmp_list.append(pkt.json)
|
|
||||||
|
|
||||||
return json.dumps(tmp_list)
|
|
||||||
else:
|
|
||||||
return json.dumps([])
|
|
||||||
|
|
||||||
|
|
||||||
@auth.login_required
|
@auth.login_required
|
||||||
@ -273,23 +197,34 @@ def save():
|
|||||||
return json.dumps({"messages": "saved"})
|
return json.dumps({"messages": "saved"})
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/log_entries", methods=["POST"])
|
||||||
|
def log_entries():
|
||||||
|
"""The url that the server can call to update the logs."""
|
||||||
|
entries = request.json
|
||||||
|
LOG.info(f"Log entries called {len(entries)}")
|
||||||
|
for entry in entries:
|
||||||
|
logging_queue.put(entry)
|
||||||
|
return json.dumps({"messages": "saved"})
|
||||||
|
|
||||||
|
|
||||||
class LogUpdateThread(threads.APRSDThread):
|
class LogUpdateThread(threads.APRSDThread):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, logging_queue=None):
|
||||||
super().__init__("LogUpdate")
|
super().__init__("LogUpdate")
|
||||||
|
self.logging_queue = logging_queue
|
||||||
|
|
||||||
def loop(self):
|
def loop(self):
|
||||||
if sio:
|
if sio:
|
||||||
log_entries = aprsd_rpc_client.RPCClient().get_log_entries()
|
try:
|
||||||
|
log_entry = self.logging_queue.get(block=True, timeout=1)
|
||||||
if log_entries:
|
if log_entry:
|
||||||
LOG.info(f"Sending log entries! {len(log_entries)}")
|
|
||||||
for entry in log_entries:
|
|
||||||
sio.emit(
|
sio.emit(
|
||||||
"log_entry", entry,
|
"log_entry",
|
||||||
|
log_entry,
|
||||||
namespace="/logs",
|
namespace="/logs",
|
||||||
)
|
)
|
||||||
time.sleep(5)
|
except queue.Empty:
|
||||||
|
pass
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@ -297,17 +232,17 @@ class LoggingNamespace(socketio.Namespace):
|
|||||||
log_thread = None
|
log_thread = None
|
||||||
|
|
||||||
def on_connect(self, sid, environ):
|
def on_connect(self, sid, environ):
|
||||||
global sio
|
global sio, logging_queue
|
||||||
LOG.debug(f"LOG on_connect {sid}")
|
LOG.info(f"LOG on_connect {sid}")
|
||||||
sio.emit(
|
sio.emit(
|
||||||
"connected", {"data": "/logs Connected"},
|
"connected", {"data": "/logs Connected"},
|
||||||
namespace="/logs",
|
namespace="/logs",
|
||||||
)
|
)
|
||||||
self.log_thread = LogUpdateThread()
|
self.log_thread = LogUpdateThread(logging_queue=logging_queue)
|
||||||
self.log_thread.start()
|
self.log_thread.start()
|
||||||
|
|
||||||
def on_disconnect(self, sid):
|
def on_disconnect(self, sid):
|
||||||
LOG.debug(f"LOG Disconnected {sid}")
|
LOG.info(f"LOG Disconnected {sid}")
|
||||||
if self.log_thread:
|
if self.log_thread:
|
||||||
self.log_thread.stop()
|
self.log_thread.stop()
|
||||||
|
|
||||||
@ -332,8 +267,8 @@ if __name__ == "__main__":
|
|||||||
async_mode = "threading"
|
async_mode = "threading"
|
||||||
sio = socketio.Server(logger=True, async_mode=async_mode)
|
sio = socketio.Server(logger=True, async_mode=async_mode)
|
||||||
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
|
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
|
||||||
log_level = init_app(log_level="DEBUG")
|
log_level = init_app()
|
||||||
log.setup_logging(app, log_level)
|
log.setup_logging(log_level)
|
||||||
sio.register_namespace(LoggingNamespace("/logs"))
|
sio.register_namespace(LoggingNamespace("/logs"))
|
||||||
CONF.log_opt_values(LOG, logging.DEBUG)
|
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||||
app.run(
|
app.run(
|
||||||
@ -352,12 +287,12 @@ if __name__ == "uwsgi_file_aprsd_wsgi":
|
|||||||
sio = socketio.Server(logger=True, async_mode=async_mode)
|
sio = socketio.Server(logger=True, async_mode=async_mode)
|
||||||
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
|
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
|
||||||
log_level = init_app(
|
log_level = init_app(
|
||||||
log_level="DEBUG",
|
# log_level="DEBUG",
|
||||||
config_file="/config/aprsd.conf",
|
config_file="/config/aprsd.conf",
|
||||||
# Commented out for local development.
|
# Commented out for local development.
|
||||||
# config_file=cli_helper.DEFAULT_CONFIG_FILE
|
# config_file=cli_helper.DEFAULT_CONFIG_FILE
|
||||||
)
|
)
|
||||||
log.setup_logging(app, log_level)
|
log.setup_logging(log_level)
|
||||||
sio.register_namespace(LoggingNamespace("/logs"))
|
sio.register_namespace(LoggingNamespace("/logs"))
|
||||||
CONF.log_opt_values(LOG, logging.DEBUG)
|
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||||
|
|
||||||
@ -371,10 +306,10 @@ if __name__ == "aprsd.wsgi":
|
|||||||
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
|
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
|
||||||
|
|
||||||
log_level = init_app(
|
log_level = init_app(
|
||||||
log_level="DEBUG",
|
# log_level="DEBUG",
|
||||||
config_file="/config/aprsd.conf",
|
config_file="/config/aprsd.conf",
|
||||||
# config_file=cli_helper.DEFAULT_CONFIG_FILE,
|
# config_file=cli_helper.DEFAULT_CONFIG_FILE,
|
||||||
)
|
)
|
||||||
log.setup_logging(app, log_level)
|
log.setup_logging(log_level)
|
||||||
sio.register_namespace(LoggingNamespace("/logs"))
|
sio.register_namespace(LoggingNamespace("/logs"))
|
||||||
CONF.log_opt_values(LOG, logging.DEBUG)
|
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||||
|
@ -1,84 +0,0 @@
|
|||||||
#
|
|
||||||
# This file is autogenerated by pip-compile with Python 3.10
|
|
||||||
# by the following command:
|
|
||||||
#
|
|
||||||
# pip-compile --annotation-style=line dev-requirements.in
|
|
||||||
#
|
|
||||||
add-trailing-comma==3.1.0 # via gray
|
|
||||||
alabaster==0.7.16 # via sphinx
|
|
||||||
autoflake==1.5.3 # via gray
|
|
||||||
babel==2.14.0 # via sphinx
|
|
||||||
black==24.2.0 # via gray
|
|
||||||
build==1.1.1 # via pip-tools
|
|
||||||
cachetools==5.3.3 # via tox
|
|
||||||
certifi==2024.2.2 # via requests
|
|
||||||
cfgv==3.4.0 # via pre-commit
|
|
||||||
chardet==5.2.0 # via tox
|
|
||||||
charset-normalizer==3.3.2 # via requests
|
|
||||||
click==8.1.7 # via black, fixit, moreorless, pip-tools
|
|
||||||
colorama==0.4.6 # via tox
|
|
||||||
commonmark==0.9.1 # via rich
|
|
||||||
configargparse==1.7 # via gray
|
|
||||||
coverage[toml]==7.4.3 # via pytest-cov
|
|
||||||
distlib==0.3.8 # via virtualenv
|
|
||||||
docutils==0.20.1 # via sphinx
|
|
||||||
exceptiongroup==1.2.0 # via pytest
|
|
||||||
filelock==3.13.1 # via tox, virtualenv
|
|
||||||
fixit==2.1.0 # via gray
|
|
||||||
flake8==7.0.0 # via -r dev-requirements.in, pep8-naming
|
|
||||||
gray==0.14.0 # via -r dev-requirements.in
|
|
||||||
identify==2.5.35 # via pre-commit
|
|
||||||
idna==3.6 # via requests
|
|
||||||
imagesize==1.4.1 # via sphinx
|
|
||||||
iniconfig==2.0.0 # via pytest
|
|
||||||
isort==5.13.2 # via -r dev-requirements.in, gray
|
|
||||||
jinja2==3.1.3 # via sphinx
|
|
||||||
libcst==1.2.0 # via fixit
|
|
||||||
markupsafe==2.1.5 # via jinja2
|
|
||||||
mccabe==0.7.0 # via flake8
|
|
||||||
moreorless==0.4.0 # via fixit
|
|
||||||
mypy==1.8.0 # via -r dev-requirements.in
|
|
||||||
mypy-extensions==1.0.0 # via black, mypy, typing-inspect
|
|
||||||
nodeenv==1.8.0 # via pre-commit
|
|
||||||
packaging==23.2 # via black, build, fixit, pyproject-api, pytest, sphinx, tox
|
|
||||||
pathspec==0.12.1 # via black, trailrunner
|
|
||||||
pep8-naming==0.13.3 # via -r dev-requirements.in
|
|
||||||
pip-tools==7.4.1 # via -r dev-requirements.in
|
|
||||||
platformdirs==4.2.0 # via black, tox, virtualenv
|
|
||||||
pluggy==1.4.0 # via pytest, tox
|
|
||||||
pre-commit==3.6.2 # via -r dev-requirements.in
|
|
||||||
pycodestyle==2.11.1 # via flake8
|
|
||||||
pyflakes==3.2.0 # via autoflake, flake8
|
|
||||||
pygments==2.17.2 # via rich, sphinx
|
|
||||||
pyproject-api==1.6.1 # via tox
|
|
||||||
pyproject-hooks==1.0.0 # via build, pip-tools
|
|
||||||
pytest==8.0.2 # via -r dev-requirements.in, pytest-cov
|
|
||||||
pytest-cov==4.1.0 # via -r dev-requirements.in
|
|
||||||
pyupgrade==3.15.1 # via gray
|
|
||||||
pyyaml==6.0.1 # via libcst, pre-commit
|
|
||||||
requests==2.31.0 # via sphinx
|
|
||||||
rich==12.6.0 # via gray
|
|
||||||
snowballstemmer==2.2.0 # via sphinx
|
|
||||||
sphinx==7.2.6 # via -r dev-requirements.in
|
|
||||||
sphinxcontrib-applehelp==1.0.8 # via sphinx
|
|
||||||
sphinxcontrib-devhelp==1.0.6 # via sphinx
|
|
||||||
sphinxcontrib-htmlhelp==2.0.5 # via sphinx
|
|
||||||
sphinxcontrib-jsmath==1.0.1 # via sphinx
|
|
||||||
sphinxcontrib-qthelp==1.0.7 # via sphinx
|
|
||||||
sphinxcontrib-serializinghtml==1.1.10 # via sphinx
|
|
||||||
tokenize-rt==5.2.0 # via add-trailing-comma, pyupgrade
|
|
||||||
toml==0.10.2 # via autoflake
|
|
||||||
tomli==2.0.1 # via black, build, coverage, fixit, mypy, pip-tools, pyproject-api, pyproject-hooks, pytest, tox
|
|
||||||
tox==4.14.0 # via -r dev-requirements.in
|
|
||||||
trailrunner==1.4.0 # via fixit
|
|
||||||
typing-extensions==4.10.0 # via black, libcst, mypy, typing-inspect
|
|
||||||
typing-inspect==0.9.0 # via libcst
|
|
||||||
unify==0.5 # via gray
|
|
||||||
untokenize==0.1.1 # via unify
|
|
||||||
urllib3==2.2.1 # via requests
|
|
||||||
virtualenv==20.25.1 # via pre-commit, tox
|
|
||||||
wheel==0.42.0 # via pip-tools
|
|
||||||
|
|
||||||
# The following packages are considered to be unsafe in a requirements file:
|
|
||||||
# pip
|
|
||||||
# setuptools
|
|
@ -1,10 +1,18 @@
|
|||||||
FROM python:3.11-slim as build
|
FROM python:3.11-slim as build
|
||||||
|
|
||||||
ARG VERSION=3.1.0
|
ARG VERSION=3.4.0
|
||||||
|
# pass this in as 'dev' if you want to install from github repo vs pypi
|
||||||
|
ARG INSTALL_TYPE=pypi
|
||||||
|
|
||||||
|
ARG BRANCH=master
|
||||||
|
ARG BUILDX_QEMU_ENV
|
||||||
|
|
||||||
|
ENV APRSD_BRANCH=${BRANCH:-master}
|
||||||
ENV TZ=${TZ:-US/Eastern}
|
ENV TZ=${TZ:-US/Eastern}
|
||||||
ENV LC_ALL=C.UTF-8
|
ENV LC_ALL=C.UTF-8
|
||||||
ENV LANG=C.UTF-8
|
ENV LANG=C.UTF-8
|
||||||
ENV APRSD_PIP_VERSION=${VERSION}
|
ENV APRSD_PIP_VERSION=${VERSION}
|
||||||
|
ENV PATH="${PATH}:/app/.local/bin"
|
||||||
|
|
||||||
ENV PIP_DEFAULT_TIMEOUT=100 \
|
ENV PIP_DEFAULT_TIMEOUT=100 \
|
||||||
# Allow statements and log messages to immediately appear
|
# Allow statements and log messages to immediately appear
|
||||||
@ -19,6 +27,7 @@ RUN set -ex \
|
|||||||
# Create a non-root user
|
# Create a non-root user
|
||||||
&& addgroup --system --gid 1001 appgroup \
|
&& addgroup --system --gid 1001 appgroup \
|
||||||
&& useradd --uid 1001 --gid 1001 -s /usr/bin/bash -m -d /app appuser \
|
&& useradd --uid 1001 --gid 1001 -s /usr/bin/bash -m -d /app appuser \
|
||||||
|
&& usermod -aG sudo appuser \
|
||||||
# Upgrade the package index and install security upgrades
|
# Upgrade the package index and install security upgrades
|
||||||
&& apt-get update \
|
&& apt-get update \
|
||||||
&& apt-get upgrade -y \
|
&& apt-get upgrade -y \
|
||||||
@ -34,26 +43,33 @@ RUN set -ex \
|
|||||||
FROM build as final
|
FROM build as final
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
RUN pip3 install aprsd==$APRSD_PIP_VERSION
|
RUN pip3 install -U pip
|
||||||
RUN pip install gevent uwsgi
|
|
||||||
RUN which aprsd
|
|
||||||
RUN mkdir /config
|
RUN mkdir /config
|
||||||
RUN chown -R appuser:appgroup /app
|
RUN chown -R appuser:appgroup /app
|
||||||
RUN chown -R appuser:appgroup /config
|
RUN chown -R appuser:appgroup /config
|
||||||
USER appuser
|
USER appuser
|
||||||
RUN echo "PATH=\$PATH:/usr/games" >> /app/.bashrc
|
RUN if [ "$INSTALL_TYPE" = "pypi" ]; then \
|
||||||
|
pip3 install aprsd==$APRSD_PIP_VERSION; \
|
||||||
|
elif [ "$INSTALL_TYPE" = "github" ]; then \
|
||||||
|
git clone -b $APRSD_BRANCH https://github.com/craigerl/aprsd; \
|
||||||
|
cd /app/aprsd && pip install .; \
|
||||||
|
ls -al /app/.local/lib/python3.11/site-packages/aprsd*; \
|
||||||
|
fi
|
||||||
|
RUN pip install gevent uwsgi
|
||||||
|
RUN echo "PATH=\$PATH:/usr/games:/app/.local/bin" >> /app/.bashrc
|
||||||
RUN which aprsd
|
RUN which aprsd
|
||||||
RUN aprsd sample-config > /config/aprsd.conf
|
RUN aprsd sample-config > /config/aprsd.conf
|
||||||
|
RUN aprsd --version
|
||||||
|
|
||||||
ADD bin/run.sh /app
|
ADD bin/setup.sh /app
|
||||||
ADD bin/listen.sh /app
|
|
||||||
ADD bin/admin.sh /app
|
ADD bin/admin.sh /app
|
||||||
|
|
||||||
# For the web admin interface
|
# For the web admin interface
|
||||||
EXPOSE 8001
|
EXPOSE 8001
|
||||||
|
|
||||||
ENTRYPOINT ["/app/run.sh"]
|
|
||||||
VOLUME ["/config"]
|
VOLUME ["/config"]
|
||||||
|
ENTRYPOINT ["/app/setup.sh"]
|
||||||
|
CMD ["server"]
|
||||||
|
|
||||||
# Set the user to run the application
|
# Set the user to run the application
|
||||||
USER appuser
|
USER appuser
|
||||||
|
@ -1,58 +0,0 @@
|
|||||||
FROM python:3.11-slim as build
|
|
||||||
|
|
||||||
ARG BRANCH=master
|
|
||||||
ARG BUILDX_QEMU_ENV
|
|
||||||
ENV APRSD_BRANCH=${BRANCH:-master}
|
|
||||||
|
|
||||||
ENV PIP_DEFAULT_TIMEOUT=100 \
|
|
||||||
# Allow statements and log messages to immediately appear
|
|
||||||
PYTHONUNBUFFERED=1 \
|
|
||||||
# disable a pip version check to reduce run-time & log-spam
|
|
||||||
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
|
||||||
# cache is useless in docker image, so disable to reduce image size
|
|
||||||
PIP_NO_CACHE_DIR=1
|
|
||||||
|
|
||||||
|
|
||||||
RUN set -ex \
|
|
||||||
# Create a non-root user
|
|
||||||
&& addgroup --system --gid 1001 appgroup \
|
|
||||||
&& useradd --uid 1001 --gid 1001 -s /usr/bin/bash -m -d /app appuser \
|
|
||||||
# Upgrade the package index and install security upgrades
|
|
||||||
&& apt-get update \
|
|
||||||
&& apt-get upgrade -y \
|
|
||||||
&& apt-get install -y git build-essential curl libffi-dev fortune \
|
|
||||||
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo \
|
|
||||||
# Install dependencies
|
|
||||||
# Clean up
|
|
||||||
&& apt-get autoremove -y \
|
|
||||||
&& apt-get clean -y
|
|
||||||
|
|
||||||
|
|
||||||
### Final stage
|
|
||||||
FROM build as final
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
RUN git clone -b $APRSD_BRANCH https://github.com/craigerl/aprsd
|
|
||||||
RUN cd aprsd && pip install --no-cache-dir .
|
|
||||||
RUN pip install gevent uwsgi
|
|
||||||
RUN which aprsd
|
|
||||||
RUN mkdir /config
|
|
||||||
RUN chown -R appuser:appgroup /app
|
|
||||||
RUN chown -R appuser:appgroup /config
|
|
||||||
USER appuser
|
|
||||||
RUN echo "PATH=\$PATH:/usr/games" >> /app/.bashrc
|
|
||||||
RUN which aprsd
|
|
||||||
RUN aprsd sample-config > /config/aprsd.conf
|
|
||||||
|
|
||||||
ADD bin/run.sh /app
|
|
||||||
ADD bin/listen.sh /app
|
|
||||||
ADD bin/admin.sh /app
|
|
||||||
|
|
||||||
EXPOSE 8000
|
|
||||||
|
|
||||||
# CMD ["gunicorn", "aprsd.wsgi:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
||||||
ENTRYPOINT ["/app/run.sh"]
|
|
||||||
VOLUME ["/config"]
|
|
||||||
|
|
||||||
# Set the user to run the application
|
|
||||||
USER appuser
|
|
50
docker/bin/setup.sh
Executable file
50
docker/bin/setup.sh
Executable file
@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -x
|
||||||
|
|
||||||
|
# The default command
|
||||||
|
# Override the command in docker-compose.yml to change
|
||||||
|
# what command you want to run in the container
|
||||||
|
COMMAND="server"
|
||||||
|
|
||||||
|
if [ ! -z "${@+x}" ]; then
|
||||||
|
COMMAND=$@
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "${APRSD_PLUGINS}" ]; then
|
||||||
|
OLDIFS=$IFS
|
||||||
|
IFS=','
|
||||||
|
echo "Installing pypi plugins '$APRSD_PLUGINS'";
|
||||||
|
for plugin in ${APRSD_PLUGINS}; do
|
||||||
|
IFS=$OLDIFS
|
||||||
|
# call your procedure/other scripts here below
|
||||||
|
echo "Installing '$plugin'"
|
||||||
|
pip3 install --user $plugin
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -z "${APRSD_EXTENSIONS}" ]; then
|
||||||
|
OLDIFS=$IFS
|
||||||
|
IFS=','
|
||||||
|
echo "Installing APRSD extensions from pypi '$APRSD_EXTENSIONS'";
|
||||||
|
for extension in ${APRSD_EXTENSIONS}; do
|
||||||
|
IFS=$OLDIFS
|
||||||
|
# call your procedure/other scripts here below
|
||||||
|
echo "Installing '$extension'"
|
||||||
|
pip3 install --user $extension
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${LOG_LEVEL}" ] || [[ ! "${LOG_LEVEL}" =~ ^(CRITICAL|ERROR|WARNING|INFO)$ ]]; then
|
||||||
|
LOG_LEVEL="DEBUG"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Log level is set to ${LOG_LEVEL}";
|
||||||
|
|
||||||
|
# check to see if there is a config file
|
||||||
|
APRSD_CONFIG="/config/aprsd.conf"
|
||||||
|
if [ ! -e "$APRSD_CONFIG" ]; then
|
||||||
|
echo "'$APRSD_CONFIG' File does not exist. Creating."
|
||||||
|
aprsd sample-config > $APRSD_CONFIG
|
||||||
|
fi
|
||||||
|
|
||||||
|
aprsd ${COMMAND} --config ${APRSD_CONFIG} --loglevel ${LOG_LEVEL}
|
@ -26,7 +26,7 @@ DEV=0
|
|||||||
REBUILD_BUILDX=0
|
REBUILD_BUILDX=0
|
||||||
TAG="latest"
|
TAG="latest"
|
||||||
BRANCH=${BRANCH:-master}
|
BRANCH=${BRANCH:-master}
|
||||||
VERSION="3.0.0"
|
VERSION="3.3.4"
|
||||||
|
|
||||||
while getopts “hdart:b:v:” OPTION
|
while getopts “hdart:b:v:” OPTION
|
||||||
do
|
do
|
||||||
@ -90,7 +90,8 @@ then
|
|||||||
# Use this script to locally build the docker image
|
# Use this script to locally build the docker image
|
||||||
docker buildx build --push --platform $PLATFORMS \
|
docker buildx build --push --platform $PLATFORMS \
|
||||||
-t hemna6969/aprsd:$TAG \
|
-t hemna6969/aprsd:$TAG \
|
||||||
-f Dockerfile-dev --build-arg branch=$BRANCH \
|
--build-arg INSTALL_TYPE=github \
|
||||||
|
--build-arg branch=$BRANCH \
|
||||||
--build-arg BUILDX_QEMU_ENV=true \
|
--build-arg BUILDX_QEMU_ENV=true \
|
||||||
--no-cache .
|
--no-cache .
|
||||||
else
|
else
|
||||||
@ -101,6 +102,5 @@ else
|
|||||||
--build-arg BUILDX_QEMU_ENV=true \
|
--build-arg BUILDX_QEMU_ENV=true \
|
||||||
-t hemna6969/aprsd:$VERSION \
|
-t hemna6969/aprsd:$VERSION \
|
||||||
-t hemna6969/aprsd:$TAG \
|
-t hemna6969/aprsd:$TAG \
|
||||||
-t hemna6969/aprsd:latest \
|
-t hemna6969/aprsd:latest .
|
||||||
-f Dockerfile .
|
|
||||||
fi
|
fi
|
||||||
|
37
docs/apidoc/aprsd.client.drivers.rst
Normal file
37
docs/apidoc/aprsd.client.drivers.rst
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
aprsd.client.drivers package
|
||||||
|
============================
|
||||||
|
|
||||||
|
Submodules
|
||||||
|
----------
|
||||||
|
|
||||||
|
aprsd.client.drivers.aprsis module
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.client.drivers.aprsis
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.client.drivers.fake module
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.client.drivers.fake
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.client.drivers.kiss module
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.client.drivers.kiss
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Module contents
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.client.drivers
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
69
docs/apidoc/aprsd.client.rst
Normal file
69
docs/apidoc/aprsd.client.rst
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
aprsd.client package
|
||||||
|
====================
|
||||||
|
|
||||||
|
Subpackages
|
||||||
|
-----------
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 4
|
||||||
|
|
||||||
|
aprsd.client.drivers
|
||||||
|
|
||||||
|
Submodules
|
||||||
|
----------
|
||||||
|
|
||||||
|
aprsd.client.aprsis module
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.client.aprsis
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.client.base module
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.client.base
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.client.factory module
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.client.factory
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.client.fake module
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.client.fake
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.client.kiss module
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.client.kiss
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.client.stats module
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.client.stats
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Module contents
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.client
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
@ -1,29 +0,0 @@
|
|||||||
aprsd.clients package
|
|
||||||
=====================
|
|
||||||
|
|
||||||
Submodules
|
|
||||||
----------
|
|
||||||
|
|
||||||
aprsd.clients.aprsis module
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
.. automodule:: aprsd.clients.aprsis
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
aprsd.clients.kiss module
|
|
||||||
-------------------------
|
|
||||||
|
|
||||||
.. automodule:: aprsd.clients.kiss
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
Module contents
|
|
||||||
---------------
|
|
||||||
|
|
||||||
.. automodule:: aprsd.clients
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
21
docs/apidoc/aprsd.log.rst
Normal file
21
docs/apidoc/aprsd.log.rst
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
aprsd.log package
|
||||||
|
=================
|
||||||
|
|
||||||
|
Submodules
|
||||||
|
----------
|
||||||
|
|
||||||
|
aprsd.log.log module
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.log.log
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Module contents
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.log
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
@ -4,6 +4,14 @@ aprsd.packets package
|
|||||||
Submodules
|
Submodules
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
aprsd.packets.collector module
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.packets.collector
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
aprsd.packets.core module
|
aprsd.packets.core module
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
@ -12,6 +20,14 @@ aprsd.packets.core module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.packets.log module
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.packets.log
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
aprsd.packets.packet\_list module
|
aprsd.packets.packet\_list module
|
||||||
---------------------------------
|
---------------------------------
|
||||||
|
|
||||||
|
@ -44,14 +44,6 @@ aprsd.plugins.ping module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
aprsd.plugins.query module
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
.. automodule:: aprsd.plugins.query
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
aprsd.plugins.time module
|
aprsd.plugins.time module
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
aprsd.rpc package
|
|
||||||
=================
|
|
||||||
|
|
||||||
Submodules
|
|
||||||
----------
|
|
||||||
|
|
||||||
aprsd.rpc.client module
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
.. automodule:: aprsd.rpc.client
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
aprsd.rpc.server module
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
.. automodule:: aprsd.rpc.server
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
Module contents
|
|
||||||
---------------
|
|
||||||
|
|
||||||
.. automodule:: aprsd.rpc
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
@ -7,13 +7,13 @@ Subpackages
|
|||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 4
|
:maxdepth: 4
|
||||||
|
|
||||||
aprsd.clients
|
aprsd.client
|
||||||
aprsd.cmds
|
aprsd.cmds
|
||||||
aprsd.conf
|
aprsd.conf
|
||||||
aprsd.log
|
aprsd.log
|
||||||
aprsd.packets
|
aprsd.packets
|
||||||
aprsd.plugins
|
aprsd.plugins
|
||||||
aprsd.rpc
|
aprsd.stats
|
||||||
aprsd.threads
|
aprsd.threads
|
||||||
aprsd.utils
|
aprsd.utils
|
||||||
aprsd.web
|
aprsd.web
|
||||||
@ -29,14 +29,6 @@ aprsd.cli\_helper module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
aprsd.client module
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
.. automodule:: aprsd.client
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
aprsd.exception module
|
aprsd.exception module
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
@ -77,14 +69,6 @@ aprsd.plugin\_utils module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
aprsd.stats module
|
|
||||||
------------------
|
|
||||||
|
|
||||||
.. automodule:: aprsd.stats
|
|
||||||
:members:
|
|
||||||
:undoc-members:
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
aprsd.wsgi module
|
aprsd.wsgi module
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
|
29
docs/apidoc/aprsd.stats.rst
Normal file
29
docs/apidoc/aprsd.stats.rst
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
aprsd.stats package
|
||||||
|
===================
|
||||||
|
|
||||||
|
Submodules
|
||||||
|
----------
|
||||||
|
|
||||||
|
aprsd.stats.app module
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.stats.app
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.stats.collector module
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.stats.collector
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
|
Module contents
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.stats
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
@ -28,6 +28,14 @@ aprsd.threads.log\_monitor module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.threads.registry module
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.threads.registry
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
aprsd.threads.rx module
|
aprsd.threads.rx module
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
@ -36,6 +44,14 @@ aprsd.threads.rx module
|
|||||||
:undoc-members:
|
:undoc-members:
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
|
aprsd.threads.stats module
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
.. automodule:: aprsd.threads.stats
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
||||||
|
|
||||||
aprsd.threads.tx module
|
aprsd.threads.tx module
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
|
1804
docs/changelog.rst
1804
docs/changelog.rst
File diff suppressed because it is too large
Load Diff
153
docs/readme.rst
153
docs/readme.rst
@ -10,32 +10,38 @@ ____________________
|
|||||||
|
|
||||||
`APRSD <http://github.com/craigerl/aprsd>`_ is a Ham radio `APRS <http://aprs.org>`_ message command gateway built on python.
|
`APRSD <http://github.com/craigerl/aprsd>`_ is a Ham radio `APRS <http://aprs.org>`_ message command gateway built on python.
|
||||||
|
|
||||||
APRSD listens on amateur radio aprs-is network for messages and respond to them.
|
|
||||||
It has a plugin architecture for extensibility. Users of APRSD can write their own
|
|
||||||
plugins that can respond to APRS-IS messages.
|
|
||||||
|
|
||||||
You must have an amateur radio callsign to use this software. APRSD gets
|
What is APRSD
|
||||||
messages for the configured HAM callsign, and sends those messages to a
|
=============
|
||||||
list of plugins for processing. There are a set of core plugins that
|
APRSD is a python application for interacting with the APRS network and providing
|
||||||
provide responding to messages to check email, get location, ping,
|
APRS services for HAM radio operators.
|
||||||
time of day, get weather, and fortune telling as well as version information
|
|
||||||
of aprsd itself.
|
APRSD currently has 4 main commands to use.
|
||||||
|
* server - Connect to APRS and listen/respond to APRS messages
|
||||||
|
* webchat - web based chat program over APRS
|
||||||
|
* send-message - Send a message to a callsign via APRS_IS.
|
||||||
|
* listen - Listen to packets on the APRS-IS Network based on FILTER.
|
||||||
|
|
||||||
|
Each of those commands can connect to the APRS-IS network if internet connectivity
|
||||||
|
is available. If internet is not available, then APRS can be configured to talk
|
||||||
|
to a TCP KISS TNC for radio connectivity.
|
||||||
|
|
||||||
Please `read the docs`_ to learn more!
|
Please `read the docs`_ to learn more!
|
||||||
|
|
||||||
|
|
||||||
.. contents:: :local:
|
|
||||||
|
|
||||||
|
|
||||||
APRSD Overview Diagram
|
APRSD Overview Diagram
|
||||||
======================
|
======================
|
||||||
|
|
||||||
.. image:: https://raw.githubusercontent.com/craigerl/aprsd/master/docs/_static/aprsd_overview.svg?sanitize=true
|
.. image:: https://raw.githubusercontent.com/craigerl/aprsd/master/docs/_static/aprsd_overview.svg?sanitize=true
|
||||||
|
|
||||||
|
|
||||||
Typical use case
|
Typical use case
|
||||||
================
|
================
|
||||||
|
|
||||||
|
APRSD's typical use case is that of providing an APRS wide service to all HAM
|
||||||
|
radio operators. For example the callsign 'REPEAT' on the APRS network is actually
|
||||||
|
an instance of APRSD that can provide a list of HAM repeaters in the area of the
|
||||||
|
callsign that sent the message.
|
||||||
|
|
||||||
|
|
||||||
Ham radio operator using an APRS enabled HAM radio sends a message to check
|
Ham radio operator using an APRS enabled HAM radio sends a message to check
|
||||||
the weather. An APRS message is sent, and then picked up by APRSD. The
|
the weather. An APRS message is sent, and then picked up by APRSD. The
|
||||||
APRS packet is decoded, and the message is sent through the list of plugins
|
APRS packet is decoded, and the message is sent through the list of plugins
|
||||||
@ -46,55 +52,6 @@ callsigns to look out for. The watch list can notify you when a HAM callsign
|
|||||||
in the list is seen and now available to message on the APRS network.
|
in the list is seen and now available to message on the APRS network.
|
||||||
|
|
||||||
|
|
||||||
Current list of built-in plugins
|
|
||||||
======================================
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
└─> aprsd list-plugins
|
|
||||||
🐍 APRSD Built-in Plugins 🐍
|
|
||||||
┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
||||||
┃ Plugin Name ┃ Info ┃ Type ┃ Plugin Path ┃
|
|
||||||
┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
|
||||||
│ AVWXWeatherPlugin │ AVWX weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.AVWXWeatherPlugin │
|
|
||||||
│ EmailPlugin │ Send and Receive email │ RegexCommand │ aprsd.plugins.email.EmailPlugin │
|
|
||||||
│ FortunePlugin │ Give me a fortune │ RegexCommand │ aprsd.plugins.fortune.FortunePlugin │
|
|
||||||
│ LocationPlugin │ Where in the world is a CALLSIGN's last GPS beacon? │ RegexCommand │ aprsd.plugins.location.LocationPlugin │
|
|
||||||
│ NotifySeenPlugin │ Notify me when a CALLSIGN is recently seen on APRS-IS │ WatchList │ aprsd.plugins.notify.NotifySeenPlugin │
|
|
||||||
│ OWMWeatherPlugin │ OpenWeatherMap weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.OWMWeatherPlugin │
|
|
||||||
│ PingPlugin │ reply with a Pong! │ RegexCommand │ aprsd.plugins.ping.PingPlugin │
|
|
||||||
│ QueryPlugin │ APRSD Owner command to query messages in the MsgTrack │ RegexCommand │ aprsd.plugins.query.QueryPlugin │
|
|
||||||
│ TimeOWMPlugin │ Current time of GPS beacon's timezone. Uses OpenWeatherMap │ RegexCommand │ aprsd.plugins.time.TimeOWMPlugin │
|
|
||||||
│ TimePlugin │ What is the current local time. │ RegexCommand │ aprsd.plugins.time.TimePlugin │
|
|
||||||
│ USMetarPlugin │ USA only METAR of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USMetarPlugin │
|
|
||||||
│ USWeatherPlugin │ Provide USA only weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USWeatherPlugin │
|
|
||||||
│ VersionPlugin │ What is the APRSD Version │ RegexCommand │ aprsd.plugins.version.VersionPlugin │
|
|
||||||
└───────────────────┴────────────────────────────────────────────────────────────┴──────────────┴─────────────────────────────────────────┘
|
|
||||||
|
|
||||||
|
|
||||||
Pypi.org APRSD Installable Plugin Packages
|
|
||||||
|
|
||||||
Install any of the following plugins with 'pip install <Plugin Package Name>'
|
|
||||||
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
|
|
||||||
┃ Plugin Package Name ┃ Description ┃ Version ┃ Released ┃ Installed? ┃
|
|
||||||
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
|
|
||||||
│ 📂 aprsd-stock-plugin │ Ham Radio APRSD Plugin for fetching stock quotes │ 0.1.3 │ Dec 2, 2022 │ No │
|
|
||||||
│ 📂 aprsd-sentry-plugin │ Ham radio APRSD plugin that does.... │ 0.1.2 │ Dec 2, 2022 │ No │
|
|
||||||
│ 📂 aprsd-timeopencage-plugin │ APRSD plugin for fetching time based on GPS location │ 0.1.0 │ Dec 2, 2022 │ No │
|
|
||||||
│ 📂 aprsd-weewx-plugin │ HAM Radio APRSD that reports weather from a weewx weather station. │ 0.1.4 │ Dec 7, 2021 │ Yes │
|
|
||||||
│ 📂 aprsd-repeat-plugins │ APRSD Plugins for the REPEAT service │ 1.0.12 │ Dec 2, 2022 │ No │
|
|
||||||
│ 📂 aprsd-telegram-plugin │ Ham Radio APRS APRSD plugin for Telegram IM service │ 0.1.3 │ Dec 2, 2022 │ No │
|
|
||||||
│ 📂 aprsd-twitter-plugin │ Python APRSD plugin to send tweets │ 0.3.0 │ Dec 7, 2021 │ No │
|
|
||||||
│ 📂 aprsd-slack-plugin │ Amateur radio APRS daemon which listens for messages and responds │ 1.0.5 │ Dec 18, 2022 │ No │
|
|
||||||
└──────────────────────────────┴────────────────────────────────────────────────────────────────────┴─────────┴──────────────┴────────────┘
|
|
||||||
|
|
||||||
|
|
||||||
🐍 APRSD Installed 3rd party Plugins 🐍
|
|
||||||
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
|
||||||
┃ Package Name ┃ Plugin Name ┃ Version ┃ Type ┃ Plugin Path ┃
|
|
||||||
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
|
||||||
│ aprsd-weewx-plugin │ WeewxMQTTPlugin │ 1.0 │ RegexCommand │ aprsd_weewx_plugin.weewx.WeewxMQTTPlugin │
|
|
||||||
└────────────────────┴─────────────────┴─────────┴──────────────┴──────────────────────────────────────────┘
|
|
||||||
|
|
||||||
Installation
|
Installation
|
||||||
=============
|
=============
|
||||||
@ -112,6 +69,7 @@ Help
|
|||||||
====
|
====
|
||||||
::
|
::
|
||||||
|
|
||||||
|
|
||||||
└─> aprsd -h
|
└─> aprsd -h
|
||||||
Usage: aprsd [OPTIONS] COMMAND [ARGS]...
|
Usage: aprsd [OPTIONS] COMMAND [ARGS]...
|
||||||
|
|
||||||
@ -121,9 +79,11 @@ Help
|
|||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
check-version Check this version against the latest in pypi.org.
|
check-version Check this version against the latest in pypi.org.
|
||||||
completion Click Completion subcommands
|
completion Show the shell completion code
|
||||||
dev Development type subcommands
|
dev Development type subcommands
|
||||||
|
fetch-stats Fetch stats from a APRSD admin web interface.
|
||||||
healthcheck Check the health of the running aprsd server.
|
healthcheck Check the health of the running aprsd server.
|
||||||
|
list-extensions List the built in plugins available to APRSD.
|
||||||
list-plugins List the built in plugins available to APRSD.
|
list-plugins List the built in plugins available to APRSD.
|
||||||
listen Listen to packets on the APRS-IS Network based on FILTER.
|
listen Listen to packets on the APRS-IS Network based on FILTER.
|
||||||
sample-config Generate a sample Config file from aprsd and all...
|
sample-config Generate a sample Config file from aprsd and all...
|
||||||
@ -133,7 +93,6 @@ Help
|
|||||||
webchat Web based HAM Radio chat program!
|
webchat Web based HAM Radio chat program!
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Commands
|
Commands
|
||||||
========
|
========
|
||||||
|
|
||||||
@ -187,6 +146,56 @@ look for incomming commands to the callsign configured in the config file
|
|||||||
12/07/2021 03:16:17 PM MainThread INFO aprs.logfile = /tmp/aprsd.log server.py:60
|
12/07/2021 03:16:17 PM MainThread INFO aprs.logfile = /tmp/aprsd.log server.py:60
|
||||||
|
|
||||||
|
|
||||||
|
Current list of built-in plugins
|
||||||
|
======================================
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
└─> aprsd list-plugins
|
||||||
|
🐍 APRSD Built-in Plugins 🐍
|
||||||
|
┏━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
|
┃ Plugin Name ┃ Info ┃ Type ┃ Plugin Path ┃
|
||||||
|
┡━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||||
|
│ AVWXWeatherPlugin │ AVWX weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.AVWXWeatherPlugin │
|
||||||
|
│ EmailPlugin │ Send and Receive email │ RegexCommand │ aprsd.plugins.email.EmailPlugin │
|
||||||
|
│ FortunePlugin │ Give me a fortune │ RegexCommand │ aprsd.plugins.fortune.FortunePlugin │
|
||||||
|
│ LocationPlugin │ Where in the world is a CALLSIGN's last GPS beacon? │ RegexCommand │ aprsd.plugins.location.LocationPlugin │
|
||||||
|
│ NotifySeenPlugin │ Notify me when a CALLSIGN is recently seen on APRS-IS │ WatchList │ aprsd.plugins.notify.NotifySeenPlugin │
|
||||||
|
│ OWMWeatherPlugin │ OpenWeatherMap weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.OWMWeatherPlugin │
|
||||||
|
│ PingPlugin │ reply with a Pong! │ RegexCommand │ aprsd.plugins.ping.PingPlugin │
|
||||||
|
│ QueryPlugin │ APRSD Owner command to query messages in the MsgTrack │ RegexCommand │ aprsd.plugins.query.QueryPlugin │
|
||||||
|
│ TimeOWMPlugin │ Current time of GPS beacon's timezone. Uses OpenWeatherMap │ RegexCommand │ aprsd.plugins.time.TimeOWMPlugin │
|
||||||
|
│ TimePlugin │ What is the current local time. │ RegexCommand │ aprsd.plugins.time.TimePlugin │
|
||||||
|
│ USMetarPlugin │ USA only METAR of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USMetarPlugin │
|
||||||
|
│ USWeatherPlugin │ Provide USA only weather of GPS Beacon location │ RegexCommand │ aprsd.plugins.weather.USWeatherPlugin │
|
||||||
|
│ VersionPlugin │ What is the APRSD Version │ RegexCommand │ aprsd.plugins.version.VersionPlugin │
|
||||||
|
└───────────────────┴────────────────────────────────────────────────────────────┴──────────────┴─────────────────────────────────────────┘
|
||||||
|
|
||||||
|
|
||||||
|
Pypi.org APRSD Installable Plugin Packages
|
||||||
|
|
||||||
|
Install any of the following plugins with 'pip install <Plugin Package Name>'
|
||||||
|
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┓
|
||||||
|
┃ Plugin Package Name ┃ Description ┃ Version ┃ Released ┃ Installed? ┃
|
||||||
|
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━┩
|
||||||
|
│ 📂 aprsd-stock-plugin │ Ham Radio APRSD Plugin for fetching stock quotes │ 0.1.3 │ Dec 2, 2022 │ No │
|
||||||
|
│ 📂 aprsd-sentry-plugin │ Ham radio APRSD plugin that does.... │ 0.1.2 │ Dec 2, 2022 │ No │
|
||||||
|
│ 📂 aprsd-timeopencage-plugin │ APRSD plugin for fetching time based on GPS location │ 0.1.0 │ Dec 2, 2022 │ No │
|
||||||
|
│ 📂 aprsd-weewx-plugin │ HAM Radio APRSD that reports weather from a weewx weather station. │ 0.1.4 │ Dec 7, 2021 │ Yes │
|
||||||
|
│ 📂 aprsd-repeat-plugins │ APRSD Plugins for the REPEAT service │ 1.0.12 │ Dec 2, 2022 │ No │
|
||||||
|
│ 📂 aprsd-telegram-plugin │ Ham Radio APRS APRSD plugin for Telegram IM service │ 0.1.3 │ Dec 2, 2022 │ No │
|
||||||
|
│ 📂 aprsd-twitter-plugin │ Python APRSD plugin to send tweets │ 0.3.0 │ Dec 7, 2021 │ No │
|
||||||
|
│ 📂 aprsd-slack-plugin │ Amateur radio APRS daemon which listens for messages and responds │ 1.0.5 │ Dec 18, 2022 │ No │
|
||||||
|
└──────────────────────────────┴────────────────────────────────────────────────────────────────────┴─────────┴──────────────┴────────────┘
|
||||||
|
|
||||||
|
|
||||||
|
🐍 APRSD Installed 3rd party Plugins 🐍
|
||||||
|
┏━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||||
|
┃ Package Name ┃ Plugin Name ┃ Version ┃ Type ┃ Plugin Path ┃
|
||||||
|
┡━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
|
||||||
|
│ aprsd-weewx-plugin │ WeewxMQTTPlugin │ 1.0 │ RegexCommand │ aprsd_weewx_plugin.weewx.WeewxMQTTPlugin │
|
||||||
|
└────────────────────┴─────────────────┴─────────┴──────────────┴──────────────────────────────────────────┘
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
send-message
|
send-message
|
||||||
@ -289,6 +298,20 @@ LOCATION
|
|||||||
AND... ping, fortune, time.....
|
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
|
Development
|
||||||
===========
|
===========
|
||||||
|
|
||||||
|
161
pyproject.toml
161
pyproject.toml
@ -1,5 +1,162 @@
|
|||||||
|
[project]
|
||||||
|
|
||||||
|
# This is the name of your project. The first time you publish this
|
||||||
|
# package, this name will be registered for you. It will determine how
|
||||||
|
# users can install this project, e.g.:
|
||||||
|
#
|
||||||
|
# $ pip install sampleproject
|
||||||
|
#
|
||||||
|
# And where it will live on PyPI: https://pypi.org/project/sampleproject/
|
||||||
|
#
|
||||||
|
# There are some restrictions on what makes a valid project name
|
||||||
|
# specification here:
|
||||||
|
# https://packaging.python.org/specifications/core-metadata/#name
|
||||||
|
name = "aprsd"
|
||||||
|
description = "APRSd is a APRS-IS server that can be used to connect to APRS-IS and send and receive APRS packets."
|
||||||
|
|
||||||
|
# Specify which Python versions you support. In contrast to the
|
||||||
|
# 'Programming Language' classifiers in this file, 'pip install' will check this
|
||||||
|
# and refuse to install the project if the version does not match. See
|
||||||
|
# https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
|
||||||
|
dynamic = ["version", "dependencies", "optional-dependencies"]
|
||||||
|
|
||||||
|
# This is an optional longer description of your project that represents
|
||||||
|
# the body of text which users will see when they visit PyPI.
|
||||||
|
#
|
||||||
|
# Often, this is the same as your README, so you can just read it in from
|
||||||
|
# that file directly.
|
||||||
|
#
|
||||||
|
# This field corresponds to the "Description" metadata field:
|
||||||
|
# https://packaging.python.org/specifications/core-metadata/#description-optional
|
||||||
|
readme = {file = "README.rst", content-type = "text/x-rst"}
|
||||||
|
|
||||||
|
|
||||||
|
# This is either text indicating the license for the distribution, or a file
|
||||||
|
# that contains the license.
|
||||||
|
# https://packaging.python.org/en/latest/specifications/core-metadata/#license
|
||||||
|
license = {file = "LICENSE"}
|
||||||
|
|
||||||
|
# This should be your name or the name of the organization who originally
|
||||||
|
# authored the project, and a valid email address corresponding to the name
|
||||||
|
# listed.
|
||||||
|
authors = [
|
||||||
|
{name = "Craig Lamparter", email = "craig@craiger.org"},
|
||||||
|
{name = "Walter A. Boring IV", email = "waboring@hemna.com"},
|
||||||
|
{name = "Emre Saglam", email = "emresaglam@gmail.com"},
|
||||||
|
{name = "Jason Martin", email= "jhmartin@toger.us"},
|
||||||
|
{name = "John", email="johng42@users.noreply.github.com"},
|
||||||
|
{name = "Martiros Shakhzadyan", email="vrzh@vrzh.net"},
|
||||||
|
{name = "Zoe Moore", email="zoenb@mailbox.org"},
|
||||||
|
{name = "ranguli", email="hello@joshmurphy.ca"},
|
||||||
|
]
|
||||||
|
|
||||||
|
# This should be your name or the names of the organization who currently
|
||||||
|
# maintains the project, and a valid email address corresponding to the name
|
||||||
|
# listed.
|
||||||
|
maintainers = [
|
||||||
|
{name = "Craig Lamparter", email = "craig@craiger.org"},
|
||||||
|
{name = "Walter A. Boring IV", email = "waboring@hemna.com"},
|
||||||
|
]
|
||||||
|
|
||||||
|
# This field adds keywords for your project which will appear on the
|
||||||
|
# project page. What does your project relate to?
|
||||||
|
#
|
||||||
|
# Note that this is a list of additional keywords, separated
|
||||||
|
# by commas, to be used to assist searching for the distribution in a
|
||||||
|
# larger catalog.
|
||||||
|
keywords = [
|
||||||
|
"aprs",
|
||||||
|
"aprs-is",
|
||||||
|
"aprsd",
|
||||||
|
"aprsd-server",
|
||||||
|
"aprsd-client",
|
||||||
|
"aprsd-socket",
|
||||||
|
"aprsd-socket-server",
|
||||||
|
"aprsd-socket-client",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Classifiers help users find your project by categorizing it.
|
||||||
|
#
|
||||||
|
# For a list of valid classifiers, see https://pypi.org/classifiers/
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 5 - Production/Stable",
|
||||||
|
"Environment :: Console",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Intended Audience :: End Users/Desktop",
|
||||||
|
"Intended Audience :: Information Technology",
|
||||||
|
"Topic :: Communications :: Ham Radio",
|
||||||
|
"Topic :: Internet",
|
||||||
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
]
|
||||||
|
|
||||||
|
# This field lists other packages that your project depends on to run.
|
||||||
|
# Any package you put here will be installed by pip when your project is
|
||||||
|
# installed, so they must be valid existing projects.
|
||||||
|
#
|
||||||
|
# For an analysis of this field vs pip's requirements files see:
|
||||||
|
# https://packaging.python.org/discussions/install-requires-vs-requirements/
|
||||||
|
[tool.setuptools.dynamic]
|
||||||
|
dependencies = {file = ["./requirements.txt"]}
|
||||||
|
optional-dependencies.dev = {file = ["./requirements-dev.txt"]}
|
||||||
|
|
||||||
|
# List additional groups of dependencies here (e.g. development
|
||||||
|
# dependencies). Users will be able to install these using the "extras"
|
||||||
|
# syntax, for example:
|
||||||
|
#
|
||||||
|
# $ pip install sampleproject[dev]
|
||||||
|
#
|
||||||
|
# Optional dependencies the project provides. These are commonly
|
||||||
|
# referred to as "extras". For a more extensive definition see:
|
||||||
|
# https://packaging.python.org/en/latest/specifications/dependency-specifiers/#extras
|
||||||
|
# [project.optional-dependencies]
|
||||||
|
|
||||||
|
# List URLs that are relevant to your project
|
||||||
|
#
|
||||||
|
# This field corresponds to the "Project-URL" and "Home-Page" metadata fields:
|
||||||
|
# https://packaging.python.org/specifications/core-metadata/#project-url-multiple-use
|
||||||
|
# https://packaging.python.org/specifications/core-metadata/#home-page-optional
|
||||||
|
#
|
||||||
|
# Examples listed include a pattern for specifying where the package tracks
|
||||||
|
# issues, where the source is hosted, where to say thanks to the package
|
||||||
|
# maintainers, and where to support the project financially. The key is
|
||||||
|
# what's used to render the link text on PyPI.
|
||||||
|
[project.urls]
|
||||||
|
"Homepage" = "https://github.com/craigerl/aprsd"
|
||||||
|
"Bug Reports" = "https://github.com/craigerl/aprsd/issues"
|
||||||
|
"Source" = "https://github.com/craigerl/aprsd"
|
||||||
|
|
||||||
|
# The following would provide a command line executable called `sample`
|
||||||
|
# which executes the function `main` from this package when invoked.
|
||||||
|
[project.scripts]
|
||||||
|
aprsd = "aprsd.main:main"
|
||||||
|
|
||||||
|
[project.entry-points."oslo.config.opts"]
|
||||||
|
"aprsd.conf" = "aprsd.conf.opts:list_opts"
|
||||||
|
|
||||||
|
[project.entry-points."oslo.config.opts.defaults"]
|
||||||
|
"aprsd.conf" = "aprsd.conf:set_lib_defaults"
|
||||||
|
|
||||||
|
# If you are using a different build backend, you will need to change this.
|
||||||
|
[tool.setuptools]
|
||||||
|
# If there are data files included in your packages that need to be
|
||||||
|
# installed, specify them here.
|
||||||
|
py-modules = ["aprsd"]
|
||||||
|
package-data = {"sample" = ["*.dat"]}
|
||||||
|
packages = ["aprsd"]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=46.0", "wheel"]
|
requires = [
|
||||||
|
"setuptools>=69.5.0",
|
||||||
|
"setuptools_scm>=0",
|
||||||
|
"wheel",
|
||||||
|
]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[tool.isort]
|
[tool.isort]
|
||||||
@ -14,3 +171,5 @@ skip_gitignore = true
|
|||||||
|
|
||||||
[tool.coverage.run]
|
[tool.coverage.run]
|
||||||
branch = true
|
branch = true
|
||||||
|
|
||||||
|
[tool.setuptools_scm]
|
||||||
|
@ -1,16 +1,23 @@
|
|||||||
|
build
|
||||||
|
check-manifest
|
||||||
flake8
|
flake8
|
||||||
|
gray
|
||||||
isort
|
isort
|
||||||
mypy
|
mypy
|
||||||
pep8-naming
|
pep8-naming
|
||||||
|
pytest
|
||||||
|
pytest-cov
|
||||||
|
pip
|
||||||
|
pip-tools
|
||||||
|
pre-commit
|
||||||
Sphinx
|
Sphinx
|
||||||
tox
|
tox
|
||||||
|
wheel
|
||||||
|
|
||||||
# Twine is used for uploading packages to pypi
|
# Twine is used for uploading packages to pypi
|
||||||
# but it induces an install of cryptography
|
# but it induces an install of cryptography
|
||||||
# This is sucky for rpi systems.
|
# This is sucky for rpi systems.
|
||||||
# twine
|
# twine
|
||||||
pre-commit
|
|
||||||
pytest
|
# m2r is for converting .md files to .rst for the docs
|
||||||
pytest-cov
|
m2r
|
||||||
gray
|
|
||||||
pip
|
|
||||||
pip-tools
|
|
86
requirements-dev.txt
Normal file
86
requirements-dev.txt
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
#
|
||||||
|
# This file is autogenerated by pip-compile with Python 3.10
|
||||||
|
# by the following command:
|
||||||
|
#
|
||||||
|
# pip-compile --annotation-style=line requirements-dev.in
|
||||||
|
#
|
||||||
|
add-trailing-comma==3.1.0 # via gray
|
||||||
|
alabaster==1.0.0 # via sphinx
|
||||||
|
autoflake==1.5.3 # via gray
|
||||||
|
babel==2.16.0 # via sphinx
|
||||||
|
black==24.8.0 # via gray
|
||||||
|
build==1.2.2 # via -r requirements-dev.in, check-manifest, pip-tools
|
||||||
|
cachetools==5.5.0 # via tox
|
||||||
|
certifi==2024.8.30 # via requests
|
||||||
|
cfgv==3.4.0 # via pre-commit
|
||||||
|
chardet==5.2.0 # via tox
|
||||||
|
charset-normalizer==3.3.2 # via requests
|
||||||
|
check-manifest==0.49 # via -r requirements-dev.in
|
||||||
|
click==8.1.7 # via black, fixit, moreorless, pip-tools
|
||||||
|
colorama==0.4.6 # via tox
|
||||||
|
commonmark==0.9.1 # via rich
|
||||||
|
configargparse==1.7 # via gray
|
||||||
|
coverage[toml]==7.6.1 # via pytest-cov
|
||||||
|
distlib==0.3.8 # via virtualenv
|
||||||
|
docutils==0.21.2 # via m2r, sphinx
|
||||||
|
exceptiongroup==1.2.2 # via pytest
|
||||||
|
filelock==3.16.0 # via tox, virtualenv
|
||||||
|
fixit==2.1.0 # via gray
|
||||||
|
flake8==7.1.1 # via -r requirements-dev.in, pep8-naming
|
||||||
|
gray==0.15.0 # via -r requirements-dev.in
|
||||||
|
identify==2.6.1 # via pre-commit
|
||||||
|
idna==3.10 # via requests
|
||||||
|
imagesize==1.4.1 # via sphinx
|
||||||
|
iniconfig==2.0.0 # via pytest
|
||||||
|
isort==5.13.2 # via -r requirements-dev.in, gray
|
||||||
|
jinja2==3.1.4 # via sphinx
|
||||||
|
libcst==1.4.0 # via fixit
|
||||||
|
m2r==0.3.1 # via -r requirements-dev.in
|
||||||
|
markupsafe==2.1.5 # via jinja2
|
||||||
|
mccabe==0.7.0 # via flake8
|
||||||
|
mistune==0.8.4 # via m2r
|
||||||
|
moreorless==0.4.0 # via fixit
|
||||||
|
mypy==1.11.2 # via -r requirements-dev.in
|
||||||
|
mypy-extensions==1.0.0 # via black, mypy
|
||||||
|
nodeenv==1.9.1 # via pre-commit
|
||||||
|
packaging==24.1 # via black, build, fixit, pyproject-api, pytest, sphinx, tox
|
||||||
|
pathspec==0.12.1 # via black, trailrunner
|
||||||
|
pep8-naming==0.14.1 # via -r requirements-dev.in
|
||||||
|
pip-tools==7.4.1 # via -r requirements-dev.in
|
||||||
|
platformdirs==4.3.3 # via black, tox, virtualenv
|
||||||
|
pluggy==1.5.0 # via pytest, tox
|
||||||
|
pre-commit==3.8.0 # via -r requirements-dev.in
|
||||||
|
pycodestyle==2.12.1 # via flake8
|
||||||
|
pyflakes==3.2.0 # via autoflake, flake8
|
||||||
|
pygments==2.18.0 # via rich, sphinx
|
||||||
|
pyproject-api==1.7.1 # via tox
|
||||||
|
pyproject-hooks==1.1.0 # via build, pip-tools
|
||||||
|
pytest==8.3.3 # via -r requirements-dev.in, pytest-cov
|
||||||
|
pytest-cov==5.0.0 # via -r requirements-dev.in
|
||||||
|
pyupgrade==3.17.0 # via gray
|
||||||
|
pyyaml==6.0.2 # via libcst, pre-commit
|
||||||
|
requests==2.32.3 # via sphinx
|
||||||
|
rich==12.6.0 # via gray
|
||||||
|
snowballstemmer==2.2.0 # via sphinx
|
||||||
|
sphinx==8.0.2 # via -r requirements-dev.in
|
||||||
|
sphinxcontrib-applehelp==2.0.0 # via sphinx
|
||||||
|
sphinxcontrib-devhelp==2.0.0 # via sphinx
|
||||||
|
sphinxcontrib-htmlhelp==2.1.0 # via sphinx
|
||||||
|
sphinxcontrib-jsmath==1.0.1 # via sphinx
|
||||||
|
sphinxcontrib-qthelp==2.0.0 # via sphinx
|
||||||
|
sphinxcontrib-serializinghtml==2.0.0 # via sphinx
|
||||||
|
tokenize-rt==6.0.0 # via add-trailing-comma, pyupgrade
|
||||||
|
toml==0.10.2 # via autoflake
|
||||||
|
tomli==2.0.1 # via black, build, check-manifest, coverage, fixit, mypy, pip-tools, pyproject-api, pytest, sphinx, tox
|
||||||
|
tox==4.18.1 # via -r requirements-dev.in
|
||||||
|
trailrunner==1.4.0 # via fixit
|
||||||
|
typing-extensions==4.12.2 # via black, mypy
|
||||||
|
unify==0.5 # via gray
|
||||||
|
untokenize==0.1.1 # via unify
|
||||||
|
urllib3==2.2.3 # via requests
|
||||||
|
virtualenv==20.26.4 # via pre-commit, tox
|
||||||
|
wheel==0.44.0 # via -r requirements-dev.in, pip-tools
|
||||||
|
|
||||||
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
|
# pip
|
||||||
|
# setuptools
|
@ -1,40 +1,32 @@
|
|||||||
aprslib>=0.7.0
|
aprslib>=0.7.0
|
||||||
click
|
|
||||||
click-params
|
|
||||||
click-completion
|
|
||||||
flask
|
|
||||||
werkzeug
|
|
||||||
flask-httpauth
|
|
||||||
imapclient
|
|
||||||
pluggy
|
|
||||||
pbr
|
|
||||||
pyyaml
|
|
||||||
requests
|
|
||||||
pytz
|
|
||||||
six
|
|
||||||
thesmuggler
|
|
||||||
update_checker
|
|
||||||
flask-socketio
|
|
||||||
python-socketio
|
|
||||||
gevent
|
|
||||||
eventlet
|
|
||||||
tabulate
|
|
||||||
# Pinned due to gray needing 12.6.0
|
|
||||||
rich~=12.6.0
|
|
||||||
# For the list-plugins pypi.org search scraping
|
# For the list-plugins pypi.org search scraping
|
||||||
beautifulsoup4
|
beautifulsoup4
|
||||||
wrapt
|
click
|
||||||
# kiss3 uses attrs
|
click-params
|
||||||
kiss3
|
|
||||||
attrs
|
|
||||||
dataclasses
|
dataclasses
|
||||||
dacite2
|
|
||||||
oslo.config
|
|
||||||
rpyc>=6.0.0
|
|
||||||
# Pin this here so it doesn't require a compile on
|
|
||||||
# raspi
|
|
||||||
shellingham
|
|
||||||
geopy
|
|
||||||
rush
|
|
||||||
dataclasses-json
|
dataclasses-json
|
||||||
|
eventlet
|
||||||
|
flask
|
||||||
|
flask-httpauth
|
||||||
|
flask-socketio
|
||||||
|
geopy
|
||||||
|
gevent
|
||||||
|
imapclient
|
||||||
|
kiss3
|
||||||
loguru
|
loguru
|
||||||
|
oslo.config
|
||||||
|
pluggy
|
||||||
|
python-socketio
|
||||||
|
pyyaml
|
||||||
|
pytz
|
||||||
|
requests
|
||||||
|
# Pinned due to gray needing 12.6.0
|
||||||
|
rich~=12.6.0
|
||||||
|
rush
|
||||||
|
shellingham
|
||||||
|
six
|
||||||
|
tabulate
|
||||||
|
thesmuggler
|
||||||
|
tzlocal
|
||||||
|
update_checker
|
||||||
|
wrapt
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user