Merge pull request #50 from craigerl/tcpkiss

Added the ability to use direwolf KISS socket
This commit is contained in:
Walter A. Boring IV 2021-09-01 16:58:57 -04:00 committed by GitHub
commit d243e577f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 364 additions and 42 deletions

View File

@ -90,6 +90,11 @@ class Aprsdis(aprslib.IS):
self.thread_stop = True self.thread_stop = True
LOG.info("Shutdown Aprsdis client.") LOG.info("Shutdown Aprsdis client.")
def send(self, msg):
"""Send an APRS Message object."""
line = str(msg)
self.sendall(line)
def _socket_readlines(self, blocking=False): def _socket_readlines(self, blocking=False):
""" """
Generator for complete lines, received from the server Generator for complete lines, received from the server

View File

@ -11,7 +11,7 @@ from flask_httpauth import HTTPBasicAuth
from werkzeug.security import check_password_hash, generate_password_hash from werkzeug.security import check_password_hash, generate_password_hash
import aprsd import aprsd
from aprsd import messaging, packets, plugin, stats, utils from aprsd import kissclient, messaging, packets, plugin, stats, utils
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -65,9 +65,38 @@ class APRSDFlask(flask_classful.FlaskView):
plugins = pm.get_plugins() plugins = pm.get_plugins()
plugin_count = len(plugins) plugin_count = len(plugins)
if self.config["aprs"].get("enabled", True):
transport = "aprs-is"
aprs_connection = (
"APRS-IS Server: <a href='http://status.aprs2.net' >"
"{}</a>".format(stats["stats"]["aprs-is"]["server"])
)
else:
# We might be connected to a KISS socket?
if kissclient.KISSClient.kiss_enabled(self.config):
transport = kissclient.KISSClient.transport(self.config)
if transport == kissclient.TRANSPORT_TCPKISS:
aprs_connection = (
"TCPKISS://{}:{}".format(
self.config["kiss"]["tcp"]["host"],
self.config["kiss"]["tcp"]["port"],
)
)
elif transport == kissclient.TRANSPORT_SERIALKISS:
aprs_connection = (
"SerialKISS://{}@{} baud".format(
self.config["kiss"]["serial"]["device"],
self.config["kiss"]["serial"]["baudrate"],
)
)
stats["transport"] = transport
stats["aprs_connection"] = aprs_connection
return flask.render_template( return flask.render_template(
"index.html", "index.html",
initial_stats=stats, initial_stats=stats,
aprs_connection=aprs_connection,
callsign=self.config["aprs"]["login"], callsign=self.config["aprs"]["login"],
version=aprsd.__version__, version=aprsd.__version__,
config_json=json.dumps(self.config), config_json=json.dumps(self.config),

152
aprsd/kissclient.py Normal file
View File

@ -0,0 +1,152 @@
import asyncio
import logging
from aioax25 import interface
from aioax25 import kiss as kiss
from aioax25.aprs import APRSInterface
from aprsd import trace
TRANSPORT_TCPKISS = "tcpkiss"
TRANSPORT_SERIALKISS = "serialkiss"
LOG = logging.getLogger("APRSD")
class KISSClient:
_instance = None
config = None
ax25client = None
loop = None
def __new__(cls, *args, **kwargs):
"""Singleton for this class."""
if cls._instance is None:
cls._instance = super().__new__(cls)
# initialize shit here
return cls._instance
def __init__(self, config=None):
if config:
self.config = config
@staticmethod
def kiss_enabled(config):
"""Return if tcp or serial KISS is enabled."""
if "kiss" not in config:
return False
if "serial" in config["kiss"]:
if config["kiss"]["serial"].get("enabled", False):
return True
if "tcp" in config["kiss"]:
if config["kiss"]["tcp"].get("enabled", False):
return True
@staticmethod
def transport(config):
if "serial" in config["kiss"]:
if config["kiss"]["serial"].get("enabled", False):
return TRANSPORT_SERIALKISS
if "tcp" in config["kiss"]:
if config["kiss"]["tcp"].get("enabled", False):
return TRANSPORT_TCPKISS
@property
def client(self):
if not self.ax25client:
self.ax25client = self.setup_connection()
return self.ax25client
def reset(self):
"""Call this to fore a rebuild/reconnect."""
self.ax25client.stop()
del self.ax25client
@trace.trace
def setup_connection(self):
ax25client = Aioax25Client(self.config)
LOG.debug("Complete")
return ax25client
class Aioax25Client:
def __init__(self, config):
self.config = config
self.setup()
def setup(self):
# we can be TCP kiss or Serial kiss
self.loop = asyncio.get_event_loop()
if "serial" in self.config["kiss"] and self.config["kiss"]["serial"].get(
"enabled",
False,
):
LOG.debug(
"Setting up Serial KISS connection to {}".format(
self.config["kiss"]["serial"]["device"],
),
)
self.kissdev = kiss.SerialKISSDevice(
device=self.config["kiss"]["serial"]["device"],
baudrate=self.config["kiss"]["serial"].get("baudrate", 9600),
loop=self.loop,
)
elif "tcp" in self.config["kiss"] and self.config["kiss"]["tcp"].get(
"enabled",
False,
):
LOG.debug(
"Setting up KISSTCP Connection to {}:{}".format(
self.config["kiss"]["tcp"]["host"],
self.config["kiss"]["tcp"]["port"],
),
)
self.kissdev = kiss.TCPKISSDevice(
self.config["kiss"]["tcp"]["host"],
self.config["kiss"]["tcp"]["port"],
loop=self.loop,
log=LOG,
)
self.kissdev.open()
self.kissport0 = self.kissdev[0]
LOG.debug("Creating AX25Interface")
self.ax25int = interface.AX25Interface(kissport=self.kissport0, loop=self.loop)
LOG.debug("Creating APRSInterface")
self.aprsint = APRSInterface(
ax25int=self.ax25int,
mycall=self.config["kiss"]["callsign"],
log=LOG,
)
def stop(self):
LOG.debug(self.kissdev)
self.kissdev._close()
self.loop.stop()
def consumer(self, callback, callsign=None):
if not callsign:
callsign = self.config["ham"]["callsign"]
self.aprsint.bind(callback=callback, callsign="WB4BOR", ssid=12, regex=False)
def send(self, msg):
"""Send an APRS Message object."""
payload = f"{msg._filter_for_send()}"
self.aprsint.send_message(
addressee=msg.tocall,
message=payload,
path=["WIDE1-1", "WIDE2-1"],
oneshot=True,
)
def get_client():
cl = KISSClient()
return cl.client

View File

@ -37,7 +37,8 @@ import click_completion
# local imports here # local imports here
import aprsd import aprsd
from aprsd import ( from aprsd import (
client, flask, messaging, packets, plugin, stats, threads, trace, utils, client, flask, kissclient, messaging, packets, plugin, stats, threads,
trace, utils,
) )
@ -458,16 +459,28 @@ def server(
trace.setup_tracing(["method", "api"]) trace.setup_tracing(["method", "api"])
stats.APRSDStats(config) stats.APRSDStats(config)
try:
cl = client.Client(config)
cl.client
except LoginError:
sys.exit(-1)
# Create the initial PM singleton and Register plugins # Create the initial PM singleton and Register plugins
plugin_manager = plugin.PluginManager(config) plugin_manager = plugin.PluginManager(config)
plugin_manager.setup_plugins() plugin_manager.setup_plugins()
if config["aprs"].get("enabled", True):
try:
cl = client.Client(config)
cl.client
except LoginError:
sys.exit(-1)
rx_thread = threads.APRSDRXThread(
msg_queues=threads.msg_queues,
config=config,
)
rx_thread.start()
else:
LOG.info(
"APRS network connection Not Enabled in config. This is"
" for setups without internet connectivity.",
)
# Now load the msgTrack from disk if any # Now load the msgTrack from disk if any
if flush: if flush:
LOG.debug("Deleting saved MsgTrack.") LOG.debug("Deleting saved MsgTrack.")
@ -478,19 +491,15 @@ def server(
messaging.MsgTrack().load() messaging.MsgTrack().load()
packets.PacketList(config=config) packets.PacketList(config=config)
packets.WatchList(config=config)
rx_thread = threads.APRSDRXThread( if kissclient.KISSClient.kiss_enabled(config):
msg_queues=threads.msg_queues, kcl = kissclient.KISSClient(config=config)
config=config, # This initializes the client object.
) kcl.client
rx_thread.start() kissrx_thread = threads.KISSRXThread(msg_queues=threads.msg_queues, config=config)
kissrx_thread.start()
if "watch_list" in config["aprsd"] and config["aprsd"]["watch_list"].get(
"enabled",
True,
):
packets.WatchList(config=config)
messaging.MsgTrack().restart() messaging.MsgTrack().restart()

View File

@ -9,7 +9,7 @@ import re
import threading import threading
import time import time
from aprsd import client, packets, stats, threads, trace, utils from aprsd import client, kissclient, packets, stats, threads, trace, utils
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -18,6 +18,10 @@ LOG = logging.getLogger("APRSD")
# and it's ok, but don't send a usage string back # and it's ok, but don't send a usage string back
NULL_MESSAGE = -1 NULL_MESSAGE = -1
MESSAGE_TRANSPORT_TCPKISS = "tcpkiss"
MESSAGE_TRANSPORT_SERIALKISS = "serialkiss"
MESSAGE_TRANSPORT_APRSIS = "aprsis"
class MsgTrack: class MsgTrack:
"""Class to keep track of outstanding text messages. """Class to keep track of outstanding text messages.
@ -228,7 +232,15 @@ class Message(metaclass=abc.ABCMeta):
last_send_time = 0 last_send_time = 0
last_send_attempt = 0 last_send_attempt = 0
def __init__(self, fromcall, tocall, msg_id=None): transport = None
def __init__(
self,
fromcall,
tocall,
msg_id=None,
transport=MESSAGE_TRANSPORT_APRSIS,
):
self.fromcall = fromcall self.fromcall = fromcall
self.tocall = tocall self.tocall = tocall
if not msg_id: if not msg_id:
@ -236,11 +248,18 @@ class Message(metaclass=abc.ABCMeta):
c.increment() c.increment()
msg_id = c.value msg_id = c.value
self.id = msg_id self.id = msg_id
self.transport = transport
@abc.abstractmethod @abc.abstractmethod
def send(self): def send(self):
"""Child class must declare.""" """Child class must declare."""
def get_transport(self):
if self.transport == MESSAGE_TRANSPORT_APRSIS:
return client.get_client()
elif self.transport == MESSAGE_TRANSPORT_TCPKISS:
return kissclient.get_client()
class RawMessage(Message): class RawMessage(Message):
"""Send a raw message. """Send a raw message.
@ -252,8 +271,8 @@ class RawMessage(Message):
message = None message = None
def __init__(self, message): def __init__(self, message, transport=MESSAGE_TRANSPORT_APRSIS):
super().__init__(None, None, msg_id=None) super().__init__(None, None, msg_id=None, transport=transport)
self.message = message self.message = message
def dict(self): def dict(self):
@ -282,7 +301,7 @@ class RawMessage(Message):
def send_direct(self): def send_direct(self):
"""Send a message without a separate thread.""" """Send a message without a separate thread."""
cl = client.get_client() cl = self.get_transport()
log_message( log_message(
"Sending Message Direct", "Sending Message Direct",
str(self).rstrip("\n"), str(self).rstrip("\n"),
@ -290,7 +309,7 @@ class RawMessage(Message):
tocall=self.tocall, tocall=self.tocall,
fromcall=self.fromcall, fromcall=self.fromcall,
) )
cl.sendall(str(self)) cl.send(self)
stats.APRSDStats().msgs_sent_inc() stats.APRSDStats().msgs_sent_inc()
@ -299,8 +318,16 @@ class TextMessage(Message):
message = None message = None
def __init__(self, fromcall, tocall, message, msg_id=None, allow_delay=True): def __init__(
super().__init__(fromcall, tocall, msg_id) self,
fromcall,
tocall,
message,
msg_id=None,
allow_delay=True,
transport=MESSAGE_TRANSPORT_APRSIS,
):
super().__init__(fromcall, tocall, msg_id, transport=transport)
self.message = message self.message = message
# do we try and save this message for later if we don't get # do we try and save this message for later if we don't get
# an ack? Some messages we don't want to do this ever. # an ack? Some messages we don't want to do this ever.
@ -354,7 +381,7 @@ class TextMessage(Message):
def send_direct(self): def send_direct(self):
"""Send a message without a separate thread.""" """Send a message without a separate thread."""
cl = client.get_client() cl = self.get_transport()
log_message( log_message(
"Sending Message Direct", "Sending Message Direct",
str(self).rstrip("\n"), str(self).rstrip("\n"),
@ -362,7 +389,7 @@ class TextMessage(Message):
tocall=self.tocall, tocall=self.tocall,
fromcall=self.fromcall, fromcall=self.fromcall,
) )
cl.sendall(str(self)) cl.send(self)
stats.APRSDStats().msgs_tx_inc() stats.APRSDStats().msgs_tx_inc()
@ -382,7 +409,6 @@ class SendMessageThread(threads.APRSDThread):
last send attempt is old enough. last send attempt is old enough.
""" """
cl = client.get_client()
tracker = MsgTrack() tracker = MsgTrack()
# lets see if the message is still in the tracking queue # lets see if the message is still in the tracking queue
msg = tracker.get(self.msg.id) msg = tracker.get(self.msg.id)
@ -392,6 +418,7 @@ class SendMessageThread(threads.APRSDThread):
LOG.info("Message Send Complete via Ack.") LOG.info("Message Send Complete via Ack.")
return False return False
else: else:
cl = msg.get_transport()
send_now = False send_now = False
if msg.last_send_attempt == msg.retry_count: if msg.last_send_attempt == msg.retry_count:
# we reached the send limit, don't send again # we reached the send limit, don't send again
@ -422,7 +449,7 @@ class SendMessageThread(threads.APRSDThread):
retry_number=msg.last_send_attempt, retry_number=msg.last_send_attempt,
msg_num=msg.id, msg_num=msg.id,
) )
cl.sendall(str(msg)) cl.send(msg)
stats.APRSDStats().msgs_tx_inc() stats.APRSDStats().msgs_tx_inc()
packets.PacketList().add(msg.dict()) packets.PacketList().add(msg.dict())
msg.last_send_time = datetime.datetime.now() msg.last_send_time = datetime.datetime.now()
@ -436,8 +463,8 @@ class SendMessageThread(threads.APRSDThread):
class AckMessage(Message): class AckMessage(Message):
"""Class for building Acks and sending them.""" """Class for building Acks and sending them."""
def __init__(self, fromcall, tocall, msg_id): def __init__(self, fromcall, tocall, msg_id, transport=MESSAGE_TRANSPORT_APRSIS):
super().__init__(fromcall, tocall, msg_id=msg_id) super().__init__(fromcall, tocall, msg_id=msg_id, transport=transport)
def dict(self): def dict(self):
now = datetime.datetime.now() now = datetime.datetime.now()
@ -463,6 +490,9 @@ class AckMessage(Message):
self.id, self.id,
) )
def _filter_for_send(self):
return f"ack{self.id}"
def send(self): def send(self):
LOG.debug(f"Send ACK({self.tocall}:{self.id}) to radio.") LOG.debug(f"Send ACK({self.tocall}:{self.id}) to radio.")
thread = SendAckThread(self) thread = SendAckThread(self)
@ -470,7 +500,7 @@ class AckMessage(Message):
def send_direct(self): def send_direct(self):
"""Send an ack message without a separate thread.""" """Send an ack message without a separate thread."""
cl = client.get_client() cl = self.get_transport()
log_message( log_message(
"Sending ack", "Sending ack",
str(self).rstrip("\n"), str(self).rstrip("\n"),
@ -479,7 +509,7 @@ class AckMessage(Message):
tocall=self.tocall, tocall=self.tocall,
fromcall=self.fromcall, fromcall=self.fromcall,
) )
cl.sendall(str(self)) cl.send(self)
class SendAckThread(threads.APRSDThread): class SendAckThread(threads.APRSDThread):
@ -515,7 +545,7 @@ class SendAckThread(threads.APRSDThread):
send_now = True send_now = True
if send_now: if send_now:
cl = client.get_client() cl = self.ack.get_transport()
log_message( log_message(
"Sending ack", "Sending ack",
str(self.ack).rstrip("\n"), str(self.ack).rstrip("\n"),
@ -524,7 +554,7 @@ class SendAckThread(threads.APRSDThread):
tocall=self.ack.tocall, tocall=self.ack.tocall,
retry_number=self.ack.last_send_attempt, retry_number=self.ack.last_send_attempt,
) )
cl.sendall(str(self.ack)) cl.send(self.ack)
stats.APRSDStats().ack_tx_inc() stats.APRSDStats().ack_tx_inc()
packets.PacketList().add(self.ack.dict()) packets.PacketList().add(self.ack.dict())
self.ack.last_send_attempt += 1 self.ack.last_send_attempt += 1

