Merge pull request #15 from Meisterschueler/feature/+protocol_0.2.5

Implemented new receiver protocol for 0.2.5
pull/16/head
Fabian P. Schmidt 2016-10-17 00:34:16 +02:00 zatwierdzone przez GitHub
commit e3de256060
8 zmienionych plików z 102 dodań i 35 usunięć

Wyświetl plik

@ -1,6 +1,7 @@
# CHANGELOG
## Unreleased
- updated the parser for the 0.2.5 protocol
## 0.5.0 - 2016-09-29
- Added aprs destination callsign as `dstcall` to aprs beacon keys (#9)

Wyświetl plik

@ -2,7 +2,7 @@ import re
from datetime import datetime
from ogn.parser.utils import createTimestamp, parseAngle, kts2kmh, feet2m, fpm2ms
from ogn.parser.pattern import PATTERN_APRS, PATTERN_RECEIVER_BEACON, PATTERN_AIRCRAFT_BEACON
from ogn.parser.pattern import PATTERN_APRS_POSITION, PATTERN_APRS_STATUS, PATTERN_RECEIVER_BEACON, PATTERN_AIRCRAFT_BEACON
from ogn.parser.exceptions import AprsParseError, OgnParseError
@ -12,22 +12,30 @@ def parse_aprs(message, reference_date=None, reference_time=None):
reference_date = now.date()
reference_time = now.time()
match = re.search(PATTERN_APRS, message)
if match:
return {'name': match.group('callsign'),
'receiver_name': match.group('receiver'),
'dstcall': match.group('dstcall'),
'timestamp': createTimestamp(match.group('time'), reference_date, reference_time),
'latitude': parseAngle('0' + match.group('latitude') + (match.group('latitude_enhancement') or '0')) *
(-1 if match.group('latitude_sign') == 'S' else 1),
'symboltable': match.group('symbol_table'),
'longitude': parseAngle(match.group('longitude') + (match.group('longitude_enhancement') or '0')) *
(-1 if match.group('longitude_sign') == 'W' else 1),
'symbolcode': match.group('symbol'),
'track': int(match.group('course')) if match.group('course_extension') else 0,
'ground_speed': int(match.group('ground_speed')) * kts2kmh if match.group('ground_speed') else 0,
'altitude': int(match.group('altitude')) * feet2m,
'comment': match.group('comment')}
match_position = re.search(PATTERN_APRS_POSITION, message)
if match_position:
return {'name': match_position.group('callsign'),
'receiver_name': match_position.group('receiver'),
'dstcall': match_position.group('dstcall'),
'timestamp': createTimestamp(match_position.group('time'), reference_date, reference_time),
'latitude': parseAngle('0' + match_position.group('latitude') + (match_position.group('latitude_enhancement') or '0')) *
(-1 if match_position.group('latitude_sign') == 'S' else 1),
'symboltable': match_position.group('symbol_table'),
'longitude': parseAngle(match_position.group('longitude') + (match_position.group('longitude_enhancement') or '0')) *
(-1 if match_position.group('longitude_sign') == 'W' else 1),
'symbolcode': match_position.group('symbol'),
'track': int(match_position.group('course')) if match_position.group('course_extension') else 0,
'ground_speed': int(match_position.group('ground_speed')) * kts2kmh if match_position.group('ground_speed') else 0,
'altitude': int(match_position.group('altitude')) * feet2m,
'comment': match_position.group('comment')}
match_status = re.search(PATTERN_APRS_STATUS, message)
if match_status:
return {'name': match_status.group('callsign'),
'receiver_name': match_status.group('receiver'),
'dstcall': match_status.group('dstcall'),
'timestamp': createTimestamp(match_status.group('time'), reference_date, reference_time),
'comment': match_status.group('comment')}
raise AprsParseError(message)
@ -42,13 +50,14 @@ def parse_ogn_aircraft_beacon(aprs_comment):
'climb_rate': int(ac_match.group('climb_rate')) * fpm2ms,
'turn_rate': float(ac_match.group('turn_rate')),
'flightlevel': float(ac_match.group('flight_level')) if ac_match.group('flight_level') else None,
'signal_strength': float(ac_match.group('signal')),
'signal_quality': float(ac_match.group('signal_quality')),
'error_count': float(ac_match.group('errors')),
'frequency_offset': float(ac_match.group('frequency_offset')),
'gps_status': ac_match.group('gps_accuracy'),
'software_version': float(ac_match.group('flarm_software_version')) if ac_match.group('flarm_software_version') else None,
'hardware_version': int(ac_match.group('flarm_hardware_version'), 16) if ac_match.group('flarm_hardware_version') else None,
'real_address': ac_match.group('flarm_id')}
'real_address': ac_match.group('flarm_id'),
'signal_power': float(ac_match.group('signal_power')) if ac_match.group('signal_power') else None}
else:
return None
@ -63,10 +72,19 @@ def parse_ogn_receiver_beacon(aprs_comment):
'total_ram': float(rec_match.group('ram_total')),
'ntp_error': float(rec_match.group('ntp_offset')),
'rt_crystal_correction': float(rec_match.group('ntp_correction')),
'voltage': float(rec_match.group('voltage')) if rec_match.group('voltage') else None,
'amperage': float(rec_match.group('amperage')) if rec_match.group('amperage') else None,
'cpu_temp': float(rec_match.group('cpu_temperature')) if rec_match.group('cpu_temperature') else None,
'rec_crystal_correction': int(rec_match.group('manual_correction')) if rec_match.group('manual_correction') else 0,
'rec_crystal_correction_fine': float(rec_match.group('automatic_correction')) if rec_match.group('automatic_correction') else 0.0,
'rec_input_noise': float(rec_match.group('input_noise')) if rec_match.group('input_noise') else None}
'senders_visible': int(rec_match.group('visible_senders')) if rec_match.group('visible_senders') else None,
'senders_total': int(rec_match.group('senders')) if rec_match.group('senders') else None,
'rec_crystal_correction': int(rec_match.group('rf_correction_manual')) if rec_match.group('rf_correction_manual') else 0,
'rec_crystal_correction_fine': float(rec_match.group('rf_correction_automatic')) if rec_match.group('rf_correction_automatic') else 0.0,
'rec_input_noise': float(rec_match.group('signal')) if rec_match.group('signal') else None,
'senders_signal': float(rec_match.group('senders_signal')) if rec_match.group('senders_signal') else None,
'senders_messages': float(rec_match.group('senders_messages')) if rec_match.group('senders_messages') else None,
'good_senders_signal': float(rec_match.group('good_senders_signal')) if rec_match.group('good_senders_signal') else None,
'good_senders': float(rec_match.group('good_senders')) if rec_match.group('good_senders') else None,
'good_and_bad_senders': float(rec_match.group('good_and_bad_senders')) if rec_match.group('good_and_bad_senders') else None}
else:
return None

