mirror of
https://github.com/craigerl/aprsd.git
synced 2026-08-18 17:44:02 -04:00
Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b2f95b0f4e | |||
| ae9e4d31ad | |||
| 65a5a90458 | |||
| 182887c20a | |||
| f228144f4b | |||
| db9e1d23d1 | |||
| 986df391b2 | |||
| 3994235380 | |||
| 9ebf2f9a30 | |||
| 011cfc55e1 | |||
| e0c3c5cbbf | |||
| 26f354b3a9 | |||
| 922a6dbb35 | |||
| d03c4fc096 | |||
| dfd3688d8f | |||
| c7d629f88a | |||
| 099b87e250 | |||
| 1ab9c3fee4 | |||
| 8891cd3002 | |||
| 4664ead9e7 | |||
| e51a501544 | |||
| 89576a3c43 | |||
| 5383b698ea | |||
| cbef93b327 | |||
| 6ae55fc9a1 | |||
| 588e140a7f | |||
| d251a2727a | |||
| d3a93b735d | |||
| fa452cc773 | |||
| 6a6e854caf | |||
| e1183a7e30 | |||
| 5723e3a77b | |||
| dee73c1060 | |||
| d8318f2ae2 | |||
| 2825cac446 | |||
| fa6e738a20 | |||
| 0c179005ee | |||
| ad004633de | |||
| ccd564a52e | |||
| 35d41582ee | |||
| 565ffe3f72 | |||
| 0bd11d05c6 | |||
| 62eff8645d | |||
| aa547cbef5 | |||
| 7f2aba702a | |||
| 63bf82aab5 | |||
| bba7b68112 | |||
| 005675cb46 | |||
| 191e1ff552 | |||
| 0a14b07fae | |||
| b2e621da4b | |||
| fe0d71de4d | |||
| 9b944142bd | |||
| b172c692a1 | |||
| 311cebaf27 | |||
| f4d60357ee | |||
| 09a0c4cb02 | |||
| 80b85e648f | |||
| 9931c8a6c5 | |||
| 319969cc08 | |||
| da20ff038b | |||
| 15bf3710d2 | |||
| 5bc589f21f | |||
| 8b73372b6e | |||
| 26c1e7afbb | |||
| c99d5b859e | |||
| cad22e1744 | |||
| 43d6b62760 | |||
| 96fa4330ba | |||
| 4e99e30f16 | |||
| 00f1c3a2ba |
@@ -0,0 +1,52 @@
|
|||||||
|
name: Manual Build docker container
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
logLevel:
|
||||||
|
description: 'Log level'
|
||||||
|
required: true
|
||||||
|
default: 'warning'
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- info
|
||||||
|
- warning
|
||||||
|
- debug
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Get Branch Name
|
||||||
|
id: branch-name
|
||||||
|
uses: tj-actions/branch-names@v7
|
||||||
|
- name: Extract Branch
|
||||||
|
id: extract_branch
|
||||||
|
run: |
|
||||||
|
echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT
|
||||||
|
- name: What is the selected branch?
|
||||||
|
run: |
|
||||||
|
echo "Selected Branch '${{ steps.extract_branch.outputs.branch }}'"
|
||||||
|
- name: Setup QEMU
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
- name: Setup Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: Login to Docker HUB
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Build the Docker image
|
||||||
|
uses: docker/build-push-action@v3
|
||||||
|
with:
|
||||||
|
context: "{{defaultContext}}:docker"
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
file: ./Dockerfile-dev
|
||||||
|
build-args: |
|
||||||
|
BRANCH=${{ steps.extract_branch.outputs.branch }}
|
||||||
|
BUILDX_QEMU_ENV=true
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
hemna6969/aprsd:${{ steps.extract_branch.outputs.branch }}
|
||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.8", "3.9", "3.10"]
|
python-version: ["3.8", "3.9", "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 }}
|
||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install tox tox-gh-actions
|
pip install tox tox-gh>=1.2
|
||||||
- name: Test with tox
|
- name: Test with tox
|
||||||
run: tox
|
run: tox
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,6 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install tox tox-gh-actions
|
pip install tox tox-gh>=1.2
|
||||||
- name: Test with tox
|
- name: Test with tox
|
||||||
run: tox
|
run: tox
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: "{{defaultContext}}:docker"
|
context: "{{defaultContext}}:docker"
|
||||||
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
platforms: linux/amd64,linux/arm64
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
build-args: |
|
build-args: |
|
||||||
VERSION=${{ inputs.aprsd_version }}
|
VERSION=${{ inputs.aprsd_version }}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
# .readthedocs.yaml
|
||||||
|
# Read the Docs configuration file
|
||||||
|
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||||
|
|
||||||
|
# Required
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
# Set the version of Python and other tools you might need
|
||||||
|
build:
|
||||||
|
os: ubuntu-22.04
|
||||||
|
tools:
|
||||||
|
python: "3.11"
|
||||||
|
|
||||||
|
# Build documentation in the docs/ directory with Sphinx
|
||||||
|
sphinx:
|
||||||
|
configuration: docs/conf.py
|
||||||
|
|
||||||
|
# We recommend specifying your dependencies to enable reproducible builds:
|
||||||
|
# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
|
||||||
|
python:
|
||||||
|
install:
|
||||||
|
- requirements: dev-requirements.txt
|
||||||
@@ -1,9 +1,85 @@
|
|||||||
CHANGES
|
CHANGES
|
||||||
=======
|
=======
|
||||||
|
|
||||||
|
v3.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
|
v3.0.3
|
||||||
------
|
------
|
||||||
|
|
||||||
|
* Update Changelog to 3.0.3
|
||||||
* cleanup some debug messages
|
* cleanup some debug messages
|
||||||
* Fixed loading of plugins for server
|
* Fixed loading of plugins for server
|
||||||
* Don't load help plugin for listen command
|
* Don't load help plugin for listen command
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ include Makefile.venv
|
|||||||
Makefile.venv:
|
Makefile.venv:
|
||||||
curl \
|
curl \
|
||||||
-o Makefile.fetched \
|
-o Makefile.fetched \
|
||||||
-L "https://github.com/sio/Makefile.venv/raw/v2022.07.20/Makefile.venv"
|
-L "https://raw.githubusercontent.com/sio/Makefile.venv/master/Makefile.venv"
|
||||||
echo "147b164f0cbbbe4a2740dcca6c9adb6e9d8d15b895be3998697aa6a821a277d8 *Makefile.fetched" \
|
echo " fb48375ed1fd19e41e0cdcf51a4a0c6d1010dfe03b672ffc4c26a91878544f82 *Makefile.fetched" \
|
||||||
| sha256sum --check - \
|
| sha256sum --check - \
|
||||||
&& mv Makefile.fetched Makefile.venv
|
&& mv Makefile.fetched Makefile.venv
|
||||||
|
|
||||||
|
|||||||
+14
@@ -289,6 +289,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
|
||||||
===========
|
===========
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from werkzeug.security import check_password_hash, generate_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.logging import rich as aprsd_logging
|
from aprsd.log import rich as aprsd_logging
|
||||||
from aprsd.rpc import client as aprsd_rpc_client
|
from aprsd.rpc import client as aprsd_rpc_client
|
||||||
|
|
||||||
|
|
||||||
@@ -335,13 +335,26 @@ def init_flask(loglevel, quiet):
|
|||||||
return socketio, flask_app
|
return socketio, flask_app
|
||||||
|
|
||||||
|
|
||||||
|
def create_app(config_file=None, log_level=None):
|
||||||
|
global socketio
|
||||||
|
global app
|
||||||
|
|
||||||
|
default_config_file = cli_helper.DEFAULT_CONFIG_FILE
|
||||||
|
if not config_file:
|
||||||
|
config_file = default_config_file
|
||||||
|
|
||||||
|
CONF(
|
||||||
|
[], project="aprsd", version=aprsd.__version__,
|
||||||
|
default_config_files=[config_file],
|
||||||
|
)
|
||||||
|
|
||||||
|
if not log_level:
|
||||||
|
log_level = CONF.logging.log_level
|
||||||
|
|
||||||
|
socketio, app = init_flask(log_level, False)
|
||||||
|
setup_logging(app, log_level, False)
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "aprsd.flask":
|
if __name__ == "aprsd.flask":
|
||||||
try:
|
sio, app = create_app()
|
||||||
default_config_file = cli_helper.DEFAULT_CONFIG_FILE
|
|
||||||
CONF(
|
|
||||||
[], project="aprsd", version=aprsd.__version__,
|
|
||||||
default_config_files=[default_config_file],
|
|
||||||
)
|
|
||||||
except cfg.ConfigFilesNotFoundError:
|
|
||||||
pass
|
|
||||||
sio, app = init_flask("DEBUG", False)
|
|
||||||
+1
-1
@@ -8,7 +8,7 @@ from oslo_config import cfg
|
|||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import conf # noqa: F401
|
from aprsd import conf # noqa: F401
|
||||||
from aprsd.logging import log
|
from aprsd.log import log
|
||||||
from aprsd.utils import trace
|
from aprsd.utils import trace
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+17
-4
@@ -49,8 +49,10 @@ class Client:
|
|||||||
@property
|
@property
|
||||||
def client(self):
|
def client(self):
|
||||||
if not self._client:
|
if not self._client:
|
||||||
|
LOG.info("Creating APRS client")
|
||||||
self._client = self.setup_connection()
|
self._client = self.setup_connection()
|
||||||
if self.filter:
|
if self.filter:
|
||||||
|
LOG.info("Creating APRS client filter")
|
||||||
self._client.set_filter(self.filter)
|
self._client.set_filter(self.filter)
|
||||||
return self._client
|
return self._client
|
||||||
|
|
||||||
@@ -62,6 +64,8 @@ class Client:
|
|||||||
"""Call this to force a rebuild/reconnect."""
|
"""Call this to force a rebuild/reconnect."""
|
||||||
if self._client:
|
if self._client:
|
||||||
del self._client
|
del self._client
|
||||||
|
else:
|
||||||
|
LOG.warning("Client not initialized, nothing to reset.")
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def setup_connection(self):
|
def setup_connection(self):
|
||||||
@@ -144,7 +148,7 @@ class APRSISClient(Client):
|
|||||||
try:
|
try:
|
||||||
LOG.info("Creating aprslib client")
|
LOG.info("Creating aprslib client")
|
||||||
aprs_client = aprsis.Aprsdis(user, passwd=password, host=host, port=port)
|
aprs_client = aprsis.Aprsdis(user, passwd=password, host=host, port=port)
|
||||||
# Force the logging to be the same
|
# Force the log to be the same
|
||||||
aprs_client.logger = LOG
|
aprs_client.logger = LOG
|
||||||
aprs_client.connect()
|
aprs_client.connect()
|
||||||
connected = True
|
connected = True
|
||||||
@@ -152,11 +156,16 @@ class APRSISClient(Client):
|
|||||||
except LoginError as e:
|
except LoginError as e:
|
||||||
LOG.error(f"Failed to login to APRS-IS Server '{e}'")
|
LOG.error(f"Failed to login to APRS-IS Server '{e}'")
|
||||||
connected = False
|
connected = False
|
||||||
raise e
|
time.sleep(backoff)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
|
LOG.error(f"Unable to connect to APRS-IS server. '{e}' ")
|
||||||
|
connected = False
|
||||||
time.sleep(backoff)
|
time.sleep(backoff)
|
||||||
backoff = backoff * 2
|
# Don't allow the backoff to go to inifinity.
|
||||||
|
if backoff > 5:
|
||||||
|
backoff = 5
|
||||||
|
else:
|
||||||
|
backoff += 1
|
||||||
continue
|
continue
|
||||||
LOG.debug(f"Logging in to APRS-IS with user '{user}'")
|
LOG.debug(f"Logging in to APRS-IS with user '{user}'")
|
||||||
self._client = aprs_client
|
self._client = aprs_client
|
||||||
@@ -226,7 +235,11 @@ class KISSClient(Client):
|
|||||||
# LOG.debug(f"Decoding {msg}")
|
# LOG.debug(f"Decoding {msg}")
|
||||||
|
|
||||||
raw = aprslib.parse(str(frame))
|
raw = aprslib.parse(str(frame))
|
||||||
return core.Packet.factory(raw)
|
packet = core.Packet.factory(raw)
|
||||||
|
if isinstance(packet, core.ThirdParty):
|
||||||
|
return packet.subpacket
|
||||||
|
else:
|
||||||
|
return packet
|
||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def setup_connection(self):
|
def setup_connection(self):
|
||||||
|
|||||||
+11
-6
@@ -112,23 +112,23 @@ class Aprsdis(aprslib.IS):
|
|||||||
self._sendall(login_str)
|
self._sendall(login_str)
|
||||||
self.sock.settimeout(5)
|
self.sock.settimeout(5)
|
||||||
test = self.sock.recv(len(login_str) + 100)
|
test = self.sock.recv(len(login_str) + 100)
|
||||||
|
self.logger.debug("Server: '%s'", test)
|
||||||
if is_py3:
|
if is_py3:
|
||||||
test = test.decode("latin-1")
|
test = test.decode("latin-1")
|
||||||
test = test.rstrip()
|
test = test.rstrip()
|
||||||
|
|
||||||
self.logger.debug("Server: %s", test)
|
self.logger.debug("Server: '%s'", test)
|
||||||
|
|
||||||
a, b, callsign, status, e = test.split(" ", 4)
|
if not test:
|
||||||
|
raise LoginError(f"Server Response Empty: '{test}'")
|
||||||
|
|
||||||
|
_, _, callsign, status, e = test.split(" ", 4)
|
||||||
s = e.split(",")
|
s = e.split(",")
|
||||||
if len(s):
|
if len(s):
|
||||||
server_string = s[0].replace("server ", "")
|
server_string = s[0].replace("server ", "")
|
||||||
else:
|
else:
|
||||||
server_string = e.replace("server ", "")
|
server_string = e.replace("server ", "")
|
||||||
|
|
||||||
self.logger.info(f"Connected to {server_string}")
|
|
||||||
self.server_string = server_string
|
|
||||||
stats.APRSDStats().set_aprsis_server(server_string)
|
|
||||||
|
|
||||||
if callsign == "":
|
if callsign == "":
|
||||||
raise LoginError("Server responded with empty callsign???")
|
raise LoginError("Server responded with empty callsign???")
|
||||||
if callsign != self.callsign:
|
if callsign != self.callsign:
|
||||||
@@ -141,6 +141,10 @@ class Aprsdis(aprslib.IS):
|
|||||||
else:
|
else:
|
||||||
self.logger.info("Login successful")
|
self.logger.info("Login successful")
|
||||||
|
|
||||||
|
self.logger.info(f"Connected to {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))
|
||||||
self.close()
|
self.close()
|
||||||
@@ -148,6 +152,7 @@ class Aprsdis(aprslib.IS):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.close()
|
self.close()
|
||||||
self.logger.error(f"Failed to login '{e}'")
|
self.logger.error(f"Failed to login '{e}'")
|
||||||
|
self.logger.exception(e)
|
||||||
raise LoginError("Failed to login")
|
raise LoginError("Failed to login")
|
||||||
|
|
||||||
def consumer(self, callback, blocking=True, immortal=False, raw=False):
|
def consumer(self, callback, blocking=True, immortal=False, raw=False):
|
||||||
|
|||||||
+9
-15
@@ -81,27 +81,18 @@ class KISS3Client:
|
|||||||
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)
|
||||||
LOG.debug("END blocking KISS consumer")
|
LOG.debug(f"END blocking KISS consumer {self.kiss}")
|
||||||
|
|
||||||
def send(self, packet):
|
def send(self, packet):
|
||||||
"""Send an APRS Message object."""
|
"""Send an APRS Message object."""
|
||||||
|
|
||||||
# payload = (':%-9s:%s' % (
|
|
||||||
# msg.tocall,
|
|
||||||
# payload
|
|
||||||
# )).encode('US-ASCII'),
|
|
||||||
# payload = str(msg).encode('US-ASCII')
|
|
||||||
payload = None
|
payload = None
|
||||||
path = ["WIDE1-1", "WIDE2-1"]
|
path = ["WIDE1-1", "WIDE2-1"]
|
||||||
if isinstance(packet, core.AckPacket):
|
if isinstance(packet, core.Packet):
|
||||||
msg_payload = f"ack{packet.msgNo}"
|
packet.prepare()
|
||||||
elif isinstance(packet, core.Packet):
|
payload = packet.payload.encode("US-ASCII")
|
||||||
payload = packet.raw.encode("US-ASCII")
|
|
||||||
path = ["WIDE2-1"]
|
|
||||||
else:
|
else:
|
||||||
msg_payload = f"{packet.raw}{{{str(packet.msgNo)}"
|
msg_payload = f"{packet.raw}{{{str(packet.msgNo)}"
|
||||||
|
|
||||||
if not payload:
|
|
||||||
payload = (
|
payload = (
|
||||||
":{:<9}:{}".format(
|
":{:<9}:{}".format(
|
||||||
packet.to_call,
|
packet.to_call,
|
||||||
@@ -109,9 +100,12 @@ class KISS3Client:
|
|||||||
)
|
)
|
||||||
).encode("US-ASCII")
|
).encode("US-ASCII")
|
||||||
|
|
||||||
LOG.debug(f"Send '{payload}' TO KISS")
|
LOG.debug(
|
||||||
|
f"KISS Send '{payload}' TO '{packet.to_call}' From "
|
||||||
|
f"'{packet.from_call}' with PATH '{path}'",
|
||||||
|
)
|
||||||
frame = Frame.ui(
|
frame = Frame.ui(
|
||||||
destination=packet.to_call,
|
destination="APZ100",
|
||||||
source=packet.from_call,
|
source=packet.from_call,
|
||||||
path=path,
|
path=path,
|
||||||
info=payload,
|
info=payload,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import click
|
import click
|
||||||
import click_completion
|
import click_completion
|
||||||
|
|
||||||
from aprsd.aprsd import cli
|
from aprsd.main import cli
|
||||||
|
|
||||||
|
|
||||||
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ from oslo_config import cfg
|
|||||||
|
|
||||||
# local imports here
|
# local imports here
|
||||||
from aprsd import cli_helper, client, conf, packets, plugin
|
from aprsd import cli_helper, client, conf, packets, plugin
|
||||||
from aprsd.aprsd import cli
|
from aprsd.main import cli
|
||||||
from aprsd.utils import trace
|
from aprsd.utils import trace
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
# Fetch active stats from a remote running instance of aprsd server
|
||||||
|
# This uses the RPC server to fetch the stats from the remote server.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import click
|
||||||
|
from oslo_config import cfg
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.table import Table
|
||||||
|
|
||||||
|
# local imports here
|
||||||
|
import aprsd
|
||||||
|
from aprsd import cli_helper
|
||||||
|
from aprsd.main import cli
|
||||||
|
from aprsd.rpc import client as rpc_client
|
||||||
|
|
||||||
|
|
||||||
|
# setup the global logger
|
||||||
|
# log.basicConfig(level=log.DEBUG) # level=10
|
||||||
|
LOG = logging.getLogger("APRSD")
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
@cli_helper.add_options(cli_helper.common_options)
|
||||||
|
@click.option(
|
||||||
|
"--host", type=str,
|
||||||
|
default=None,
|
||||||
|
help="IP address of the remote aprsd server to fetch stats from.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--port", type=int,
|
||||||
|
default=None,
|
||||||
|
help="Port of the remote aprsd server rpc port 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
|
||||||
|
@cli_helper.process_standard_options
|
||||||
|
def fetch_stats(ctx, host, port, magic_word):
|
||||||
|
"""Fetch stats from a remote running instance of aprsd server."""
|
||||||
|
LOG.info(f"APRSD Fetch-Stats started version: {aprsd.__version__}")
|
||||||
|
|
||||||
|
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||||
|
if not host:
|
||||||
|
host = CONF.rpc_settings.ip
|
||||||
|
if not port:
|
||||||
|
port = CONF.rpc_settings.port
|
||||||
|
if not magic_word:
|
||||||
|
magic_word = CONF.rpc_settings.magic_word
|
||||||
|
|
||||||
|
msg = f"Fetching stats from {host}:{port} with magic word '{magic_word}'"
|
||||||
|
console = Console()
|
||||||
|
console.print(msg)
|
||||||
|
with console.status(msg):
|
||||||
|
client = rpc_client.RPCClient(host, port, magic_word)
|
||||||
|
stats = client.get_stats_dict()
|
||||||
|
console.print_json(data=stats)
|
||||||
|
aprsd_title = (
|
||||||
|
"APRSD "
|
||||||
|
f"[bold cyan]v{stats['aprsd']['version']}[/] "
|
||||||
|
f"Callsign [bold green]{stats['aprsd']['callsign']}[/] "
|
||||||
|
f"Uptime [bold yellow]{stats['aprsd']['uptime']}[/]"
|
||||||
|
)
|
||||||
|
|
||||||
|
console.rule(f"Stats from {host}:{port} with magic word '{magic_word}'")
|
||||||
|
console.print("\n\n")
|
||||||
|
console.rule(aprsd_title)
|
||||||
|
|
||||||
|
# Show the connection to APRS
|
||||||
|
# It can be a connection to an APRS-IS server or a local TNC via KISS or KISSTCP
|
||||||
|
if "aprs-is" in stats:
|
||||||
|
title = f"APRS-IS Connection {stats['aprs-is']['server']}"
|
||||||
|
table = Table(title=title)
|
||||||
|
table.add_column("Key")
|
||||||
|
table.add_column("Value")
|
||||||
|
for key, value in stats["aprs-is"].items():
|
||||||
|
table.add_row(key, value)
|
||||||
|
console.print(table)
|
||||||
|
|
||||||
|
threads_table = Table(title="Threads")
|
||||||
|
threads_table.add_column("Name")
|
||||||
|
threads_table.add_column("Alive?")
|
||||||
|
for name, alive in stats["aprsd"]["threads"].items():
|
||||||
|
threads_table.add_row(name, str(alive))
|
||||||
|
|
||||||
|
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.add_column("Key")
|
||||||
|
packet_totals.add_column("Value")
|
||||||
|
packet_totals.add_row("Total Received", str(stats["packets"]["total_received"]))
|
||||||
|
packet_totals.add_row("Total Sent", str(stats["packets"]["total_sent"]))
|
||||||
|
packet_totals.add_row("Total Tracked", str(stats["packets"]["total_tracked"]))
|
||||||
|
console.print(packet_totals)
|
||||||
|
|
||||||
|
# Show each of the packet types
|
||||||
|
packets_table = Table(title="Packets By Type")
|
||||||
|
packets_table.add_column("Packet Type")
|
||||||
|
packets_table.add_column("TX")
|
||||||
|
packets_table.add_column("RX")
|
||||||
|
for key, value in stats["packets"]["by_type"].items():
|
||||||
|
packets_table.add_row(key, str(value["tx"]), str(value["rx"]))
|
||||||
|
|
||||||
|
console.print(packets_table)
|
||||||
|
|
||||||
|
if "plugins" in stats:
|
||||||
|
count = len(stats["plugins"])
|
||||||
|
plugins_table = Table(title=f"Plugins ({count})")
|
||||||
|
plugins_table.add_column("Plugin")
|
||||||
|
plugins_table.add_column("Enabled")
|
||||||
|
plugins_table.add_column("Version")
|
||||||
|
plugins_table.add_column("TX")
|
||||||
|
plugins_table.add_column("RX")
|
||||||
|
for key, value in stats["plugins"].items():
|
||||||
|
plugins_table.add_row(
|
||||||
|
key,
|
||||||
|
str(stats["plugins"][key]["enabled"]),
|
||||||
|
stats["plugins"][key]["version"],
|
||||||
|
str(stats["plugins"][key]["tx"]),
|
||||||
|
str(stats["plugins"][key]["rx"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
console.print(plugins_table)
|
||||||
|
|
||||||
|
if "seen_list" in stats["aprsd"]:
|
||||||
|
count = len(stats["aprsd"]["seen_list"])
|
||||||
|
seen_table = Table(title=f"Seen List ({count})")
|
||||||
|
seen_table.add_column("Callsign")
|
||||||
|
seen_table.add_column("Message Count")
|
||||||
|
seen_table.add_column("Last Heard")
|
||||||
|
for key, value in stats["aprsd"]["seen_list"].items():
|
||||||
|
seen_table.add_row(key, str(value["count"]), value["last"])
|
||||||
|
|
||||||
|
console.print(seen_table)
|
||||||
|
|
||||||
|
if "watch_list" in stats["aprsd"]:
|
||||||
|
count = len(stats["aprsd"]["watch_list"])
|
||||||
|
watch_table = Table(title=f"Watch List ({count})")
|
||||||
|
watch_table.add_column("Callsign")
|
||||||
|
watch_table.add_column("Last Heard")
|
||||||
|
for key, value in stats["aprsd"]["watch_list"].items():
|
||||||
|
watch_table.add_row(key, value["last"])
|
||||||
|
|
||||||
|
console.print(watch_table)
|
||||||
@@ -16,12 +16,12 @@ import aprsd
|
|||||||
from aprsd import cli_helper, utils
|
from aprsd import cli_helper, utils
|
||||||
from aprsd import conf # noqa
|
from aprsd import conf # noqa
|
||||||
# local imports here
|
# local imports here
|
||||||
from aprsd.aprsd import cli
|
from aprsd.main import cli
|
||||||
from aprsd.rpc import client as aprsd_rpc_client
|
from aprsd.rpc import client as aprsd_rpc_client
|
||||||
|
|
||||||
|
|
||||||
# setup the global logger
|
# setup the global logger
|
||||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
# log.basicConfig(level=log.DEBUG) # level=10
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from thesmuggler import smuggle
|
|||||||
|
|
||||||
from aprsd import cli_helper
|
from aprsd import cli_helper
|
||||||
from aprsd import plugin as aprsd_plugin
|
from aprsd import plugin as aprsd_plugin
|
||||||
from aprsd.aprsd 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, query, time, version, weather,
|
||||||
)
|
)
|
||||||
|
|||||||
+10
-2
@@ -16,12 +16,13 @@ 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, client, packets, plugin, stats, threads
|
||||||
from aprsd.aprsd import cli
|
from aprsd.main import cli
|
||||||
|
from aprsd.rpc import server as rpc_server
|
||||||
from aprsd.threads import rx
|
from aprsd.threads import rx
|
||||||
|
|
||||||
|
|
||||||
# setup the global logger
|
# setup the global logger
|
||||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
# log.basicConfig(level=log.DEBUG) # level=10
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
console = Console()
|
console = Console()
|
||||||
@@ -181,6 +182,10 @@ def listen(
|
|||||||
keepalive = threads.KeepAliveThread()
|
keepalive = threads.KeepAliveThread()
|
||||||
keepalive.start()
|
keepalive.start()
|
||||||
|
|
||||||
|
if CONF.rpc_settings.enabled:
|
||||||
|
rpc = rpc_server.APRSDRPCThread()
|
||||||
|
rpc.start()
|
||||||
|
|
||||||
pm = None
|
pm = None
|
||||||
pm = plugin.PluginManager()
|
pm = plugin.PluginManager()
|
||||||
if load_plugins:
|
if load_plugins:
|
||||||
@@ -204,3 +209,6 @@ def listen(
|
|||||||
keepalive.join()
|
keepalive.join()
|
||||||
LOG.debug("listen_thread Join")
|
LOG.debug("listen_thread Join")
|
||||||
listen_thread.join()
|
listen_thread.join()
|
||||||
|
|
||||||
|
if CONF.rpc_settings.enabled:
|
||||||
|
rpc.join()
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from oslo_config import cfg
|
|||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import cli_helper, client, packets
|
from aprsd import cli_helper, client, packets
|
||||||
from aprsd import conf # noqa : F401
|
from aprsd import conf # noqa : F401
|
||||||
from aprsd.aprsd import cli
|
from aprsd.main import cli
|
||||||
from aprsd.threads import tx
|
from aprsd.threads import tx
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ import click
|
|||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import aprsd as aprsd_main
|
from aprsd import cli_helper, client
|
||||||
from aprsd import cli_helper, client, packets, plugin, threads, utils
|
from aprsd import main as aprsd_main
|
||||||
from aprsd.aprsd import cli
|
from aprsd import packets, plugin, threads, utils
|
||||||
|
from aprsd.main import cli
|
||||||
from aprsd.rpc import server as rpc_server
|
from aprsd.rpc import server as rpc_server
|
||||||
from aprsd.threads import rx
|
from aprsd.threads import rx
|
||||||
|
|
||||||
|
|||||||
+114
-110
@@ -12,7 +12,6 @@ import click
|
|||||||
import flask
|
import flask
|
||||||
from flask import request
|
from flask import request
|
||||||
from flask.logging import default_handler
|
from flask.logging import default_handler
|
||||||
import flask_classful
|
|
||||||
from flask_httpauth import HTTPBasicAuth
|
from flask_httpauth import HTTPBasicAuth
|
||||||
from flask_socketio import Namespace, SocketIO
|
from flask_socketio import Namespace, SocketIO
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
@@ -22,8 +21,8 @@ import wrapt
|
|||||||
|
|
||||||
import aprsd
|
import aprsd
|
||||||
from aprsd import cli_helper, client, conf, packets, stats, threads, utils
|
from aprsd import cli_helper, client, conf, packets, stats, threads, utils
|
||||||
from aprsd.aprsd import cli
|
from aprsd.log import rich as aprsd_logging
|
||||||
from aprsd.logging import rich as aprsd_logging
|
from aprsd.main import cli
|
||||||
from aprsd.threads import rx, tx
|
from aprsd.threads import rx, tx
|
||||||
from aprsd.utils import objectstore, trace
|
from aprsd.utils import objectstore, trace
|
||||||
|
|
||||||
@@ -31,9 +30,16 @@ from aprsd.utils import objectstore, trace
|
|||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
auth = HTTPBasicAuth()
|
auth = HTTPBasicAuth()
|
||||||
users = None
|
users = {}
|
||||||
socketio = None
|
socketio = None
|
||||||
|
|
||||||
|
flask_app = flask.Flask(
|
||||||
|
"aprsd",
|
||||||
|
static_url_path="/static",
|
||||||
|
static_folder="web/chat/static",
|
||||||
|
template_folder="web/chat/templates",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def signal_handler(sig, frame):
|
def signal_handler(sig, frame):
|
||||||
|
|
||||||
@@ -174,106 +180,112 @@ class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class WebChatFlask(flask_classful.FlaskView):
|
def set_config():
|
||||||
|
global users
|
||||||
|
|
||||||
def set_config(self):
|
|
||||||
global users
|
|
||||||
self.users = {}
|
|
||||||
user = CONF.admin.user
|
|
||||||
self.users[user] = generate_password_hash(CONF.admin.password)
|
|
||||||
users = self.users
|
|
||||||
|
|
||||||
def _get_transport(self, stats):
|
def _get_transport(stats):
|
||||||
if CONF.aprs_network.enabled:
|
if CONF.aprs_network.enabled:
|
||||||
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["stats"]["aprs-is"]["server"])
|
||||||
)
|
|
||||||
else:
|
|
||||||
# We might be connected to a KISS socket?
|
|
||||||
if client.KISSClient.is_enabled():
|
|
||||||
transport = client.KISSClient.transport()
|
|
||||||
if transport == client.TRANSPORT_TCPKISS:
|
|
||||||
aprs_connection = (
|
|
||||||
"TCPKISS://{}:{}".format(
|
|
||||||
CONF.kiss_tcp.host,
|
|
||||||
CONF.kiss_tcp.port,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif transport == client.TRANSPORT_SERIALKISS:
|
|
||||||
# for pep8 violation
|
|
||||||
aprs_connection = (
|
|
||||||
"SerialKISS://{}@{} baud".format(
|
|
||||||
CONF.kiss_serial.device,
|
|
||||||
CONF.kiss_serial.baudrate,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return transport, aprs_connection
|
|
||||||
|
|
||||||
@auth.login_required
|
|
||||||
def index(self):
|
|
||||||
ua_str = request.headers.get("User-Agent")
|
|
||||||
# this takes about 2 seconds :(
|
|
||||||
user_agent = ua_parse(ua_str)
|
|
||||||
LOG.debug(f"Is mobile? {user_agent.is_mobile}")
|
|
||||||
stats = self._stats()
|
|
||||||
|
|
||||||
if user_agent.is_mobile:
|
|
||||||
html_template = "mobile.html"
|
|
||||||
else:
|
|
||||||
html_template = "index.html"
|
|
||||||
|
|
||||||
# For development
|
|
||||||
# html_template = "mobile.html"
|
|
||||||
|
|
||||||
LOG.debug(f"Template {html_template}")
|
|
||||||
|
|
||||||
transport, aprs_connection = self._get_transport(stats)
|
|
||||||
LOG.debug(f"transport {transport} aprs_connection {aprs_connection}")
|
|
||||||
|
|
||||||
stats["transport"] = transport
|
|
||||||
stats["aprs_connection"] = aprs_connection
|
|
||||||
LOG.debug(f"initial stats = {stats}")
|
|
||||||
|
|
||||||
return flask.render_template(
|
|
||||||
html_template,
|
|
||||||
initial_stats=stats,
|
|
||||||
aprs_connection=aprs_connection,
|
|
||||||
callsign=CONF.callsign,
|
|
||||||
version=aprsd.__version__,
|
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
# We might be connected to a KISS socket?
|
||||||
|
if client.KISSClient.is_enabled():
|
||||||
|
transport = client.KISSClient.transport()
|
||||||
|
if transport == client.TRANSPORT_TCPKISS:
|
||||||
|
aprs_connection = (
|
||||||
|
"TCPKISS://{}:{}".format(
|
||||||
|
CONF.kiss_tcp.host,
|
||||||
|
CONF.kiss_tcp.port,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif transport == client.TRANSPORT_SERIALKISS:
|
||||||
|
# for pep8 violation
|
||||||
|
aprs_connection = (
|
||||||
|
"SerialKISS://{}@{} baud".format(
|
||||||
|
CONF.kiss_serial.device,
|
||||||
|
CONF.kiss_serial.baudrate,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
@auth.login_required
|
return transport, aprs_connection
|
||||||
def send_message_status(self):
|
|
||||||
LOG.debug(request)
|
|
||||||
msgs = SentMessages()
|
|
||||||
info = msgs.get_all()
|
|
||||||
return json.dumps(info)
|
|
||||||
|
|
||||||
def _stats(self):
|
|
||||||
stats_obj = stats.APRSDStats()
|
|
||||||
now = datetime.datetime.now()
|
|
||||||
|
|
||||||
time_format = "%m-%d-%Y %H:%M:%S"
|
@auth.login_required
|
||||||
stats_dict = stats_obj.stats()
|
@flask_app.route("/")
|
||||||
# Webchat doesnt need these
|
def index():
|
||||||
|
ua_str = request.headers.get("User-Agent")
|
||||||
|
# this takes about 2 seconds :(
|
||||||
|
user_agent = ua_parse(ua_str)
|
||||||
|
LOG.debug(f"Is mobile? {user_agent.is_mobile}")
|
||||||
|
stats = _stats()
|
||||||
|
|
||||||
|
if user_agent.is_mobile:
|
||||||
|
html_template = "mobile.html"
|
||||||
|
else:
|
||||||
|
html_template = "index.html"
|
||||||
|
|
||||||
|
# For development
|
||||||
|
# html_template = "mobile.html"
|
||||||
|
|
||||||
|
LOG.debug(f"Template {html_template}")
|
||||||
|
|
||||||
|
transport, aprs_connection = _get_transport(stats)
|
||||||
|
LOG.debug(f"transport {transport} aprs_connection {aprs_connection}")
|
||||||
|
|
||||||
|
stats["transport"] = transport
|
||||||
|
stats["aprs_connection"] = aprs_connection
|
||||||
|
LOG.debug(f"initial stats = {stats}")
|
||||||
|
|
||||||
|
return flask.render_template(
|
||||||
|
html_template,
|
||||||
|
initial_stats=stats,
|
||||||
|
aprs_connection=aprs_connection,
|
||||||
|
callsign=CONF.callsign,
|
||||||
|
version=aprsd.__version__,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@auth.login_required
|
||||||
|
@flask_app.route("//send-message-status")
|
||||||
|
def send_message_status():
|
||||||
|
LOG.debug(request)
|
||||||
|
msgs = SentMessages()
|
||||||
|
info = msgs.get_all()
|
||||||
|
return json.dumps(info)
|
||||||
|
|
||||||
|
|
||||||
|
def _stats():
|
||||||
|
stats_obj = stats.APRSDStats()
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
|
||||||
|
time_format = "%m-%d-%Y %H:%M:%S"
|
||||||
|
stats_dict = stats_obj.stats()
|
||||||
|
# Webchat doesnt need these
|
||||||
|
if "watch_list" in stats_dict["aprsd"]:
|
||||||
del stats_dict["aprsd"]["watch_list"]
|
del stats_dict["aprsd"]["watch_list"]
|
||||||
|
if "seen_list" in stats_dict["aprsd"]:
|
||||||
del stats_dict["aprsd"]["seen_list"]
|
del stats_dict["aprsd"]["seen_list"]
|
||||||
# del stats_dict["email"]
|
if "threads" in stats_dict["aprsd"]:
|
||||||
# del stats_dict["plugins"]
|
del stats_dict["aprsd"]["threads"]
|
||||||
# del stats_dict["messages"]
|
# del stats_dict["email"]
|
||||||
|
# del stats_dict["plugins"]
|
||||||
|
# del stats_dict["messages"]
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"time": now.strftime(time_format),
|
"time": now.strftime(time_format),
|
||||||
"stats": stats_dict,
|
"stats": stats_dict,
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def stats(self):
|
|
||||||
return json.dumps(self._stats())
|
@flask_app.route("/stats")
|
||||||
|
def get_stats():
|
||||||
|
return json.dumps(_stats())
|
||||||
|
|
||||||
|
|
||||||
class SendMessageNamespace(Namespace):
|
class SendMessageNamespace(Namespace):
|
||||||
@@ -377,21 +389,9 @@ def setup_logging(flask_app, loglevel, quiet):
|
|||||||
|
|
||||||
@trace.trace
|
@trace.trace
|
||||||
def init_flask(loglevel, quiet):
|
def init_flask(loglevel, quiet):
|
||||||
global socketio
|
global socketio, flask_app
|
||||||
|
|
||||||
flask_app = flask.Flask(
|
|
||||||
"aprsd",
|
|
||||||
static_url_path="/static",
|
|
||||||
static_folder="web/chat/static",
|
|
||||||
template_folder="web/chat/templates",
|
|
||||||
)
|
|
||||||
setup_logging(flask_app, loglevel, quiet)
|
setup_logging(flask_app, loglevel, quiet)
|
||||||
server = WebChatFlask()
|
|
||||||
server.set_config()
|
|
||||||
flask_app.route("/", methods=["GET"])(server.index)
|
|
||||||
flask_app.route("/stats", methods=["GET"])(server.stats)
|
|
||||||
# flask_app.route("/send-message", methods=["GET"])(server.send_message)
|
|
||||||
flask_app.route("/send-message-status", methods=["GET"])(server.send_message_status)
|
|
||||||
|
|
||||||
socketio = SocketIO(
|
socketio = SocketIO(
|
||||||
flask_app, logger=False, engineio_logger=False,
|
flask_app, logger=False, engineio_logger=False,
|
||||||
@@ -407,7 +407,7 @@ def init_flask(loglevel, quiet):
|
|||||||
"/sendmsg",
|
"/sendmsg",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return socketio, flask_app
|
return socketio
|
||||||
|
|
||||||
|
|
||||||
# main() ###
|
# main() ###
|
||||||
@@ -448,6 +448,8 @@ def webchat(ctx, flush, port):
|
|||||||
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(LOG, logging.DEBUG)
|
||||||
|
user = CONF.admin.user
|
||||||
|
users[user] = generate_password_hash(CONF.admin.password)
|
||||||
|
|
||||||
# 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
|
||||||
@@ -466,7 +468,7 @@ def webchat(ctx, flush, port):
|
|||||||
packets.WatchList()
|
packets.WatchList()
|
||||||
packets.SeenList()
|
packets.SeenList()
|
||||||
|
|
||||||
(socketio, app) = init_flask(loglevel, quiet)
|
socketio = init_flask(loglevel, quiet)
|
||||||
rx_thread = rx.APRSDPluginRXThread(
|
rx_thread = rx.APRSDPluginRXThread(
|
||||||
packet_queue=threads.packet_queue,
|
packet_queue=threads.packet_queue,
|
||||||
)
|
)
|
||||||
@@ -482,8 +484,10 @@ def webchat(ctx, flush, port):
|
|||||||
keepalive.start()
|
keepalive.start()
|
||||||
LOG.info("Start socketio.run()")
|
LOG.info("Start socketio.run()")
|
||||||
socketio.run(
|
socketio.run(
|
||||||
app,
|
flask_app,
|
||||||
ssl_context="adhoc",
|
# This is broken for now after removing cryptography
|
||||||
|
# and pyopenssl
|
||||||
|
# ssl_context="adhoc",
|
||||||
host=CONF.admin.web_ip,
|
host=CONF.admin.web_ip,
|
||||||
port=port,
|
port=port,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ def set_lib_defaults():
|
|||||||
|
|
||||||
|
|
||||||
def set_log_defaults():
|
def set_log_defaults():
|
||||||
# logging.set_defaults(default_log_levels=logging.get_default_log_levels())
|
# log.set_defaults(default_log_levels=log.get_default_log_levels())
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
The options for logging setup
|
The options for log setup
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|||||||
+8
-2
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
The options for logging setup
|
The options for log setup
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ logging_opts = [
|
|||||||
cfg.BoolOpt(
|
cfg.BoolOpt(
|
||||||
"rich_logging",
|
"rich_logging",
|
||||||
default=True,
|
default=True,
|
||||||
help="Enable Rich logging",
|
help="Enable Rich log",
|
||||||
),
|
),
|
||||||
cfg.StrOpt(
|
cfg.StrOpt(
|
||||||
"logfile",
|
"logfile",
|
||||||
@@ -45,6 +45,12 @@ logging_opts = [
|
|||||||
default=DEFAULT_LOG_FORMAT,
|
default=DEFAULT_LOG_FORMAT,
|
||||||
help="Log file format, unless rich_logging enabled.",
|
help="Log file format, unless rich_logging enabled.",
|
||||||
),
|
),
|
||||||
|
cfg.StrOpt(
|
||||||
|
"log_level",
|
||||||
|
default="INFO",
|
||||||
|
choices=LOG_LEVELS.keys(),
|
||||||
|
help="Log level for logging of events.",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import sys
|
|||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
from aprsd import conf
|
from aprsd import conf
|
||||||
from aprsd.logging import rich as aprsd_logging
|
from aprsd.log import rich as aprsd_logging
|
||||||
|
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@@ -15,8 +15,8 @@ LOG = logging.getLogger("APRSD")
|
|||||||
logging_queue = queue.Queue()
|
logging_queue = queue.Queue()
|
||||||
|
|
||||||
|
|
||||||
# Setup the logging faciility
|
# Setup the log faciility
|
||||||
# to disable logging to stdout, but still log to file
|
# to disable log to stdout, but still log to file
|
||||||
# use the --quiet option on the cmdln
|
# use the --quiet option on the cmdln
|
||||||
def setup_logging(loglevel, quiet):
|
def setup_logging(loglevel, quiet):
|
||||||
log_level = conf.log.LOG_LEVELS[loglevel]
|
log_level = conf.log.LOG_LEVELS[loglevel]
|
||||||
@@ -130,7 +130,7 @@ class APRSDRichHandler(RichHandler):
|
|||||||
"""Render log for display.
|
"""Render log for display.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
record (LogRecord): logging Record.
|
record (LogRecord): log Record.
|
||||||
traceback (Optional[Traceback]): Traceback instance or None for no Traceback.
|
traceback (Optional[Traceback]): Traceback instance or None for no Traceback.
|
||||||
message_renderable (ConsoleRenderable): Renderable (typically Text) containing log message contents.
|
message_renderable (ConsoleRenderable): Renderable (typically Text) containing log message contents.
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ from aprsd import cli_helper, packets, stats, threads, utils
|
|||||||
|
|
||||||
|
|
||||||
# setup the global logger
|
# setup the global logger
|
||||||
# logging.basicConfig(level=logging.DEBUG) # level=10
|
# log.basicConfig(level=log.DEBUG) # level=10
|
||||||
CONF = cfg.CONF
|
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"])
|
||||||
@@ -70,8 +70,8 @@ def main():
|
|||||||
# First import all the possible commands for the CLI
|
# First import all the possible commands for the CLI
|
||||||
# The commands themselves live in the cmds directory
|
# The commands themselves live in the cmds directory
|
||||||
from .cmds import ( # noqa
|
from .cmds import ( # noqa
|
||||||
completion, dev, healthcheck, list_plugins, listen, send_message,
|
completion, dev, fetch_stats, healthcheck, list_plugins, listen,
|
||||||
server, webchat,
|
send_message, server, webchat,
|
||||||
)
|
)
|
||||||
cli(auto_envvar_prefix="APRSD")
|
cli(auto_envvar_prefix="APRSD")
|
||||||
|
|
||||||
+141
-48
@@ -25,6 +25,7 @@ PACKET_TYPE_OBJECT = "object"
|
|||||||
PACKET_TYPE_UNKNOWN = "unknown"
|
PACKET_TYPE_UNKNOWN = "unknown"
|
||||||
PACKET_TYPE_STATUS = "status"
|
PACKET_TYPE_STATUS = "status"
|
||||||
PACKET_TYPE_BEACON = "beacon"
|
PACKET_TYPE_BEACON = "beacon"
|
||||||
|
PACKET_TYPE_THIRDPARTY = "thirdparty"
|
||||||
PACKET_TYPE_UNCOMPRESSED = "uncompressed"
|
PACKET_TYPE_UNCOMPRESSED = "uncompressed"
|
||||||
|
|
||||||
|
|
||||||
@@ -57,6 +58,8 @@ class Packet(metaclass=abc.ABCMeta):
|
|||||||
# or holds the raw string from input packet
|
# or holds the raw string from input packet
|
||||||
raw: str = None
|
raw: str = None
|
||||||
raw_dict: dict = field(repr=False, default_factory=lambda: {})
|
raw_dict: dict = field(repr=False, default_factory=lambda: {})
|
||||||
|
# Built by calling prepare(). raw needs this built first.
|
||||||
|
payload: str = None
|
||||||
|
|
||||||
# Fields related to sending packets out
|
# Fields related to sending packets out
|
||||||
send_count: int = field(repr=False, default=0)
|
send_count: int = field(repr=False, default=0)
|
||||||
@@ -92,14 +95,28 @@ class Packet(metaclass=abc.ABCMeta):
|
|||||||
def prepare(self):
|
def prepare(self):
|
||||||
"""Do stuff here that is needed prior to sending over the air."""
|
"""Do stuff here that is needed prior to sending over the air."""
|
||||||
# now build the raw message for sending
|
# now build the raw message for sending
|
||||||
|
self._build_payload()
|
||||||
self._build_raw()
|
self._build_raw()
|
||||||
|
|
||||||
|
def _build_payload(self):
|
||||||
|
"""The payload is the non headers portion of the packet."""
|
||||||
|
msg = self._filter_for_send().rstrip("\n")
|
||||||
|
self.payload = (
|
||||||
|
f":{self.to_call.ljust(9)}"
|
||||||
|
f":{msg}"
|
||||||
|
)
|
||||||
|
|
||||||
def _build_raw(self):
|
def _build_raw(self):
|
||||||
"""Build the self.raw string which is what is sent over the air."""
|
"""Build the self.raw which is what is sent over the air."""
|
||||||
self.raw = self._filter_for_send().rstrip("\n")
|
self.raw = "{}>APZ100:{}".format(
|
||||||
|
self.from_call,
|
||||||
|
self.payload,
|
||||||
|
)
|
||||||
|
LOG.debug(f"_build_raw: payload '{self.payload}' raw '{self.raw}'")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def factory(raw_packet):
|
def factory(raw_packet):
|
||||||
|
"""Factory method to create a packet from a raw packet string."""
|
||||||
raw = raw_packet
|
raw = raw_packet
|
||||||
raw["raw_dict"] = raw.copy()
|
raw["raw_dict"] = raw.copy()
|
||||||
translate_fields = {
|
translate_fields = {
|
||||||
@@ -118,6 +135,19 @@ class Packet(metaclass=abc.ABCMeta):
|
|||||||
packet_type = get_packet_type(raw)
|
packet_type = get_packet_type(raw)
|
||||||
raw["packet_type"] = packet_type
|
raw["packet_type"] = packet_type
|
||||||
class_name = TYPE_LOOKUP[packet_type]
|
class_name = TYPE_LOOKUP[packet_type]
|
||||||
|
if packet_type == PACKET_TYPE_THIRDPARTY:
|
||||||
|
# We have an encapsulated packet!
|
||||||
|
# So we need to decode it and return the inner packet
|
||||||
|
# as the packet we are going to process.
|
||||||
|
# This is a recursive call to the factory
|
||||||
|
subpacket_raw = raw["subpacket"]
|
||||||
|
subpacket = Packet.factory(subpacket_raw)
|
||||||
|
del raw["subpacket"]
|
||||||
|
# raw["subpacket"] = subpacket
|
||||||
|
packet = dacite.from_dict(data_class=class_name, data=raw)
|
||||||
|
packet.subpacket = subpacket
|
||||||
|
return packet
|
||||||
|
|
||||||
if packet_type == PACKET_TYPE_UNKNOWN:
|
if packet_type == PACKET_TYPE_UNKNOWN:
|
||||||
# Try and figure it out here
|
# Try and figure it out here
|
||||||
if "latitude" in raw:
|
if "latitude" in raw:
|
||||||
@@ -126,9 +156,43 @@ class Packet(metaclass=abc.ABCMeta):
|
|||||||
if packet_type == PACKET_TYPE_WX:
|
if packet_type == PACKET_TYPE_WX:
|
||||||
# the weather information is in a dict
|
# the weather information is in a dict
|
||||||
# this brings those values out to the outer dict
|
# this brings those values out to the outer dict
|
||||||
|
|
||||||
for key in raw["weather"]:
|
for key in raw["weather"]:
|
||||||
raw[key] = raw["weather"][key]
|
raw[key] = raw["weather"][key]
|
||||||
|
|
||||||
|
# If we have the broken aprslib, then we need to
|
||||||
|
# Convert the course and speed to wind_speed and wind_direction
|
||||||
|
# aprslib issue #80
|
||||||
|
# https://github.com/rossengeorgiev/aprs-python/issues/80
|
||||||
|
# Wind speed and course is option in the SPEC.
|
||||||
|
# For some reason aprslib multiplies the speed by 1.852.
|
||||||
|
if "wind_speed" not in raw and "wind_direction" not in raw:
|
||||||
|
# Most likely this is the broken aprslib
|
||||||
|
# So we need to convert the wind_gust speed
|
||||||
|
raw["wind_gust"] = round(raw.get("wind_gust", 0) / 0.44704, 3)
|
||||||
|
if "wind_speed" not in raw:
|
||||||
|
wind_speed = raw.get("speed")
|
||||||
|
if wind_speed:
|
||||||
|
raw["wind_speed"] = round(wind_speed / 1.852, 3)
|
||||||
|
raw["weather"]["wind_speed"] = raw["wind_speed"]
|
||||||
|
if "speed" in raw:
|
||||||
|
del raw["speed"]
|
||||||
|
# Let's adjust the rain numbers as well, since it's wrong
|
||||||
|
raw["rain_1h"] = round((raw.get("rain_1h", 0) / .254) * .01, 3)
|
||||||
|
raw["weather"]["rain_1h"] = raw["rain_1h"]
|
||||||
|
raw["rain_24h"] = round((raw.get("rain_24h", 0) / .254) * .01, 3)
|
||||||
|
raw["weather"]["rain_24h"] = raw["rain_24h"]
|
||||||
|
raw["rain_since_midnight"] = round((raw.get("rain_since_midnight", 0) / .254) * .01, 3)
|
||||||
|
raw["weather"]["rain_since_midnight"] = raw["rain_since_midnight"]
|
||||||
|
|
||||||
|
if "wind_direction" not in raw:
|
||||||
|
wind_direction = raw.get("course")
|
||||||
|
if wind_direction:
|
||||||
|
raw["wind_direction"] = wind_direction
|
||||||
|
raw["weather"]["wind_direction"] = raw["wind_direction"]
|
||||||
|
if "course" in raw:
|
||||||
|
del raw["course"]
|
||||||
|
|
||||||
return dacite.from_dict(data_class=class_name, data=raw)
|
return dacite.from_dict(data_class=class_name, data=raw)
|
||||||
|
|
||||||
def log(self, header=None):
|
def log(self, header=None):
|
||||||
@@ -199,7 +263,7 @@ class PathPacket(Packet):
|
|||||||
path: List[str] = field(default_factory=list)
|
path: List[str] = field(default_factory=list)
|
||||||
via: str = None
|
via: str = None
|
||||||
|
|
||||||
def _build_raw(self):
|
def _build_payload(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
@@ -211,13 +275,8 @@ class AckPacket(PathPacket):
|
|||||||
if self.response:
|
if self.response:
|
||||||
LOG.warning("Response set!")
|
LOG.warning("Response set!")
|
||||||
|
|
||||||
def _build_raw(self):
|
def _build_payload(self):
|
||||||
"""Build the self.raw which is what is sent over the air."""
|
self.payload = f":{self.to_call.ljust(9)}:ack{self.msgNo}"
|
||||||
self.raw = "{}>APZ100::{}:ack{}".format(
|
|
||||||
self.from_call,
|
|
||||||
self.to_call.ljust(9),
|
|
||||||
self.msgNo,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -228,13 +287,8 @@ class RejectPacket(PathPacket):
|
|||||||
if self.response:
|
if self.response:
|
||||||
LOG.warning("Response set!")
|
LOG.warning("Response set!")
|
||||||
|
|
||||||
def _build_raw(self):
|
def _build_payload(self):
|
||||||
"""Build the self.raw which is what is sent over the air."""
|
self.payload = f":{self.to_call.ljust(9)} :rej{self.msgNo}"
|
||||||
self.raw = "{}>APZ100::{} :rej{}".format(
|
|
||||||
self.from_call,
|
|
||||||
self.to_call.ljust(9),
|
|
||||||
self.msgNo,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -251,10 +305,8 @@ class MessagePacket(PathPacket):
|
|||||||
# We all miss George Carlin
|
# We all miss George Carlin
|
||||||
return re.sub("fuck|shit|cunt|piss|cock|bitch", "****", message)
|
return re.sub("fuck|shit|cunt|piss|cock|bitch", "****", message)
|
||||||
|
|
||||||
def _build_raw(self):
|
def _build_payload(self):
|
||||||
"""Build the self.raw which is what is sent over the air."""
|
self.payload = ":{}:{}{{{}".format(
|
||||||
self.raw = "{}>APZ100::{}:{}{{{}".format(
|
|
||||||
self.from_call,
|
|
||||||
self.to_call.ljust(9),
|
self.to_call.ljust(9),
|
||||||
self._filter_for_send().rstrip("\n"),
|
self._filter_for_send().rstrip("\n"),
|
||||||
str(self.msgNo),
|
str(self.msgNo),
|
||||||
@@ -267,7 +319,7 @@ class StatusPacket(PathPacket):
|
|||||||
messagecapable: bool = False
|
messagecapable: bool = False
|
||||||
comment: str = None
|
comment: str = None
|
||||||
|
|
||||||
def _build_raw(self):
|
def _build_payload(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
@@ -281,10 +333,6 @@ class GPSPacket(PathPacket):
|
|||||||
comment: str = None
|
comment: str = None
|
||||||
symbol: str = field(default="l")
|
symbol: str = field(default="l")
|
||||||
symbol_table: str = field(default="/")
|
symbol_table: str = field(default="/")
|
||||||
# in MPH
|
|
||||||
speed: float = 0.00
|
|
||||||
# 0 to 360
|
|
||||||
course: int = 0
|
|
||||||
|
|
||||||
def decdeg2dms(self, degrees_decimal):
|
def decdeg2dms(self, degrees_decimal):
|
||||||
is_positive = degrees_decimal >= 0
|
is_positive = degrees_decimal >= 0
|
||||||
@@ -370,16 +418,24 @@ class GPSPacket(PathPacket):
|
|||||||
time_zulu = result_utc_datetime.strftime("%d%H%M")
|
time_zulu = result_utc_datetime.strftime("%d%H%M")
|
||||||
return time_zulu
|
return time_zulu
|
||||||
|
|
||||||
def _build_raw(self):
|
def _build_payload(self):
|
||||||
|
"""The payload is the non headers portion of the packet."""
|
||||||
time_zulu = self._build_time_zulu()
|
time_zulu = self._build_time_zulu()
|
||||||
|
lat = self.latitude
|
||||||
|
long = self.longitude
|
||||||
|
self.payload = (
|
||||||
|
f"@{time_zulu}z{lat}{self.symbol_table}"
|
||||||
|
f"{long}{self.symbol}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.comment:
|
||||||
|
self.payload = f"{self.payload}{self.comment}"
|
||||||
|
|
||||||
|
def _build_raw(self):
|
||||||
self.raw = (
|
self.raw = (
|
||||||
f"{self.from_call}>{self.to_call},WIDE2-1:"
|
f"{self.from_call}>{self.to_call},WIDE2-1:"
|
||||||
f"@{time_zulu}z{self.latitude}{self.symbol_table}"
|
f"{self.payload}"
|
||||||
f"{self.longitude}{self.symbol}"
|
|
||||||
)
|
)
|
||||||
if self.comment:
|
|
||||||
self.raw = f"{self.raw}{self.comment}"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -387,8 +443,12 @@ class MicEPacket(GPSPacket):
|
|||||||
messagecapable: bool = False
|
messagecapable: bool = False
|
||||||
mbits: str = None
|
mbits: str = None
|
||||||
mtype: str = None
|
mtype: str = None
|
||||||
|
# in MPH
|
||||||
|
speed: float = 0.00
|
||||||
|
# 0 to 360
|
||||||
|
course: int = 0
|
||||||
|
|
||||||
def _build_raw(self):
|
def _build_payload(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
@@ -397,6 +457,23 @@ class ObjectPacket(GPSPacket):
|
|||||||
alive: bool = True
|
alive: bool = True
|
||||||
raw_timestamp: str = None
|
raw_timestamp: str = None
|
||||||
symbol: str = field(default="r")
|
symbol: str = field(default="r")
|
||||||
|
# in MPH
|
||||||
|
speed: float = 0.00
|
||||||
|
# 0 to 360
|
||||||
|
course: int = 0
|
||||||
|
|
||||||
|
def _build_payload(self):
|
||||||
|
time_zulu = self._build_time_zulu()
|
||||||
|
lat = self.convert_latitude(self.latitude)
|
||||||
|
long = self.convert_longitude(self.longitude)
|
||||||
|
|
||||||
|
self.payload = (
|
||||||
|
f"*{time_zulu}z{lat}{self.symbol_table}"
|
||||||
|
f"{long}{self.symbol}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.comment:
|
||||||
|
self.payload = f"{self.payload}{self.comment}"
|
||||||
|
|
||||||
def _build_raw(self):
|
def _build_raw(self):
|
||||||
"""
|
"""
|
||||||
@@ -408,22 +485,18 @@ class ObjectPacket(GPSPacket):
|
|||||||
callsign is the station callsign for the object
|
callsign is the station callsign for the object
|
||||||
The frequency, uplink_tone, offset is part of the comment
|
The frequency, uplink_tone, offset is part of the comment
|
||||||
"""
|
"""
|
||||||
time_zulu = self._build_time_zulu()
|
|
||||||
lat = self.convert_latitude(self.latitude)
|
|
||||||
long = self.convert_longitude(self.longitude)
|
|
||||||
|
|
||||||
self.raw = (
|
self.raw = (
|
||||||
f"{self.from_call}>APZ100:;{self.to_call:9s}"
|
f"{self.from_call}>APZ100:;{self.to_call:9s}"
|
||||||
f"*{time_zulu}z{lat}{self.symbol_table}"
|
f"{self.payload}"
|
||||||
f"{long}{self.symbol}"
|
|
||||||
)
|
)
|
||||||
if self.comment:
|
|
||||||
self.raw = f"{self.raw}{self.comment}"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass()
|
@dataclass()
|
||||||
class WeatherPacket(GPSPacket):
|
class WeatherPacket(GPSPacket):
|
||||||
symbol: str = "_"
|
symbol: str = "_"
|
||||||
|
wind_speed: float = 0.00
|
||||||
|
wind_direction: int = 0
|
||||||
wind_gust: float = 0.00
|
wind_gust: float = 0.00
|
||||||
temperature: float = 0.00
|
temperature: float = 0.00
|
||||||
# in inches. 1.04 means 1.04 inches
|
# in inches. 1.04 means 1.04 inches
|
||||||
@@ -434,7 +507,7 @@ class WeatherPacket(GPSPacket):
|
|||||||
pressure: float = 0.00
|
pressure: float = 0.00
|
||||||
comment: str = None
|
comment: str = None
|
||||||
|
|
||||||
def _build_raw(self):
|
def _build_payload(self):
|
||||||
"""Build an uncompressed weather packet
|
"""Build an uncompressed weather packet
|
||||||
|
|
||||||
Format =
|
Format =
|
||||||
@@ -461,16 +534,12 @@ class WeatherPacket(GPSPacket):
|
|||||||
"""
|
"""
|
||||||
time_zulu = self._build_time_zulu()
|
time_zulu = self._build_time_zulu()
|
||||||
|
|
||||||
course = "%03u" % self.course
|
|
||||||
|
|
||||||
contents = [
|
contents = [
|
||||||
f"{self.from_call}>{self.to_call},WIDE1-1,WIDE2-1:",
|
|
||||||
f"@{time_zulu}z{self.latitude}{self.symbol_table}",
|
f"@{time_zulu}z{self.latitude}{self.symbol_table}",
|
||||||
f"{self.longitude}{self.symbol}",
|
f"{self.longitude}{self.symbol}",
|
||||||
# Add CSE = Course
|
f"{self.wind_direction:03d}",
|
||||||
f"{course}",
|
|
||||||
# Speed = sustained 1 minute wind speed in mph
|
# Speed = sustained 1 minute wind speed in mph
|
||||||
f"{self.symbol_table}", f"{self.speed:03.0f}",
|
f"{self.symbol_table}", f"{self.wind_speed:03.0f}",
|
||||||
# wind gust (peak wind speed in mph in the last 5 minutes)
|
# wind gust (peak wind speed in mph in the last 5 minutes)
|
||||||
f"g{self.wind_gust:03.0f}",
|
f"g{self.wind_gust:03.0f}",
|
||||||
# Temperature in degrees F
|
# Temperature in degrees F
|
||||||
@@ -486,11 +555,32 @@ class WeatherPacket(GPSPacket):
|
|||||||
# Barometric pressure (in tenths of millibars/tenths of hPascal)
|
# Barometric pressure (in tenths of millibars/tenths of hPascal)
|
||||||
f"b{self.pressure:05.0f}",
|
f"b{self.pressure:05.0f}",
|
||||||
]
|
]
|
||||||
|
|
||||||
if self.comment:
|
if self.comment:
|
||||||
contents.append(self.comment)
|
contents.append(self.comment)
|
||||||
|
self.payload = "".join(contents)
|
||||||
|
|
||||||
self.raw = "".join(contents)
|
def _build_raw(self):
|
||||||
|
|
||||||
|
self.raw = (
|
||||||
|
f"{self.from_call}>{self.to_call},WIDE1-1,WIDE2-1:"
|
||||||
|
f"{self.payload}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ThirdParty(Packet):
|
||||||
|
# Holds the encapsulated packet
|
||||||
|
subpacket: Packet = None
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
"""Build the repr version of the packet."""
|
||||||
|
repr_str = (
|
||||||
|
f"{self.__class__.__name__}:"
|
||||||
|
f" From: {self.from_call} "
|
||||||
|
f" To: {self.to_call} "
|
||||||
|
f" Subpacket: {repr(self.subpacket)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return repr_str
|
||||||
|
|
||||||
|
|
||||||
TYPE_LOOKUP = {
|
TYPE_LOOKUP = {
|
||||||
@@ -503,6 +593,7 @@ TYPE_LOOKUP = {
|
|||||||
PACKET_TYPE_STATUS: StatusPacket,
|
PACKET_TYPE_STATUS: StatusPacket,
|
||||||
PACKET_TYPE_BEACON: GPSPacket,
|
PACKET_TYPE_BEACON: GPSPacket,
|
||||||
PACKET_TYPE_UNKNOWN: Packet,
|
PACKET_TYPE_UNKNOWN: Packet,
|
||||||
|
PACKET_TYPE_THIRDPARTY: ThirdParty,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -529,6 +620,8 @@ def get_packet_type(packet: dict):
|
|||||||
elif pkt_format == PACKET_TYPE_UNCOMPRESSED:
|
elif pkt_format == PACKET_TYPE_UNCOMPRESSED:
|
||||||
if packet.get("symbol", None) == "_":
|
if packet.get("symbol", None) == "_":
|
||||||
packet_type = PACKET_TYPE_WX
|
packet_type = PACKET_TYPE_WX
|
||||||
|
elif pkt_format == PACKET_TYPE_THIRDPARTY:
|
||||||
|
packet_type = PACKET_TYPE_THIRDPARTY
|
||||||
return packet_type
|
return packet_type
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ def get_weather_gov_for_gps(lat, lon):
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
url2 = (
|
url2 = (
|
||||||
# "https://forecast.weather.gov/MapClick.php?lat=%s"
|
"https://forecast.weather.gov/MapClick.php?lat=%s"
|
||||||
# "&lon=%s&FcstType=json" % (lat, lon)
|
"&lon=%s&FcstType=json" % (lat, lon)
|
||||||
f"https://api.weather.gov/points/{lat},{lon}"
|
# f"https://api.weather.gov/points/{lat},{lon}"
|
||||||
)
|
)
|
||||||
LOG.debug(f"Fetching weather '{url2}'")
|
LOG.debug(f"Fetching weather '{url2}'")
|
||||||
response = requests.get(url2, headers=headers)
|
response = requests.get(url2, headers=headers)
|
||||||
|
|||||||
@@ -666,7 +666,7 @@ class APRSDEmailThread(threads.APRSDThread):
|
|||||||
EmailInfo().delay = 60
|
EmailInfo().delay = 60
|
||||||
|
|
||||||
# reset clock
|
# reset clock
|
||||||
LOG.debug("Done looping over Server.fetch, logging out.")
|
LOG.debug("Done looping over Server.fetch, log out.")
|
||||||
self.past = datetime.datetime.now()
|
self.past = datetime.datetime.now()
|
||||||
try:
|
try:
|
||||||
server.logout()
|
server.logout()
|
||||||
|
|||||||
+26
-15
@@ -2,6 +2,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
from geopy.geocoders import Nominatim
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
from aprsd import packets, plugin, plugin_utils
|
from aprsd import packets, plugin, plugin_utils
|
||||||
@@ -50,8 +51,28 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
|||||||
LOG.error("Didn't get any entries from aprs.fi")
|
LOG.error("Didn't get any entries from aprs.fi")
|
||||||
return "Failed to fetch aprs.fi location"
|
return "Failed to fetch aprs.fi location"
|
||||||
|
|
||||||
lat = aprs_data["entries"][0]["lat"]
|
lat = float(aprs_data["entries"][0]["lat"])
|
||||||
lon = aprs_data["entries"][0]["lng"]
|
lon = float(aprs_data["entries"][0]["lng"])
|
||||||
|
|
||||||
|
# Get some information about their location
|
||||||
|
try:
|
||||||
|
tic = time.perf_counter()
|
||||||
|
geolocator = Nominatim(user_agent="APRSD")
|
||||||
|
coordinates = f"{lat:0.6f}, {lon:0.6f}"
|
||||||
|
location = geolocator.reverse(coordinates)
|
||||||
|
address = location.raw.get("address")
|
||||||
|
toc = time.perf_counter()
|
||||||
|
if address:
|
||||||
|
LOG.info(f"Geopy address {address} took {toc - tic:0.4f}")
|
||||||
|
if address.get("country_code") == "us":
|
||||||
|
area_info = f"{address.get('county')}, {address.get('state')}"
|
||||||
|
else:
|
||||||
|
# what to do for address for non US?
|
||||||
|
area_info = f"{address.get('country'), 'Unknown'}"
|
||||||
|
except Exception as ex:
|
||||||
|
LOG.error(f"Failed to fetch Geopy address {ex}")
|
||||||
|
area_info = "Unknown Location"
|
||||||
|
|
||||||
try: # altitude not always provided
|
try: # altitude not always provided
|
||||||
alt = float(aprs_data["entries"][0]["altitude"])
|
alt = float(aprs_data["entries"][0]["altitude"])
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -64,22 +85,12 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
|||||||
delta_seconds = time.time() - int(aprs_lasttime_seconds)
|
delta_seconds = time.time() - int(aprs_lasttime_seconds)
|
||||||
delta_hours = delta_seconds / 60 / 60
|
delta_hours = delta_seconds / 60 / 60
|
||||||
|
|
||||||
try:
|
|
||||||
wx_data = plugin_utils.get_weather_gov_for_gps(lat, lon)
|
|
||||||
except Exception as ex:
|
|
||||||
LOG.error(f"Couldn't fetch forecast.weather.gov '{ex}'")
|
|
||||||
wx_data = {"location": {"areaDescription": "Unknown Location"}}
|
|
||||||
|
|
||||||
if "location" not in wx_data:
|
|
||||||
LOG.error(f"Couldn't fetch forecast.weather.gov '{wx_data}'")
|
|
||||||
wx_data = {"location": {"areaDescription": "Unknown Location"}}
|
|
||||||
|
|
||||||
reply = "{}: {} {}' {},{} {}h ago".format(
|
reply = "{}: {} {}' {},{} {}h ago".format(
|
||||||
searchcall,
|
searchcall,
|
||||||
wx_data["location"]["areaDescription"],
|
area_info,
|
||||||
str(altfeet),
|
str(altfeet),
|
||||||
str(lat),
|
f"{lat:0.2f}",
|
||||||
str(lon),
|
f"{lon:0.2f}",
|
||||||
str("%.1f" % round(delta_hours, 1)),
|
str("%.1f" % round(delta_hours, 1)),
|
||||||
).rstrip()
|
).rstrip()
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
|
|||||||
"weather" - returns weather near the calling callsign
|
"weather" - returns weather near the calling callsign
|
||||||
"""
|
"""
|
||||||
|
|
||||||
command_regex = r"^([w]|[w]\s|[w][x]|[w][x]\s|weather)"
|
command_regex = r"^([w][x]|[w][x]\s|weather)"
|
||||||
command_name = "USWeather"
|
command_name = "USWeather"
|
||||||
short_description = "Provide USA only weather of GPS Beacon location"
|
short_description = "Provide USA only weather of GPS Beacon location"
|
||||||
|
|
||||||
@@ -37,11 +37,18 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
|
|||||||
def process(self, packet):
|
def process(self, packet):
|
||||||
LOG.info("Weather Plugin")
|
LOG.info("Weather Plugin")
|
||||||
fromcall = packet.from_call
|
fromcall = packet.from_call
|
||||||
|
message = packet.get("message_text", None)
|
||||||
# message = packet.get("message_text", None)
|
# message = packet.get("message_text", None)
|
||||||
# ack = packet.get("msgNo", "0")
|
# ack = packet.get("msgNo", "0")
|
||||||
|
a = re.search(r"^.*\s+(.*)", message)
|
||||||
|
if a is not None:
|
||||||
|
searchcall = a.group(1)
|
||||||
|
searchcall = searchcall.upper()
|
||||||
|
else:
|
||||||
|
searchcall = fromcall
|
||||||
api_key = CONF.aprs_fi.apiKey
|
api_key = CONF.aprs_fi.apiKey
|
||||||
try:
|
try:
|
||||||
aprs_data = plugin_utils.get_aprs_fi(api_key, fromcall)
|
aprs_data = plugin_utils.get_aprs_fi(api_key, searchcall)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
LOG.error(f"Failed to fetch aprs.fi data {ex}")
|
LOG.error(f"Failed to fetch aprs.fi data {ex}")
|
||||||
return "Failed to fetch aprs.fi location"
|
return "Failed to fetch aprs.fi location"
|
||||||
@@ -92,7 +99,7 @@ class USMetarPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
command_regex = r"^([m]|[m]|[m]\s|metar)"
|
command_regex = r"^([m]|[M]|[m]\s|metar)"
|
||||||
command_name = "USMetar"
|
command_name = "USMetar"
|
||||||
short_description = "USA only METAR of GPS Beacon location"
|
short_description = "USA only METAR of GPS Beacon location"
|
||||||
|
|
||||||
@@ -182,7 +189,7 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
command_regex = r"^([w]|[w]\s|[w][x]|[w][x]\s|weather)"
|
command_regex = r"^([w][x]|[w][x]\s|weather)"
|
||||||
command_name = "OpenWeatherMap"
|
command_name = "OpenWeatherMap"
|
||||||
short_description = "OpenWeatherMap weather of GPS Beacon location"
|
short_description = "OpenWeatherMap weather of GPS Beacon location"
|
||||||
|
|
||||||
|
|||||||
+30
-15
@@ -16,31 +16,47 @@ class RPCClient:
|
|||||||
_instance = None
|
_instance = None
|
||||||
_rpc_client = None
|
_rpc_client = None
|
||||||
|
|
||||||
|
ip = None
|
||||||
|
port = None
|
||||||
|
magic_word = None
|
||||||
|
|
||||||
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 __init__(self):
|
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._check_settings()
|
||||||
self.get_rpc_client()
|
self.get_rpc_client()
|
||||||
|
|
||||||
def _check_settings(self):
|
def _check_settings(self):
|
||||||
if not CONF.rpc_settings.enabled:
|
if not CONF.rpc_settings.enabled:
|
||||||
LOG.error("RPC is not enabled, no way to get stats!!")
|
LOG.warning("RPC is not enabled, no way to get stats!!")
|
||||||
|
|
||||||
if CONF.rpc_settings.magic_word == conf.common.APRSD_DEFAULT_MAGIC_WORD:
|
if self.magic_word == conf.common.APRSD_DEFAULT_MAGIC_WORD:
|
||||||
LOG.warning("You are using the default RPC magic word!!!")
|
LOG.warning("You are using the default RPC magic word!!!")
|
||||||
LOG.warning("edit aprsd.conf and change rpc_settings.magic_word")
|
LOG.warning("edit aprsd.conf and change rpc_settings.magic_word")
|
||||||
|
|
||||||
def _rpyc_connect(
|
LOG.debug(f"RPC Client: {self.ip}:{self.port} {self.magic_word}")
|
||||||
self, host, port,
|
|
||||||
service=rpyc.VoidService,
|
|
||||||
config={}, ipv6=False,
|
|
||||||
keepalive=False, authorizer=None,
|
|
||||||
):
|
|
||||||
|
|
||||||
print(f"Connecting to RPC host {host}:{port}")
|
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:
|
try:
|
||||||
s = rpc.AuthSocketStream.connect(
|
s = rpc.AuthSocketStream.connect(
|
||||||
host, port, ipv6=ipv6, keepalive=keepalive,
|
host, port, ipv6=ipv6, keepalive=keepalive,
|
||||||
@@ -48,16 +64,15 @@ class RPCClient:
|
|||||||
)
|
)
|
||||||
return rpyc.utils.factory.connect_stream(s, service, config=config)
|
return rpyc.utils.factory.connect_stream(s, service, config=config)
|
||||||
except ConnectionRefusedError:
|
except ConnectionRefusedError:
|
||||||
LOG.error(f"Failed to connect to RPC host {host}")
|
LOG.error(f"Failed to connect to RPC host '{host}:{port}'")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_rpc_client(self):
|
def get_rpc_client(self):
|
||||||
if not self._rpc_client:
|
if not self._rpc_client:
|
||||||
magic = CONF.rpc_settings.magic_word
|
|
||||||
self._rpc_client = self._rpyc_connect(
|
self._rpc_client = self._rpyc_connect(
|
||||||
CONF.rpc_settings.ip,
|
self.ip,
|
||||||
CONF.rpc_settings.port,
|
self.port,
|
||||||
authorizer=lambda sock: sock.send(magic.encode()),
|
authorizer=lambda sock: sock.send(self.magic_word.encode()),
|
||||||
)
|
)
|
||||||
return self._rpc_client
|
return self._rpc_client
|
||||||
|
|
||||||
|
|||||||
+14
-3
@@ -18,7 +18,10 @@ LOG = logging.getLogger("APRSD")
|
|||||||
def magic_word_authenticator(sock):
|
def magic_word_authenticator(sock):
|
||||||
magic = sock.recv(len(CONF.rpc_settings.magic_word)).decode()
|
magic = sock.recv(len(CONF.rpc_settings.magic_word)).decode()
|
||||||
if magic != CONF.rpc_settings.magic_word:
|
if magic != CONF.rpc_settings.magic_word:
|
||||||
raise AuthenticationError(f"wrong magic word {magic}")
|
raise AuthenticationError(
|
||||||
|
f"wrong magic word passed in '{magic}'"
|
||||||
|
f" != '{CONF.rpc_settings.magic_word}'",
|
||||||
|
)
|
||||||
return sock, None
|
return sock, None
|
||||||
|
|
||||||
|
|
||||||
@@ -60,32 +63,40 @@ class APRSDService(rpyc.Service):
|
|||||||
|
|
||||||
@rpyc.exposed
|
@rpyc.exposed
|
||||||
def get_stats(self):
|
def get_stats(self):
|
||||||
|
LOG.info("get_stats")
|
||||||
stat = stats.APRSDStats()
|
stat = stats.APRSDStats()
|
||||||
stats_dict = stat.stats()
|
stats_dict = stat.stats()
|
||||||
LOG.debug(stats_dict)
|
return_str = json.dumps(stats_dict, indent=4, sort_keys=True, default=str)
|
||||||
return json.dumps(stats_dict, indent=4, sort_keys=True, default=str)
|
LOG.info(f"get_stats: return size {len(return_str)}")
|
||||||
|
return return_str
|
||||||
|
|
||||||
@rpyc.exposed
|
@rpyc.exposed
|
||||||
def get_stats_obj(self):
|
def get_stats_obj(self):
|
||||||
|
LOG.info("get_stats_obj")
|
||||||
return stats.APRSDStats()
|
return stats.APRSDStats()
|
||||||
|
|
||||||
@rpyc.exposed
|
@rpyc.exposed
|
||||||
def get_packet_list(self):
|
def get_packet_list(self):
|
||||||
|
LOG.info("get_packet_list")
|
||||||
return packets.PacketList()
|
return packets.PacketList()
|
||||||
|
|
||||||
@rpyc.exposed
|
@rpyc.exposed
|
||||||
def get_packet_track(self):
|
def get_packet_track(self):
|
||||||
|
LOG.info("get_packet_track")
|
||||||
return packets.PacketTrack()
|
return packets.PacketTrack()
|
||||||
|
|
||||||
@rpyc.exposed
|
@rpyc.exposed
|
||||||
def get_watch_list(self):
|
def get_watch_list(self):
|
||||||
|
LOG.info("get_watch_list")
|
||||||
return packets.WatchList()
|
return packets.WatchList()
|
||||||
|
|
||||||
@rpyc.exposed
|
@rpyc.exposed
|
||||||
def get_seen_list(self):
|
def get_seen_list(self):
|
||||||
|
LOG.info("get_seen_list")
|
||||||
return packets.SeenList()
|
return packets.SeenList()
|
||||||
|
|
||||||
@rpyc.exposed
|
@rpyc.exposed
|
||||||
def get_log_entries(self):
|
def get_log_entries(self):
|
||||||
|
LOG.info("get_log_entries")
|
||||||
entries = log_monitor.LogEntries().get_all_and_purge()
|
entries = log_monitor.LogEntries().get_all_and_purge()
|
||||||
return json.dumps(entries, default=str)
|
return json.dumps(entries, default=str)
|
||||||
|
|||||||
+17
-4
@@ -29,6 +29,8 @@ class APRSDStats:
|
|||||||
_mem_current = 0
|
_mem_current = 0
|
||||||
_mem_peak = 0
|
_mem_peak = 0
|
||||||
|
|
||||||
|
_thread_info = {}
|
||||||
|
|
||||||
_pkt_cnt = {
|
_pkt_cnt = {
|
||||||
"Packet": {
|
"Packet": {
|
||||||
"tx": 0,
|
"tx": 0,
|
||||||
@@ -95,6 +97,15 @@ class APRSDStats:
|
|||||||
def set_memory_peak(self, memory):
|
def set_memory_peak(self, memory):
|
||||||
self._mem_peak = 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)
|
@wrapt.synchronized(lock)
|
||||||
@property
|
@property
|
||||||
def aprsis_server(self):
|
def aprsis_server(self):
|
||||||
@@ -207,6 +218,7 @@ class APRSDStats:
|
|||||||
"memory_current_str": utils.human_size(self.memory),
|
"memory_current_str": utils.human_size(self.memory),
|
||||||
"memory_peak": int(self.memory_peak),
|
"memory_peak": int(self.memory_peak),
|
||||||
"memory_peak_str": utils.human_size(self.memory_peak),
|
"memory_peak_str": utils.human_size(self.memory_peak),
|
||||||
|
"threads": self._thread_info,
|
||||||
"watch_list": wl.get_all(),
|
"watch_list": wl.get_all(),
|
||||||
"seen_list": sl.get_all(),
|
"seen_list": sl.get_all(),
|
||||||
},
|
},
|
||||||
@@ -216,9 +228,10 @@ class APRSDStats:
|
|||||||
"last_update": last_aprsis_keepalive,
|
"last_update": last_aprsis_keepalive,
|
||||||
},
|
},
|
||||||
"packets": {
|
"packets": {
|
||||||
"tracked": int(pl.total_tx() + pl.total_rx()),
|
"total_tracked": int(pl.total_tx() + pl.total_rx()),
|
||||||
"sent": int(pl.total_tx()),
|
"total_sent": int(pl.total_tx()),
|
||||||
"received": int(pl.total_rx()),
|
"total_received": int(pl.total_rx()),
|
||||||
|
"by_type": self._pkt_cnt,
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"sent": self._pkt_cnt["MessagePacket"]["tx"],
|
"sent": self._pkt_cnt["MessagePacket"]["tx"],
|
||||||
@@ -233,7 +246,7 @@ class APRSDStats:
|
|||||||
},
|
},
|
||||||
"plugins": plugin_stats,
|
"plugins": plugin_stats,
|
||||||
}
|
}
|
||||||
LOG.debug(f"STATS = {stats}")
|
LOG.info("APRSD Stats: DONE")
|
||||||
return stats
|
return stats
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import abc
|
import abc
|
||||||
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
@@ -50,6 +51,7 @@ class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
|||||||
super().__init__(name=name)
|
super().__init__(name=name)
|
||||||
self.thread_stop = False
|
self.thread_stop = False
|
||||||
APRSDThreadList().add(self)
|
APRSDThreadList().add(self)
|
||||||
|
self._last_loop = datetime.datetime.now()
|
||||||
|
|
||||||
def _should_quit(self):
|
def _should_quit(self):
|
||||||
""" see if we have a quit message from the global queue."""
|
""" see if we have a quit message from the global queue."""
|
||||||
@@ -70,10 +72,15 @@ class APRSDThread(threading.Thread, metaclass=abc.ABCMeta):
|
|||||||
out = f"Thread <{self.__class__.__name__}({self.name}) Alive? {self.is_alive()}>"
|
out = f"Thread <{self.__class__.__name__}({self.name}) Alive? {self.is_alive()}>"
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
def loop_age(self):
|
||||||
|
"""How old is the last loop call?"""
|
||||||
|
return datetime.datetime.now() - self._last_loop
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
LOG.debug("Starting")
|
LOG.debug("Starting")
|
||||||
while not self._should_quit():
|
while not self._should_quit():
|
||||||
can_loop = self.loop()
|
can_loop = self.loop()
|
||||||
|
self._last_loop = datetime.datetime.now()
|
||||||
if not can_loop:
|
if not can_loop:
|
||||||
self.stop()
|
self.stop()
|
||||||
self._cleanup()
|
self._cleanup()
|
||||||
|
|||||||
@@ -65,12 +65,20 @@ class KeepAliveThread(APRSDThread):
|
|||||||
)
|
)
|
||||||
LOG.info(keepalive)
|
LOG.info(keepalive)
|
||||||
thread_out = []
|
thread_out = []
|
||||||
|
thread_info = {}
|
||||||
for thread in thread_list.threads_list:
|
for thread in thread_list.threads_list:
|
||||||
alive = thread.is_alive()
|
alive = thread.is_alive()
|
||||||
thread_out.append(f"{thread.__class__.__name__}:{alive}")
|
age = thread.loop_age()
|
||||||
|
key = thread.__class__.__name__
|
||||||
|
thread_out.append(f"{key}:{alive}:{age}")
|
||||||
|
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(",".join(thread_out))
|
||||||
|
stats_obj.set_thread_info(thread_info)
|
||||||
|
|
||||||
# check the APRS connection
|
# check the APRS connection
|
||||||
cl = client.factory.create()
|
cl = client.factory.create()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import threading
|
|||||||
import wrapt
|
import wrapt
|
||||||
|
|
||||||
from aprsd import threads
|
from aprsd import threads
|
||||||
from aprsd.logging import log
|
from aprsd.log import log
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|||||||
+25
-6
@@ -3,9 +3,13 @@ import logging
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from ratelimiter import RateLimiter
|
from rush import quota, throttle
|
||||||
|
from rush.contrib import decorator
|
||||||
|
from rush.limiters import periodic
|
||||||
|
from rush.stores import dictionary
|
||||||
|
|
||||||
from aprsd import client
|
from aprsd import client
|
||||||
|
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.packets import core, tracker
|
||||||
|
|
||||||
@@ -13,10 +17,25 @@ from aprsd.packets import core, tracker
|
|||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
LOG = logging.getLogger("APRSD")
|
LOG = logging.getLogger("APRSD")
|
||||||
|
|
||||||
|
msg_t = throttle.Throttle(
|
||||||
|
limiter=periodic.PeriodicLimiter(
|
||||||
|
store=dictionary.DictionaryStore(),
|
||||||
|
),
|
||||||
|
rate=quota.Quota.per_second(
|
||||||
|
count=CONF.msg_rate_limit_period,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
ack_t = throttle.Throttle(
|
||||||
|
limiter=periodic.PeriodicLimiter(
|
||||||
|
store=dictionary.DictionaryStore(),
|
||||||
|
),
|
||||||
|
rate=quota.Quota.per_second(
|
||||||
|
count=CONF.ack_rate_limit_period,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def limited(until):
|
msg_throttle_decorator = decorator.ThrottleDecorator(throttle=msg_t)
|
||||||
duration = int(round(until - time.time()))
|
ack_throttle_decorator = decorator.ThrottleDecorator(throttle=ack_t)
|
||||||
LOG.debug(f"Rate limited, sleeping for {duration:d} seconds")
|
|
||||||
|
|
||||||
|
|
||||||
def send(packet: core.Packet, direct=False, aprs_client=None):
|
def send(packet: core.Packet, direct=False, aprs_client=None):
|
||||||
@@ -30,7 +49,7 @@ def send(packet: core.Packet, direct=False, aprs_client=None):
|
|||||||
_send_packet(packet, direct=direct, aprs_client=aprs_client)
|
_send_packet(packet, direct=direct, aprs_client=aprs_client)
|
||||||
|
|
||||||
|
|
||||||
@RateLimiter(max_calls=1, period=CONF.msg_rate_limit_period, callback=limited)
|
@msg_throttle_decorator.sleep_and_retry
|
||||||
def _send_packet(packet: core.Packet, direct=False, aprs_client=None):
|
def _send_packet(packet: core.Packet, direct=False, aprs_client=None):
|
||||||
if not direct:
|
if not direct:
|
||||||
thread = SendPacketThread(packet=packet)
|
thread = SendPacketThread(packet=packet)
|
||||||
@@ -39,7 +58,7 @@ def _send_packet(packet: core.Packet, direct=False, aprs_client=None):
|
|||||||
_send_direct(packet, aprs_client=aprs_client)
|
_send_direct(packet, aprs_client=aprs_client)
|
||||||
|
|
||||||
|
|
||||||
@RateLimiter(max_calls=1, period=CONF.ack_rate_limit_period, callback=limited)
|
@ack_throttle_decorator.sleep_and_retry
|
||||||
def _send_ack(packet: core.AckPacket, direct=False, aprs_client=None):
|
def _send_ack(packet: core.AckPacket, direct=False, aprs_client=None):
|
||||||
if not direct:
|
if not direct:
|
||||||
thread = SendAckThread(packet=packet)
|
thread = SendAckThread(packet=packet)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
|
||||||
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
|
<link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css">
|
||||||
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
|
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
|
||||||
<script src="https://cdn.socket.io/4.1.2/socket.io.min.js" integrity="sha384-toS6mmwu70G0fw54EGlWWeA4z3dyJ+dlXBtSURSKN4vyRFOcxd3Bzjj/AoOwY+Rg" crossorigin="anonymous"></script>
|
<script src="https://cdn.socket.io/4.7.1/socket.io.min.js" integrity="sha512-+NaO7d6gQ1YPxvc/qHIqZEchjGm207SszoNeMgppoqD/67fEqmc1edS8zrbxPD+4RQI3gDgT/83ihpFW61TG/Q==" crossorigin="anonymous"></script>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.bundle.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.bundle.js"></script>
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,11 @@
|
|||||||
start_update();
|
start_update();
|
||||||
init_chat();
|
init_chat();
|
||||||
reset_Tabs();
|
reset_Tabs();
|
||||||
|
|
||||||
|
if (location.protocol != 'https:') {
|
||||||
|
// Have to disable the beacon button.
|
||||||
|
$('#send_beacon').prop('disabled', true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
+368
@@ -0,0 +1,368 @@
|
|||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
import time
|
||||||
|
|
||||||
|
import flask
|
||||||
|
from flask import Flask
|
||||||
|
from flask.logging import default_handler
|
||||||
|
from flask_httpauth import HTTPBasicAuth
|
||||||
|
from oslo_config import cfg
|
||||||
|
import socketio
|
||||||
|
from werkzeug.security import check_password_hash
|
||||||
|
|
||||||
|
import aprsd
|
||||||
|
from aprsd import cli_helper, client, conf, packets, plugin, threads
|
||||||
|
from aprsd.log import rich as aprsd_logging
|
||||||
|
from aprsd.rpc import client as aprsd_rpc_client
|
||||||
|
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
LOG = logging.getLogger("gunicorn.access")
|
||||||
|
|
||||||
|
auth = HTTPBasicAuth()
|
||||||
|
users = {}
|
||||||
|
app = Flask(
|
||||||
|
"aprsd",
|
||||||
|
static_url_path="/static",
|
||||||
|
static_folder="web/admin/static",
|
||||||
|
template_folder="web/admin/templates",
|
||||||
|
)
|
||||||
|
bg_thread = None
|
||||||
|
app.config["SECRET_KEY"] = "secret!"
|
||||||
|
|
||||||
|
|
||||||
|
# HTTPBasicAuth doesn't work on a class method.
|
||||||
|
# This has to be out here. Rely on the APRSDFlask
|
||||||
|
# class to initialize the users from the config
|
||||||
|
@auth.verify_password
|
||||||
|
def verify_password(username, password):
|
||||||
|
global users
|
||||||
|
|
||||||
|
if username in users and check_password_hash(users.get(username), password):
|
||||||
|
return username
|
||||||
|
|
||||||
|
|
||||||
|
def _stats():
|
||||||
|
track = aprsd_rpc_client.RPCClient().get_packet_track()
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
|
||||||
|
time_format = "%m-%d-%Y %H:%M:%S"
|
||||||
|
|
||||||
|
stats_dict = aprsd_rpc_client.RPCClient().get_stats_dict()
|
||||||
|
if not stats_dict:
|
||||||
|
stats_dict = {
|
||||||
|
"aprsd": {},
|
||||||
|
"aprs-is": {"server": ""},
|
||||||
|
"messages": {
|
||||||
|
"sent": 0,
|
||||||
|
"received": 0,
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"sent": 0,
|
||||||
|
"received": 0,
|
||||||
|
},
|
||||||
|
"seen_list": {
|
||||||
|
"sent": 0,
|
||||||
|
"received": 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Convert the watch_list entries to age
|
||||||
|
wl = aprsd_rpc_client.RPCClient().get_watch_list()
|
||||||
|
new_list = {}
|
||||||
|
if wl:
|
||||||
|
for call in wl.get_all():
|
||||||
|
# call_date = datetime.datetime.strptime(
|
||||||
|
# str(wl.last_seen(call)),
|
||||||
|
# "%Y-%m-%d %H:%M:%S.%f",
|
||||||
|
# )
|
||||||
|
|
||||||
|
# We have to convert the RingBuffer to a real list
|
||||||
|
# so that json.dumps works.
|
||||||
|
# pkts = []
|
||||||
|
# for pkt in wl.get(call)["packets"].get():
|
||||||
|
# pkts.append(pkt)
|
||||||
|
|
||||||
|
new_list[call] = {
|
||||||
|
"last": wl.age(call),
|
||||||
|
# "packets": pkts
|
||||||
|
}
|
||||||
|
|
||||||
|
stats_dict["aprsd"]["watch_list"] = new_list
|
||||||
|
packet_list = aprsd_rpc_client.RPCClient().get_packet_list()
|
||||||
|
rx = tx = 0
|
||||||
|
if packet_list:
|
||||||
|
rx = packet_list.total_rx()
|
||||||
|
tx = packet_list.total_tx()
|
||||||
|
stats_dict["packets"] = {
|
||||||
|
"sent": tx,
|
||||||
|
"received": rx,
|
||||||
|
}
|
||||||
|
if track:
|
||||||
|
size_tracker = len(track)
|
||||||
|
else:
|
||||||
|
size_tracker = 0
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"time": now.strftime(time_format),
|
||||||
|
"size_tracker": size_tracker,
|
||||||
|
"stats": stats_dict,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/stats")
|
||||||
|
def stats():
|
||||||
|
LOG.debug("/stats called")
|
||||||
|
return json.dumps(_stats())
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
stats = _stats()
|
||||||
|
LOG.debug(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()
|
||||||
|
plugins = pm.get_plugins()
|
||||||
|
plugin_count = len(plugins)
|
||||||
|
|
||||||
|
if CONF.aprs_network.enabled:
|
||||||
|
transport = "aprs-is"
|
||||||
|
aprs_connection = (
|
||||||
|
"APRS-IS Server: <a href='http://status.aprs2.net' >"
|
||||||
|
"{}</a>".format(stats["stats"]["aprs-is"]["server"])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# We might be connected to a KISS socket?
|
||||||
|
if client.KISSClient.kiss_enabled():
|
||||||
|
transport = client.KISSClient.transport()
|
||||||
|
if transport == client.TRANSPORT_TCPKISS:
|
||||||
|
aprs_connection = (
|
||||||
|
"TCPKISS://{}:{}".format(
|
||||||
|
CONF.kiss_tcp.host,
|
||||||
|
CONF.kiss_tcp.port,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif transport == client.TRANSPORT_SERIALKISS:
|
||||||
|
aprs_connection = (
|
||||||
|
"SerialKISS://{}@{} baud".format(
|
||||||
|
CONF.kiss_serial.device,
|
||||||
|
CONF.kiss_serial.baudrate,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
stats["transport"] = transport
|
||||||
|
stats["aprs_connection"] = aprs_connection
|
||||||
|
entries = conf.conf_to_dict()
|
||||||
|
|
||||||
|
return flask.render_template(
|
||||||
|
"index.html",
|
||||||
|
initial_stats=stats,
|
||||||
|
aprs_connection=aprs_connection,
|
||||||
|
callsign=CONF.callsign,
|
||||||
|
version=aprsd.__version__,
|
||||||
|
config_json=json.dumps(
|
||||||
|
entries, indent=4,
|
||||||
|
sort_keys=True, default=str,
|
||||||
|
),
|
||||||
|
watch_count=watch_count,
|
||||||
|
watch_age=watch_age,
|
||||||
|
seen_count=seen_count,
|
||||||
|
plugin_count=plugin_count,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@auth.login_required
|
||||||
|
def messages():
|
||||||
|
track = packets.PacketTrack()
|
||||||
|
msgs = []
|
||||||
|
for id in track:
|
||||||
|
LOG.info(track[id].dict())
|
||||||
|
msgs.append(track[id].dict())
|
||||||
|
|
||||||
|
return flask.render_template("messages.html", messages=json.dumps(msgs))
|
||||||
|
|
||||||
|
|
||||||
|
@auth.login_required
|
||||||
|
@app.route("/packets")
|
||||||
|
def get_packets():
|
||||||
|
LOG.debug("/packets called")
|
||||||
|
packet_list = aprsd_rpc_client.RPCClient().get_packet_list()
|
||||||
|
if packet_list:
|
||||||
|
packets = packet_list.get()
|
||||||
|
tmp_list = []
|
||||||
|
for pkt in packets:
|
||||||
|
tmp_list.append(pkt.json)
|
||||||
|
|
||||||
|
return json.dumps(tmp_list)
|
||||||
|
else:
|
||||||
|
return json.dumps([])
|
||||||
|
|
||||||
|
|
||||||
|
@auth.login_required
|
||||||
|
@app.route("/plugins")
|
||||||
|
def plugins():
|
||||||
|
LOG.debug("/plugins called")
|
||||||
|
pm = plugin.PluginManager()
|
||||||
|
pm.reload_plugins()
|
||||||
|
|
||||||
|
return "reloaded"
|
||||||
|
|
||||||
|
|
||||||
|
@auth.login_required
|
||||||
|
@app.route("/save")
|
||||||
|
def save():
|
||||||
|
"""Save the existing queue to disk."""
|
||||||
|
track = packets.PacketTrack()
|
||||||
|
track.save()
|
||||||
|
return json.dumps({"messages": "saved"})
|
||||||
|
|
||||||
|
|
||||||
|
class LogUpdateThread(threads.APRSDThread):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("LogUpdate")
|
||||||
|
|
||||||
|
def loop(self):
|
||||||
|
if sio:
|
||||||
|
log_entries = aprsd_rpc_client.RPCClient().get_log_entries()
|
||||||
|
|
||||||
|
if log_entries:
|
||||||
|
LOG.info(f"Sending log entries! {len(log_entries)}")
|
||||||
|
for entry in log_entries:
|
||||||
|
sio.emit(
|
||||||
|
"log_entry", entry,
|
||||||
|
namespace="/logs",
|
||||||
|
)
|
||||||
|
time.sleep(5)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingNamespace(socketio.Namespace):
|
||||||
|
log_thread = None
|
||||||
|
|
||||||
|
def on_connect(self, sid, environ):
|
||||||
|
global sio
|
||||||
|
LOG.debug(f"LOG on_connect {sid}")
|
||||||
|
sio.emit(
|
||||||
|
"connected", {"data": "/logs Connected"},
|
||||||
|
namespace="/logs",
|
||||||
|
)
|
||||||
|
self.log_thread = LogUpdateThread()
|
||||||
|
self.log_thread.start()
|
||||||
|
|
||||||
|
def on_disconnect(self, sid):
|
||||||
|
LOG.debug(f"LOG Disconnected {sid}")
|
||||||
|
if self.log_thread:
|
||||||
|
self.log_thread.stop()
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging(flask_app, loglevel):
|
||||||
|
global app, LOG
|
||||||
|
log_level = conf.log.LOG_LEVELS[loglevel]
|
||||||
|
app.logger.setLevel(log_level)
|
||||||
|
flask_app.logger.removeHandler(default_handler)
|
||||||
|
|
||||||
|
date_format = CONF.logging.date_format
|
||||||
|
|
||||||
|
if CONF.logging.rich_logging:
|
||||||
|
log_format = "%(message)s"
|
||||||
|
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||||
|
rh = aprsd_logging.APRSDRichHandler(
|
||||||
|
show_thread=True, thread_width=15,
|
||||||
|
rich_tracebacks=True, omit_repeated_times=False,
|
||||||
|
)
|
||||||
|
rh.setFormatter(log_formatter)
|
||||||
|
app.logger.addHandler(rh)
|
||||||
|
|
||||||
|
log_file = CONF.logging.logfile
|
||||||
|
|
||||||
|
if log_file:
|
||||||
|
log_format = CONF.logging.logformat
|
||||||
|
log_formatter = logging.Formatter(fmt=log_format, datefmt=date_format)
|
||||||
|
fh = RotatingFileHandler(
|
||||||
|
log_file, maxBytes=(10248576 * 5),
|
||||||
|
backupCount=4,
|
||||||
|
)
|
||||||
|
fh.setFormatter(log_formatter)
|
||||||
|
app.logger.addHandler(fh)
|
||||||
|
|
||||||
|
LOG = app.logger
|
||||||
|
|
||||||
|
|
||||||
|
def init_app(config_file=None, log_level=None):
|
||||||
|
default_config_file = cli_helper.DEFAULT_CONFIG_FILE
|
||||||
|
if not config_file:
|
||||||
|
config_file = default_config_file
|
||||||
|
|
||||||
|
CONF(
|
||||||
|
[], project="aprsd", version=aprsd.__version__,
|
||||||
|
default_config_files=[config_file],
|
||||||
|
)
|
||||||
|
|
||||||
|
if not log_level:
|
||||||
|
log_level = CONF.logging.log_level
|
||||||
|
|
||||||
|
return log_level
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
async_mode = "threading"
|
||||||
|
sio = socketio.Server(logger=True, async_mode=async_mode)
|
||||||
|
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
|
||||||
|
log_level = init_app(log_level="DEBUG")
|
||||||
|
setup_logging(app, log_level)
|
||||||
|
sio.register_namespace(LoggingNamespace("/logs"))
|
||||||
|
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||||
|
app.run(
|
||||||
|
threaded=True,
|
||||||
|
debug=False,
|
||||||
|
port=CONF.admin.web_port,
|
||||||
|
host=CONF.admin.web_ip,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "uwsgi_file_aprsd_wsgi":
|
||||||
|
# Start with
|
||||||
|
# uwsgi --http :8000 --gevent 1000 --http-websockets --master -w aprsd.wsgi --callable app
|
||||||
|
|
||||||
|
async_mode = "gevent_uwsgi"
|
||||||
|
sio = socketio.Server(logger=True, async_mode=async_mode)
|
||||||
|
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
|
||||||
|
log_level = init_app(
|
||||||
|
log_level="DEBUG",
|
||||||
|
config_file="/config/aprsd.conf",
|
||||||
|
)
|
||||||
|
setup_logging(app, log_level)
|
||||||
|
sio.register_namespace(LoggingNamespace("/logs"))
|
||||||
|
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "aprsd.wsgi":
|
||||||
|
# set async_mode to 'threading', 'eventlet', 'gevent' or 'gevent_uwsgi' to
|
||||||
|
# force a mode else, the best mode is selected automatically from what's
|
||||||
|
# installed
|
||||||
|
async_mode = "gevent_uwsgi"
|
||||||
|
sio = socketio.Server(logger=True, async_mode=async_mode)
|
||||||
|
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
|
||||||
|
|
||||||
|
log_level = init_app(config_file="/config/aprsd.conf", log_level="DEBUG")
|
||||||
|
setup_logging(app, log_level)
|
||||||
|
sio.register_namespace(LoggingNamespace("/logs"))
|
||||||
|
CONF.log_opt_values(LOG, logging.DEBUG)
|
||||||
+60
-61
@@ -1,88 +1,87 @@
|
|||||||
#
|
#
|
||||||
# This file is autogenerated by pip-compile with Python 3.9
|
# This file is autogenerated by pip-compile with Python 3.10
|
||||||
# by the following command:
|
# by the following command:
|
||||||
#
|
#
|
||||||
# pip-compile --annotation-style=line --resolver=backtracking dev-requirements.in
|
# pip-compile --annotation-style=line dev-requirements.in
|
||||||
#
|
#
|
||||||
add-trailing-comma==2.4.0 # via gray
|
add-trailing-comma==3.0.1 # via gray
|
||||||
alabaster==0.7.13 # via sphinx
|
alabaster==0.7.13 # via sphinx
|
||||||
attrs==22.2.0 # via jsonschema, pytest
|
attrs==23.1.0 # via jsonschema, referencing
|
||||||
autoflake==1.5.3 # via gray
|
autoflake==1.5.3 # via gray
|
||||||
babel==2.11.0 # via sphinx
|
babel==2.12.1 # via sphinx
|
||||||
black==22.12.0 # via gray
|
black==23.7.0 # via gray
|
||||||
build==0.10.0 # via pip-tools
|
build==0.10.0 # via pip-tools
|
||||||
cachetools==5.2.1 # via tox
|
cachetools==5.3.1 # via tox
|
||||||
certifi==2022.12.7 # via requests
|
certifi==2023.7.22 # via requests
|
||||||
cfgv==3.3.1 # via pre-commit
|
cfgv==3.4.0 # via pre-commit
|
||||||
chardet==5.1.0 # via tox
|
chardet==5.2.0 # via tox
|
||||||
charset-normalizer==3.0.1 # via requests
|
charset-normalizer==3.2.0 # via requests
|
||||||
click==8.1.3 # via black, pip-tools
|
click==8.1.6 # via black, pip-tools
|
||||||
colorama==0.4.6 # via tox
|
colorama==0.4.6 # via tox
|
||||||
commonmark==0.9.1 # via rich
|
commonmark==0.9.1 # via rich
|
||||||
configargparse==1.5.3 # via gray
|
configargparse==1.7 # via gray
|
||||||
coverage[toml]==7.0.5 # via pytest-cov
|
coverage[toml]==7.3.0 # via pytest-cov
|
||||||
distlib==0.3.6 # via virtualenv
|
distlib==0.3.7 # via virtualenv
|
||||||
docutils==0.19 # via sphinx
|
docutils==0.20.1 # via sphinx
|
||||||
exceptiongroup==1.1.0 # via pytest
|
exceptiongroup==1.1.3 # via pytest
|
||||||
filelock==3.9.0 # via tox, virtualenv
|
filelock==3.12.2 # via tox, virtualenv
|
||||||
fixit==0.1.4 # via gray
|
fixit==0.1.4 # via gray
|
||||||
flake8==6.0.0 # via -r dev-requirements.in, fixit, pep8-naming
|
flake8==6.1.0 # via -r dev-requirements.in, fixit, pep8-naming
|
||||||
gray==0.13.0 # via -r dev-requirements.in
|
gray==0.13.0 # via -r dev-requirements.in
|
||||||
identify==2.5.13 # via pre-commit
|
identify==2.5.26 # via pre-commit
|
||||||
idna==3.4 # via requests
|
idna==3.4 # via requests
|
||||||
imagesize==1.4.1 # via sphinx
|
imagesize==1.4.1 # via sphinx
|
||||||
importlib-metadata==6.0.0 # via sphinx
|
importlib-resources==6.0.1 # via fixit
|
||||||
importlib-resources==5.10.2 # via fixit
|
|
||||||
iniconfig==2.0.0 # via pytest
|
iniconfig==2.0.0 # via pytest
|
||||||
isort==5.11.4 # via -r dev-requirements.in, gray
|
isort==5.12.0 # via -r dev-requirements.in, gray
|
||||||
jinja2==3.1.2 # via sphinx
|
jinja2==3.1.2 # via sphinx
|
||||||
jsonschema==4.17.3 # via fixit
|
jsonschema==4.19.0 # via fixit
|
||||||
libcst==0.4.9 # via fixit
|
jsonschema-specifications==2023.7.1 # via jsonschema
|
||||||
markupsafe==2.1.2 # via jinja2
|
libcst==1.0.1 # via fixit
|
||||||
|
markupsafe==2.1.3 # via jinja2
|
||||||
mccabe==0.7.0 # via flake8
|
mccabe==0.7.0 # via flake8
|
||||||
mypy==0.991 # via -r dev-requirements.in
|
mypy==1.5.0 # via -r dev-requirements.in
|
||||||
mypy-extensions==0.4.3 # via black, mypy, typing-inspect
|
mypy-extensions==1.0.0 # via black, mypy, typing-inspect
|
||||||
nodeenv==1.7.0 # via pre-commit
|
nodeenv==1.8.0 # via pre-commit
|
||||||
packaging==23.0 # via build, pyproject-api, pytest, sphinx, tox
|
packaging==23.1 # via black, build, pyproject-api, pytest, sphinx, tox
|
||||||
pathspec==0.10.3 # via black
|
pathspec==0.11.2 # via black
|
||||||
pep8-naming==0.13.3 # via -r dev-requirements.in
|
pep8-naming==0.13.3 # via -r dev-requirements.in
|
||||||
pip-tools==6.12.1 # via -r dev-requirements.in
|
pip-tools==7.3.0 # via -r dev-requirements.in
|
||||||
platformdirs==2.6.2 # via black, tox, virtualenv
|
platformdirs==3.10.0 # via black, tox, virtualenv
|
||||||
pluggy==1.0.0 # via pytest, tox
|
pluggy==1.2.0 # via pytest, tox
|
||||||
pre-commit==2.21.0 # via -r dev-requirements.in
|
pre-commit==3.3.3 # via -r dev-requirements.in
|
||||||
pycodestyle==2.10.0 # via flake8
|
pycodestyle==2.11.0 # via flake8
|
||||||
pyflakes==3.0.1 # via autoflake, flake8
|
pyflakes==3.1.0 # via autoflake, flake8
|
||||||
pygments==2.14.0 # via rich, sphinx
|
pygments==2.16.1 # via rich, sphinx
|
||||||
pyproject-api==1.5.0 # via tox
|
pyproject-api==1.5.3 # via tox
|
||||||
pyproject-hooks==1.0.0 # via build
|
pyproject-hooks==1.0.0 # via build
|
||||||
pyrsistent==0.19.3 # via jsonschema
|
pytest==7.4.0 # via -r dev-requirements.in, pytest-cov
|
||||||
pytest==7.2.1 # via -r dev-requirements.in, pytest-cov
|
pytest-cov==4.1.0 # via -r dev-requirements.in
|
||||||
pytest-cov==4.0.0 # via -r dev-requirements.in
|
pyupgrade==3.10.1 # via gray
|
||||||
pytz==2022.7.1 # via babel
|
pyyaml==6.0.1 # via fixit, libcst, pre-commit
|
||||||
pyupgrade==3.3.1 # via gray
|
referencing==0.30.2 # via jsonschema, jsonschema-specifications
|
||||||
pyyaml==6.0 # via fixit, libcst, pre-commit
|
requests==2.31.0 # via sphinx
|
||||||
requests==2.28.2 # via sphinx
|
|
||||||
rich==12.6.0 # via gray
|
rich==12.6.0 # via gray
|
||||||
|
rpds-py==0.9.2 # via jsonschema, referencing
|
||||||
snowballstemmer==2.2.0 # via sphinx
|
snowballstemmer==2.2.0 # via sphinx
|
||||||
sphinx==6.1.3 # via -r dev-requirements.in
|
sphinx==7.1.2 # via -r dev-requirements.in, sphinxcontrib-applehelp, sphinxcontrib-devhelp, sphinxcontrib-htmlhelp, sphinxcontrib-qthelp, sphinxcontrib-serializinghtml
|
||||||
sphinxcontrib-applehelp==1.0.3 # via sphinx
|
sphinxcontrib-applehelp==1.0.7 # via sphinx
|
||||||
sphinxcontrib-devhelp==1.0.2 # via sphinx
|
sphinxcontrib-devhelp==1.0.5 # via sphinx
|
||||||
sphinxcontrib-htmlhelp==2.0.0 # via sphinx
|
sphinxcontrib-htmlhelp==2.0.4 # via sphinx
|
||||||
sphinxcontrib-jsmath==1.0.1 # via sphinx
|
sphinxcontrib-jsmath==1.0.1 # via sphinx
|
||||||
sphinxcontrib-qthelp==1.0.3 # via sphinx
|
sphinxcontrib-qthelp==1.0.6 # via sphinx
|
||||||
sphinxcontrib-serializinghtml==1.1.5 # via sphinx
|
sphinxcontrib-serializinghtml==1.1.8 # via sphinx
|
||||||
tokenize-rt==5.0.0 # via add-trailing-comma, pyupgrade
|
tokenize-rt==5.2.0 # via add-trailing-comma, pyupgrade
|
||||||
toml==0.10.2 # via autoflake
|
toml==0.10.2 # via autoflake
|
||||||
tomli==2.0.1 # via black, build, coverage, mypy, pyproject-api, pyproject-hooks, pytest, tox
|
tomli==2.0.1 # via black, build, coverage, mypy, pip-tools, pyproject-api, pyproject-hooks, pytest, tox
|
||||||
tox==4.3.5 # via -r dev-requirements.in
|
tox==4.8.0 # via -r dev-requirements.in
|
||||||
typing-extensions==4.4.0 # via black, libcst, mypy, typing-inspect
|
typing-extensions==4.7.1 # via libcst, mypy, typing-inspect
|
||||||
typing-inspect==0.8.0 # via libcst
|
typing-inspect==0.9.0 # via libcst
|
||||||
unify==0.5 # via gray
|
unify==0.5 # via gray
|
||||||
untokenize==0.1.1 # via unify
|
untokenize==0.1.1 # via unify
|
||||||
urllib3==1.26.14 # via requests
|
urllib3==2.0.4 # via requests
|
||||||
virtualenv==20.17.1 # via pre-commit, tox
|
virtualenv==20.24.3 # via pre-commit, tox
|
||||||
wheel==0.38.4 # via pip-tools
|
wheel==0.41.1 # via pip-tools
|
||||||
zipp==3.11.0 # via importlib-metadata, importlib-resources
|
|
||||||
|
|
||||||
# The following packages are considered to be unsafe in a requirements file:
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
# pip
|
# pip
|
||||||
|
|||||||
+43
-41
@@ -1,59 +1,61 @@
|
|||||||
#FROM python:3-bullseye as aprsd
|
FROM python:3.11-slim as build
|
||||||
FROM ubuntu:22.04 as aprsd
|
|
||||||
|
|
||||||
# Dockerfile for building a container during aprsd development.
|
ARG VERSION=3.1.0
|
||||||
|
|
||||||
ARG UID
|
|
||||||
ARG GID
|
|
||||||
ARG TZ
|
|
||||||
ARG VERSION=3.0.0
|
|
||||||
ARG BUILDX_QEMU_ENV
|
|
||||||
ENV APRS_USER=aprs
|
|
||||||
ENV HOME=/home/aprs
|
|
||||||
ENV TZ=${TZ:-US/Eastern}
|
ENV TZ=${TZ:-US/Eastern}
|
||||||
ENV UID=${UID:-1000}
|
|
||||||
ENV GID=${GID:-1000}
|
|
||||||
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 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
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
|
||||||
RUN apt update
|
|
||||||
RUN apt install -y git build-essential
|
|
||||||
RUN apt install -y libffi-dev python3-dev libssl-dev libxml2-dev libxslt-dev
|
|
||||||
RUN apt install -y python3 python3-pip python3-dev python3-lxml
|
|
||||||
|
|
||||||
RUN pip3 install -U pip
|
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 vim libffi-dev \
|
||||||
|
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo \
|
||||||
|
# Install dependencies
|
||||||
|
# Clean up
|
||||||
|
&& apt-get autoremove -y \
|
||||||
|
&& apt-get clean -y
|
||||||
|
|
||||||
RUN addgroup --gid $GID $APRS_USER
|
|
||||||
RUN useradd -m -u $UID -g $APRS_USER $APRS_USER
|
|
||||||
|
|
||||||
# Handle an extremely specific issue when building the cryptography package for
|
### Final stage
|
||||||
# 32-bit architectures within QEMU running on a 64-bit host (issue #30).
|
FROM build as final
|
||||||
RUN if [ "${BUILDX_QEMU_ENV}" = "true" -a "$(getconf LONG_BIT)" = "32" ]; then \
|
WORKDIR /app
|
||||||
pip3 install -U cryptography==3.3.2; \
|
|
||||||
else \
|
|
||||||
pip3 install cryptography ;\
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure /config is there with a default config file
|
|
||||||
USER root
|
|
||||||
# Install aprsd
|
|
||||||
RUN pip3 install aprsd==$APRSD_PIP_VERSION
|
RUN pip3 install aprsd==$APRSD_PIP_VERSION
|
||||||
RUN mkdir -p /config
|
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 which aprsd
|
||||||
RUN aprsd sample-config > /config/aprsd.conf
|
RUN aprsd sample-config > /config/aprsd.conf
|
||||||
RUN chown -R $APRS_USER:$APRS_USER /config
|
|
||||||
RUN chown -R $APRS_USER:$APRS_USER $HOME
|
|
||||||
|
|
||||||
# override this to run another configuration
|
ADD bin/run.sh /app
|
||||||
ENV CONF default
|
ADD bin/listen.sh /app
|
||||||
VOLUME ["/config", "/plugins"]
|
ADD bin/admin.sh /app
|
||||||
|
|
||||||
USER $APRS_USER
|
# For the web admin interface
|
||||||
ADD bin/run.sh /usr/local/bin
|
EXPOSE 8001
|
||||||
ADD bin/listen.sh /usr/local/bin
|
|
||||||
ENTRYPOINT ["/usr/local/bin/run.sh"]
|
ENTRYPOINT ["/app/run.sh"]
|
||||||
|
VOLUME ["/config"]
|
||||||
|
|
||||||
|
# Set the user to run the application
|
||||||
|
USER appuser
|
||||||
|
|
||||||
HEALTHCHECK --interval=5m --timeout=12s --start-period=30s \
|
HEALTHCHECK --interval=5m --timeout=12s --start-period=30s \
|
||||||
CMD aprsd healthcheck --config /config/aprsd.conf
|
CMD aprsd healthcheck --config /config/aprsd.conf
|
||||||
|
|||||||
+43
-57
@@ -1,71 +1,57 @@
|
|||||||
# syntax=docker/dockerfile:1
|
FROM python:3.11-slim as build
|
||||||
FROM ubuntu:22.04
|
|
||||||
|
|
||||||
# Dockerfile for building a container during aprsd development.
|
|
||||||
ARG BRANCH=master
|
ARG BRANCH=master
|
||||||
ARG UID
|
|
||||||
ARG GID
|
|
||||||
|
|
||||||
ARG BUILDX_QEMU_ENV
|
ARG BUILDX_QEMU_ENV
|
||||||
|
|
||||||
ENV APRS_USER=aprs
|
|
||||||
ENV HOME=/home/aprs
|
|
||||||
ENV APRSD=http://github.com/craigerl/aprsd.git
|
|
||||||
ENV APRSD_BRANCH=${BRANCH:-master}
|
ENV APRSD_BRANCH=${BRANCH:-master}
|
||||||
ENV VIRTUAL_ENV=$HOME/.venv3
|
|
||||||
ENV UID=${UID:-1000}
|
|
||||||
ENV GID=${GID:-1000}
|
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV PIP_DEFAULT_TIMEOUT=100 \
|
||||||
ENV INSTALL=$HOME/install
|
# Allow statements and log messages to immediately appear
|
||||||
RUN apt update
|
PYTHONUNBUFFERED=1 \
|
||||||
RUN apt install -y --no-install-recommends git build-essential bash fortune
|
# disable a pip version check to reduce run-time & log-spam
|
||||||
RUN apt install -y libffi-dev python3-dev libssl-dev libxml2-dev libxslt-dev
|
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
||||||
RUN apt install -y python3 python3-pip python3-dev python3-lxml
|
# cache is useless in docker image, so disable to reduce image size
|
||||||
#RUN apt-get clean
|
PIP_NO_CACHE_DIR=1
|
||||||
RUN apt-get -o Dpkg::Options::="--force-confmiss" install --reinstall netbase
|
|
||||||
|
|
||||||
RUN addgroup --gid 1001 $APRS_USER
|
|
||||||
RUN useradd -m -u $UID -g $APRS_USER $APRS_USER
|
|
||||||
|
|
||||||
ENV LC_ALL=C.UTF-8
|
RUN set -ex \
|
||||||
ENV LANG=C.UTF-8
|
# 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 vim libffi-dev \
|
||||||
|
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo \
|
||||||
|
# Install dependencies
|
||||||
|
# Clean up
|
||||||
|
&& apt-get autoremove -y \
|
||||||
|
&& apt-get clean -y
|
||||||
|
|
||||||
WORKDIR $HOME
|
|
||||||
USER $APRS_USER
|
|
||||||
RUN pip install wheel
|
|
||||||
#RUN python3 -m venv $VIRTUAL_ENV
|
|
||||||
#ENV PATH="$VIRTUAL_ENV/bin:$PATH"
|
|
||||||
RUN echo "export PATH=\$PATH:\$HOME/.local/bin" >> $HOME/.bashrc
|
|
||||||
RUN cat $HOME/.bashrc
|
|
||||||
|
|
||||||
USER root
|
### Final stage
|
||||||
WORKDIR $HOME
|
FROM build as final
|
||||||
# Handle an extremely specific issue when building the cryptography package for
|
WORKDIR /app
|
||||||
# 32-bit architectures within QEMU running on a 64-bit host (issue #30).
|
|
||||||
RUN if [ "${BUILDX_QEMU_ENV}" = "true" -a "$(getconf LONG_BIT)" = "32" ]; then \
|
RUN git clone -b $APRSD_BRANCH https://github.com/craigerl/aprsd
|
||||||
pip3 install -U cryptography==3.3.2; \
|
RUN cd aprsd && pip install --no-cache-dir .
|
||||||
else \
|
RUN pip install gevent uwsgi
|
||||||
pip3 install cryptography ;\
|
RUN which aprsd
|
||||||
fi
|
RUN mkdir /config
|
||||||
RUN mkdir $INSTALL
|
RUN chown -R appuser:appgroup /app
|
||||||
RUN git clone -b $BRANCH $APRSD $INSTALL/aprsd
|
RUN chown -R appuser:appgroup /config
|
||||||
RUN cd $INSTALL/aprsd && pip3 install -v .
|
USER appuser
|
||||||
RUN ls -al /usr/local/bin
|
|
||||||
RUN ls -al /usr/bin
|
|
||||||
RUN which aprsd
|
RUN which aprsd
|
||||||
RUN mkdir -p /config
|
|
||||||
RUN aprsd sample-config > /config/aprsd.conf
|
RUN aprsd sample-config > /config/aprsd.conf
|
||||||
RUN chown -R $APRS_USER:$APRS_USER /config
|
|
||||||
|
|
||||||
# override this to run another configuration
|
ADD bin/run.sh /app
|
||||||
ENV CONF default
|
ADD bin/listen.sh /app
|
||||||
USER $APRS_USER
|
ADD bin/admin.sh /app
|
||||||
VOLUME ["/config", "/plugins"]
|
|
||||||
|
|
||||||
ADD bin/run.sh $HOME/
|
EXPOSE 8000
|
||||||
ADD bin/listen.sh $HOME/
|
|
||||||
ENTRYPOINT ["/home/aprs/run.sh"]
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=5m --timeout=12s --start-period=30s \
|
# CMD ["gunicorn", "aprsd.wsgi:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||||
CMD aprsd healthcheck --config /config/aprsd.conf
|
ENTRYPOINT ["/app/run.sh"]
|
||||||
|
VOLUME ["/config"]
|
||||||
|
|
||||||
|
# Set the user to run the application
|
||||||
|
USER appuser
|
||||||
|
|||||||
Executable
+33
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -x
|
||||||
|
|
||||||
|
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 "${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
|
||||||
|
|
||||||
|
export COLUMNS=200
|
||||||
|
#exec gunicorn -b :8000 --workers 4 "aprsd.admin_web:create_app(config_file='$APRSD_CONFIG', log_level='$LOG_LEVEL')"
|
||||||
|
# exec gunicorn -b :8000 --workers 4 "aprsd.wsgi:app"
|
||||||
|
exec uwsgi --http :8000 --gevent 1000 --http-websockets --master -w aprsd.wsgi --callable app
|
||||||
|
#exec aprsd listen -c $APRSD_CONFIG --loglevel ${LOG_LEVEL} ${APRSD_LOAD_PLUGINS} ${APRSD_LISTEN_FILTER}
|
||||||
@@ -9,7 +9,7 @@ if [ ! -z "${APRSD_PLUGINS}" ]; then
|
|||||||
IFS=$OLDIFS
|
IFS=$OLDIFS
|
||||||
# call your procedure/other scripts here below
|
# call your procedure/other scripts here below
|
||||||
echo "Installing '$plugin'"
|
echo "Installing '$plugin'"
|
||||||
pip3 install $plugin
|
pip3 install --user $plugin
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -9,7 +9,7 @@ if [ ! -z "${APRSD_PLUGINS}" ]; then
|
|||||||
IFS=$OLDIFS
|
IFS=$OLDIFS
|
||||||
# call your procedure/other scripts here below
|
# call your procedure/other scripts here below
|
||||||
echo "Installing '$plugin'"
|
echo "Installing '$plugin'"
|
||||||
pip3 install $plugin
|
pip3 install --user $plugin
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class HelloPlugin(plugin.APRSDRegexCommandPluginBase):
|
|||||||
command_regex = "^[hH]"
|
command_regex = "^[hH]"
|
||||||
command_name = "hello"
|
command_name = "hello"
|
||||||
|
|
||||||
def command(self, packet):
|
def process(self, packet):
|
||||||
LOG.info("HelloPlugin")
|
LOG.info("HelloPlugin")
|
||||||
reply = f"Hello '{packet.from_call}'"
|
reply = f"Hello '{packet.from_call}'"
|
||||||
return reply
|
return reply
|
||||||
|
|||||||
+8
-7
@@ -1,9 +1,9 @@
|
|||||||
aprslib>=0.7.0
|
aprslib>=0.7.0
|
||||||
click
|
click
|
||||||
|
click-params
|
||||||
click-completion
|
click-completion
|
||||||
flask==2.1.2
|
flask
|
||||||
werkzeug==2.1.2
|
werkzeug
|
||||||
flask-classful
|
|
||||||
flask-httpauth
|
flask-httpauth
|
||||||
imapclient
|
imapclient
|
||||||
pluggy
|
pluggy
|
||||||
@@ -15,6 +15,8 @@ six
|
|||||||
thesmuggler
|
thesmuggler
|
||||||
update_checker
|
update_checker
|
||||||
flask-socketio
|
flask-socketio
|
||||||
|
python-socketio
|
||||||
|
gevent
|
||||||
eventlet
|
eventlet
|
||||||
tabulate
|
tabulate
|
||||||
# Pinned due to gray needing 12.6.0
|
# Pinned due to gray needing 12.6.0
|
||||||
@@ -27,13 +29,12 @@ kiss3
|
|||||||
attrs
|
attrs
|
||||||
# for mobile checking
|
# for mobile checking
|
||||||
user-agents
|
user-agents
|
||||||
pyopenssl
|
|
||||||
dataclasses
|
dataclasses
|
||||||
dacite2
|
dacite2
|
||||||
oslo.config
|
oslo.config
|
||||||
rpyc
|
rpyc
|
||||||
# Pin this here so it doesn't require a compile on
|
# Pin this here so it doesn't require a compile on
|
||||||
# raspi
|
# raspi
|
||||||
cryptography==38.0.1
|
shellingham
|
||||||
shellingham==1.5.0.post1
|
geopy
|
||||||
ratelimiter
|
rush
|
||||||
|
|||||||
+46
-39
@@ -1,69 +1,76 @@
|
|||||||
#
|
#
|
||||||
# This file is autogenerated by pip-compile with Python 3.9
|
# This file is autogenerated by pip-compile with Python 3.10
|
||||||
# by the following command:
|
# by the following command:
|
||||||
#
|
#
|
||||||
# pip-compile --annotation-style=line --resolver=backtracking requirements.in
|
# pip-compile --annotation-style=line requirements.in
|
||||||
#
|
#
|
||||||
aprslib==0.7.2 # via -r requirements.in
|
aprslib==0.7.2 # via -r requirements.in
|
||||||
attrs==22.2.0 # via -r requirements.in, ax253, kiss3
|
attrs==23.1.0 # via -r requirements.in, ax253, kiss3, rush
|
||||||
ax253==0.1.5.post1 # via kiss3
|
ax253==0.1.5.post1 # via kiss3
|
||||||
beautifulsoup4==4.11.1 # via -r requirements.in
|
beautifulsoup4==4.12.2 # via -r requirements.in
|
||||||
bidict==0.22.1 # via python-socketio
|
bidict==0.22.1 # via python-socketio
|
||||||
bitarray==2.6.2 # via ax253, kiss3
|
bitarray==2.8.1 # via ax253, kiss3
|
||||||
certifi==2022.12.7 # via requests
|
blinker==1.6.2 # via flask
|
||||||
cffi==1.15.1 # via cryptography
|
certifi==2023.7.22 # via requests
|
||||||
charset-normalizer==3.0.1 # via requests
|
charset-normalizer==3.2.0 # via requests
|
||||||
click==8.1.3 # via -r requirements.in, click-completion, flask
|
click==8.1.6 # via -r requirements.in, click-completion, click-params, flask
|
||||||
click-completion==0.5.2 # via -r requirements.in
|
click-completion==0.5.2 # via -r requirements.in
|
||||||
|
click-params==0.4.1 # via -r requirements.in
|
||||||
commonmark==0.9.1 # via rich
|
commonmark==0.9.1 # via rich
|
||||||
cryptography==38.0.1 # via -r requirements.in, pyopenssl
|
|
||||||
dacite2==2.0.0 # via -r requirements.in
|
dacite2==2.0.0 # via -r requirements.in
|
||||||
dataclasses==0.6 # via -r requirements.in
|
dataclasses==0.6 # via -r requirements.in
|
||||||
debtcollector==2.5.0 # via oslo-config
|
debtcollector==2.5.0 # via oslo-config
|
||||||
dnspython==2.3.0 # via eventlet
|
decorator==5.1.1 # via validators
|
||||||
|
dnspython==2.4.2 # via eventlet
|
||||||
eventlet==0.33.3 # via -r requirements.in
|
eventlet==0.33.3 # via -r requirements.in
|
||||||
flask==2.1.2 # via -r requirements.in, flask-classful, flask-httpauth, flask-socketio
|
flask==2.3.2 # via -r requirements.in, flask-httpauth, flask-socketio
|
||||||
flask-classful==0.14.2 # via -r requirements.in
|
flask-httpauth==4.8.0 # via -r requirements.in
|
||||||
flask-httpauth==4.7.0 # via -r requirements.in
|
flask-socketio==5.3.5 # via -r requirements.in
|
||||||
flask-socketio==5.3.2 # via -r requirements.in
|
geographiclib==2.0 # via geopy
|
||||||
greenlet==2.0.1 # via eventlet
|
geopy==2.3.0 # via -r requirements.in
|
||||||
|
gevent==23.7.0 # via -r requirements.in
|
||||||
|
greenlet==2.0.2 # via eventlet, gevent
|
||||||
idna==3.4 # via requests
|
idna==3.4 # via requests
|
||||||
imapclient==2.3.1 # via -r requirements.in
|
imapclient==2.3.1 # via -r requirements.in
|
||||||
importlib-metadata==6.0.0 # via ax253, flask, kiss3
|
importlib-metadata==6.8.0 # via ax253, kiss3
|
||||||
itsdangerous==2.1.2 # via flask
|
itsdangerous==2.1.2 # via flask
|
||||||
jinja2==3.1.2 # via click-completion, flask
|
jinja2==3.1.2 # via click-completion, flask
|
||||||
kiss3==8.0.0 # via -r requirements.in
|
kiss3==8.0.0 # via -r requirements.in
|
||||||
markupsafe==2.1.2 # via jinja2
|
markupsafe==2.1.3 # via jinja2, werkzeug
|
||||||
netaddr==0.8.0 # via oslo-config
|
netaddr==0.8.0 # via oslo-config
|
||||||
oslo-config==9.1.0 # via -r requirements.in
|
oslo-config==9.1.1 # via -r requirements.in
|
||||||
oslo-i18n==5.1.0 # via oslo-config
|
oslo-i18n==6.0.0 # via oslo-config
|
||||||
pbr==5.11.1 # via -r requirements.in, oslo-i18n, stevedore
|
pbr==5.11.1 # via -r requirements.in, oslo-i18n, stevedore
|
||||||
pluggy==1.0.0 # via -r requirements.in
|
pluggy==1.2.0 # via -r requirements.in
|
||||||
plumbum==1.8.1 # via rpyc
|
plumbum==1.8.2 # via rpyc
|
||||||
pycparser==2.21 # via cffi
|
pygments==2.16.1 # via rich
|
||||||
pygments==2.14.0 # via rich
|
|
||||||
pyopenssl==23.0.0 # via -r requirements.in
|
|
||||||
pyserial==3.5 # via pyserial-asyncio
|
pyserial==3.5 # via pyserial-asyncio
|
||||||
pyserial-asyncio==0.6 # via kiss3
|
pyserial-asyncio==0.6 # via kiss3
|
||||||
python-engineio==4.3.4 # via python-socketio
|
python-engineio==4.5.1 # via python-socketio
|
||||||
python-socketio==5.7.2 # via flask-socketio
|
python-socketio==5.8.0 # via -r requirements.in, flask-socketio
|
||||||
pytz==2022.7.1 # via -r requirements.in
|
pytz==2023.3 # via -r requirements.in
|
||||||
pyyaml==6.0 # via -r requirements.in, oslo-config
|
pyyaml==6.0.1 # via -r requirements.in, oslo-config
|
||||||
ratelimiter==1.2.0.post0 # via -r requirements.in
|
requests==2.31.0 # via -r requirements.in, oslo-config, update-checker
|
||||||
requests==2.28.2 # via -r requirements.in, oslo-config, update-checker
|
|
||||||
rfc3986==2.0.0 # via oslo-config
|
rfc3986==2.0.0 # via oslo-config
|
||||||
rich==12.6.0 # via -r requirements.in
|
rich==12.6.0 # via -r requirements.in
|
||||||
rpyc==5.3.0 # via -r requirements.in
|
rpyc==5.3.1 # via -r requirements.in
|
||||||
|
rush==2021.4.0 # via -r requirements.in
|
||||||
shellingham==1.5.0.post1 # via -r requirements.in, click-completion
|
shellingham==1.5.0.post1 # via -r requirements.in, click-completion
|
||||||
six==1.16.0 # via -r requirements.in, click-completion, eventlet, imapclient
|
six==1.16.0 # via -r requirements.in, click-completion, eventlet, imapclient
|
||||||
soupsieve==2.3.2.post1 # via beautifulsoup4
|
soupsieve==2.4.1 # via beautifulsoup4
|
||||||
stevedore==4.1.1 # via oslo-config
|
stevedore==5.1.0 # via oslo-config
|
||||||
tabulate==0.9.0 # via -r requirements.in
|
tabulate==0.9.0 # via -r requirements.in
|
||||||
thesmuggler==1.0.1 # via -r requirements.in
|
thesmuggler==1.0.1 # via -r requirements.in
|
||||||
ua-parser==0.16.1 # via user-agents
|
ua-parser==0.18.0 # via user-agents
|
||||||
update-checker==0.18.0 # via -r requirements.in
|
update-checker==0.18.0 # via -r requirements.in
|
||||||
urllib3==1.26.14 # via requests
|
urllib3==2.0.4 # via requests
|
||||||
user-agents==2.2.0 # via -r requirements.in
|
user-agents==2.2.0 # via -r requirements.in
|
||||||
werkzeug==2.1.2 # via -r requirements.in, flask
|
validators==0.20.0 # via click-params
|
||||||
wrapt==1.14.1 # via -r requirements.in, debtcollector
|
werkzeug==2.3.7 # via -r requirements.in, flask
|
||||||
zipp==3.11.0 # via importlib-metadata
|
wrapt==1.15.0 # via -r requirements.in, debtcollector
|
||||||
|
zipp==3.16.2 # via importlib-metadata
|
||||||
|
zope-event==5.0 # via gevent
|
||||||
|
zope-interface==6.0 # via gevent
|
||||||
|
|
||||||
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
|
# setuptools
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ packages =
|
|||||||
|
|
||||||
[entry_points]
|
[entry_points]
|
||||||
console_scripts =
|
console_scripts =
|
||||||
aprsd = aprsd.aprsd:main
|
aprsd = aprsd.main:main
|
||||||
oslo.config.opts =
|
oslo.config.opts =
|
||||||
aprsd.conf = aprsd.conf.opts:list_opts
|
aprsd.conf = aprsd.conf.opts:list_opts
|
||||||
oslo.config.opts.defaults =
|
oslo.config.opts.defaults =
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ from click.testing import CliRunner
|
|||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
from aprsd import conf # noqa : F401
|
from aprsd import conf # noqa : F401
|
||||||
from aprsd.aprsd import cli
|
|
||||||
from aprsd.cmds import send_message # noqa
|
from aprsd.cmds import send_message # noqa
|
||||||
|
from aprsd.main import cli
|
||||||
|
|
||||||
from .. import fake
|
from .. import fake
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ class TestSendMessageCommand(unittest.TestCase):
|
|||||||
CONF.admin.user = "admin"
|
CONF.admin.user = "admin"
|
||||||
CONF.admin.password = "password"
|
CONF.admin.password = "password"
|
||||||
|
|
||||||
@mock.patch("aprsd.logging.log.setup_logging")
|
@mock.patch("aprsd.log.log.setup_logging")
|
||||||
def test_no_tocallsign(self, mock_logging):
|
def test_no_tocallsign(self, mock_logging):
|
||||||
"""Make sure we get an error if there is no tocallsign."""
|
"""Make sure we get an error if there is no tocallsign."""
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ class TestSendMessageCommand(unittest.TestCase):
|
|||||||
assert result.exit_code == 2
|
assert result.exit_code == 2
|
||||||
assert "Error: Missing argument 'TOCALLSIGN'" in result.output
|
assert "Error: Missing argument 'TOCALLSIGN'" in result.output
|
||||||
|
|
||||||
@mock.patch("aprsd.logging.log.setup_logging")
|
@mock.patch("aprsd.log.log.setup_logging")
|
||||||
def test_no_command(self, mock_logging):
|
def test_no_command(self, mock_logging):
|
||||||
"""Make sure we get an error if there is no command."""
|
"""Make sure we get an error if there is no command."""
|
||||||
|
|
||||||
|
|||||||
@@ -32,16 +32,16 @@ class TestSendMessageCommand(unittest.TestCase):
|
|||||||
CONF.admin.user = "admin"
|
CONF.admin.user = "admin"
|
||||||
CONF.admin.password = "password"
|
CONF.admin.password = "password"
|
||||||
|
|
||||||
@mock.patch("aprsd.logging.log.setup_logging")
|
@mock.patch("aprsd.log.log.setup_logging")
|
||||||
def test_init_flask(self, mock_logging):
|
def test_init_flask(self, mock_logging):
|
||||||
"""Make sure we get an error if there is no login and config."""
|
"""Make sure we get an error if there is no login and config."""
|
||||||
|
|
||||||
CliRunner()
|
CliRunner()
|
||||||
self.config_and_init()
|
self.config_and_init()
|
||||||
|
|
||||||
socketio, flask_app = webchat.init_flask("DEBUG", False)
|
socketio = webchat.init_flask("DEBUG", False)
|
||||||
self.assertIsInstance(socketio, flask_socketio.SocketIO)
|
self.assertIsInstance(socketio, flask_socketio.SocketIO)
|
||||||
self.assertIsInstance(flask_app, flask.Flask)
|
self.assertIsInstance(webchat.flask_app, flask.Flask)
|
||||||
|
|
||||||
@mock.patch("aprsd.packets.tracker.PacketTrack.remove")
|
@mock.patch("aprsd.packets.tracker.PacketTrack.remove")
|
||||||
@mock.patch("aprsd.cmds.webchat.socketio")
|
@mock.patch("aprsd.cmds.webchat.socketio")
|
||||||
|
|||||||
@@ -49,51 +49,61 @@ class TestLocationPlugin(test_plugin.TestPlugin):
|
|||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
|
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
|
||||||
@mock.patch("aprsd.plugin_utils.get_weather_gov_for_gps")
|
@mock.patch("geopy.geocoders.Nominatim.reverse")
|
||||||
@mock.patch("time.time")
|
@mock.patch("time.time")
|
||||||
def test_location_unknown_gps(self, mock_time, mock_weather, mock_check_aprs):
|
def test_location_unknown_gps(self, mock_time, mock_geocode, mock_check_aprs):
|
||||||
# When the aprs.fi api key isn't set, then
|
# When the aprs.fi api key isn't set, then
|
||||||
# the LocationPlugin will be disabled.
|
# the LocationPlugin will be disabled.
|
||||||
mock_check_aprs.return_value = {
|
mock_check_aprs.return_value = {
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
"lat": 10,
|
"lat": 1,
|
||||||
"lng": 11,
|
"lng": 1,
|
||||||
"lasttime": 10,
|
"lasttime": 10,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
mock_weather.side_effect = Exception
|
mock_geocode.side_effect = Exception
|
||||||
mock_time.return_value = 10
|
mock_time.return_value = 10
|
||||||
CONF.callsign = fake.FAKE_TO_CALLSIGN
|
CONF.callsign = fake.FAKE_TO_CALLSIGN
|
||||||
fortune = location_plugin.LocationPlugin()
|
fortune = location_plugin.LocationPlugin()
|
||||||
expected = "KFAKE: Unknown Location 0' 10,11 0.0h ago"
|
expected = "KFAKE: Unknown Location 0' 1.00,1.00 0.0h ago"
|
||||||
packet = fake.fake_packet(message="location")
|
packet = fake.fake_packet(message="location")
|
||||||
actual = fortune.filter(packet)
|
actual = fortune.filter(packet)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
|
@mock.patch("aprsd.plugin_utils.get_aprs_fi")
|
||||||
@mock.patch("aprsd.plugin_utils.get_weather_gov_for_gps")
|
@mock.patch("geopy.geocoders.Nominatim.reverse")
|
||||||
@mock.patch("time.time")
|
@mock.patch("time.time")
|
||||||
def test_location_works(self, mock_time, mock_weather, mock_check_aprs):
|
def test_location_works(self, mock_time, mock_geocode, mock_check_aprs):
|
||||||
# When the aprs.fi api key isn't set, then
|
# When the aprs.fi api key isn't set, then
|
||||||
# the LocationPlugin will be disabled.
|
# the LocationPlugin will be disabled.
|
||||||
mock_check_aprs.return_value = {
|
mock_check_aprs.return_value = {
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
{
|
||||||
"lat": 10,
|
"lat": 1,
|
||||||
"lng": 11,
|
"lng": 1,
|
||||||
"lasttime": 10,
|
"lasttime": 10,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
expected_town = "Appomattox, VA"
|
expected = "Appomattox"
|
||||||
wx_data = {"location": {"areaDescription": expected_town}}
|
state = "VA"
|
||||||
mock_weather.return_value = wx_data
|
|
||||||
|
class TempLocation:
|
||||||
|
raw = {
|
||||||
|
"address": {
|
||||||
|
"county": expected,
|
||||||
|
"country_code": "us",
|
||||||
|
"state": state,
|
||||||
|
"country": "United States",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
mock_geocode.return_value = TempLocation()
|
||||||
mock_time.return_value = 10
|
mock_time.return_value = 10
|
||||||
CONF.callsign = fake.FAKE_TO_CALLSIGN
|
CONF.callsign = fake.FAKE_TO_CALLSIGN
|
||||||
fortune = location_plugin.LocationPlugin()
|
fortune = location_plugin.LocationPlugin()
|
||||||
expected = f"KFAKE: {expected_town} 0' 10,11 0.0h ago"
|
expected = f"KFAKE: {expected}, {state} 0' 1.00,1.00 0.0h ago"
|
||||||
packet = fake.fake_packet(message="location")
|
packet = fake.fake_packet(message="location")
|
||||||
actual = fortune.filter(packet)
|
actual = fortune.filter(packet)
|
||||||
self.assertEqual(expected, actual)
|
self.assertEqual(expected, actual)
|
||||||
|
|||||||
Reference in New Issue
Block a user