1
0
mirror of https://github.com/craigerl/aprsd.git synced 2026-08-18 17:44:02 -04:00

Compare commits

..

55 Commits

Author SHA1 Message Date
hemna 12a3113192 Force fortune path during setup test
For whatever reason shutil.which() can't find
fortune in the path, unless you specify the entire path.
2024-01-09 01:30:43 +00:00
hemna 026a64c003 added /usr/games to path 2024-01-08 22:56:45 +00:00
hemna 682e138ec2 Added fortune to Dockerfile-dev 2024-01-08 19:40:08 +00:00
hemna e4e9c6e98b Merge pull request #144 from v-rzh/vrzh-fix-typo-0
aprsd: main.py: Fix premature return in sample_config
2024-01-08 13:01:34 -05:00
hemna f02824b796 Added missing fortune app 2024-01-08 18:00:26 +00:00
Martiros Shakhzadyan 530ac30a09 aprsd: main.py: Fix premature return in sample_config
Fix a typo in sample_config that causes the function to return before
config is generated.
2024-01-04 08:41:06 -05:00
Craig Lamparter 9350cf6534 Update weather.py because you can't sort icons by penis 2023-12-21 11:07:43 -08:00
Craig Lamparter 651cf014b7 Update weather.py both weather plugins have new Ww regex 2023-12-21 11:01:23 -08:00
Craig Lamparter b6df9de8aa Update weather.py
get back the "starts with w" is the weather command regex
2023-12-21 10:54:07 -08:00
hemna 0fd7daaae0 Merge pull request #140 from craigerl/location_plugin
Rework Location Plugin
2023-11-24 19:48:22 -05:00
hemna 0433768784 Fixed a bug with OWMWeatherPlugin
The weather plugin wasn't able to find the from callsign,
so all of the weather reports were random and wrong.
2023-11-24 19:15:52 -05:00
hemna a8f73610fe Rework Location Plugin
This Patch updates the location plugin to allow configuring which
geopy library's supported geocoders.  This patch also adds a fake
geopy geocoder class that uses the us government's API for location.
2023-11-22 20:55:38 -05:00
hemna c0e2ef1199 Update for v3.2.2 release 2023-11-22 12:35:12 -05:00
hemna 809a41f123 Fix for types 2023-11-17 14:23:29 -05:00
hemna b0bfdaa1fb Fix wsgi for prod 2023-11-17 14:02:29 -05:00
hemna b73373db3f Merge pull request #139 from craigerl/walt-test
Walt test
2023-11-17 13:47:05 -05:00
hemna 6b397cbdf1 pep8 fixes 2023-11-17 13:34:10 -05:00
hemna 638128adf8 remove python 3.12 from github builds 2023-11-17 13:15:44 -05:00
hemna b9dd21bc14 Fixed datetime access in core.py 2023-11-17 13:01:55 -05:00
hemna fae7032346 removed invalid reference to config.py 2023-11-17 11:59:50 -05:00
hemna 4b1214de74 Updated requirements 2023-11-17 11:44:12 -05:00
hemna 763c9ab897 Reworked the admin graphs
This patch fixes some bugs wth the rpc for packets as well
as reworks the admin graphs to use echarts.
2023-11-17 11:39:42 -05:00
hemna fe1ebf2ec1 Test new packet serialization 2023-11-17 11:39:42 -05:00
hemna c01037d398 Merge pull request #138 from craigerl/no-internets
Try to localize js libs and css for no internet
2023-10-31 08:04:42 -04:00
hemna 072a1f4430 Merge pull request #137 from jhmartin/mismatched-arguments
Normalize listen --aprs-login
2023-10-28 19:39:09 -04:00
hemna 8b2613ec47 Try to localize js libs and css for no internet
this patch fixes some issues with webchat not loading css and js
when there is no internet.  The index.html was relying on internet
being available to fetch remote css and js.
2023-10-28 19:26:50 -04:00
Jason Martin d39ce76475 Normalize listen --aprs-login
The click block specifies aprs-login but the error indicated aprs_login
2023-10-27 23:39:56 +00:00
hemna 3e9c3612ba Merge pull request #136 from craigerl/dependabot/pip/werkzeug-3.0.1
Bump werkzeug from 2.3.7 to 3.0.1
2023-10-26 09:22:03 -04:00
hemna 8746a9477c Merge pull request #135 from jhmartin/update-installdoc
Update INSTALL with new conf files
2023-10-26 09:20:20 -04:00
dependabot[bot] 7d0524cee5 Bump werkzeug from 2.3.7 to 3.0.1
Bumps [werkzeug](https://github.com/pallets/werkzeug) from 2.3.7 to 3.0.1.
- [Release notes](https://github.com/pallets/werkzeug/releases)
- [Changelog](https://github.com/pallets/werkzeug/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/werkzeug/compare/2.3.7...3.0.1)

---
updated-dependencies:
- dependency-name: werkzeug
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-25 19:55:49 +00:00
Jason Martin 5828643f2e Update INSTALL with new conf files
The name of the config files has changed, update INSTALL with the new names.
2023-10-23 00:14:59 +00:00
hemna 313ea5b6a5 Merge pull request #134 from craigerl/dependabot/pip/urllib3-2.0.7
Bump urllib3 from 2.0.6 to 2.0.7
2023-10-17 17:28:04 -04:00
dependabot[bot] 7853e19c79 Bump urllib3 from 2.0.6 to 2.0.7
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.0.6 to 2.0.7.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.0.6...2.0.7)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-17 21:09:50 +00:00
hemna acf2b62bce Changelog for 3.2.1 2023-10-09 11:39:53 -04:00
Craig Lamparter 8e9a0213e9 Update index.html disable form autocomplete 2023-10-07 10:06:42 -07:00
hemna bf905a0e9f Update the packet_dupe_timeout warning
The warning text was hardcoded at the old 60 second value,
instead of using the config option.
2023-10-06 16:06:41 -04:00
hemna 5ae45ce42f Update the webchat paths
This reorders the paths available for selection in webchat and
sets the selected to default
2023-10-06 16:02:00 -04:00
hemna 0155923341 Changed the path option to a ListOpt
Both serial_kiss and tcp_kiss path option is converted to a ListOpt
to help generate a single line during sample-config generation.
2023-10-06 15:44:25 -04:00
hemna 156d9d9592 Fixed default path for tcp_kiss client.
The tcp_kiss client initialization was using the serial_kiss client's
path setting.
2023-10-06 15:41:12 -04:00
hemna 81169600bd Set a default password for admin
This patch sets a default password of "password" for the admin webui.
2023-10-06 15:32:31 -04:00
hemna 746eeb81b0 Fix path for KISS clients
The kiss client send method was always forcing the config
path.  If a packet has a path specified in it, that will
override the config setting for the kiss client setting in the config.
2023-10-05 18:00:45 -04:00
hemna f41488b48a Added packet_dupe_timeout conf
This patch adds the new packet_dump_timeout config option, defaulting to
60 seconds.   If the same packet matching the from, to, msgNo is RX'd
within that timeout the packet is considered a dupe and will be
dropped.  Ack packets are not subject to dupe checking.
2023-10-05 13:56:02 -04:00
hemna 116f201394 Merge pull request #133 from craigerl/dependabot/pip/urllib3-2.0.6
Bump urllib3 from 2.0.4 to 2.0.6
2023-10-05 10:42:41 -04:00
hemna ddd4d25e9d Add ability to change path on every TX packet
This patch adds the ability to webchat to set the path
on every outbound packet for the KISS clients as well as
the fake client.  The path dropdown includes the options for
Default path (which will default to the config setting)
WIDE1-1,WIDE2-1
ARISS
2023-10-05 10:33:07 -04:00
hemna e2f89a6043 Merge pull request #132 from craigerl/RF_dupe_fix
Fix for dupe packets.
2023-10-03 16:18:34 -04:00
hemna 544600a96b Make Packet objects hashable
This patch makes the packet key a property of the Packet object and
makes packet objects comparable and hashable.
2023-10-03 16:01:43 -04:00
dependabot[bot] c16f3a0bb2 Bump urllib3 from 2.0.4 to 2.0.6
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.0.4 to 2.0.6.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.0.4...2.0.6)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-03 00:04:40 +00:00
hemna 59cec1317d Don't process AckPackets as dupes
If we RX an AckPacket, then send it on for processing.  There is no need
to check for a dupe.
2023-10-02 08:42:00 -04:00
hemna 751bbc2514 Fixed another msgNo int issue 2023-09-29 15:40:42 -04:00
hemna 9bdfd166fd Fixed issue with packet tracker and msgNO Counter
The packet msgNo field is a string, but is typically is an integer
counter to keep track of a specific packet id.  The counter was
returning an int, but the packet.msgNo is a string.  So, when trying to
delete a packet from the packet tracker, the key for accessing the
packet is the msgNo, which has to be a string.  Passing an int, will
cause the packet tracker to not find the packet, and hence silently
fail.

This patch forces the msgNo counter to be a string.
2023-09-29 10:04:15 -04:00
hemna f79b88ec1b Fixed import of Mutablemapping
python 3.10 moved it to collections.abc
2023-09-28 15:30:54 -04:00
hemna 99a0f877f4 pep8 fixes 2023-09-28 12:34:01 -04:00
hemna 4f87d5da12 rewrote packet_list and drop dupe packets
This patch rewrites the packet_list internally to be a dictionary
instead of a list for very fast lookups.  This was needed to test for
duplicate packets already in the list.

This patch drops packets that have the same data and are < 60 seconds
in age from the last time we got the packet.   On RF based clients
we can get dupes!!
2023-09-28 12:19:18 -04:00
hemna 0d7e50d2ba Log a warning on dupe
This patch logs a warning if we detect a dupe packet inbound.
2023-09-27 15:45:39 -04:00
hemna 1f6c55d2bf Fix for dupe packets.
Sometimes over KISS clients (RF), we can get duplicate packets
due to having many digipeters in range of the TNC that aprsd is
connected to.   We will now filter out any dupe packets that aprsd
is still in the process of doing it's 3 acks.
2023-09-27 14:55:47 -04:00
41 changed files with 1315 additions and 394 deletions
+43
View File
@@ -1,9 +1,52 @@
CHANGES CHANGES
======= =======
v3.2.2
------
* Fix for types
* Fix wsgi for prod
* pep8 fixes
* remove python 3.12 from github builds
* Fixed datetime access in core.py
* removed invalid reference to config.py
* Updated requirements
* Reworked the admin graphs
* Test new packet serialization
* Try to localize js libs and css for no internet
* Normalize listen --aprs-login
* Bump werkzeug from 2.3.7 to 3.0.1
* Update INSTALL with new conf files
* Bump urllib3 from 2.0.6 to 2.0.7
v3.2.1
------
* Changelog for 3.2.1
* Update index.html disable form autocomplete
* Update the packet\_dupe\_timeout warning
* Update the webchat paths
* Changed the path option to a ListOpt
* Fixed default path for tcp\_kiss client
* Set a default password for admin
* Fix path for KISS clients
* Added packet\_dupe\_timeout conf
* Add ability to change path on every TX packet
* Make Packet objects hashable
* Bump urllib3 from 2.0.4 to 2.0.6
* Don't process AckPackets as dupes
* Fixed another msgNo int issue
* Fixed issue with packet tracker and msgNO Counter
* Fixed import of Mutablemapping
* pep8 fixes
* rewrote packet\_list and drop dupe packets
* Log a warning on dupe
* Fix for dupe packets
v3.2.0 v3.2.0
------ ------
* Update Changelog for 3.2.0
* minor cleanup prior to release * minor cleanup prior to release
* Webchat: fix input maxlength * Webchat: fix input maxlength
* WebChat: cleanup some console.logs * WebChat: cleanup some console.logs
+3 -2
View File
@@ -27,9 +27,10 @@ pip install -e .
# CONFIGURE # CONFIGURE
# Now configure aprsd HERE # Now configure aprsd HERE
./aprsd sample-config # generates a config.yml template mkdir -p ~/.config/aprsd
./aprsd sample-config > ~/.config/aprsd/aprsd.conf # generates a config template
vi ~/.config/aprsd/config.yml # copy/edit config here vi ~/.config/aprsd/aprsd.conf # copy/edit config here
aprsd server aprsd server
+35 -1
View File
@@ -7,7 +7,7 @@ from aprslib.exceptions import LoginError
from oslo_config import cfg from oslo_config import cfg
from aprsd import exception from aprsd import exception
from aprsd.clients import aprsis, kiss from aprsd.clients import aprsis, fake, kiss
from aprsd.packets import core, packet_list from aprsd.packets import core, packet_list
from aprsd.utils import trace from aprsd.utils import trace
@@ -17,6 +17,7 @@ LOG = logging.getLogger("APRSD")
TRANSPORT_APRSIS = "aprsis" TRANSPORT_APRSIS = "aprsis"
TRANSPORT_TCPKISS = "tcpkiss" TRANSPORT_TCPKISS = "tcpkiss"
TRANSPORT_SERIALKISS = "serialkiss" TRANSPORT_SERIALKISS = "serialkiss"
TRANSPORT_FAKE = "fake"
# Main must create this from the ClientFactory # Main must create this from the ClientFactory
# object such that it's populated with the # object such that it's populated with the
@@ -248,6 +249,35 @@ class KISSClient(Client, metaclass=trace.TraceWrapperMetaclass):
return self._client return self._client
class APRSDFakeClient(Client, metaclass=trace.TraceWrapperMetaclass):
@staticmethod
def is_enabled():
if CONF.fake_client.enabled:
return True
return False
@staticmethod
def is_configured():
return APRSDFakeClient.is_enabled()
def is_alive(self):
return True
def setup_connection(self):
return fake.APRSDFakeClient()
@staticmethod
def transport():
return TRANSPORT_FAKE
def decode_packet(self, *args, **kwargs):
LOG.debug(f"kwargs {kwargs}")
pkt = kwargs["packet"]
LOG.debug(f"Got an APRS Fake Packet '{pkt}'")
return pkt
class ClientFactory: class ClientFactory:
_instance = None _instance = None
@@ -270,8 +300,11 @@ class ClientFactory:
key = TRANSPORT_APRSIS key = TRANSPORT_APRSIS
elif KISSClient.is_enabled(): elif KISSClient.is_enabled():
key = KISSClient.transport() key = KISSClient.transport()
elif APRSDFakeClient.is_enabled():
key = TRANSPORT_FAKE
builder = self._builders.get(key) builder = self._builders.get(key)
LOG.debug(f"Creating client {key}")
if not builder: if not builder:
raise ValueError(key) raise ValueError(key)
return builder() return builder()
@@ -312,3 +345,4 @@ class ClientFactory:
factory.register(TRANSPORT_APRSIS, APRSISClient) factory.register(TRANSPORT_APRSIS, APRSISClient)
factory.register(TRANSPORT_TCPKISS, KISSClient) factory.register(TRANSPORT_TCPKISS, KISSClient)
factory.register(TRANSPORT_SERIALKISS, KISSClient) factory.register(TRANSPORT_SERIALKISS, KISSClient)
factory.register(TRANSPORT_FAKE, APRSDFakeClient)
+1 -1
View File
@@ -106,7 +106,7 @@ class Aprsdis(aprslib.IS):
aprsd.__version__, aprsd.__version__,
) )
self.logger.info("Sending login information") self.logger.debug("Sending login information")
try: try:
self._sendall(login_str) self._sendall(login_str)
+73
View File
@@ -0,0 +1,73 @@
import logging
import threading
import time
import aprslib
from oslo_config import cfg
import wrapt
from aprsd import conf # noqa
from aprsd.packets import core
from aprsd.utils import trace
CONF = cfg.CONF
LOG = logging.getLogger("APRSD")
class APRSDFakeClient(metaclass=trace.TraceWrapperMetaclass):
'''Fake client for testing.'''
# flag to tell us to stop
thread_stop = False
lock = threading.Lock()
path = []
def __init__(self):
LOG.info("Starting APRSDFakeClient client.")
self.path = ["WIDE1-1", "WIDE2-1"]
def stop(self):
self.thread_stop = True
LOG.info("Shutdown APRSDFakeClient client.")
def is_alive(self):
"""If the connection is alive or not."""
return not self.thread_stop
@wrapt.synchronized(lock)
def send(self, packet: core.Packet):
"""Send an APRS Message object."""
LOG.info(f"Sending packet: {packet}")
payload = None
if isinstance(packet, core.Packet):
packet.prepare()
payload = packet.payload.encode("US-ASCII")
if packet.path:
packet.path
else:
self.path
else:
msg_payload = f"{packet.raw}{{{str(packet.msgNo)}"
payload = (
":{:<9}:{}".format(
packet.to_call,
msg_payload,
)
).encode("US-ASCII")
LOG.debug(
f"FAKE::Send '{payload}' TO '{packet.to_call}' From "
f"'{packet.from_call}' with PATH \"{self.path}\"",
)
def consumer(self, callback, blocking=False, immortal=False, raw=False):
LOG.debug("Start non blocking FAKE consumer")
# Generate packets here?
raw = "GTOWN>APDW16,WIDE1-1,WIDE2-1:}KM6LYW-9>APZ100,TCPIP,GTOWN*::KM6LYW :KM6LYW: 19 Miles SW"
pkt_raw = aprslib.parse(raw)
pkt = core.Packet.factory(pkt_raw)
callback(packet=pkt)
LOG.debug(f"END blocking FAKE consumer {self}")
time.sleep(8)
+7 -1
View File
@@ -14,6 +14,8 @@ LOG = logging.getLogger("APRSD")
class KISS3Client: class KISS3Client:
path = []
def __init__(self): def __init__(self):
self.setup() self.setup()
@@ -34,6 +36,7 @@ class KISS3Client:
speed=CONF.kiss_serial.baudrate, speed=CONF.kiss_serial.baudrate,
strip_df_start=True, strip_df_start=True,
) )
self.path = CONF.kiss_serial.path
elif CONF.kiss_tcp.enabled: elif CONF.kiss_tcp.enabled:
LOG.debug( LOG.debug(
"KISS({}) TCP Connection to {}:{}".format( "KISS({}) TCP Connection to {}:{}".format(
@@ -47,6 +50,7 @@ class KISS3Client:
port=CONF.kiss_tcp.port, port=CONF.kiss_tcp.port,
strip_df_start=True, strip_df_start=True,
) )
self.path = CONF.kiss_tcp.path
LOG.debug("Starting KISS interface connection") LOG.debug("Starting KISS interface connection")
self.kiss.start() self.kiss.start()
@@ -87,10 +91,12 @@ class KISS3Client:
"""Send an APRS Message object.""" """Send an APRS Message object."""
payload = None payload = None
path = ["WIDE1-1", "WIDE2-1"] path = self.path
if isinstance(packet, core.Packet): if isinstance(packet, core.Packet):
packet.prepare() packet.prepare()
payload = packet.payload.encode("US-ASCII") payload = packet.payload.encode("US-ASCII")
if packet.path:
path = packet.path
else: else:
msg_payload = f"{packet.raw}{{{str(packet.msgNo)}" msg_payload = f"{packet.raw}{{{str(packet.msgNo)}"
payload = ( payload = (
+1 -1
View File
@@ -144,7 +144,7 @@ def listen(
if not aprs_login: if not aprs_login:
click.echo(ctx.get_help()) click.echo(ctx.get_help())
click.echo("") click.echo("")
ctx.fail("Must set --aprs_login or APRS_LOGIN") ctx.fail("Must set --aprs-login or APRS_LOGIN")
ctx.exit() ctx.exit()
if not aprs_password: if not aprs_password:
+31 -20
View File
@@ -131,9 +131,9 @@ class WebChatProcessPacketThread(rx.APRSDProcessPacketThread):
def process_ack_packet(self, packet: packets.AckPacket): def process_ack_packet(self, packet: packets.AckPacket):
super().process_ack_packet(packet) super().process_ack_packet(packet)
ack_num = packet.get("msgNo") ack_num = packet.get("msgNo")
SentMessages().ack(int(ack_num)) SentMessages().ack(ack_num)
self.socketio.emit( self.socketio.emit(
"ack", SentMessages().get(int(ack_num)), "ack", SentMessages().get(ack_num),
namespace="/sendmsg", namespace="/sendmsg",
) )
self.got_ack = True self.got_ack = True
@@ -157,25 +157,26 @@ def _get_transport(stats):
"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: elif client.KISSClient.is_enabled():
# We might be connected to a KISS socket? transport = client.KISSClient.transport()
if client.KISSClient.is_enabled(): if transport == client.TRANSPORT_TCPKISS:
transport = client.KISSClient.transport() aprs_connection = (
if transport == client.TRANSPORT_TCPKISS: "TCPKISS://{}:{}".format(
aprs_connection = ( CONF.kiss_tcp.host,
"TCPKISS://{}:{}".format( CONF.kiss_tcp.port,
CONF.kiss_tcp.host,
CONF.kiss_tcp.port,
)
)
elif transport == client.TRANSPORT_SERIALKISS:
# for pep8 violation
aprs_connection = (
"SerialKISS://{}@{} baud".format(
CONF.kiss_serial.device,
CONF.kiss_serial.baudrate,
),
) )
)
elif transport == client.TRANSPORT_SERIALKISS:
# for pep8 violation
aprs_connection = (
"SerialKISS://{}@{} baud".format(
CONF.kiss_serial.device,
CONF.kiss_serial.baudrate,
),
)
elif CONF.fake_client.enabled:
transport = client.TRANSPORT_FAKE
aprs_connection = "Fake Client"
return transport, aprs_connection return transport, aprs_connection
@@ -279,10 +280,20 @@ class SendMessageNamespace(Namespace):
LOG.debug(f"WS: on_send {data}") LOG.debug(f"WS: on_send {data}")
self.request = data self.request = data
data["from"] = CONF.callsign data["from"] = CONF.callsign
path = data.get("path", None)
if not path:
path = []
elif "," in path:
path_opts = path.split(",")
path = [x.strip() for x in path_opts]
else:
path = [path]
pkt = packets.MessagePacket( pkt = packets.MessagePacket(
from_call=data["from"], from_call=data["from"],
to_call=data["to"].upper(), to_call=data["to"].upper(),
message_text=data["message"], message_text=data["message"],
path=path,
) )
pkt.prepare() pkt.prepare()
self.msg = pkt self.msg = pkt
+29
View File
@@ -11,14 +11,21 @@ aprs_group = cfg.OptGroup(
name="aprs_network", name="aprs_network",
title="APRS-IS Network settings", title="APRS-IS Network settings",
) )
kiss_serial_group = cfg.OptGroup( kiss_serial_group = cfg.OptGroup(
name="kiss_serial", name="kiss_serial",
title="KISS Serial device connection", title="KISS Serial device connection",
) )
kiss_tcp_group = cfg.OptGroup( kiss_tcp_group = cfg.OptGroup(
name="kiss_tcp", name="kiss_tcp",
title="KISS TCP/IP Device connection", title="KISS TCP/IP Device connection",
) )
fake_client_group = cfg.OptGroup(
name="fake_client",
title="Fake Client settings",
)
aprs_opts = [ aprs_opts = [
cfg.BoolOpt( cfg.BoolOpt(
"enabled", "enabled",
@@ -65,6 +72,11 @@ kiss_serial_opts = [
default=9600, default=9600,
help="The Serial device baud rate for communication", help="The Serial device baud rate for communication",
), ),
cfg.ListOpt(
"path",
default=["WIDE1-1", "WIDE2-1"],
help="The APRS path to use for wide area coverage.",
),
] ]
kiss_tcp_opts = [ kiss_tcp_opts = [
@@ -82,6 +94,19 @@ kiss_tcp_opts = [
default=8001, default=8001,
help="The KISS TCP/IP network port", help="The KISS TCP/IP network port",
), ),
cfg.ListOpt(
"path",
default=["WIDE1-1", "WIDE2-1"],
help="The APRS path to use for wide area coverage.",
),
]
fake_client_opts = [
cfg.BoolOpt(
"enabled",
default=False,
help="Enable fake client connection.",
),
] ]
@@ -93,10 +118,14 @@ def register_opts(config):
config.register_opts(kiss_serial_opts, group=kiss_serial_group) config.register_opts(kiss_serial_opts, group=kiss_serial_group)
config.register_opts(kiss_tcp_opts, group=kiss_tcp_group) config.register_opts(kiss_tcp_opts, group=kiss_tcp_group)
config.register_group(fake_client_group)
config.register_opts(fake_client_opts, group=fake_client_group)
def list_opts(): def list_opts():
return { return {
aprs_group.name: aprs_opts, aprs_group.name: aprs_opts,
kiss_serial_group.name: kiss_serial_opts, kiss_serial_group.name: kiss_serial_opts,
kiss_tcp_group.name: kiss_tcp_opts, kiss_tcp_group.name: kiss_tcp_opts,
fake_client_group.name: fake_client_opts,
} }
+6
View File
@@ -65,6 +65,11 @@ aprsd_opts = [
"2 means 1 packet every 2 seconds allowed." "2 means 1 packet every 2 seconds allowed."
"5 means 1 pack packet every 5 seconds allowed", "5 means 1 pack packet every 5 seconds allowed",
), ),
cfg.IntOpt(
"packet_dupe_timeout",
default=60,
help="The number of seconds before a packet is not considered a duplicate.",
),
] ]
watch_list_opts = [ watch_list_opts = [
@@ -119,6 +124,7 @@ admin_opts = [
), ),
cfg.StrOpt( cfg.StrOpt(
"password", "password",
default="password",
secret=True, secret=True,
help="Admin interface password", help="Admin interface password",
), ),
+108
View File
@@ -18,6 +18,11 @@ owm_wx_group = cfg.OptGroup(
title="Options for the OWMWeatherPlugin", title="Options for the OWMWeatherPlugin",
) )
location_group = cfg.OptGroup(
name="location_plugin",
title="Options for the LocationPlugin",
)
aprsfi_opts = [ aprsfi_opts = [
cfg.StrOpt( cfg.StrOpt(
"apiKey", "apiKey",
@@ -62,6 +67,106 @@ avwx_opts = [
), ),
] ]
location_opts = [
cfg.StrOpt(
"geopy_geocoder",
choices=[
"ArcGIS", "AzureMaps", "Baidu", "Bing", "GoogleV3", "HERE",
"Nominatim", "OpenCage", "TomTom", "USGov", "What3Words", "Woosmap",
],
default="Nominatim",
help="The geopy geocoder to use. Default is Nominatim."
"See https://geopy.readthedocs.io/en/stable/#module-geopy.geocoders"
"for more information.",
),
cfg.StrOpt(
"user_agent",
default="APRSD",
help="The user agent to use for the Nominatim geocoder."
"See https://geopy.readthedocs.io/en/stable/#module-geopy.geocoders"
"for more information.",
),
cfg.StrOpt(
"arcgis_username",
default=None,
help="The username to use for the ArcGIS geocoder."
"See https://geopy.readthedocs.io/en/latest/#arcgis"
"for more information."
"Only used for the ArcGIS geocoder.",
),
cfg.StrOpt(
"arcgis_password",
default=None,
help="The password to use for the ArcGIS geocoder."
"See https://geopy.readthedocs.io/en/latest/#arcgis"
"for more information."
"Only used for the ArcGIS geocoder.",
),
cfg.StrOpt(
"azuremaps_subscription_key",
help="The subscription key to use for the AzureMaps geocoder."
"See https://geopy.readthedocs.io/en/latest/#azuremaps"
"for more information."
"Only used for the AzureMaps geocoder.",
),
cfg.StrOpt(
"baidu_api_key",
help="The API key to use for the Baidu geocoder."
"See https://geopy.readthedocs.io/en/latest/#baidu"
"for more information."
"Only used for the Baidu geocoder.",
),
cfg.StrOpt(
"bing_api_key",
help="The API key to use for the Bing geocoder."
"See https://geopy.readthedocs.io/en/latest/#bing"
"for more information."
"Only used for the Bing geocoder.",
),
cfg.StrOpt(
"google_api_key",
help="The API key to use for the Google geocoder."
"See https://geopy.readthedocs.io/en/latest/#googlev3"
"for more information."
"Only used for the Google geocoder.",
),
cfg.StrOpt(
"here_api_key",
help="The API key to use for the HERE geocoder."
"See https://geopy.readthedocs.io/en/latest/#here"
"for more information."
"Only used for the HERE geocoder.",
),
cfg.StrOpt(
"opencage_api_key",
help="The API key to use for the OpenCage geocoder."
"See https://geopy.readthedocs.io/en/latest/#opencage"
"for more information."
"Only used for the OpenCage geocoder.",
),
cfg.StrOpt(
"tomtom_api_key",
help="The API key to use for the TomTom geocoder."
"See https://geopy.readthedocs.io/en/latest/#tomtom"
"for more information."
"Only used for the TomTom geocoder.",
),
cfg.StrOpt(
"what3words_api_key",
help="The API key to use for the What3Words geocoder."
"See https://geopy.readthedocs.io/en/latest/#what3words"
"for more information."
"Only used for the What3Words geocoder.",
),
cfg.StrOpt(
"woosmap_api_key",
help="The API key to use for the Woosmap geocoder."
"See https://geopy.readthedocs.io/en/latest/#woosmap"
"for more information."
"Only used for the Woosmap geocoder.",
),
]
def register_opts(config): def register_opts(config):
config.register_group(aprsfi_group) config.register_group(aprsfi_group)
@@ -72,6 +177,8 @@ def register_opts(config):
config.register_opts(owm_wx_opts, group=owm_wx_group) config.register_opts(owm_wx_opts, group=owm_wx_group)
config.register_group(avwx_group) config.register_group(avwx_group)
config.register_opts(avwx_opts, group=avwx_group) config.register_opts(avwx_opts, group=avwx_group)
config.register_group(location_group)
config.register_opts(location_opts, group=location_group)
def list_opts(): def list_opts():
@@ -80,4 +187,5 @@ def list_opts():
query_group.name: query_plugin_opts, query_group.name: query_plugin_opts,
owm_wx_group.name: owm_wx_opts, owm_wx_group.name: owm_wx_opts,
avwx_group.name: avwx_opts, avwx_group.name: avwx_opts,
location_group.name: location_opts,
} }
+2
View File
@@ -145,7 +145,9 @@ def sample_config(ctx):
if not sys.argv[1:]: if not sys.argv[1:]:
raise SystemExit raise SystemExit
raise raise
LOG.warning(conf.namespace)
generator.generate(conf) generator.generate(conf)
return
@cli.command() @cli.command()
+2 -2
View File
@@ -1,6 +1,6 @@
from aprsd.packets.core import ( # noqa: F401 from aprsd.packets.core import ( # noqa: F401
AckPacket, GPSPacket, MessagePacket, MicEPacket, Packet, PathPacket, AckPacket, GPSPacket, MessagePacket, MicEPacket, Packet, RejectPacket,
RejectPacket, StatusPacket, WeatherPacket, StatusPacket, WeatherPacket,
) )
from aprsd.packets.packet_list import PacketList # noqa: F401 from aprsd.packets.packet_list import PacketList # noqa: F401
from aprsd.packets.seen_list import SeenList # noqa: F401 from aprsd.packets.seen_list import SeenList # noqa: F401
+93 -61
View File
@@ -1,7 +1,6 @@
import abc import abc
from dataclasses import asdict, dataclass, field from dataclasses import asdict, dataclass, field
import datetime from datetime import datetime
import json
import logging import logging
import re import re
import time import time
@@ -9,9 +8,9 @@ import time
from typing import List from typing import List
import dacite import dacite
from dataclasses_json import dataclass_json
from aprsd.utils import counter from aprsd.utils import counter
from aprsd.utils import json as aprsd_json
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@@ -28,12 +27,20 @@ PACKET_TYPE_BEACON = "beacon"
PACKET_TYPE_THIRDPARTY = "thirdparty" PACKET_TYPE_THIRDPARTY = "thirdparty"
PACKET_TYPE_UNCOMPRESSED = "uncompressed" PACKET_TYPE_UNCOMPRESSED = "uncompressed"
NO_DATE = datetime(1900, 10, 24)
def _int_timestamp():
def _init_timestamp():
"""Build a unix style timestamp integer""" """Build a unix style timestamp integer"""
return int(round(time.time())) return int(round(time.time()))
def _init_send_time():
# We have to use a datetime here, or the json encoder
# Fails on a NoneType.
return NO_DATE
def _init_msgNo(): # noqa: N802 def _init_msgNo(): # noqa: N802
"""For some reason __post__init doesn't get called. """For some reason __post__init doesn't get called.
@@ -45,42 +52,66 @@ def _init_msgNo(): # noqa: N802
return c.value return c.value
@dataclass def factory_from_dict(packet_dict):
pkt_type = get_packet_type(packet_dict)
if pkt_type:
cls = TYPE_LOOKUP[pkt_type]
return cls.from_dict(packet_dict)
def factory_from_json(packet_dict):
pkt_type = get_packet_type(packet_dict)
if pkt_type:
return TYPE_LOOKUP[pkt_type].from_json(packet_dict)
@dataclass_json
@dataclass(unsafe_hash=True)
class Packet(metaclass=abc.ABCMeta): class Packet(metaclass=abc.ABCMeta):
from_call: str from_call: str = field(default=None)
to_call: str to_call: str = field(default=None)
addresse: str = None addresse: str = field(default=None)
format: str = None format: str = field(default=None)
msgNo: str = field(default_factory=_init_msgNo) # noqa: N815 msgNo: str = field(default_factory=_init_msgNo) # noqa: N815
packet_type: str = None packet_type: str = field(default=None)
timestamp: float = field(default_factory=_int_timestamp) timestamp: float = field(default_factory=_init_timestamp, compare=False, hash=False)
# Holds the raw text string to be sent over the wire # Holds the raw text string to be sent over the wire
# or holds the raw string from input packet # or holds the raw string from input packet
raw: str = None raw: str = field(default=None, compare=False, hash=False)
raw_dict: dict = field(repr=False, default_factory=lambda: {}) raw_dict: dict = field(repr=False, default_factory=lambda: {}, compare=False, hash=False)
# Built by calling prepare(). raw needs this built first. # Built by calling prepare(). raw needs this built first.
payload: str = None payload: str = field(default=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, compare=False, hash=False)
retry_count: int = field(repr=False, default=3) retry_count: int = field(repr=False, default=3, compare=False, hash=False)
last_send_time: datetime.timedelta = field(repr=False, default=None) # last_send_time: datetime = field(
# metadata=dc_json_config(
# encoder=datetime.isoformat,
# decoder=datetime.fromisoformat,
# ),
# repr=True,
# default_factory=_init_send_time,
# compare=False,
# hash=False
# )
last_send_time: float = field(repr=False, default=0, compare=False, hash=False)
last_send_attempt: int = field(repr=False, default=0, compare=False, hash=False)
# Do we allow this packet to be saved to send later? # Do we allow this packet to be saved to send later?
allow_delay: bool = field(repr=False, default=True) allow_delay: bool = field(repr=False, default=True, compare=False, hash=False)
path: List[str] = field(default_factory=list, compare=False, hash=False)
via: str = field(default=None, compare=False, hash=False)
def __post__init__(self): def __post__init__(self):
LOG.warning(f"POST INIT {self}") LOG.warning(f"POST INIT {self}")
@property
def __dict__(self):
return asdict(self)
@property @property
def json(self): def json(self):
""" """
get the json formated string get the json formated string
""" """
return json.dumps(self.__dict__, cls=aprsd_json.EnhancedJSONEncoder) return self.to_json()
def get(self, key, default=None): def get(self, key, default=None):
"""Emulate a getter on a dict.""" """Emulate a getter on a dict."""
@@ -89,8 +120,13 @@ class Packet(metaclass=abc.ABCMeta):
else: else:
return default return default
@property
def key(self):
"""Build a key for finding this packet in a dict."""
return f"{self.from_call}:{self.addresse}:{self.msgNo}"
def update_timestamp(self): def update_timestamp(self):
self.timestamp = _int_timestamp() self.timestamp = _init_timestamp()
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."""
@@ -258,18 +294,9 @@ class Packet(metaclass=abc.ABCMeta):
return repr return repr
@dataclass @dataclass(unsafe_hash=True)
class PathPacket(Packet): class AckPacket(Packet):
path: List[str] = field(default_factory=list) response: str = field(default=None)
via: str = None
def _build_payload(self):
raise NotImplementedError
@dataclass
class AckPacket(PathPacket):
response: str = None
def __post__init__(self): def __post__init__(self):
if self.response: if self.response:
@@ -279,9 +306,9 @@ class AckPacket(PathPacket):
self.payload = f":{self.to_call.ljust(9)}:ack{self.msgNo}" self.payload = f":{self.to_call.ljust(9)}:ack{self.msgNo}"
@dataclass @dataclass(unsafe_hash=True)
class RejectPacket(PathPacket): class RejectPacket(Packet):
response: str = None response: str = field(default=None)
def __post__init__(self): def __post__init__(self):
if self.response: if self.response:
@@ -291,9 +318,10 @@ class RejectPacket(PathPacket):
self.payload = f":{self.to_call.ljust(9)} :rej{self.msgNo}" self.payload = f":{self.to_call.ljust(9)} :rej{self.msgNo}"
@dataclass @dataclass_json
class MessagePacket(PathPacket): @dataclass(unsafe_hash=True)
message_text: str = None class MessagePacket(Packet):
message_text: str = field(default=None)
def _filter_for_send(self) -> str: def _filter_for_send(self) -> str:
"""Filter and format message string for FCC.""" """Filter and format message string for FCC."""
@@ -313,24 +341,24 @@ class MessagePacket(PathPacket):
) )
@dataclass @dataclass(unsafe_hash=True)
class StatusPacket(PathPacket): class StatusPacket(Packet):
status: str = None status: str = field(default=None)
messagecapable: bool = False messagecapable: bool = field(default=False)
comment: str = None comment: str = field(default=None)
def _build_payload(self): def _build_payload(self):
raise NotImplementedError raise NotImplementedError
@dataclass @dataclass(unsafe_hash=True)
class GPSPacket(PathPacket): class GPSPacket(Packet):
latitude: float = 0.00 latitude: float = field(default=0.00)
longitude: float = 0.00 longitude: float = field(default=0.00)
altitude: float = 0.00 altitude: float = field(default=0.00)
rng: float = 0.00 rng: float = field(default=0.00)
posambiguity: int = 0 posambiguity: int = field(default=0)
comment: str = None comment: str = field(default=None)
symbol: str = field(default="l") symbol: str = field(default="l")
symbol_table: str = field(default="/") symbol_table: str = field(default="/")
@@ -408,12 +436,12 @@ class GPSPacket(PathPacket):
def _build_time_zulu(self): def _build_time_zulu(self):
"""Build the timestamp in UTC/zulu.""" """Build the timestamp in UTC/zulu."""
if self.timestamp: if self.timestamp:
local_dt = datetime.datetime.fromtimestamp(self.timestamp) local_dt = datetime.fromtimestamp(self.timestamp)
else: else:
local_dt = datetime.datetime.now() local_dt = datetime.now()
self.timestamp = datetime.datetime.timestamp(local_dt) self.timestamp = datetime.timestamp(local_dt)
utc_offset_timedelta = datetime.datetime.utcnow() - local_dt utc_offset_timedelta = datetime.utcnow() - local_dt
result_utc_datetime = local_dt + utc_offset_timedelta result_utc_datetime = local_dt + utc_offset_timedelta
time_zulu = result_utc_datetime.strftime("%d%H%M") time_zulu = result_utc_datetime.strftime("%d%H%M")
return time_zulu return time_zulu
@@ -569,7 +597,7 @@ class WeatherPacket(GPSPacket):
class ThirdParty(Packet): class ThirdParty(Packet):
# Holds the encapsulated packet # Holds the encapsulated packet
subpacket: Packet = None subpacket: Packet = field(default=None, compare=True, hash=False)
def __repr__(self): def __repr__(self):
"""Build the repr version of the packet.""" """Build the repr version of the packet."""
@@ -602,7 +630,7 @@ def get_packet_type(packet: dict):
pkt_format = packet.get("format", None) pkt_format = packet.get("format", None)
msg_response = packet.get("response", None) msg_response = packet.get("response", None)
packet_type = "unknown" packet_type = PACKET_TYPE_UNKNOWN
if pkt_format == "message" and msg_response == "ack": if pkt_format == "message" and msg_response == "ack":
packet_type = PACKET_TYPE_ACK packet_type = PACKET_TYPE_ACK
elif pkt_format == "message" and msg_response == "rej": elif pkt_format == "message" and msg_response == "rej":
@@ -622,6 +650,10 @@ def get_packet_type(packet: dict):
packet_type = PACKET_TYPE_WX packet_type = PACKET_TYPE_WX
elif pkt_format == PACKET_TYPE_THIRDPARTY: elif pkt_format == PACKET_TYPE_THIRDPARTY:
packet_type = PACKET_TYPE_THIRDPARTY packet_type = PACKET_TYPE_THIRDPARTY
if packet_type == PACKET_TYPE_UNKNOWN:
if "latitude" in packet:
packet_type = PACKET_TYPE_BEACON
return packet_type return packet_type
+53 -15
View File
@@ -1,10 +1,12 @@
from collections import OrderedDict
from collections.abc import MutableMapping
import logging import logging
import threading import threading
from oslo_config import cfg from oslo_config import cfg
import wrapt import wrapt
from aprsd import stats, utils from aprsd import stats
from aprsd.packets import seen_list from aprsd.packets import seen_list
@@ -12,31 +14,29 @@ CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
class PacketList: class PacketList(MutableMapping):
"""Class to track all of the packets rx'd and tx'd by aprsd."""
_instance = None _instance = None
lock = threading.Lock() lock = threading.Lock()
packet_list: utils.RingBuffer = utils.RingBuffer(1000)
_total_rx: int = 0 _total_rx: int = 0
_total_tx: int = 0 _total_tx: int = 0
types = {}
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if cls._instance is None: if cls._instance is None:
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
cls._maxlen = 100
cls.d = OrderedDict()
return cls._instance return cls._instance
@wrapt.synchronized(lock)
def __iter__(self):
return iter(self.packet_list)
@wrapt.synchronized(lock) @wrapt.synchronized(lock)
def rx(self, packet): def rx(self, packet):
"""Add a packet that was received.""" """Add a packet that was received."""
self._total_rx += 1 self._total_rx += 1
self.packet_list.append(packet) self._add(packet)
ptype = packet.__class__.__name__
if not ptype in self.types:
self.types[ptype] = {"tx": 0, "rx": 0}
self.types[ptype]["rx"] += 1
seen_list.SeenList().update_seen(packet) seen_list.SeenList().update_seen(packet)
stats.APRSDStats().rx(packet) stats.APRSDStats().rx(packet)
@@ -44,13 +44,51 @@ class PacketList:
def tx(self, packet): def tx(self, packet):
"""Add a packet that was received.""" """Add a packet that was received."""
self._total_tx += 1 self._total_tx += 1
self.packet_list.append(packet) self._add(packet)
ptype = packet.__class__.__name__
if not ptype in self.types:
self.types[ptype] = {"tx": 0, "rx": 0}
self.types[ptype]["tx"] += 1
seen_list.SeenList().update_seen(packet) seen_list.SeenList().update_seen(packet)
stats.APRSDStats().tx(packet) stats.APRSDStats().tx(packet)
@wrapt.synchronized(lock) @wrapt.synchronized(lock)
def get(self): def add(self, packet):
return self.packet_list.get() self._add(packet)
def _add(self, packet):
self[packet.key] = packet
def copy(self):
return self.d.copy()
@property
def maxlen(self):
return self._maxlen
@wrapt.synchronized(lock)
def find(self, packet):
return self.get(packet.key)
def __getitem__(self, key):
# self.d.move_to_end(key)
return self.d[key]
def __setitem__(self, key, value):
if key in self.d:
self.d.move_to_end(key)
elif len(self.d) == self.maxlen:
self.d.popitem(last=False)
self.d[key] = value
def __delitem__(self, key):
del self.d[key]
def __iter__(self):
return self.d.__iter__()
def __len__(self):
return len(self.d)
@wrapt.synchronized(lock) @wrapt.synchronized(lock)
def total_rx(self): def total_rx(self):
+9 -16
View File
@@ -62,36 +62,29 @@ class PacketTrack(objectstore.ObjectStoreMixin):
def __len__(self): def __len__(self):
return len(self.data) return len(self.data)
@wrapt.synchronized(lock)
def __str__(self):
result = "{"
for key in self.data.keys():
result += f"{key}: {str(self.data[key])}, "
result += "}"
return result
@wrapt.synchronized(lock) @wrapt.synchronized(lock)
def add(self, packet): def add(self, packet):
key = int(packet.msgNo) key = packet.msgNo
packet._last_send_attempt = 0
self.data[key] = packet self.data[key] = packet
self.total_tracked += 1 self.total_tracked += 1
@wrapt.synchronized(lock) @wrapt.synchronized(lock)
def get(self, id): def get(self, key):
if id in self.data: return self.data.get(key, None)
return self.data[id]
@wrapt.synchronized(lock) @wrapt.synchronized(lock)
def remove(self, id): def remove(self, key):
key = int(id) try:
if key in self.data.keys():
del self.data[key] del self.data[key]
except KeyError:
pass
def restart(self): def restart(self):
"""Walk the list of messages and restart them if any.""" """Walk the list of messages and restart them if any."""
for key in self.data.keys(): for key in self.data.keys():
pkt = self.data[key] pkt = self.data[key]
if pkt.last_send_attempt < pkt.retry_count: if pkt._last_send_attempt < pkt.retry_count:
tx.send(pkt) tx.send(pkt)
def _resend(self, packet): def _resend(self, packet):
+1
View File
@@ -76,6 +76,7 @@ def fetch_openweathermap(api_key, lat, lon, units="metric", exclude=None):
exclude, exclude,
) )
) )
LOG.debug(f"Fetching OWM weather '{url}'")
response = requests.get(url) response = requests.get(url)
except Exception as e: except Exception as e:
LOG.error(e) LOG.error(e)
+4 -1
View File
@@ -8,6 +8,8 @@ from aprsd.utils import trace
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
DEFAULT_FORTUNE_PATH = '/usr/games/fortune'
class FortunePlugin(plugin.APRSDRegexCommandPluginBase): class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
"""Fortune.""" """Fortune."""
@@ -19,7 +21,8 @@ class FortunePlugin(plugin.APRSDRegexCommandPluginBase):
fortune_path = None fortune_path = None
def setup(self): def setup(self):
self.fortune_path = shutil.which("fortune") self.fortune_path = shutil.which(DEFAULT_FORTUNE_PATH)
LOG.info(f"Fortune path {self.fortune_path}")
if not self.fortune_path: if not self.fortune_path:
self.enabled = False self.enabled = False
else: else:
+88 -6
View File
@@ -2,7 +2,8 @@ import logging
import re import re
import time import time
from geopy.geocoders import Nominatim from geopy.geocoders import ArcGIS, AzureMaps, Baidu, Bing, GoogleV3
from geopy.geocoders import HereV7, Nominatim, OpenCage, TomTom, What3WordsV3, Woosmap
from oslo_config import cfg from oslo_config import cfg
from aprsd import packets, plugin, plugin_utils from aprsd import packets, plugin, plugin_utils
@@ -13,6 +14,82 @@ CONF = cfg.CONF
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
class UsLocation:
raw = {}
def __init__(self, info):
self.info = info
def __str__(self):
return self.info
class USGov:
"""US Government geocoder that uses the geopy API.
This is a dummy class the implements the geopy reverse API,
so the factory can return an object that conforms to the API.
"""
def reverse(self, coordinates):
"""Reverse geocode a coordinate."""
LOG.info(f"USGov reverse geocode {coordinates}")
coords = coordinates.split(",")
lat = float(coords[0])
lon = float(coords[1])
result = plugin_utils.get_weather_gov_for_gps(lat, lon)
# LOG.info(f"WEATHER: {result}")
# LOG.info(f"area description {result['location']['areaDescription']}")
if 'location' in result:
loc = UsLocation(result['location']['areaDescription'])
else:
loc = UsLocation("Unknown Location")
LOG.info(f"USGov reverse geocode LOC {loc}")
return loc
def geopy_factory():
"""Factory function for geopy geocoders."""
geocoder = CONF.location_plugin.geopy_geocoder
LOG.info(f"Using geocoder: {geocoder}")
user_agent = CONF.location_plugin.user_agent
LOG.info(f"Using user_agent: {user_agent}")
if geocoder == "Nominatim":
return Nominatim(user_agent=user_agent)
elif geocoder == "USGov":
return USGov()
elif geocoder == "ArcGIS":
return ArcGIS(
username=CONF.location_plugin.arcgis_username,
password=CONF.location_plugin.arcgis_password,
user_agent=user_agent,
)
elif geocoder == "AzureMaps":
return AzureMaps(
user_agent=user_agent,
subscription_key=CONF.location_plugin.azuremaps_subscription_key,
)
elif geocoder == "Baidu":
return Baidu(user_agent=user_agent, api_key=CONF.location_plugin.baidu_api_key)
elif geocoder == "Bing":
return Bing(user_agent=user_agent, api_key=CONF.location_plugin.bing_api_key)
elif geocoder == "GoogleV3":
return GoogleV3(user_agent=user_agent, api_key=CONF.location_plugin.google_api_key)
elif geocoder == "HERE":
return HereV7(user_agent=user_agent, api_key=CONF.location_plugin.here_api_key)
elif geocoder == "OpenCage":
return OpenCage(user_agent=user_agent, api_key=CONF.location_plugin.opencage_api_key)
elif geocoder == "TomTom":
return TomTom(user_agent=user_agent, api_key=CONF.location_plugin.tomtom_api_key)
elif geocoder == "What3Words":
return What3WordsV3(user_agent=user_agent, api_key=CONF.location_plugin.what3words_api_key)
elif geocoder == "Woosmap":
return Woosmap(user_agent=user_agent, api_key=CONF.location_plugin.woosmap_api_key)
else:
raise ValueError(f"Unknown geocoder: {geocoder}")
class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin): class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
"""Location!""" """Location!"""
@@ -57,19 +134,24 @@ class LocationPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin):
# Get some information about their location # Get some information about their location
try: try:
tic = time.perf_counter() tic = time.perf_counter()
geolocator = Nominatim(user_agent="APRSD") geolocator = geopy_factory()
LOG.info(f"Using GEOLOCATOR: {geolocator}")
coordinates = f"{lat:0.6f}, {lon:0.6f}" coordinates = f"{lat:0.6f}, {lon:0.6f}"
location = geolocator.reverse(coordinates) location = geolocator.reverse(coordinates)
address = location.raw.get("address") address = location.raw.get("address")
LOG.debug(f"GEOLOCATOR address: {address}")
toc = time.perf_counter() toc = time.perf_counter()
if address: if address:
LOG.info(f"Geopy address {address} took {toc - tic:0.4f}") LOG.info(f"Geopy address {address} took {toc - tic:0.4f}")
if address.get("country_code") == "us": if address.get("country_code") == "us":
area_info = f"{address.get('county')}, {address.get('state')}" area_info = f"{address.get('county')}, {address.get('state')}"
else:
# what to do for address for non US?
area_info = f"{address.get('country'), 'Unknown'}"
else: else:
# what to do for address for non US? area_info = str(location)
area_info = f"{address.get('country'), 'Unknown'}"
except Exception as ex: except Exception as ex:
LOG.error(ex)
LOG.error(f"Failed to fetch Geopy address {ex}") LOG.error(f"Failed to fetch Geopy address {ex}")
area_info = "Unknown Location" area_info = "Unknown Location"
+7 -3
View File
@@ -26,7 +26,9 @@ class USWeatherPlugin(plugin.APRSDRegexCommandPluginBase, plugin.APRSFIKEYMixin)
"weather" - returns weather near the calling callsign "weather" - returns weather near the calling callsign
""" """
command_regex = r"^([w][x]|[w][x]\s|weather)" # command_regex = r"^([w][x]|[w][x]\s|weather)"
command_regex = r"^[wW]"
command_name = "USWeather" command_name = "USWeather"
short_description = "Provide USA only weather of GPS Beacon location" short_description = "Provide USA only weather of GPS Beacon location"
@@ -189,7 +191,9 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
""" """
command_regex = r"^([w][x]|[w][x]\s|weather)" # command_regex = r"^([w][x]|[w][x]\s|weather)"
command_regex = r"^[wW]"
command_name = "OpenWeatherMap" command_name = "OpenWeatherMap"
short_description = "OpenWeatherMap weather of GPS Beacon location" short_description = "OpenWeatherMap weather of GPS Beacon location"
@@ -211,7 +215,7 @@ class OWMWeatherPlugin(plugin.APRSDRegexCommandPluginBase):
@trace.trace @trace.trace
def process(self, packet): def process(self, packet):
fromcall = packet.get("from") fromcall = packet.get("from_call")
message = packet.get("message_text", None) message = packet.get("message_text", None)
# ack = packet.get("msgNo", "0") # ack = packet.get("msgNo", "0")
LOG.info(f"OWMWeather Plugin '{message}'") LOG.info(f"OWMWeather Plugin '{message}'")
+46 -2
View File
@@ -67,11 +67,55 @@ class APRSDPluginRXThread(APRSDRXThread):
""" """
def process_packet(self, *args, **kwargs): def process_packet(self, *args, **kwargs):
"""This handles the processing of an inbound packet.
When a packet is received by the connected client object,
it sends the raw packet into this function. This function then
decodes the packet via the client, and then processes the packet.
Ack Packets are sent to the PluginProcessPacketThread for processing.
All other packets have to be checked as a dupe, and then only after
we haven't seen this packet before, do we send it to the
PluginProcessPacketThread for processing.
"""
packet = self._client.decode_packet(*args, **kwargs) packet = self._client.decode_packet(*args, **kwargs)
# LOG.debug(raw) # LOG.debug(raw)
packet.log(header="RX") packet.log(header="RX")
packets.PacketList().rx(packet)
self.packet_queue.put(packet) if isinstance(packet, packets.AckPacket):
# We don't need to drop AckPackets, those should be
# processed.
self.packet_queue.put(packet)
else:
# Make sure we aren't re-processing the same packet
# For RF based APRS Clients we can get duplicate packets
# So we need to track them and not process the dupes.
found = False
pkt_list = packets.PacketList()
try:
# Find the packet in the list of already seen packets
# Based on the packet.key
found = pkt_list.find(packet)
except KeyError:
found = False
if not found:
# If we are in the process of already ack'ing
# a packet, we should drop the packet
# because it's a dupe within the time that
# we send the 3 acks for the packet.
pkt_list.rx(packet)
self.packet_queue.put(packet)
elif packet.timestamp - found.timestamp < CONF.packet_dupe_timeout:
# If the packet came in within 60 seconds of the
# Last time seeing the packet, then we drop it as a dupe.
LOG.warning(f"Packet {packet.from_call}:{packet.msgNo} already tracked, dropping.")
else:
LOG.warning(
f"Packet {packet.from_call}:{packet.msgNo} already tracked "
f"but older than {CONF.packet_dupe_timeout} seconds. processing.",
)
pkt_list.rx(packet)
self.packet_queue.put(packet)
class APRSDProcessPacketThread(APRSDThread): class APRSDProcessPacketThread(APRSDThread):
+6 -7
View File
@@ -1,4 +1,3 @@
import datetime
import logging import logging
import time import time
@@ -128,10 +127,10 @@ class SendPacketThread(aprsd_threads.APRSDThread):
# Message is still outstanding and needs to be acked. # Message is still outstanding and needs to be acked.
if packet.last_send_time: if packet.last_send_time:
# Message has a last send time tracking # Message has a last send time tracking
now = datetime.datetime.now() now = int(round(time.time()))
sleeptime = (packet.send_count + 1) * 31 sleeptime = (packet.send_count + 1) * 31
delta = now - packet.last_send_time delta = now - packet.last_send_time
if delta > datetime.timedelta(seconds=sleeptime): if delta > sleeptime:
# It's time to try to send it again # It's time to try to send it again
send_now = True send_now = True
else: else:
@@ -140,7 +139,7 @@ class SendPacketThread(aprsd_threads.APRSDThread):
if send_now: if send_now:
# no attempt time, so lets send it, and start # no attempt time, so lets send it, and start
# tracking the time. # tracking the time.
packet.last_send_time = datetime.datetime.now() packet.last_send_time = int(round(time.time()))
send(packet, direct=True) send(packet, direct=True)
packet.send_count += 1 packet.send_count += 1
@@ -173,13 +172,13 @@ class SendAckThread(aprsd_threads.APRSDThread):
if self.packet.last_send_time: if self.packet.last_send_time:
# Message has a last send time tracking # Message has a last send time tracking
now = datetime.datetime.now() now = int(round(time.time()))
# aprs duplicate detection is 30 secs? # aprs duplicate detection is 30 secs?
# (21 only sends first, 28 skips middle) # (21 only sends first, 28 skips middle)
sleep_time = 31 sleep_time = 31
delta = now - self.packet.last_send_time delta = now - self.packet.last_send_time
if delta > datetime.timedelta(seconds=sleep_time): if delta > sleep_time:
# It's time to try to send it again # It's time to try to send it again
send_now = True send_now = True
elif self.loop_count % 10 == 0: elif self.loop_count % 10 == 0:
@@ -190,7 +189,7 @@ class SendAckThread(aprsd_threads.APRSDThread):
if send_now: if send_now:
send(self.packet, direct=True) send(self.packet, direct=True)
self.packet.send_count += 1 self.packet.send_count += 1
self.packet.last_send_time = datetime.datetime.now() self.packet.last_send_time = int(round(time.time()))
time.sleep(1) time.sleep(1)
self.loop_count += 1 self.loop_count += 1
+1 -1
View File
@@ -37,7 +37,7 @@ class PacketCounter:
@property @property
@wrapt.synchronized(lock) @wrapt.synchronized(lock)
def value(self): def value(self):
return self.val.value return str(self.val.value)
@wrapt.synchronized(lock) @wrapt.synchronized(lock)
def __repr__(self): def __repr__(self):
+403
View File
@@ -0,0 +1,403 @@
var packet_list = {};
var tx_data = [];
var rx_data = [];
var packet_types_data = {};
var mem_current = []
var mem_peak = []
function start_charts() {
console.log("start_charts() called");
// Initialize the echarts instance based on the prepared dom
create_packets_chart();
create_packets_types_chart();
create_messages_chart();
create_ack_chart();
create_memory_chart();
}
function create_packets_chart() {
// The packets totals TX/RX chart.
pkt_c_canvas = document.getElementById('packetsChart');
packets_chart = echarts.init(pkt_c_canvas);
// Specify the configuration items and data for the chart
var option = {
title: {
text: 'APRS Packet totals'
},
legend: {},
tooltip : {
trigger: 'axis'
},
toolbox: {
show : true,
feature : {
mark : {show: true},
dataView : {show: true, readOnly: true},
magicType : {show: true, type: ['line', 'bar']},
restore : {show: true},
saveAsImage : {show: true}
}
},
calculable : true,
xAxis: { type: 'time' },
yAxis: { },
series: [
{
name: 'tx',
type: 'line',
smooth: true,
color: 'red',
encode: {
x: 'timestamp',
y: 'tx' // refer sensor 1 value
}
},{
name: 'rx',
type: 'line',
smooth: true,
encode: {
x: 'timestamp',
y: 'rx'
}
}]
};
// Display the chart using the configuration items and data just specified.
packets_chart.setOption(option);
}
function create_packets_types_chart() {
// The packets types chart
pkt_types_canvas = document.getElementById('packetTypesChart');
packet_types_chart = echarts.init(pkt_types_canvas);
// The series and data are built and updated on the fly
// as packets come in.
var option = {
title: {
text: 'Packet Types'
},
legend: {},
tooltip : {
trigger: 'axis'
},
toolbox: {
show : true,
feature : {
mark : {show: true},
dataView : {show: true, readOnly: true},
magicType : {show: true, type: ['line', 'bar']},
restore : {show: true},
saveAsImage : {show: true}
}
},
calculable : true,
xAxis: { type: 'time' },
yAxis: { },
}
packet_types_chart.setOption(option);
}
function create_messages_chart() {
msg_c_canvas = document.getElementById('messagesChart');
message_chart = echarts.init(msg_c_canvas);
// Specify the configuration items and data for the chart
var option = {
title: {
text: 'Message Packets'
},
legend: {},
tooltip: {
trigger: 'axis'
},
toolbox: {
show: true,
feature: {
mark : {show: true},
dataView : {show: true, readOnly: true},
magicType : {show: true, type: ['line', 'bar']},
restore : {show: true},
saveAsImage : {show: true}
}
},
calculable: true,
xAxis: { type: 'time' },
yAxis: { },
series: [
{
name: 'tx',
type: 'line',
smooth: true,
color: 'red',
encode: {
x: 'timestamp',
y: 'tx' // refer sensor 1 value
}
},{
name: 'rx',
type: 'line',
smooth: true,
encode: {
x: 'timestamp',
y: 'rx'
}
}]
};
// Display the chart using the configuration items and data just specified.
message_chart.setOption(option);
}
function create_ack_chart() {
ack_canvas = document.getElementById('acksChart');
ack_chart = echarts.init(ack_canvas);
// Specify the configuration items and data for the chart
var option = {
title: {
text: 'Ack Packets'
},
legend: {},
tooltip: {
trigger: 'axis'
},
toolbox: {
show: true,
feature: {
mark : {show: true},
dataView : {show: true, readOnly: false},
magicType : {show: true, type: ['line', 'bar']},
restore : {show: true},
saveAsImage : {show: true}
}
},
calculable: true,
xAxis: { type: 'time' },
yAxis: { },
series: [
{
name: 'tx',
type: 'line',
smooth: true,
color: 'red',
encode: {
x: 'timestamp',
y: 'tx' // refer sensor 1 value
}
},{
name: 'rx',
type: 'line',
smooth: true,
encode: {
x: 'timestamp',
y: 'rx'
}
}]
};
ack_chart.setOption(option);
}
function create_memory_chart() {
ack_canvas = document.getElementById('memChart');
memory_chart = echarts.init(ack_canvas);
// Specify the configuration items and data for the chart
var option = {
title: {
text: 'Memory Usage'
},
legend: {},
tooltip: {
trigger: 'axis'
},
toolbox: {
show: true,
feature: {
mark : {show: true},
dataView : {show: true, readOnly: false},
magicType : {show: true, type: ['line', 'bar']},
restore : {show: true},
saveAsImage : {show: true}
}
},
calculable: true,
xAxis: { type: 'time' },
yAxis: { },
series: [
{
name: 'current',
type: 'line',
smooth: true,
color: 'red',
encode: {
x: 'timestamp',
y: 'current' // refer sensor 1 value
}
},{
name: 'peak',
type: 'line',
smooth: true,
encode: {
x: 'timestamp',
y: 'peak'
}
}]
};
memory_chart.setOption(option);
}
function updatePacketData(chart, time, first, second) {
tx_data.push([time, first]);
rx_data.push([time, second]);
option = {
series: [
{
name: 'tx',
data: tx_data,
},
{
name: 'rx',
data: rx_data,
}
]
}
chart.setOption(option);
}
function updatePacketTypesData(time, typesdata) {
//The options series is created on the fly each time based on
//the packet types we have in the data
var series = []
for (const k in typesdata) {
tx = [time, typesdata[k]["tx"]]
rx = [time, typesdata[k]["rx"]]
if (packet_types_data.hasOwnProperty(k)) {
packet_types_data[k]["tx"].push(tx)
packet_types_data[k]["rx"].push(rx)
} else {
packet_types_data[k] = {'tx': [tx], 'rx': [rx]}
}
}
}
function updatePacketTypesChart() {
series = []
for (const k in packet_types_data) {
entry = {
name: k+"tx",
data: packet_types_data[k]["tx"],
type: 'line',
smooth: true,
encode: {
x: 'timestamp',
y: k+'tx' // refer sensor 1 value
}
}
series.push(entry)
entry = {
name: k+"rx",
data: packet_types_data[k]["rx"],
type: 'line',
smooth: true,
encode: {
x: 'timestamp',
y: k+'rx' // refer sensor 1 value
}
}
series.push(entry)
}
option = {
series: series
}
console.log(option)
packet_types_chart.setOption(option);
}
function updateTypeChart(chart, key) {
//Generic function to update a packet type chart
if (! packet_types_data.hasOwnProperty(key)) {
return;
}
if (! packet_types_data[key].hasOwnProperty('tx')) {
return;
}
var option = {
series: [{
name: "tx",
data: packet_types_data[key]["tx"],
},
{
name: "rx",
data: packet_types_data[key]["rx"]
}]
}
chart.setOption(option);
}
function updateMemChart(time, current, peak) {
mem_current.push([time, current]);
mem_peak.push([time, peak]);
option = {
series: [
{
name: 'current',
data: mem_current,
},
{
name: 'peak',
data: mem_peak,
}
]
}
memory_chart.setOption(option);
}
function updateMessagesChart() {
updateTypeChart(message_chart, "MessagePacket")
}
function updateAcksChart() {
updateTypeChart(ack_chart, "AckPacket")
}
function update_stats( data ) {
console.log(data);
our_callsign = data["stats"]["aprsd"]["callsign"];
$("#version").text( data["stats"]["aprsd"]["version"] );
$("#aprs_connection").html( data["aprs_connection"] );
$("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] );
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
$("#jsonstats").html(html_pretty);
t = Date.parse(data["time"]);
ts = new Date(t);
updatePacketData(packets_chart, ts, data["stats"]["packets"]["sent"], data["stats"]["packets"]["received"]);
updatePacketTypesData(ts, data["stats"]["packets"]["types"]);
updatePacketTypesChart();
updateMessagesChart();
updateAcksChart();
updateMemChart(ts, data["stats"]["aprsd"]["memory_current"], data["stats"]["aprsd"]["memory_peak"]);
//updateQuadData(message_chart, short_time, data["stats"]["messages"]["sent"], data["stats"]["messages"]["received"], data["stats"]["messages"]["ack_sent"], data["stats"]["messages"]["ack_recieved"]);
//updateDualData(email_chart, short_time, data["stats"]["email"]["sent"], data["stats"]["email"]["recieved"]);
//updateDualData(memory_chart, short_time, data["stats"]["aprsd"]["memory_peak"], data["stats"]["aprsd"]["memory_current"]);
}
+24 -16
View File
@@ -6,6 +6,7 @@
<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.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>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
<script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.js"></script>
@@ -15,7 +16,7 @@
<link rel="stylesheet" href="/static/css/prism.css"> <link rel="stylesheet" href="/static/css/prism.css">
<script src="/static/js/prism.js"></script> <script src="/static/js/prism.js"></script>
<script src="/static/js/main.js"></script> <script src="/static/js/main.js"></script>
<script src="/static/js/charts.js"></script> <script src="/static/js/echarts.js"></script>
<script src="/static/js/tabs.js"></script> <script src="/static/js/tabs.js"></script>
<script src="/static/js/send-message.js"></script> <script src="/static/js/send-message.js"></script>
<script src="/static/js/logs.js"></script> <script src="/static/js/logs.js"></script>
@@ -83,6 +84,7 @@
<div class="item" data-tab="plugin-tab">Plugins</div> <div class="item" data-tab="plugin-tab">Plugins</div>
<div class="item" data-tab="config-tab">Config</div> <div class="item" data-tab="config-tab">Config</div>
<div class="item" data-tab="log-tab">LogFile</div> <div class="item" data-tab="log-tab">LogFile</div>
<!-- <div class="item" data-tab="oslo-tab">OSLO CONFIG</div> //-->
<div class="item" data-tab="raw-tab">Raw JSON</div> <div class="item" data-tab="raw-tab">Raw JSON</div>
</div> </div>
@@ -92,25 +94,25 @@
<div class="ui equal width relaxed grid"> <div class="ui equal width relaxed grid">
<div class="row"> <div class="row">
<div class="column"> <div class="column">
<div class="ui segment" style="height: 300px"> <div class="ui segment" style="height: 300px" id="packetsChart"></div>
<canvas id="packetsChart"></canvas>
</div>
</div>
<div class="column">
<div class="ui segment" style="height: 300px">
<canvas id="messageChart"></canvas>
</div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="column"> <div class="column">
<div class="ui segment" style="height: 300px"> <div class="ui segment" style="height: 300px" id="packetTypesChart"></div>
<canvas id="emailChart"></canvas> </div>
</div> </div>
<div class="row">
<div class="column">
<div class="ui segment" style="height: 300px" id="messagesChart"></div>
</div> </div>
<div class="column"> <div class="column">
<div class="ui segment" style="height: 300px"> <div class="ui segment" style="height: 300px" id="acksChart"></div>
<canvas id="memChart"></canvas> </div>
</div>
<div class="row">
<div class="column">
<div class="ui segment" style="height: 300px" id="memChart">
</div> </div>
</div> </div>
</div> </div>
@@ -118,7 +120,7 @@
<div id="stats" class="two column"> <div id="stats" class="two column">
<button class="ui button" id="toggleStats">Toggle raw json</button> <button class="ui button" id="toggleStats">Toggle raw json</button>
<pre id="jsonstats" class="language-json">{{ stats }}</pre> <pre id="jsonstats" class="language-json">{{ stats }}</pre>
</div> --!> </div> //-->
</div> </div>
</div> </div>
@@ -164,9 +166,15 @@
<pre id="logContainer" style="height: 600px;overflow-y:auto;overflow-x:auto;"><code id="logtext" class="language-log" ></code></pre> <pre id="logContainer" style="height: 600px;overflow-y:auto;overflow-x:auto;"><code id="logtext" class="language-log" ></code></pre>
</div> </div>
<!--
<div class="ui bottom attached tab segment" data-tab="oslo-tab">
<h3 class="ui dividing header">OSLO</h3>
<pre id="osloContainer" style="height:600px;overflow-y:auto;" class="language-json">{{ oslo_out|safe }}</pre>
</div> //-->
<div class="ui bottom attached tab segment" data-tab="raw-tab"> <div class="ui bottom attached tab segment" data-tab="raw-tab">
<h3 class="ui dividing header">Raw JSON</h3> <h3 class="ui dividing header">Raw JSON</h3>
<pre id="jsonstats" class="language-json">{{ stats|safe }}</pre> <pre id="jsonstats" class="language-yaml" style="height:600px;overflow-y:auto;">{{ stats|safe }}</pre>
</div> </div>
<div class="ui text container"> <div class="ui text container">
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1,23 @@
/* fallback */
@font-face {
font-family: 'Material Symbols Rounded';
font-style: normal;
font-weight: 200;
src: url(/static/css/upstream/font.woff2) format('woff2');
}
.material-symbols-rounded {
font-family: 'Material Symbols Rounded';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}
File diff suppressed because one or more lines are too long
+19 -5
View File
@@ -34,7 +34,6 @@ function init_chat() {
console.log("SENT: "); console.log("SENT: ");
console.log(msg); console.log(msg);
if (cleared === false) { if (cleared === false) {
console.log("CLEARING #msgsTabsDiv");
var msgsdiv = $("#msgsTabsDiv"); var msgsdiv = $("#msgsTabsDiv");
msgsdiv.html(''); msgsdiv.html('');
cleared = true; cleared = true;
@@ -62,6 +61,7 @@ function init_chat() {
event.preventDefault(); event.preventDefault();
to_call = $('#to_call').val(); to_call = $('#to_call').val();
message = $('#message').val(); message = $('#message').val();
path = $('#pkt_path option:selected').val();
if (to_call == "") { if (to_call == "") {
raise_error("You must enter a callsign to send a message") raise_error("You must enter a callsign to send a message")
return false; return false;
@@ -70,7 +70,8 @@ function init_chat() {
raise_error("You must enter a message to send") raise_error("You must enter a message to send")
return false; return false;
} }
msg = {'to': to_call, 'message': message, } msg = {'to': to_call, 'message': message, 'path': path};
console.log(msg);
socket.emit("send", msg); socket.emit("send", msg);
$('#message').val(''); $('#message').val('');
} }
@@ -295,7 +296,7 @@ function delete_tab(callsign) {
save_data(); save_data();
} }
function add_callsign(callsign) { function add_callsign(callsign, msg) {
/* Ensure a callsign exists in the left hand nav */ /* Ensure a callsign exists in the left hand nav */
if (callsign in callsign_list) { if (callsign in callsign_list) {
return false return false
@@ -307,10 +308,19 @@ function add_callsign(callsign) {
active = false; active = false;
} }
create_callsign_tab(callsign, active); create_callsign_tab(callsign, active);
callsign_list[callsign] = true; callsign_list[callsign] = '';
return true; return true;
} }
function update_callsign_path(callsign, path) {
//Get the selected path to save for this callsign
path = msg['path']
console.log("Path is " + path);
$('#pkt_path').val(path);
callsign_list[callsign] = path;
}
function append_message(callsign, msg, msg_html) { function append_message(callsign, msg, msg_html) {
new_callsign = false new_callsign = false
if (!message_list.hasOwnProperty(callsign)) { if (!message_list.hasOwnProperty(callsign)) {
@@ -331,7 +341,9 @@ function append_message(callsign, msg, msg_html) {
} }
// Find the right div to place the html // Find the right div to place the html
new_callsign = add_callsign(callsign);
new_callsign = add_callsign(callsign, msg);
update_callsign_path(callsign, msg['path']);
append_message_html(callsign, msg_html, new_callsign); append_message_html(callsign, msg_html, new_callsign);
if (new_callsign) { if (new_callsign) {
//Now click the tab //Now click the tab
@@ -487,4 +499,6 @@ function callsign_select(callsign) {
tab_notify_id = tab_notification_id(callsign, true); tab_notify_id = tab_notification_id(callsign, true);
$(tab_notify_id).addClass('visually-hidden'); $(tab_notify_id).addClass('visually-hidden');
$(tab_notify_id).text(0); $(tab_notify_id).text(0);
// Now update the path
$('#pkt_path').val(callsign_list[callsign]);
} }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+19 -12
View File
@@ -2,26 +2,24 @@
<head> <head>
<meta name="viewport" <meta name="viewport"
content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1"> content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script> <script src="/static/js/upstream/jquery-3.7.1.min.js"></script>
<script src="/static/js/upstream/jquery.toast.js"></script> <script src="/static/js/upstream/jquery.toast.js"></script>
<!--<script src="/static/js/upstream/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="/static/css/upstream/jquery-ui.css">
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.13.1/jquery-ui.min.js"></script> -->
<!-- <script src="/static/js/upstream/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="/static/js/upstream/socket.io.min.js"></script> <script src="/static/js/upstream/socket.io.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css" <link rel="stylesheet" href="/static/css/upstream/bootstrap.min.css">
<script src="/static/js/upstream/bootstrap.bundle.min.js"></script>
<!--
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css"
rel="stylesheet" rel="stylesheet"
integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9" integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9"
crossorigin="anonymous"> crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js" <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-HwwvtgBNo3bZJJLYd8oVXjrBZt8cqVSpeBNS5n7C8IVInixGAoxmnlMuBnhbgrkm" integrity="sha384-HwwvtgBNo3bZJJLYd8oVXjrBZt8cqVSpeBNS5n7C8IVInixGAoxmnlMuBnhbgrkm"
crossorigin="anonymous"></script> crossorigin="anonymous"></script>
-->
<link rel="stylesheet" <link rel="stylesheet" href="/static/css/upstream/google-fonts.css">
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,200,1,200">
<link rel="stylesheet" href="/static/css/upstream/jquery.toast.css"> <link rel="stylesheet" href="/static/css/upstream/jquery.toast.css">
<link rel="stylesheet" href="/static/css/chat.css"> <link rel="stylesheet" href="/static/css/chat.css">
@@ -89,11 +87,20 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<form class="row gx-1 gy-1 justify-content-center align-items-center" id="sendform" name="sendmsg" action=""> <form class="row gx-1 gy-1 justify-content-center align-items-center" id="sendform" name="sendmsg" action="" autocomplete="off">
<div class="col-sm-3"> <div class="col-sm-2" style="width:150px;">
<label for="to_call" class="visually-hidden">Callsign</label> <label for="to_call" class="visually-hidden">Callsign</label>
<input type="search" class="form-control mb-2 mr-sm-2" name="to_call" id="to_call" placeholder="To Callsign" size="11" maxlength="9"> <input type="search" class="form-control mb-2 mr-sm-2" name="to_call" id="to_call" placeholder="To Callsign" size="11" maxlength="9">
</div> </div>
<div class="col-auto">
<label for="pkt_path" class="visually-hidden">PATH</label>
<select class="form-control mb-2 mr-sm-2" name="pkt_path" id="pkt_path" style="width:auto;">
<option value="" selected>Default Path</option>
<option value="WIDE1-1">WIDE1-1</option>
<option value="WIDE1-1,WIDE2-1">WIDE1-1,WIDE2-1</option>
<option value="ARISS">ARISS</option>
</select>
</div>
<div class="col-sm-3"> <div class="col-sm-3">
<label for="message" class="visually-hidden">Message</label> <label for="message" class="visually-hidden">Message</label>
<input type="search" class="form-control mb-2 mr-sm-2" name="message" id="message" size="40" maxlength="67" placeholder="Message"> <input type="search" class="form-control mb-2 mr-sm-2" name="message" id="message" size="40" maxlength="67" placeholder="Message">
+53 -6
View File
@@ -1,4 +1,6 @@
import datetime import datetime
import importlib.metadata as imp
import io
import json import json
import logging import logging
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
@@ -8,7 +10,7 @@ import flask
from flask import Flask from flask import Flask
from flask.logging import default_handler from flask.logging import default_handler
from flask_httpauth import HTTPBasicAuth from flask_httpauth import HTTPBasicAuth
from oslo_config import cfg from oslo_config import cfg, generator
import socketio import socketio
from werkzeug.security import check_password_hash from werkzeug.security import check_password_hash
@@ -93,12 +95,19 @@ def _stats():
stats_dict["aprsd"]["watch_list"] = new_list stats_dict["aprsd"]["watch_list"] = new_list
packet_list = aprsd_rpc_client.RPCClient().get_packet_list() packet_list = aprsd_rpc_client.RPCClient().get_packet_list()
rx = tx = 0 rx = tx = 0
types = {}
if packet_list: if packet_list:
rx = packet_list.total_rx() rx = packet_list.total_rx()
tx = packet_list.total_tx() tx = packet_list.total_tx()
types_copy = packet_list.types.copy()
for key in types_copy:
types[str(key)] = dict(types_copy[key])
stats_dict["packets"] = { stats_dict["packets"] = {
"sent": tx, "sent": tx,
"received": rx, "received": rx,
"types": types,
} }
if track: if track:
size_tracker = len(track) size_tracker = len(track)
@@ -123,7 +132,6 @@ def stats():
@app.route("/") @app.route("/")
def index(): def index():
stats = _stats() stats = _stats()
LOG.debug(stats)
wl = aprsd_rpc_client.RPCClient().get_watch_list() wl = aprsd_rpc_client.RPCClient().get_watch_list()
if wl and wl.is_enabled(): if wl and wl.is_enabled():
watch_count = len(wl) watch_count = len(wl)
@@ -185,6 +193,7 @@ def index():
watch_age=watch_age, watch_age=watch_age,
seen_count=seen_count, seen_count=seen_count,
plugin_count=plugin_count, plugin_count=plugin_count,
# oslo_out=generate_oslo()
) )
@@ -205,10 +214,12 @@ def get_packets():
LOG.debug("/packets called") LOG.debug("/packets called")
packet_list = aprsd_rpc_client.RPCClient().get_packet_list() packet_list = aprsd_rpc_client.RPCClient().get_packet_list()
if packet_list: if packet_list:
packets = packet_list.get()
tmp_list = [] tmp_list = []
for pkt in packets: pkts = packet_list.copy()
tmp_list.append(pkt.json) for key in pkts:
pkt = packet_list.get(key)
if pkt:
tmp_list.append(pkt.json)
return json.dumps(tmp_list) return json.dumps(tmp_list)
else: else:
@@ -225,6 +236,36 @@ def plugins():
return "reloaded" return "reloaded"
def _get_namespaces():
args = []
all = imp.entry_points()
selected = []
if "oslo.config.opts" in all:
for x in all["oslo.config.opts"]:
if x.group == "oslo.config.opts":
selected.append(x)
for entry in selected:
if "aprsd" in entry.name:
args.append("--namespace")
args.append(entry.name)
return args
def generate_oslo():
CONF.namespace = _get_namespaces()
string_out = io.StringIO()
generator.generate(CONF, string_out)
return string_out.getvalue()
@auth.login_required
@app.route("/oslo")
def oslo():
return generate_oslo()
@auth.login_required @auth.login_required
@app.route("/save") @app.route("/save")
def save(): def save():
@@ -348,6 +389,8 @@ if __name__ == "uwsgi_file_aprsd_wsgi":
log_level = init_app( log_level = init_app(
log_level="DEBUG", log_level="DEBUG",
config_file="/config/aprsd.conf", config_file="/config/aprsd.conf",
# Commented out for local development.
# config_file=cli_helper.DEFAULT_CONFIG_FILE
) )
setup_logging(app, log_level) setup_logging(app, log_level)
sio.register_namespace(LoggingNamespace("/logs")) sio.register_namespace(LoggingNamespace("/logs"))
@@ -362,7 +405,11 @@ if __name__ == "aprsd.wsgi":
sio = socketio.Server(logger=True, async_mode=async_mode) sio = socketio.Server(logger=True, async_mode=async_mode)
app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app) app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app)
log_level = init_app(config_file="/config/aprsd.conf", log_level="DEBUG") log_level = init_app(
log_level="DEBUG",
config_file="/config/aprsd.conf",
# config_file=cli_helper.DEFAULT_CONFIG_FILE,
)
setup_logging(app, log_level) setup_logging(app, log_level)
sio.register_namespace(LoggingNamespace("/logs")) sio.register_namespace(LoggingNamespace("/logs"))
CONF.log_opt_values(LOG, logging.DEBUG) CONF.log_opt_values(LOG, logging.DEBUG)
+29 -27
View File
@@ -1,5 +1,5 @@
# #
# This file is autogenerated by pip-compile with Python 3.11 # This file is autogenerated by pip-compile with Python 3.10
# by the following command: # by the following command:
# #
# pip-compile --annotation-style=line dev-requirements.in # pip-compile --annotation-style=line dev-requirements.in
@@ -8,62 +8,63 @@ add-trailing-comma==3.1.0 # via gray
alabaster==0.7.13 # via sphinx alabaster==0.7.13 # via sphinx
attrs==23.1.0 # via jsonschema, referencing attrs==23.1.0 # via jsonschema, referencing
autoflake==1.5.3 # via gray autoflake==1.5.3 # via gray
babel==2.12.1 # via sphinx babel==2.13.1 # via sphinx
black==23.7.0 # via gray black==23.11.0 # via gray
build==1.0.3 # via pip-tools build==1.0.3 # via pip-tools
cachetools==5.3.1 # via tox cachetools==5.3.2 # via tox
certifi==2023.7.22 # via requests certifi==2023.7.22 # via requests
cfgv==3.4.0 # via pre-commit cfgv==3.4.0 # via pre-commit
chardet==5.2.0 # via tox chardet==5.2.0 # via tox
charset-normalizer==3.2.0 # via requests charset-normalizer==3.3.2 # via requests
click==8.1.7 # via black, pip-tools click==8.1.7 # 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.7 # via gray configargparse==1.7 # via gray
coverage[toml]==7.3.1 # via pytest-cov coverage[toml]==7.3.2 # via coverage, pytest-cov
distlib==0.3.7 # via virtualenv distlib==0.3.7 # via virtualenv
docutils==0.20.1 # via sphinx docutils==0.20.1 # via sphinx
filelock==3.12.3 # via tox, virtualenv exceptiongroup==1.1.3 # via pytest
filelock==3.13.1 # via tox, virtualenv
fixit==0.1.4 # via gray fixit==0.1.4 # via gray
flake8==6.1.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.27 # via pre-commit identify==2.5.31 # 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-resources==6.0.1 # via fixit importlib-resources==6.1.1 # via fixit
iniconfig==2.0.0 # via pytest iniconfig==2.0.0 # via pytest
isort==5.12.0 # via -r dev-requirements.in, gray isort==5.12.0 # via -r dev-requirements.in, gray
jinja2==3.1.2 # via sphinx jinja2==3.1.2 # via sphinx
jsonschema==4.19.0 # via fixit jsonschema==4.20.0 # via fixit
jsonschema-specifications==2023.7.1 # via jsonschema jsonschema-specifications==2023.11.1 # via jsonschema
libcst==1.0.1 # via fixit libcst==1.1.0 # via fixit
markupsafe==2.1.3 # via jinja2 markupsafe==2.1.3 # via jinja2
mccabe==0.7.0 # via flake8 mccabe==0.7.0 # via flake8
mypy==1.5.1 # via -r dev-requirements.in mypy==1.7.0 # via -r dev-requirements.in
mypy-extensions==1.0.0 # via black, mypy, typing-inspect mypy-extensions==1.0.0 # via black, mypy, typing-inspect
nodeenv==1.8.0 # via pre-commit nodeenv==1.8.0 # via pre-commit
packaging==23.1 # via black, build, pyproject-api, pytest, sphinx, tox packaging==23.2 # via black, build, pyproject-api, pytest, sphinx, tox
pathspec==0.11.2 # 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==7.3.0 # via -r dev-requirements.in pip-tools==7.3.0 # via -r dev-requirements.in
platformdirs==3.10.0 # via black, tox, virtualenv platformdirs==3.11.0 # via black, tox, virtualenv
pluggy==1.3.0 # via pytest, tox pluggy==1.3.0 # via pytest, tox
pre-commit==3.4.0 # via -r dev-requirements.in pre-commit==3.5.0 # via -r dev-requirements.in
pycodestyle==2.11.0 # via flake8 pycodestyle==2.11.1 # via flake8
pyflakes==3.1.0 # via autoflake, flake8 pyflakes==3.1.0 # via autoflake, flake8
pygments==2.16.1 # via rich, sphinx pygments==2.16.1 # via rich, sphinx
pyproject-api==1.6.1 # via tox pyproject-api==1.6.1 # via tox
pyproject-hooks==1.0.0 # via build pyproject-hooks==1.0.0 # via build
pytest==7.4.2 # via -r dev-requirements.in, pytest-cov pytest==7.4.3 # via -r dev-requirements.in, pytest-cov
pytest-cov==4.1.0 # via -r dev-requirements.in pytest-cov==4.1.0 # via -r dev-requirements.in
pyupgrade==3.10.1 # via gray pyupgrade==3.15.0 # via gray
pyyaml==6.0.1 # via fixit, libcst, pre-commit pyyaml==6.0.1 # via fixit, libcst, pre-commit
referencing==0.30.2 # via jsonschema, jsonschema-specifications referencing==0.31.0 # via jsonschema, jsonschema-specifications
requests==2.31.0 # via sphinx requests==2.31.0 # via sphinx
rich==12.6.0 # via gray rich==12.6.0 # via gray
rpds-py==0.10.2 # via jsonschema, referencing rpds-py==0.13.0 # via jsonschema, referencing
snowballstemmer==2.2.0 # via sphinx snowballstemmer==2.2.0 # via sphinx
sphinx==7.2.5 # via -r dev-requirements.in, sphinxcontrib-applehelp, sphinxcontrib-devhelp, sphinxcontrib-htmlhelp, sphinxcontrib-qthelp, sphinxcontrib-serializinghtml sphinx==7.2.6 # via -r dev-requirements.in, sphinxcontrib-applehelp, sphinxcontrib-devhelp, sphinxcontrib-htmlhelp, sphinxcontrib-qthelp, sphinxcontrib-serializinghtml
sphinxcontrib-applehelp==1.0.7 # via sphinx sphinxcontrib-applehelp==1.0.7 # via sphinx
sphinxcontrib-devhelp==1.0.5 # via sphinx sphinxcontrib-devhelp==1.0.5 # via sphinx
sphinxcontrib-htmlhelp==2.0.4 # via sphinx sphinxcontrib-htmlhelp==2.0.4 # via sphinx
@@ -72,14 +73,15 @@ sphinxcontrib-qthelp==1.0.6 # via sphinx
sphinxcontrib-serializinghtml==1.1.9 # via sphinx sphinxcontrib-serializinghtml==1.1.9 # via sphinx
tokenize-rt==5.2.0 # via add-trailing-comma, pyupgrade tokenize-rt==5.2.0 # via add-trailing-comma, pyupgrade
toml==0.10.2 # via autoflake toml==0.10.2 # via autoflake
tox==4.11.2 # via -r dev-requirements.in tomli==2.0.1 # via black, build, coverage, mypy, pip-tools, pyproject-api, pyproject-hooks, pytest, tox
typing-extensions==4.7.1 # via libcst, mypy, typing-inspect tox==4.11.3 # via -r dev-requirements.in
typing-extensions==4.8.0 # via black, libcst, mypy, typing-inspect
typing-inspect==0.9.0 # via libcst typing-inspect==0.9.0 # via libcst
unify==0.5 # via gray unify==0.5 # via gray
untokenize==0.1.1 # via unify untokenize==0.1.1 # via unify
urllib3==2.0.4 # via requests urllib3==2.1.0 # via requests
virtualenv==20.24.5 # via pre-commit, tox virtualenv==20.24.6 # via pre-commit, tox
wheel==0.41.2 # via pip-tools wheel==0.41.3 # via pip-tools
# The following packages are considered to be unsafe in a requirements file: # The following packages are considered to be unsafe in a requirements file:
# pip # pip
+2 -1
View File
@@ -23,7 +23,7 @@ RUN set -ex \
&& apt-get update \ && apt-get update \
&& apt-get upgrade -y \ && apt-get upgrade -y \
&& apt-get install -y git build-essential curl libffi-dev \ && apt-get install -y git build-essential curl libffi-dev \
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo \ python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo fortune \
# Install dependencies # Install dependencies
# Clean up # Clean up
&& apt-get autoremove -y \ && apt-get autoremove -y \
@@ -41,6 +41,7 @@ RUN mkdir /config
RUN chown -R appuser:appgroup /app RUN chown -R appuser:appgroup /app
RUN chown -R appuser:appgroup /config RUN chown -R appuser:appgroup /config
USER appuser USER appuser
RUN echo "PATH=\$PATH:/usr/games" >> /app/.bashrc
RUN which aprsd RUN which aprsd
RUN aprsd sample-config > /config/aprsd.conf RUN aprsd sample-config > /config/aprsd.conf
+2 -1
View File
@@ -20,7 +20,7 @@ RUN set -ex \
# Upgrade the package index and install security upgrades # Upgrade the package index and install security upgrades
&& apt-get update \ && apt-get update \
&& apt-get upgrade -y \ && apt-get upgrade -y \
&& apt-get install -y git build-essential curl libffi-dev \ && apt-get install -y git build-essential curl libffi-dev fortune \
python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo \ python3-dev libssl-dev libxml2-dev libxslt-dev telnet sudo \
# Install dependencies # Install dependencies
# Clean up # Clean up
@@ -40,6 +40,7 @@ RUN mkdir /config
RUN chown -R appuser:appgroup /app RUN chown -R appuser:appgroup /app
RUN chown -R appuser:appgroup /config RUN chown -R appuser:appgroup /config
USER appuser USER appuser
RUN echo "PATH=\$PATH:/usr/games" >> /app/.bashrc
RUN which aprsd RUN which aprsd
RUN aprsd sample-config > /config/aprsd.conf RUN aprsd sample-config > /config/aprsd.conf
+1
View File
@@ -36,3 +36,4 @@ rpyc
shellingham shellingham
geopy geopy
rush rush
dataclasses-json
+75 -172
View File
@@ -1,180 +1,83 @@
# #
# This file is autogenerated by pip-compile with Python 3.11 # This file is autogenerated by pip-compile with Python 3.10
# by the following command: # by the following command:
# #
# pip-compile --annotation-style=line requirements.in # pip-compile --annotation-style=line requirements.in
# #
aprslib==0.7.2 aprslib==0.7.2 # via -r requirements.in
# via -r requirements.in attrs==23.1.0 # via -r requirements.in, ax253, kiss3, rush
attrs==23.1.0 ax253==0.1.5.post1 # via kiss3
# via beautifulsoup4==4.12.2 # via -r requirements.in
# -r requirements.in bidict==0.22.1 # via python-socketio
# ax253 bitarray==2.8.3 # via ax253, kiss3
# kiss3 blinker==1.7.0 # via flask
# rush certifi==2023.7.22 # via requests
ax253==0.1.5.post1 charset-normalizer==3.3.2 # via requests
# via kiss3 click==8.1.7 # via -r requirements.in, click-completion, click-params, flask
beautifulsoup4==4.12.2 click-completion==0.5.2 # via -r requirements.in
# via -r requirements.in click-params==0.4.1 # via -r requirements.in
bidict==0.22.1 commonmark==0.9.1 # via rich
# via python-socketio dacite2==2.0.0 # via -r requirements.in
bitarray==2.8.1 dataclasses==0.6 # via -r requirements.in
# via dataclasses-json==0.6.2 # via -r requirements.in
# ax253 debtcollector==2.5.0 # via oslo-config
# kiss3 decorator==5.1.1 # via validators
blinker==1.6.2 dnspython==2.4.2 # via eventlet
# via flask eventlet==0.33.3 # via -r requirements.in
certifi==2023.7.22 flask==3.0.0 # via -r requirements.in, flask-httpauth, flask-socketio
# via requests flask-httpauth==4.8.0 # via -r requirements.in
charset-normalizer==3.2.0 flask-socketio==5.3.6 # via -r requirements.in
# via requests geographiclib==2.0 # via geopy
click==8.1.7 geopy==2.4.0 # via -r requirements.in
# via gevent==23.9.1 # via -r requirements.in
# -r requirements.in greenlet==3.0.1 # via eventlet, gevent
# click-completion h11==0.14.0 # via wsproto
# click-params idna==3.4 # via requests
# flask imapclient==3.0.0 # via -r requirements.in
click-completion==0.5.2 importlib-metadata==6.8.0 # via ax253, kiss3
# via -r requirements.in itsdangerous==2.1.2 # via flask
click-params==0.4.1 jinja2==3.1.2 # via click-completion, flask
# via -r requirements.in kiss3==8.0.0 # via -r requirements.in
commonmark==0.9.1 markupsafe==2.1.3 # via jinja2, werkzeug
# via rich marshmallow==3.20.1 # via dataclasses-json
dacite2==2.0.0 mypy-extensions==1.0.0 # via typing-inspect
# via -r requirements.in netaddr==0.9.0 # via oslo-config
dataclasses==0.6 oslo-config==9.2.0 # via -r requirements.in
# via -r requirements.in oslo-i18n==6.2.0 # via oslo-config
debtcollector==2.5.0 packaging==23.2 # via marshmallow
# via oslo-config pbr==6.0.0 # via -r requirements.in, oslo-i18n, stevedore
decorator==5.1.1 pluggy==1.3.0 # via -r requirements.in
# via validators plumbum==1.8.2 # via rpyc
dnspython==2.4.2 pygments==2.16.1 # via rich
# via eventlet pyserial==3.5 # via pyserial-asyncio
eventlet==0.33.3 pyserial-asyncio==0.6 # via kiss3
# via -r requirements.in python-engineio==4.8.0 # via python-socketio
flask==2.3.3 python-socketio==5.10.0 # via -r requirements.in, flask-socketio
# via pytz==2023.3.post1 # via -r requirements.in
# -r requirements.in pyyaml==6.0.1 # via -r requirements.in, oslo-config
# flask-httpauth requests==2.31.0 # via -r requirements.in, oslo-config, update-checker
# flask-socketio rfc3986==2.0.0 # via oslo-config
flask-httpauth==4.8.0 rich==12.6.0 # via -r requirements.in
# via -r requirements.in rpyc==5.3.1 # via -r requirements.in
flask-socketio==5.3.6 rush==2021.4.0 # via -r requirements.in
# via -r requirements.in shellingham==1.5.4 # via -r requirements.in, click-completion
geographiclib==2.0 simple-websocket==1.0.0 # via python-engineio
# via geopy six==1.16.0 # via -r requirements.in, click-completion, eventlet
geopy==2.4.0 soupsieve==2.5 # via beautifulsoup4
# via -r requirements.in stevedore==5.1.0 # via oslo-config
gevent==23.9.1 tabulate==0.9.0 # via -r requirements.in
# via -r requirements.in thesmuggler==1.0.1 # via -r requirements.in
greenlet==3.0.0rc3 typing-extensions==4.8.0 # via typing-inspect
# via typing-inspect==0.9.0 # via dataclasses-json
# eventlet update-checker==0.18.0 # via -r requirements.in
# gevent urllib3==2.1.0 # via requests
idna==3.4 validators==0.20.0 # via click-params
# via requests werkzeug==3.0.1 # via -r requirements.in, flask
imapclient==2.3.1 wrapt==1.16.0 # via -r requirements.in, debtcollector
# via -r requirements.in wsproto==1.2.0 # via simple-websocket
importlib-metadata==6.8.0 zipp==3.17.0 # via importlib-metadata
# via zope-event==5.0 # via gevent
# ax253 zope-interface==6.1 # via gevent
# kiss3
itsdangerous==2.1.2
# via flask
jinja2==3.1.2
# via
# click-completion
# flask
kiss3==8.0.0
# via -r requirements.in
markupsafe==2.1.3
# via
# jinja2
# werkzeug
netaddr==0.8.0
# via oslo-config
oslo-config==9.2.0
# via -r requirements.in
oslo-i18n==6.1.0
# via oslo-config
pbr==5.11.1
# via
# -r requirements.in
# oslo-i18n
# stevedore
pluggy==1.3.0
# via -r requirements.in
plumbum==1.8.2
# via rpyc
pygments==2.16.1
# via rich
pyserial==3.5
# via pyserial-asyncio
pyserial-asyncio==0.6
# via kiss3
python-engineio==4.7.0
# via python-socketio
python-socketio==5.9.0
# via
# -r requirements.in
# flask-socketio
pytz==2023.3.post1
# via -r requirements.in
pyyaml==6.0.1
# via
# -r requirements.in
# oslo-config
requests==2.31.0
# via
# -r requirements.in
# oslo-config
# update-checker
rfc3986==2.0.0
# via oslo-config
rich==12.6.0
# via -r requirements.in
rpyc==5.3.1
# via -r requirements.in
rush==2021.4.0
# via -r requirements.in
shellingham==1.5.3
# via
# -r requirements.in
# click-completion
six==1.16.0
# via
# -r requirements.in
# click-completion
# eventlet
# imapclient
soupsieve==2.5
# via beautifulsoup4
stevedore==5.1.0
# via oslo-config
tabulate==0.9.0
# via -r requirements.in
thesmuggler==1.0.1
# via -r requirements.in
update-checker==0.18.0
# via -r requirements.in
urllib3==2.0.4
# via requests
validators==0.20.0
# via click-params
werkzeug==2.3.7
# via
# -r requirements.in
# flask
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: # The following packages are considered to be unsafe in a requirements file:
# setuptools # setuptools
+1 -1
View File
@@ -2,7 +2,7 @@
minversion = 2.9.0 minversion = 2.9.0
skipdist = True skipdist = True
skip_missing_interpreters = true skip_missing_interpreters = true
envlist = pep8,py{39,310} envlist = pep8,py{39,310,311}
#requires = tox-pipenv #requires = tox-pipenv
# pip==22.0.4 # pip==22.0.4
# pip-tools==5.4.0 # pip-tools==5.4.0