Wyświetl plik

@ -1,7 +1,8 @@
import re
PATTERN_APRS = re.compile(r"^(?P<callsign>.+?)>(?P<dstcall>[A-Z0-9]+),.+,(?P<receiver>.+?):/(?P<time>\d{6})+h(?P<latitude>\d{4}\.\d{2})(?P<latitude_sign>N|S)(?P<symbol_table>.)(?P<longitude>\d{5}\.\d{2})(?P<longitude_sign>E|W)(?P<symbol>.)(?P<course_extension>(?P<course>\d{3})/(?P<ground_speed>\d{3}))?/A=(?P<altitude>\d{6})(?P<pos_extension>\s!W((?P<latitude_enhancement>\d)(?P<longitude_enhancement>\d))!)?\s(?P<comment>.*)$")
PATTERN_APRS_POSITION = re.compile(r"^(?P<callsign>.+?)>(?P<dstcall>[A-Z0-9]+),.+,(?P<receiver>.+?):/(?P<time>\d{6})+h(?P<latitude>\d{4}\.\d{2})(?P<latitude_sign>N|S)(?P<symbol_table>.)(?P<longitude>\d{5}\.\d{2})(?P<longitude_sign>E|W)(?P<symbol>.)(?P<course_extension>(?P<course>\d{3})/(?P<ground_speed>\d{3}))?/A=(?P<altitude>\d{6})(?P<pos_extension>\s!W((?P<latitude_enhancement>\d)(?P<longitude_enhancement>\d))!)?\s(?P<comment>.*)$")
PATTERN_APRS_STATUS = re.compile(r"(?P<callsign>.+?)>(?P<dstcall>[A-Z0-9]+),.+,(?P<receiver>.+?):>(?P<time>\d{6})+h\s(?P<comment>.*)$")
# The following regexp patterns are part of the ruby ogn-client.
# source: https://github.com/svoop/ogn_client-ruby
@ -35,14 +36,19 @@ PATTERN_RECEIVER_BEACON = re.compile(r"""
\s)?
CPU:(?P<cpu_load>[\d.]+)\s
RAM:(?P<ram_free>[\d.]+)\/(?P<ram_total>[\d.]+)MB\s
NTP:(?P<ntp_offset>[\d.]+)ms\/(?P<ntp_correction>[+-][\d.]+)ppm\s?
NTP:(?P<ntp_offset>[\d.]+)ms\/(?P<ntp_correction>[+-][\d.]+)ppm\s
(?:(?P<voltage>[\d.]+)V\s)?
(?:(?P<amperage>[\d.]+)A\s)?
(?:(?P<cpu_temperature>[+-][\d.]+)C\s*)?
(?:(?P<visible_senders>\d+)\/(?P<senders>\d+)Acfts\[1h\]\s*)?
(?:RF:
(?:
(?P<manual_correction>[+-][\d]+)
(?P<automatic_correction>[+-][\d.]+)ppm\/
(?P<rf_correction_manual>[+-][\d]+)
(?P<rf_correction_automatic>[+-][\d.]+)ppm\/
)?
(?P<input_noise>[+-][\d.]+)dB
(?P<signal>[+-][\d.]+)dB
(?:\/(?P<senders_signal>[+-][\d.]+)dB@10km\[(?P<senders_messages>\d+)\])?
(?:\/(?P<good_senders_signal>[+-][\d.]+)dB@10km\[(?P<good_senders>\d+)\/(?P<good_and_bad_senders>\d+)\])?
)?
""", re.VERBOSE | re.MULTILINE)
@ -52,12 +58,13 @@ PATTERN_AIRCRAFT_BEACON = re.compile(r"""
(?P<climb_rate>[+-]\d+?)fpm\s
(?P<turn_rate>[+-][\d.]+?)rot\s
(?:FL(?P<flight_level>[\d.]+)\s)?
(?P<signal>[\d.]+?)dB\s
(?P<signal_quality>[\d.]+?)dB\s
(?P<errors>\d+)e\s
(?P<frequency_offset>[+-][\d.]+?)kHz\s?
(?:gps(?P<gps_accuracy>\d+x\d+)\s?)?
(?:s(?P<flarm_software_version>[\d.]+)\s?)?
(?:h(?P<flarm_hardware_version>[\dA-F]{2})\s?)?
(?:r(?P<flarm_id>[\dA-F]+)\s?)?
(?:(?P<signal_power>[+-][\d.]+)dBm\s?)?
(?:hear(?P<proximity>.+))?
""", re.VERBOSE | re.MULTILINE)

