# -*- encoding: utf-8 -*- # -*- test-case-name: twisted.test.test_positioning -*- # Copyright (c) 2009 Twisted Matrix Laboratories. # See LICENSE for details. """ Classes for using NMEA sentences. """ import functools from twisted.protocols.positioning.base import Sentence, SentenceReceiver SENTENCE_CONTENTS = { 'GPGGA': [ ('timestamp', fixTimestamp), ('latitudeFloat', fixLatitudeHemisphere), ('latitudeHemisphere', fixLatitudeHemisphere), ('longitudeFloat', fixLongitudeHemisphere), ('longitudeHemisphere', fixLongitudeHemisphere), ('validGGA', validateGGA), ('numberOfSatellites', fixNumberOfSatellites), ('horizontalDilutionOfPosition', fixHDOP), ('altitude', fixAltitude), ('altitudeUnits', fixAltitudeUnits), ('heightAboveGeoid', fixHeightAboveGeoidUnits), ('heightAboveGeoidUnits', fixHeightAboveGeoidUnits), ] } class NMEASentence(Sentence): def isUsable(): """ Checks if the current sentence is usable for positioning information. """ return self.validateChecksum() and self.isReliableFix() def validateChecksum(self): """ Validates the checksum of an NMEA sentence. Sentences with a valid checksum or missing checksum can be used to instantiate NMEAParser objects: >>> sentence = ''' ... $GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47 ... ''' >>> s = NMEASentence(sentence) Same thing with missing checksum: >>> sentence = ''' ... $GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,* ... ''' >>> s = NMEASentence(sentence) Instantiating with a sentence that has a bad checksum will raise an exception. >>> sentence = ''' ... $GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*46 ... ''' >>> s = NMEASentence(sentence) Traceback (most recent call last): ... InvalidChecksum: 71 != 70 """ if self.sentence[-3] == '*': # sentence has a checksum reference, source = int(self.sentence[-2:], 16), self.sentence[1:-3] computed = reduce(operator.xor, (ord(x) for x in source)) if computed != reference: raise InvalidChecksum("%s != %s" % (computed, reference)) else: log.msg("sentence %s missing checksum" % self.sentence) def isReliableFix(self): """ Checks if the fix is reliable. Returns False if and only if the information was present and it showed that the fix is *un*reliable. This method is optimistic in the sense that it will return True if no information is known about the quality of the fix. This is usually acceptable, since most GPS devices will not send any sentences (or send empty ones) until the fix is reliable. """ if self.sentenceData.get('validGGA') == GGA_INVALID: return False if self.sentenceData.get('validRMC') == RMC_INVALID: return False return True def fixTimestamp(sentenceData, oldTimestamp): """ Turns the NMEA timestamp notation into a datetime.time object. The time in this object is expressed as Zulu time. """ hoursMinutesSeconds = oldTimestamp.split('.')[0] datetimeObject = datetime.datetime.strptime(hoursMinutesSeconds, '%H%M%S') sentenceData['timestamp'] = datetimeObject.time() # TODO: Add tzinfo (this is Zulu time!) def fixCoordinateFloat(sentenceData, value, coordinate): currentValue = sentenceData.get(coordinate, 1) left, right = str(value).split('.') degrees = int(left[:-2]) minutes = float("%s.%s" % (left[-2:], right)) magnitude = degrees + minutes/60 self.sentenceData[coordinate] = currentValue * magnitude fixLatitudeFloat = functools.partial(fixCoordinateFloat, coordinate='latitude') fixLongitudeFloat = functools.partial(fixCoordinateFloat, coordinate='longitude') def fixHemisphereSign(sentenceData, hemisphere, coordinate): """ Fixes the sign for a hemisphere. @param sentenceData: partial sentence data @type sentenceData: C{dict} @param hemisphere: Single letter hemisphere representation (N,S,E,W). @type hemisphere: C{str} @param coordinate: Coordinate type (latitude or longitude). @type coordinate: C{str} Coordinates in the northern and eastern hemispheres will be preserved. >>> sentenceData = {'latitude': '12.34', 'latitudeHemisphere': 'N'} >>> sentenceData = {'longitude': '12.34', 'longitudeHemisphere': 'E'} """ sign = (-1 if hemisphere.upper() in ('S', 'W') else 1) currentValue = self.sentenceData.get(coordinate, 1) self.sentenceData[coordinate] = sign * currentValue fixLatitudeHemisphere = functools.partial(fixHemisphereSign, coordinate='latitude') fixLongitudeHemisphere = functools.partial(fixHemisphereSign, coordinate='longitude') def simpleConverter(sentenceData, value, key=None, converter=float): sentenceData[key] = converter(value) fixAltitude = functools.partial(simpleconverter, key="altitude") fixAboveGeoidConverter = functools.partial(simpleConverter, key='heightAboveGeoid') fixHDOP = functools.partial(fixUnits, converter=int,, unitsFor='horizontalDilutionOfPosition') fixNumberOfSatellites = functools.partial(fixUnits, converter=int, unitsFor='numberOfSatellites') acceptableUnits = frozenset(['M']) unitConverters = { 'knots': lambda inKnots: inKnots * 0.51444444444444444, } def fixUnits(sentenceData, unit, unitsFor): """ Changes the units (if necessary) to more sensible SI units. """ if unit in acceptableUnits: return currentValue = sentenceData.get(unitsFor, 1) sentenceData[unitsFor] = unitConverters[unit](currentValue) fixAltitudeUnits = functools.partial(fixUnits, unitsFor='altitude') fixHeightAboveGeoidUnits = functools.partial(fixUnits, unitsFor='heightAboveGeoid') def validateGGA(sentenceData, value): """ Checks if a GGA sentence is valid. """ currentValue = sentenceData.get('valid', True) sentenceData['valid'] = currentValue and (value != GGA_INVALID) def validateRMC(sentenceData, value): """ Checks if an RMC sentence is valid. """ currentValue = sentenceData.get('valid', True) sentenceData['valid'] = currentValue and (value != RMC_INVALID)