Skip to content

Commit afbe99f

Browse files
committed
Account for missing binary bits
This patch updates the telemetry parsing to account for missing binary bits in the packet.
1 parent 3dcceef commit afbe99f

File tree

3 files changed

+108
-38
lines changed

3 files changed

+108
-38
lines changed

aprslib/parsing/telemetry.py

Lines changed: 80 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,20 @@ def parse_telemetry_config(body):
8888
elif form == "BITS":
8989
# APRS spec says 23 chars, but real-world packets may be longer
9090
# Accept any reasonable length (up to 100 chars to be safe)
91+
# Some packets may be missing binary bits (just title)
9192
match = re.findall(r"^([01]{8}),(.{0,100})$", body.rstrip())
92-
if not match:
93-
raise ParseError("incorrect format of %s" % form)
94-
95-
bits, title = match[0]
96-
97-
parsed.update({
98-
't%s' % form: bits,
99-
'title': title.strip(' ')
93+
if match:
94+
bits, title = match[0]
95+
parsed.update({
96+
't%s' % form: bits,
97+
'title': title.strip(' ')
98+
})
99+
else:
100+
# No binary bits found - treat entire body as title
101+
# This handles malformed packets like "BITS.title" without bits
102+
parsed.update({
103+
't%s' % form: '00000000', # Default to all zeros
104+
'title': body.rstrip().strip(' ')
100105
})
101106

102107
return (body, parsed)
@@ -137,10 +142,46 @@ def parse_telemetry_report(body):
137142
# Pad to 5 analog values if we have fewer
138143
while len(analog_strs) < 5:
139144
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 ''
145+
146+
# Digital I/O field (may be missing or in next position)
147+
digital_field = parts[6] if len(parts) > 6 else ''
148+
comment = ''
149+
150+
# If digital field is empty, check if next field could be digital I/O
151+
if not digital_field or digital_field.strip() == '':
152+
if len(parts) > 7:
153+
next_field = parts[7]
154+
# Check if next field starts with binary digits (could be digital I/O with concatenated comment)
155+
binary_match = re.match(r'^([01]+)', next_field)
156+
if binary_match:
157+
# Extract binary digits from start of next field
158+
binary_str = binary_match.group(1)
159+
# Use it as digital I/O if it's reasonable length (1-8 digits)
160+
if len(binary_str) <= 8:
161+
digital_field = binary_str
162+
# Rest of next_field and remaining parts are comment
163+
remaining = next_field[len(binary_str):].lstrip(',')
164+
remaining_parts = [remaining] if remaining else []
165+
if len(parts) > 8:
166+
remaining_parts.extend(parts[8:])
167+
comment = ','.join(remaining_parts) if remaining_parts else ''
168+
else:
169+
# Too long, use first 8 as digital I/O
170+
digital_field = binary_str[:8]
171+
remaining = binary_str[8:] + next_field[len(binary_str):]
172+
remaining_parts = [remaining] if remaining else []
173+
if len(parts) > 8:
174+
remaining_parts.extend(parts[8:])
175+
comment = ','.join(remaining_parts) if remaining_parts else ''
176+
else:
177+
# Next field is not binary, treat as comment
178+
digital_field = '00000000'
179+
comment = ','.join(parts[7:]) if len(parts) > 7 else ''
180+
else:
181+
# No more fields, use default
182+
digital_field = '00000000'
183+
else:
184+
comment = ','.join(parts[7:]) if len(parts) > 7 else ''
144185

145186
# Validate and parse sequence number (allow any positive integer)
146187
# APRS spec says 000-999, but real-world packets use larger numbers
@@ -156,7 +197,7 @@ def parse_telemetry_report(body):
156197
if not val_str or val_str.strip() == '':
157198
analog_vals.append(0.0)
158199
continue
159-
200+
160201
# Allow integers, decimals, and negative numbers
161202
if not re.match(r'^-?\d+\.?\d*$', val_str):
162203
raise ParseError("telemetry analog value %d has invalid format" % (i+1))
@@ -169,28 +210,36 @@ def parse_telemetry_report(body):
169210
# Validate digital I/O (must be binary digits, pad to 8 if shorter)
170211
# Some packets have comment concatenated without comma separator
171212
# 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:
213+
# Some packets have non-binary characters mixed in (extract leading binary digits)
214+
# Check if field starts with binary digits
215+
binary_match = re.match(r'^([01]+)', digital_field)
216+
if binary_match:
217+
binary_str = binary_match.group(1)
218+
219+
# If there are non-binary characters after binary digits, require at least 4 binary digits
220+
# This prevents false positives like "123" (1 binary + 2 non-binary)
221+
if len(binary_str) < len(digital_field):
222+
# Has non-binary characters following
223+
if len(binary_str) < 4:
224+
# Too few binary digits before non-binary - likely invalid
225+
raise ParseError("telemetry digital I/O must be binary digits")
226+
227+
# Extract leading binary digits
228+
if len(binary_str) < 8:
176229
# 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
230+
digital_str = binary_str.zfill(8)
231+
elif len(binary_str) == 8:
232+
digital_str = binary_str
180233
else:
181234
# 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")
235+
digital_str = binary_str[:8]
236+
237+
# If there's non-binary content after the binary digits, treat as comment if no comment yet
238+
if len(binary_str) < len(digital_field) and not comment:
239+
remaining = digital_field[len(binary_str):]
240+
comment = remaining
192241
else:
193-
# No valid binary digits found or invalid format
242+
# No valid binary digits found - this is invalid
194243
raise ParseError("telemetry digital I/O must be binary digits")
195244

196245
parsed.update({

tests/test_parse_telemetry.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,18 @@ def test_valid_telemetry_minimal_packet(self):
126126
# Digital I/O should default to all zeros
127127
self.assertEqual(result['telemetry']['bits'], '00000000')
128128

129+
def test_valid_telemetry_missing_digital_with_binary_in_next(self):
130+
"""Test telemetry packet with missing digital I/O field, binary digits in next position"""
131+
packet = "E25HML-13>APRS,TCPIP*,qAC,T2PERTH:T#075,039,,,000,000,,0000,ESP8266 Test WX DHT22 version"
132+
result = parse(packet)
133+
134+
self.assertEqual(result['format'], 'telemetry')
135+
self.assertEqual(result['telemetry']['seq'], 75)
136+
self.assertEqual(result['telemetry']['vals'], [39.0, 0.0, 0.0, 0.0, 0.0])
137+
# 0000 should be padded to 8 digits
138+
self.assertEqual(result['telemetry']['bits'], '00000000')
139+
self.assertEqual(result['comment'], 'ESP8266 Test WX DHT22 version')
140+
129141
def test_valid_telemetry_empty_analog_value(self):
130142
"""Test telemetry packet with empty analog value"""
131143
packet = "VU2IB-13>APRS,TCPIP*,qAC,T2DENMARK:T#126,250,057,000,040,,1011,Solar Power WX Station"
@@ -226,7 +238,7 @@ def test_invalid_telemetry_invalid_analog_value(self):
226238
self.assertIn("invalid format", str(context.exception))
227239

228240
def test_invalid_telemetry_invalid_digital_bits(self):
229-
"""Test that invalid digital I/O format raises error"""
241+
"""Test that digital I/O with no binary digits raises error"""
230242
packet = "TEST>APRS:T#123,456,789,012,345,678,123"
231243

232244
with self.assertRaises(ParseError) as context:
@@ -242,14 +254,14 @@ def test_valid_telemetry_digital_bits_short_length(self):
242254
# Short binary strings are padded with leading zeros
243255
self.assertEqual(result['telemetry']['bits'], '01100101')
244256

245-
def test_invalid_telemetry_digital_bits_non_binary(self):
246-
"""Test that digital I/O with non-binary characters raises error"""
257+
def test_valid_telemetry_digital_bits_with_non_binary_suffix(self):
258+
"""Test that digital I/O with leading binary digits and non-binary suffix is handled (real-world packets)"""
247259
packet = "TEST>APRS:T#123,456,789,012,345,678,11001012"
260+
result = parse(packet)
248261

249-
with self.assertRaises(ParseError) as context:
250-
parse(packet)
251-
252-
self.assertIn("telemetry digital I/O must be binary digits", str(context.exception))
262+
# Should extract leading binary digits (7 digits), pad to 8, and treat '2' as comment
263+
self.assertEqual(result['telemetry']['bits'], '01100101')
264+
self.assertEqual(result.get('comment', ''), '2')
253265

254266
def test_parse_telemetry_report_function_direct(self):
255267
"""Test parse_telemetry_report function directly"""

tests/test_parse_telemetry_config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,15 @@ def test_parse_telemetry_config_eqns(self):
108108
self.assertIsInstance(result['tEQNS'], list)
109109
self.assertEqual(len(result['tEQNS']), 5)
110110

111+
def test_bits_without_binary_bits(self):
112+
"""Test BITS telemetry config without binary bits (malformed packet)"""
113+
packet = "E25HML-13>APRS,TCPIP*,qAC,T2PERTH::E25HML-13:BITS.ESP8266 Test WX DHT22 version"
114+
result = parse(packet)
115+
116+
self.assertEqual(result['format'], 'telemetry-message')
117+
self.assertEqual(result['tBITS'], '00000000') # Default to all zeros
118+
self.assertEqual(result['title'], 'ESP8266 Test WX DHT22 version')
119+
111120

112121
if __name__ == '__main__':
113122
unittest.main()

0 commit comments

Comments
 (0)