1
0
mirror of https://github.com/craigerl/aprsd.git synced 2025-07-31 12:52:24 -04:00

Got webchat working with KISS tcp

This patch reworks the KISS client to get rid of
aioax25 as it was too difficult to work with due to
heavy use of asyncio.

Switched to the kiss3 pypi library.
This commit is contained in:
Hemna 2022-11-22 13:32:19 -05:00
parent d717a22717
commit 7d970cbe70
6 changed files with 108 additions and 104 deletions

View File

@ -195,8 +195,8 @@ class KISSClient(Client):
@trace.trace @trace.trace
def setup_connection(self): def setup_connection(self):
ax25client = kiss.Aioax25Client(self.config) client = kiss.KISS3Client(self.config)
return ax25client return client
class ClientFactory: class ClientFactory:
@ -223,7 +223,7 @@ class ClientFactory:
elif KISSClient.is_enabled(self.config): elif KISSClient.is_enabled(self.config):
key = KISSClient.transport(self.config) key = KISSClient.transport(self.config)
LOG.debug(f"GET client {key}") LOG.debug(f"GET client '{key}'")
builder = self._builders.get(key) builder = self._builders.get(key)
if not builder: if not builder:
raise ValueError(key) raise ValueError(key)

View File

@ -1,21 +1,19 @@
import asyncio
import logging import logging
from aioax25 import interface import aprslib
from aioax25 import kiss as kiss from ax253 import Frame
from aioax25.aprs import APRSInterface import kiss
from aioax25.aprs.frame import APRSFrame
from aprsd import messaging
from aprsd.utils import trace
LOG = logging.getLogger("APRSD") LOG = logging.getLogger("APRSD")
class Aioax25Client: class KISS3Client:
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
self.loop = asyncio.get_event_loop()
self.setup() self.setup()
def setup(self): def setup(self):
@ -25,84 +23,65 @@ class Aioax25Client:
False, False,
): ):
LOG.debug( LOG.debug(
"Setting up Serial KISS connection to {}".format( "KISS({}) Serial connection to {}".format(
kiss.__version__,
self.config["kiss"]["serial"]["device"], self.config["kiss"]["serial"]["device"],
), ),
) )
self.kissdev = kiss.SerialKISSDevice( self.kiss = kiss.SerialKISS(
device=self.config["kiss"]["serial"]["device"], port=self.config["kiss"]["serial"]["device"],
baudrate=self.config["kiss"]["serial"].get("baudrate", 9600), speed=self.config["kiss"]["serial"].get("baudrate", 9600),
loop=self.loop, strip_df_start=True,
log=LOG,
) )
elif "tcp" in self.config["kiss"] and self.config["kiss"]["tcp"].get( elif "tcp" in self.config["kiss"] and self.config["kiss"]["tcp"].get(
"enabled", "enabled",
False, False,
): ):
LOG.debug( LOG.debug(
"Setting up KISSTCP Connection to {}:{}".format( "KISS({}) TCP Connection to {}:{}".format(
kiss.__version__,
self.config["kiss"]["tcp"]["host"], self.config["kiss"]["tcp"]["host"],
self.config["kiss"]["tcp"]["port"], self.config["kiss"]["tcp"]["port"],
), ),
) )
self.kissdev = kiss.TCPKISSDevice( self.kiss = kiss.TCPKISS(
self.config["kiss"]["tcp"]["host"], host=self.config["kiss"]["tcp"]["host"],
self.config["kiss"]["tcp"]["port"], port=int(self.config["kiss"]["tcp"]["port"]),
loop=self.loop, strip_df_start=True,
log=LOG,
) )
LOG.debug("Creating AX25Interface") LOG.debug("Starting KISS interface connection")
self.ax25int = interface.AX25Interface( self.kiss.start()
kissport=self.kissdev[0],
loop=self.loop,
log=LOG,
)
LOG.debug("Creating APRSInterface")
self.aprsint = APRSInterface(
ax25int=self.ax25int,
mycall=self.config["aprsd"]["callsign"],
log=LOG,
)
self.kissdev.open()
@trace.trace
def stop(self): def stop(self):
LOG.debug(self.kissdev) try:
self.loop.stop() self.kiss.stop()
self.kissdev.close() self.kiss.loop.call_soon_threadsafe(
self.kiss.protocol.transport.close,
)
except Exception as ex:
LOG.exception(ex)
def set_filter(self, filter): def set_filter(self, filter):
# This does nothing right now. # This does nothing right now.
pass pass
def consumer(self, callback, blocking=False, immortal=False, raw=False): def parse_frame(self, frame_bytes):
callsign = self.config["aprsd"]["callsign"] frame = Frame.from_bytes(frame_bytes)
call = callsign.split("-") # Now parse it with aprslib
if len(call) > 1: packet = aprslib.parse(str(frame))
callsign = call[0] kwargs = {
ssid = int(call[1]) "frame": str(frame),
else: "packet": packet,
ssid = 0 }
self.aprsint.bind(callback=callback, callsign=callsign, ssid=ssid, regex=False) self._parse_callback(**kwargs)
# async def set_after(fut, delay, value): def consumer(self, callback, blocking=False, immortal=False, raw=False):
# # Sleep for *delay* seconds. LOG.debug("Start blocking KISS consumer")
# await asyncio.sleep(delay) self._parse_callback = callback
# self.kiss.read(callback=self.parse_frame, min_frames=None)
# # Set *value* as a result of *fut* Future. LOG.debug("END blocking KISS consumer")
# fut.set_result(value)
#
# async def my_wait(fut):
# await fut
#
# fut = self.loop.create_future()
# self.loop.create_task(
# set_after(fut, 5, "nothing")
# )
LOG.debug("RUN FOREVER")
self.loop.run_forever()
# my_wait(fut)
def send(self, msg): def send(self, msg):
"""Send an APRS Message object.""" """Send an APRS Message object."""
@ -112,7 +91,10 @@ class Aioax25Client:
# payload # payload
# )).encode('US-ASCII'), # )).encode('US-ASCII'),
# payload = str(msg).encode('US-ASCII') # payload = str(msg).encode('US-ASCII')
msg_payload = f"{msg.message}{{{str(msg.id)}" if isinstance(msg, messaging.AckMessage):
msg_payload = f"ack{msg.id}"
else:
msg_payload = f"{msg.message}{{{str(msg.id)}"
payload = ( payload = (
":{:<9}:{}".format( ":{:<9}:{}".format(
msg.tocall, msg.tocall,
@ -120,19 +102,10 @@ class Aioax25Client:
) )
).encode("US-ASCII") ).encode("US-ASCII")
LOG.debug(f"Send '{payload}' TO KISS") LOG.debug(f"Send '{payload}' TO KISS")
frame = Frame.ui(
self.aprsint.transmit( destination=msg.tocall,
APRSFrame( source=msg.fromcall,
destination=msg.tocall, path=["WIDE1-1", "WIDE2-1"],
source=msg.fromcall, info=payload,
payload=payload,
repeaters=["WIDE1-1", "WIDE2-1"],
),
) )
self.kiss.write(frame)
# self.aprsint.send_message(
# addressee=msg.tocall,
# message=payload,
# path=["WIDE1-1", "WIDE2-1"],
# oneshot=True,
# )