Wyświetl plik

@ -43,7 +43,6 @@ class OgnClientTest(unittest.TestCase):
client.sock.shutdown.assert_called_once_with(0)
client.sock.close.assert_called_once_with()
@unittest.skip("messages from rtlsdr_ogn v0.2.5 are not supported yet")
def test_50_live_messages(self):
print("Enter")
self.remaining_messages = 50

Wyświetl plik

@ -15,7 +15,7 @@ class TestStringMethods(unittest.TestCase):
self.assertEqual(aircraft_beacon['address'], "DDA5BA")
self.assertAlmostEqual(aircraft_beacon['climb_rate'] * ms2fpm, -454, 2)
self.assertEqual(aircraft_beacon['turn_rate'], -1.1)
self.assertEqual(aircraft_beacon['signal_strength'], 8.8)
self.assertEqual(aircraft_beacon['signal_quality'], 8.8)
self.assertEqual(aircraft_beacon['error_count'], 0)
self.assertEqual(aircraft_beacon['frequency_offset'], 51.2)
self.assertEqual(aircraft_beacon['gps_status'], '4x5')
@ -44,6 +44,11 @@ class TestStringMethods(unittest.TestCase):
self.assertEqual(aircraft_beacon['flightlevel'], 4.43)
def test_v025(self):
aircraft_beacon = parse_ogn_aircraft_beacon("id06DDE28D +535fpm +3.8rot 11.5dB 0e -1.0kHz gps2x3 s6.01 h0C +7.4dBm")
self.assertEqual(aircraft_beacon['signal_power'], 7.4)
if __name__ == '__main__':
unittest.main()

Wyświetl plik

