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
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):
"""
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
import aprsd
from aprsd import messaging, packets, plugin, stats, utils
from aprsd import kissclient, messaging, packets, plugin, stats, utils
LOG = logging.getLogger("APRSD")
@ -65,9 +65,38 @@ class APRSDFlask(flask_classful.FlaskView):
plugins = pm.get_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(
"index.html",
initial_stats=stats,
aprs_connection=aprs_connection,
callsign=self.config["aprs"]["login"],
version=aprsd.__version__,
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
import aprsd
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"])
stats.APRSDStats(config)
try:
cl = client.Client(config)
cl.client
except LoginError:
sys.exit(-1)
# Create the initial PM singleton and Register plugins
plugin_manager = plugin.PluginManager(config)
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
if flush:
LOG.debug("Deleting saved MsgTrack.")
@ -478,19 +491,15 @@ def server(
messaging.MsgTrack().load()
packets.PacketList(config=config)
packets.WatchList(config=config)
rx_thread = threads.APRSDRXThread(
msg_queues=threads.msg_queues,
config=config,
)
if kissclient.KISSClient.kiss_enabled(config):
kcl = kissclient.KISSClient(config=config)
# This initializes the client object.
kcl.client
rx_thread.start()
if "watch_list" in config["aprsd"] and config["aprsd"]["watch_list"].get(
"enabled",
True,
):
packets.WatchList(config=config)
kissrx_thread = threads.KISSRXThread(msg_queues=threads.msg_queues, config=config)
kissrx_thread.start()
messaging.MsgTrack().restart()

View File

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

View File

@ -8,7 +8,7 @@ import tracemalloc
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")
@ -180,9 +180,10 @@ class APRSDRXThread(APRSDThread):
class APRSDProcessPacketThread(APRSDThread):
def __init__(self, packet, config):
def __init__(self, packet, config, transport="aprsis"):
self.packet = packet
self.config = config
self.transport = transport
name = self.packet["raw"][:10]
super().__init__(f"RX_PACKET-{name}")
@ -237,6 +238,7 @@ class APRSDProcessPacketThread(APRSDThread):
self.config["aprs"]["login"],
fromcall,
msg_id=msg_id,
transport=self.transport,
)
ack.send()
@ -255,6 +257,7 @@ class APRSDProcessPacketThread(APRSDThread):
self.config["aprs"]["login"],
fromcall,
subreply,
transport=self.transport,
)
msg.send()
@ -271,6 +274,7 @@ class APRSDProcessPacketThread(APRSDThread):
self.config["aprs"]["login"],
fromcall,
reply,
transport=self.transport,
)
msg.send()
@ -283,6 +287,7 @@ class APRSDProcessPacketThread(APRSDThread):
self.config["aprs"]["login"],
fromcall,
reply,
transport=self.transport,
)
msg.send()
except Exception as ex:
@ -294,6 +299,7 @@ class APRSDProcessPacketThread(APRSDThread):
self.config["aprs"]["login"],
fromcall,
reply,
transport=self.transport,
)
msg.send()
@ -314,3 +320,66 @@ class APRSDTXThread(APRSDThread):
pass
# Continue to loop
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 = {
"ham": {"callsign": "NOCALL"},
"aprs": {
"login": "NOCALL",
"enabled": True,
"login": "CALLSIGN",
"password": "00000",
"host": "rotate.aprs2.net",
"port": 14580,
},
"kiss": {
"tcp": {
"enabled": False,
"host": "direwolf.ip.address",
"port": "8001",
},
"serial": {
"enabled": False,
"device": "/dev/ttyS0",
"baudrate": 9600,
},
},
"aprsd": {
"logfile": "/tmp/aprsd.log",
"logformat": DEFAULT_LOG_FORMAT,
@ -172,6 +185,9 @@ def add_config_comments(raw_yaml):
# lets insert a comment
raw_yaml = insert_str(
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 # https://apps.magicbug.co.uk/passcode",
end_idx,

View File

@ -220,7 +220,7 @@ function updateQuadData(chart, label, first, second, third, fourth) {
function update_stats( data ) {
$("#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"] );
const html_pretty = Prism.highlight(JSON.stringify(data, null, '\t'), Prism.languages.json, 'json');
$("#jsonstats").html(html_pretty);

View File

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

View File

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

View File

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