View File

@ -20,7 +20,6 @@ from werkzeug.security import check_password_hash, generate_password_hash
import wrapt import wrapt
import aprsd import aprsd
from aprsd import aprsd as aprsd_main
from aprsd import cli_helper, client from aprsd import cli_helper, client
from aprsd import config as aprsd_config from aprsd import config as aprsd_config
from aprsd import messaging, packets, stats, threads, utils from aprsd import messaging, packets, stats, threads, utils
@ -44,6 +43,25 @@ msg_queues = {
} }
def signal_handler(sig, frame):
click.echo("signal_handler: called")
LOG.info(
f"Ctrl+C, Sending all threads({len(threads.APRSDThreadList())}) exit! "
f"Can take up to 10 seconds {datetime.datetime.now()}",
)
threads.APRSDThreadList().stop_all()
if "subprocess" not in str(frame):
time.sleep(1.5)
# messaging.MsgTrack().save()
# packets.WatchList().save()
# packets.SeenList().save()
LOG.info(stats.APRSDStats())
LOG.info("Telling flask to bail.")
signal.signal(signal.SIGTERM, sys.exit(0))
sys.exit(0)
class SentMessages(objectstore.ObjectStoreMixin): class SentMessages(objectstore.ObjectStoreMixin):
_instance = None _instance = None
lock = threading.Lock() lock = threading.Lock()
@ -123,11 +141,15 @@ def verify_password(username, password):
class WebChatRXThread(rx.APRSDRXThread): class WebChatRXThread(rx.APRSDRXThread):
"""Class that connects to aprsis and waits for messages.""" """Class that connects to aprsis/kiss and waits for messages."""
def connected(self, connected=True): def connected(self, connected=True):
self.connected = connected self.connected = connected
def stop(self):
self.thread_stop = True
client.factory.create().client.stop()
def loop(self): def loop(self):
# setup the consumer of messages and block until a messages # setup the consumer of messages and block until a messages
msg = None msg = None
@ -154,15 +176,10 @@ class WebChatRXThread(rx.APRSDRXThread):
time.sleep(2) time.sleep(2)
try: try:
# This will register a packet consumer with aprslib
# When new packets come in the consumer will process # When new packets come in the consumer will process
# the packet # the packet
# Do a partial here because the consumer signature doesn't allow # This call blocks until thread stop() is called.
# 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
self._client.client.consumer( self._client.client.consumer(
self.process_packet, raw=False, blocking=False, self.process_packet, raw=False, blocking=False,
) )
@ -177,12 +194,16 @@ class WebChatRXThread(rx.APRSDRXThread):
# This will cause a reconnect, next time client.get_client() # This will cause a reconnect, next time client.get_client()
# is called # is called
self._client.reset() self._client.reset()
# Continue to loop return True
time.sleep(1)
return True return True
def process_packet(self, *args, **kwargs): def process_packet(self, *args, **kwargs):
packet = self._client.decode_packet(*args, **kwargs) # packet = self._client.decode_packet(*args, **kwargs)
if "packet" in kwargs:
packet = kwargs["packet"]
else:
packet = self._client.decode_packet(*args, **kwargs)
LOG.debug(f"GOT Packet {packet}") LOG.debug(f"GOT Packet {packet}")
self.msg_queues["rx"].put(packet) self.msg_queues["rx"].put(packet)
@ -404,8 +425,9 @@ class SendMessageNamespace(Namespace):
msgs = SentMessages() msgs = SentMessages()
msgs.add(msg) msgs.add(msg)
msgs.set_status(msg.id, "Sending") msgs.set_status(msg.id, "Sending")
obj = msgs.get(self.msg.id)
socketio.emit( socketio.emit(
"sent", SentMessages().get(self.msg.id), "sent", obj,
namespace="/sendmsg", namespace="/sendmsg",
) )
msg.send() msg.send()
@ -527,8 +549,8 @@ def webchat(ctx, flush, port):
quiet = ctx.obj["quiet"] quiet = ctx.obj["quiet"]
config = ctx.obj["config"] config = ctx.obj["config"]
signal.signal(signal.SIGINT, aprsd_main.signal_handler) signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, aprsd_main.signal_handler) signal.signal(signal.SIGTERM, signal_handler)
if not quiet: if not quiet:
click.echo("Load config") click.echo("Load config")
@ -567,28 +589,29 @@ def webchat(ctx, flush, port):
packets.WatchList(config=config) packets.WatchList(config=config)
packets.SeenList(config=config) packets.SeenList(config=config)
aprsd_main.flask_enabled = True
(socketio, app) = init_flask(config, loglevel, quiet) (socketio, app) = init_flask(config, loglevel, quiet)
rx_thread = WebChatRXThread( rx_thread = WebChatRXThread(
msg_queues=msg_queues, msg_queues=msg_queues,
config=config, config=config,
) )
LOG.warning("Start RX Thread") LOG.info("Start RX Thread")
rx_thread.start() rx_thread.start()
tx_thread = WebChatTXThread( tx_thread = WebChatTXThread(
msg_queues=msg_queues, msg_queues=msg_queues,
config=config, config=config,
socketio=socketio, socketio=socketio,
) )
LOG.warning("Start TX Thread") LOG.info("Start TX Thread")
tx_thread.start() tx_thread.start()
keepalive = threads.KeepAliveThread(config=config) keepalive = threads.KeepAliveThread(config=config)
LOG.warning("Start KeepAliveThread") LOG.info("Start KeepAliveThread")
keepalive.start() keepalive.start()
LOG.warning("Start socketio.run()") LOG.info("Start socketio.run()")
socketio.run( socketio.run(
app, app,
host=config["aprsd"]["web"]["host"], host=config["aprsd"]["web"]["host"],
port=port, port=port,
) )
LOG.info("WebChat exiting!!!! Bye.")

