API

common

Some common framework things.

nmeatools.common.logged(class_)

Class-level decorator to insert logging.

This assures that a class has a .log member.

@logged
class Something:
    def __init__(self, args):
        self.log(f"init with {args}")
class nmeatools.common.Logging(**kw)

Logging context manager.

with Logging(stream=sys.stderr, level=logging.INFO):
    do the work.

This guarantees that logging.shutdown() is run on exit.

haversine

Haversine computation.

Defines a handy function for distance in Nautical Miles.

nmeatools.haversine.nm_haversine(lat_1, lon_1, lat_2, lon_2)

Computes distance in NM

Parameters:
  • lat_1 – Latitude of point 1
  • lon_1 – Longitude of point 1
  • lat_2 – Latitude of point 2
  • lon_2 – Longitude of point 2
Returns:

distance, NM

nmeatools.haversine.haversine(lat_1: float, lon_1: float, lat_2: float, lon_2: float, R: float = 3440) → float

Distance between points.

a = \sqrt { \sin^2(\frac{\Delta_{lat}}{2}) + \cos(lat_1) \cos(lat_2) \sin^2(\frac{\Delta_{lon}}{2}) }

c = 2R \arcsin{a}

R is radius, R=MI computes in miles. Default is nautical miles.

Parameters:
  • lat_1 – Latitude of point 1
  • lon_1 – Longitude of point 1
  • lat_2 – Latitude of point 2
  • lon_2 – Longitude of point 2
  • R – Mean earth radius in desired units. R=NM is the default.
Returns:

distance based on units of R.

>>> round(haversine(36.12, -86.67, 33.94, -118.40, R=6372.8), 5)
2887.25995

nmea_capture

Capture Waypoints or Routes from Chart Plotter.

usage: nmea_capture.py [-h] [--output OUTPUT] [--baud BAUD]
                       [--timeout TIMEOUT]
                       input

Arguments

input

The device to monitor. Usually /dev/cu.usbserial-A6009TFG

Options

-h, --help

show this help message and exit

--output OUTPUT, -o OUTPUT

The file to write the captured NMEA data to. This will be in JSON format and can be used by waypoint_to_gpx.

--baud BAUD

BAUD setting, default is 4800

--timeout TIMEOUT

Timeout setting, default is 2 seconds

Description

This an an interactive exercise between the computer capturing the data and the chartplotter producing the data.

Chartplotter This App
  1. Start chart plotter.
 
  1. Navigate to Waypoints or Route send operation.
 
 
  1. Start capture.n``python3 nmeatools.nmea_capture -o data /dev/cu.usbserial-A6009TFG``
  1. Start Send.
Watch . and + to confirm receipt.
 
  1. Stop capture, saving the file. ^C
nmeatools.nmea_capture.capture(target_file, sentence_source)

Write captured messages to the target file.

Parameters:
  • target_file – an open file to which JSON text is written.
  • sentence_source – an iterable source of sentences.
nmeatools.nmea_capture.get_options(argv)

Parses command-line options.

Parameters:argv – Command-line options from sys.argv[1:].
Returns:options namespace.
nmeatools.nmea_capture.main()

Main process for conversion: parse options, gather data until ^C, then writes the output file with the captured sentences.

nmeatools.nmea_capture.sentence_iter(options)

Filtered reader of sentnces. Rejects any sentences from the background list.

Currently, the reject list is:

('GPRMC', 'GPGGA', 'GPGLL', 'GPGSA', 'GPGSV', 'GPVTG', 'GPZDA', 'GPXTE')
Parameters:options – Options namespace, must have the following items. :input: the mounted device, often /dev/cu.usbserial-A6009TFG :baud: the baud rate to use, generally 4800 :timeout: the timeout, generally 2 seconds
Returns:yields individual sentences that are not in a list of background messages.

nmea_data_eager

Define NMEA Sentences. This eagerly populates many fields from the source bytes.

Messages that are captured and (to an extent) parsed.

  • $GPRMC - Recommended Minimum Specific GPS/TRANSIT Data
  • $GPGGA - Global Positioning System Fix Data
  • $GPGLL - Geographic position, latitude / longitude
  • $GPGSA - GPS DOP and active satellites
  • $GPGSV - GPS Satellites in view
  • $GPVTG - Track made good and ground speed
  • $GPZDA - Date & Time
  • $GPXTE - Cross-track error, Measured
  • $GPWPL - Waypoint (example: b‘3845.363’, b’N’, b‘07629.551’, b’W’, b’FISHTRP’)
  • $GPRTE - Route

These are not (currently) interpreted.

  • $GPDBT - Depth Below Transducer
  • $GPDPT - Depth
  • $GPMTW - Water Temperature
  • $GPVHW - Water Speed and Heading
class nmeatools.nmea_data_eager.Decoder(*args, **kw)

