Skip to content

Commit e6d5cc9

Browse files
feat: support ackrej
1 parent 46edb12 commit e6d5cc9

File tree

2 files changed

+251
-3
lines changed

2 files changed

+251
-3
lines changed

aprs_backend/packets/ackrej.py

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# Parses packets to check for new ackrej format packets
2+
import re
3+
4+
import logging
5+
6+
log = logging.getLogger(__name__)
7+
8+
9+
def parse_ack_rej_msg_id(message: str) -> tuple[str | None, str | None, str | None, str | None]:
10+
"""Parses the "new" (1999) ackrej message format. APRSlib doesn't
11+
handle it very well
12+
13+
Args:
14+
message (str): APRS message text to be parsed. If this text contains
15+
the new ack/rej message as _sole_ data, then the response_text field
16+
will be populated with the ack/rej, the message numbers will be assigned
17+
and the message_text itself will be None. Otherwise, the original
18+
message text will be returned
19+
20+
Returns:
21+
tuple[str, str, str, str]:
22+
message: str | None - original message text or None if message was an ack/rej
23+
response: str | None - ack, rej, or None
24+
sender_message_id: str | None - Sender's message ID or None
25+
err_message_id: str | None - message id for a message from te bot or None
26+
"""
27+
matches = re.search(r"^(ack|rej)(..)}(..)$", message, re.IGNORECASE)
28+
response = None
29+
sender_message_id = None
30+
err_message_id = None
31+
if matches:
32+
response = matches[1]
33+
sender_message_id = matches[2]
34+
err_message_id = matches[3]
35+
message = None
36+
log.debug("Message: %s response: %s sender_message_id: %s, err_message_id: %s", message, response, sender_message_id, err_message_id)
37+
return message, response
38+
39+
40+
def parse_new_ackrej_format(message: str) -> tuple[str | None, str | None, bool]:
41+
"""
42+
Have a look at the incoming APRS message and check if it
43+
contains a message no which does not follow the APRS
44+
standard (see aprs101.pdf chapter 14)
45+
46+
http://www.aprs.org/aprs11/replyacks.txt
47+
48+
but rather follow the new format
49+
50+
http://www.aprs.org/aprs11/replyacks.txt
51+
52+
Parameters
53+
==========
54+
message_text: 'str'
55+
The original aprs message as originally extracted by aprslib
56+
57+
Returns
58+
=======
59+
msg: 'str'
60+
original message OR the modified message minus message no and trailing
61+
data
62+
msg_no: 'str'
63+
Null if no message_no was present
64+
new_ackrej_format: 'bool'
65+
True if the ackrej_format has to follow the new ack-rej handling
66+
process as described in http://www.aprs.org/aprs11/replyacks.txt
67+
"""
68+
69+
"""
70+
The following assumptions apply when handling APRS messages in general:
71+
72+
Option 1: no message ID present:
73+
send no ACK
74+
outgoing messages have no msg number attachment
75+
76+
Example data exchange 1:
77+
DF1JSL-4>APRS,TCPIP*,qAC,T2PRT::WXBOT :94043
78+
WXBOT>APRS,qAS,KI6WJP::DF1JSL-4 :Mountain View CA. Today,Sunny High 60
79+
80+
Example data exchange 2:
81+
DF1JSL-4>APRS,TCPIP*,qAC,T2SPAIN::EMAIL-2 :jsl24469@gmail.com Hallo
82+
EMAIL-2>APJIE4,TCPIP*,qAC,AE5PL-JF::DF1JSL-4 :Email sent to jsl24469@gmail.com
83+
84+
85+
Option 2: old message number format is present: (example: msg{12345)
86+
Send ack with message number from original message (ack12345)
87+
All outgoing messages have trailing msg number ( {abcde ); can be numeric or
88+
slphanumeric counter. See aprs101.pdf chapter 14
89+
90+
Example data exchange 1:
91+
DF1JSL-4>APRS,TCPIP*,qAC,T2SP::EMAIL-2 :jsl24469@gmail.com Hallo{12345
92+
EMAIL-2>APJIE4,TCPIP*,qAC,AE5PL-JF::DF1JSL-4 :ack12345
93+
EMAIL-2>APJIE4,TCPIP*,qAC,AE5PL-JF::DF1JSL-4 :Email sent to jsl24469@gmail.com{891
94+
DF1JSL-4>APOSB,TCPIP*,qAS,DF1JSL::EMAIL-2 :ack891
95+
96+
Example data exchange 2:
97+
DF1JSL-4>APRS,TCPIP*,qAC,T2CSNGRAD::EMAIL-2 :jsl24469@gmail.com{ABCDE
98+
EMAIL-2>APJIE4,TCPIP*,qAC,AE5PL-JF::DF1JSL-4 :ackABCDE
99+
EMAIL-2>APJIE4,TCPIP*,qAC,AE5PL-JF::DF1JSL-4 :Email sent to jsl24469@gmail.com{893
100+
DF1JSL-4>APOSB,TCPIP*,qAS,DF1JSL::EMAIL-2 :ack893
101+
102+
103+
Option 3: new messages with message ID but without trailing retry msg ids: msg{AB}
104+
Do NOT send extra ack
105+
All outgoing messages have 2-character msg id, followed by message ID from original message
106+
Example:
107+
User sends message "Hello{AB}" to MPAD
108+
MPAD responds "Message content line 1{DE}AB" to user
109+
MPAD responds "Message content line 2{DF}AB" to user
110+
111+
AB -> original message
112+
DE, DF -> message IDs generated by MPAD
113+
114+
Example data exchange 1:
115+
DF1JSL-4>APRS,TCPIP*,qAC,T2NUERNBG::WXBOT :99801{AB}
116+
WXBOT>APRS,qAS,KI6WJP::DF1JSL-4 :Lemon Creek AK. Today,Scattered Rain/Snow and Patchy Fog 50% High 4{QL}AB
117+
DF1JSL-4>APOSB,TCPIP*,qAS,DF1JSL::WXBOT :ackQL}AB
118+
WXBOT>APRS,qAS,KI6WJP::DF1JSL-4 :0{QM}AB
119+
DF1JSL-4>APOSB,TCPIP*,qAS,DF1JSL::WXBOT :ackQM}AB
120+
121+
Example data exchange 2:
122+
DF1JSL-4>APRS,TCPIP*,qAC,T2SPAIN::EMAIL-2 :jsl24469@gmail.com Hallo{AB}
123+
EMAIL-2>APJIE4,TCPIP*,qAC,AE5PL-JF::DF1JSL-4 :Email sent to jsl24469@gmail.com{OQ}AB
124+
DF1JSL-4>APOSB,TCPIP*,qAS,DF1JSL::EMAIL-2 :ackOQ}AB
125+
126+
127+
Option 4: new messages with message ID and with trailing retry msg ids: msg{AB}CD
128+
We don't handle retries - therefore, apply option #3 for processing these
129+
the "CD" part gets omitted and is not used
130+
131+
Example data exchange 1:
132+
DF1JSL-4>APRS,TCPIP*,qAC,T2CZECH::WXBOT :99801{LM}AA
133+
WXBOT>APRS,qAS,KI6WJP::DF1JSL-4 :Lemon Creek AK. Today,Scattered Rain/Snow and Patchy Fog 50% High 4{QP}LM
134+
DF1JSL-4>APOSB,TCPIP*,qAS,DF1JSL::WXBOT :ackQP}LM
135+
WXBOT>APRS,qAS,KI6WJP::DF1JSL-4 :0{QQ}LM
136+
DF1JSL-4>APOSB,TCPIP*,qAS,DF1JSL::WXBOT :ackQQ}LM
137+
138+
Example data exchange 2:
139+
DF1JSL-4>APRS,TCPIP*,qAC,T2SP::EMAIL-2 :jsl24469@gmail.com Welt{DE}FG
140+
EMAIL-2>APJIE4,TCPIP*,qAC,AE5PL-JF::DF1JSL-4 :Email sent to jsl24469@gmail.com{OS}DE
141+
DF1JSL-4>APOSB,TCPIP*,qAS,DF1JSL::EMAIL-2 :ackOS}DE
142+
143+
"""
144+
145+
msg = msgno = None
146+
new_ackrej_format = False
147+
148+
# if message text is present, split up between aaaaaa{bb}cc
149+
# where aaaaaa = message text
150+
# bb = message number
151+
# cc = message retry (may or may not be present)
152+
if message:
153+
matches = re.search(
154+
r"^(.*){([a-zA-Z0-9]{2})}(\w*)$", message, re.IGNORECASE
155+
)
156+
if matches:
157+
try:
158+
msg = matches[1].rstrip()
159+
msgno = matches[2]
160+
new_ackrej_format = True
161+
except IndexError:
162+
msg = message
163+
msgno = None
164+
new_ackrej_format = False
165+
else:
166+
msg = message
167+
else:
168+
msg = message
169+
return msg, msgno, new_ackrej_format

