kopia lustrzana https://github.com/glidernet/python-ogn-client
Porównaj commity
170 Commity
Autor | SHA1 | Data |
---|---|---|
dependabot[bot] | 471840f657 | |
dependabot[bot] | d5b8412c9c | |
dependabot[bot] | b0f3333fd0 | |
Konstantin Gründger | e58c1d8c0e | |
Konstantin Gründger | 7eacc96313 | |
Konstantin Gründger | 3e7ec2d917 | |
Konstantin Gründger | 0621307952 | |
lemoidului | 22d930b403 | |
Konstantin Gründger | 28848b6248 | |
Konstantin Gründger | 7e1a7e75a9 | |
dependabot[bot] | eddd3a5a72 | |
dependabot[bot] | 0c53ccc5af | |
lemoidului | 5291ddeaf4 | |
dependabot[bot] | a5337c85b4 | |
dependabot[bot] | 49f0519c5c | |
Konstantin Gründger | 84d09c8b1b | |
Meisterschueler | f83c4c71fb | |
dependabot[bot] | 3c36923b9a | |
dependabot-preview[bot] | ba79be2803 | |
lemoidului | b51a3e95e6 | |
lemoidului | 68f8b9b3a7 | |
lemoidului | faa73997d7 | |
Konstantin Gründger | 464969a70e | |
dependabot-preview[bot] | 8db0e7706b | |
dependabot-preview[bot] | acc73561bc | |
Konstantin Gründger | edc470212c | |
tfraudet | 8fef35830e | |
tfraudet | f11e0efa97 | |
dependabot-preview[bot] | 029b7add80 | |
Konstantin Gründger | bd0759eec0 | |
Konstantin Gründger | 8b85415fef | |
Konstantin Gründger | 74f6f9671f | |
Konstantin Gründger | a612ca9965 | |
Konstantin Gründger | fe0257b4aa | |
Konstantin Gründger | ed1634603e | |
Konstantin Gründger | f500b36fb6 | |
Konstantin Gründger | e1c4623f64 | |
Konstantin Gründger | 3d36b88996 | |
Konstantin Gründger | 8dba8f7491 | |
Konstantin Gründger | e272ab0645 | |
Konstantin Gründger | 4a9a884d7c | |
Konstantin Gründger | 8855a4f097 | |
Konstantin Gründger | ffa6c8b1f0 | |
dependabot-preview[bot] | 9551d9c54e | |
Konstantin Gründger | 5a90299ca5 | |
Konstantin Gründger | 0c0ce12f25 | |
Konstantin Gründger | 166023b352 | |
Konstantin Gründger | bc3178f513 | |
Konstantin Gründger | 7b60095366 | |
Konstantin Gründger | 0784ecdb17 | |
Konstantin Gründger | a610900548 | |
Konstantin Gründger | b69e733df4 | |
Konstantin Gründger | 92a8575378 | |
Konstantin Gründger | 1a9e1f2edd | |
Konstantin Gründger | 01fecbd1f0 | |
Meisterschueler | f8f658a54c | |
Meisterschueler | ca4513a334 | |
scls19fr | 922616865b | |
dependabot-preview[bot] | 3862176244 | |
Meisterschueler | b8f6db0e2b | |
Axel Fahy | 62ee197add | |
Axel Fahy | 2c1612de93 | |
Meisterschueler | 7669ac4559 | |
Paul-Etienne Coisne | 0a32f7519f | |
dependabot-preview[bot] | 4d85b33429 | |
dependabot-preview[bot] | a1f6150cde | |
dependabot-preview[bot] | 418c904885 | |
dependabot-preview[bot] | 1a60c7937b | |
Konstantin Gründger | b495e6aec0 | |
Konstantin Gründger | 14fbcd7c12 | |
Konstantin Gründger | 4968b9adf7 | |
Konstantin Gründger | 4d2557863f | |
dependabot-preview[bot] | 1ec2603940 | |
Konstantin Gründger | 2d414f70e0 | |
Konstantin Gründger | 43f9ce4b7c | |
Konstantin Gründger | e0f363be91 | |
Meisterschueler | 858b0af032 | |
dependabot-preview[bot] | 446f4766ea | |
Konstantin Gründger | 3f36915c1b | |
Meisterschueler | b1ce503c32 | |
Meisterschueler | 4d380d5eb7 | |
Zoran Bošnjak | ab25b2b626 | |
Meisterschueler | f5f5a4f2e9 | |
dependabot-preview[bot] | 17c73b9ca5 | |
Meisterschueler | e06474398d | |
Zoran Bošnjak | b48ee35e70 | |
Zoran Bošnjak | 689d60ebbe | |
dependabot-preview[bot] | 58caf55f42 | |
dependabot-preview[bot] | 4b62b4002c | |
Konstantin Gründger | 226a8239fa | |
dependabot-preview[bot] | 8446140bce | |
Konstantin Gründger | 7260245e15 | |
dependabot-preview[bot] | 1892cce7b3 | |
Konstantin Gründger | 2e02015e52 | |
dependabot-preview[bot] | f85455cc51 | |
Konstantin Gründger | 2d2a003e8a | |
Konstantin Gründger | 342af0a253 | |
Konstantin Gründger | 37d089acac | |
Konstantin Gründger | b7f51b92f8 | |
Konstantin Gründger | 706a725305 | |
Konstantin Gründger | 7682a69fd7 | |
Konstantin Gründger | b8f0028f1e | |
Konstantin Gründger | 1eae059cf6 | |
Konstantin Gründger | 061d47bb03 | |
dependabot-preview[bot] | 002d92a42b | |
dependabot-preview[bot] | e9d461b8ce | |
Meisterschueler | a07d07be9a | |
Philip Lee | d76b7525e6 | |
Philip Lee | 55e3696a34 | |
Konstantin Gründger | 37de8673c2 | |
Konstantin Gründger | 7a577ab9ef | |
Konstantin Gründger | dab3ca3ddb | |
Konstantin Gründger | b0bf5c82ee | |
Meisterschueler | 4780a08500 | |
Matt | b6659bb216 | |
Konstantin Gründger | ae58ad7f0d | |
Konstantin Gründger | 0ec3dc277d | |
Konstantin Gründger | 4a2c2429f1 | |
Konstantin Gründger | 32064f5364 | |
Konstantin Gründger | 595519053d | |
Konstantin Gründger | 84dbfdecdb | |
Konstantin Gründger | 485282ea3b | |
Konstantin Gründger | c779e908af | |
Konstantin Gründger | a1f84c6cb7 | |
Konstantin Gründger | 79ab1cfc8a | |
Konstantin Gründger | 3c63cffb23 | |
Konstantin Gründger | cda1cb511b | |
Konstantin Gründger | fabb92bc88 | |
Konstantin Gründger | 8c874a3226 | |
Konstantin Gründger | c8d739eccd | |
Konstantin Gründger | 572d8bbc54 | |
Konstantin Gründger | 8cff2a2c4d | |
Konstantin Gründger | 80b5cc4a48 | |
Konstantin Gründger | 6e0271cfc1 | |
Konstantin Gründger | b7458fa021 | |
Meisterschueler | a3beaf7804 | |
Meisterschueler | c3012784ab | |
Meisterschueler | 4acde4b387 | |
Konstantin Gründger | 74bd6d7b34 | |
Konstantin Gründger | 12bdf25243 | |
Konstantin Gründger | a8a83e960b | |
Konstantin Gründger | aa682a9e10 | |
Konstantin Gründger | 8f8974446c | |
Konstantin Gründger | a8d0901192 | |
Konstantin Gründger | 22d545c86c | |
Konstantin Gründger | bc3e31838f | |
Konstantin Gründger | c58739705f | |
Konstantin Gründger | 52468a4bd1 | |
Konstantin Gründger | 7ca937a17d | |
Konstantin Gründger | 48d0faf374 | |
Meisterschueler | 58e44e7530 | |
Colin S | 5a0a382914 | |
Konstantin Gründger | fdb05074a3 | |
Konstantin Gründger | b1d26ce0c0 | |
Konstantin Gründger | 41e7128253 | |
Konstantin Gründger | caffdabb4e | |
Meisterschueler | 88a73c00d6 | |
Konstantin Gründger | 37e239fb3a | |
Meisterschueler | f562580edc | |
kammermark@gmx.de | f1a9788078 | |
Konstantin Gründger | 1607738b12 | |
Konstantin Gründger | 9a90c347fa | |
Konstantin Gründger | b309cfe805 | |
Konstantin Gründger | a580481590 | |
Meisterschueler | 483b25d4fa | |
Konstantin Gründger | c2b82817c7 | |
Meisterschueler | 7d738931e2 | |
Konstantin Gründger | d1e394c1d7 | |
Konstantin Gründger | 85b7ae5adb | |
Konstantin Gründger | 2ccdcd90be |
|
@ -0,0 +1,8 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: pip
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: daily
|
||||
time: "04:00"
|
||||
open-pull-requests-limit: 10
|
|
@ -1,9 +1,11 @@
|
|||
language: python
|
||||
|
||||
python:
|
||||
- 3.4
|
||||
- 3.5
|
||||
- 3.6
|
||||
- 3.7
|
||||
- 3.8
|
||||
- 3.9-dev
|
||||
|
||||
before_script:
|
||||
- flake8 tests ogn
|
||||
|
|
76
CHANGELOG.md
76
CHANGELOG.md
|
@ -1,10 +1,84 @@
|
|||
# CHANGELOG
|
||||
|
||||
## unreleased
|
||||
- client: If no reference_timestamp provided use timestamp from APRS server (fixes #85)
|
||||
|
||||
## 1.2.1: - 2021-06-06
|
||||
- client: Added peer IP to log messages
|
||||
- parser: Added rainfall_1h and rainfall_24h to beacon_type 'position_weather'
|
||||
|
||||
## 1.2.0: - 2021-06-01
|
||||
- parser: Added support for OGNSKY (safesky) beacons
|
||||
- client: Replace bad characters with <20> instead of raising an exception (restore old behaviour with parameter ignore_decoding_error=False)
|
||||
|
||||
## 1.1.0: - 2021-04-05
|
||||
- parser: Added no-tracking flag decoding
|
||||
- parser: Fixed aircraft_type decoding
|
||||
## 1.0.1: - 2020-11-02
|
||||
- client: catch errors while connecting (fixes #74 and #91)
|
||||
- client: no logging messages by default (fixes #92)
|
||||
|
||||
## 1.0.0: - 2020-10-15
|
||||
- client: changed socket mode from blocking to 5s timeout (fixes #89)
|
||||
- parser: Added optional distance/bearing/normalized_quality calculation if parameter "calculate_relatives" is True (fixes #86)
|
||||
- parser: Added support for weather data (new in receiver software v0.2.8) from FANET ground stations (aprs_type: position_weather)
|
||||
- parser: Added support for latency (new in receiver software v0.2.8) in receiver messages (OGNSDR) (fixes #87)
|
||||
- parser: Added support for reference_timestamp with tzinfo (fixes #84)
|
||||
- parser: Fixed textual altitude part (fixes #81)
|
||||
- parser: Skip keys where value is "None"
|
||||
|
||||
## 0.9.8: - 2020-08-21
|
||||
- parser: Changed InReach parser (fixes #73)
|
||||
- parser: separated incompatible ID into parser dependant ID (lt24: address -> lt24_id, skylines: address -> skylines_id,
|
||||
spider: id_spider -> spider_registration, address -> spider_id, spot: address -> spot_id) (fixes #64)
|
||||
- client: Added keyword arguments for the callback function in the 'run' method of the client
|
||||
|
||||
## 0.9.7: - 2020-05-21
|
||||
- parser: Added support for OGPAW (PilotAware) beacons
|
||||
- client: Dropped compatibility for Python 3.4
|
||||
|
||||
## 0.9.6: - 2020-01-17
|
||||
- parser: Better support for OGFLR beacons from PilotAware
|
||||
- client: Allow dynamic settings override with optional "settings" parameter
|
||||
|
||||
## 0.9.5: - 2019-09-07
|
||||
- parser: fixed telnet parser
|
||||
|
||||
## 0.9.4: - 2019-06-10
|
||||
- parser: Added support for OGINREACH (Garmin inReach) beacons
|
||||
- parser: Added support for OGFLYM (Flymaster) beacons
|
||||
- parser: Added support for comments in tracker beacons (OGNTRK)
|
||||
- parser: Added support for OGCAPT (Capturs) beacons
|
||||
|
||||
## 0.9.3: - 2019-06-03
|
||||
- parser: Added Generic parser for unknown formats
|
||||
|
||||
## 0.9.2: - 2019-05-07
|
||||
- parser: Exception handling for bad OGNTRK beacons
|
||||
|
||||
## 0.9.1: - 2018-09-18
|
||||
- parser: Fixed SPOT beacons and Tracker beacons
|
||||
- parser: Fixed kph to ms conversion
|
||||
- client: Catch ConnectionResetError
|
||||
|
||||
## 0.9.0: - 2018-05-14
|
||||
- parser: Added support for OGNLT24 (LT24), OGSKYL (Skylines), OGSPID (Spider), OGSPOT (Spot) and OGNFNT (Fanet)
|
||||
- parser: Added support for (server) comments
|
||||
- parser: Added parser for local receiver output (port 50001)
|
||||
- parser: Changed unit for rotation from "half turn per minute" to "degrees/s"
|
||||
|
||||
## 0.8.2: - 2018-01-20
|
||||
- parser: Better validation of timestamp, lat/lon and altitude
|
||||
|
||||
## 0.8.1: - 2018-01-12
|
||||
- client: Ignore messages other than UTF-8
|
||||
- parser: Allow IDs only with hexadecimal values
|
||||
|
||||
## 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 ddhhmm time format (eg. '312359z')
|
||||
- parser: Added support for heared aircrafts
|
||||
- client: Allow client to do sequential connect-disconnect
|
||||
|
||||
|
|
48
README.md
48
README.md
|
@ -10,33 +10,40 @@ It can be used to connect to the OGN-APRS-Servers and to parse APRS-/OGN-Message
|
|||
A full featured gateway with build-in database is provided by [ogn-python](https://github.com/glidernet/ogn-python).
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
python-ogn-client is available at PyPI. So for installation simply use pip:
|
||||
|
||||
```
|
||||
pip install ogn-client
|
||||
```
|
||||
|
||||
## Example Usage
|
||||
|
||||
Parse APRS/OGN packet.
|
||||
### Parse APRS/OGN packet.
|
||||
|
||||
```
|
||||
```python
|
||||
from ogn.parser import parse
|
||||
from datetime import date, time
|
||||
from datetime import datetime
|
||||
|
||||
beacon = parse("FLRDDDEAD>APRS,qAS,EDER:/114500h5029.86N/00956.98E'342/049/A=005524 id0ADDDEAD -454fpm -1.1rot 8.8dB 0e +51.2kHz gps4x5",
|
||||
reference_date=date(2016,1,1), reference_time=time(11,46))
|
||||
reference_timestamp=datetime(2015, 7, 31, 12, 34, 56))
|
||||
```
|
||||
|
||||
Connect to OGN and display all incoming beacons.
|
||||
### Connect to OGN and display all incoming beacons.
|
||||
|
||||
```
|
||||
```python
|
||||
from ogn.client import AprsClient
|
||||
from ogn.parser import parse, ParseError
|
||||
|
||||
def process_beacon(raw_message):
|
||||
if raw_message[0] == '#':
|
||||
print('Server Status: {}'.format(raw_message))
|
||||
return
|
||||
try:
|
||||
beacon = parse(raw_message)
|
||||
print('Received {beacon_type} from {name}'.format(**beacon))
|
||||
print('Received {aprs_type}: {raw_message}'.format(**beacon))
|
||||
except ParseError as e:
|
||||
print('Error, {}'.format(e.message))
|
||||
except NotImplementedError as e:
|
||||
print('{}: {}'.format(e, raw_message))
|
||||
|
||||
client = AprsClient(aprs_user='N0CALL')
|
||||
client.connect()
|
||||
|
@ -48,5 +55,26 @@ except KeyboardInterrupt:
|
|||
client.disconnect()
|
||||
```
|
||||
|
||||
### Connect to telnet console and display all decoded beacons.
|
||||
|
||||
```python
|
||||
from ogn.client import TelnetClient
|
||||
from ogn.parser.telnet_parser import parse
|
||||
|
||||
def process_beacon(raw_message):
|
||||
beacon = parse(raw_message)
|
||||
if beacon:
|
||||
print(beacon)
|
||||
|
||||
client = TelnetClient()
|
||||
client.connect()
|
||||
|
||||
try:
|
||||
client.run(callback=process_beacon)
|
||||
except KeyboardInterrupt:
|
||||
print('\nStop ogn gateway')
|
||||
client.disconnect()
|
||||
```
|
||||
|
||||
## License
|
||||
Licensed under the [AGPLv3](LICENSE).
|
||||
|
|
|
@ -1 +1,10 @@
|
|||
from ogn.client.client import AprsClient # flake8: noqa
|
||||
from ogn.client.client import AprsClient # noqa: F401
|
||||
from ogn.client.client import TelnetClient # noqa: F401
|
||||
|
||||
|
||||
class CustomSettings(object):
|
||||
def __init__(self, **kw):
|
||||
self.kw = kw
|
||||
|
||||
def __getattr__(self, name):
|
||||
return self.kw[name]
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import socket
|
||||
import logging
|
||||
from time import time
|
||||
from time import time, sleep
|
||||
|
||||
from ogn.client import settings
|
||||
|
||||
|
@ -13,71 +13,129 @@ def create_aprs_login(user_name, pass_code, app_name, app_version, aprs_filter=N
|
|||
|
||||
|
||||
class AprsClient:
|
||||
def __init__(self, aprs_user, aprs_filter=''):
|
||||
def __init__(self, aprs_user, aprs_filter='', settings=settings):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.logger.info("Connect to OGN as {} with filter '{}'".format(aprs_user, (aprs_filter if aprs_filter else 'full-feed')))
|
||||
self.logger.addHandler(logging.NullHandler())
|
||||
|
||||
self.aprs_user = aprs_user
|
||||
self.aprs_filter = aprs_filter
|
||||
self.settings = settings
|
||||
|
||||
self._sock_peer_ip = None
|
||||
self._kill = False
|
||||
|
||||
def connect(self):
|
||||
def connect(self, retries=1, wait_period=15):
|
||||
# create socket, connect to server, login and make a file object associated with the socket
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
while retries > 0:
|
||||
retries -= 1
|
||||
try:
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
self.sock.settimeout(5)
|
||||
|
||||
if self.aprs_filter:
|
||||
port = settings.APRS_SERVER_PORT_CLIENT_DEFINED_FILTERS
|
||||
else:
|
||||
port = settings.APRS_SERVER_PORT_FULL_FEED
|
||||
if self.aprs_filter:
|
||||
port = self.settings.APRS_SERVER_PORT_CLIENT_DEFINED_FILTERS
|
||||
else:
|
||||
port = self.settings.APRS_SERVER_PORT_FULL_FEED
|
||||
|
||||
self.sock.connect((settings.APRS_SERVER_HOST, port))
|
||||
self.logger.debug('Server port {}'.format(port))
|
||||
self.sock.connect((self.settings.APRS_SERVER_HOST, port))
|
||||
self._sock_peer_ip = self.sock.getpeername()[0]
|
||||
|
||||
login = create_aprs_login(self.aprs_user, -1, settings.APRS_APP_NAME, settings.APRS_APP_VER, self.aprs_filter)
|
||||
self.sock.send(login.encode())
|
||||
self.sock_file = self.sock.makefile('rw')
|
||||
login = create_aprs_login(self.aprs_user, -1, self.settings.APRS_APP_NAME, self.settings.APRS_APP_VER, self.aprs_filter)
|
||||
self.sock.send(login.encode())
|
||||
self.sock_file = self.sock.makefile('rb')
|
||||
|
||||
self._kill = False
|
||||
self._kill = False
|
||||
self.logger.info("Connect to OGN ({}/{}:{}) as {} with filter: {}".
|
||||
format(self.settings.APRS_SERVER_HOST, self._sock_peer_ip, port, self.aprs_user,
|
||||
"'" + self.aprs_filter + "'" if self.aprs_filter else 'none (full-feed)'))
|
||||
break
|
||||
except (socket.error, ConnectionError) as e:
|
||||
self.logger.error('Connect error: {}'.format(e))
|
||||
if retries > 0:
|
||||
self.logger.info('Waiting {}s before next connection try ({} attempts left).'.format(wait_period, retries))
|
||||
sleep(wait_period)
|
||||
else:
|
||||
self._kill = True
|
||||
self.logger.critical('Could not connect to OGN.')
|
||||
|
||||
def disconnect(self):
|
||||
self.logger.info('Disconnect')
|
||||
self.logger.info('Disconnect from {}'.format(self._sock_peer_ip))
|
||||
try:
|
||||
# close everything
|
||||
self.sock.shutdown(0)
|
||||
self.sock.close()
|
||||
except OSError:
|
||||
self.logger.error('Socket close error', exc_info=True)
|
||||
self.logger.error('Socket close error')
|
||||
|
||||
self._kill = True
|
||||
|
||||
def run(self, callback, timed_callback=lambda client: None, autoreconnect=False):
|
||||
def run(self, callback, timed_callback=lambda client: None, autoreconnect=False, ignore_decoding_error=True,
|
||||
**kwargs):
|
||||
while not self._kill:
|
||||
try:
|
||||
keepalive_time = time()
|
||||
while not self._kill:
|
||||
if time() - keepalive_time > settings.APRS_KEEPALIVE_TIME:
|
||||
self.logger.info('Send keepalive')
|
||||
if time() - keepalive_time > self.settings.APRS_KEEPALIVE_TIME:
|
||||
self.logger.info('Send keepalive to {}'.format(self._sock_peer_ip))
|
||||
self.sock.send('#keepalive\n'.encode())
|
||||
timed_callback(self)
|
||||
keepalive_time = time()
|
||||
|
||||
# Read packet string from socket
|
||||
packet_str = self.sock_file.readline().strip()
|
||||
packet_b = self.sock_file.readline().strip()
|
||||
packet_str = packet_b.decode(errors="replace") if ignore_decoding_error else packet_b.decode()
|
||||
|
||||
# A zero length line should not be return if keepalives are being sent
|
||||
# A zero length line will only be returned after ~30m if keepalives are not sent
|
||||
if len(packet_str) == 0:
|
||||
self.logger.warning('Read returns zero length string. Failure. Orderly closeout')
|
||||
self.logger.warning('Read returns zero length string. Failure. Orderly closeout from {}'.
|
||||
format(self._sock_peer_ip))
|
||||
break
|
||||
|
||||
callback(packet_str)
|
||||
except BrokenPipeError:
|
||||
self.logger.error('BrokenPipeError', exc_info=True)
|
||||
callback(packet_str, **kwargs)
|
||||
except socket.error:
|
||||
self.logger.error('socket.error', exc_info=True)
|
||||
self.logger.error('socket.error')
|
||||
except OSError:
|
||||
self.logger.error('OSError')
|
||||
except UnicodeDecodeError:
|
||||
self.logger.error('UnicodeDecodeError')
|
||||
self.logger.debug(packet_b)
|
||||
|
||||
if autoreconnect and not self._kill:
|
||||
self.connect(retries=100)
|
||||
else:
|
||||
return
|
||||
|
||||
|
||||
class TelnetClient:
|
||||
def __init__(self, settings=settings):
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.logger.info("Connect to local telnet server")
|
||||
self.settings = settings
|
||||
|
||||
def connect(self):
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock.connect((self.settings.TELNET_SERVER_HOST, self.settings.TELNET_SERVER_PORT))
|
||||
|
||||
def run(self, callback, autoreconnect=False):
|
||||
while True:
|
||||
try:
|
||||
self.sock_file = self.sock.makefile(mode='rw', encoding='iso-8859-1')
|
||||
while True:
|
||||
packet_str = self.sock_file.readline().strip()
|
||||
callback(packet_str)
|
||||
|
||||
except ConnectionRefusedError:
|
||||
self.logger.error('Telnet server not running', exc_info=True)
|
||||
|
||||
if autoreconnect:
|
||||
sleep(1)
|
||||
self.connect()
|
||||
else:
|
||||
return
|
||||
|
||||
def disconnect(self):
|
||||
self.logger.info('Disconnect')
|
||||
self.sock.shutdown(0)
|
||||
self.sock.close()
|
||||
|
|
|
@ -4,7 +4,10 @@ APRS_SERVER_PORT_CLIENT_DEFINED_FILTERS = 14580
|
|||
|
||||
APRS_APP_NAME = 'python-ogn-client'
|
||||
|
||||
PACKAGE_VERSION = '0.8.0'
|
||||
PACKAGE_VERSION = '1.2.1'
|
||||
APRS_APP_VER = PACKAGE_VERSION[:3]
|
||||
|
||||
APRS_KEEPALIVE_TIME = 240
|
||||
|
||||
TELNET_SERVER_HOST = 'localhost'
|
||||
TELNET_SERVER_PORT = 50001
|
||||
|
|
|
@ -1 +1 @@
|
|||
from ogn.ddb.utils import get_ddb_devices # flake8: noqa
|
||||
from ogn.ddb.utils import get_ddb_devices # noqa: F401
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
from ogn.parser import parse as parse_module # only for test functions. Without this a mock of parse would mock the function instead of the module
|
||||
from ogn.parser.parse import parse, parse_aprs, parse_comment # flake8: noqa
|
||||
from ogn.parser.exceptions import ParseError, AprsParseError, OgnParseError, AmbigousTimeError # flake8: noqa
|
||||
from ogn.parser import parse as parse_module # noqa: F40 --- only for test functions. Without this a mock of parse would mock the function instead of the module
|
||||
from ogn.parser.parse import parse, parse_aprs, parse_comment # noqa: F401
|
||||
from ogn.parser.exceptions import ParseError, AprsParseError, OgnParseError # noqa: F401
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
class BaseParser():
|
||||
def __init__(self):
|
||||
self.beacon_type = 'unknown'
|
||||
|
||||
def parse(self, aprs_comment, aprs_type):
|
||||
if aprs_type.startswith('position'):
|
||||
data = self.parse_position(aprs_comment)
|
||||
elif aprs_type.startswith('status'):
|
||||
data = self.parse_status(aprs_comment)
|
||||
else:
|
||||
raise ValueError("aprs_type {} unknown".format(aprs_type))
|
||||
data.update({'beacon_type': self.beacon_type})
|
||||
return data
|
||||
|
||||
def parse_position(self, aprs_comment):
|
||||
raise NotImplementedError("Position parser for parser '{}' not yet implemented".format(self.beacon_type))
|
||||
|
||||
def parse_status(self, aprs_comment):
|
||||
raise NotImplementedError("Status parser for parser '{}' not yet implemented".format(self.beacon_type))
|
|
@ -0,0 +1,34 @@
|
|||
from ogn.parser.utils import FPM_TO_MS
|
||||
from ogn.parser.pattern import PATTERN_FANET_POSITION_COMMENT, PATTERN_FANET_STATUS_COMMENT
|
||||
|
||||
from .base import BaseParser
|
||||
|
||||
|
||||
class FanetParser(BaseParser):
|
||||
def __init__(self):
|
||||
self.beacon_type = 'fanet'
|
||||
self.position_pattern = PATTERN_FANET_POSITION_COMMENT
|
||||
self.status_pattern = PATTERN_FANET_STATUS_COMMENT
|
||||
|
||||
def parse_position(self, aprs_comment):
|
||||
match = self.position_pattern.match(aprs_comment)
|
||||
result = {}
|
||||
if match.group('details'):
|
||||
result.update({
|
||||
'address_type': int(match.group('details'), 16) & 0b00000011,
|
||||
'aircraft_type': (int(match.group('details'), 16) & 0b01111100) >> 2,
|
||||
'stealth': (int(match.group('details'), 16) & 0b10000000) >> 7 == 1,
|
||||
'address': match.group('address')
|
||||
})
|
||||
if match.group('climb_rate'): result['climb_rate'] = int(match.group('climb_rate')) * FPM_TO_MS
|
||||
return result
|
||||
|
||||
def parse_status(self, aprs_comment):
|
||||
match = self.status_pattern.match(aprs_comment)
|
||||
result = {}
|
||||
if match.group('fanet_name'): result['fanet_name'] = match.group('fanet_name')
|
||||
if match.group('signal_quality'): result['signal_quality'] = float(match.group('signal_quality'))
|
||||
if match.group('frequency_offset'): result['frequency_offset'] = float(match.group('frequency_offset'))
|
||||
if match.group('error_count'): result['error_count'] = int(match.group('error_count'))
|
||||
|
||||
return result
|
|
@ -0,0 +1,40 @@
|
|||
from ogn.parser.pattern import PATTERN_FLARM_POSITION_COMMENT
|
||||
from ogn.parser.utils import FPM_TO_MS, HPM_TO_DEGS
|
||||
|
||||
from .base import BaseParser
|
||||
|
||||
|
||||
class FlarmParser(BaseParser):
|
||||
def __init__(self):
|
||||
self.beacon_type = 'flarm'
|
||||
self.position_pattern = PATTERN_FLARM_POSITION_COMMENT
|
||||
|
||||
def parse_position(self, aprs_comment):
|
||||
match = self.position_pattern.match(aprs_comment)
|
||||
|
||||
result = {}
|
||||
if match.group('details'):
|
||||
result.update({
|
||||
'address_type': int(match.group('details'), 16) & 0b00000011,
|
||||
'aircraft_type': (int(match.group('details'), 16) & 0b00111100) >> 2,
|
||||
'no-tracking': (int(match.group('details'), 16) & 0b01000000) >> 6 == 1,
|
||||
'stealth': (int(match.group('details'), 16) & 0b10000000) >> 7 == 1,
|
||||
'address': match.group('address'),
|
||||
})
|
||||
if match.group('climb_rate'): result['climb_rate'] = int(match.group('climb_rate')) * FPM_TO_MS
|
||||
if match.group('turn_rate'): result['turn_rate'] = float(match.group('turn_rate')) * HPM_TO_DEGS
|
||||
if match.group('signal_quality'): result['signal_quality'] = float(match.group('signal_quality'))
|
||||
if match.group('error_count'): result['error_count'] = int(match.group('error_count'))
|
||||
if match.group('frequency_offset'): result['frequency_offset'] = float(match.group('frequency_offset'))
|
||||
if match.group('gps_quality'):
|
||||
result.update({
|
||||
'gps_quality': {
|
||||
'horizontal': int(match.group('gps_quality_horizontal')),
|
||||
'vertical': int(match.group('gps_quality_vertical'))
|
||||
}
|
||||
})
|
||||
if match.group('software_version'): result['software_version'] = float(match.group('software_version'))
|
||||
if match.group('hardware_version'): result['hardware_version'] = int(match.group('hardware_version'), 16)
|
||||
if match.group('real_address'): result['real_address'] = match.group('real_address')
|
||||
if match.group('signal_power'): result['signal_power'] = float(match.group('signal_power'))
|
||||
return result
|
|
@ -0,0 +1,12 @@
|
|||
from .base import BaseParser
|
||||
|
||||
|
||||
class GenericParser(BaseParser):
|
||||
def __init__(self, beacon_type='unknown'):
|
||||
self.beacon_type = beacon_type
|
||||
|
||||
def parse_position(self, aprs_comment):
|
||||
return {'comment': aprs_comment}
|
||||
|
||||
def parse_status(self, aprs_comment):
|
||||
return {'comment': aprs_comment}
|
|
@ -0,0 +1,16 @@
|
|||
from ogn.parser.pattern import PATTERN_INREACH_POSITION_COMMENT
|
||||
|
||||
from .base import BaseParser
|
||||
|
||||
|
||||
class InreachParser(BaseParser):
|
||||
def __init__(self):
|
||||
self.beacon_type = 'inreach'
|
||||
self.position_pattern = PATTERN_INREACH_POSITION_COMMENT
|
||||
|
||||
def parse_position(self, aprs_comment):
|
||||
match = self.position_pattern.match(aprs_comment)
|
||||
return {'address': match.group('id'),
|
||||
'model': match.group('model') if match.group('model') else None,
|
||||
'status': match.group('status') == 'True' if match.group('status') else None,
|
||||
'pilot_name': match.group('pilot_name') if match.group('pilot_name') else None}
|
|
@ -0,0 +1,16 @@
|
|||
from ogn.parser.utils import FPM_TO_MS
|
||||
from ogn.parser.pattern import PATTERN_LT24_POSITION_COMMENT
|
||||
|
||||
from .base import BaseParser
|
||||
|
||||
|
||||
class LT24Parser(BaseParser):
|
||||
def __init__(self):
|
||||
self.beacon_type = 'lt24'
|
||||
self.position_pattern = PATTERN_LT24_POSITION_COMMENT
|
||||
|
||||
def parse_position(self, aprs_comment):
|
||||
match = self.position_pattern.match(aprs_comment)
|
||||
return {'lt24_id': match.group('lt24_id'),
|
||||
'climb_rate': int(match.group('climb_rate')) * FPM_TO_MS if match.group('climb_rate') else None,
|
||||
'source': match.group('source') if match.group('source') else None}
|
|
@ -0,0 +1,21 @@
|
|||
from ogn.parser.pattern import PATTERN_NAVITER_POSITION_COMMENT
|
||||
from ogn.parser.utils import FPM_TO_MS, HPM_TO_DEGS
|
||||
|
||||
from .base import BaseParser
|
||||
|
||||
|
||||
class NaviterParser(BaseParser):
|
||||
def __init__(self):
|
||||
self.beacon_type = 'naviter'
|
||||
self.position_pattern = PATTERN_NAVITER_POSITION_COMMENT
|
||||
|
||||
def parse_position(self, aprs_comment):
|
||||
match = self.position_pattern.match(aprs_comment)
|
||||
return {'stealth': (int(match.group('details'), 16) & 0b1000000000000000) >> 15 == 1,
|
||||
'do_not_track': (int(match.group('details'), 16) & 0b0100000000000000) >> 14 == 1,
|
||||
'aircraft_type': (int(match.group('details'), 16) & 0b0011110000000000) >> 10,
|
||||
'address_type': (int(match.group('details'), 16) & 0b0000001111110000) >> 4,
|
||||
'reserved': (int(match.group('details'), 16) & 0b0000000000001111),
|
||||
'address': match.group('address'),
|
||||
'climb_rate': int(match.group('climb_rate')) * FPM_TO_MS if match.group('climb_rate') else None,
|
||||
'turn_rate': float(match.group('turn_rate')) * HPM_TO_DEGS if match.group('turn_rate') else None}
|
|
@ -0,0 +1,92 @@
|
|||
from ogn.parser.utils import FPM_TO_MS, HPM_TO_DEGS
|
||||
from ogn.parser.pattern import PATTERN_RECEIVER_BEACON, PATTERN_AIRCRAFT_BEACON
|
||||
|
||||
from .base import BaseParser
|
||||
|
||||
|
||||
class OgnParser(BaseParser):
|
||||
def __init__(self):
|
||||
self.beacon_type = None
|
||||
self.aircraft_pattern = PATTERN_AIRCRAFT_BEACON
|
||||
self.receiver_pattern = PATTERN_RECEIVER_BEACON
|
||||
|
||||
def parse(self, aprs_comment, aprs_type):
|
||||
if not aprs_comment:
|
||||
return {'beacon_type': 'aprs_receiver'}
|
||||
|
||||
ab_data = self.parse_aircraft_beacon(aprs_comment)
|
||||
if ab_data:
|
||||
ab_data.update({'beacon_type': 'aprs_aircraft'})
|
||||
return ab_data
|
||||
|
||||
rb_data = self.parse_receiver_beacon(aprs_comment)
|
||||
if rb_data:
|
||||
rb_data.update({'beacon_type': 'aprs_receiver'})
|
||||
return rb_data
|
||||
else:
|
||||
return {'user_comment': aprs_comment,
|
||||
'beacon_type': 'aprs_receiver'}
|
||||
|
||||
def parse_aircraft_beacon(self, aprs_comment):
|
||||
match = self.aircraft_pattern.match(aprs_comment)
|
||||
if match:
|
||||
result = {}
|
||||
if match.group('details'):
|
||||
result.update({
|
||||
'address_type': int(match.group('details'), 16) & 0b00000011,
|
||||
'aircraft_type': (int(match.group('details'), 16) & 0b00111100) >> 2,
|
||||
'no-tracking': (int(match.group('details'), 16) & 0b01000000) >> 6 == 1,
|
||||
'stealth': (int(match.group('details'), 16) & 0b10000000) >> 7 == 1,
|
||||
'address': match.group('address'),
|
||||
})
|
||||
if match.group('climb_rate'): result['climb_rate'] = int(match.group('climb_rate')) * FPM_TO_MS
|
||||
if match.group('turn_rate'): result['turn_rate'] = float(match.group('turn_rate')) * HPM_TO_DEGS
|
||||
if match.group('flight_level'): result['flightlevel'] = float(match.group('flight_level'))
|
||||
if match.group('signal_quality'): result['signal_quality'] = float(match.group('signal_quality'))
|
||||
if match.group('errors'): result['error_count'] = int(match.group('errors'))
|
||||
if match.group('frequency_offset'): result['frequency_offset'] = float(match.group('frequency_offset'))
|
||||
if match.group('gps_quality'):
|
||||
result.update({
|
||||
'gps_quality': {
|
||||
'horizontal': int(match.group('gps_quality_horizontal')),
|
||||
'vertical': int(match.group('gps_quality_vertical'))
|
||||
}
|
||||
})
|
||||
if match.group('flarm_software_version'): result['software_version'] = float(match.group('flarm_software_version'))
|
||||
if match.group('flarm_hardware_version'): result['hardware_version'] = int(match.group('flarm_hardware_version'), 16)
|
||||
if match.group('flarm_id'): result['real_address'] = match.group('flarm_id')
|
||||
if match.group('signal_power'): result['signal_power'] = float(match.group('signal_power'))
|
||||
if match.group('proximity'): result['proximity'] = [hear[4:] for hear in match.group('proximity').split(' ')]
|
||||
return result
|
||||
else:
|
||||
return None
|
||||
|
||||
def parse_receiver_beacon(self, aprs_comment):
|
||||
match = self.receiver_pattern.match(aprs_comment)
|
||||
if match:
|
||||
result = {
|
||||
'version': match.group('version'),
|
||||
'platform': match.group('platform'),
|
||||
'cpu_load': float(match.group('cpu_load')),
|
||||
'free_ram': float(match.group('ram_free')),
|
||||
'total_ram': float(match.group('ram_total')),
|
||||
'ntp_error': float(match.group('ntp_offset')),
|
||||
'rt_crystal_correction': float(match.group('ntp_correction'))
|
||||
}
|
||||
if match.group('voltage'): result['voltage'] = float(match.group('voltage'))
|
||||
if match.group('amperage'): result['amperage'] = float(match.group('amperage'))
|
||||
if match.group('cpu_temperature'): result['cpu_temp'] = float(match.group('cpu_temperature'))
|
||||
if match.group('visible_senders'): result['senders_visible'] = int(match.group('visible_senders'))
|
||||
if match.group('senders'): result['senders_total'] = int(match.group('senders'))
|
||||
if match.group('latency'): result['latency'] = float(match.group('latency'))
|
||||
if match.group('rf_correction_manual'): result['rec_crystal_correction'] = int(match.group('rf_correction_manual'))
|
||||
if match.group('rf_correction_automatic'): result['rec_crystal_correction_fine'] = float(match.group('rf_correction_automatic'))
|
||||
if match.group('signal_quality'): result['rec_input_noise'] = float(match.group('signal_quality'))
|
||||
if match.group('senders_signal_quality'): result['senders_signal'] = float(match.group('senders_signal_quality'))
|
||||
if match.group('senders_messages'): result['senders_messages'] = float(match.group('senders_messages'))
|
||||
if match.group('good_senders_signal_quality'): result['good_senders_signal'] = float(match.group('good_senders_signal_quality'))
|
||||
if match.group('good_senders'): result['good_senders'] = float(match.group('good_senders'))
|
||||
if match.group('good_and_bad_senders'): result['good_and_bad_senders'] = float(match.group('good_and_bad_senders'))
|
||||
return result
|
||||
else:
|
||||
return None
|
|
@ -0,0 +1,45 @@
|
|||
from ogn.parser.pattern import PATTERN_RECEIVER_POSITION_COMMENT, PATTERN_RECEIVER_STATUS_COMMENT
|
||||
|
||||
from .base import BaseParser
|
||||
|
||||
|
||||
class ReceiverParser(BaseParser):
|
||||
def __init__(self):
|
||||
self.beacon_type = 'receiver'
|
||||
self.position_pattern = PATTERN_RECEIVER_POSITION_COMMENT
|
||||
self.status_pattern = PATTERN_RECEIVER_STATUS_COMMENT
|
||||
|
||||
def parse_position(self, aprs_comment):
|
||||
if aprs_comment is None:
|
||||
return {}
|
||||
else:
|
||||
match = self.position_pattern.match(aprs_comment)
|
||||
return {'user_comment': match.group('user_comment') if match.group('user_comment') else None}
|
||||
|
||||
def parse_status(self, aprs_comment):
|
||||
match = self.status_pattern.match(aprs_comment)
|
||||
result = {
|
||||
'version': match.group('version'),
|
||||
'platform': match.group('platform'),
|
||||
'cpu_load': float(match.group('cpu_load')),
|
||||
'free_ram': float(match.group('ram_free')),
|
||||
'total_ram': float(match.group('ram_total')),
|
||||
'ntp_error': float(match.group('ntp_offset')),
|
||||
}
|
||||
|
||||
if match.group('ntp_correction'): result['rt_crystal_correction'] = float(match.group('ntp_correction'))
|
||||
if match.group('voltage'): result['voltage'] = float(match.group('voltage'))
|
||||
if match.group('amperage'): result['amperage'] = float(match.group('amperage'))
|
||||
if match.group('cpu_temperature'): result['cpu_temp'] = float(match.group('cpu_temperature'))
|
||||
if match.group('visible_senders'): result['senders_visible'] = int(match.group('visible_senders'))
|
||||
if match.group('senders'): result['senders_total'] = int(match.group('senders'))
|
||||
if match.group('rf_correction_manual'): result['rec_crystal_correction'] = int(match.group('rf_correction_manual'))
|
||||
if match.group('rf_correction_automatic'): result['rec_crystal_correction_fine'] = float(match.group('rf_correction_automatic'))
|
||||
if match.group('signal_quality'): result['rec_input_noise'] = float(match.group('signal_quality'))
|
||||
if match.group('senders_signal_quality'): result['senders_signal'] = float(match.group('senders_signal_quality'))
|
||||
if match.group('senders_messages'): result['senders_messages'] = float(match.group('senders_messages'))
|
||||
if match.group('good_senders_signal_quality'): result['good_senders_signal'] = float(match.group('good_senders_signal_quality'))
|
||||
if match.group('good_senders'): result['good_senders'] = float(match.group('good_senders'))
|
||||
if match.group('good_and_bad_senders'): result['good_and_bad_senders'] = float(match.group('good_and_bad_senders'))
|
||||
|
||||
return result
|
|
@ -0,0 +1,32 @@
|
|||
from ogn.parser.utils import FPM_TO_MS
|
||||
from ogn.parser.pattern import PATTERN_SAFESKY_POSITION_COMMENT
|
||||
|
||||
from .base import BaseParser
|
||||
|
||||
|
||||
class SafeskyParser(BaseParser):
|
||||
def __init__(self):
|
||||
self.beacon_type = 'safesky'
|
||||
self.position_pattern = PATTERN_SAFESKY_POSITION_COMMENT
|
||||
|
||||
def parse_position(self, aprs_comment):
|
||||
match = self.position_pattern.match(aprs_comment)
|
||||
result = dict()
|
||||
if match.group('details'):
|
||||
result.update({
|
||||
'address_type': int(match.group('details'), 16) & 0b00000011,
|
||||
'aircraft_type': (int(match.group('details'), 16) & 0b00111100) >> 2,
|
||||
'no-tracking': (int(match.group('details'), 16) & 0b01000000) >> 6 == 1,
|
||||
'stealth': (int(match.group('details'), 16) & 0b10000000) >> 7 == 1,
|
||||
'address': match.group('address'),
|
||||
})
|
||||
result.update(
|
||||
{'climb_rate': int(match.group('climb_rate')) * FPM_TO_MS if match.group('climb_rate') else None})
|
||||
if match.group('gps_quality'):
|
||||
result.update({
|
||||
'gps_quality': {
|
||||
'horizontal': int(match.group('gps_quality_horizontal')),
|
||||
'vertical': int(match.group('gps_quality_vertical'))
|
||||
}
|
||||
})
|
||||
return result
|
|
@ -0,0 +1,15 @@
|
|||
from ogn.parser.utils import FPM_TO_MS
|
||||
from ogn.parser.pattern import PATTERN_SKYLINES_POSITION_COMMENT
|
||||
|
||||
from .base import BaseParser
|
||||
|
||||
|
||||
class SkylinesParser(BaseParser):
|
||||
def __init__(self):
|
||||
self.beacon_type = 'skylines'
|
||||
self.position_pattern = PATTERN_SKYLINES_POSITION_COMMENT
|
||||
|
||||
def parse_position(self, aprs_comment):
|
||||
match = self.position_pattern.match(aprs_comment)
|
||||
return {'skylines_id': match.group('skylines_id'),
|
||||
'climb_rate': int(match.group('climb_rate')) * FPM_TO_MS if match.group('climb_rate') else None}
|
|
@ -0,0 +1,16 @@
|
|||
from ogn.parser.pattern import PATTERN_SPIDER_POSITION_COMMENT
|
||||
|
||||
from .base import BaseParser
|
||||
|
||||
|
||||
class SpiderParser(BaseParser):
|
||||
def __init__(self):
|
||||
self.beacon_type = 'spider'
|
||||
self.position_pattern = PATTERN_SPIDER_POSITION_COMMENT
|
||||
|
||||
def parse_position(self, aprs_comment):
|
||||
match = self.position_pattern.match(aprs_comment)
|
||||
return {'spider_id': match.group('spider_id'),
|
||||
'signal_power': int(match.group('signal_power')) if match.group('signal_power') else None,
|
||||
'spider_registration': match.group('spider_registration') if match.group('spider_registration') else None,
|
||||
'gps_quality': match.group('gps_quality') if match.group('gps_quality') else None}
|
|
@ -0,0 +1,15 @@
|
|||
from ogn.parser.pattern import PATTERN_SPOT_POSITION_COMMENT
|
||||
|
||||
from .base import BaseParser
|
||||
|
||||
|
||||
class SpotParser(BaseParser):
|
||||
def __init__(self):
|
||||
self.beacon_type = 'spot'
|
||||
self.position_pattern = PATTERN_SPOT_POSITION_COMMENT
|
||||
|
||||
def parse_position(self, aprs_comment):
|
||||
match = self.position_pattern.match(aprs_comment)
|
||||
return {'spot_id': match.group('spot_id'),
|
||||
'model': match.group('model') if match.group('model') else None,
|
||||
'status': match.group('status') if match.group('status') else None}
|
|
@ -0,0 +1,59 @@
|
|||
from ogn.parser.pattern import PATTERN_TRACKER_POSITION_COMMENT, PATTERN_TRACKER_STATUS_COMMENT
|
||||
from ogn.parser.utils import FPM_TO_MS, HPM_TO_DEGS
|
||||
|
||||
from .base import BaseParser
|
||||
|
||||
|
||||
class TrackerParser(BaseParser):
|
||||
def __init__(self):
|
||||
self.beacon_type = 'tracker'
|
||||
self.position_pattern = PATTERN_TRACKER_POSITION_COMMENT
|
||||
self.status_pattern = PATTERN_TRACKER_STATUS_COMMENT
|
||||
|
||||
def parse_position(self, aprs_comment):
|
||||
match = self.position_pattern.match(aprs_comment)
|
||||
|
||||
result = {}
|
||||
if match.group('details'):
|
||||
result.update({
|
||||
'address_type': int(match.group('details'), 16) & 0b00000011,
|
||||
'aircraft_type': (int(match.group('details'), 16) & 0b01111100) >> 2,
|
||||
'stealth': (int(match.group('details'), 16) & 0b10000000) >> 7 == 1,
|
||||
'address': match.group('address'),
|
||||
})
|
||||
if match.group('climb_rate'): result['climb_rate'] = int(match.group('climb_rate')) * FPM_TO_MS
|
||||
if match.group('turn_rate'): result['turn_rate'] = float(match.group('turn_rate')) * HPM_TO_DEGS
|
||||
if match.group('flight_level'): result['flightlevel'] = float(match.group('flight_level'))
|
||||
if match.group('signal_quality'): result['signal_quality'] = float(match.group('signal_quality'))
|
||||
if match.group('error_count'): result['error_count'] = int(match.group('error_count'))
|
||||
if match.group('frequency_offset'): result['frequency_offset'] = float(match.group('frequency_offset'))
|
||||
if match.group('gps_quality'):
|
||||
result.update({
|
||||
'gps_quality': {
|
||||
'horizontal': int(match.group('gps_quality_horizontal')),
|
||||
'vertical': int(match.group('gps_quality_vertical'))
|
||||
}
|
||||
})
|
||||
if match.group('signal_power'): result['signal_power'] = float(match.group('signal_power'))
|
||||
return result
|
||||
|
||||
def parse_status(self, aprs_comment):
|
||||
match = self.status_pattern.match(aprs_comment)
|
||||
if match:
|
||||
result = {}
|
||||
|
||||
if match.group('hardware_version'): result['hardware_version'] = int(match.group('hardware_version'))
|
||||
if match.group('software_version'): result['software_version'] = int(match.group('software_version'))
|
||||
if match.group('gps_satellites'): result['gps_satellites'] = int(match.group('gps_satellites'))
|
||||
if match.group('gps_quality'): result['gps_quality'] = int(match.group('gps_quality'))
|
||||
if match.group('gps_altitude'): result['gps_altitude'] = int(match.group('gps_altitude'))
|
||||
if match.group('pressure'): result['pressure'] = float(match.group('pressure'))
|
||||
if match.group('temperature'): result['temperature'] = float(match.group('temperature'))
|
||||
if match.group('humidity'): result['humidity'] = int(match.group('humidity'))
|
||||
if match.group('voltage'): result['voltage'] = float(match.group('voltage'))
|
||||
if match.group('transmitter_power'): result['transmitter_power'] = int(match.group('transmitter_power'))
|
||||
if match.group('noise_level'): result['noise_level'] = float(match.group('noise_level'))
|
||||
if match.group('relays'): result['relays'] = int(match.group('relays'))
|
||||
return result
|
||||
else:
|
||||
return {'comment': aprs_comment}
|
|
@ -1,7 +1,6 @@
|
|||
"""
|
||||
exception definitions
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class ParseError(Exception):
|
||||
|
@ -24,14 +23,3 @@ class OgnParseError(ParseError):
|
|||
|
||||
self.message = "This is not a valid OGN message: {}".format(aprs_comment)
|
||||
super(OgnParseError, self).__init__(self.message)
|
||||
|
||||
|
||||
class AmbigousTimeError(ParseError):
|
||||
"""Timstamp from the past/future, can't fully reconstruct datetime from timestamp."""
|
||||
def __init__(self, reference, packet_time):
|
||||
self.reference = reference
|
||||
self.packet_time = packet_time
|
||||
self.timedelta = reference - datetime.combine(reference, packet_time)
|
||||
|
||||
self.message = "Can't reconstruct timstamp, {:.0f}s from past.".format(self.timedelta.total_seconds())
|
||||
super(AmbigousTimeError, self).__init__(self.message)
|
||||
|
|
|
@ -1,121 +1,178 @@
|
|||
import re
|
||||
from datetime import datetime
|
||||
|
||||
from ogn.parser.utils import createTimestamp, parseAngle, kts2kmh, feet2m
|
||||
from ogn.parser.pattern import PATTERN_APRS_POSITION, PATTERN_APRS_STATUS
|
||||
from ogn.parser.exceptions import AprsParseError, OgnParseError
|
||||
from ogn.parser.utils import createTimestamp, parseAngle, KNOTS_TO_MS, KPH_TO_MS, FEETS_TO_METER, INCH_TO_MM, fahrenheit_to_celsius, CheapRuler, normalized_quality
|
||||
from ogn.parser.pattern import PATTERN_APRS, PATTERN_APRS_POSITION, PATTERN_APRS_POSITION_WEATHER, PATTERN_APRS_STATUS, PATTERN_SERVER
|
||||
from ogn.parser.exceptions import AprsParseError
|
||||
|
||||
from ogn.parser.parse_ogn import parse_aircraft_beacon, parse_receiver_beacon
|
||||
from ogn.parser.parse_naviter import parse as parse_naviter_beacon
|
||||
from ogn.parser.parse_lt24 import parse as parse_lt24_beacon
|
||||
from ogn.parser.parse_spider import parse as parse_spider_beacon
|
||||
from ogn.parser.parse_spot import parse as parse_spot_beacon
|
||||
from ogn.parser.parse_skylines import parse as parse_skylines_beacon
|
||||
from ogn.parser.parse_tracker import parse_position as parse_tracker_position
|
||||
from ogn.parser.parse_tracker import parse_status as parse_tracker_status
|
||||
from ogn.parser.parse_receiver import parse_position as parse_receiver_position
|
||||
from ogn.parser.parse_receiver import parse_status as parse_receiver_status
|
||||
from ogn.parser.aprs_comment.ogn_parser import OgnParser
|
||||
from ogn.parser.aprs_comment.fanet_parser import FanetParser
|
||||
from ogn.parser.aprs_comment.lt24_parser import LT24Parser
|
||||
from ogn.parser.aprs_comment.naviter_parser import NaviterParser
|
||||
from ogn.parser.aprs_comment.flarm_parser import FlarmParser
|
||||
from ogn.parser.aprs_comment.tracker_parser import TrackerParser
|
||||
from ogn.parser.aprs_comment.receiver_parser import ReceiverParser
|
||||
from ogn.parser.aprs_comment.skylines_parser import SkylinesParser
|
||||
from ogn.parser.aprs_comment.spider_parser import SpiderParser
|
||||
from ogn.parser.aprs_comment.spot_parser import SpotParser
|
||||
from ogn.parser.aprs_comment.inreach_parser import InreachParser
|
||||
from ogn.parser.aprs_comment.safesky_parser import SafeskyParser
|
||||
from ogn.parser.aprs_comment.generic_parser import GenericParser
|
||||
|
||||
positions = {}
|
||||
server_timestamp = None
|
||||
|
||||
|
||||
def parse(aprs_message, reference_date=None, reference_time=None):
|
||||
if reference_date is None:
|
||||
now = datetime.utcnow()
|
||||
reference_date = now.date()
|
||||
reference_time = now.time()
|
||||
def parse(aprs_message, reference_timestamp=None, calculate_relations=False, use_server_timestamp=True):
|
||||
global positions
|
||||
global server_timestamp
|
||||
|
||||
if use_server_timestamp is True:
|
||||
reference_timestamp = server_timestamp or datetime.utcnow()
|
||||
elif reference_timestamp is None:
|
||||
reference_timestamp = datetime.utcnow()
|
||||
|
||||
message = parse_aprs(aprs_message, reference_timestamp=reference_timestamp)
|
||||
if message['aprs_type'] == 'position' or message['aprs_type'] == 'status':
|
||||
message.update(parse_comment(message['comment'],
|
||||
dstcall=message['dstcall'],
|
||||
aprs_type=message['aprs_type']))
|
||||
|
||||
if message['aprs_type'].startswith('position') and calculate_relations is True:
|
||||
positions[message['name']] = (message['longitude'], message['latitude'])
|
||||
if message['receiver_name'] in positions:
|
||||
cheap_ruler = CheapRuler((message['latitude'] + positions[message['receiver_name']][1]) / 2.0)
|
||||
message['distance'] = cheap_ruler.distance((message['longitude'], message['latitude']), positions[message['receiver_name']])
|
||||
message['bearing'] = cheap_ruler.bearing((message['longitude'], message['latitude']), positions[message['receiver_name']])
|
||||
message['normalized_quality'] = normalized_quality(message['distance'], message['signal_quality']) if 'signal_quality' in message else None
|
||||
|
||||
if message['aprs_type'] == 'server':
|
||||
server_timestamp = message['timestamp']
|
||||
|
||||
message = parse_aprs(aprs_message, reference_date, reference_time)
|
||||
message.update(parse_comment(message['comment'], dstcall=message['dstcall'], aprs_type=message['aprs_type']))
|
||||
return message
|
||||
|
||||
|
||||
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'),
|
||||
'dstcall': match_position.group('dstcall'),
|
||||
'relay': match_position.group('relay') if match_position.group('relay') else None,
|
||||
'receiver_name': match_position.group('receiver'),
|
||||
'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 None,
|
||||
'ground_speed': int(match_position.group('ground_speed')) * kts2kmh if match_position.group('ground_speed') else None,
|
||||
'altitude': int(match_position.group('altitude')) * feet2m,
|
||||
'comment': match_position.group('comment') if match_position.group('comment') else "",
|
||||
'aprs_type': 'position'}
|
||||
def parse_aprs(message, reference_timestamp=None):
|
||||
if reference_timestamp is None:
|
||||
reference_timestamp = datetime.utcnow()
|
||||
|
||||
match_status = re.search(PATTERN_APRS_STATUS, message)
|
||||
if match_status:
|
||||
return {'name': match_status.group('callsign'),
|
||||
'dstcall': match_status.group('dstcall'),
|
||||
'receiver_name': match_status.group('receiver'),
|
||||
'timestamp': createTimestamp(match_status.group('time'), reference_date, reference_time),
|
||||
'comment': match_status.group('comment') if match_status.group('comment') else "",
|
||||
'aprs_type': 'status'}
|
||||
result = {'raw_message': message,
|
||||
'reference_timestamp': reference_timestamp}
|
||||
|
||||
raise AprsParseError(message)
|
||||
|
||||
|
||||
def parse_comment(aprs_comment, dstcall="APRS", aprs_type="position"):
|
||||
if dstcall == "APRS": # this can be a receiver or an aircraft
|
||||
if not aprs_comment:
|
||||
return {'beacon_type': 'receiver_beacon'}
|
||||
|
||||
ac_data = parse_aircraft_beacon(aprs_comment)
|
||||
if ac_data:
|
||||
ac_data.update({'beacon_type': 'aircraft_beacon'})
|
||||
return ac_data
|
||||
|
||||
rc_data = parse_receiver_beacon(aprs_comment)
|
||||
if rc_data:
|
||||
rc_data.update({'beacon_type': 'receiver_beacon'})
|
||||
return rc_data
|
||||
if message and message[0] == '#':
|
||||
match_server = re.search(PATTERN_SERVER, message)
|
||||
if match_server:
|
||||
result.update({
|
||||
'version': match_server.group('version'),
|
||||
'timestamp': datetime.strptime(match_server.group('timestamp'), "%d %b %Y %H:%M:%S %Z"),
|
||||
'server': match_server.group('server'),
|
||||
'ip_address': match_server.group('ip_address'),
|
||||
'port': match_server.group('port'),
|
||||
'aprs_type': 'server'})
|
||||
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'})
|
||||
return ac_data
|
||||
elif dstcall == "OGNTRK":
|
||||
if aprs_type == "position":
|
||||
data = parse_tracker_position(aprs_comment)
|
||||
data.update({'beacon_type': 'aircraft_beacon'})
|
||||
elif aprs_type == "status":
|
||||
data = parse_tracker_status(aprs_comment)
|
||||
data.update({'beacon_type': 'aircraft_beacon'})
|
||||
return data
|
||||
elif dstcall == "OGNSDR":
|
||||
if aprs_type == "position":
|
||||
data = parse_receiver_position(aprs_comment)
|
||||
data.update({'beacon_type': 'receiver_beacon'})
|
||||
elif aprs_type == "status":
|
||||
data = parse_receiver_status(aprs_comment)
|
||||
data.update({'beacon_type': 'receiver_beacon'})
|
||||
return data
|
||||
elif dstcall == "OGLT24":
|
||||
ac_data = parse_lt24_beacon(aprs_comment)
|
||||
ac_data.update({'beacon_type': 'lt24_beacon'})
|
||||
return ac_data
|
||||
elif dstcall == "OGNAVI":
|
||||
ac_data = parse_naviter_beacon(aprs_comment)
|
||||
ac_data.update({'beacon_type': 'naviter_beacon'})
|
||||
return ac_data
|
||||
elif dstcall == "OGSKYL":
|
||||
ac_data = parse_skylines_beacon(aprs_comment)
|
||||
ac_data.update({'beacon_type': 'skylines_beacon'})
|
||||
return ac_data
|
||||
elif dstcall == "OGSPID":
|
||||
ac_data = parse_spider_beacon(aprs_comment)
|
||||
ac_data.update({'beacon_type': 'spider_beacon'})
|
||||
return ac_data
|
||||
elif dstcall == "OGSPOT":
|
||||
ac_data = parse_spot_beacon(aprs_comment)
|
||||
ac_data.update({'beacon_type': 'spot_beacon'})
|
||||
return ac_data
|
||||
result.update({
|
||||
'comment': message,
|
||||
'aprs_type': 'comment'})
|
||||
else:
|
||||
raise OgnParseError("No parser for dstcall {} found. APRS comment: {}".format(dstcall, aprs_comment))
|
||||
match = re.search(PATTERN_APRS, message)
|
||||
if match:
|
||||
aprs_type = 'position' if match.group('aprs_type') == '/' else 'status' if match.group('aprs_type') == '>' else 'unknown'
|
||||
result.update({'aprs_type': aprs_type})
|
||||
aprs_body = match.group('aprs_body')
|
||||
if aprs_type == 'position':
|
||||
match_position = re.search(PATTERN_APRS_POSITION, aprs_body)
|
||||
if match_position:
|
||||
result.update({
|
||||
'name': match.group('callsign'),
|
||||
'dstcall': match.group('dstcall'),
|
||||
'relay': match.group('relay') if match.group('relay') else None,
|
||||
'receiver_name': match.group('receiver'),
|
||||
'timestamp': createTimestamp(match_position.group('time'), reference_timestamp),
|
||||
'latitude': parseAngle('0' + match_position.group('latitude') + (match_position.group('latitude_enhancement') or '0')) * # noqa: W504
|
||||
(-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')) * # noqa: W504
|
||||
(-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 None,
|
||||
'ground_speed': int(match_position.group('ground_speed')) * KNOTS_TO_MS / KPH_TO_MS if match_position.group('ground_speed') else None,
|
||||
'altitude': int(match_position.group('altitude')) * FEETS_TO_METER if match_position.group('altitude') else None,
|
||||
|
||||
'comment': match_position.group('comment') if match_position.group('comment') else "",
|
||||
})
|
||||
return result
|
||||
|
||||
match_position_weather = re.search(PATTERN_APRS_POSITION_WEATHER, aprs_body)
|
||||
if match_position_weather:
|
||||
result.update({
|
||||
'aprs_type': 'position_weather',
|
||||
|
||||
'name': match.group('callsign'),
|
||||
'dstcall': match.group('dstcall'),
|
||||
'relay': match.group('relay') if match.group('relay') else None,
|
||||
'receiver_name': match.group('receiver'),
|
||||
'timestamp': createTimestamp(match_position_weather.group('time'), reference_timestamp),
|
||||
'latitude': parseAngle('0' + match_position_weather.group('latitude')) * # noqa: W504
|
||||
(-1 if match_position_weather.group('latitude_sign') == 'S' else 1),
|
||||
'symboltable': match_position_weather.group('symbol_table'),
|
||||
'longitude': parseAngle(match_position_weather.group('longitude')) * # noqa: W504
|
||||
(-1 if match_position_weather.group('longitude_sign') == 'W' else 1),
|
||||
'symbolcode': match_position_weather.group('symbol'),
|
||||
|
||||
'wind_direction': int(match_position_weather.group('wind_direction')) if match_position_weather.group('wind_direction') != '...' else None,
|
||||
'wind_speed': int(match_position_weather.group('wind_speed')) * KNOTS_TO_MS / KPH_TO_MS if match_position_weather.group('wind_speed') != '...' else None,
|
||||
'wind_speed_peak': int(match_position_weather.group('wind_speed_peak')) * KNOTS_TO_MS / KPH_TO_MS if match_position_weather.group('wind_speed_peak') != '...' else None,
|
||||
'temperature': fahrenheit_to_celsius(float(match_position_weather.group('temperature'))) if match_position_weather.group('temperature') != '...' else None,
|
||||
'rainfall_1h': int(match_position_weather.group('rainfall_1h')) / 100.0 * INCH_TO_MM if match_position_weather.group('rainfall_1h') else None,
|
||||
'rainfall_24h': int(match_position_weather.group('rainfall_24h')) / 100.0 * INCH_TO_MM if match_position_weather.group('rainfall_24h') else None,
|
||||
'humidity': int(match_position_weather.group('humidity')) * 0.01 if match_position_weather.group('humidity') else None,
|
||||
'barometric_pressure': int(match_position_weather.group('barometric_pressure')) if match_position_weather.group('barometric_pressure') else None,
|
||||
|
||||
'comment': match_position_weather.group('comment') if match_position_weather.group('comment') else "",
|
||||
})
|
||||
return result
|
||||
|
||||
raise AprsParseError(message)
|
||||
elif aprs_type == 'status':
|
||||
match_status = re.search(PATTERN_APRS_STATUS, aprs_body)
|
||||
if match_status:
|
||||
result.update({
|
||||
'name': match.group('callsign'),
|
||||
'dstcall': match.group('dstcall'),
|
||||
'receiver_name': match.group('receiver'),
|
||||
'timestamp': createTimestamp(match_status.group('time'), reference_timestamp),
|
||||
'comment': match_status.group('comment') if match_status.group('comment') else ""})
|
||||
else:
|
||||
raise NotImplementedError(message)
|
||||
else:
|
||||
raise AprsParseError(message)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
dstcall_parser_mapping = {'APRS': OgnParser(),
|
||||
'OGNFNT': FanetParser(),
|
||||
'OGFLR': FlarmParser(),
|
||||
'OGNTRK': TrackerParser(),
|
||||
'OGNSDR': ReceiverParser(),
|
||||
'OGCAPT': GenericParser(beacon_type='capturs'),
|
||||
'OGFLYM': GenericParser(beacon_type='flymaster'),
|
||||
'OGINRE': InreachParser(),
|
||||
'OGLT24': LT24Parser(),
|
||||
'OGNAVI': NaviterParser(),
|
||||
'OGPAW': GenericParser(beacon_type='pilot_aware'),
|
||||
'OGSKYL': SkylinesParser(),
|
||||
'OGSPID': SpiderParser(),
|
||||
'OGSPOT': SpotParser(),
|
||||
'OGNSKY': SafeskyParser(),
|
||||
'GENERIC': GenericParser(beacon_type='unknown'),
|
||||
}
|
||||
|
||||
|
||||
def parse_comment(aprs_comment, dstcall='APRS', aprs_type="position"):
|
||||
parser = dstcall_parser_mapping.get(dstcall)
|
||||
if parser:
|
||||
return parser.parse(aprs_comment, aprs_type)
|
||||
else:
|
||||
return dstcall_parser_mapping.get('GENERIC').parse(aprs_comment, aprs_type)
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
def parse(aprs_comment):
|
||||
raise NotImplementedError("LT24 beacon parser not yet implemented")
|
|
@ -1,16 +0,0 @@
|
|||
import re
|
||||
|
||||
from ogn.parser.utils import fpm2ms
|
||||
from ogn.parser.pattern import PATTERN_NAVITER_BEACON
|
||||
|
||||
|
||||
def parse(aprs_comment):
|
||||
match = re.search(PATTERN_NAVITER_BEACON, aprs_comment)
|
||||
return {'stealth': (int(match.group('details'), 16) & 0b1000000000000000) >> 15 == 1,
|
||||
'do_not_track': (int(match.group('details'), 16) & 0b0100000000000000) >> 14 == 1,
|
||||
'aircraft_type': (int(match.group('details'), 16) & 0b0011110000000000) >> 10,
|
||||
'address_type': (int(match.group('details'), 16) & 0b0000001111110000) >> 4,
|
||||
'reserved': (int(match.group('details'), 16) & 0b0000000000001111),
|
||||
'address': match.group('id'),
|
||||
'climb_rate': int(match.group('climb_rate')) * fpm2ms if match.group('climb_rate') else None,
|
||||
'turn_rate': float(match.group('turn_rate')) if match.group('turn_rate') else None}
|
|
@ -1,54 +0,0 @@
|
|||
import re
|
||||
|
||||
from ogn.parser.utils import fpm2ms
|
||||
from ogn.parser.pattern import PATTERN_RECEIVER_BEACON, PATTERN_AIRCRAFT_BEACON
|
||||
|
||||
|
||||
def parse_aircraft_beacon(aprs_comment):
|
||||
ac_match = re.search(PATTERN_AIRCRAFT_BEACON, aprs_comment)
|
||||
if ac_match:
|
||||
return {'address_type': int(ac_match.group('details'), 16) & 0b00000011,
|
||||
'aircraft_type': (int(ac_match.group('details'), 16) & 0b01111100) >> 2,
|
||||
'stealth': (int(ac_match.group('details'), 16) & 0b10000000) >> 7 == 1,
|
||||
'address': ac_match.group('id'),
|
||||
'climb_rate': int(ac_match.group('climb_rate')) * fpm2ms if ac_match.group('climb_rate') else None,
|
||||
'turn_rate': float(ac_match.group('turn_rate')) if ac_match.group('turn_rate') else None,
|
||||
'flightlevel': float(ac_match.group('flight_level')) if ac_match.group('flight_level') else None,
|
||||
'signal_quality': float(ac_match.group('signal_quality')) if ac_match.group('signal_quality') else None,
|
||||
'error_count': int(ac_match.group('errors')) if ac_match.group('errors') else None,
|
||||
'frequency_offset': float(ac_match.group('frequency_offset')) if ac_match.group('frequency_offset') else None,
|
||||
'gps_status': ac_match.group('gps_accuracy') if ac_match.group('gps_accuracy') else None,
|
||||
'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') if ac_match.group('flarm_id') else None,
|
||||
'signal_power': float(ac_match.group('signal_power')) if ac_match.group('signal_power') else None,
|
||||
'proximity': [hear[4:] for hear in ac_match.group('proximity').split(" ")] if ac_match.group('proximity') else None}
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def parse_receiver_beacon(aprs_comment):
|
||||
rec_match = re.search(PATTERN_RECEIVER_BEACON, aprs_comment)
|
||||
if rec_match:
|
||||
return {'version': rec_match.group('version'),
|
||||
'platform': rec_match.group('platform'),
|
||||
'cpu_load': float(rec_match.group('cpu_load')),
|
||||
'free_ram': float(rec_match.group('ram_free')),
|
||||
'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,
|
||||
'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 None,
|
||||
'rec_crystal_correction_fine': float(rec_match.group('rf_correction_automatic')) if rec_match.group('rf_correction_automatic') else None,
|
||||
'rec_input_noise': float(rec_match.group('signal_quality')) if rec_match.group('signal_quality') else None,
|
||||
'senders_signal': float(rec_match.group('senders_signal_quality')) if rec_match.group('senders_signal_quality') 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_quality')) if rec_match.group('good_senders_signal_quality') 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
|
|
@ -1,35 +0,0 @@
|
|||
import re
|
||||
|
||||
from ogn.parser.pattern import PATTERN_RECEIVER_POSITION, PATTERN_RECEIVER_STATUS
|
||||
|
||||
|
||||
def parse_position(aprs_comment):
|
||||
if aprs_comment is None:
|
||||
return {}
|
||||
else:
|
||||
match = re.search(PATTERN_RECEIVER_POSITION, aprs_comment)
|
||||
return {'user_comment': match.group('user_comment') if match.group('user_comment') else None}
|
||||
|
||||
|
||||
def parse_status(aprs_comment):
|
||||
match = re.search(PATTERN_RECEIVER_STATUS, aprs_comment)
|
||||
return {'version': match.group('version'),
|
||||
'platform': match.group('platform'),
|
||||
'cpu_load': float(match.group('cpu_load')),
|
||||
'free_ram': float(match.group('ram_free')),
|
||||
'total_ram': float(match.group('ram_total')),
|
||||
'ntp_error': float(match.group('ntp_offset')),
|
||||
'rt_crystal_correction': float(match.group('ntp_correction')),
|
||||
'voltage': float(match.group('voltage')) if match.group('voltage') else None,
|
||||
'amperage': float(match.group('amperage')) if match.group('amperage') else None,
|
||||
'cpu_temp': float(match.group('cpu_temperature')) if match.group('cpu_temperature') else None,
|
||||
'senders_visible': int(match.group('visible_senders')) if match.group('visible_senders') else None,
|
||||
'senders_total': int(match.group('senders')) if match.group('senders') else None,
|
||||
'rec_crystal_correction': int(match.group('rf_correction_manual')) if match.group('rf_correction_manual') else None,
|
||||
'rec_crystal_correction_fine': float(match.group('rf_correction_automatic')) if match.group('rf_correction_automatic') else None,
|
||||
'rec_input_noise': float(match.group('signal_quality')) if match.group('signal_quality') else None,
|
||||
'senders_signal': float(match.group('senders_signal_quality')) if match.group('senders_signal_quality') else None,
|
||||
'senders_messages': float(match.group('senders_messages')) if match.group('senders_messages') else None,
|
||||
'good_senders_signal': float(match.group('good_senders_signal_quality')) if match.group('good_senders_signal_quality') else None,
|
||||
'good_senders': float(match.group('good_senders')) if match.group('good_senders') else None,
|
||||
'good_and_bad_senders': float(match.group('good_and_bad_senders')) if match.group('good_and_bad_senders') else None}
|
|
@ -1,2 +0,0 @@
|
|||
def parse(aprs_comment):
|
||||
raise NotImplementedError("Skylines beacon parser not yet implemented")
|
|
@ -1,2 +0,0 @@
|
|||
def parse(aprs_comment):
|
||||
raise NotImplementedError("Spider beacon parser not yet implemented")
|
|
@ -1,2 +0,0 @@
|
|||
def parse(aprs_comment):
|
||||
raise NotImplementedError("SPOT beacon parser not yet implemented")
|
|
@ -1,37 +0,0 @@
|
|||
import re
|
||||
|
||||
from ogn.parser.utils import fpm2ms
|
||||
from ogn.parser.pattern import PATTERN_TRACKER_BEACON_POSITION, PATTERN_TRACKER_BEACON_STATUS
|
||||
|
||||
|
||||
def parse_position(aprs_comment):
|
||||
match = re.search(PATTERN_TRACKER_BEACON_POSITION, aprs_comment)
|
||||
return {'address_type': int(match.group('details'), 16) & 0b00000011,
|
||||
'aircraft_type': (int(match.group('details'), 16) & 0b01111100) >> 2,
|
||||
'stealth': (int(match.group('details'), 16) & 0b10000000) >> 7 == 1,
|
||||
'address': match.group('id'),
|
||||
'climb_rate': int(match.group('climb_rate')) * fpm2ms if match.group('climb_rate') else None,
|
||||
'turn_rate': float(match.group('turn_rate')) if match.group('turn_rate') else None,
|
||||
'flightlevel': float(match.group('flight_level')) if match.group('flight_level') else None,
|
||||
'signal_quality': float(match.group('signal_quality')) if match.group('signal_quality') else None,
|
||||
'error_count': int(match.group('errors')) if match.group('errors') else None,
|
||||
'frequency_offset': float(match.group('frequency_offset')) if match.group('frequency_offset') else None,
|
||||
'gps_status': match.group('gps_accuracy') if match.group('gps_accuracy') else None,
|
||||
'software_version': float(match.group('flarm_software_version')) if match.group('flarm_software_version') else None,
|
||||
'hardware_version': int(match.group('flarm_hardware_version'), 16) if match.group('flarm_hardware_version') else None}
|
||||
|
||||
|
||||
def parse_status(aprs_comment):
|
||||
match = re.search(PATTERN_TRACKER_BEACON_STATUS, aprs_comment)
|
||||
return {'hardware_version': int(match.group('hardware_version')) if match.group('hardware_version') else None,
|
||||
'software_version': int(match.group('software_version')) if match.group('software_version') else None,
|
||||
'gps_satellites': int(match.group('gps_satellites')) if match.group('gps_satellites') else None,
|
||||
'gps_quality': int(match.group('gps_quality')) if match.group('gps_quality') else None,
|
||||
'gps_altitude': int(match.group('gps_altitude')) if match.group('gps_altitude') else None,
|
||||
'pressure': float(match.group('pressure')) if match.group('pressure') else None,
|
||||
'temperature': float(match.group('temperature')) if match.group('temperature') else None,
|
||||
'humidity': int(match.group('humidity')) if match.group('humidity') else None,
|
||||
'voltage': float(match.group('voltage')) if match.group('voltage') else None,
|
||||
'transmitter_power': int(match.group('transmitter_power')) if match.group('transmitter_power') else None,
|
||||
'noise_level': float(match.group('noise_level')) if match.group('noise_level') else None,
|
||||
'relays': int(match.group('relays')) if match.group('relays') else None}
|
|
@ -1,46 +1,111 @@
|
|||
import re
|
||||
|
||||
PATTERN_APRS = re.compile(r"^(?P<callsign>.+?)>(?P<dstcall>[A-Z0-9]+),((?P<relay>[A-Za-z0-9]+)\*)?.*,(?P<receiver>.+?):(?P<aprs_type>(.))(?P<aprs_body>.*)$")
|
||||
PATTERN_APRS_POSITION = re.compile(r"^(?P<time>(([0-1]\d|2[0-3])[0-5]\d[0-5]\dh|([0-2]\d|3[0-1])([0-1]\d|2[0-3])[0-5]\dz))(?P<latitude>9000\.00|[0-8]\d{3}\.\d{2})(?P<latitude_sign>N|S)(?P<symbol_table>.)(?P<longitude>18000\.00|1[0-7]\d{3}\.\d{2}|0\d{4}\.\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{5}|\d{6})))?(?P<pos_extension>\s!W((?P<latitude_enhancement>\d)(?P<longitude_enhancement>\d))!)?(?:\s(?P<comment>.*))?$")
|
||||
PATTERN_APRS_POSITION_WEATHER = re.compile(r"^(?P<time>(([0-1]\d|2[0-3])[0-5]\d[0-5]\dh|([0-2]\d|3[0-1])([0-1]\d|2[0-3])[0-5]\dz))(?P<latitude>9000\.00|[0-8]\d{3}\.\d{2})(?P<latitude_sign>N|S)(?P<symbol_table>.)(?P<longitude>18000\.00|1[0-7]\d{3}\.\d{2}|0\d{4}\.\d{2})(?P<longitude_sign>E|W)(?P<symbol>.)(?P<wind_direction>(\d{3}|\.{3}))/(?P<wind_speed>(\d{3}|\.{3}))g(?P<wind_speed_peak>(\d{3}|\.{3}))t(?P<temperature>(\d{3}|\.{3}))(r(?P<rainfall_1h>\d{3}))?(p(?P<rainfall_24h>\d{3}))?(h(?P<humidity>\d{2}))?(b(?P<barometric_pressure>\d{5}))?(?:\s(?P<comment>.*))?$")
|
||||
PATTERN_APRS_STATUS = re.compile(r"^(?P<time>(([0-1]\d|2[0-3])[0-5]\d[0-5]\dh|([0-2]\d|3[0-1])([0-1]\d|2[0-3])[0-5]\dz))\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_SERVER = re.compile(r"^# aprsc (?P<version>[a-z0-9\.\-]+) (?P<timestamp>\d+ [A-Za-z]+ \d+ \d{2}:\d{2}:\d{2} GMT) (?P<server>[A-Z0-9]+) (?P<ip_address>\d+\.\d+\.\d+\.\d+):(?P<port>\d+)$")
|
||||
|
||||
PATTERN_NAVITER_BEACON = re.compile("""
|
||||
id(?P<details>[\dA-F]{4})(?P<id>[\dA-F]{6})\s
|
||||
PATTERN_FANET_POSITION_COMMENT = re.compile(r"""
|
||||
(id(?P<details>[\dA-F]{2})(?P<address>[\dA-F]{6}?)\s?)?
|
||||
(?:(?P<climb_rate>[+-]\d+)fpm)?
|
||||
""", re.VERBOSE | re.MULTILINE)
|
||||
|
||||
PATTERN_FANET_STATUS_COMMENT = re.compile(r"""
|
||||
(?:(Name=\"(?P<fanet_name>[^\"]*)\")\s?)?
|
||||
(?:(?P<signal_quality>[\d.]+?)dB\s?)?
|
||||
(?:(?P<frequency_offset>[+-][\d.]+?)kHz\s?)?
|
||||
(?:(?P<error_count>\d+)e\s?)?
|
||||
""", re.VERBOSE | re.MULTILINE)
|
||||
|
||||
PATTERN_FLARM_POSITION_COMMENT = re.compile(r"""
|
||||
id(?P<details>[\dA-F]{2})(?P<address>[\dA-F]{6}?)\s?
|
||||
(?:(?P<climb_rate>[+-]\d+?)fpm\s)?
|
||||
(?:(?P<turn_rate>[+-][\d.]+?)rot\s)?
|
||||
(?:(?P<signal_quality>[\d.]+?)dB\s)?
|
||||
(?:(?P<error_count>\d+)e\s)?
|
||||
(?:(?P<frequency_offset>[+-][\d.]+?)kHz\s?)?
|
||||
(?:gps(?P<gps_quality>(?P<gps_quality_horizontal>(\d+))x(?P<gps_quality_vertical>(\d+)))\s?)?
|
||||
(?:s(?P<software_version>[\d.]+)\s?)?
|
||||
(?:h(?P<hardware_version>[\dA-F]{2})\s?)?
|
||||
(?:r(?P<real_address>[\dA-F]+)\s?)?
|
||||
(?:(?P<signal_power>[+-][\d.]+)dBm\s?)?
|
||||
""", re.VERBOSE | re.MULTILINE)
|
||||
|
||||
PATTERN_LT24_POSITION_COMMENT = re.compile(r"""
|
||||
id(?P<lt24_id>\d+)\s
|
||||
(?P<climb_rate>[+-]\d+)fpm\s
|
||||
(?P<source>.+)
|
||||
""", re.VERBOSE | re.MULTILINE)
|
||||
|
||||
PATTERN_NAVITER_POSITION_COMMENT = re.compile(r"""
|
||||
id(?P<details>[\dA-F]{4})(?P<address>[\dA-F]{6})\s
|
||||
(?P<climb_rate>[+-]\d+)fpm\s
|
||||
(?P<turn_rate>[+-][\d.]+)rot
|
||||
""", re.VERBOSE | re.MULTILINE)
|
||||
|
||||
PATTERN_TRACKER_BEACON_POSITION = re.compile("""
|
||||
id(?P<details>\w{2})(?P<id>\w{6}?)\s?
|
||||
PATTERN_SKYLINES_POSITION_COMMENT = re.compile(r"""
|
||||
id(?P<skylines_id>\d+)\s
|
||||
(?P<climb_rate>[+-]\d+)fpm
|
||||
""", re.VERBOSE | re.MULTILINE)
|
||||
|
||||
PATTERN_SPIDER_POSITION_COMMENT = re.compile(r"""
|
||||
id(?P<spider_id>[\d-]+)\s
|
||||
(?P<signal_power>[+-]\d+)dB\s
|
||||
(?P<spider_registration>[A-Z0-9]+)\s
|
||||
(?P<gps_quality>.+)
|
||||
""", re.VERBOSE | re.MULTILINE)
|
||||
|
||||
PATTERN_SPOT_POSITION_COMMENT = re.compile(r"""
|
||||
id(?P<spot_id>[\d-]+)\s
|
||||
(?P<model>SPOT[A-Z\d]+)\s
|
||||
(?P<status>[A-Z]+)
|
||||
""", re.VERBOSE | re.MULTILINE)
|
||||
|
||||
PATTERN_INREACH_POSITION_COMMENT = re.compile(r"""
|
||||
id(?P<id>[\d]+)\s
|
||||
(?P<model>inReac[A-Za-z\d]*)\s
|
||||
(?P<status>[A-Za-z]+)\s?
|
||||
(?P<pilot_name>.+)?
|
||||
""", re.VERBOSE | re.MULTILINE)
|
||||
|
||||
PATTERN_TRACKER_POSITION_COMMENT = re.compile(r"""
|
||||
id(?P<details>[\dA-F]{2})(?P<address>[\dA-F]{6}?)\s?
|
||||
(?:(?P<climb_rate>[+-]\d+?)fpm\s)?
|
||||
(?:(?P<turn_rate>[+-][\d.]+?)rot\s)?
|
||||
(?:FL(?P<flight_level>[\d.]+)\s)?
|
||||
(?:(?P<signal_quality>[\d.]+?)dB\s)?
|
||||
(?:(?P<errors>\d+)e\s)?
|
||||
(?:(?P<error_count>\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?)?
|
||||
(?:gps(?P<gps_quality>(?P<gps_quality_horizontal>(\d+))x(?P<gps_quality_vertical>(\d+)))\s?)?
|
||||
(?:(?P<signal_power>[+-][\d.]+)dBm\s?)?
|
||||
""", re.VERBOSE | re.MULTILINE)
|
||||
|
||||
PATTERN_TRACKER_BEACON_STATUS = re.compile("""
|
||||
PATTERN_SAFESKY_POSITION_COMMENT = re.compile(r"""
|
||||
id(?P<details>[\dA-F]{2})(?P<address>[\dA-F]{6}?)\s?
|
||||
(?:(?P<climb_rate>[+-]\d+?)fpm\s)?
|
||||
(?:gps(?P<gps_quality>(?P<gps_quality_horizontal>(\d+))x(?P<gps_quality_vertical>(\d+)))?)?
|
||||
""", re.VERBOSE | re.MULTILINE)
|
||||
|
||||
PATTERN_TRACKER_STATUS_COMMENT = re.compile(r"""
|
||||
h(?P<hardware_version>[\d]{2})\s
|
||||
v(?P<software_version>[\d]{2})\s
|
||||
(?P<gps_satellites>[\d]+)sat/(?P<gps_quality>\d)\s
|
||||
(?P<gps_altitude>\d+)m\s
|
||||
(?P<pressure>[\d.]+)hPa\s
|
||||
(?P<temperature>[+-][\d.]+)degC\s
|
||||
(?P<humidity>\d+)%\s
|
||||
(?P<voltage>[\d.]+)V\s
|
||||
(?P<transmitter_power>\d+)/(?P<noise_level>[+-][\d.]+)dBm\s
|
||||
(?P<relays>\d+)/min
|
||||
v(?P<software_version>[\d]{2})\s?
|
||||
(?:(?P<gps_satellites>[\d]+)sat/(?P<gps_quality>\d)\s?)?
|
||||
(?:(?P<gps_altitude>\d+)m\s?)?
|
||||
(?:(?P<pressure>[\d.]+)hPa\s?)?
|
||||
(?:(?P<temperature>[+-][\d.]+)degC\s?)?
|
||||
(?:(?P<humidity>\d+)%\s?)?
|
||||
(?:(?P<voltage>[\d.]+)V\s?)?
|
||||
(?:(?P<transmitter_power>\d+)/(?P<noise_level>[+-][\d.]+)dBm\s?)?
|
||||
(?:(?P<relays>\d+)/min)?
|
||||
""", re.VERBOSE | re.MULTILINE)
|
||||
|
||||
PATTERN_RECEIVER_POSITION = re.compile(r"""
|
||||
PATTERN_RECEIVER_POSITION_COMMENT = re.compile(r"""
|
||||
(?:(?P<user_comment>.+))?
|
||||
""", re.VERBOSE | re.MULTILINE)
|
||||
|
||||
PATTERN_RECEIVER_STATUS = re.compile("""
|
||||
PATTERN_RECEIVER_STATUS_COMMENT = re.compile(r"""
|
||||
(?:
|
||||
v(?P<version>\d+\.\d+\.\d+)
|
||||
(?:\.(?P<platform>.+?))?
|
||||
|
@ -63,6 +128,30 @@ PATTERN_RECEIVER_STATUS = re.compile("""
|
|||
)?
|
||||
""", re.VERBOSE | re.MULTILINE)
|
||||
|
||||
PATTERN_TELNET_50001 = re.compile(r"""
|
||||
(?P<pps_offset>\d\.\d+)sec:(?P<frequency>\d+\.\d+)MHz:\s+
|
||||
(?P<aircraft_type>\d):(?P<address_type>\d):(?P<address>[A-F0-9]{6})\s
|
||||
(?P<timestamp>\d{6}):\s
|
||||
\[\s*(?P<latitude>[+-]\d+\.\d+),\s*(?P<longitude>[+-]\d+\.\d+)\]deg\s*
|
||||
(?P<altitude>\d+)m\s*
|
||||
(?P<climb_rate>[+-]\d+\.\d+)m/s\s*
|
||||
(?P<ground_speed>\d+\.\d+)m/s\s*
|
||||
(?P<track>\d+\.\d+)deg\s*
|
||||
(?P<turn_rate>[+-]\d+\.\d+)deg/sec\s*
|
||||
(?P<magic_number>\d+)\s*
|
||||
(?P<gps_status>[0-9x]+)m\s*
|
||||
(?P<channel>\d+)(?P<flarm_timeslot>[f_])(?P<ogn_timeslot>[o_])\s*
|
||||
(?P<frequency_offset>[+-]\d+\.\d+)kHz\s*
|
||||
(?P<decode_quality>\d+\.\d+)/(?P<signal_quality>\d+\.\d+)dB/(?P<demodulator_type>\d+)\s+
|
||||
(?P<error_count>\d+)e\s*
|
||||
(?P<distance>\d+\.\d+)km\s*
|
||||
(?P<bearing>\d+\.\d+)deg\s*
|
||||
(?P<phi>[+-]\d+\.\d+)deg\s*
|
||||
(?P<multichannel>\+)?\s*
|
||||
\?\s*
|
||||
R?\s*
|
||||
(B(?P<baro_altitude>\d+))?
|
||||
""", re.VERBOSE | re.MULTILINE)
|
||||
|
||||
# The following regexp patterns are part of the ruby ogn-client.
|
||||
# source: https://github.com/svoop/ogn_client-ruby
|
||||
|
@ -96,11 +185,12 @@ 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*)?
|
||||
(Lat\:(?P<latency>\d+\.\d+)s\s*)?
|
||||
(?:RF:
|
||||
(?:
|
||||
(?P<rf_correction_manual>[+-][\d]+)
|
||||
|
@ -114,14 +204,14 @@ PATTERN_RECEIVER_BEACON = re.compile(r"""
|
|||
|
||||
|
||||
PATTERN_AIRCRAFT_BEACON = re.compile(r"""
|
||||
id(?P<details>\w{2})(?P<id>\w{6}?)\s?
|
||||
id(?P<details>[\dA-F]{2})(?P<address>[\dA-F]{6}?)\s?
|
||||
(?:(?P<climb_rate>[+-]\d+?)fpm\s)?
|
||||
(?:(?P<turn_rate>[+-][\d.]+?)rot\s)?
|
||||
(?:FL(?P<flight_level>[\d.]+)\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?)?
|
||||
(?:gps(?P<gps_quality>(?P<gps_quality_horizontal>(\d+))x(?P<gps_quality_vertical>(\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?)?
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
from datetime import datetime
|
||||
|
||||
from ogn.parser.utils import createTimestamp
|
||||
from ogn.parser.pattern import PATTERN_TELNET_50001
|
||||
|
||||
telnet_50001_pattern = PATTERN_TELNET_50001
|
||||
|
||||
|
||||
def parse(telnet_data):
|
||||
reference_timestamp = datetime.utcnow()
|
||||
|
||||
match = telnet_50001_pattern.match(telnet_data)
|
||||
if match:
|
||||
return {'pps_offset': float(match.group('pps_offset')),
|
||||
'frequency': float(match.group('frequency')),
|
||||
'aircraft_type': int(match.group('aircraft_type')),
|
||||
'address_type': int(match.group('address_type')),
|
||||
'address': match.group('address'),
|
||||
'timestamp': createTimestamp(match.group('timestamp') + 'h', reference_timestamp),
|
||||
'latitude': float(match.group('latitude')),
|
||||
'longitude': float(match.group('longitude')),
|
||||
'altitude': int(match.group('altitude')),
|
||||
'climb_rate': float(match.group('climb_rate')),
|
||||
'ground_speed': float(match.group('ground_speed')),
|
||||
'track': float(match.group('track')),
|
||||
'turn_rate': float(match.group('turn_rate')),
|
||||
'magic_number': int(match.group('magic_number')),
|
||||
'gps_status': match.group('gps_status'),
|
||||
'channel': int(match.group('channel')),
|
||||
'flarm_timeslot': match.group('flarm_timeslot') == 'f',
|
||||
'ogn_timeslot': match.group('ogn_timeslot') == 'o',
|
||||
'frequency_offset': float(match.group('frequency_offset')),
|
||||
'decode_quality': float(match.group('decode_quality')),
|
||||
'signal_quality': float(match.group('signal_quality')),
|
||||
'demodulator_type': int(match.group('demodulator_type')),
|
||||
'error_count': float(match.group('error_count')),
|
||||
'distance': float(match.group('distance')),
|
||||
'bearing': float(match.group('bearing')),
|
||||
'phi': float(match.group('phi')),
|
||||
'multichannel': match.group('multichannel') == '+'}
|
||||
else:
|
||||
return None
|
|
@ -1,61 +1,95 @@
|
|||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
import math
|
||||
|
||||
from ogn.parser.exceptions import AmbigousTimeError
|
||||
FEETS_TO_METER = 0.3048 # ratio feets to meter
|
||||
FPM_TO_MS = FEETS_TO_METER / 60 # ratio fpm to m/s
|
||||
KNOTS_TO_MS = 0.5144 # ratio knots to m/s
|
||||
KPH_TO_MS = 0.27778 # ratio kph to m/s
|
||||
HPM_TO_DEGS = 180 / 60 # ratio between half turn per minute and degrees/s
|
||||
INCH_TO_MM = 25.4 # ratio inch to mm
|
||||
|
||||
|
||||
kmh2kts = 0.539957
|
||||
feet2m = 0.3048
|
||||
ms2fpm = 196.85
|
||||
|
||||
kts2kmh = 1 / kmh2kts
|
||||
m2feet = 1 / feet2m
|
||||
fpm2ms = 1 / ms2fpm
|
||||
def fahrenheit_to_celsius(fahrenheit):
|
||||
return (fahrenheit - 32.0) * 5.0 / 9.0
|
||||
|
||||
|
||||
def parseAngle(dddmmhht):
|
||||
return float(dddmmhht[:3]) + float(dddmmhht[3:]) / 60
|
||||
|
||||
|
||||
def createTimestamp(timestamp, reference_date, reference_time=None):
|
||||
if timestamp[-1] == "z":
|
||||
day = int(timestamp[0:2])
|
||||
hhmm = timestamp[2:6]
|
||||
if reference_date.day < day:
|
||||
if reference_date.month == 1:
|
||||
reference_date = reference_date.replace(year=reference_date.year - 1, month=12, day=day)
|
||||
else:
|
||||
reference_date = reference_date.replace(month=reference_date.month - 1, day=day)
|
||||
else:
|
||||
reference_date = reference_date.replace(day=day)
|
||||
packet_time = datetime.strptime(hhmm, '%H%M').time()
|
||||
return datetime.combine(reference_date, packet_time)
|
||||
elif timestamp[-1] == "h":
|
||||
hhmmss = timestamp[:-1]
|
||||
packet_time = datetime.strptime(hhmmss, '%H%M%S').time()
|
||||
def createTimestamp(time_string, reference_timestamp):
|
||||
if time_string[-1] == "z":
|
||||
dd = int(time_string[0:2])
|
||||
hh = int(time_string[2:4])
|
||||
mm = int(time_string[4:6])
|
||||
|
||||
result = datetime(reference_timestamp.year,
|
||||
reference_timestamp.month,
|
||||
dd,
|
||||
hh, mm, 0,
|
||||
tzinfo=timezone.utc if reference_timestamp.tzinfo is not None else None)
|
||||
|
||||
# correct wrong month
|
||||
if result > reference_timestamp + timedelta(days=14):
|
||||
result = (result.replace(day=1) - timedelta(days=14)).replace(day=result.day)
|
||||
elif result < reference_timestamp - timedelta(days=14):
|
||||
result = (result.replace(day=28) + timedelta(days=14)).replace(day=result.day)
|
||||
else:
|
||||
raise ValueError()
|
||||
hh = int(time_string[0:2])
|
||||
mm = int(time_string[2:4])
|
||||
ss = int(time_string[4:6])
|
||||
|
||||
if reference_time is None:
|
||||
return datetime.combine(reference_date, packet_time)
|
||||
else:
|
||||
reference_datetime = datetime.combine(reference_date, reference_time)
|
||||
timestamp = datetime.combine(reference_date, packet_time)
|
||||
delta = timestamp - reference_datetime
|
||||
result = datetime(reference_timestamp.year,
|
||||
reference_timestamp.month,
|
||||
reference_timestamp.day,
|
||||
hh, mm, ss,
|
||||
tzinfo=timezone.utc if reference_timestamp.tzinfo is not None else None)
|
||||
|
||||
# This function reconstructs the packet date from the timestamp and a reference_datetime time.
|
||||
# delta vs. packet date:
|
||||
# -24h -12h 0 +12h +24h
|
||||
# |-------------------------|---------------------|------------------------|----------------------|
|
||||
# [-] <-- tomorrow [---------today---------] [-------yesterday------]
|
||||
if result > reference_timestamp + timedelta(hours=12):
|
||||
# shift timestamp to previous day
|
||||
result -= timedelta(days=1)
|
||||
elif result < reference_timestamp - timedelta(hours=12):
|
||||
# shift timestamp to next day
|
||||
result += timedelta(days=1)
|
||||
|
||||
if timedelta(hours=-12) <= delta <= timedelta(minutes=30):
|
||||
# Packet less than 12h from the past or 30min from the future
|
||||
return timestamp
|
||||
elif delta < timedelta(hours=-23, minutes=-30):
|
||||
# Packet from next day, less than 30min from the future
|
||||
return datetime.combine(reference_datetime + timedelta(hours=+12), packet_time)
|
||||
elif timedelta(hours=12) < delta:
|
||||
# Packet from previous day, less than 12h from the past
|
||||
return datetime.combine(reference_datetime + timedelta(hours=-12), packet_time)
|
||||
else:
|
||||
raise AmbigousTimeError(reference_datetime, packet_time)
|
||||
return result
|
||||
|
||||
|
||||
MATH_PI = 3.14159265359
|
||||
|
||||
|
||||
class CheapRuler():
|
||||
"""Extreme fast distance calculating for distances below 500km."""
|
||||
|
||||
def __init__(self, lat):
|
||||
c = math.cos(lat * MATH_PI / 180)
|
||||
c2 = 2 * c * c - 1
|
||||
c3 = 2 * c * c2 - c
|
||||
c4 = 2 * c * c3 - c2
|
||||
c5 = 2 * c * c4 - c3
|
||||
|
||||
self.kx = 1000 * (111.41513 * c - 0.09455 * c3 + 0.00012 * c5) # longitude correction
|
||||
self.ky = 1000 * (111.13209 - 0.56605 * c2 + 0.0012 * c4) # latitude correction
|
||||
|
||||
def distance(self, a, b):
|
||||
"""Distance between point a and b. A point is a tuple(lon,lat)."""
|
||||
|
||||
dx = (a[0] - b[0]) * self.kx
|
||||
dy = (a[1] - b[1]) * self.ky
|
||||
return math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
def bearing(self, a, b):
|
||||
"""Returns the bearing from point a to point b."""
|
||||
|
||||
dx = (b[0] - a[0]) * self.kx
|
||||
dy = (b[1] - a[1]) * self.ky
|
||||
if dx == 0 and dy == 0:
|
||||
return 0
|
||||
result = math.atan2(-dy, dx) * 180 / MATH_PI + 90
|
||||
return result if result >= 0 else result + 360
|
||||
|
||||
|
||||
def normalized_quality(distance, signal_quality):
|
||||
"""Signal quality normalized to 10km."""
|
||||
|
||||
return signal_quality + 20.0 * math.log10(distance / 10000.0) if distance > 0 else None
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
[flake8]
|
||||
ignore = E501
|
||||
ignore = E501,E701
|
||||
|
|
16
setup.py
16
setup.py
|
@ -17,29 +17,33 @@ setup(
|
|||
version=PACKAGE_VERSION,
|
||||
description='A python module for the Open Glider Network',
|
||||
long_description=long_description,
|
||||
long_description_content_type='text/markdown',
|
||||
url='https://github.com/glidernet/python-ogn-client',
|
||||
author='Konstantin Gründger aka Meisterschueler, Fabian P. Schmidt aka kerel',
|
||||
author_email='kerel-fs@gmx.de',
|
||||
license='AGPLv3',
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Development Status :: 4 - Beta',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: Science/Research',
|
||||
'Topic :: Scientific/Engineering :: GIS',
|
||||
'License :: OSI Approved :: GNU Affero General Public License v3',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3 :: Only',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9'
|
||||
],
|
||||
keywords='gliding ogn',
|
||||
keywords=['gliding', 'ogn'],
|
||||
packages=['ogn.{}'.format(package) for package in find_packages(where='ogn')],
|
||||
python_requires='>=3',
|
||||
install_requires=[],
|
||||
extras_require={
|
||||
'dev': [
|
||||
'nose==1.3.7',
|
||||
'coveralls==1.2',
|
||||
'flake8==3.4.1'
|
||||
'coveralls==3.3.1',
|
||||
'flake8==4.0.1'
|
||||
]
|
||||
},
|
||||
zip_safe=False
|
||||
|
|
|
@ -3,7 +3,7 @@ import unittest.mock as mock
|
|||
|
||||
from ogn.parser import parse
|
||||
from ogn.client.client import create_aprs_login, AprsClient
|
||||
from ogn.client.settings import APRS_APP_NAME, APRS_APP_VER
|
||||
from ogn.client.settings import APRS_APP_NAME, APRS_APP_VER, APRS_KEEPALIVE_TIME
|
||||
|
||||
|
||||
class AprsClientTest(unittest.TestCase):
|
||||
|
@ -25,7 +25,7 @@ class AprsClientTest(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.assert_called_once_with('rw')
|
||||
client.sock.makefile.assert_called_once_with('rb')
|
||||
|
||||
@mock.patch('ogn.client.client.socket')
|
||||
def test_connect_client_defined_filter(self, mock_socket):
|
||||
|
@ -33,7 +33,7 @@ class AprsClientTest(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.assert_called_once_with('rw')
|
||||
client.sock.makefile.assert_called_once_with('rb')
|
||||
|
||||
@mock.patch('ogn.client.client.socket')
|
||||
def test_disconnect(self, mock_socket):
|
||||
|
@ -44,6 +44,62 @@ class AprsClientTest(unittest.TestCase):
|
|||
client.sock.close.assert_called_once_with()
|
||||
self.assertTrue(client._kill)
|
||||
|
||||
@mock.patch('ogn.client.client.socket')
|
||||
def test_run(self, mock_socket):
|
||||
import socket
|
||||
mock_socket.error = socket.error
|
||||
|
||||
client = AprsClient(aprs_user='testuser', aprs_filter='')
|
||||
client.connect()
|
||||
|
||||
client.sock_file.readline = mock.MagicMock()
|
||||
client.sock_file.readline.side_effect = [b'Normal text blabla',
|
||||
b'my weird character \xc2\xa5',
|
||||
UnicodeDecodeError('funnycodec', b'\x00\x00', 1, 2, 'This is just a fake reason!'),
|
||||
b'... show must go on',
|
||||
BrokenPipeError(),
|
||||
b'... and on',
|
||||
ConnectionResetError(),
|
||||
b'... and on',
|
||||
socket.error(),
|
||||
b'... and on',
|
||||
b'',
|
||||
b'... and on',
|
||||
KeyboardInterrupt()]
|
||||
|
||||
try:
|
||||
client.run(callback=lambda msg: print("got: {}".format(msg)), autoreconnect=True)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
client.disconnect()
|
||||
|
||||
@mock.patch('ogn.client.client.time')
|
||||
@mock.patch('ogn.client.client.socket')
|
||||
def test_run_keepalive(self, mock_socket, mock_time):
|
||||
import socket
|
||||
mock_socket.error = socket.error
|
||||
|
||||
client = AprsClient(aprs_user='testuser', aprs_filter='')
|
||||
client.connect()
|
||||
|
||||
client.sock_file.readline = mock.MagicMock()
|
||||
client.sock_file.readline.side_effect = [b'Normal text blabla',
|
||||
KeyboardInterrupt()]
|
||||
|
||||
mock_time.side_effect = [0, 0, APRS_KEEPALIVE_TIME + 1, APRS_KEEPALIVE_TIME + 1]
|
||||
|
||||
timed_callback = mock.MagicMock()
|
||||
|
||||
try:
|
||||
client.run(callback=lambda msg: print("got: {}".format(msg)), timed_callback=timed_callback)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
client.disconnect()
|
||||
|
||||
timed_callback.assert_called_with(client)
|
||||
|
||||
def test_reset_kill_reconnect(self):
|
||||
client = AprsClient(aprs_user='testuser', aprs_filter='')
|
||||
client.connect()
|
||||
|
@ -74,7 +130,7 @@ class AprsClientTest(unittest.TestCase):
|
|||
return
|
||||
try:
|
||||
message = parse(raw_message)
|
||||
print("{}: {}".format(message['beacon_type'], raw_message))
|
||||
print("{}: {}".format(message['aprs_type'], raw_message))
|
||||
except NotImplementedError as e:
|
||||
print("{}: {}".format(e, raw_message))
|
||||
return
|
||||
|
@ -91,4 +147,4 @@ class AprsClientTest(unittest.TestCase):
|
|||
pass
|
||||
finally:
|
||||
client.disconnect()
|
||||
self.assert_(True)
|
||||
self.assertTrue(True)
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import unittest
|
||||
import unittest.mock as mock
|
||||
|
||||
from ogn.client.client import TelnetClient
|
||||
|
||||
|
||||
class TelnetClientTest(unittest.TestCase):
|
||||
@mock.patch('ogn.client.client.socket')
|
||||
def test_connect_disconnect(self, socket_mock):
|
||||
client = TelnetClient()
|
||||
client.connect()
|
||||
client.sock.connect.assert_called_once_with(('localhost', 50001))
|
||||
|
||||
client.disconnect()
|
||||
client.sock.shutdown.assert_called_once_with(0)
|
||||
client.sock.close.assert_called_once_with()
|
||||
|
||||
@mock.patch('ogn.client.client.socket')
|
||||
def test_run(self, socket_mock):
|
||||
def callback(raw_message):
|
||||
raise ConnectionRefusedError
|
||||
|
||||
client = TelnetClient()
|
||||
client.connect()
|
||||
|
||||
client.run(callback=callback)
|
|
@ -13,43 +13,63 @@ class TestStringMethods(unittest.TestCase):
|
|||
def parse_valid_beacon_data_file(self, filename, beacon_type):
|
||||
with open(os.path.dirname(__file__) + '/valid_beacon_data/' + filename) as f:
|
||||
for line in f:
|
||||
if not line[0] == '#':
|
||||
try:
|
||||
message = parse(line, datetime(2015, 4, 10, 17, 0))
|
||||
self.assertFalse(message is None)
|
||||
try:
|
||||
message = parse(line, datetime(2015, 4, 10, 17, 0))
|
||||
self.assertFalse(message is None)
|
||||
if message['aprs_type'] == 'position' or message['aprs_type'] == 'status':
|
||||
self.assertEqual(message['beacon_type'], beacon_type)
|
||||
except NotImplementedError as e:
|
||||
print(e)
|
||||
except NotImplementedError as e:
|
||||
print(e)
|
||||
|
||||
def test_aprs_aircraft_beacons(self):
|
||||
self.parse_valid_beacon_data_file(filename='aprs_aircraft.txt', beacon_type='aircraft_beacon')
|
||||
self.parse_valid_beacon_data_file(filename='aprs_aircraft.txt', beacon_type='aprs_aircraft')
|
||||
|
||||
def test_aprs_receiver_beacons(self):
|
||||
self.parse_valid_beacon_data_file(filename='aprs_receiver.txt', beacon_type='receiver_beacon')
|
||||
self.parse_valid_beacon_data_file(filename='aprs_receiver.txt', beacon_type='aprs_receiver')
|
||||
|
||||
def test_ogn_flarm_beacons(self):
|
||||
self.parse_valid_beacon_data_file(filename='ogn_flarm.txt', beacon_type='aircraft_beacon')
|
||||
def test_aprs_fanet_beacons(self):
|
||||
self.parse_valid_beacon_data_file(filename='fanet.txt', beacon_type='fanet')
|
||||
|
||||
def test_ogn_receiver_beacons(self):
|
||||
self.parse_valid_beacon_data_file(filename='ogn_receiver.txt', beacon_type='receiver_beacon')
|
||||
def test_flarm_beacons(self):
|
||||
self.parse_valid_beacon_data_file(filename='flarm.txt', beacon_type='flarm')
|
||||
|
||||
def test_ogn_tracker_beacons(self):
|
||||
self.parse_valid_beacon_data_file(filename='ogn_tracker.txt', beacon_type='aircraft_beacon')
|
||||
def test_receiver_beacons(self):
|
||||
self.parse_valid_beacon_data_file(filename='receiver.txt', beacon_type='receiver')
|
||||
|
||||
def test_tracker_beacons(self):
|
||||
self.parse_valid_beacon_data_file(filename='tracker.txt', beacon_type='tracker')
|
||||
|
||||
def test_capturs_beacons(self):
|
||||
self.parse_valid_beacon_data_file(filename='capturs.txt', beacon_type='capturs')
|
||||
|
||||
def test_flymaster_beacons(self):
|
||||
self.parse_valid_beacon_data_file(filename='flymaster.txt', beacon_type='flymaster')
|
||||
|
||||
def test_inreach_beacons(self):
|
||||
self.parse_valid_beacon_data_file(filename='inreach.txt', beacon_type='inreach')
|
||||
|
||||
def test_lt24_beacons(self):
|
||||
self.parse_valid_beacon_data_file(filename='lt24.txt', beacon_type='lt24_beacon')
|
||||
self.parse_valid_beacon_data_file(filename='lt24.txt', beacon_type='lt24')
|
||||
|
||||
def test_naviter_beacons(self):
|
||||
self.parse_valid_beacon_data_file(filename='naviter.txt', beacon_type='naviter_beacon')
|
||||
self.parse_valid_beacon_data_file(filename='naviter.txt', beacon_type='naviter')
|
||||
|
||||
def test_pilot_aware_beacons(self):
|
||||
self.parse_valid_beacon_data_file(filename='pilot_aware.txt', beacon_type='pilot_aware')
|
||||
|
||||
def test_skylines_beacons(self):
|
||||
self.parse_valid_beacon_data_file(filename='skylines.txt', beacon_type='skylines_beacon')
|
||||
self.parse_valid_beacon_data_file(filename='skylines.txt', beacon_type='skylines')
|
||||
|
||||
def test_spider_beacons(self):
|
||||
self.parse_valid_beacon_data_file(filename='spider.txt', beacon_type='spider_beacon')
|
||||
self.parse_valid_beacon_data_file(filename='spider.txt', beacon_type='spider')
|
||||
|
||||
def test_spot_beacons(self):
|
||||
self.parse_valid_beacon_data_file(filename='spot.txt', beacon_type='spot_beacon')
|
||||
self.parse_valid_beacon_data_file(filename='spot.txt', beacon_type='spot')
|
||||
|
||||
def test_generic_beacons(self):
|
||||
message = parse("EPZR>WTFDSTCALL,TCPIP*,qAC,GLIDERN1:>093456h this is a comment")
|
||||
self.assertEqual(message['beacon_type'], 'unknown')
|
||||
self.assertEqual(message['comment'], "this is a comment")
|
||||
|
||||
def test_fail_parse_aprs_none(self):
|
||||
with self.assertRaises(TypeError):
|
||||
|
@ -65,10 +85,14 @@ class TestStringMethods(unittest.TestCase):
|
|||
|
||||
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))
|
||||
message = parse("VITACURA1>APRS,TCPIP*,qAC,GLIDERN4:/201146h3322.79SI07034.80W&/A=002329 Vitacura Municipal Aerodrome, Club de Planeadores Vitacura")
|
||||
|
||||
self.assertEqual(message['user_comment'], "Vitacura Municipal Aerodrome, Club de Planeadores Vitacura")
|
||||
|
||||
message_with_id = parse("ALFALFAL>APRS,TCPIP*,qAC,GLIDERN4:/221830h3330.40SI07007.88W&/A=008659 Alfalfal Hidroelectric Plant, Club de Planeadores Vitacurs")
|
||||
|
||||
self.assertEqual(message_with_id['user_comment'], "Alfalfal Hidroelectric Plant, Club de Planeadores Vitacurs")
|
||||
|
||||
@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"
|
||||
|
@ -85,7 +109,7 @@ class TestStringMethods(unittest.TestCase):
|
|||
|
||||
def test_copy_constructor(self):
|
||||
valid_aprs_string = "FLRDDA5BA>APRS,qAS,LFMX:/160829h4415.41N/00600.03E'342/049/A=005524 id0ADDA5BA -454fpm -1.1rot 8.8dB 0e +51.2kHz gps4x5"
|
||||
message = parse(valid_aprs_string, reference_date=datetime(2015, 1, 1, 16, 8, 29))
|
||||
message = parse(valid_aprs_string)
|
||||
|
||||
self.assertEqual(message['name'], 'FLRDDA5BA')
|
||||
self.assertEqual(message['address'], 'DDA5BA')
|
||||
|
|
|
@ -2,7 +2,7 @@ import unittest
|
|||
|
||||
from datetime import datetime
|
||||
|
||||
from ogn.parser.utils import kts2kmh, m2feet
|
||||
from ogn.parser.utils import KNOTS_TO_MS, KPH_TO_MS, FEETS_TO_METER, INCH_TO_MM, fahrenheit_to_celsius
|
||||
from ogn.parser.parse import parse_aprs
|
||||
from ogn.parser.exceptions import AprsParseError
|
||||
|
||||
|
@ -10,11 +10,12 @@ from ogn.parser.exceptions import AprsParseError
|
|||
class TestStringMethods(unittest.TestCase):
|
||||
def test_fail_validation(self):
|
||||
with self.assertRaises(AprsParseError):
|
||||
parse_aprs("notAValidString", reference_date=datetime(2015, 1, 1))
|
||||
parse_aprs("notAValidString")
|
||||
|
||||
def test_basic(self):
|
||||
message = parse_aprs("FLRDDA5BA>APRS,qAS,LFMX:/160829h4415.41N/00600.03E'342/049/A=005524 this is a comment",
|
||||
reference_date=datetime(2015, 1, 1, 16, 8, 29))
|
||||
message = parse_aprs("FLRDDA5BA>APRS,qAS,LFMX:/160829h4415.41N/00600.03E'342/049/A=005524 this is a comment")
|
||||
|
||||
self.assertEqual(message['aprs_type'], 'position')
|
||||
self.assertEqual(message['name'], "FLRDDA5BA")
|
||||
self.assertEqual(message['dstcall'], "APRS")
|
||||
self.assertEqual(message['receiver_name'], "LFMX")
|
||||
|
@ -24,59 +25,139 @@ class TestStringMethods(unittest.TestCase):
|
|||
self.assertAlmostEqual(message['longitude'], 6.0005, 5)
|
||||
self.assertEqual(message['symbolcode'], '\'')
|
||||
self.assertEqual(message['track'], 342)
|
||||
self.assertEqual(message['ground_speed'], 49 * kts2kmh)
|
||||
self.assertAlmostEqual(message['altitude'] * m2feet, 5524, 5)
|
||||
self.assertEqual(message['ground_speed'], 49 * KNOTS_TO_MS / KPH_TO_MS)
|
||||
self.assertAlmostEqual(message['altitude'], 5524 * FEETS_TO_METER, 5)
|
||||
self.assertEqual(message['comment'], "this is a comment")
|
||||
|
||||
self.assertEqual(message['aprs_type'], 'position')
|
||||
|
||||
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))
|
||||
message = parse_aprs(raw_message)
|
||||
|
||||
self.assertEqual(message['aprs_type'], 'position')
|
||||
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, reference_date=datetime(2015, 1, 1, 9, 35, 29))
|
||||
message = parse_aprs(raw_message)
|
||||
|
||||
self.assertEqual(message['aprs_type'], 'status')
|
||||
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")
|
||||
|
||||
self.assertEqual(message['aprs_type'], 'status')
|
||||
|
||||
def test_v026(self):
|
||||
# from 0.2.6 the ogn comment of a receiver beacon is just optional
|
||||
raw_message = "Ulrichamn>APRS,TCPIP*,qAC,GLIDERN1:/085616h5747.30NI01324.77E&/A=001322"
|
||||
message = parse_aprs(raw_message, reference_date=datetime(2015, 1, 1, 8, 56, 0))
|
||||
message = parse_aprs(raw_message)
|
||||
|
||||
self.assertEqual(message['aprs_type'], 'position')
|
||||
self.assertEqual(message['comment'], '')
|
||||
|
||||
def test_v026_relay(self):
|
||||
# beacons can be relayed
|
||||
raw_message = "FLRFFFFFF>OGNAVI,NAV07220E*,qAS,NAVITER:/092002h1000.00S/01000.00W'000/000/A=003281 !W00! id2820FFFFFF +300fpm +1.7rot"
|
||||
message = parse_aprs(raw_message, reference_date=datetime(2015, 1, 1, 8, 56, 0))
|
||||
message = parse_aprs(raw_message)
|
||||
|
||||
self.assertEqual(message['aprs_type'], 'position')
|
||||
self.assertEqual(message['relay'], "NAV07220E")
|
||||
|
||||
def test_v027_ddhhmm(self):
|
||||
# beacons can have hhmmss or ddhhmm timestamp
|
||||
raw_message = "ICA4B0678>APRS,qAS,LSZF:/301046z4729.50N/00812.89E'227/091/A=002854 !W01! id054B0678 +040fpm +0.0rot 19.0dB 0e +1.5kHz gps1x1"
|
||||
message = parse_aprs(raw_message, reference_date=datetime(2015, 1, 1, 9, 35, 29))
|
||||
message = parse_aprs(raw_message)
|
||||
|
||||
self.assertEqual(message['aprs_type'], 'position')
|
||||
self.assertEqual(message['timestamp'].strftime('%d %H:%M'), "30 10:46")
|
||||
|
||||
def test_v028_fanet_position_weather(self):
|
||||
# with v0.2.8 fanet devices can report weather data
|
||||
raw_message = 'FNTFC9002>OGNFNT,qAS,LSXI2:/163051h4640.33N/00752.21E_187/004g007t075h78b63620 29.0dB -8.0kHz'
|
||||
message = parse_aprs(raw_message)
|
||||
|
||||
self.assertEqual(message['aprs_type'], 'position_weather')
|
||||
self.assertEqual(message['wind_direction'], 187)
|
||||
self.assertEqual(message['wind_speed'], 4 * KNOTS_TO_MS / KPH_TO_MS)
|
||||
self.assertEqual(message['wind_speed_peak'], 7 * KNOTS_TO_MS / KPH_TO_MS)
|
||||
self.assertEqual(message['temperature'], fahrenheit_to_celsius(75))
|
||||
self.assertEqual(message['humidity'], 78 * 0.01)
|
||||
self.assertEqual(message['barometric_pressure'], 63620)
|
||||
|
||||
self.assertEqual(message['comment'], '29.0dB -8.0kHz')
|
||||
|
||||
def test_GXAirCom_fanet_position_weather_rainfall(self):
|
||||
raw_message = 'FNT08F298>OGNFNT,qAS,DREIFBERG:/082654h4804.90N/00845.74E_273/005g008t057r123p234h90b10264 0.0dB'
|
||||
message = parse_aprs(raw_message)
|
||||
|
||||
self.assertEqual(message['aprs_type'], 'position_weather')
|
||||
self.assertEqual(message['rainfall_1h'], 123 / 100 * INCH_TO_MM)
|
||||
self.assertEqual(message['rainfall_24h'], 234 / 100 * INCH_TO_MM)
|
||||
|
||||
def test_v028_fanet_position_weather_empty(self):
|
||||
raw_message = 'FNT010115>OGNFNT,qAS,DB7MJ:/065738h4727.72N/01012.83E_.../...g...t... 27.8dB -13.8kHz'
|
||||
message = parse_aprs(raw_message)
|
||||
|
||||
self.assertEqual(message['aprs_type'], 'position_weather')
|
||||
self.assertIsNone(message['wind_direction'])
|
||||
self.assertIsNone(message['wind_speed'])
|
||||
self.assertIsNone(message['wind_speed_peak'])
|
||||
self.assertIsNone(message['temperature'])
|
||||
self.assertIsNone(message['humidity'])
|
||||
self.assertIsNone(message['barometric_pressure'])
|
||||
|
||||
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))
|
||||
message = parse_aprs(raw_message)
|
||||
|
||||
self.assertAlmostEqual(message['altitude'] * m2feet, -13, 5)
|
||||
self.assertAlmostEqual(message['altitude'], -13 * FEETS_TO_METER, 5)
|
||||
|
||||
def test_no_altitude(self):
|
||||
# altitude is not a 'must have'
|
||||
raw_message = "FLRDDEEF1>OGCAPT,qAS,CAPTURS:/065511h4837.63N/00233.79E'000/000"
|
||||
message = parse_aprs(raw_message)
|
||||
|
||||
self.assertEqual(message['altitude'], None)
|
||||
|
||||
def test_invalid_coordinates(self):
|
||||
# sometimes the coordinates leave their valid range: -90<=latitude<=90 or -180<=longitude<=180
|
||||
with self.assertRaises(AprsParseError):
|
||||
parse_aprs("RND000000>APRS,qAS,TROCALAN1:/210042h6505.31S/18136.75W^054/325/A=002591 !W31! idA4000000 +099fpm +1.8rot FL029.04 12.0dB 5e -6.3kHz gps11x17")
|
||||
|
||||
with self.assertRaises(AprsParseError):
|
||||
parse_aprs("RND000000>APRS,qAS,TROCALAN1:/210042h9505.31S/17136.75W^054/325/A=002591 !W31! idA4000000 +099fpm +1.8rot FL029.04 12.0dB 5e -6.3kHz gps11x17")
|
||||
|
||||
def test_invalid_timestamp(self):
|
||||
with self.assertRaises(AprsParseError):
|
||||
parse_aprs("OGND4362A>APRS,qAS,Eternoz:/194490h4700.25N/00601.47E'003/063/A=000000 !W22! id07D4362A 0fpm +0.0rot FL000.00 2.0dB 3e -2.8kHz gps3x4 +12.2dBm")
|
||||
|
||||
with self.assertRaises(AprsParseError):
|
||||
parse_aprs("Ulrichamn>APRS,TCPIP*,qAC,GLIDERN1:/194490h5747.30NI01324.77E&/A=001322")
|
||||
|
||||
def test_invalid_altitude(self):
|
||||
with self.assertRaises(AprsParseError):
|
||||
parse_aprs("Ulrichamn>APRS,TCPIP*,qAC,GLIDERN1:/085616h5747.30NI01324.77E&/A=12-345")
|
||||
|
||||
def test_bad_comment(self):
|
||||
raw_message = "# bad configured ogn receiver"
|
||||
message = parse_aprs(raw_message)
|
||||
|
||||
self.assertEqual(message['comment'], raw_message)
|
||||
self.assertEqual(message['aprs_type'], 'comment')
|
||||
|
||||
def test_server_comment(self):
|
||||
raw_message = "# aprsc 2.1.4-g408ed49 17 Mar 2018 09:30:36 GMT GLIDERN1 37.187.40.234:10152"
|
||||
message = parse_aprs(raw_message)
|
||||
|
||||
self.assertEqual(message['version'], '2.1.4-g408ed49')
|
||||
self.assertEqual(message['timestamp'], datetime(2018, 3, 17, 9, 30, 36))
|
||||
self.assertEqual(message['server'], 'GLIDERN1')
|
||||
self.assertEqual(message['ip_address'], '37.187.40.234')
|
||||
self.assertEqual(message['port'], '10152')
|
||||
self.assertEqual(message['aprs_type'], 'server')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import unittest
|
||||
|
||||
from ogn.parser.utils import FPM_TO_MS
|
||||
from ogn.parser.aprs_comment.fanet_parser import FanetParser
|
||||
|
||||
|
||||
class TestStringMethods(unittest.TestCase):
|
||||
def test_position_comment(self):
|
||||
message = FanetParser().parse_position("id1E1103CE -02fpm")
|
||||
|
||||
self.assertEqual(message['address_type'], 2)
|
||||
self.assertEqual(message['aircraft_type'], 7)
|
||||
self.assertFalse(message['stealth'])
|
||||
self.assertEqual(message['address'], "1103CE")
|
||||
self.assertAlmostEqual(message['climb_rate'], -2 * FPM_TO_MS, 0.1)
|
||||
|
||||
def test_pseudo_status_comment(self):
|
||||
message = FanetParser().parse_position("")
|
||||
|
||||
self.assertEqual(message, {})
|
||||
|
||||
def test_v028_status(self):
|
||||
message = FanetParser().parse_status('Name="Juerg Zweifel" 15.0dB -17.1kHz 1e')
|
||||
|
||||
self.assertEqual(message['fanet_name'], "Juerg Zweifel")
|
||||
self.assertEqual(message['signal_quality'], 15.0)
|
||||
self.assertEqual(message['frequency_offset'], -17.1)
|
||||
self.assertEqual(message['error_count'], 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -0,0 +1,35 @@
|
|||
import unittest
|
||||
|
||||
from ogn.parser.utils import FPM_TO_MS, HPM_TO_DEGS
|
||||
from ogn.parser.aprs_comment.flarm_parser import FlarmParser
|
||||
|
||||
|
||||
class TestStringMethods(unittest.TestCase):
|
||||
def test_position_comment(self):
|
||||
message = FlarmParser().parse_position("id21A8CBA8 -039fpm +0.1rot 3.5dB 2e -8.7kHz gps1x2 s6.09 h43 rDF0267")
|
||||
|
||||
self.assertEqual(message['address_type'], 1)
|
||||
self.assertEqual(message['aircraft_type'], 8)
|
||||
self.assertFalse(message['stealth'])
|
||||
self.assertFalse(message['no-tracking'])
|
||||
self.assertEqual(message['address'], "A8CBA8")
|
||||
self.assertAlmostEqual(message['climb_rate'], -39 * FPM_TO_MS, 2)
|
||||
self.assertEqual(message['turn_rate'], 0.1 * HPM_TO_DEGS)
|
||||
self.assertEqual(message['signal_quality'], 3.5)
|
||||
self.assertEqual(message['error_count'], 2)
|
||||
self.assertEqual(message['frequency_offset'], -8.7)
|
||||
self.assertEqual(message['gps_quality'], {'horizontal': 1, 'vertical': 2})
|
||||
self.assertEqual(message['software_version'], 6.09)
|
||||
self.assertEqual(message['hardware_version'], 67)
|
||||
self.assertEqual(message['real_address'], "DF0267")
|
||||
|
||||
def test_position_comment_relevant_keys_only(self):
|
||||
# return only keys where we got informations
|
||||
message = FlarmParser().parse_position("id21A8CBA8")
|
||||
|
||||
self.assertIsNotNone(message)
|
||||
self.assertEqual(sorted(message.keys()), sorted(['address_type', 'aircraft_type', 'stealth', 'address', 'no-tracking']))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -0,0 +1,16 @@
|
|||
import unittest
|
||||
|
||||
from ogn.parser.aprs_comment.generic_parser import GenericParser
|
||||
|
||||
|
||||
class TestStringMethods(unittest.TestCase):
|
||||
def test_position_comment(self):
|
||||
message = GenericParser().parse_position("id0123456789 weather is good, climbing with 123fpm")
|
||||
self.assertTrue('comment' in message)
|
||||
|
||||
message = GenericParser().parse_status("id0123456789 weather is good, climbing with 123fpm")
|
||||
self.assertTrue('comment' in message)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -0,0 +1,21 @@
|
|||
import unittest
|
||||
from ogn.parser.aprs_comment.inreach_parser import InreachParser
|
||||
|
||||
|
||||
class TestStringMethods(unittest.TestCase):
|
||||
def test_position_comment(self):
|
||||
message = InreachParser().parse_position("id300434060496190 inReac True")
|
||||
self.assertEqual(message['address'], "300434060496190")
|
||||
self.assertEqual(message['model'], 'inReac')
|
||||
self.assertEqual(message['status'], True)
|
||||
self.assertEqual(message['pilot_name'], None)
|
||||
|
||||
message = InreachParser().parse_position("id300434060496190 inReac True Jim Bob")
|
||||
self.assertEqual(message['address'], "300434060496190")
|
||||
self.assertEqual(message['model'], 'inReac')
|
||||
self.assertEqual(message['status'], True)
|
||||
self.assertEqual(message['pilot_name'], "Jim Bob")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -0,0 +1,17 @@
|
|||
import unittest
|
||||
|
||||
from ogn.parser.utils import FPM_TO_MS
|
||||
from ogn.parser.aprs_comment.lt24_parser import LT24Parser
|
||||
|
||||
|
||||
class TestStringMethods(unittest.TestCase):
|
||||
def test_position_comment(self):
|
||||
message = LT24Parser().parse_position("id25387 +123fpm GPS")
|
||||
|
||||
self.assertEqual(message['lt24_id'], "25387")
|
||||
self.assertAlmostEqual(message['climb_rate'], 123 * FPM_TO_MS, 2)
|
||||
self.assertEqual(message['source'], 'GPS')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -1,12 +1,12 @@
|
|||
import unittest
|
||||
|
||||
from ogn.parser.utils import ms2fpm
|
||||
from ogn.parser.parse_naviter import parse
|
||||
from ogn.parser.utils import FPM_TO_MS, HPM_TO_DEGS
|
||||
from ogn.parser.aprs_comment.naviter_parser import NaviterParser
|
||||
|
||||
|
||||
class TestStringMethods(unittest.TestCase):
|
||||
def test_OGNAVI_1(self):
|
||||
message = parse("id0440042121 +123fpm +0.5rot")
|
||||
message = NaviterParser().parse_position("id0440042121 +123fpm +0.5rot")
|
||||
|
||||
# id0440042121 == 0b0000 0100 0100 0000 0000 0100 0010 0001 0010 0001
|
||||
# bit 0: stealth mode
|
||||
|
@ -22,8 +22,8 @@ class TestStringMethods(unittest.TestCase):
|
|||
self.assertEqual(message['reserved'], 0)
|
||||
self.assertEqual(message['address'], "042121")
|
||||
|
||||
self.assertAlmostEqual(message['climb_rate'] * ms2fpm, 123, 2)
|
||||
self.assertEqual(message['turn_rate'], 0.5)
|
||||
self.assertAlmostEqual(message['climb_rate'], 123 * FPM_TO_MS, 2)
|
||||
self.assertEqual(message['turn_rate'], 0.5 * HPM_TO_DEGS)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
@ -1,63 +1,78 @@
|
|||
import unittest
|
||||
|
||||
from ogn.parser.utils import ms2fpm
|
||||
from ogn.parser.parse import parse_aircraft_beacon
|
||||
from ogn.parser.utils import FPM_TO_MS, HPM_TO_DEGS
|
||||
from ogn.parser.aprs_comment.ogn_parser import OgnParser
|
||||
|
||||
|
||||
class TestStringMethods(unittest.TestCase):
|
||||
def test_invalid_token(self):
|
||||
self.assertEqual(parse_aircraft_beacon("notAValidToken"), None)
|
||||
self.assertEqual(OgnParser().parse_aircraft_beacon("notAValidToken"), None)
|
||||
|
||||
def test_basic(self):
|
||||
message = parse_aircraft_beacon("id0ADDA5BA -454fpm -1.1rot 8.8dB 0e +51.2kHz gps4x5 hear1084 hearB597 hearB598")
|
||||
message = OgnParser().parse_aircraft_beacon("id0ADDA5BA -454fpm -1.1rot 8.8dB 0e +51.2kHz gps4x5 hear1084 hearB597 hearB598")
|
||||
|
||||
self.assertEqual(message['address_type'], 2)
|
||||
self.assertEqual(message['aircraft_type'], 2)
|
||||
self.assertFalse(message['stealth'])
|
||||
self.assertFalse(message['no-tracking'])
|
||||
self.assertEqual(message['address'], "DDA5BA")
|
||||
self.assertAlmostEqual(message['climb_rate'] * ms2fpm, -454, 2)
|
||||
self.assertEqual(message['turn_rate'], -1.1)
|
||||
self.assertAlmostEqual(message['climb_rate'], -454 * FPM_TO_MS, 2)
|
||||
self.assertEqual(message['turn_rate'], -1.1 * HPM_TO_DEGS)
|
||||
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(message['gps_quality'], {'horizontal': 4, 'vertical': 5})
|
||||
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_no_tracking(self):
|
||||
message = OgnParser().parse_aircraft_beacon("id0ADD1234 -454fpm -1.1rot 8.8dB 0e +51.2kHz gps4x5 hear1084 hearB597 hearB598")
|
||||
self.assertFalse(message['no-tracking'])
|
||||
|
||||
message = OgnParser().parse_aircraft_beacon("id4ADD1234 -454fpm -1.1rot 8.8dB 0e +51.2kHz gps4x5 hear1084 hearB597 hearB598")
|
||||
self.assertTrue(message['no-tracking'])
|
||||
|
||||
def test_stealth(self):
|
||||
message = parse_aircraft_beacon("id0ADD1234 -454fpm -1.1rot 8.8dB 0e +51.2kHz gps4x5 hear1084 hearB597 hearB598")
|
||||
message = OgnParser().parse_aircraft_beacon("id0ADD1234 -454fpm -1.1rot 8.8dB 0e +51.2kHz gps4x5 hear1084 hearB597 hearB598")
|
||||
self.assertFalse(message['stealth'])
|
||||
|
||||
message = parse_aircraft_beacon("id8ADD1234 -454fpm -1.1rot 8.8dB 0e +51.2kHz gps4x5 hear1084 hearB597 hearB598")
|
||||
message = OgnParser().parse_aircraft_beacon("id8ADD1234 -454fpm -1.1rot 8.8dB 0e +51.2kHz gps4x5 hear1084 hearB597 hearB598")
|
||||
self.assertTrue(message['stealth'])
|
||||
|
||||
def test_v024(self):
|
||||
message = parse_aircraft_beacon("id21400EA9 -2454fpm +0.9rot 19.5dB 0e -6.6kHz gps1x1 s6.02 h0A rDF0C56")
|
||||
message = OgnParser().parse_aircraft_beacon("id21400EA9 -2454fpm +0.9rot 19.5dB 0e -6.6kHz gps1x1 s6.02 h0A rDF0C56")
|
||||
|
||||
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):
|
||||
message = parse_aircraft_beacon("id07353800 +020fpm -14.0rot FL004.43 38.5dB 0e -2.9kHz")
|
||||
message = OgnParser().parse_aircraft_beacon("id07353800 +020fpm -14.0rot FL004.43 38.5dB 0e -2.9kHz")
|
||||
|
||||
self.assertEqual(message['flightlevel'], 4.43)
|
||||
|
||||
def test_v025(self):
|
||||
message = parse_aircraft_beacon("id06DDE28D +535fpm +3.8rot 11.5dB 0e -1.0kHz gps2x3 s6.01 h0C +7.4dBm")
|
||||
message = OgnParser().parse_aircraft_beacon("id06DDE28D +535fpm +3.8rot 11.5dB 0e -1.0kHz gps2x3 s6.01 h0C +7.4dBm")
|
||||
|
||||
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
|
||||
message_triple = parse_aircraft_beacon("id093D0930 +000fpm +0.0rot")
|
||||
message_single = parse_aircraft_beacon("id093D0930")
|
||||
message_triple = OgnParser().parse_aircraft_beacon("id093D0930 +000fpm +0.0rot")
|
||||
message_single = OgnParser().parse_aircraft_beacon("id093D0930")
|
||||
|
||||
self.assertIsNotNone(message_triple)
|
||||
self.assertIsNotNone(message_single)
|
||||
|
||||
def test_relevant_keys_only(self):
|
||||
# return only keys where we got informations
|
||||
message = OgnParser().parse_aircraft_beacon("id093D0930")
|
||||
|
||||
self.assertIsNotNone(message)
|
||||
self.assertEqual(sorted(message.keys()), sorted(['address_type', 'aircraft_type', 'stealth', 'address', 'no-tracking']))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import unittest
|
||||
|
||||
from ogn.parser.parse import parse_receiver_beacon
|
||||
from ogn.parser.aprs_comment.ogn_parser import OgnParser
|
||||
|
||||
|
||||
class TestStringMethods(unittest.TestCase):
|
||||
def test_fail_validation(self):
|
||||
self.assertEqual(parse_receiver_beacon("notAValidToken"), None)
|
||||
self.assertEqual(OgnParser().parse_receiver_beacon("notAValidToken"), None)
|
||||
|
||||
def test_v021(self):
|
||||
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")
|
||||
message = OgnParser().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(message['version'], "0.2.1")
|
||||
self.assertEqual(message['cpu_load'], 0.8)
|
||||
|
@ -23,17 +23,17 @@ class TestStringMethods(unittest.TestCase):
|
|||
self.assertEqual(message['rec_input_noise'], -0.25)
|
||||
|
||||
def test_v022(self):
|
||||
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")
|
||||
message = OgnParser().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):
|
||||
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]")
|
||||
message = OgnParser().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)
|
||||
|
||||
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]")
|
||||
message = OgnParser().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)
|
||||
|
@ -42,6 +42,17 @@ class TestStringMethods(unittest.TestCase):
|
|||
self.assertEqual(message['good_senders'], 68)
|
||||
self.assertEqual(message['good_and_bad_senders'], 135)
|
||||
|
||||
def test_v028(self):
|
||||
message = OgnParser().parse_receiver_beacon("v0.2.8.RPI-GPU CPU:0.3 RAM:744.5/968.2MB NTP:3.6ms/+2.0ppm +68.2C 3/3Acfts[1h] Lat:1.6s RF:-8+67.8ppm/+10.33dB/+1.3dB@10km[30998]/+10.4dB@10km[3/5]")
|
||||
self.assertEqual(message['latency'], 1.6)
|
||||
|
||||
def test_relevant_keys_only(self):
|
||||
# return only keys where we got informations
|
||||
message = OgnParser().parse_receiver_beacon("v0.2.5.ARM CPU:0.4 RAM:638.0/970.5MB NTP:0.2ms/-1.1ppm")
|
||||
|
||||
self.assertIsNotNone(message)
|
||||
self.assertEqual(sorted(message.keys()), sorted(['version', 'platform', 'cpu_load', 'free_ram', 'total_ram', 'ntp_error', 'rt_crystal_correction']))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
import unittest
|
||||
|
||||
from ogn.parser.parse_receiver import parse_position, parse_status
|
||||
from ogn.parser.aprs_comment.receiver_parser import ReceiverParser
|
||||
|
||||
|
||||
class TestStringMethods(unittest.TestCase):
|
||||
def test_position(self):
|
||||
message = parse_position("Antenna: chinese, on a pylon, 20 meter above ground")
|
||||
def test_position_comment(self):
|
||||
message = ReceiverParser().parse_position("Antenna: chinese, on a pylon, 20 meter above ground")
|
||||
|
||||
self.assertEqual(message['user_comment'], "Antenna: chinese, on a pylon, 20 meter above ground")
|
||||
|
||||
def test_position_empty(self):
|
||||
message = parse_position("")
|
||||
def test_position_comment_empty(self):
|
||||
message = ReceiverParser().parse_position("")
|
||||
|
||||
self.assertIsNotNone(message)
|
||||
|
||||
def test_status(self):
|
||||
message = parse_status("v0.2.7.RPI-GPU CPU:0.7 RAM:770.2/968.2MB NTP:1.8ms/-3.3ppm +55.7C 7/8Acfts[1h] RF:+54-1.1ppm/-0.16dB/+7.1dB@10km[19481]/+16.8dB@10km[7/13]")
|
||||
def test_status_comment(self):
|
||||
message = ReceiverParser().parse_status("v0.2.7.RPI-GPU CPU:0.7 RAM:770.2/968.2MB NTP:1.8ms/-3.3ppm +55.7C 7/8Acfts[1h] RF:+54-1.1ppm/-0.16dB/+7.1dB@10km[19481]/+16.8dB@10km[7/13]")
|
||||
|
||||
self.assertEqual(message['version'], "0.2.7")
|
||||
self.assertEqual(message['platform'], 'RPI-GPU')
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import unittest
|
||||
|
||||
from ogn.parser.utils import FPM_TO_MS
|
||||
from ogn.parser.aprs_comment.safesky_parser import SafeskyParser
|
||||
|
||||
|
||||
class TestStringMethods(unittest.TestCase):
|
||||
def test_position_comment(self):
|
||||
# "SKY3E5906>OGNSKY,qAS,SafeSky:/072555h5103.47N/00524.81E'065/031/A=001250 !W05! id1C3E5906 +010fpm gps6x1"
|
||||
message = SafeskyParser().parse_position("id1C3E5906 +010fpm gps6x1")
|
||||
self.assertEqual(message['address'], '3E5906')
|
||||
self.assertEqual(message['address_type'], 0)
|
||||
self.assertEqual(message['aircraft_type'], 7)
|
||||
self.assertFalse(message['stealth'])
|
||||
self.assertAlmostEqual(message['climb_rate'], 10 * FPM_TO_MS, 2)
|
||||
self.assertEqual(message['gps_quality'], {'horizontal': 6, 'vertical': 1})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -0,0 +1,16 @@
|
|||
import unittest
|
||||
|
||||
from ogn.parser.utils import FPM_TO_MS
|
||||
from ogn.parser.aprs_comment.skylines_parser import SkylinesParser
|
||||
|
||||
|
||||
class TestStringMethods(unittest.TestCase):
|
||||
def test_position_comment(self):
|
||||
message = SkylinesParser().parse_position("id2816 -015fpm")
|
||||
|
||||
self.assertEqual(message['skylines_id'], "2816")
|
||||
self.assertAlmostEqual(message['climb_rate'], -15 * FPM_TO_MS, 2)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -0,0 +1,17 @@
|
|||
import unittest
|
||||
|
||||
from ogn.parser.aprs_comment.spider_parser import SpiderParser
|
||||
|
||||
|
||||
class TestStringMethods(unittest.TestCase):
|
||||
def test_position_comment(self):
|
||||
message = SpiderParser().parse_position("id300234060668560 +30dB K23W 3D")
|
||||
|
||||
self.assertEqual(message['spider_id'], "300234060668560")
|
||||
self.assertEqual(message['signal_power'], 30)
|
||||
self.assertEqual(message['spider_registration'], "K23W")
|
||||
self.assertEqual(message['gps_quality'], "3D")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -0,0 +1,16 @@
|
|||
import unittest
|
||||
|
||||
from ogn.parser.aprs_comment.spot_parser import SpotParser
|
||||
|
||||
|
||||
class TestStringMethods(unittest.TestCase):
|
||||
def test_position_comment(self):
|
||||
message = SpotParser().parse_position("id0-2860357 SPOT3 GOOD")
|
||||
|
||||
self.assertEqual(message['spot_id'], "0-2860357")
|
||||
self.assertEqual(message['model'], 'SPOT3')
|
||||
self.assertEqual(message['status'], "GOOD")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -0,0 +1,58 @@
|
|||
import unittest
|
||||
import unittest.mock as mock
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from ogn.parser.telnet_parser import parse
|
||||
from ogn.parser.exceptions import ParseError
|
||||
|
||||
|
||||
class TestStringMethods(unittest.TestCase):
|
||||
@unittest.skip("Not yet implemented")
|
||||
def test_telnet_fail_corrupt(self):
|
||||
with self.assertRaises(ParseError):
|
||||
parse('This is rubbish')
|
||||
|
||||
@mock.patch('ogn.parser.telnet_parser.datetime')
|
||||
def test_telnet_parse_complete(self, datetime_mock):
|
||||
# set the utcnow-mock near to the time in the test string
|
||||
datetime_mock.utcnow.return_value = datetime(2015, 1, 1, 10, 0, 55)
|
||||
|
||||
message = parse('0.181sec:868.394MHz: 1:2:DDA411 103010: [ +50.86800, +12.15279]deg 988m +0.1m/s 25.7m/s 085.4deg -3.5deg/sec 5 03x04m 01f_-12.61kHz 5.8/15.5dB/2 10e 30.9km 099.5deg +1.1deg + ? R B8949')
|
||||
|
||||
self.assertEqual(message['pps_offset'], 0.181)
|
||||
self.assertEqual(message['frequency'], 868.394)
|
||||
self.assertEqual(message['aircraft_type'], 1)
|
||||
self.assertEqual(message['address_type'], 2)
|
||||
self.assertEqual(message['address'], 'DDA411')
|
||||
self.assertEqual(message['timestamp'], datetime(2015, 1, 1, 10, 30, 10))
|
||||
self.assertEqual(message['latitude'], 50.868)
|
||||
self.assertEqual(message['longitude'], 12.15279)
|
||||
self.assertEqual(message['altitude'], 988)
|
||||
self.assertEqual(message['climb_rate'], 0.1)
|
||||
self.assertEqual(message['ground_speed'], 25.7)
|
||||
self.assertEqual(message['track'], 85.4)
|
||||
self.assertEqual(message['turn_rate'], -3.5)
|
||||
self.assertEqual(message['magic_number'], 5) # the '5' is a magic number... 1 if ground_speed is 0.0m/s an 3 or 5 if airborne. Do you have an idea what it is?
|
||||
self.assertEqual(message['gps_status'], '03x04')
|
||||
self.assertEqual(message['channel'], 1)
|
||||
self.assertEqual(message['flarm_timeslot'], True)
|
||||
self.assertEqual(message['ogn_timeslot'], False)
|
||||
self.assertEqual(message['frequency_offset'], -12.61)
|
||||
self.assertEqual(message['decode_quality'], 5.8)
|
||||
self.assertEqual(message['signal_quality'], 15.5)
|
||||
self.assertEqual(message['demodulator_type'], 2)
|
||||
self.assertEqual(message['error_count'], 10)
|
||||
self.assertEqual(message['distance'], 30.9)
|
||||
self.assertEqual(message['bearing'], 99.5)
|
||||
self.assertEqual(message['phi'], 1.1)
|
||||
self.assertEqual(message['multichannel'], True)
|
||||
|
||||
def test_telnet_parse_corrupt(self):
|
||||
message = parse('0.397sec:868.407MHz: sA:1:784024 205656: [ +5.71003, +20.48951]deg 34012m +14.5m/s 109.7m/s 118.5deg +21.0deg/sec 0 27x40m 01_o +7.03kHz 17.2/27.0dB/2 12e 4719.5km 271.1deg -8.5deg ? R B34067')
|
||||
|
||||
self.assertIsNone(message)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -1,27 +1,28 @@
|
|||
import unittest
|
||||
|
||||
from ogn.parser.utils import ms2fpm
|
||||
from ogn.parser.parse_tracker import parse_position, parse_status
|
||||
from ogn.parser.utils import FPM_TO_MS, HPM_TO_DEGS
|
||||
from ogn.parser.aprs_comment.tracker_parser import TrackerParser
|
||||
|
||||
|
||||
class TestStringMethods(unittest.TestCase):
|
||||
def test_position_beacon(self):
|
||||
message = parse_position("id072FD00F -058fpm +1.1rot FL003.12 32.8dB 0e -0.8kHz gps3x5")
|
||||
def test_position_comment(self):
|
||||
message = TrackerParser().parse_position("id072FD00F -058fpm +1.1rot FL003.12 32.8dB 0e -0.8kHz gps3x5 +12.7dBm")
|
||||
|
||||
self.assertEqual(message['address_type'], 3)
|
||||
self.assertEqual(message['aircraft_type'], 1)
|
||||
self.assertFalse(message['stealth'])
|
||||
self.assertEqual(message['address'], "2FD00F")
|
||||
self.assertAlmostEqual(message['climb_rate'] * ms2fpm, -58, 2)
|
||||
self.assertEqual(message['turn_rate'], 1.1)
|
||||
self.assertAlmostEqual(message['climb_rate'], -58 * FPM_TO_MS, 2)
|
||||
self.assertEqual(message['turn_rate'], 1.1 * HPM_TO_DEGS)
|
||||
self.assertEqual(message['flightlevel'], 3.12)
|
||||
self.assertEqual(message['signal_quality'], 32.8)
|
||||
self.assertEqual(message['error_count'], 0)
|
||||
self.assertEqual(message['frequency_offset'], -0.8)
|
||||
self.assertEqual(message['gps_status'], '3x5')
|
||||
self.assertEqual(message['gps_quality'], {'horizontal': 3, 'vertical': 5})
|
||||
self.assertEqual(message['signal_power'], 12.7)
|
||||
|
||||
def test_status(self):
|
||||
message = parse_status("h00 v00 9sat/1 164m 1002.6hPa +20.2degC 0% 3.34V 14/-110.5dBm 1/min")
|
||||
def test_status_comment(self):
|
||||
message = TrackerParser().parse_status("h00 v00 9sat/1 164m 1002.6hPa +20.2degC 0% 3.34V 14/-110.5dBm 1/min")
|
||||
|
||||
self.assertEqual(message['hardware_version'], 0)
|
||||
self.assertEqual(message['software_version'], 0)
|
||||
|
@ -36,6 +37,11 @@ class TestStringMethods(unittest.TestCase):
|
|||
self.assertEqual(message['noise_level'], -110.5)
|
||||
self.assertEqual(message['relays'], 1)
|
||||
|
||||
def test_status_comment_comment(self):
|
||||
message = TrackerParser().parse_status("Pilot=Pawel Hard=DIY/STM32")
|
||||
|
||||
self.assertEqual(message['comment'], "Pilot=Pawel Hard=DIY/STM32")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import unittest
|
||||
from datetime import date, time, datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from ogn.parser.utils import parseAngle, createTimestamp
|
||||
from ogn.parser.exceptions import AmbigousTimeError
|
||||
from ogn.parser.utils import parseAngle, createTimestamp, CheapRuler, normalized_quality
|
||||
|
||||
|
||||
class TestStringMethods(unittest.TestCase):
|
||||
|
@ -11,35 +10,57 @@ class TestStringMethods(unittest.TestCase):
|
|||
|
||||
def proceed_test_data(self, test_data={}):
|
||||
for test in test_data:
|
||||
if test[3]:
|
||||
timestamp = createTimestamp(test[0], reference_date=test[1], reference_time=test[2])
|
||||
self.assertEqual(timestamp, test[3])
|
||||
else:
|
||||
with self.assertRaises(AmbigousTimeError):
|
||||
createTimestamp(test[0], reference_date=test[1], reference_time=test[2])
|
||||
timestamp = createTimestamp(test[0], reference_timestamp=test[1])
|
||||
self.assertEqual(timestamp, test[2])
|
||||
|
||||
def test_createTimestamp_hhmmss(self):
|
||||
test_data = [
|
||||
('000001h', date(2015, 1, 10), time(0, 0, 1), datetime(2015, 1, 10, 0, 0, 1)), # packet from current day (on the tick)
|
||||
('235959h', date(2015, 1, 10), time(0, 0, 1), datetime(2015, 1, 9, 23, 59, 59)), # packet from previous day (2 seconds old)
|
||||
('110000h', date(2015, 1, 10), time(0, 0, 1), None), # packet 11 hours from future or 13 hours old
|
||||
('123500h', date(2015, 1, 10), time(23, 50, 0), datetime(2015, 1, 10, 12, 35, 0)), # packet from current day (11 hours old)
|
||||
('000001h', date(2015, 1, 10), time(23, 50, 0), datetime(2015, 1, 11, 0, 0, 1)), # packet from next day (11 minutes from future)
|
||||
('000001h', date(2015, 1, 10), None, datetime(2015, 1, 10, 0, 0, 1)), # first packet of a specific day
|
||||
('235959h', date(2015, 1, 10), None, datetime(2015, 1, 10, 23, 59, 59)), # last packet of a specific day
|
||||
('000001h', datetime(2015, 1, 10, 0, 0, 1), datetime(2015, 1, 10, 0, 0, 1)), # packet from current day (on the tick)
|
||||
('235959h', datetime(2015, 1, 10, 0, 0, 1), datetime(2015, 1, 9, 23, 59, 59)), # packet from previous day (2 seconds old)
|
||||
('110000h', datetime(2015, 1, 10, 0, 0, 1), datetime(2015, 1, 10, 11, 0, 0)), # packet 11 hours from future or 13 hours old
|
||||
('123500h', datetime(2015, 1, 10, 23, 50, 0), datetime(2015, 1, 10, 12, 35, 0)), # packet from current day (11 hours old)
|
||||
('000001h', datetime(2015, 1, 10, 23, 50, 0), datetime(2015, 1, 11, 0, 0, 1)), # packet from next day (11 minutes from future)
|
||||
]
|
||||
|
||||
self.proceed_test_data(test_data)
|
||||
|
||||
def test_createTimestamp_ddhhmm(self):
|
||||
test_data = [
|
||||
('011212z', date(2017, 9, 28), time(0, 0, 1), datetime(2017, 9, 1, 12, 12, 0)), # packet from 1st of month, received on september 28th,
|
||||
('281313z', date(2017, 10, 1), time(0, 0, 1), datetime(2017, 9, 28, 13, 13, 0)), # packet from 28th of month, received on october 1st,
|
||||
('281414z', date(2017, 1, 1), time(0, 0, 1), datetime(2016, 12, 28, 14, 14, 0)), # packet from 28th of month, received on january 1st,
|
||||
('011212z', datetime(2017, 9, 28, 0, 0, 1), datetime(2017, 10, 1, 12, 12, 0)), # packet from 1st of month, received on september 28th,
|
||||
('281313z', datetime(2017, 10, 1, 0, 0, 1), datetime(2017, 9, 28, 13, 13, 0)), # packet from 28th of month, received on october 1st,
|
||||
('281414z', datetime(2017, 1, 1, 0, 0, 1), datetime(2016, 12, 28, 14, 14, 0)), # packet from 28th of month, received on january 1st,
|
||||
]
|
||||
|
||||
self.proceed_test_data(test_data)
|
||||
|
||||
def test_createTimestamp_tzinfo(self):
|
||||
test_data = [
|
||||
('000001h', datetime(2020, 9, 10, 0, 0, 1, tzinfo=timezone.utc), (datetime(2020, 9, 10, 0, 0, 1, tzinfo=timezone.utc)))
|
||||
]
|
||||
|
||||
self.proceed_test_data(test_data)
|
||||
|
||||
def test_cheap_ruler_distance(self):
|
||||
koenigsdf = (11.465353, 47.829825)
|
||||
hochkoenig = (13.062405, 47.420516)
|
||||
|
||||
cheap_ruler = CheapRuler((koenigsdf[1] + hochkoenig[1]) / 2)
|
||||
distance = cheap_ruler.distance(koenigsdf, hochkoenig)
|
||||
self.assertAlmostEqual(distance, 128381.47612138899)
|
||||
|
||||
def test_cheap_ruler_bearing(self):
|
||||
koenigsdf = (11.465353, 47.829825)
|
||||
hochkoenig = (13.062405, 47.420516)
|
||||
|
||||
cheap_ruler = CheapRuler((koenigsdf[1] + hochkoenig[1]) / 2)
|
||||
bearing = cheap_ruler.bearing(koenigsdf, hochkoenig)
|
||||
self.assertAlmostEqual(bearing, 110.761300063515)
|
||||
|
||||
def test_normalized_quality(self):
|
||||
self.assertAlmostEqual(normalized_quality(10000, 1), 1)
|
||||
self.assertAlmostEqual(normalized_quality(20000, 10), 16.020599913279625)
|
||||
self.assertAlmostEqual(normalized_quality(5000, 5), -1.0205999132796242)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
|
|
@ -25,3 +25,6 @@ VITACURA2>APRS,TCPIP*,qAC,GLIDERN3:>042136h v0.2.5.ARM CPU:0.3 RAM:695.0/970.5MB
|
|||
# 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
|
||||
#
|
||||
# ... and user comment can include a 'id'
|
||||
ALFALFAL>APRS,TCPIP*,qAC,GLIDERN4:/221830h3330.40SI07007.88W&/A=008659 Alfalfal Hidroelectric Plant, Club de Planeadores Vitacurs
|
|
@ -0,0 +1,11 @@
|
|||
# The following beacons are example for the Capture APRS format
|
||||
# source: https://github.com/glidernet/ogn-aprs-protocol
|
||||
#
|
||||
FLRDDEEF1>OGCAPT,qAS,CAPTURS:/062744h4845.03N/00230.46E'000/000
|
||||
FLRDDEEF1>OGCAPT,qAS,CAPTURS:/064243h4839.64N/00236.78E'000/085/A=000410
|
||||
FLRDDEEF1>OGCAPT,qAS,CAPTURS:/064548h4838.87N/00234.03E'000/042/A=000377
|
||||
FLRDDEEF1>OGCAPT,qAS,CAPTURS:/064847h4837.95N/00234.36E'000/000
|
||||
FLRDDEEF1>OGCAPT,qAS,CAPTURS:/065144h4837.56N/00233.80E'000/000
|
||||
FLRDDEEF1>OGCAPT,qAS,CAPTURS:/065511h4837.63N/00233.79E'000/000
|
||||
FLRDDEEF1>OGCAPT,qAS,CAPTURS:/070016h4837.63N/00233.77E'000/001/A=000360
|
||||
FLRDDEEF1>OGCAPT,qAS,CAPTURS:/070153h4837.62N/00233.77E'000/001/A=000344
|
|
@ -0,0 +1,19 @@
|
|||
# With OGN software 0.2.7 receivers have the dstcall "OGNFNT"
|
||||
#
|
||||
FNT1103CE>OGNFNT,qAS,FNB1103CE:/183727h5057.94N/00801.00Eg355/002/A=001042 !W10! id1E1103CE +03fpm
|
||||
FNT1103CE>OGNFNT,qAS,FNB1103CE:/183729h5057.94N/00801.00Eg354/001/A=001042 !W10! id1E1103CE +07fpm
|
||||
FNT1103CE>OGNFNT,qAS,FNB1103CE:/183731h5057.94N/00801.00Eg354/001/A=001042 !W10! id1E1103CE +05fpm
|
||||
FNT1103CE>OGNFNT,qAS,FNB1103CE:/183734h5057.94N/00801.00Eg354/001/A=001042 !W30! id1E1103CE -10fpm
|
||||
FNT1103CE>OGNFNT,qAS,FNB1103CE:/183736h5057.94N/00801.00Eg354/001/A=001042 !W40! id1E1103CE -02fpm
|
||||
FNB1103CE>OGNFNT,TCPIP*,qAC,GLIDERN3:/183738h5057.95NI00801.00E&/A=001042
|
||||
#
|
||||
# With OGN software 0.2.8 we get weather data in the position message ...
|
||||
#
|
||||
FNTFC9002>OGNFNT,qAS,LSXI2:/163051h4640.33N/00752.21E_187/004g007t075h78b63620 29.0dB -8.0kHz
|
||||
FNT051015>OGNFNT,qAS,LSXI2:/112540h4641.18N/00751.53E_097/007g008t082h54 28.8dB -8.3kHz
|
||||
#
|
||||
# ... and additional fanet data in the status message
|
||||
#
|
||||
FNT1122AE>OGNFNT,qAS,LIDH:>112528h Name="Juerg Zweifel" 15.0dB -17.1kHz 1e
|
||||
FNT0728B8>OGNFNT,qAS,Huenenbg:>112533h Name="Huenenb2" 26.8dB +3.0kHz 6e
|
||||
FNT111369>OGNFNT,qAS,LSXI2:>112535h Name="Zaugg Thomas" 18.3dB -15.4kHz
|
|
@ -4,4 +4,5 @@ FLRDD89C9>OGFLR,qAS,LIDH:/115054h4543.22N/01132.84E'260/072/A=002542 !W10! id06D
|
|||
FLRDD98C6>OGFLR,qAS,LIDH:/115054h4543.21N/01132.80E'255/074/A=002535 !W83! id0ADD98C6 +158fpm -1.8rot 10.5dB 0e -0.8kHz gps2x3 s6.09 h02
|
||||
ICAA8CBA8>OGFLR,qAS,MontCAIO:/231150z4512.12N\01059.03E^192/106/A=009519 !W20! id21A8CBA8 -039fpm +0.0rot 3.5dB 2e -8.7kHz gps1x2 s6.09 h43 rDF0267
|
||||
ICAA8CBA8>OGFLR,qAS,MontCAIO:/114949h4512.44N\01059.12E^190/106/A=009522 !W33! id21A8CBA8 -039fpm +0.1rot 4.5dB 1e -8.7kHz gps1x2 +14.3dBm
|
||||
ICA3D1C35>OGFLR,qAS,Padova:/094220h4552.41N/01202.28E'110/099/A=003982 !W96! id053D1C35 -1187fpm +0.0rot 0.8dB 2e +4.5kHz gps1x2 s6.09 h32 rDD09D0
|
||||
ICA3D1C35>OGFLR,qAS,Padova:/094220h4552.41N/01202.28E'110/099/A=003982 !W96! id053D1C35 -1187fpm +0.0rot 0.8dB 2e +4.5kHz gps1x2 s6.09 h32 rDD09D0
|
||||
FLR200295>OGFLR,qAS,TT:/071005h4613.92N/01427.53Eg000/000/A=001313 !W00! id1E200295 +000fpm +0.0rot 37.0dB -1.8kHz gps3x5
|
|
@ -0,0 +1,35 @@
|
|||
# The following beacons are example for Flymaster APRS format
|
||||
# source: https://github.com/glidernet/ogn-aprs-protocol
|
||||
#
|
||||
FMT924469>OGFLYM,qAS,FLYMASTER:/155232h3720.70N/00557.97W^222/092/A=000029 !W52!
|
||||
FMT003549>OGFLYM,qAS,FLYMASTER:/155231h3751.35N/00126.13W^270/022/A=001430 !W14!
|
||||
FMT001300>OGFLYM,qAS,FLYMASTER:/155249h3706.99N/00807.27W^178/000/A=000131 !W86!
|
||||
FMT798890>OGFLYM,qAS,FLYMASTER:/155256h3720.49N/00558.27W^234/086/A=000009 !W00!
|
||||
FMT549112>OGFLYM,qAS,FLYMASTER:/155256h3720.48N/00558.27W^234/086/A=000032 !W81!
|
||||
FMT148694>OGFLYM,qAS,FLYMASTER:/155244h3720.58N/00558.11W^226/087/A=000019 !W81!
|
||||
FMT842374>OGFLYM,qAS,FLYMASTER:/155302h3720.44N/00558.34W^236/082/A=000013 !W88!
|
||||
FMT003725>OGFLYM,qAS,FLYMASTER:/155304h3652.58N/00255.91W^346/000/A=001968 !W66!
|
||||
FMT924469>OGFLYM,qAS,FLYMASTER:/155306h3720.42N/00558.40W^250/081/A=000013 !W85!
|
||||
FMT003549>OGFLYM,qAS,FLYMASTER:/155316h3751.64N/00126.08W^020/048/A=001322 !W98!
|
||||
FMT148694>OGFLYM,qAS,FLYMASTER:/155318h3720.42N/00558.59W^282/088/A=000026 !W85!
|
||||
FMT549112>OGFLYM,qAS,FLYMASTER:/155328h3720.45N/00558.75W^282/079/A=000032 !W60!
|
||||
FMT842374>OGFLYM,qAS,FLYMASTER:/155335h3720.47N/00558.84W^280/078/A=000019 !W68!
|
||||
FMT001300>OGFLYM,qAS,FLYMASTER:/155339h3706.99N/00807.27W^178/000/A=000131 !W95!
|
||||
FMT798890>OGFLYM,qAS,FLYMASTER:/155338h3720.48N/00558.89W^282/080/A=000019 !W46!
|
||||
FMT924469>OGFLYM,qAS,FLYMASTER:/155341h3720.49N/00558.93W^282/075/A=000009 !W27!
|
||||
FMT003725>OGFLYM,qAS,FLYMASTER:/155346h3652.58N/00255.91W^346/000/A=001971 !W75!
|
||||
FMT003549>OGFLYM,qAS,FLYMASTER:/155349h3751.76N/00125.91W^064/032/A=001414 !W27!
|
||||
FMT148694>OGFLYM,qAS,FLYMASTER:/155352h3720.51N/00559.02W^292/026/A=000026 !W48!
|
||||
FMT549112>OGFLYM,qAS,FLYMASTER:/155400h3720.52N/00559.06W^298/031/A=000045 !W74!
|
||||
FMT842374>OGFLYM,qAS,FLYMASTER:/155409h3720.54N/00559.10W^302/019/A=000042 !W70!
|
||||
FMT798890>OGFLYM,qAS,FLYMASTER:/155412h3720.54N/00559.10W^304/001/A=000026 !W96!
|
||||
FMT924469>OGFLYM,qAS,FLYMASTER:/155415h3720.54N/00559.10W^000/001/A=000022 !W95!
|
||||
FMT003725>OGFLYM,qAS,FLYMASTER:/155420h3652.58N/00255.91W^346/000/A=001971 !W75!
|
||||
FMT003549>OGFLYM,qAS,FLYMASTER:/155422h3751.81N/00125.73W^220/002/A=001584 !W42!
|
||||
FMT001300>OGFLYM,qAS,FLYMASTER:/155429h3706.99N/00807.27W^178/000/A=000131 !W96!
|
||||
FMT148694>OGFLYM,qAS,FLYMASTER:/155435h3720.58N/00559.16W^314/017/A=000039 !W83!
|
||||
FMT549112>OGFLYM,qAS,FLYMASTER:/155443h3720.59N/00559.16W^000/000/A=000065 !W18!
|
||||
FMT798890>OGFLYM,qAS,FLYMASTER:/155444h3720.59N/00559.16W^000/000/A=000039 !W29!
|
||||
FMT924469>OGFLYM,qAS,FLYMASTER:/155447h3720.59N/00559.16W^000/000/A=000039 !W28!
|
||||
FMT842374>OGFLYM,qAS,FLYMASTER:/155453h3720.60N/00559.17W^316/020/A=000055 !W07!
|
||||
FMT003549>OGFLYM,qAS,FLYMASTER:/155455h3751.82N/00125.81W^248/012/A=001676 !W99!
|
|
@ -0,0 +1,4 @@
|
|||
# The following beacons are example for Garmin inReach APRS format
|
||||
# source: https://github.com/glidernet/ogn-aprs-protocol
|
||||
#
|
||||
OGN8A0749>OGINRE,qAS,INREACH:/142700h0448.38N/07600.74W'000/000/A=004583 id300434060496190 inReac True
|
|
@ -0,0 +1,5 @@
|
|||
# The following beacons are example for PilotAware's APRS format version OGPAW-1
|
||||
# source: https://github.com/glidernet/ogn-aprs-protocol
|
||||
#
|
||||
ICA404EC3>OGPAW,qAS,UKWOG:/104337h5211.24N\00032.65W^124/081/A=004026 !W62! id21404EC3 12.5dB +2.2kHz
|
||||
ICA404EC3>OGPAW,qAS,UKWOG:/104341h5211.18N\00032.53W^131/081/A=004010 !W85! id21404EC3 9.2dB +2.2kHz +10.0dBm
|
|
@ -13,4 +13,8 @@ LZHL>OGNSDR,TCPIP*,qAC,GLIDERN3:>132457h v0.2.7.arm CPU:0.9 RAM:75.3/253.6MB NTP
|
|||
BELG>OGNSDR,TCPIP*,qAC,GLIDERN3:/132507h4509.60NI00919.20E&/A=000246
|
||||
BELG>OGNSDR,TCPIP*,qAC,GLIDERN3:>132507h v0.2.7.RPI-GPU CPU:1.2 RAM:35.7/455.2MB NTP:2.5ms/-5.3ppm +67.0C 1/1Acfts[1h] RF:+79+8.8ppm/+4.97dB/-0.0dB@10km[299]/+4.9dB@10km[2/3]
|
||||
Saleve>OGNSDR,TCPIP*,qAC,GLIDERN1:/132624h4607.70NI00610.41E&/A=004198 Antenna: chinese, on a pylon, 20 meter above ground
|
||||
Saleve>OGNSDR,TCPIP*,qAC,GLIDERN1:>132624h v0.2.7.arm CPU:1.7 RAM:812.3/1022.5MB NTP:1.8ms/+4.5ppm 0.000V 0.000A 3/4Acfts[1h] RF:+67+2.9ppm/+4.18dB/+11.7dB@10km[5018]/+17.2dB@10km[8/16]
|
||||
Saleve>OGNSDR,TCPIP*,qAC,GLIDERN1:>132624h v0.2.7.arm CPU:1.7 RAM:812.3/1022.5MB NTP:1.8ms/+4.5ppm 0.000V 0.000A 3/4Acfts[1h] RF:+67+2.9ppm/+4.18dB/+11.7dB@10km[5018]/+17.2dB@10km[8/16]
|
||||
#
|
||||
# With OGN software 0.2.8 we got the latency
|
||||
#
|
||||
SCVH>OGNSDR,TCPIP*,qAC,GLIDERN4:>153734h v0.2.8.RPI-GPU CPU:0.3 RAM:744.5/968.2MB NTP:3.6ms/+2.0ppm +68.2C 3/3Acfts[1h] Lat:1.6s RF:-8+67.8ppm/+10.33dB/+1.3dB@10km[30998]/+10.4dB@10km[3/5]
|
|
@ -8,3 +8,4 @@
|
|||
ICA3E7540>OGSPOT,qAS,SPOT:/161427h1448.35S/04610.86W'000/000/A=008677 id0-2860357 SPOT3 GOOD
|
||||
ICA3E7540>OGSPOT,qAS,SPOT:/162923h1431.99S/04604.33W'000/000/A=006797 id0-2860357 SPOT3 GOOD
|
||||
ICA3E7540>OGSPOT,qAS,SPOT:/163421h1430.38S/04604.43W'000/000/A=007693 id0-2860357 SPOT3 GOOD
|
||||
FLRDF0CBA>OGSPOT,qAS,SPOT:/145808h3317.84S/07021.04W'000/000/A=010085 id0-2120121 SPOTCONNECT GOOD
|
|
@ -6,4 +6,6 @@ FLRDD9C70>OGNTRK,OGN2FD00F*,qAS,LZHL:/093214h4848.77N/01708.33E'000/000/A=000515
|
|||
FLRDD9C70>OGNTRK,OGN2FD00F*,qAS,LZHL:/093021h4848.77N/01708.33E'000/000/A=000518 !W66! id06DD9C70 -019fpm +0.0rot 29.0dB 0e -0.8kHz gps2x3 s6.09 h03
|
||||
OGN03AF2A>OGNTRK,qAS,LZHL:/092912h4848.77N/01708.33E'000/000/A=000535 !W53! id0703AF2A +000fpm +0.0rot FL003.15 4.5dB 1e -0.1kHz gps4x5 -11.2dBm
|
||||
OGN2FD00F>OGNTRK,qAS,LZHL:>092840h h00 v00 11sat/2 165m 1001.9hPa +27.1degC 0% 3.28V 14/-111.5dBm 127/min
|
||||
FLRDD9C70>OGNTRK,RELAY*,qAS,LZHL:/094124h4848.78N/01708.33E'000/000/A=000397 !W15! id06DD9C70 +099fpm +0.0rot 24.5dB 0e -1.4kHz gps10x15
|
||||
FLRDD9C70>OGNTRK,RELAY*,qAS,LZHL:/094124h4848.78N/01708.33E'000/000/A=000397 !W15! id06DD9C70 +099fpm +0.0rot 24.5dB 0e -1.4kHz gps10x15
|
||||
OGN7402C8>OGNTRK,qAS,OxfBarton:>055357h h02 v01
|
||||
OGN395F39>OGNTRK,qAS,OxfBarton:>055451h Pilot=Pawel Hard=DIY/STM32
|
Ładowanie…
Reference in New Issue