View File

@ -8,7 +8,7 @@ import tracemalloc
import aprslib import aprslib
from aprsd import client, messaging, packets, plugin, stats, utils from aprsd import client, kissclient, messaging, packets, plugin, stats, utils
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
@ -180,9 +180,10 @@ class APRSDRXThread(APRSDThread):
class APRSDProcessPacketThread(APRSDThread): class APRSDProcessPacketThread(APRSDThread):
def __init__(self, packet, config): def __init__(self, packet, config, transport="aprsis"):
self.packet = packet self.packet = packet
self.config = config self.config = config
self.transport = transport
name = self.packet["raw"][:10] name = self.packet["raw"][:10]
super().__init__(f"RX_PACKET-{name}") super().__init__(f"RX_PACKET-{name}")
@ -237,6 +238,7 @@ class APRSDProcessPacketThread(APRSDThread):
self.config["aprs"]["login"], self.config["aprs"]["login"],
fromcall, fromcall,
msg_id=msg_id, msg_id=msg_id,
transport=self.transport,
) )
ack.send() ack.send()
@ -255,6 +257,7 @@ class APRSDProcessPacketThread(APRSDThread):
self.config["aprs"]["login"], self.config["aprs"]["login"],
fromcall, fromcall,
subreply, subreply,
transport=self.transport,
) )
msg.send() msg.send()
@ -271,6 +274,7 @@ class APRSDProcessPacketThread(APRSDThread):
self.config["aprs"]["login"], self.config["aprs"]["login"],
fromcall, fromcall,
reply, reply,
transport=self.transport,
) )
msg.send() msg.send()
@ -283,6 +287,7 @@ class APRSDProcessPacketThread(APRSDThread):
self.config["aprs"]["login"], self.config["aprs"]["login"],
fromcall, fromcall,
reply, reply,
transport=self.transport,
) )
msg.send() msg.send()
except Exception as ex: except Exception as ex:
@ -294,6 +299,7 @@ class APRSDProcessPacketThread(APRSDThread):
self.config["aprs"]["login"], self.config["aprs"]["login"],
fromcall, fromcall,
reply, reply,
transport=self.transport,
) )
msg.send() msg.send()
@ -314,3 +320,66 @@ class APRSDTXThread(APRSDThread):
pass pass
# Continue to loop # Continue to loop
return True return True
class KISSRXThread(APRSDThread):
"""Thread that connects to direwolf's TCPKISS interface.
All Packets are processed and sent back out the direwolf
interface instead of the aprs-is server.
"""
def __init__(self, msg_queues, config):
super().__init__("KISSRX_MSG")
self.msg_queues = msg_queues
self.config = config
def stop(self):
self.thread_stop = True
kissclient.get_client().stop()
def loop(self):
kiss_client = kissclient.get_client()
# setup the consumer of messages and block until a messages
try:
# This will register a packet consumer with aprslib
# When new packets come in the consumer will process
# the packet
# Do a partial here because the consumer signature doesn't allow
# For kwargs to be passed in to the consumer func we declare
# and the aprslib developer didn't want to allow a PR to add
# kwargs. :(
# https://github.com/rossengeorgiev/aprs-python/pull/56
kiss_client.consumer(self.process_packet, callsign=self.config["kiss"]["callsign"])
kiss_client.loop.run_forever()
except aprslib.exceptions.ConnectionDrop:
LOG.error("Connection dropped, reconnecting")
time.sleep(5)
# Force the deletion of the client object connected to aprs
# This will cause a reconnect, next time client.get_client()
# is called
client.Client().reset()
# Continue to loop
def process_packet(self, interface, frame):
"""Process a packet recieved from aprs-is server."""
LOG.debug(f"Got an APRS Frame '{frame}'")
# try and nuke the * from the fromcall sign.
frame.header._source._ch = False
payload = str(frame.payload.decode())
msg = f"{str(frame.header)}:{payload}"
LOG.debug(f"Decoding {msg}")
packet = aprslib.parse(msg)
LOG.debug(packet)
thread = APRSDProcessPacketThread(
packet=packet, config=self.config,
transport=messaging.MESSAGE_TRANSPORT_TCPKISS,
)
thread.start()
return