Decode a sentence from JSON notation. This re-applies the class definition, computing and derived values from the original bytes.

Note that the JSON document doesn’t really work in bytes. We had two choices: base64 encode the original bytes, or trust that the bytes were only a subset of printable ASCII characters that overlap with UTF-8.

We chose the latter approach. The input is text, which overlaps with ASCII. We need to encode it into ASCII to recover the bytes. These are then passed to the Sentence_Factory to recover Sentence instances.

>>> object = GPWPL(b'GPWPL',b'5128.62',b'N',b'00027.58',b'W',b'EGLL')
>>> object
GPWPL 51°28.62′N 0°27.58′W EGLL
>>> text = Encoder().encode(object)
>>> new_object = Decoder().decode(text)
>>> new_object
GPWPL 51°28.62′N 0°27.58′W EGLL
nmea_object_hook(as_dict)
class nmeatools.nmea_data_eager.Encoder(*, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False, indent=None, separators=None, default=None)

Encode the sentence into JSON. This dumps the raw _args value, ignoring all derived values. This allows a change to the class definition; when the JSON is decoded, additional or different values will be created from the original raw data.

Note that the JSON document doesn’t really work in bytes. We had two choices: base64 encode the original bytes, or trust that the bytes were only a subset of printable ASCII characters that overlap with UTF-8.

We chose the latter approach. The output is text, which overlaps with ASCII. This allows the Sentence_Factory and the various subclasses of Sentence to use text internally.

>>> object = GPWPL(b'GPWPL',b'5128.62',b'N',b'00027.58',b'W',b'EGLL')
>>> object
GPWPL 51°28.62′N 0°27.58′W EGLL
>>> text = Encoder(sort_keys=True, indent=2).encode(object)
>>> print(text)
{
  "_args": [
    "5128.62",
    "N",
    "00027.58",
    "W",
    "EGLL"
  ],
  "_class": "GPWPL",
  "_name": "GPWPL"
}
default(obj)
log = <Logger Encoder (WARNING)>
class nmeatools.nmea_data_eager.Field(title, name, conversion)
conversion

Alias for field number 2

name

Alias for field number 1

title

Alias for field number 0

class nmeatools.nmea_data_eager.GPGGA(*args)

Fix data

fields = [Field(title='UTC Time', name='time_utc', conversion=<function utc_time>), Field(title='Latitude', name='lat', conversion=<function lat>), Field(title='N/S Indicator', name='lat_h', conversion=<function text>), Field(title='Longitude', name='lon', conversion=<function lon>), Field(title='E/W Indicator', name='lon_h', conversion=<function text>), Field(title='Position Fix', name='fix', conversion=<function text>), Field(title='Satellites Used', name='sat_used', conversion=<function nint>), Field(title='Horizontal dilution of precision (HDOP)', name='hdop', conversion=<function nfloat>), Field(title='Altitude in meters according to WGS-84 ellipsoid', name='alt', conversion=<function nfloat>), Field(title='Altitude Units', name='units_alt', conversion=<function text>), Field(title='Geoid seperation in meters according to WGS-84 ellipsoid', name='geoid_sep', conversion=<function nfloat>), Field(title='Seperation Units', name='units_sep', conversion=<function text>), Field(title='Age of DGPS data in seconds', name='age', conversion=<function nfloat>), Field(title='DGPS Station ID', name='station', conversion=<function text>)]
class nmeatools.nmea_data_eager.GPGLL(*args)

Position

fields = [Field(title='Latitude', name='lat', conversion=<function lat>), Field(title='N/S Indicator', name='lat_h', conversion=<function text>), Field(title='Longitude', name='lon', conversion=<function lon>), Field(title='E/W Indicator', name='lon_h', conversion=<function text>), Field(title='UTC Time', name='time_utc', conversion=<function utc_time>), Field(title='Status', name='status', conversion=<function text>)]
class nmeatools.nmea_data_eager.GPGSA(*args)

Active satellites

fields = [Field(title='Mode 1', name='mode1', conversion=<function text>), Field(title='Mode 2', name='mode2', conversion=<function text>), Field(title='Satellite used on channel PRN', name='prn_00', conversion=<function text>), Field(title='PRN01', name='prn_01', conversion=<function text>), Field(title='PRN02', name='prn_02', conversion=<function text>), Field(title='PRN03', name='prn_03', conversion=<function text>), Field(title='PRN04', name='prn_04', conversion=<function text>), Field(title='PRN05', name='prn_05', conversion=<function text>), Field(title='PRN06', name='prn_06', conversion=<function text>), Field(title='PRN07', name='prn_07', conversion=<function text>), Field(title='PRN08', name='prn_08', conversion=<function text>), Field(title='PRN09', name='prn_09', conversion=<function text>), Field(title='PRN10', name='prn_10', conversion=<function text>), Field(title='PRN11', name='prn_11', conversion=<function text>), Field(title='Position dilution of precision (PDOP)', name='pdop', conversion=<function nfloat>), Field(title='Horizontal dilution of precision (HDOP)', name='hdop', conversion=<function nfloat>), Field(title='Vertical dilution of precision (VDOP)', name='vdop', conversion=<function nfloat>)]
class nmeatools.nmea_data_eager.GPGSV(*args)

