parser.py
Go to the documentation of this file.
1 # Software License Agreement (BSD License)
2 #
3 # Copyright (c) 2013, Eric Perko
4 # All rights reserved.
5 #
6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions
8 # are met:
9 #
10 # * Redistributions of source code must retain the above copyright
11 # notice, this list of conditions and the following disclaimer.
12 # * Redistributions in binary form must reproduce the above
13 # copyright notice, this list of conditions and the following
14 # disclaimer in the documentation and/or other materials provided
15 # with the distribution.
16 # * Neither the names of the authors nor the names of their
17 # affiliated organizations may be used to endorse or promote products derived
18 # from this software without specific prior written permission.
19 #
20 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
23 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
24 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
25 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
26 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31 # POSSIBILITY OF SUCH DAMAGE.
32 
33 """Parsing functions for NMEA sentence strings."""
34 
35 import re
36 import datetime
37 import calendar
38 import math
39 import logging
40 
41 
42 logger = logging.getLogger('rosout')
43 
44 
45 def safe_float(field):
46  """Convert field to a float.
47 
48  Args:
49  field: The field (usually a str) to convert to float.
50 
51  Returns:
52  The float value represented by field or NaN if float conversion throws a ValueError.
53  """
54  try:
55  return float(field)
56  except ValueError:
57  return float('NaN')
58 
59 
60 def safe_int(field):
61  """Convert field to an int.
62 
63  Args:
64  field: The field (usually a str) to convert to int.
65 
66  Returns:
67  The int value represented by field or 0 if int conversion throws a ValueError.
68  """
69  try:
70  return int(field)
71  except ValueError:
72  return 0
73 
74 
75 def convert_latitude(field):
76  """Convert a latitude string to floating point decimal degrees.
77 
78  Args:
79  field (str): Latitude string, expected to be formatted as DDMM.MMM, where
80  DD is the latitude degrees, and MM.MMM are the minutes latitude.
81 
82  Returns:
83  Floating point latitude in decimal degrees.
84  """
85  return safe_float(field[0:2]) + safe_float(field[2:]) / 60.0
86 
87 
88 def convert_longitude(field):
89  """Convert a longitude string to floating point decimal degrees.
90 
91  Args:
92  field (str): Longitude string, expected to be formatted as DDDMM.MMM, where
93  DDD is the longitude degrees, and MM.MMM are the minutes longitude.
94 
95  Returns:
96  Floating point latitude in decimal degrees.
97  """
98  return safe_float(field[0:3]) + safe_float(field[3:]) / 60.0
99 
100 
101 def convert_time(nmea_utc):
102  """Extract time info from a NMEA UTC time string and use it to generate a UNIX epoch time.
103 
104  Time information (hours, minutes, seconds) is extracted from the given string and augmented
105  with the date, which is taken from the current system time on the host computer (i.e. UTC now).
106  The date ambiguity is resolved by adding a day to the current date if the host time is more than
107  12 hours behind the NMEA time and subtracting a day from the current date if the host time is
108  more than 12 hours ahead of the NMEA time.
109 
110  Args:
111  nmea_utc (str): NMEA UTC time string to convert. The expected format is HHMMSS.SS where
112  HH is the number of hours [0,24), MM is the number of minutes [0,60),
113  and SS.SS is the number of seconds [0,60) of the time in UTC.
114 
115  Returns:
116  tuple(int, int): 2-tuple of (unix seconds, nanoseconds) if the sentence contains valid time.
117  tuple(float, float): 2-tuple of (NaN, NaN) if the sentence does not contain valid time.
118  """
119  # If one of the time fields is empty, return NaN seconds
120  if not nmea_utc[0:2] or not nmea_utc[2:4] or not nmea_utc[4:6]:
121  return (float('NaN'), float('NaN'))
122 
123  # Get current time in UTC for date information
124  utc_time = datetime.datetime.utcnow()
125  hours = int(nmea_utc[0:2])
126  minutes = int(nmea_utc[2:4])
127  seconds = int(nmea_utc[4:6])
128  nanosecs = int(nmea_utc[7:]) * pow(10, 9 - len(nmea_utc[7:]))
129 
130  # Resolve the ambiguity of day
131  day_offset = int((utc_time.hour - hours)/12.0)
132  utc_time += datetime.timedelta(day_offset)
133  utc_time.replace(hour=hours, minute=minutes, second=seconds)
134 
135  unix_secs = calendar.timegm(utc_time.timetuple())
136  return (unix_secs, nanosecs)
137 
138 
139 def convert_time_rmc(date_str, time_str):
140  """Convert a NMEA RMC date string and time string to UNIX epoch time.
141 
142  Args:
143  date_str (str): NMEA UTC date string to convert, formatted as DDMMYY.
144  nmea_utc (str): NMEA UTC time string to convert. The expected format is HHMMSS.SS where
145  HH is the number of hours [0,24), MM is the number of minutes [0,60),
146  and SS.SS is the number of seconds [0,60) of the time in UTC.
147 
148  Returns:
149  tuple(int, int): 2-tuple of (unix seconds, nanoseconds) if the sentence contains valid time.
150  tuple(float, float): 2-tuple of (NaN, NaN) if the sentence does not contain valid time.
151  """
152  # If one of the time fields is empty, return NaN seconds
153  if not time_str[0:2] or not time_str[2:4] or not time_str[4:6]:
154  return (float('NaN'), float('NaN'))
155 
156  pc_year = datetime.date.today().year
157 
158  # Resolve the ambiguity of century
159  """
160  example 1: utc_year = 99, pc_year = 2100
161  years = 2100 + int((2100 % 100 - 99) / 50.0) = 2099
162  example 2: utc_year = 00, pc_year = 2099
163  years = 2099 + int((2099 % 100 - 00) / 50.0) = 2100
164  """
165  utc_year = int(date_str[4:6])
166  years = pc_year + int((pc_year % 100 - utc_year) / 50.0)
167 
168  months = int(date_str[2:4])
169  days = int(date_str[0:2])
170 
171  hours = int(time_str[0:2])
172  minutes = int(time_str[2:4])
173  seconds = int(time_str[4:6])
174  nanosecs = int(time_str[7:]) * pow(10, 9 - len(time_str[7:]))
175 
176  unix_secs = calendar.timegm((years, months, days, hours, minutes, seconds))
177  return (unix_secs, nanosecs)
178 
179 
180 def convert_status_flag(status_flag):
181  """Convert a NMEA RMB/RMC status flag to bool.
182 
183  Args:
184  status_flag (str): NMEA status flag, which should be "A" or "V"
185 
186  Returns:
187  True if the status_flag is "A" for Active.
188  """
189  if status_flag == "A":
190  return True
191  elif status_flag == "V":
192  return False
193  else:
194  return False
195 
196 
198  """Convert a speed in knots to meters per second.
199 
200  Args:
201  knots (float, int, or str): Speed in knots.
202 
203  Returns:
204  The value of safe_float(knots) converted from knots to meters/second.
205  """
206  return safe_float(knots) * 0.514444444444
207 
208 
210  """Convert an angle in degrees to radians.
211 
212  This wrapper is needed because math.radians doesn't accept non-numeric inputs.
213 
214  Args:
215  degs (float, int, or str): Angle in degrees
216 
217  Returns:
218  The value of safe_float(degs) converted from degrees to radians.
219  """
220  return math.radians(safe_float(degs))
221 
222 
223 parse_maps = {
224  "GGA": [
225  ("fix_type", int, 6),
226  ("latitude", convert_latitude, 2),
227  ("latitude_direction", str, 3),
228  ("longitude", convert_longitude, 4),
229  ("longitude_direction", str, 5),
230  ("altitude", safe_float, 9),
231  ("mean_sea_level", safe_float, 11),
232  ("hdop", safe_float, 8),
233  ("num_satellites", safe_int, 7),
234  ("utc_time", convert_time, 1),
235  ],
236  "RMC": [
237  ("fix_valid", convert_status_flag, 2),
238  ("latitude", convert_latitude, 3),
239  ("latitude_direction", str, 4),
240  ("longitude", convert_longitude, 5),
241  ("longitude_direction", str, 6),
242  ("speed", convert_knots_to_mps, 7),
243  ("true_course", convert_deg_to_rads, 8),
244  ],
245  "GST": [
246  ("utc_time", convert_time, 1),
247  ("ranges_std_dev", safe_float, 2),
248  ("semi_major_ellipse_std_dev", safe_float, 3),
249  ("semi_minor_ellipse_std_dev", safe_float, 4),
250  ("semi_major_orientation", safe_float, 5),
251  ("lat_std_dev", safe_float, 6),
252  ("lon_std_dev", safe_float, 7),
253  ("alt_std_dev", safe_float, 8),
254  ],
255  "HDT": [
256  ("heading", safe_float, 1),
257  ],
258  "VTG": [
259  ("true_course", safe_float, 1),
260  ("speed", convert_knots_to_mps, 5)
261  ]
262 }
263 """A dictionary that maps from sentence identifier string (e.g. "GGA") to a list of tuples.
264 Each tuple is a three-tuple of (str: field name, callable: conversion function, int: field index).
265 The parser splits the sentence into comma-delimited fields. The string value of each field is passed
266 to the appropriate conversion function based on the field index."""
267 
268 
269 def parse_nmea_sentence(nmea_sentence):
270  """Parse a NMEA sentence string into a dictionary.
271 
272  Args:
273  nmea_sentence (str): A single NMEA sentence of one of the types in parse_maps.
274 
275  Returns:
276  A dict mapping string field names to values for each field in the NMEA sentence or
277  False if the sentence could not be parsed.
278  """
279  # Check for a valid nmea sentence
280 
281  if not re.match(
282  r'(^\$GP|^\$GN|^\$GL|^\$IN).*\*[0-9A-Fa-f]{2}$', nmea_sentence):
283  logger.debug(
284  "Regex didn't match, sentence not valid NMEA? Sentence was: %s" %
285  repr(nmea_sentence))
286  return False
287  fields = [field.strip(',') for field in nmea_sentence.split(',')]
288 
289  # Ignore the $ and talker ID portions (e.g. GP)
290  sentence_type = fields[0][3:]
291 
292  if sentence_type not in parse_maps:
293  logger.debug("Sentence type %s not in parse map, ignoring."
294  % repr(sentence_type))
295  return False
296 
297  parse_map = parse_maps[sentence_type]
298 
299  parsed_sentence = {}
300  for entry in parse_map:
301  parsed_sentence[entry[0]] = entry[1](fields[entry[2]])
302 
303  if sentence_type == "RMC":
304  parsed_sentence["utc_time"] = convert_time_rmc(fields[9], fields[1])
305 
306  return {sentence_type: parsed_sentence}
def convert_time_rmc(date_str, time_str)
Definition: parser.py:139
def convert_status_flag(status_flag)
Definition: parser.py:180
def convert_time(nmea_utc)
Definition: parser.py:101
def convert_deg_to_rads(degs)
Definition: parser.py:209
def parse_nmea_sentence(nmea_sentence)
Definition: parser.py:269
def convert_latitude(field)
Definition: parser.py:75
def convert_longitude(field)
Definition: parser.py:88
def convert_knots_to_mps(knots)
Definition: parser.py:197


nmea_navsat_driver
Author(s): Eric Perko , Steven Martin
autogenerated on Mon Feb 28 2022 22:57:09