Package: aioax25
aioax25: AX.25 and APRS library inasyncioThe aim of this project is to implement a simple-to-understand asynchronous
AX.25 library built onasyncioandpyserial, implementing a AX.25 and APRS
stack in pure Python.Python 3.5+ and above is required as of 2021-11-12I did try to support 3.4, but this proved to be infeasible for the following
reasons:Python 3.8+ makesasyncio.coroutinedeprecated (apparently will be removed
in 3.10).  This meant I neededcoroutineandasync defversions of some
API functions, and the necessary logic to "hide" the latter from Python 3.4.Trying to coax generator-based coroutines to run "in the background" for unit
test purposes proved to be a pain in the arse.Python 3.5 support is planned to continue until it too, becomes infeasible
(e.g. if type annotations become required).What worksWe can put a Kantronics KPC-3 TNC into KISS mode automaticallyMulti-port KISS TNCs (tested withDirewolfand theNWDR UDRC-II)We can receive AX.25 UI framesWe can send AX.25 UI framesWhat doesn't workConnecting to AX.25 nodesAccepting connections from AX.25 nodesWhat isn't testedPlatforms other than GNU/LinuxCurrent plansRight now, I intend to get enough going for APRS operation, as that is my
immediate need now.  Hence the focus on UI frames.I intend to write a core class that will take care of some core AX.25 message
handling work and provide the basis of what's needed to implement APRS.After that, some things I'd like to tackle in no particular order:Connected mode operationNET/ROM supportSupported platforms will be GNU/Linux, and possibly BSD variants.  I don't
have access to recent Apple hardware (my 2008-era MacBook will not run
contemporary MacOS X) so I'm unable to test this software there, but itshouldwork nonetheless.It might work on Windows -- most probably using Cygwin or Subsystem for Linux.
While I do have a Windows 7 machine handy, life's too short to muck around
with an OS that can't decide if it's pretending to be Linux, VMS or CP/M.
There's an abundance of AX.25 stacks and tools for that platform, I'll accept
patches here on the proviso they don't break things or make the code
unmaintainable.UsageThis is a rough guide regarding how to useaioax25in your programs.Create a KISS device interface and portsRight now we only support serial KISS interfaces (patches for TCP-based
interfaces are welcome).  Importmake_devicefromaioax25.kiss, then
create an instance as shown:kissdev=make_device(type='serial',device='/dev/ttyS4',baudrate=9600,log=logging.getLogger('your.kiss.log'))Or for a TCP-connected KISS interface:kissdev=make_device(type='tcp',host='kissdevice.example.com',port=12345,log=logging.getLogger('your.kiss.log'))(Note: ifkissdevice.example.comis going over the Internet, I suggest either
routing via a VPN or supplying assl.SSLContextvia thesslparameter so
that your client is authenticated with the server.)Or for a subprocess:kissdev = make_device(
        type='subproc', command=['/path/to/your/command', 'arg1', 'arg2'],
        log=logging.getLogger('your.kiss.log')
    )Some optional parameters:reset_on_close: When asked to close the device, try to issue ac0 ff c0reset sequence to the TNC to put it back into CMD mode.send_block_size,send_block_delay: If a KISS frame is larger than