Satellites in view

fields = [Field(title='Number of messages (1 to 9)', name='num', conversion=<function nint>), Field(title='Sequence number', name='seq', conversion=<function nint>), Field(title='Satellites in view', name='satinview', conversion=<function nint>), Field(title='Satellite ID 1 (1-32)', name='sat1_id', conversion=<function nint>), Field(title='Elevation in degrees (0-90)', name='sat1_el', conversion=<function nint>), Field(title='Azimuth in degrees (0-359)', name='sat1_az', conversion=<function nint>), Field(title='Signal to noise ration in dBHZ (0-99)', name='sat1_sn', conversion=<function nint>), Field(title='Satellite ID 2 (1-32)', name='sat2_id', conversion=<function nint>), Field(title='Elevation in degrees (0-90)', name='sat2_el', conversion=<function nint>), Field(title='Azimuth in degrees (0-359)', name='sat2_az', conversion=<function nint>), Field(title='Signal to noise ration in dBHZ (0-99)', name='sat2_sn', conversion=<function nint>), Field(title='Satellite ID 3 (1-32)', name='sat3_id', conversion=<function nint>), Field(title='Elevation in degrees (0-90)', name='sat3_el', conversion=<function nint>), Field(title='Azimuth in degrees (0-359)', name='sat3_az', conversion=<function nint>), Field(title='Signal to noise ration in dBHZ (0-99)', name='sat3_sn', conversion=<function nint>), Field(title='Satellite ID 4 (1-32)', name='sat4_id', conversion=<function nint>), Field(title='Elevation in degrees (0-90)', name='sat4_el', conversion=<function nint>), Field(title='Azimuth in degrees (0-359)', name='sat4_az', conversion=<function nint>), Field(title='Signal to noise ration in dBHZ (0-99)', name='sat4_sn', conversion=<function nint>)]
class nmeatools.nmea_data_eager.GPRMC(*args)

Position and time

fields = [Field(title='UTC Time', name='time_utc', conversion=<function utc_time>), Field(title='Status', name='status', conversion=<function text>), Field(title='Latitude', name='lat', conversion=<function lat>), Field(title='N/S Indicator', name='lat_h', conversion=<function text>), Field(title='Longitude', name='lon', conversion=<function lon>), Field(title='E/W Indicator', name='lon_h', conversion=<function text>), Field(title='Speed over ground', name='sog', conversion=<function nfloat>), Field(title='Course over ground', name='cog', conversion=<function nfloat>), Field(title='UTC Date', name='date_utc', conversion=<function utc_date>), Field(title='Magnetic variation', name='mag_var', conversion=<function nfloat>), Field(title='Magnetic variation', name='mag_var_flag', conversion=<function text>)]
class nmeatools.nmea_data_eager.GPRTE(*args)

GP Route

Examples:

$GPRTE,2,1,c,0,PBRCPK,PBRTO,PTELGR,PPLAND,PYAMBU,PPFAIR,PWARRN,PMORTL,PLISMR*73
$GPRTE,2,2,c,0,PCRESY,GRYRIE,GCORIO,GWERR,GWESTG,7FED*34
       1 2 3 4 5 ..
       
1. Number of sentences in sequence
2. Sentence number
3. 'c' = Current active route, 'w' = waypoint list starts with destination waypoint
4. Name or number of the active route
5. Rest of the body is the names of waypoints in Route
fields = [Field(title='Number of sentences in sequence', name='length', conversion=<function nint>), Field(title='Sentence number', name='sentence', conversion=<function nint>), Field(title='Current or Waypoint', name='status', conversion=<function text>), Field(title='Name or Number', name='id', conversion=<function text>)]
class nmeatools.nmea_data_eager.GPVTG(*args)

Course over ground

fields = [Field(title='Course in degrees', name='course_1', conversion=<function nfloat>), Field(title='Reference, T = True heading', name='ref_1', conversion=<function text>), Field(title='Course in degrees', name='course_2', conversion=<function nfloat>), Field(title='Reference, M = Magnetic heading', name='ref_2', conversion=<function text>), Field(title='Horizontal speed (SOG)', name='sog_1', conversion=<function nfloat>), Field(title='Units, N = Knots', name='units_sog_1', conversion=<function text>), Field(title='Horizontal Speed (SOG)', name='sog_2', conversion=<function nfloat>), Field(title='Units, К = Km/h', name='units_sog_2', conversion=<function text>)]
class nmeatools.nmea_data_eager.GPWPL(*args)