aprs_backend/threads/rx.py

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from aprs_backend.packets.list import ErrbotPacketList
88
from aprs_backend.threads import ErrbotAPRSDThread
99
from aprsd import packets
10+
from aprs_backend.packets.ackrej import parse_ack_rej_msg_id, parse_new_ackrej_format
1011
import logging
1112

1213

@@ -69,9 +70,87 @@ def process_packet(self, *args, **kwargs):
6970
"""
7071
packet = self._client.decode_packet(*args, **kwargs)
7172
packet.log(header="RX")
72-
print(packet)
73-
if isinstance(packet, packets.AckPacket):
74-
# We don't need to drop AckPackets, those should be
73+
log.debug(packet)
74+
75+
# Check if this packet is in the new ack-rej format
76+
# arpslib cannot handle these messages properly so we have to apply a workaround
77+
# as we don't resubmit data in case it hasn't been received
78+
response = None
79+
if isinstance(packet, packets.MessagePacket):
80+
if packet.message_text is not None and packet.message_text != "":
81+
packet.message_text, response = parse_ack_rej_msg_id(packet.message_text)
82+
83+
84+
# This is a special handler for the new(er) APRS ack/rej/format
85+
# By default (and described in aprs101.pdf pg. 71), APRS supports two
86+
# messages:
87+
# - messages withOUT message ID, e.g. Hello World
88+
# - message WITH 5-character message ID, e.g. Hello World{12345
89+
# The latter require the program to send a seperate ACK to the original
90+
# recipient
91+
#
92+
# Introduced through an addendum (http://www.aprs.org/aprs11/replyacks.txt),
93+
# a third option came into place. This message also has a message ID but
94+
# instead of sending a separate ACK, the original message ID is returned to
95+
# the user for all messages that relate to the original one. aprslib does
96+
# currently not recognise these new message IDs - therefore, we need to
97+
# extract them from the message text and switch the program logic if we
98+
# discover that new ID.
99+
is_new_ackrej = False
100+
if packet.msgNo is None and packet.message_text is not None:
101+
packet.message_text, packet.msgNo, is_new_ackrej = parse_new_ackrej_format(packet.message_text)
102+
103+
log.debug("Message %s is_new_ackrej %s", packet, is_new_ackrej)
104+
105+
if response == "ack":
106+
log.debug("Packet is new style ack, transforming")
107+
new_packet = packets.AckPacket(
108+
from_call = packet.from_call,
109+
to_call = packet.to_call,
110+
addresse = packet.addresse,
111+
format = packet.format,
112+
msgNo = packet.msgNo,
113+
packet_type = packet.packet_type,
114+
timestamp = packet.timestamp,
115+
raw = packet.raw,
116+
raw_dict = packet.raw_dict,
117+
payload = packet.payload,
118+
send_count = packet.send_count,
119+
retry_count = packet.retry_count,
120+
last_send_time = packet.last_send_time,
121+
last_send_attempt = packet.last_send_attempt,
122+
allow_delay = packet.allow_delay,
123+
path = packet.path,
124+
via = packet.va,
125+
response = response
126+
)
127+
packet = new_packet
128+
if response == "rej":
129+
log.debug("Packet is new style rejection, transforming")
130+
new_packet = packets.RejectPacket(
131+
from_call = packet.from_call,
132+
to_call = packet.to_call,
133+
addresse = packet.addresse,
134+
format = packet.format,
135+
msgNo = packet.msgNo,
136+
packet_type = packet.packet_type,
137+
timestamp = packet.timestamp,
138+
raw = packet.raw,
139+
raw_dict = packet.raw_dict,
140+
payload = packet.payload,
141+
send_count = packet.send_count,
142+
retry_count = packet.retry_count,
143+
last_send_time = packet.last_send_time,
144+
last_send_attempt = packet.last_send_attempt,
145+
allow_delay = packet.allow_delay,
146+
path = packet.path,
147+
via = packet.va,
148+
response = response
149+
)
150+
packet = new_packet
151+
152+
if isinstance(packet, packets.AckPacket) or isinstance(packet, packets.RejectPacket):
153+
# We don't need to drop AckPackets of Rejects, those should be
75154
# processed.
76155
self.packet_queue.put(packet)
77156
else:

0 commit comments

Comments
 (0)