Skip to content

Commit 7776146

Browse files
committed
Added Telemetry parsing
This patch adds telemetry packet parsing and unit tests.
1 parent c2a0f18 commit 7776146

4 files changed

Lines changed: 535 additions & 3 deletions

File tree

aprslib/parsing/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ def detect(x):
5757
'.':'reserved',
5858
'<':'station capabilities',
5959
'?':'general query format',
60-
'T':'telemetry report',
6160
'[':'maidenhead locator beacon',
6261
'\\':'unused',
6362
']':'unused',
@@ -202,6 +201,12 @@ def _try_toparse_body(packet_type, body, parsed):
202201

203202
body, result = parse_weather(body)
204203

204+
# Telemetry report
205+
elif packet_type == 'T':
206+
logger.debug("Attempting to parse as telemetry report")
207+
208+
body, result = parse_telemetry_report(body)
209+
205210
# postion report (regular or compressed)
206211
elif (packet_type in '!=/@;' or
207212
0 <= body.find('!') < 40): # page 28 of spec (PDF)

aprslib/parsing/telemetry.py

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
__all__ = [
77
'parse_comment_telemetry',
88
'parse_telemetry_config',
9+
'parse_telemetry_report',
910
]
1011

1112

@@ -85,9 +86,11 @@ def parse_telemetry_config(body):
8586
't%s' % form: teqns
8687
})
8788
elif form == "BITS":
88-
match = re.findall(r"^([01]{8}),(.{0,23})$", body.rstrip())
89+
# APRS spec says 23 chars, but real-world packets may be longer
90+
# Accept any reasonable length (up to 100 chars to be safe)
91+
match = re.findall(r"^([01]{8}),(.{0,100})$", body.rstrip())
8992
if not match:
90-
raise ParseError("incorrect format of %s (title too long?)" % form)
93+
raise ParseError("incorrect format of %s" % form)
9194

9295
bits, title = match[0]
9396

@@ -98,3 +101,111 @@ def parse_telemetry_config(body):
98101

99102
return (body, parsed)
100103

104+
105+
def parse_telemetry_report(body):
106+
"""
107+
Parses APRS 1.2 telemetry report format: T#sss,aaa,bbb,ccc,ddd,eee,bbbbbbbb,comment
108+
109+
Format:
110+
- T# indicates telemetry report
111+
- sss is sequence number (000-999)
112+
- aaa to eee are 5 analog values (000-999)
113+
- bbbbbbbb is 8 binary digits (digital I/O)
114+
- comment is optional text
115+
116+
Returns (remaining_body, parsed_dict)
117+
"""
118+
parsed = {}
119+
120+
# Check if body starts with '#'
121+
if not body.startswith('#'):
122+
raise ParseError("telemetry report must start with '#'")
123+
124+
# Remove the '#' prefix
125+
body = body[1:]
126+
127+
# Split by comma - need at least sequence number
128+
# Some real-world packets are incomplete (missing analog values or digital I/O)
129+
parts = body.split(',', 7)
130+
131+
if len(parts) < 1:
132+
raise ParseError("telemetry report must have at least a sequence number")
133+
134+
seq_str = parts[0]
135+
# Extract analog values (up to 5, pad with empty strings if missing)
136+
analog_strs = parts[1:6] if len(parts) > 1 else []
137+
# Pad to 5 analog values if we have fewer
138+
while len(analog_strs) < 5:
139+
analog_strs.append('')
140+
141+
# Digital I/O field (may be missing)
142+
digital_field = parts[6] if len(parts) > 6 else '00000000'
143+
comment = parts[7] if len(parts) > 7 else ''
144+
145+
# Validate and parse sequence number (allow any positive integer)
146+
# APRS spec says 000-999, but real-world packets use larger numbers
147+
if not re.match(r'^\d+$', seq_str):
148+
raise ParseError("telemetry sequence number must be numeric")
149+
seq = int(seq_str)
150+
151+
# Parse analog values (can be 000-999, allow decimals and negatives per APRS 1.2)
152+
# Empty values are allowed and treated as 0
153+
analog_vals = []
154+
for i, val_str in enumerate(analog_strs):
155+
# Allow empty values (treated as 0)
156+
if not val_str or val_str.strip() == '':
157+
analog_vals.append(0.0)
158+
continue
159+
160+
# Allow integers, decimals, and negative numbers
161+
if not re.match(r'^-?\d+\.?\d*$', val_str):
162+
raise ParseError("telemetry analog value %d has invalid format" % (i+1))
163+
try:
164+
val = float(val_str)
165+
except ValueError:
166+
raise ParseError("telemetry analog value %d is not a valid number" % (i+1))
167+
analog_vals.append(val)
168+
169+
# Validate digital I/O (must be binary digits, pad to 8 if shorter)
170+
# Some packets have comment concatenated without comma separator
171+
# Some packets have shorter binary strings (pad with leading zeros)
172+
# Check if field is entirely binary digits
173+
if re.match(r'^[01]+$', digital_field):
174+
# Pure binary string (all 0s and 1s)
175+
if len(digital_field) < 8:
176+
# Pad shorter binary strings to 8 digits
177+
digital_str = digital_field.zfill(8)
178+
elif len(digital_field) == 8:
179+
digital_str = digital_field
180+
else:
181+
# Longer than 8, use first 8
182+
digital_str = digital_field[:8]
183+
elif re.match(r'^[01]{8,}[^01]', digital_field):
184+
# Starts with 8+ binary digits followed by non-binary (concatenated comment)
185+
digital_str = digital_field[:8]
186+
if not comment:
187+
comment = digital_field[8:]
188+
elif re.match(r'^[01]{1,7}[^01]', digital_field):
189+
# Starts with 1-7 binary digits followed by non-binary
190+
# This is invalid - need at least 8 binary digits before comment
191+
raise ParseError("telemetry digital I/O must be binary digits")
192+
else:
193+
# No valid binary digits found or invalid format
194+
raise ParseError("telemetry digital I/O must be binary digits")
195+
196+
parsed.update({
197+
'format': 'telemetry',
198+
'telemetry': {
199+
'seq': seq,
200+
'vals': analog_vals,
201+
'bits': digital_str
202+
}
203+
})
204+
205+
# Add comment if present
206+
if comment:
207+
parsed['comment'] = comment.strip(' ')
208+
209+
# Return empty remaining body since we consumed everything
210+
return ('', parsed)
211+

0 commit comments

Comments
 (0)