this size, break the transmissions out the serial port into chunks of
the given size, and waitsend_block_delayseconds between each chunk.
(If your TNC has a small buffer, this may help.)This represents the KISS TNC itself, with its ports accessible using the usual__getitem__syntax:kissport0=kissdev[0]kissport1=kissdev[1]These KISS port interfaces just spit out the content of raw AX.25 frames via
theirreceivedsignals and accept raw AX.25 frames via thesendmethod.
Any object passed tosendis wrapped in abytescall -- this will
implicitly call the__bytes__method on the object you pass in.Setting up an AX.25 InterfaceThe AX.25 interface is a logical routing and queueing layer which decodes the
data received from a KISS port and routes it according to the destination
call-sign.AX25Interfaceis found in theaioax25.interfacepackage.  Import that, then
do the following to set up your interface:ax25int=AX25Interface(kissport=kissdev[0],# or whatever port number you needlog=logging.getLogger('your.ax25.log'))Some optional parameters:cts_delay,cts_rand: The number of seconds to wait after making a
transmission/receiving a transmission, before we send another transmission.
The delay time iscts_delay + (random.random() * cts_rand), the idea
being to avoid doubling when two stations attempt transmission.TheAX25Interfaceis a subclass ofRouter(seeaioax25.router), which
exposes the following methods and properties:received_msg: This is aSignalobject which is fired for every AX.25
frame received.  Slots are expected to take two keyword arguments:interface(the interface that received the frame) andframe(the
AX.25 frame itself).bind(callback, callsign, ssid=0, regex=False): This method allows you to
bind a call-back function to receive AX.25 frames whosedestinationfield
is addressed to the call-sign and SSID specified.  The call-sign may be a
regular expression ifregex=True.  This will be compiled and matched
against all incoming traffic.  Regardless of the value ofregex, thecallsignparametermustbe a string.unbind(callback, callsign, ssid=0, regex=False): This method un-binds a
previously bound call-back method from receiving the nominated traffic.Additionally, for transmitting frames,AX25Interfaceadds the following:transmit(frame, callback=None): This method allows you to transmit
arbitrary AX.25 frames.  They are assumed to be instances ofAX25Frame(fromaioax25.frame).  Thecallback, if given, will be called once the
frame is sent with the following keyword arguments:interface(theAX25Interfacethat sent the frame),frame(the frame that was sent).cancel_transmit(frame): This cancels a pending transmission of a frame.
If the frame has been sent, this has no effect.APRS Traffic handlingTheAX25Interfacejust deals in AX.25 traffic, and does not provide any
special handling of APRS UI frames.  For this, one may look atAPRSInterface.Import this fromaioax25.aprs.  It too, is a subclass ofRouter, and sobind,unbindandreceived_msgare there -- the messages received will
be instances ofAPRSFrame(seeaioax25.aprs.frame), otherwise the behaviour
is identical.aprsint=APRSInterface(ax25int=ax25int,# Your AX25Interface objectmycall='VK4MSL-9',# Your call-sign and SSIDlog=logging.getLogger('your.aprs.log'))Other optional parameters:retransmit_count,retransmit_timeout_base,retransmit_timeout_rand,retransmit_timeout_scale: These control the timing of retransmissions
when sendingconfirmableAPRS messages.  Before transmission, a time-out
is computed astimeout = retransmit_timeout_base + (random.random() * retransmit_timeout_rand), and a retry counter is initialised toretransmit_count.  On each re-transmission, the retry counter is
decremented and the timeout is multiplied byretransmit_timeout_scale.aprs_destination: This sets the destination call-sign used for APRS
traffic.  Right now, we use the experimental call ofAPZAIOfor all
traffic except direct messages (which instead are sent directly to the
station addressed).aprs_pathspecifies the digipeater path to use when sending APRS traffic.listen_destinationsis a list of AX.25 destinations.  Behind the scenes,
these are values passed toRouter.bind, and thus are given asdicts of
the form:{callsign: "CALL", regex: True/False, ssid: None/int}.Setting
this may break reception of MICe packets!listen_altnetsis an additional list of AX.25 destinations, given using
the same scheme aslisten_destinations.Setting this may break
reception of MICe packets!msgid_modulosets the modulo value used when generating a message ID.
The default value (1000) results in a message ID that starts at 1 and wraps
around at 999.deduplication_expirysets the number of seconds we store message hashes
for de-duplication purposes.  The default is 28 seconds.To send APRS messages, there issend_messageandsend_response:send_message(addressee, path=None, oneshot=False, replyack=False):
This sends an APRS message to the addressed station.  IfpathisNone,
then theaprs_pathis used.  Ifoneshot=True, then the message is sent
without a message ID, no ACK/REJ is expected and no retransmissions will be
made, the method returnsNone.  Otherwise, aAPRSMessageHandler(fromaioax25.aprs.message) is returned.Ifreplyackis set toTrue, then the message will advertisereply-ackcapability to
the recipient.  Not all APRS implementations support this.Ifreplyackreferences an incoming message which itself hasreplyackset (either toTrueor to a previous message ID), then the outgoing
message will have a reply-ack suffix appended to "ack" the given message.The default ofreplyack=Falsedisables all reply-ack capability (an
incoming reply-ack message will still be treated as an ACK however).send_response(message, ack=True): This is used when you have received
a message from another station -- passing that message to this function
will send aACKorREJmessage to that station.TheAPRSMessageHandlerclassTheAPRSMessageHandlerclass implements the APRS message retransmission
logic.  The objects have adonesignal which is emitted upon any of the
following events:Message time-out (no ACK/REJ received) (state=HandlerState.TIMEOUT)Message was cancelled (via thecancel()method)
(state=HandlerState.CANCEL)An ACK or REJ frame was received (state=HandlerState.SUCCESSorstate=HandlerState.REJECT)The signal will call call-back functions with the following keyword arguments:handler: TheAPRSMessageHandlerobject emitting the signalstate: The state of theAPRSMessageHandlerobject.TAPR TNC2 packet formatSometimes, you need the incoming packet in TAPR TNC2 format, notably for
APRS-IS interaction.  This is somewhat experimental inaioax25as no one
seems to have a definition of what "TNC2 format" is.AllAX25Frameinstances implementtnc2property, which returns the frame in
a hopefully TNC2-compatible format.  For UI frames, which may be encoded in a
number of different formats, there is also aget_tnc2method, which accepts
arguments that are passed tobytes.decode(); the default is to decode the
payload as ISO-8859-1 since this preserves the byte values losslessly.APRS Digipeatingaioax25includes a module that implements basic digipeating for APRS
including handling of theWIDEn-NSSIDs.  The implementation treatsWIDElikeTRACE: inserting the station's own call-sign in the path (which I
believe is more compliant with theAmateur License Conditions
Determinationin that it
ensures each digipeater "identifies" itself).Theaioax25.aprs.uidigimodule can be configured to digipeat for other
aliases such as the legacyWIDEandRELAY, or any alias of your choosing.It is capable of handling multiple interfaces, but will repeat incoming
messages on the interface they were received fromONLY.  (i.e. if you connect
a 2m interface and a HF interface, it willNOTdigipeat from HF to 2m).Set-up is pretty simple:from aioax25.aprs.uidigi import APRSDigipeater

# Given an APRSInterface class (aprsint)
# Create a digipeater instance
digipeater = APRSDigipeater()

# Connect your interface
digipeater.connect(aprsint)

# Optionally add any aliases you want handled
digipeater.addaliases('WIDE', 'GATE')You're now digipeating.  The digipeater will automatically handleWIDEn-NandTRACEn-N, and in the above example, will also digipeat forWIDE,GATE.Preventing message loops on busy networksIf you have alotof digipeaters in close proximity (say about 6) and there's
a lot of traffic, you can get the situation where a message queued up to be
digipeated sits in the transmit queue longer than the 28 seconds needed for
other digipeaters to "forget" the message.This leads to a network with the memory of an elephant, it almost never forgets
a message because the digipeats come more than 30 secondsafterthe original.TheAPRSDigipeaterclass constructor can take a single parameter,digipeater_timeout, which sets an expiry (default of 5 seconds) on queued
digipeat messages.  If a message is not sent by the time this timeout expires,
the message is quietly dropped, preventing the memory effect.