kopia lustrzana https://github.com/glidernet/python-ogn-client
commit
53ecdb251e
|
@ -3,6 +3,7 @@ language: python
|
|||
python:
|
||||
- 3.4
|
||||
- 3.5
|
||||
- 3.6
|
||||
|
||||
before_script:
|
||||
- flake8 tests ogn
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
# CHANGELOG
|
||||
|
||||
## Unreleased
|
||||
- parser: Added support for heared aircrafts
|
||||
## 0.8.0 - 2017-10-02
|
||||
- parser: Merged function 'parse_aprs' and 'parse_ogn_beacon' to 'parse'
|
||||
- parser: Added support for OGNSDR (receiver), OGNTRK (ogn tracker), OGNFLR (flarm) and OGNAV (Naviter) beacons
|
||||
- parser: Added support for RELAYed messages
|
||||
- parser: Added support for ddmmss time format (eg. '312359z')
|
||||
- parser: Added support for heared aircrafts
|
||||
- client: Allow client to do sequential connect-disconnect
|
||||
|
||||
## 0.7.1 - 2017-06-05
|
||||
|
|
|
@ -4,7 +4,7 @@ APRS_SERVER_PORT_CLIENT_DEFINED_FILTERS = 14580
|
|||
|
||||
APRS_APP_NAME = 'python-ogn-client'
|
||||
|
||||
PACKAGE_VERSION = '0.7.1'
|
||||
PACKAGE_VERSION = '0.8.0'
|
||||
APRS_APP_VER = PACKAGE_VERSION[:3]
|
||||
|
||||
APRS_KEEPALIVE_TIME = 240
|
||||
|
|
|
@ -28,7 +28,7 @@ def parse(aprs_message, reference_date=None, reference_time=None):
|
|||
return message
|
||||
|
||||
|
||||
def parse_aprs(message, reference_date=None, reference_time=None):
|
||||
def parse_aprs(message, reference_date, reference_time=None):
|
||||
match_position = re.search(PATTERN_APRS_POSITION, message)
|
||||
if match_position:
|
||||
return {'name': match_position.group('callsign'),
|
||||
|
@ -74,8 +74,9 @@ def parse_comment(aprs_comment, dstcall="APRS", aprs_type="position"):
|
|||
if rc_data:
|
||||
rc_data.update({'beacon_type': 'receiver_beacon'})
|
||||
return rc_data
|
||||
|
||||
raise OgnParseError(aprs_comment)
|
||||
else:
|
||||
return {'user_comment': aprs_comment,
|
||||
'beacon_type': 'receiver_beacon'}
|
||||
elif dstcall == "OGFLR":
|
||||
ac_data = parse_aircraft_beacon(aprs_comment)
|
||||
ac_data.update({'beacon_type': 'aircraft_beacon'})
|
||||
|
@ -117,4 +118,4 @@ def parse_comment(aprs_comment, dstcall="APRS", aprs_type="position"):
|
|||
ac_data.update({'beacon_type': 'spot_beacon'})
|
||||
return ac_data
|
||||
else:
|
||||
raise ValueError("dstcall {} unknown".format(dstcall))
|
||||
raise OgnParseError("No parser for dstcall {} found. APRS comment: {}".format(dstcall, aprs_comment))
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import re
|
||||
|
||||
|
||||
PATTERN_APRS_POSITION = re.compile(r"^(?P<callsign>.+?)>(?P<dstcall>[A-Z0-9]+),((?P<relay>[A-Za-z0-9]+)\*)?.*,(?P<receiver>.+?):/(?P<time>\d{6}(h|z))(?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<relay>[A-Za-z0-9]+)\*)?.*,(?P<receiver>.+?):/(?P<time>\d{6}(h|z))(?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|z))\s(?P<comment>.*)$")
|
||||
|
||||
PATTERN_NAVITER_BEACON = re.compile("""
|
||||
|
|
5
setup.py
5
setup.py
|
@ -30,6 +30,7 @@ setup(
|
|||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
],
|
||||
keywords='gliding ogn',
|
||||
packages=['ogn.{}'.format(package) for package in find_packages(where='ogn')],
|
||||
|
@ -37,8 +38,8 @@ setup(
|
|||
extras_require={
|
||||
'dev': [
|
||||
'nose==1.3.7',
|
||||
'coveralls==1.1',
|
||||
'flake8==3.3.0'
|
||||
'coveralls==1.2',
|
||||
'flake8==3.4.1'
|
||||
]
|
||||
},
|
||||
zip_safe=False
|
||||
|
|
|
@ -6,7 +6,7 @@ from ogn.client.client import create_aprs_login, AprsClient
|
|||
from ogn.client.settings import APRS_APP_NAME, APRS_APP_VER
|
||||
|
||||
|
||||
class OgnClientTest(unittest.TestCase):
|
||||
class AprsClientTest(unittest.TestCase):
|
||||
def test_create_aprs_login(self):
|
||||
basic_login = create_aprs_login('klaus', -1, 'myApp', '0.1')
|
||||
self.assertEqual('user klaus pass -1 vers myApp 0.1\n', basic_login)
|
||||
|
@ -25,7 +25,7 @@ class OgnClientTest(unittest.TestCase):
|
|||
client.connect()
|
||||
client.sock.send.assert_called_once_with('user testuser pass -1 vers {} {}\n'.format(
|
||||
APRS_APP_NAME, APRS_APP_VER).encode('ascii'))
|
||||
client.sock.makefile.asser_called_once_with('rw')
|
||||
client.sock.makefile.assert_called_once_with('rw')
|
||||
|
||||
@mock.patch('ogn.client.client.socket')
|
||||
def test_connect_client_defined_filter(self, mock_socket):
|
||||
|
@ -33,7 +33,7 @@ class OgnClientTest(unittest.TestCase):
|
|||
client.connect()
|
||||
client.sock.send.assert_called_once_with('user testuser pass -1 vers {} {} filter r/50.4976/9.9495/100\n'.format(
|
||||
APRS_APP_NAME, APRS_APP_VER).encode('ascii'))
|
||||
client.sock.makefile.asser_called_once_with('rw')
|
||||
client.sock.makefile.assert_called_once_with('rw')
|
||||
|
||||
@mock.patch('ogn.client.client.socket')
|
||||
def test_disconnect(self, mock_socket):
|
||||
|
@ -72,7 +72,12 @@ class OgnClientTest(unittest.TestCase):
|
|||
def process_message(raw_message):
|
||||
if raw_message[0] == '#':
|
||||
return
|
||||
parse(raw_message)
|
||||
try:
|
||||
message = parse(raw_message)
|
||||
print("{}: {}".format(message['beacon_type'], raw_message))
|
||||
except NotImplementedError as e:
|
||||
print("{}: {}".format(e, raw_message))
|
||||
return
|
||||
if self.remaining_messages > 0:
|
||||
self.remaining_messages -= 1
|
||||
else:
|
||||
|
|
|
@ -63,6 +63,12 @@ class TestStringMethods(unittest.TestCase):
|
|||
with self.assertRaises(AprsParseError):
|
||||
parse("Lachens>APRS,TCPIwontbeavalidstring")
|
||||
|
||||
def test_v026_chile(self):
|
||||
# receiver beacons from chile have a APRS position message with a pure user comment
|
||||
message = parse("VITACURA1>APRS,TCPIP*,qAC,GLIDERN4:/201146h3322.79SI07034.80W&/A=002329 Vitacura Municipal Aerodrome, Club de Planeadores Vitacura", reference_date=datetime(2015, 1, 1))
|
||||
|
||||
self.assertEqual(message['user_comment'], "Vitacura Municipal Aerodrome, Club de Planeadores Vitacura")
|
||||
|
||||
@mock.patch('ogn.parser.parse_module.createTimestamp')
|
||||
def test_default_reference_date(self, createTimestamp_mock):
|
||||
valid_aprs_string = "Lachens>APRS,TCPIP*,qAC,GLIDERN2:/165334h4344.70NI00639.19E&/A=005435 v0.2.1 CPU:0.3 RAM:1764.4/2121.4MB NTP:2.8ms/+4.9ppm +47.0C RF:+0.70dB"
|
||||
|
|
|
@ -10,7 +10,7 @@ from ogn.parser.exceptions import AprsParseError
|
|||
class TestStringMethods(unittest.TestCase):
|
||||
def test_fail_validation(self):
|
||||
with self.assertRaises(AprsParseError):
|
||||
parse_aprs("notAValidString")
|
||||
parse_aprs("notAValidString", reference_date=datetime(2015, 1, 1))
|
||||
|
||||
def test_basic(self):
|
||||
message = parse_aprs("FLRDDA5BA>APRS,qAS,LFMX:/160829h4415.41N/00600.03E'342/049/A=005524 this is a comment",
|
||||
|
@ -71,6 +71,13 @@ class TestStringMethods(unittest.TestCase):
|
|||
|
||||
self.assertEqual(message['timestamp'].strftime('%d %H:%M'), "30 10:46")
|
||||
|
||||
def test_negative_altitude(self):
|
||||
# some devices can report negative altitudes
|
||||
raw_message = "OGNF71F40>APRS,qAS,NAVITER:/080852h4414.37N/01532.06E'253/052/A=-00013 !W73! id1EF71F40 -060fpm +0.0rot"
|
||||
message = parse_aprs(raw_message, reference_date=datetime(2015, 1, 1))
|
||||
|
||||
self.assertAlmostEqual(message['altitude'] * m2feet, -13, 5)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
@ -9,54 +9,54 @@ class TestStringMethods(unittest.TestCase):
|
|||
self.assertEqual(parse_aircraft_beacon("notAValidToken"), None)
|
||||
|
||||
def test_basic(self):
|
||||
aircraft_beacon = parse_aircraft_beacon("id0ADDA5BA -454fpm -1.1rot 8.8dB 0e +51.2kHz gps4x5 hear1084 hearB597 hearB598")
|
||||
message = parse_aircraft_beacon("id0ADDA5BA -454fpm -1.1rot 8.8dB 0e +51.2kHz gps4x5 hear1084 hearB597 hearB598")
|
||||
|
||||
self.assertEqual(aircraft_beacon['address_type'], 2)
|
||||
self.assertEqual(aircraft_beacon['aircraft_type'], 2)
|
||||
self.assertFalse(aircraft_beacon['stealth'])
|
||||
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_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')
|
||||
self.assertEqual(len(aircraft_beacon['proximity']), 3)
|
||||
self.assertEqual(aircraft_beacon['proximity'][0], '1084')
|
||||
self.assertEqual(aircraft_beacon['proximity'][1], 'B597')
|
||||
self.assertEqual(aircraft_beacon['proximity'][2], 'B598')
|
||||
self.assertEqual(message['address_type'], 2)
|
||||
self.assertEqual(message['aircraft_type'], 2)
|
||||
self.assertFalse(message['stealth'])
|
||||
self.assertEqual(message['address'], "DDA5BA")
|
||||
self.assertAlmostEqual(message['climb_rate'] * ms2fpm, -454, 2)
|
||||
self.assertEqual(message['turn_rate'], -1.1)
|
||||
self.assertEqual(message['signal_quality'], 8.8)
|
||||
self.assertEqual(message['error_count'], 0)
|
||||
self.assertEqual(message['frequency_offset'], 51.2)
|
||||
self.assertEqual(message['gps_status'], '4x5')
|
||||
self.assertEqual(len(message['proximity']), 3)
|
||||
self.assertEqual(message['proximity'][0], '1084')
|
||||
self.assertEqual(message['proximity'][1], 'B597')
|
||||
self.assertEqual(message['proximity'][2], 'B598')
|
||||
|
||||
def test_stealth(self):
|
||||
aircraft_beacon = parse_aircraft_beacon("id0ADD1234 -454fpm -1.1rot 8.8dB 0e +51.2kHz gps4x5 hear1084 hearB597 hearB598")
|
||||
self.assertFalse(aircraft_beacon['stealth'])
|
||||
message = parse_aircraft_beacon("id0ADD1234 -454fpm -1.1rot 8.8dB 0e +51.2kHz gps4x5 hear1084 hearB597 hearB598")
|
||||
self.assertFalse(message['stealth'])
|
||||
|
||||
aircraft_beacon = parse_aircraft_beacon("id8ADD1234 -454fpm -1.1rot 8.8dB 0e +51.2kHz gps4x5 hear1084 hearB597 hearB598")
|
||||
self.assertTrue(aircraft_beacon['stealth'])
|
||||
message = parse_aircraft_beacon("id8ADD1234 -454fpm -1.1rot 8.8dB 0e +51.2kHz gps4x5 hear1084 hearB597 hearB598")
|
||||
self.assertTrue(message['stealth'])
|
||||
|
||||
def test_v024(self):
|
||||
aircraft_beacon = parse_aircraft_beacon("id21400EA9 -2454fpm +0.9rot 19.5dB 0e -6.6kHz gps1x1 s6.02 h0A rDF0C56")
|
||||
message = parse_aircraft_beacon("id21400EA9 -2454fpm +0.9rot 19.5dB 0e -6.6kHz gps1x1 s6.02 h0A rDF0C56")
|
||||
|
||||
self.assertEqual(aircraft_beacon['software_version'], 6.02)
|
||||
self.assertEqual(aircraft_beacon['hardware_version'], 10)
|
||||
self.assertEqual(aircraft_beacon['real_address'], "DF0C56")
|
||||
self.assertEqual(message['software_version'], 6.02)
|
||||
self.assertEqual(message['hardware_version'], 10)
|
||||
self.assertEqual(message['real_address'], "DF0C56")
|
||||
|
||||
def test_v024_ogn_tracker(self):
|
||||
aircraft_beacon = parse_aircraft_beacon("id07353800 +020fpm -14.0rot FL004.43 38.5dB 0e -2.9kHz")
|
||||
message = parse_aircraft_beacon("id07353800 +020fpm -14.0rot FL004.43 38.5dB 0e -2.9kHz")
|
||||
|
||||
self.assertEqual(aircraft_beacon['flightlevel'], 4.43)
|
||||
self.assertEqual(message['flightlevel'], 4.43)
|
||||
|
||||
def test_v025(self):
|
||||
aircraft_beacon = parse_aircraft_beacon("id06DDE28D +535fpm +3.8rot 11.5dB 0e -1.0kHz gps2x3 s6.01 h0C +7.4dBm")
|
||||
message = parse_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)
|
||||
self.assertEqual(message['signal_power'], 7.4)
|
||||
|
||||
def test_v026(self):
|
||||
# from 0.2.6 it is sufficent we have only the ID, climb and turn rate or just the ID
|
||||
aircraft_beacon_triple = parse_aircraft_beacon("id093D0930 +000fpm +0.0rot")
|
||||
aircraft_beacon_single = parse_aircraft_beacon("id093D0930")
|
||||
message_triple = parse_aircraft_beacon("id093D0930 +000fpm +0.0rot")
|
||||
message_single = parse_aircraft_beacon("id093D0930")
|
||||
|
||||
self.assertIsNotNone(aircraft_beacon_triple)
|
||||
self.assertIsNotNone(aircraft_beacon_single)
|
||||
self.assertIsNotNone(message_triple)
|
||||
self.assertIsNotNone(message_single)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -8,39 +8,39 @@ class TestStringMethods(unittest.TestCase):
|
|||
self.assertEqual(parse_receiver_beacon("notAValidToken"), None)
|
||||
|
||||
def test_v021(self):
|
||||
receiver_beacon = parse_receiver_beacon("v0.2.1 CPU:0.8 RAM:25.6/458.9MB NTP:0.1ms/+2.3ppm +51.9C RF:+26-1.4ppm/-0.25dB")
|
||||
message = parse_receiver_beacon("v0.2.1 CPU:0.8 RAM:25.6/458.9MB NTP:0.1ms/+2.3ppm +51.9C RF:+26-1.4ppm/-0.25dB")
|
||||
|
||||
self.assertEqual(receiver_beacon['version'], "0.2.1")
|
||||
self.assertEqual(receiver_beacon['cpu_load'], 0.8)
|
||||
self.assertEqual(receiver_beacon['free_ram'], 25.6)
|
||||
self.assertEqual(receiver_beacon['total_ram'], 458.9)
|
||||
self.assertEqual(receiver_beacon['ntp_error'], 0.1)
|
||||
self.assertEqual(receiver_beacon['rt_crystal_correction'], 2.3)
|
||||
self.assertEqual(receiver_beacon['cpu_temp'], 51.9)
|
||||
self.assertEqual(message['version'], "0.2.1")
|
||||
self.assertEqual(message['cpu_load'], 0.8)
|
||||
self.assertEqual(message['free_ram'], 25.6)
|
||||
self.assertEqual(message['total_ram'], 458.9)
|
||||
self.assertEqual(message['ntp_error'], 0.1)
|
||||
self.assertEqual(message['rt_crystal_correction'], 2.3)
|
||||
self.assertEqual(message['cpu_temp'], 51.9)
|
||||
|
||||
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)
|
||||
self.assertEqual(message['rec_crystal_correction'], 26)
|
||||
self.assertEqual(message['rec_crystal_correction_fine'], -1.4)
|
||||
self.assertEqual(message['rec_input_noise'], -0.25)
|
||||
|
||||
def test_v022(self):
|
||||
receiver_beacon = parse_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['platform'], 'x86')
|
||||
message = parse_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(message['platform'], 'x86')
|
||||
|
||||
def test_v025(self):
|
||||
receiver_beacon = parse_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)
|
||||
message = parse_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(message['voltage'], 5.016)
|
||||
self.assertEqual(message['amperage'], 0.534)
|
||||
self.assertEqual(message['senders_signal'], 10.8)
|
||||
self.assertEqual(message['senders_messages'], 57282)
|
||||
|
||||
receiver_beacon = parse_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)
|
||||
message = parse_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(message['senders_visible'], 14)
|
||||
self.assertEqual(message['senders_total'], 16)
|
||||
self.assertEqual(message['senders_signal'], 24.0)
|
||||
self.assertEqual(message['senders_messages'], 143717)
|
||||
self.assertEqual(message['good_senders_signal'], 26.7)
|
||||
self.assertEqual(message['good_senders'], 68)
|
||||
self.assertEqual(message['good_and_bad_senders'], 135)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -22,5 +22,6 @@ CNF3a>APRS,TCPIP*,qAC,GLIDERN3:>042143h v0.2.5.ARM CPU:0.6 RAM:514.6/970.8MB NTP
|
|||
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]
|
||||
#
|
||||
# since 0.2.6 the ogn comment of a receiver beacon is just optional
|
||||
# since 0.2.6 the ogn comment of a receiver beacon is just optional, it also can be a user comment
|
||||
Ulrichamn>APRS,TCPIP*,qAC,GLIDERN1:/085616h5747.30NI01324.77E&/A=001322
|
||||
ROBLE3>APRS,TCPIP*,qAC,GLIDERN4:/200022h3258.58SI07100.78W&/A=007229 Contact: achanes@manquehue.net, brito.felipe@gmail.com
|
||||
|
|
Ładowanie…
Reference in New Issue