GP Waypoint Location

Examples:

$GPWPL,4917.16,N,12310.64,W,003*65
       1         2          3
       
  1. 4917.16,N    Latitude of waypoint. This is 49°16.17′N
  2. 12310.64,W   Longitude of waypoint. This is 123°10.64′W
  3. 003          Waypoint ID

$GPWPL,5128.62,N,00027.58,W,EGLL*59
       1       2 3        4 5

  1. 5128.62   Latitude of this waypoint
  2. N         North/South
  3. 00027.58  Longitude of this waypoint
  4. W         East/West
  5. EGLL      Ident of this waypoint
fields = [Field(title='Latitude', name='lat_src', conversion=<function lat>), Field(title='N/S Indicator', name='lat_h', conversion=<function text>), Field(title='Longitude', name='lon_src', conversion=<function lon>), Field(title='E/W Indicator', name='lon_h', conversion=<function text>), Field(title='Name', name='name', conversion=<function text>)]
class nmeatools.nmea_data_eager.GPXTE(*args)

Cross-Track Error, Measured

fields = [Field(title='General warning flag', name='warning', conversion=<function text>), Field(title='Not Used', name='not_used', conversion=<function text>), Field(title='cross track error distance', name='distance', conversion=<function nfloat>), Field(title='steer to correct (L/R)', name='steer', conversion=<function text>), Field(title='Units N = Nautical miles', name='units', conversion=<function text>)]
class nmeatools.nmea_data_eager.GPZDA(*args)

UTC Date and Time

fields = [Field(title='UTC Time', name='time_utc', conversion=<function utc_time>), Field(title='Day (01 to 31)', name='day', conversion=<function nint>), Field(title='Month (01 to 12)', name='month', conversion=<function nint>), Field(title='Year', name='year', conversion=<function nint>), Field(title='Time zone, GMT displacement, hours (00 to ± 13)', name='tz_hr', conversion=<function nint>), Field(title='Time zone, GMT displacement, minutes', name='tz_min', conversion=<function nint>)]
class nmeatools.nmea_data_eager.Sentence(*args)

Superclass for NMEA0183 Sentences.

Each subclass provides a value for fields. This sequence if Field objects is used to convert the items in the message from bytes to useful values.

There are two fields common to all sentences.

_name:The sentence type as text. The bytes are decoded from ASCII.
_args:The tuple with the original argument values as Python text. These have been decoded from ASCII, which is (perhaps) not the best idea, but it makes access simple.
fields = []
log = <Logger Sentence (WARNING)>
class nmeatools.nmea_data_eager.Sentence_Factory

Given a sequence of values, locate the class with a name that matches the sentence header and instantiate that class.

This examines all subclasses of Sentence. The class names must match the sentence header. If there’s no match, create an UnknownSentence instance.

Params args:The message fields.
Returns:Sentence instance.
>>> sf = Sentence_Factory()
>>> fields = b'GPVTG,59.53,T,,M,0.14,N,0.3,K'.split(b',')
>>> s = sf(*fields)
>>> s
GPVTG {'_args': ['59.53', 'T', '', 'M', '0.14', 'N', '0.3', 'K'],
 '_name': 'GPVTG',
 'course_1': 59.53,
 'course_2': None,
 'ref_1': 'T',
 'ref_2': 'M',
 'sog_1': 0.14,
 'sog_2': 0.3,
 'units_sog_1': 'N',
 'units_sog_2': 'K'}
log = <Logger Sentence_Factory (WARNING)>
sentence_class_map = {b'UnknownSentence': <class 'nmeatools.nmea_data_eager.UnknownSentence'>, b'GPRMC': <class 'nmeatools.nmea_data_eager.GPRMC'>, b'GPGGA': <class 'nmeatools.nmea_data_eager.GPGGA'>, b'GPGLL': <class 'nmeatools.nmea_data_eager.GPGLL'>, b'GPGSA': <class 'nmeatools.nmea_data_eager.GPGSA'>, b'GPGSV': <class 'nmeatools.nmea_data_eager.GPGSV'>, b'GPVTG': <class 'nmeatools.nmea_data_eager.GPVTG'>, b'GPZDA': <class 'nmeatools.nmea_data_eager.GPZDA'>, b'GPXTE': <class 'nmeatools.nmea_data_eager.GPXTE'>, b'GPWPL': <class 'nmeatools.nmea_data_eager.GPWPL'>, b'GPRTE': <class 'nmeatools.nmea_data_eager.GPRTE'>}
class nmeatools.nmea_data_eager.UnknownSentence(*args)

Fallback for NMEA0183 Sentences that aren’t otherwise parseable.

