User Guide
Rather than just lay out the API class by class and method by method, this User Guide walks through some use cases for the PyHam KISS package.
For complete details of the classes and methods, see the API Reference.
Receiving and decoding packets
Our first task will be to monitor all incoming packets so that we can watch all of the incoming data. There are several ways in which we could do this. We could build a command line tool that prints out each incoming packet, and blocks until the next packet arrives. More interesting, though, is to build a GUI application that remains responsive to the user while waiting for new packets to arrive.
Each GUI framework provides a slightly different means of enabling some kind
of background operation so that, for example in our situation, incoming packets
can be processed as the user continues with other tasks. While we could build
things differently for each framework, the pyham_kiss package uses threads,
enabling a common approach to be taken.
To many, threads are a scary topic. However, there are only a couple of aspects that are important to understand in our scenario.
The
pyham_kisspackage uses a background thread to receive incoming packets. As each new packet arrives, it calls a function provided by the client and passes the decoded packet.Almost all GUI frameworks require that any changes made to the UI must be done from the main application thread. This means that we cannot simply update the UI from the callback function mentioned above. Instead, we must synchronize our updates.
Let’s look at some code. The first thing we need to do is set up the code that will start, and later stop, the background thread for receiving packets.
Note
For a couple of reasons, we are going to set up our application as its own
class. This is not strictly necessary, but it avoids the need for the use
of the global keyword when referencing shared data. It also meshes well
with the manner in which GUI applications are typically constructed, though
we will avoid reference to any particular GUI framework in this guide.
Here’s the bare-bones outline of our application class.
class Application:
def __init__(self, host, port):
super().__init__()
# Connection to the KISS server
self._connection = None
# Synchronized queue of incoming frames
self._frame_queue = queue.Queue()
# Connect to the KISS server to receive packets
self._start_receiving(host, port)
def _quit(self):
self._stop_receiving()
def _start_receiving(self, host, port):
pass
def _stop_receiving(self):
pass
There are a couple of things to note here.
The
Queueinstance will be our means of communication between the thread that will be receiving incoming packets and the thread that runs the user interface.The
_start_receiving()method is called right away, initiating reception of packets as soon as the application starts. In a more user-friendly application, it may be more practical to wait until the user provides the host and port via a form or dialog. Similarly, the_stop_receiving()method is called on application exit, rather than upon a specific user request.Other code specific to the GUI framework’s setup and teardown will be needed in any real application. It is skipped here for clarity and focus.
Now let’s add the code to connect to the KISS server and start receiving packets.
def _start_receiving(self, host, port):
# Create a new connection instance, providing our callback
self._connection = kiss.Connection(receive_callback)
error = None
try:
# Make the connection to the KISS server
self._connection.connect_to_server(host, int(port))
except socket.gaierror as e:
if e.errno == socket.EAI_NONAME:
error = 'Server name not found'
else:
error = 'Invalid server address'
except ConnectionRefusedError:
error = 'Connection refused by server'
except Exception:
error = 'Unknown error connecting to server'
else:
# Set up a UI mechanism to handle new incoming data
set_idle_callback(self._process_frames)
if error:
print(error)
self._quit()
A few more things to note here:
When we create the connection instance, we provide the callback that will be called for each incoming packet. We will see this function shortly.
Once we have successfully connected to the server, we set up the mechanism that will be used to check our queue periodically for new information to be added to our user interface. This mechanism is specific to each GUI framework. Here we have called a fictional function named
set_idle_callback(), and provided it with a member function that we will see later.While we don’t usually include this much error handling code in this guide, here it is illustrative of the kinds of things that could go wrong while attempting to connect to the server.
At this point, the connect_to_server() function has created and started a
background thread to receive incoming packets from the KISS server. Now we
need to provide the means for handling those packets. There are two parts to
this, provided, as you may have surmised, by receive_callback() and
_process_frames(). These functions add items to, and remove items from, our
synchronized queue, respectively.
The receive_callback() is so simple, and has so little to do, that we will
embed it within our _start_receiving() method. The callback receives the
id of the KISS port on which the packet arrived, and, of course, the packet
itself. The packet has already been decoded, so we now have the raw packet
data.
Note that the receive_callback() function is invoked within the background
thread that is receiving packets from the server. Thus it cannot interact with
the user interface directly, which is why it is putting items on to the queue
instead.
def _start_receiving(self, host, port):
def receive_callback(kiss_port, data):
frame = ax25.Frame.unpack(data)
self._frame_queue.put((frame, datetime.datetime.now()))
In our application scenario, we are handling AX.25 packets coming from a server
such as Direwolf, so we can go ahead and unpack the frame data, using the
pyham_ax25 package. Once unpacked, we can add it to our queue, along with
the timestamp of its arrival, so that it can be later processed for possible
display in the user interface. In your application, you could also choose to
perform no processing at this stage and simply add the incoming raw packet to
the queue.
Recall that, using our fictional set_idle_callback() function, we told the
GUI framework that it should invoke our _process_frames() method when it
has some idle time (i.e. the application is not busy doing other things, such
as interacting with the user). Thus the _process_frames() method will be
invoked within the same thread as the user interface (typically the
application’s main thread), and can update that user interface as necessary.
def _process_frames(self):
while not self._frame_queue.empty():
(frame, timestamp) = self._frame_queue.get()
self._display_new_frame_info(frame.src, frame.dst, timestamp)
It is important to note here that,in this scenario, we are emptying the queue
of all available frames each time _process_frames() is invoked. Since we
are simply displaying the source, destination, and timestamp of each frame,
this places no noticeable load on the application, so it does not cause any
problems. However, if your application needs to do significant work with each
incoming frame, you will need to take additional measures in order to avoid
locking up the user interface, potentially processing only one or a few frames
each time _process_frames() is invoked.
Sending an Unproto message
A common use for KISS in conjunction with a server such as Direwolf is to send periodic Unproto messages, perhaps as a beacon. While we won’t go into the mechanics of setting up a periodic task here, we’ll see just how easy it is to construct and send our messages.
For simplicity, we’ll hardcode the details of the message we’re going to send.
The first thing we need to do is create an AX.25 frame containing these details
- who it’s being sent from, who it’s being sent to, and the
message itself. We do this using the pyham_ax25 package.
CALL_FROM = 'KD6YAM-2'
CALL_TO = 'BEACON'
MESSAGE = 'Testing, testing, 1, 2, 3'
PID_TEXT = 0xF0 # We'll be sending plain text
frame = ax25.Frame(
CALL_TO,
CALL_FROM,
control=ax25.Control(ax25.FrameType.UI), # Create a UI, or unproto, frame
pid=_PID_TEXT,
data=MESSAGE.encode('utf-8'))
Next we need to connect to the KISS server. This is almost the same as what we saw for receiving packets, above.
connection = kiss.Connection(None)
error = None
try:
connection.connect_to_server(host, int(port))
except socket.gaierror as e:
if e.errno == socket.EAI_NONAME:
error = 'Server name not found'
else:
error = 'Invalid server address'
except ConnectionRefusedError:
error = 'Connection refused by server'
except Exception:
error = 'Unknown error connecting to server'
if error:
print(error)
sys.exit(1)
Notice that, this time, we passed None to the Connection constructor.
This is because we’re not interested here in receiving any packets; we’re only
going to send a message and then exit our application. In this situation, we
don’t need to create a receive callback as we did before.
All that’s left is to pack up the frame we created, send it via the connection, and then close that connection.
connection.send_data(frame.pack())
connection.disconnect_from_server()