@ -28,12 +28,22 @@ class TestStringMethods(unittest.TestCase):
self.assertEqual(message['comment'], "this is a comment")
def test_v024(self):
# higher precision datum format introduced
raw_message = "FLRDDA5BA>APRS,qAS,LFMX:/160829h4415.41N/00600.03E'342/049/A=005524 !W26! id21400EA9 -2454fpm +0.9rot 19.5dB 0e -6.6kHz gps1x1 s6.02 h44 rDF0C56"
message = parse_aprs(raw_message, reference_date=datetime(2015, 1, 1, 16, 8, 29))
self.assertAlmostEqual(message['latitude'] - 44.2568 - 1 / 30000, 2 / 1000 / 60, 10)
self.assertAlmostEqual(message['longitude'] - 6.0005, 6 / 1000 / 60, 10)
def test_v025(self):
# introduced the "aprs status" format where many informations (lat, lon, alt, speed, ...) are just optional
raw_message = "EPZR>APRS,TCPIP*,qAC,GLIDERN1:>093456h this is a comment"
message = parse_aprs(raw_message)
self.assertEqual(message['name'], "EPZR")
self.assertEqual(message['receiver_name'], "GLIDERN1")
self.assertEqual(message['timestamp'].strftime('%H:%M:%S'), "09:34:56")
self.assertEqual(message['comment'], "this is a comment")
if __name__ == '__main__':
unittest.main()

Wyświetl plik

@ -7,6 +7,12 @@ class TestStringMethods(unittest.TestCase):
def test_fail_validation(self):
self.assertEqual(parse_ogn_receiver_beacon("notAValidToken"), None)
def test_v021(self):
receiver_beacon = parse_ogn_receiver_beacon("v0.2.1 CPU:0.8 RAM:25.6/458.9MB NTP:0.0ms/+0.0ppm +51.9C RF:+26-1.4ppm/-0.25dB")
self.assertEqual(receiver_beacon['rec_crystal_correction'], 26)
self.assertEqual(receiver_beacon['rec_crystal_correction_fine'], -1.4)
self.assertEqual(receiver_beacon['rec_input_noise'], -0.25)
def test_v022(self):
receiver_beacon = parse_ogn_receiver_beacon("v0.2.2.x86 CPU:0.5 RAM:669.9/887.7MB NTP:1.0ms/+6.2ppm +52.0C RF:+0.06dB")
self.assertEqual(receiver_beacon['version'], '0.2.2')
@ -20,11 +26,21 @@ class TestStringMethods(unittest.TestCase):
self.assertEqual(receiver_beacon['rec_crystal_correction_fine'], 0.0)
self.assertEqual(receiver_beacon['rec_input_noise'], 0.06)
def test_v021(self):
receiver_beacon = parse_ogn_receiver_beacon("v0.2.1 CPU:0.8 RAM:25.6/458.9MB NTP:0.0ms/+0.0ppm +51.9C RF:+26-1.4ppm/-0.25dB")
self.assertEqual(receiver_beacon['rec_crystal_correction'], 26)
self.assertEqual(receiver_beacon['rec_crystal_correction_fine'], -1.4)
self.assertEqual(receiver_beacon['rec_input_noise'], -0.25)
def test_v025(self):
receiver_beacon = parse_ogn_receiver_beacon("v0.2.5.RPI-GPU CPU:0.8 RAM:287.3/458.7MB NTP:1.0ms/-6.4ppm 5.016V 0.534A +51.9C RF:+55+0.4ppm/-0.67dB/+10.8dB@10km[57282]")
self.assertEqual(receiver_beacon['voltage'], 5.016)
self.assertEqual(receiver_beacon['amperage'], 0.534)
self.assertEqual(receiver_beacon['senders_signal'], 10.8)
self.assertEqual(receiver_beacon['senders_messages'], 57282)
receiver_beacon = parse_ogn_receiver_beacon("v0.2.5.ARM CPU:0.4 RAM:638.0/970.5MB NTP:0.2ms/-1.1ppm +65.5C 14/16Acfts[1h] RF:+45+0.0ppm/+3.88dB/+24.0dB@10km[143717]/+26.7dB@10km[68/135]")
self.assertEqual(receiver_beacon['senders_visible'], 14)
self.assertEqual(receiver_beacon['senders_total'], 16)
self.assertEqual(receiver_beacon['senders_signal'], 24.0)
self.assertEqual(receiver_beacon['senders_messages'], 143717)
self.assertEqual(receiver_beacon['good_senders_signal'], 26.7)
self.assertEqual(receiver_beacon['good_senders'], 68)
self.assertEqual(receiver_beacon['good_and_bad_senders'], 135)
if __name__ == '__main__':

Wyświetl plik