log = <Logger UnknownSentence (WARNING)>
nmeatools.nmea_data_eager.lat(source)

Convert source bytes to a latitude (deg, min) pair. Latitude: 2543.7024 = DDMM.MMMM

>>> lat(b'2543.7024')
(25, 43.7024)
nmeatools.nmea_data_eager.lon(source)

Convert source bytes to longitude (deg, mim) pair. Longitude: 08014.5267 = DDDMM.MMMM

>>> lon(b'08014.5267')
(80, 14.5267)
nmeatools.nmea_data_eager.nfloat(source)

Convert to float or None

>>> nfloat(b'')
>>> nfloat(b'123.45')
123.45
nmeatools.nmea_data_eager.nint(source)

Convert to int or None

>>> nint(b'')
>>> nint(b'123')
123
nmeatools.nmea_data_eager.sample_CP(listener)

Chart Plotter. The background message cycle is filtered out. Reads until Ctrl-C.

nmeatools.nmea_data_eager.sample_GPS(listener, limit=16)

GPS. Displays selected messages until some limit is reached.

nmeatools.nmea_data_eager.text(source)

Convert source bytes to text.

>>> text(b'xyz')
'xyz'
nmeatools.nmea_data_eager.utc_date(source)

Convert source bytes to UTC date. mmddyy

>>> utc_date(b'091056')
(9, 10, 56)
nmeatools.nmea_data_eager.utc_time(source)

Convert source bytes to UTC time as (H, M, S) triple HHMMSS.000

>>> utc_time(b'123456.000')
(12, 34, 56.0)

nmea_data_lazy

Define NMEA Sentences. This lazily builds fields from the source bytes using descriptors for the individual conversions.

Messages that are captured and (to an extent) parsed.

  • $GPWPL - Waypoint (example: b‘3845.363’, b’N’, b‘07629.551’, b’W’, b’FISHTRP’)
  • $GPRTE - Route

These are not (currently) interpreted.

  • $GPDBT - Depth Below Transducer
  • $GPDPT - Depth
  • $GPMTW - Water Temperature
  • $GPVHW - Water Speed and Heading
  • $GPRMC - Recommended Minimum Specific GPS/TRANSIT Data
  • $GPGGA - Global Positioning System Fix Data
  • $GPGLL - Geographic position, latitude / longitude
  • $GPGSA - GPS DOP and active satellites
  • $GPGSV - GPS Satellites in view
  • $GPVTG - Track made good and ground speed
  • $GPZDA - Date & Time
  • $GPXTE - Cross-track error, Measured

Unit Tests

>>> class Sample(Sentence):    
...    f0 = Integer(1, "Item One")
...    f1 = Float(2, "Item Two")
...    @property
...    def f2(self):
...        return self.f0 + self.f1
>>> s1 = Sample(b'Sample', b'1', b'2.3')
>>> s1.f0
1
>>> s1.f1
2.3
>>> s1.f2
3.3
>>> s2 = GPWPL(b'GPWPL', b'5128.62', b'N', b'00027.58', b'W', b'EGLL')
>>> import json
>>> txt = json.dumps(s2.to_json)
>>> print(txt)
{"_class": "GPWPL", "_args": ["GPWPL", "5128.62", "N", "00027.58", "W", "EGLL"]}
>>> obj = json.loads(txt, object_hook=decode)
>>> print(obj)
GPWPL 51°28.62′N 0°27.58′W EGLL
class nmeatools.nmea_data_lazy.Field(position, title, name=None, conversion=<function Field.<lambda>>)

Define a field. Provide the position, a description, and a conversion rule.

The name is there to parallel the nmea_data Namedtuple implementation.

Subclasses should include conversions directly. This can be used with conversion plug-in functions.

f = Text(1, "Description") is better than

f = Field(1, "Description", "f", text) which is better than

f = Field(1, "Description", "f", lambda b: b.decode('ascii'))

>>> class Sample(Sentence):
...     f = Field(1, "title", "f", lambda b: b.decode('ascii'))
>>> s = Sample(b'Sample', b'text')
>>> s.f
'text'
static transform(func, value)
class nmeatools.nmea_data_lazy.Float(position, title)
static nfloat(value)

Convert to float or None

>>> Float.nfloat(b'')
>>> Float.nfloat(b'123.45')
123.45
class nmeatools.nmea_data_lazy.GPRTE(*args)

GP Route

Examples:

$GPRTE,2,1,c,0,PBRCPK,PBRTO,PTELGR,PPLAND,PYAMBU,PPFAIR,PWARRN,PMORTL,PLISMR*73
$GPRTE,2,2,c,0,PCRESY,GRYRIE,GCORIO,GWERR,GWESTG,7FED*34
       1 2 3 4 5 ..
       