View File

@ -10,3 +10,5 @@ pip-tools
pytest pytest
pytest-cov pytest-cov
gray gray
pip==22.0.4
pip-tools==5.4.0

View File

@ -1,8 +1,8 @@
aioax25>=0.0.10
aprslib>=0.7.0 aprslib>=0.7.0
click click
click-completion click-completion
flask flask==2.1.2
werkzeug==2.1.2
flask-classful flask-classful
flask-httpauth flask-httpauth
imapclient imapclient
@ -25,3 +25,6 @@ rich
# For the list-plugins pypi.org search scraping # For the list-plugins pypi.org search scraping
beautifulsoup4 beautifulsoup4
wrapt wrapt
# kiss3 uses attrs
kiss3
attrs==22.1.0

View File

@ -3,6 +3,9 @@ minversion = 2.9.0
skipdist = True skipdist = True
skip_missing_interpreters = true skip_missing_interpreters = true
envlist = pre-commit,pep8,py{36,37,38,39} envlist = pre-commit,pep8,py{36,37,38,39}
#requires = tox-pipenv
# pip==22.0.4
# pip-tools==5.4.0
# Activate isolated build environment. tox will use a virtual environment # Activate isolated build environment. tox will use a virtual environment
# to build a source distribution from the source tree. For build tools and # to build a source distribution from the source tree. For build tools and