@ -15,3 +15,14 @@ Salland>APRS,TCPIP*,qAC,GLIDERN2:/165426h5227.93NI00620.03E&/A=000049 v0.2.2 CPU
LSGS>APRS,TCPIP*,qAC,GLIDERN1:/165345h4613.25NI00719.68E&/A=001581 CPU:0.7 RAM:247.9/456.4MB NTP:0.7ms/-11.4ppm +44.4C RF:+53+71.9ppm/+0.4dB
Drenstein>APRS,TCPIP*,qAC,GLIDERN1:/165011h5147.51NI00744.45E&/A=000213 v0.2.2 CPU:0.8 RAM:695.7/4025.5MB NTP:16000.0ms/+0.0ppm +63.0C
ZK-GSC>APRS,qAS,Omarama:/165202h4429.25S/16959.33E'/A=001407 id05C821EA +020fpm +0.0rot 16.8dB 0e -3.1kHz gps1x3 hear1084 hearB597 hearB598
# since 0.2.5 for receiver information not only the "aprs position" format is used but also the "aprs status" format (without lat/lon/alt informations)
Cordoba>APRS,TCPIP*,qAC,GLIDERN3:/194847h3112.85SI06409.56W&/A=001712 v0.2.5.ARM CPU:0.4 RAM:755.4/970.8MB NTP:6.7ms/-0.1ppm +45.5C RF:+48+18.3ppm/+3.45dB
Cordoba>APRS,TCPIP*,qAC,GLIDERN3:>194847h v0.2.5.ARM CPU:0.4 RAM:755.4/970.8MB NTP:6.7ms/-0.1ppm +45.5C 0/0Acfts[1h] RF:+48+18.3ppm/+3.45dB/+0.4dB@10km[71]/+0.4dB@10km[1/1]
VITACURA1>APRS,TCPIP*,qAC,GLIDERN3:/042149h3322.81SI07034.95W&/A=002345 v0.2.5.ARM CPU:0.6 RAM:694.4/970.5MB NTP:0.8ms/-7.5ppm +54.8C RF:+0-0.2ppm/+3.81dB
VITACURA1>APRS,TCPIP*,qAC,GLIDERN3:>042149h v0.2.5.ARM CPU:0.6 RAM:694.4/970.5MB NTP:0.8ms/-7.5ppm +54.8C 0/0Acfts[1h] RF:+0-0.2ppm/+3.81dB/+1.3dB@10km[132205]/+6.6dB@10km[10/20]
Arnsberg>APRS,TCPIP*,qAC,GLIDERN1:/042146h5123.04NI00803.77E&/A=000623 v0.2.5.ARM CPU:0.4 RAM:765.1/970.8MB NTP:0.4ms/-1.7ppm +62.3C RF:+27+1.1ppm/+3.17dB
Arnsberg>APRS,TCPIP*,qAC,GLIDERN1:>042146h v0.2.5.ARM CPU:0.4 RAM:764.9/970.8MB NTP:0.4ms/-1.7ppm +62.3C 0/0Acfts[1h] RF:+27+1.1ppm/+3.17dB/+9.2dB@10km[44487]/+12.1dB@10km[20/40]
CNF3a>APRS,TCPIP*,qAC,GLIDERN3:/042143h4529.25NI07505.65W&/A=000259 v0.2.5.ARM CPU:0.6 RAM:514.6/970.8MB NTP:4.5ms/-1.5ppm +27.2C RF:+0-0.4ppm/+18.69dB
CNF3a>APRS,TCPIP*,qAC,GLIDERN3:>042143h v0.2.5.ARM CPU:0.6 RAM:514.6/970.8MB NTP:4.5ms/-1.5ppm +27.2C 0/0Acfts[1h] RF:+0-0.4ppm/+18.69dB/+13.0dB@10km[104282]/+9.7dB@10km[2/3]
VITACURA2>APRS,TCPIP*,qAC,GLIDERN3:/042136h3322.81SI07034.95W&/A=002345 v0.2.5.ARM CPU:0.3 RAM:695.0/970.5MB NTP:0.6ms/-5.7ppm +51.5C RF:+0-0.0ppm/+1.32dB
VITACURA2>APRS,TCPIP*,qAC,GLIDERN3:>042136h v0.2.5.ARM CPU:0.3 RAM:695.0/970.5MB NTP:0.6ms/-5.7ppm +52.1C 0/0Acfts[1h] RF:+0-0.0ppm/+1.32dB/+2.1dB@10km[193897]/+9.0dB@10km[10/20]