1. Number of sentences in sequence
2. Sentence number
3. 'c' = Current active route, 'w' = waypoint list starts with destination waypoint
4. Name or number of the active route
5. Rest of the body is the names of waypoints in Route
id = None
length = None
sentence = None
status = None
waypoints
class nmeatools.nmea_data_lazy.GPWPL(*args)

GP Waypoint Location

Examples:

$GPWPL,4917.16,N,12310.64,W,003*65
       1         2          3
       
  1. 4917.16,N    Latitude of waypoint. This is 49°16.17′N
  2. 12310.64,W   Longitude of waypoint. This is 123°10.64′W
  3. 003          Waypoint ID

$GPWPL,5128.62,N,00027.58,W,EGLL*59
       1       2 3        4 5

  1. 5128.62   Latitude of this waypoint
  2. N         North/South
  3. 00027.58  Longitude of this waypoint
  4. W         East/West
  5. EGLL      Ident of this waypoint
lat_angle = None
lat_h = None
latitude

Two source fields are combined: the angle and the hemisphere (N/S).

lon_angle = None
lon_h = None
longitude

Two source fields are combined: the angle and the hemisphere (E/W).

name = None
class nmeatools.nmea_data_lazy.Integer(position, title)
static nint(value)

Convert to int or None

>>> Integer.nint(b'')
>>> Integer.nint(b'123')
123
class nmeatools.nmea_data_lazy.LatAngle(position, title)

Note that the hemisphere information (N/S) isn’t present.

static lat(source)

Convert source bytes to latitude (deg, min) pair. Latitude: 2543.7024 = DDMM.MMMM

>>> LatAngle.lat(b'2543.7024')
(25, 43.7024)
class nmeatools.nmea_data_lazy.Latitude(pos_angle, pos_h, title)

Two source fields are combined: the angle and the hemisphere (N/S).

class nmeatools.nmea_data_lazy.LonAngle(position, title)

Note that the hemisphere information (E/W) isn’t present.

static lon(source)

Convert source bytes to longitude (deg, min) pair. Longitude: 08014.5267 = DDDMM.MMMM

>>> LonAngle.lon(b'08014.5267')
(80, 14.5267)
class nmeatools.nmea_data_lazy.Longitude(pos_angle, pos_h, title)

Two source fields are combined: the angle and the hemisphere (E/W).

class nmeatools.nmea_data_lazy.Sentence(*args)
>>> class Sample(Sentence):    
...     f0 = Integer(1, "Item One")
...     f1 = Float(2, "Item Two")
...     @property
...     def f2(self):
...         return self.f0 + self.f1
>>> s = Sample(b'Sample', b'1', b'2.3')
>>> s.f0
1
>>> s.f1
2.3
>>> s.f2
3.3

Can be converted from original fields = [...] lists.

>>> class Sample2(Sentence):
...     f0 = Field(1, "Item One", "f0", int)
...     f1 = Field(2, "Item Two", "f1", float)
...     @property
...     def f2(self):
...         return self.f0 + self.f1
>>> s = Sample2(b'Sample', b'1', b'2.3')
>>> s.f0
1
>>> s.f1
2.3
>>> s.f2
3.3
log = <Logger Sentence (WARNING)>
to_json
class nmeatools.nmea_data_lazy.Sentence_Factory

Given a sequence of values, locate the class with a name that matches the sentence header and instantiate that class.

This examines all subclasses of Sentence. The class names must match the sentence header. If there’s no match, create an UnknownSentence instance.

Params args:The message fields.
Returns:Sentence instance.
>>> sf = Sentence_Factory()
>>> fields1 = b'GPVTG,59.53,T,,M,0.14,N,0.3,K'.split(b',')
>>> s1 = sf(*fields1)
>>> s1
UnknownSentence(*(b'GPVTG', b'59.53', b'T', b'', b'M', b'0.14', b'N', b'0.3', b'K'))
>>> fields2 = b'GPWPL', b'5128.62', b'N', b'00027.58', b'W', b'EGLL'
>>> s2 = sf(*fields2)
>>> s2
GPWPL 51°28.62′N 0°27.58′W EGLL
log = <Logger Sentence_Factory (WARNING)>
class nmeatools.nmea_data_lazy.Text(position, title)
static text(value)
>>> Text.text(b'xyz')
'xyz'
class nmeatools.nmea_data_lazy.UTC_Date(position, title)
static utc_date(source)

Convert source bytes to UTC date. mmddyy

>>> UTC_Date.utc_date(b'091056')
(9, 10, 56)
class nmeatools.nmea_data_lazy.UTC_Time(position, title)
static utc_time(source)

Convert source bytes to UTC time as (H, M, S) triple HHMMSS.000

>>> UTC_Time.utc_time(b'123456.000')
(12, 34, 56.0)
class nmeatools.nmea_data_lazy.UnknownSentence(*args)