View File

@ -37,11 +37,24 @@ DEFAULT_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p"
DEFAULT_CONFIG_DICT = { DEFAULT_CONFIG_DICT = {
"ham": {"callsign": "NOCALL"}, "ham": {"callsign": "NOCALL"},
"aprs": { "aprs": {
"login": "NOCALL", "enabled": True,
"login": "CALLSIGN",
"password": "00000", "password": "00000",
"host": "rotate.aprs2.net", "host": "rotate.aprs2.net",
"port": 14580, "port": 14580,
}, },
"kiss": {
"tcp": {
"enabled": False,
"host": "direwolf.ip.address",
"port": "8001",
},
"serial": {
"enabled": False,
"device": "/dev/ttyS0",
"baudrate": 9600,
},
},
"aprsd": { "aprsd": {
"logfile": "/tmp/aprsd.log", "logfile": "/tmp/aprsd.log",
"logformat": DEFAULT_LOG_FORMAT, "logformat": DEFAULT_LOG_FORMAT,
@ -172,6 +185,9 @@ def add_config_comments(raw_yaml):
# lets insert a comment # lets insert a comment
raw_yaml = insert_str( raw_yaml = insert_str(
raw_yaml, raw_yaml,
"\n # Set enabled to False if there is no internet connectivity."
"\n # This is useful for a direwolf KISS aprs connection only. "
"\n"
"\n # Get the passcode for your callsign here: " "\n # Get the passcode for your callsign here: "
"\n # https://apps.magicbug.co.uk/passcode", "\n # https://apps.magicbug.co.uk/passcode",
end_idx, end_idx,

View File

@ -220,7 +220,7 @@ function updateQuadData(chart, label, first, second, third, fourth) {
function update_stats( data ) { function update_stats( data ) {
$("#version").text( data["stats"]["aprsd"]["version"] ); $("#version").text( data["stats"]["aprsd"]["version"] );
$("#aprsis").html( "APRS-IS Server: <a href='http://status.aprs2.net' >" + data["stats"]["aprs-is"]["server"] + "</a>" ); $("#aprs_connection").html( data["aprs_connection"] );
$("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] ); $("#uptime").text( "uptime: " + data["stats"]["aprsd"]["uptime"] );
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json'); const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
$("#jsonstats").html(html_pretty); $("#jsonstats").html(html_pretty);

View File

@ -58,7 +58,7 @@
<div class='left floated ten wide column'> <div class='left floated ten wide column'>
<span style='color: green'>{{ callsign }}</span> <span style='color: green'>{{ callsign }}</span>
connected to connected to
<span style='color: blue' id='aprsis'>NONE</span> <span style='color: blue' id='aprs_connection'>{{ aprs_connection|safe }}</span>
</div> </div>
<div class='right floated four wide column'> <div class='right floated four wide column'>

View File

@ -1,3 +1,4 @@
aioax25>=0.0.10
aprslib aprslib
click click
click-completion click-completion

View File

@ -4,6 +4,8 @@
# #
# pip-compile requirements.in # pip-compile requirements.in
# #
aioax25==0.0.10
# via -r requirements.in
aprslib==0.6.47 aprslib==0.6.47
# via -r requirements.in # via -r requirements.in
backoff==1.10.0 backoff==1.10.0
@ -21,6 +23,8 @@ click==7.1.2
# -r requirements.in # -r requirements.in
# click-completion # click-completion
# flask # flask
contexter==0.1.4
# via signalslot
cryptography==3.4.7 cryptography==3.4.7
# via pyopenssl # via pyopenssl
dnspython==2.1.0 dnspython==2.1.0
@ -72,6 +76,8 @@ pycparser==2.20
# via cffi # via cffi
pyopenssl==20.0.1 pyopenssl==20.0.1
# via opencage # via opencage
pyserial==3.5
# via aioax25
python-dateutil==2.8.1 python-dateutil==2.8.1
# via pandas # via pandas
pytz==2021.1 pytz==2021.1
@ -88,6 +94,8 @@ requests==2.25.1
# yfinance # yfinance
shellingham==1.4.0 shellingham==1.4.0
# via click-completion # via click-completion
signalslot==0.1.2
# via aioax25
six==1.15.0 six==1.15.0
# via # via
# -r requirements.in # -r requirements.in
@ -96,12 +104,15 @@ six==1.15.0
# opencage # opencage
# pyopenssl # pyopenssl
# python-dateutil # python-dateutil
# signalslot
thesmuggler==1.0.1 thesmuggler==1.0.1
# via -r requirements.in # via -r requirements.in
update-checker==0.18.0 update-checker==0.18.0
# via -r requirements.in # via -r requirements.in
urllib3==1.26.5 urllib3==1.26.5
# via requests # via requests
weakrefmethod==1.0.3
# via signalslot
werkzeug==1.0.1 werkzeug==1.0.1
# via flask # via flask
yfinance==0.1.59 yfinance==0.1.59