Fallback for NMEA0183 Sentences that aren’t otherwise parseable.

log = <Logger UnknownSentence (WARNING)>
nmeatools.nmea_data_lazy.decode(object)

nmea_device

Listener to NMEA interface.

Here’s an example of NMEA 0183 to USB hardware.

http://www.digitalyachtamerica.com/index.php/en/products/interfacing/nmeausb/product/67-usb-to-nmea-adaptor

PySerial is required. Using PySerial 3.3. https://pypi.python.org/pypi/pyserial

Typical interface to NMEA 0183 requires this kind of serial interface:

  • Typical Baud rate 4800
  • Data bits 8
  • Parity None
  • Stop bits 1
  • Handshake None

We’ll decode NMEA 0183 Sentences. See http://www.robosoft.info/en/technologies/knowledgebase/nmea0183

The checksum is the bitwise exclusive OR of ASCII codes of all characters between the $ and *. This is done with reduce(operator.xor, bytes)

The listener is an Interator as well as a Context Manager.

Also – since messages are ”,”-separated, it handles the split operation.

Unit Test

Note the subtle complexity of passing end-of-line in a string to doctest. We can’t simply use `` `` in a sample input string, or the compiler being used by doctest gets confused.

Good Messages:

>>> m0= b'''$GPRMC,162823.000,A,2542.9243,N,08013.6310,W,0.14,59.53,180214,,*2F
... '''
>>> m1= b'''$GPVTG,59.53,T,,M,0.14,N,0.3,K*5C
... '''
>>> m2= b'''$GPGGA,162824.000,2542.9243,N,08013.6311,W,1,06,1.5,3.3,M,-27.3,M,,0000*6E
... '''
>>> m3= b'''$GPGLL,2542.9243,N,08013.6310,W,162823.000,A*29
... '''
>>> m4= b'''$GPGSA,A,3,29,24,18,14,22,27,,,,,,,2.9,1.5,2.5*3E
... '''
>>> m5= b'''$GPGSV,3,1,10,21,82,249,18,24,54,090,37,18,52,343,33,15,32,039,34*7D
... '''
>>> m6= b'''$GPGSV,3,2,10,14,28,244,36,22,27,307,33,29,12,190,32,06,10,293,28*74
... '''
>>> m7= b'''$GPGSV,3,3,10,27,08,303,27,12,00,139,25*7F
... '''
>>> Listener.validate(m0)
(b'GPRMC', b'162823.000', b'A', b'2542.9243', b'N', b'08013.6310', b'W', b'0.14', b'59.53', b'180214', b'', b'')
>>> Listener.validate(m1)
(b'GPVTG', b'59.53', b'T', b'', b'M', b'0.14', b'N', b'0.3', b'K')
>>> Listener.validate(m2)
(b'GPGGA', b'162824.000', b'2542.9243', b'N', b'08013.6311', b'W', b'1', b'06', b'1.5', b'3.3', b'M', b'-27.3', b'M', b'', b'0000')
>>> Listener.validate(m4)
(b'GPGSA', b'A', b'3', b'29', b'24', b'18', b'14', b'22', b'27', b'', b'', b'', b'', b'', b'', b'2.9', b'1.5', b'2.5')
>>> Listener.validate(m5)
(b'GPGSV', b'3', b'1', b'10', b'21', b'82', b'249', b'18', b'24', b'54', b'090', b'37', b'18', b'52', b'343', b'33', b'15', b'32', b'039', b'34')
>>> Listener.validate(m6)
(b'GPGSV', b'3', b'2', b'10', b'14', b'28', b'244', b'36', b'22', b'27', b'307', b'33', b'29', b'12', b'190', b'32', b'06', b'10', b'293', b'28')
>>> Listener.validate(m7)
(b'GPGSV', b'3', b'3', b'10', b'27', b'08', b'303', b'27', b'12', b'00', b'139', b'25')

Broken Message, typical case:

>>> b0= b'''42.9243,N,08013.6310,W,0.14,59.53,180214,,*2F
... '''
>>> x= Listener.validate(b0)  
Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/doctest.py", line 1330, in __run
    compileflags, 1), test.globs)
  File "<doctest __main__[16]>", line 1, in <module>
    x= Scanner.validate(b0)
  File "gps_spike.py", line 143, in validate
    assert sentence_bytes.startswith(b'$'), "Sentence Fragment"
AssertionError: Sentence Fragment
class nmeatools.nmea_device.Listener(options)

Listen to the device, yielding a sequence of sentences.

This is an Interable and a Context Manager for listening to an NMEA-0183 device.

The typical use case is this

with Listener(options) as GPS:
    for sentnce in GPS:
        print(sentence)
log = <Logger Listener (WARNING)>
static validate(sentence_bytes)

Validate an NMEA sentence, returning either a tuple of substrings or an AssertionError.

Parameters:sentence_bytes – A bytes object with the raw sentence.
Returns:tuple with individual fields, suitable for use with a Sentence_Factory isntance.
Raises:AssertionError – If the sentence is incomplete in some way.

waypoint_merge

Merge waypoints from two sources:

  1. The master GPX file. These have precedence. Any duplicates here create warnings.
  2. A new GPX file. Duplicates from here are dropped.

The output is a combined list with duplicated locations removed. This becomes the new master.

class nmeatools.waypoint_merge.Waypoint(lat, lon, name, time, sym)

Behaves a little bit like an NMEA GPWPL sentence.

args
distance(other)

Distance in NM.

log = <Logger Waypoint (WARNING)>
nmeatools.waypoint_merge.merge(master_path=PosixPath('/Volumes/NO NAME/WaypointsRoutesTracks.gpx'), update_path=PosixPath('/Users/slott/Documents/Sailing/Cruise History/routes/waypoints.gpx'))
nmeatools.waypoint_merge.waypoint_iter(root, namespace)

waypoint_to_gpx

Convert waypoints or routes to GPX format for iNavX/GPSNavX

Synopsis

usage: waypoint_to_gpx.py [-h] --desc DESC [--force]
                          [--format {.gpx,.csv,.kml}]
                          [input [input ...]]

Arguments

input

One or more files to convert. These must be JSON-format files created by the nmea_capture program.

Options

optional arguments:

-h, --help

show this help message and exit

--desc DESC, -d DESC

Description

--force

Force overwrite of the .gpx output file.

--format {.gpx,.csv,.kml}, -f {.gpx,.csv,.kml}

The output formaat. Currently, only .gpx is supported and it’s the default.

nmeatools.waypoint_to_gpx.build_gpx(document, name, description)

Create the <gpx> Element, inserting metadata.

Parameters:
  • document – The root document
  • name – The string name to put in the metadata
  • description – the string description to put in the metadata
Returns:

the <gpx> element

nmeatools.waypoint_to_gpx.build_routepoint(document, s, sym=None)

Create a <rtept> element, inserting a <name> element (optionally a <sym>).

Parameters:
  • document – The root document
  • s – An nmeatools.nmea_data.GPWPL instance.
  • sym – An optional string with a symbol name to include.
Returns:

the <rtept> e,ement

nmeatools.waypoint_to_gpx.build_waypoint_location(document, s)

Create a <wpt> element, inserting a <name> element.

Parameters:
  • document – The root document
  • s – An nmeatools.nmea_data.GPWPL instance.
Returns:

the <wpt> e,ement

nmeatools.waypoint_to_gpx.convert_route(route_path, description='2017 Waypoints from Red Ranger chartplotter.')

Load JSON document with GPWPL and GPRTE sentences; return GPX representation.

Parameters:
  • route_path – Path with location of routes in JSON notation.
  • description – Description to insert into the metadata.
Returns:

root document with <gpx> and <rte> and <rtept> tags.

nmeatools.waypoint_to_gpx.convert_waypoints(waypoints_path, description='2017 Waypoints from Red Ranger chartplotter.')

Load JSON document with GPWPL sentences; return GPX representation.

Parameters:
  • waypoints_path – Path with location of waypoints in JSON notation.
  • description – Description to insert into the metadata.
Returns:

root document with <gpx> and <wpt> tags.

nmeatools.waypoint_to_gpx.get_options(argv)

Parses command-line options.

Parameters:argv – Command-line options from sys.argv[1:].
Returns:options namespace.
nmeatools.waypoint_to_gpx.main()

Main process for conversion: parse options, process files. Only the GPX output is supported currently.

Each file is scanned to see what it contains.

  • ‘GPRTE’, ‘GPWPL’ – a route
  • ‘GPWPL’ – only waypoints

The output path matches the input path with a suffix changed to .gpx.

nmeatools.waypoint_to_gpx.route_to_gpx(sentences, name, description)

Create GPX doc with a route that contains waypoints.

  1. Save waypoint as {name : sentence} map.
  2. Flatten route sentences into a single list of waypoints. This presumes the nmeatools.nmea_data.GPRTE sentences are already in their proper order.
Parameters:
  • sentences – iterable sequence of nmeatools.nmea_data.GPWPL and nmeatools.nmea_data.GPRTE instances
  • name – The string name to put in the metadata
  • description – the string description to put in the metadata
Returns:

<gpx> element containing the <rte> and <rtpte> waypoints.

nmeatools.waypoint_to_gpx.waypoints_to_gpx(sentences, name, description)

Create GPX doc with waypoints.

Parameters:
  • sentences – iterable sequence of nmeatools.nmea_data.GPWPL instances
  • name – The string name to put in the metadata
  • description – the string description to put in the metadata
Returns:

<gpx> element containing the <wpt> waypoints.