Ticket #3926: positioning-3926.patch

File positioning-3926.patch, 135.1 KB (added by lvh, 3 years ago)
  • new file w/twisted/positioning/__init__.py

    diff --git c/twisted/positioning/__init__.py w/twisted/positioning/__init__.py
    new file mode 100644
    index 0000000..3454a64
    - +  
     1# Copyright (c) 2009-2011 Twisted Matrix Laboratories. 
     2# See LICENSE for details. 
     3""" 
     4The Twisted positioning framework. 
     5 
     6@since: 11.1 
     7""" 
  • new file w/twisted/positioning/base.py

    diff --git c/twisted/positioning/base.py w/twisted/positioning/base.py
    new file mode 100644
    index 0000000..1e707fb
    - +  
     1# -*- test-case-name: twisted.positioning.test.test_base,twisted.positioning.test.test_sentence -*- 
     2# Copyright (c) 2009-2011 Twisted Matrix Laboratories. 
     3# See LICENSE for details. 
     4""" 
     5Generic positioning base classes. 
     6 
     7@since: 11.1 
     8""" 
     9from zope.interface import implements 
     10from twisted.python.util import FancyEqMixin 
     11 
     12from twisted.positioning import ipositioning 
     13 
     14MPS_PER_KNOT = 0.5144444444444444 
     15MPS_PER_KPH = 0.27777777777777777 
     16METERS_PER_FOOT = 0.3048 
     17 
     18LATITUDE, LONGITUDE, HEADING, VARIATION = range(4) 
     19NORTH, EAST, SOUTH, WEST = range(4) 
     20 
     21 
     22 
     23class BasePositioningReceiver(object): 
     24    """ 
     25    A base positioning receiver. 
     26 
     27    This class would be a good base class for building positioning 
     28    receivers. It implements the interface (so you don't have to) with stub 
     29    methods. 
     30 
     31    People who want to implement positioning receivers should subclass this 
     32    class and override the specific callbacks they want to handle. 
     33    """ 
     34    implements(ipositioning.IPositioningReceiver) 
     35 
     36    def timeReceived(self, time): 
     37        """ 
     38        Implements L{IPositioningReceiver.timeReceived} stub. 
     39        """ 
     40 
     41 
     42    def headingReceived(self, heading): 
     43        """ 
     44        Implements L{IPositioningReceiver.headingReceived} stub. 
     45        """ 
     46 
     47 
     48    def speedReceived(self, speed): 
     49        """ 
     50        Implements L{IPositioningReceiver.speedReceived} stub. 
     51        """ 
     52 
     53 
     54    def climbReceived(self, climb): 
     55        """ 
     56        Implements L{IPositioningReceiver.climbReceived} stub. 
     57        """ 
     58 
     59 
     60    def positionReceived(self, latitude, longitude): 
     61        """ 
     62        Implements L{IPositioningReceiver.positionReceived} stub. 
     63        """ 
     64 
     65 
     66    def positionErrorReceived(self, positionError): 
     67        """ 
     68        Implements L{IPositioningReceiver.positioningErrorReceived} stub. 
     69        """ 
     70 
     71 
     72    def altitudeReceived(self, altitude): 
     73        """ 
     74        Implements L{IPositioningReceiver.altitudeReceived} stub. 
     75        """ 
     76 
     77 
     78    def beaconInformationReceived(self, beaconInformation): 
     79        """ 
     80        Implements L{IPositioningReceiver.beaconInformationReceived} stub. 
     81        """ 
     82 
     83 
     84 
     85class InvalidSentence(Exception): 
     86    """ 
     87    An exception raised when a sentence is invalid. 
     88    """ 
     89 
     90 
     91 
     92class InvalidChecksum(Exception): 
     93    """ 
     94    An exception raised when the checksum of a sentence is invalid. 
     95    """ 
     96 
     97 
     98class BaseSentence(object): 
     99    """ 
     100    A base sentence class for a particular protocol. 
     101 
     102    Using this base class, specific sentence classes can almost automatically 
     103    be created for a particular protocol (except for the documentation of 
     104    course) if that protocol implements the L{IPositioningSentenceProducer} 
     105    interface. To do this, fill the ALLOWED_ATTRIBUTES class attribute using 
     106    the C{getSentenceAttributes} class method of the producer:: 
     107 
     108        class FooSentence(BaseSentence): 
     109            \"\"\" 
     110            A sentence for integalactic transmodulator sentences. 
     111 
     112            @ivar transmogrificationConstant: The value used in the 
     113                transmogrifier while producing this sentence, corrected for 
     114                gravitational fields. 
     115            @type transmogrificationConstant: C{Tummy} 
     116            \"\"\" 
     117            ALLOWED_ATTRIBUTES = FooProtocol.getSentenceAttributes() 
     118 
     119    @ivar presentAttribues: An iterable containing the names of the 
     120        attributes that are present in this sentence. 
     121    @type presentAttributes: iterable of C{str} 
     122 
     123    @cvar ALLOWED_ATTRIBUTES: A set of attributes that are allowed in this 
     124        sentence. 
     125    @type ALLOWED_ATTRIBUTES: C{set} of C{str} 
     126    """ 
     127    ALLOWED_ATTRIBUTES = set() 
     128 
     129 
     130    def __init__(self, sentenceData): 
     131        """ 
     132        Initializes a sentence with parsed sentence data. 
     133 
     134        @param sentenceData: The parsed sentence data. 
     135        @type sentenceData: C{dict} (C{str} -> C{str} or C{NoneType}) 
     136        """ 
     137        self._sentenceData = sentenceData 
     138 
     139 
     140    presentAttributes = property(lambda self: iter(self._sentenceData)) 
     141 
     142 
     143    def __getattr__(self, name): 
     144        """ 
     145        Gets an attribute of this sentence. 
     146        """ 
     147        if name in self.ALLOWED_ATTRIBUTES: 
     148            return self._sentenceData.get(name, None) 
     149        else: 
     150            className = self.__class__.__name__ 
     151            msg = "%s sentences have no %s attributes" % (className, name) 
     152            raise AttributeError(msg) 
     153 
     154 
     155    def __repr__(self): 
     156        """ 
     157        Returns a textual representation of this sentence. 
     158 
     159        @return: A textual representation of this sentence. 
     160        @rtype: C{str} 
     161        """ 
     162        items = self._sentenceData.items() 
     163        data = ["%s: %s" % (k, v) for k, v in sorted(items) if k != "type"] 
     164        dataRepr = ", ".join(data) 
     165 
     166        typeRepr = self._sentenceData.get("type") or "unknown type" 
     167        className = self.__class__.__name__ 
     168 
     169        return "<%s (%s) {%s}>" % (className, typeRepr, dataRepr) 
     170 
     171 
     172 
     173class PositioningSentenceProducerMixin(object): 
     174    """ 
     175    A mixin for certain protocols that produce positioning sentences. 
     176 
     177    This mixin helps protocols that have C{SENTENCE_CONTENTS} class variables 
     178    (such as the C{NMEAProtocol} and the C{ClassicGPSDProtocol}) implement the 
     179    L{IPositioningSentenceProducingProtocol} interface. 
     180    """ 
     181    #@classmethod 
     182    def getSentenceAttributes(cls): 
     183        """ 
     184        Returns a set of all attributes that might be found in the sentences 
     185        produced by this protocol. 
     186 
     187        This is basically a set of all the attributes of all the sentences that 
     188        this protocol can produce. 
     189 
     190        @return: The set of all possible sentence attribute names. 
     191        @rtype: C{set} of C{str} 
     192        """ 
     193        attributes = set(["type"]) 
     194        for attributeList in cls.SENTENCE_CONTENTS.values(): 
     195            for attribute in attributeList: 
     196                if attribute is None: 
     197                    continue 
     198                attributes.add(attribute) 
     199 
     200        return attributes 
     201 
     202 
     203    getSentenceAttributes = classmethod(getSentenceAttributes) 
     204 
     205 
     206     
     207class Angle(object, FancyEqMixin): 
     208    """ 
     209    An object representing an angle. 
     210 
     211    @ivar inDecimalDegrees: The value of this angle, expressed in decimal 
     212        degrees. C{None} if unknown. This attribute is read-only. 
     213    @type inDecimalDegrees: C{float} (or C{NoneType}) 
     214    @ivar inDegreesMinutesSeconds: The value of this angle, expressed in 
     215        degrees, minutes and seconds. C{None} if unknown. This attribute is 
     216        read-only. 
     217    @type inDegreesMinutesSeconds: 3-C{tuple} of C{int} (or C{NoneType}) 
     218 
     219    @cvar RANGE_EXPRESSIONS: A collections of expressions for the allowable 
     220        range for the angular value of a particular coordinate value. 
     221    @type RANGE_EXPRESSIONS: A mapping of coordinate types (C{LATITUDE}, 
     222        C{LONGITUDE}, C{HEADING}, C{VARIATION}) to 1-argument callables. 
     223    """ 
     224    RANGE_EXPRESSIONS = { 
     225        LATITUDE: lambda latitude: -90.0 < latitude < 90.0, 
     226        LONGITUDE: lambda longitude: -180.0 < longitude < 180.0, 
     227        HEADING: lambda heading:  0 <= heading < 360, 
     228        VARIATION: lambda variation: -180 < variation <= 180, 
     229    } 
     230 
     231 
     232    ANGLE_TYPE_NAMES  = { 
     233        LATITUDE: "latitude", 
     234        LONGITUDE: "longitude", 
     235        VARIATION: "variation", 
     236        HEADING: "heading", 
     237    } 
     238 
     239 
     240    compareAttributes = 'angleType', 'inDecimalDegrees' 
     241 
     242 
     243    def __init__(self, angle=None, angleType=None): 
     244        """ 
     245        Initializes an angle. 
     246 
     247        @param angle: The value of the angle in decimal degrees. (C{None} if 
     248            unknown). 
     249        @type angle: C{float} or C{NoneType} 
     250        @param angleType: A symbolic constant describing the angle type. Should 
     251            be one of LATITUDE, LONGITUDE, HEADING, VARIATION. C{None} if 
     252            unknown. 
     253 
     254        @raises ValueError: If the angle type is not the default argument, but it 
     255            is an unknown type (it's not present in C{Angle.RANGE_EXPRESSIONS}), 
     256            or it is a known type but the supplied value was out of the allowable 
     257            range for said type. 
     258        """ 
     259        if angle is not None and angleType is not None: 
     260            if angleType not in self.RANGE_EXPRESSIONS: 
     261                raise ValueError("Unknown angle type") 
     262            elif not self.RANGE_EXPRESSIONS[angleType](angle): 
     263                raise ValueError("Angle %s not in allowed range for type %s" 
     264                                 % (angle, self.ANGLE_TYPE_NAMES[angleType])) 
     265 
     266        self.angleType = angleType 
     267        self._angle = angle 
     268 
     269 
     270    inDecimalDegrees = property(lambda self: self._angle) 
     271 
     272 
     273    def _getDMS(self): 
     274        """ 
     275        Gets the value of this angle as a degrees, minutes, seconds tuple. 
     276 
     277        @return: This angle expressed in degrees, minutes, seconds. C{None} if 
     278            the angle is unknown. 
     279        @rtype: 3-C{tuple} of C{int} (or C{NoneType}) 
     280        """ 
     281        if self._angle is None: 
     282            return None 
     283 
     284        degrees = abs(int(self._angle)) 
     285        fractionalDegrees = abs(self._angle - int(self._angle)) 
     286        decimalMinutes = 60 * fractionalDegrees 
     287 
     288        minutes = int(decimalMinutes) 
     289        fractionalMinutes = decimalMinutes - int(decimalMinutes) 
     290        decimalSeconds = 60 * fractionalMinutes 
     291 
     292        return degrees, minutes, int(decimalSeconds) 
     293 
     294 
     295    inDegreesMinutesSeconds = property(_getDMS) 
     296 
     297 
     298    def setSign(self, sign): 
     299        """ 
     300        Sets the sign of this angle. 
     301 
     302        @param sign: The new sign. C{1} for positive and C{-1} for negative 
     303            signs, respectively. 
     304        @type sign: C{int} 
     305 
     306        @raise ValueError: If the C{sign} parameter is not C{-1} or C{1}. 
     307        """ 
     308        if sign not in (-1, 1): 
     309            raise ValueError("bad sign (got %s, expected -1 or 1)" % sign) 
     310 
     311        self._angle = sign * abs(self._angle) 
     312 
     313 
     314    def __float__(self): 
     315        """ 
     316        Returns this angle as a float. 
     317 
     318        @return: The float value of this angle, expressed in degrees. 
     319        @rtype: C{float} 
     320        """ 
     321        return self._angle 
     322 
     323 
     324    def __repr__(self): 
     325        """ 
     326        Returns a string representation of this angle. 
     327 
     328        @return: The string representation. 
     329        @rtype: C{str} 
     330        """ 
     331        return "<%s (%s)>" % (self._angleTypeNameRepr, self._angleValueRepr) 
     332 
     333 
     334    def _getAngleValueRepr(self): 
     335        """ 
     336        Returns a string representation of the angular value of this angle. 
     337 
     338        This is a helper function for the actual C{__repr__}. 
     339 
     340        @return: The string representation. 
     341        @rtype: C{str} 
     342        """ 
     343        if self.inDecimalDegrees is not None: 
     344            return "%s degrees" % round(self.inDecimalDegrees, 2) 
     345        else: 
     346            return "unknown value" 
     347 
     348 
     349    _angleValueRepr = property(_getAngleValueRepr) 
     350 
     351 
     352    def _getAngleTypeNameRepr(self): 
     353        """ 
     354        Returns a string representation of the type of this angle. 
     355 
     356        This is a helper function for the actual C{__repr__}. 
     357 
     358        @return: The string representation. 
     359        @rtype: C{str} 
     360        """ 
     361        angleTypeName = self.ANGLE_TYPE_NAMES.get( 
     362            self.angleType, "angle of unknown type").capitalize() 
     363        return angleTypeName 
     364 
     365 
     366    _angleTypeNameRepr = property(_getAngleTypeNameRepr) 
     367 
     368 
     369 
     370class Heading(Angle): 
     371    """ 
     372    The heading of a mobile object. 
     373 
     374    @ivar variation: The (optional) variation. 
     375        The sign of the variation is positive for variations towards the east 
     376        (clockwise from north), and negative for variations towards the west 
     377        (counterclockwise from north). 
     378        If the variation is unknown or not applicable, this is C{None}. 
     379    @type variation: C{Angle} or C{NoneType}. 
     380    @ivar correctedHeading: The heading, corrected for variation. If the 
     381        variation is unknown (C{None}), is None. This attribute is read-only (its 
     382        value is determined by the angle and variation attributes). The value is 
     383        coerced to being between 0 (inclusive) and 360 (exclusive). 
     384    """ 
     385    def __init__(self, angle=None, variation=None): 
     386        """ 
     387        Initializes a angle with an optional variation. 
     388        """ 
     389        Angle.__init__(self, angle, HEADING) 
     390        self.variation = variation 
     391 
     392 
     393    #@classmethod 
     394    def fromFloats(cls, angleValue=None, variationValue=None): 
     395        """ 
     396        Constructs a Heading from the float values of the angle and variation. 
     397 
     398        @param angleValue: The angle value of this heading. 
     399        @type angleValue: C{float} 
     400        @param variationValue: The value of the variation of this heading. 
     401        @type variationValue: C{float} 
     402        """ 
     403        variation = Angle(variationValue, VARIATION) 
     404        return cls(angleValue, variation) 
     405 
     406 
     407    fromFloats = classmethod(fromFloats) 
     408 
     409 
     410    def _getCorrectedHeading(self): 
     411        """ 
     412        Corrects the heading by the given variation. This is sometimes known as 
     413        the true heading. 
     414 
     415        @return: The heading, corrected by the variation. If the variation or 
     416            the angle are unknown, returns C{None}. 
     417        @rtype: C{float} or C{NoneType} 
     418        """ 
     419        if self._angle is None or self.variation is None: 
     420            return None 
     421 
     422        angle = (self.inDecimalDegrees - self.variation.inDecimalDegrees) % 360 
     423        return Angle(angle, HEADING) 
     424 
     425 
     426    correctedHeading = property(_getCorrectedHeading) 
     427 
     428 
     429    def setSign(self, sign): 
     430        """ 
     431        Sets the sign of the variation of this heading. 
     432 
     433        @param sign: The new sign. C{1} for positive and C{-1} for negative 
     434            signs, respectively. 
     435        @type sign: C{int} 
     436 
     437        @raise ValueErorr: If the C{sign} parameter is not C{-1} or C{1}. 
     438        """ 
     439        if self.variation.inDecimalDegrees is None: 
     440            raise ValueError("can't set the sign of an unknown variation") 
     441 
     442        self.variation.setSign(sign) 
     443 
     444 
     445    compareAttributes = list(Angle.compareAttributes) + ["variation"] 
     446 
     447 
     448    def __repr__(self): 
     449        """ 
     450        Returns a string representation of this angle. 
     451 
     452        @return: The string representation. 
     453        @rtype: C{str} 
     454        """ 
     455        if self.variation is None: 
     456            variationRepr = "unknown variation" 
     457        else: 
     458            variationRepr = repr(self.variation) 
     459 
     460        return "<%s (%s, %s)>" % ( 
     461            self._angleTypeNameRepr, self._angleValueRepr, variationRepr) 
     462 
     463 
     464 
     465class Coordinate(Angle, FancyEqMixin): 
     466    """ 
     467    A coordinate. 
     468 
     469    @ivar angle: The value of the coordinate in decimal degrees, with the usual 
     470        rules for sign (northern and eastern hemispheres are positive, southern 
     471        and western hemispheres are negative). 
     472    @type angle: C{float} 
     473    """ 
     474    def __init__(self, angle, coordinateType=None): 
     475        """ 
     476        Initializes a coordinate. 
     477 
     478        @param angle: The angle of this coordinate in decimal degrees. The 
     479            hemisphere is determined by the sign (north and east are positive). 
     480            If this coordinate describes a latitude, this value must be within 
     481            -90.0 and +90.0 (exclusive). If this value describes a longitude, 
     482            this value must be within -180.0 and +180.0 (exclusive). 
     483        @type angle: C{float} 
     484        @param coordinateType: One of L{LATITUDE}, L{LONGITUDE}. Used to return 
     485            hemisphere names. 
     486        """ 
     487        Angle.__init__(self, angle, coordinateType) 
     488 
     489 
     490    HEMISPHERES_BY_TYPE_AND_SIGN = { 
     491        LATITUDE: [ 
     492            NORTH, # positive 
     493            SOUTH, # negative 
     494        ], 
     495 
     496        LONGITUDE: [ 
     497            EAST, # positve 
     498            WEST, # negative 
     499        ] 
     500    } 
     501 
     502 
     503    def _getHemisphere(self): 
     504        """ 
     505        Gets the hemisphere of this coordinate. 
     506 
     507        @return: A symbolic constant representing a hemisphere (C{NORTH}, 
     508            C{EAST}, C{SOUTH} or C{WEST}). 
     509        """ 
     510        try: 
     511            sign = int(self.inDecimalDegrees < 0) 
     512            return self.HEMISPHERES_BY_TYPE_AND_SIGN[self.angleType][sign] 
     513        except KeyError: 
     514            raise ValueError("unknown coordinate type (cant find hemisphere)") 
     515 
     516 
     517    hemisphere = property(fget=_getHemisphere) 
     518 
     519 
     520 
     521class Altitude(object, FancyEqMixin): 
     522    """ 
     523    An altitude. 
     524 
     525    @ivar inMeters: The altitude represented by this object, in meters. This 
     526        attribute is read-only. 
     527    @type inMeters: C{float} 
     528 
     529    @ivar inFeet: As above, but expressed in feet. 
     530    @type inFeet: C{float} 
     531    """ 
     532    compareAttributes = 'inMeters', 
     533 
     534    def __init__(self, altitude): 
     535        """ 
     536        Initializes an altitude. 
     537 
     538        @param altitude: The altitude in meters. 
     539        @type altitude: C{float} 
     540        """ 
     541        self._altitude = altitude 
     542 
     543 
     544    def _getAltitudeInFeet(self): 
     545        """ 
     546        Gets the altitude this object represents, in feet. 
     547 
     548        @return: The altitude, expressed in feet. 
     549        @rtype: C{float} 
     550        """ 
     551        return self._altitude / METERS_PER_FOOT 
     552 
     553 
     554    inFeet = property(_getAltitudeInFeet) 
     555 
     556 
     557    def _getAltitudeInMeters(self): 
     558        """ 
     559        Returns the altitude this object represents, in meters. 
     560 
     561        @return: The altitude, expressed in feet. 
     562        @rtype: C{float} 
     563        """ 
     564        return self._altitude 
     565 
     566 
     567    inMeters = property(_getAltitudeInMeters) 
     568 
     569 
     570    def __float__(self): 
     571        """ 
     572        Returns the altitude represented by this object expressed in meters. 
     573 
     574        @return: The altitude represented by this object, expressed in meters. 
     575        @rtype: C{float} 
     576        """ 
     577        return self._altitude 
     578 
     579 
     580    def __repr__(self): 
     581        """ 
     582        Returns a string representation of this altitude. 
     583 
     584        @return: The string representation. 
     585        @rtype: C{str} 
     586        """ 
     587        return "<Altitude (%s m)>" % (self._altitude,) 
     588 
     589 
     590 
     591class _BaseSpeed(object, FancyEqMixin): 
     592    """ 
     593    An object representing the abstract concept of the speed (rate of 
     594    movement) of a mobile object. 
     595 
     596    This primarily has behavior for converting between units and comparison. 
     597 
     598    @ivar inMetersPerSecond: The speed that this object represents, expressed 
     599        in meters per second. This attribute is immutable. 
     600    @type inMetersPerSecond: C{float} 
     601 
     602    @ivar inKnots: Same as above, but expressed in knots. 
     603    @type inKnots: C{float} 
     604    """ 
     605    compareAttributes = 'inMetersPerSecond', 
     606 
     607    def __init__(self, speed): 
     608        """ 
     609        Initializes a speed. 
     610 
     611        @param speed: The speed that this object represents, expressed in 
     612            meters per second. 
     613        @type speed: C{float} 
     614 
     615        @raises ValueError: Raised if value was invalid for this particular 
     616            kind of speed. Only happens in subclasses. 
     617        """ 
     618        self._speed = speed 
     619 
     620 
     621    def _getSpeedInKnots(self): 
     622        """ 
     623        Returns the speed represented by this object, expressed in knots. 
     624 
     625        @return: The speed this object represents, in knots. 
     626        @rtype: C{float} 
     627        """ 
     628        return self._speed / MPS_PER_KNOT 
     629 
     630 
     631    inKnots = property(_getSpeedInKnots) 
     632 
     633 
     634    inMetersPerSecond = property(lambda self: self._speed) 
     635 
     636 
     637    def __float__(self): 
     638        """ 
     639        Returns the speed represented by this object expressed in meters per 
     640        second. 
     641 
     642        @return: The speed represented by this object, expressed in meters per 
     643            second. 
     644        @rtype: C{float} 
     645        """ 
     646        return self._speed 
     647 
     648 
     649    def __repr__(self): 
     650        """ 
     651        Returns a string representation of this speed object. 
     652 
     653        @return: The string representation. 
     654        @rtype: C{str} 
     655        """ 
     656        speedValue = round(self.inMetersPerSecond, 2) 
     657        return "<%s (%s m/s)>" % (self.__class__.__name__, speedValue) 
     658 
     659 
     660 
     661class Speed(_BaseSpeed): 
     662    """ 
     663    The speed (rate of movement) of a mobile object. 
     664    """ 
     665    def __init__(self, speed): 
     666        """ 
     667        Initializes a L{Speed} object. 
     668 
     669        @param speed: The speed that this object represents, expressed in 
     670            meters per second. 
     671        @type speed: C{float} 
     672 
     673        @raises ValueError: Raised if C{speed} is negative. 
     674        """ 
     675        if speed < 0: 
     676            raise ValueError("negative speed: %r" % (speed,)) 
     677 
     678        _BaseSpeed.__init__(self, speed) 
     679 
     680 
     681 
     682class Climb(_BaseSpeed): 
     683    """ 
     684    The climb ("vertical speed") of an object. 
     685    """ 
     686    def __init__(self, climb): 
     687        """ 
     688        Initializes a L{Clib} object. 
     689 
     690        @param climb: The climb that this object represents, expressed in 
     691            meters per second. 
     692        @type climb: C{float} 
     693 
     694        @raises ValueError: Raised if the provided climb was less than zero. 
     695        """ 
     696        _BaseSpeed.__init__(self, climb) 
     697 
     698 
     699 
     700class PositionError(object, FancyEqMixin): 
     701    """ 
     702    Position error information. 
     703 
     704    @ivar pdop: The position dilution of precision. C{None} if unknown. 
     705    @type pdop: C{float} or C{NoneType} 
     706    @ivar hdop: The horizontal dilution of precision. C{None} if unknown. 
     707    @type hdop: C{float} or C{NoneType} 
     708    @ivar vdop: The vertical dilution of precision. C{None} if unknown. 
     709    @type vdop: C{float} or C{NoneType} 
     710    """ 
     711    compareAttributes = 'pdop', 'hdop', 'vdop' 
     712 
     713    def __init__(self, pdop=None, hdop=None, vdop=None, testInvariant=False): 
     714        """ 
     715        Initializes a positioning error object. 
     716 
     717        @param pdop: The position dilution of precision. C{None} if unknown. 
     718        @type pdop: C{float} or C{NoneType} 
     719        @param hdop: The horizontal dilution of precision. C{None} if unknown. 
     720        @type hdop: C{float} or C{NoneType} 
     721        @param vdop: The vertical dilution of precision. C{None} if unknown. 
     722        @type vdop: C{float} or C{NoneType} 
     723        @param testInvariant: Flag to test if the DOP invariant is valid or 
     724            not. If C{True}, the invariant (PDOP = (HDOP**2 + VDOP**2)*.5) is 
     725            checked at every mutation. By default, this is false, because the 
     726            vast majority of DOP-providing devices ignore this invariant. 
     727        @type testInvariant: c{bool} 
     728        """ 
     729        self._pdop = pdop 
     730        self._hdop = hdop 
     731        self._vdop = vdop 
     732 
     733        self._testInvariant = testInvariant 
     734        self._testDilutionOfPositionInvariant() 
     735 
     736 
     737    ALLOWABLE_TRESHOLD = 0.01 
     738 
     739 
     740    def _testDilutionOfPositionInvariant(self): 
     741        """ 
     742        Tests if this positioning error object satisfies the dilution of 
     743        position invariant (PDOP = (HDOP**2 + VDOP**2)*.5), unless the 
     744        C{self._testInvariant} instance variable is C{False}. 
     745 
     746        @return: C{None} if the invariant was not satisifed or not tested. 
     747        @raises ValueError: Raised if the invariant was tested but not 
     748            satisfied. 
     749        """ 
     750        if not self._testInvariant: 
     751            return 
     752 
     753        for x in (self.pdop, self.hdop, self.vdop): 
     754            if x is None: 
     755                return 
     756 
     757        delta = abs(self.pdop - (self.hdop**2 + self.vdop**2)**.5) 
     758        if delta > self.ALLOWABLE_TRESHOLD: 
     759            raise ValueError("invalid combination of dilutions of precision: " 
     760                             "position: %s, horizontal: %s, vertical: %s" 
     761                             % (self.pdop, self.hdop, self.vdop)) 
     762 
     763 
     764    DOP_EXPRESSIONS = { 
     765        'pdop': [ 
     766            lambda self: float(self._pdop), 
     767            lambda self: (self._hdop**2 + self._vdop**2)**.5, 
     768        ], 
     769 
     770        'hdop': [ 
     771            lambda self: float(self._hdop), 
     772            lambda self: (self._pdop**2 - self._vdop**2)**.5, 
     773        ], 
     774 
     775        'vdop': [ 
     776            lambda self: float(self._vdop), 
     777            lambda self: (self._pdop**2 - self._hdop**2)**.5, 
     778        ], 
     779    } 
     780 
     781 
     782    def _getDOP(self, dopType): 
     783        """ 
     784        Gets a particular dilution of position value. 
     785 
     786        @return: The DOP if it is known, C{None} otherwise. 
     787        @rtype: C{float} or C{NoneType} 
     788        """ 
     789        for dopExpression in self.DOP_EXPRESSIONS[dopType]: 
     790            try: 
     791                return dopExpression(self) 
     792            except TypeError: 
     793                continue 
     794 
     795 
     796    def _setDOP(self, dopType, value): 
     797        """ 
     798        Sets a particular dilution of position value. 
     799 
     800        @param dopType: The type of dilution of position to set. One of 
     801            ('pdop', 'hdop', 'vdop'). 
     802        @type dopType: C{str} 
     803 
     804        @param value: The value to set the dilution of position type to. 
     805        @type value: C{float} 
     806 
     807        If this position error tests dilution of precision invariants, 
     808        it will be checked. If the invariant is not satisfied, the 
     809        assignment will be undone and C{ValueError} is raised. 
     810        """ 
     811        attributeName = "_" + dopType 
     812 
     813        oldValue = getattr(self, attributeName) 
     814        setattr(self, attributeName, float(value)) 
     815 
     816        try: 
     817            self._testDilutionOfPositionInvariant() 
     818        except ValueError: 
     819            setattr(self, attributeName, oldValue) 
     820            raise 
     821 
     822 
     823    pdop = property(fget=lambda self: self._getDOP('pdop'), 
     824                    fset=lambda self, value: self._setDOP('pdop', value)) 
     825 
     826 
     827    hdop = property(fget=lambda self: self._getDOP('hdop'), 
     828                    fset=lambda self, value: self._setDOP('hdop', value)) 
     829 
     830 
     831    vdop = property(fget=lambda self: self._getDOP('vdop'), 
     832                    fset=lambda self, value: self._setDOP('vdop', value)) 
     833 
     834 
     835    _REPR_TEMPLATE = "<PositionError (pdop: %s, hdop: %s, vdop: %s)>" 
     836 
     837 
     838    def __repr__(self): 
     839        """ 
     840        Returns a string representation of positioning information object. 
     841 
     842        @return: The string representation. 
     843        @rtype: C{str} 
     844        """ 
     845        return self._REPR_TEMPLATE % (self.pdop, self.hdop, self.vdop) 
     846 
     847 
     848 
     849class BeaconInformation(object): 
     850    """ 
     851    Information about positioning beacons (a generalized term for the reference 
     852    objects that help you determine your position, such as satellites or cell 
     853    towers). 
     854 
     855    @ivar beacons: A set of visible beacons. Note that visible beacons are not 
     856        necessarily used in acquiring a postioning fix. 
     857    @type beacons: C{set} of L{IPositioningBeacon} 
     858 
     859    @ivar usedBeacons: An iterable of the beacons that were used in obtaining a 
     860        positioning fix. This only contains beacons that are actually used, not 
     861        beacons of which it is  unknown if they are used or not. This attribute 
     862        is immutable. 
     863    @type usedBeacons: iterable of L{IPositioningBeacon} 
     864 
     865    @ivar seen: The amount of beacons that can be seen. This attribute is 
     866        immutable. 
     867    @type seen: C{int} 
     868    @ivar used: The amount of beacons that were used in obtaining the 
     869        positioning fix. This attribute is immutable. 
     870    @type used: C{int} 
     871    """ 
     872    def __init__(self, beacons=None): 
     873        """ 
     874        Initializes a beacon information object. 
     875 
     876        @param beacons: A collection of beacons that will be present in this 
     877            beacon information object. 
     878        @type beacons: iterable of L{IPositioningBeacon} or C{Nonetype} 
     879        """ 
     880        self.beacons = set(beacons or []) 
     881 
     882 
     883    def _getUsedBeacons(self): 
     884        """ 
     885        Returns a generator of used beacons. 
     886 
     887        @return: A generator containing all of the used positioning beacons. This 
     888            only contains beacons that are actually used, not beacons of which it 
     889            is  unknown if they are used or not. 
     890        @rtype: iterable of L{PositioningBeacon} 
     891        """ 
     892        for beacon in self.beacons: 
     893            if beacon.isUsed: 
     894                yield beacon 
     895 
     896 
     897    usedBeacons = property(fget=_getUsedBeacons) 
     898 
     899 
     900    def _getNumberOfBeaconsSeen(self): 
     901        """ 
     902        Returns the number of beacons that can be seen. 
     903 
     904        @return: The number of beacons that can be seen. 
     905        @rtype: C{int} 
     906        """ 
     907        return len(self.beacons) 
     908 
     909 
     910    seen = property(_getNumberOfBeaconsSeen) 
     911 
     912 
     913    def _getNumberOfBeaconsUsed(self): 
     914        """ 
     915        Returns the number of beacons that can be seen. 
     916 
     917        @return: The number of beacons that can be seen, or C{None} if the number 
     918            is unknown. This happens as soon as one of the beacons has an unknown 
     919            (C{None}) C{isUsed} attribute. 
     920        @rtype: C{int} or C{NoneType} 
     921        """ 
     922        numberOfUsedBeacons = 0 
     923        for beacon in self.beacons: 
     924            if beacon.isUsed is None: 
     925                return None 
     926            elif beacon.isUsed: 
     927                numberOfUsedBeacons += 1 
     928        return numberOfUsedBeacons 
     929 
     930 
     931    used = property(_getNumberOfBeaconsUsed) 
     932 
     933 
     934    def __iter__(self): 
     935        """ 
     936        Yields the beacons in this beacon information object. 
     937 
     938        @return: A generator producing the beacons in this beacon information 
     939            object. 
     940        @rtype: iterable of L{PositioningBeacon} 
     941        """ 
     942        for beacon in self.beacons: 
     943            yield beacon 
     944 
     945 
     946    def __repr__(self): 
     947        """ 
     948        Returns a string representation of this beacon information object. 
     949 
     950        The beacons are sorted by their identifier. 
     951 
     952        @return: The string representation. 
     953        @rtype: C{str} 
     954        """ 
     955        beaconReprs = ", ".join([repr(beacon) for beacon in 
     956            sorted(self.beacons, key=lambda x: x.identifier)]) 
     957 
     958        if self.used is not None: 
     959            used = str(self.used) 
     960        else: 
     961            used = "?" 
     962 
     963        return "<BeaconInformation (seen: %s, used: %s, beacons: {%s})>" % ( 
     964            self.seen, used, beaconReprs) 
     965 
     966 
     967 
     968class PositioningBeacon(object): 
     969    """ 
     970    A positioning beacon. 
     971 
     972    @ivar identifier: The unqiue identifier for this satellite. This is usually 
     973        an integer. For GPS, this is also known as the PRN. 
     974    @type identifier: Pretty much anything that can be used as a unique 
     975        identifier. Depends on the implementation. 
     976    @ivar isUsed: C{True} if the satellite is currently being used to obtain a 
     977        fix, C{False} if it is not currently being used, C{None} if unknown. 
     978    @type isUsed: C{bool} or C{NoneType} 
     979    """ 
     980    def __init__(self, identifier, isUsed=None): 
     981        """ 
     982        Initializes a positioning beacon. 
     983 
     984        @param identifier: The identifier for this beacon. 
     985        @type identifier: Can be pretty much anything (see ivar documentation). 
     986        @param isUsed: Determines if this beacon is used in obtaining a 
     987            positioning fix (see the ivar documentation). 
     988        @type isUsed: C{bool} or C{NoneType} 
     989        """ 
     990        self.identifier = identifier 
     991        self.isUsed = isUsed 
     992 
     993 
     994    def __hash__(self): 
     995        """ 
     996        Returns the hash of the identifier for this beacon. 
     997 
     998        @return: The hash of the identifier. (C{hash(self.identifier)}) 
     999        @rtype: C{int} 
     1000        """ 
     1001        return hash(self.identifier) 
     1002 
     1003 
     1004    def _usedRepr(self): 
     1005        """ 
     1006        Returns a single character representation of the status of this 
     1007        satellite in terms of being used for attaining a positioning fix. 
     1008 
     1009        @return: One of ("Y", "N", "?") depending on the status of the 
     1010            satellite. 
     1011        @rtype: C{str} 
     1012        """ 
     1013        return {True: "Y", False: "N", None: "?"}[self.isUsed] 
     1014 
     1015 
     1016    def __repr__(self): 
     1017        """ 
     1018        Returns a string representation of this beacon. 
     1019 
     1020        @return: The string representation. 
     1021        @rtype: C{str} 
     1022        """ 
     1023        return "<Beacon (identifier: %s, used: %s)>" \ 
     1024            % (self.identifier, self._usedRepr()) 
     1025 
     1026 
     1027 
     1028class Satellite(PositioningBeacon): 
     1029    """ 
     1030    A satellite. 
     1031 
     1032    @ivar azimuth: The azimuth of the satellite. This is the heading (positive 
     1033        angle relative to true north) where the satellite appears to be to the 
     1034        device. 
     1035    @ivar elevation: The (positive) angle above the horizon where this 
     1036        satellite appears to be to the device. 
     1037    @ivar signalToNoiseRatio: The signal to noise ratio of the signal coming 
     1038        from this satellite. 
     1039    """ 
     1040    def __init__(self, 
     1041                 identifier, 
     1042                 azimuth=None, 
     1043                 elevation=None, 
     1044                 signalToNoiseRatio=None, 
     1045                 isUsed=None): 
     1046        """ 
     1047        Initializes a satellite object. 
     1048 
     1049        @param identifier: The PRN (unique identifier) of this satellite. 
     1050        @type identifier: C{int} 
     1051        @param azimuth: The azimuth of the satellite (see instance variable 
     1052            documentation). 
     1053        @type azimuth: C{float} 
     1054        @param elevation: The elevation of the satellite (see instance variable 
     1055            documentation). 
     1056        @type elevation: C{float} 
     1057        @param signalToNoiseRatio: The signal to noise ratio of the connection 
     1058            to this satellite (see instance variable documentation). 
     1059        @type signalToNoiseRatio: C{float} 
     1060 
     1061        """ 
     1062        super(Satellite, self).__init__(int(identifier), isUsed) 
     1063 
     1064        self.azimuth = azimuth 
     1065        self.elevation = elevation 
     1066        self.signalToNoiseRatio = signalToNoiseRatio 
     1067 
     1068 
     1069    def __repr__(self): 
     1070        """ 
     1071        Returns a string representation of this Satellite. 
     1072 
     1073        @return: The string representation. 
     1074        @rtype: C{str} 
     1075        """ 
     1076        azimuth, elevation, snr = [{None: "?"}.get(x, x) 
     1077            for x in self.azimuth, self.elevation, self.signalToNoiseRatio] 
     1078 
     1079        properties = "azimuth: %s, elevation: %s, snr: %s" % ( 
     1080            azimuth, elevation, snr) 
     1081 
     1082        return "<Satellite (%s), %s, used: %s>" % ( 
     1083            self.identifier, properties, self._usedRepr()) 
  • new file w/twisted/positioning/ipositioning.py

    diff --git c/twisted/positioning/ipositioning.py w/twisted/positioning/ipositioning.py
    new file mode 100644
    index 0000000..403d738
    - +  
     1# Copyright (c) 2009-2011 Twisted Matrix Laboratories. 
     2# See LICENSE for details. 
     3""" 
     4Positioning interfaces. 
     5 
     6@since: 11.1 
     7""" 
     8from zope.interface import Interface 
     9 
     10 
     11class IPositioningReceiver(Interface): 
     12    """ 
     13    An interface for positioning providers. 
     14    """ 
     15    def positionReceived(latitude, longitude): 
     16        """ 
     17        Method called when a position is received. 
     18 
     19        @param latitude: The latitude of the received position. 
     20        @type latitude: L{twisted.positioning.base.Coordinate} 
     21        @param longitude: The longitude of the received position. 
     22        @type longitude: L{twisted.positioning.base.Coordinate} 
     23        """ 
     24 
     25 
     26    def positionErrorReceived(positionError): 
     27        """ 
     28        Method called when position error is received. 
     29 
     30        @param positioningError: The position error. 
     31        @type positioningError: L{twisted.positioning.base.PositionError} 
     32        """ 
     33 
     34    def timeReceived(time): 
     35        """ 
     36        Method called when time and date information arrives. 
     37 
     38        @param time: The date and time (expressed in UTC unless otherwise 
     39            specified). 
     40        @type time: L{datetime.datetime} 
     41        """ 
     42 
     43 
     44    def headingReceived(heading): 
     45        """ 
     46        Method called when a true heading is received. 
     47 
     48        @param heading: The heading. 
     49        @type heading: L{twisted.positioning.base.Heading} 
     50        """ 
     51 
     52 
     53    def altitudeReceived(altitude): 
     54        """ 
     55        Method called when an altitude is received. 
     56 
     57        @param altitude: The altitude. 
     58        @type altitude: L{twisted.positioning.base.Altitude} 
     59        """ 
     60 
     61 
     62    def speedReceived(speed): 
     63        """ 
     64        Method called when the speed is received. 
     65 
     66        @param speed: The speed of a mobile object. 
     67        @type speed: L{twisted.positioning.base.Speed} 
     68        """ 
     69 
     70 
     71    def climbReceived(climb): 
     72        """ 
     73        Method called when the climb is received. 
     74 
     75        @param climb: The climb of the mobile object. 
     76        @type climb: L{twisted.positioning.base.Climb} 
     77        """ 
     78 
     79    def beaconInformationReceived(beaconInformation): 
     80        """ 
     81        Method called when positioning beacon information is received. 
     82 
     83        @param beaconInformation: The beacon information. 
     84        @type beaconInformation: L{twisted.positioning.base.BeaconInformation} 
     85        """ 
     86 
     87 
     88 
     89class INMEAReceiver(Interface): 
     90    """ 
     91    An object that can receive NMEA data. 
     92    """ 
     93    def sentenceReceived(sentence): 
     94        """ 
     95        Method called when a sentence is received. 
     96 
     97        @param sentence: The received NMEA sentence. 
     98        @type L{twisted.positioning.nmea.NMEASentence} 
     99        """ 
     100 
     101 
     102 
     103class IPositioningSentenceProducer(Interface): 
     104    """ 
     105    A protocol that produces positioning sentences. 
     106 
     107    Implementing this protocol allows sentence classes to be automagically 
     108    generated for a particular protocol. 
     109    """ 
     110    def getSentenceAttributes(self): 
     111        """ 
     112        Returns a set of attributes that might be present in a sentence produced 
     113        by this sentence producer. 
     114 
     115        @return: A set of attributes that might be present in a given sentence. 
     116        @rtype: C{set} of C{str} 
     117        """ 
  • new file w/twisted/positioning/nmea.py

    diff --git c/twisted/positioning/nmea.py w/twisted/positioning/nmea.py
    new file mode 100644
    index 0000000..c25aefe
    - +  
     1# -*- test-case-name: twisted.positioning.test.test_nmea -*- 
     2# Copyright (c) 2009-2011 Twisted Matrix Laboratories. 
     3# See LICENSE for details. 
     4""" 
     5Classes for working with NMEA (and vaguely NMEA-like) sentence producing 
     6devices. 
     7 
     8@since: 11.1 
     9""" 
     10 
     11import itertools 
     12import operator 
     13import datetime 
     14from zope.interface import implements, classProvides 
     15 
     16from twisted.protocols.basic import LineReceiver 
     17from twisted.positioning import base, ipositioning 
     18from twisted.positioning.base import LATITUDE, LONGITUDE, VARIATION 
     19 
     20# GPGGA fix quality: 
     21(GGA_INVALID_FIX, GGA_GPS_FIX, GGA_DGPS_FIX, GGA_PPS_FIX, GGA_RTK_FIX, 
     22 GGA_FLOAT_RTK_FIX, GGA_DEAD_RECKONING, GGA_MANUAL_FIX, GGA_SIMULATED_FIX 
     23 ) = [str(x) for x in range(9)] 
     24 
     25# GPGLL/GPRMC fix quality: 
     26DATA_ACTIVE, DATA_VOID = "A", "V" 
     27 
     28# Selection modes (used in a variety of sentences): 
     29MODE_AUTO, MODE_MANUAL = 'A', 'M' 
     30 
     31# GPGSA fix types: 
     32GSA_NO_FIX, GSA_2D_FIX, GSA_3D_FIX = '1', '2', '3' 
     33 
     34NMEA_NORTH, NMEA_EAST, NMEA_SOUTH, NMEA_WEST = "N", "E", "S", "W" 
     35 
     36 
     37def split(sentence): 
     38    """ 
     39    Returns the split version of an NMEA sentence, minus header 
     40    and checksum. 
     41 
     42    @param sentence: The NMEA sentence to split. 
     43    @type sentence: C{str} 
     44 
     45    >>> split("$GPGGA,spam,eggs*00") 
     46    ['GPGGA', 'spam', 'eggs'] 
     47    """ 
     48    if sentence[-3] == "*": # sentence with checksum 
     49        return sentence[1:-3].split(',') 
     50    elif sentence[-1] == "*": # sentence without checksum 
     51        return sentence[1:-1].split(',') 
     52    else: 
     53        raise base.InvalidSentence("malformed sentence %s" % sentence) 
     54 
     55 
     56def validateChecksum(sentence): 
     57    """ 
     58    Validates the checksum of an NMEA sentence. 
     59 
     60    @param sentence: The NMEA sentence to check the checksum of. 
     61    @type sentence: C{str} 
     62 
     63    @raise ValueError: If the sentence has an invalid checksum. 
     64 
     65    Simply returns on sentences that either don't have a checksum, 
     66    or have a valid checksum. 
     67    """ 
     68    if sentence[-3] == '*': # sentence has a checksum 
     69        reference, source = int(sentence[-2:], 16), sentence[1:-3] 
     70        computed = reduce(operator.xor, (ord(x) for x in source)) 
     71        if computed != reference: 
     72            raise base.InvalidChecksum("%02x != %02x" % (computed, reference)) 
     73 
     74 
     75 
     76class NMEAProtocol(LineReceiver, base.PositioningSentenceProducerMixin): 
     77    """ 
     78    A protocol that parses and verifies the checksum of an NMEA sentence (in 
     79    string form, not L{NMEASentence}), and delegates to a receiver. 
     80 
     81    It receives lines and verifies these lines are NMEA sentences. If 
     82    they are, verifies their checksum and unpacks them into their 
     83    components. It then wraps them in L{NMEASentence} objects and 
     84    calls the appropriate receiver method with them. 
     85    """ 
     86    classProvides(ipositioning.IPositioningSentenceProducer) 
     87    METHOD_PREFIX = "nmea_" 
     88 
     89    def __init__(self, receiver): 
     90        """ 
     91        Initializes an NMEAProtocol. 
     92 
     93        @param receiver: A receiver for NMEAProtocol sentence objects. 
     94        @type receiver: L{INMEAReceiver} 
     95        """ 
     96        self.receiver = receiver 
     97 
     98 
     99    def lineReceived(self, rawSentence): 
     100        """ 
     101        Parses the data from the sentence and validates the checksum. 
     102 
     103        @param rawSentence: The MMEA positioning sentence. 
     104        @type rawSentence: C{str} 
     105        """ 
     106        sentence = rawSentence.strip() 
     107 
     108        validateChecksum(sentence) 
     109        splitSentence = split(sentence) 
     110 
     111        sentenceType, contents = splitSentence[0], splitSentence[1:] 
     112 
     113        try: 
     114            keys = self.SENTENCE_CONTENTS[sentenceType] 
     115        except KeyError: 
     116            raise ValueError("unknown sentence type %s" % sentenceType) 
     117 
     118        sentenceData = {"type": sentenceType} 
     119        for key, value in itertools.izip(keys, contents): 
     120            if key is not None and value != "": 
     121                sentenceData[key] = value 
     122 
     123        sentence = NMEASentence(sentenceData) 
     124 
     125        try: 
     126            callback = getattr(self, self.METHOD_PREFIX + sentenceType) 
     127            callback(sentence) 
     128        except AttributeError: 
     129            pass # No sentence-specific callback on the protocol 
     130 
     131        if self.receiver is not None: 
     132            self.receiver.sentenceReceived(sentence) 
     133 
     134 
     135    SENTENCE_CONTENTS = { 
     136        'GPGGA': [ 
     137            'timestamp', 
     138 
     139            'latitudeFloat', 
     140            'latitudeHemisphere', 
     141            'longitudeFloat', 
     142            'longitudeHemisphere', 
     143 
     144            'fixQuality', 
     145            'numberOfSatellitesSeen', 
     146            'horizontalDilutionOfPrecision', 
     147 
     148            'altitude', 
     149            'altitudeUnits', 
     150            'heightOfGeoidAboveWGS84', 
     151            'heightOfGeoidAboveWGS84Units', 
     152 
     153            # The next parts are DGPS information. 
     154        ], 
     155 
     156        'GPRMC': [ 
     157            'timestamp', 
     158 
     159            'dataMode', 
     160 
     161            'latitudeFloat', 
     162            'latitudeHemisphere', 
     163            'longitudeFloat', 
     164            'longitudeHemisphere', 
     165 
     166            'speedInKnots', 
     167 
     168            'trueHeading', 
     169 
     170            'datestamp', 
     171 
     172            'magneticVariation', 
     173            'magneticVariationDirection', 
     174        ], 
     175 
     176        'GPGSV': [ 
     177            'numberOfGSVSentences', 
     178            'GSVSentenceIndex', 
     179 
     180            'numberOfSatellitesSeen', 
     181 
     182            'satellitePRN_0', 
     183            'elevation_0', 
     184            'azimuth_0', 
     185            'signalToNoiseRatio_0', 
     186 
     187            'satellitePRN_1', 
     188            'elevation_1', 
     189            'azimuth_1', 
     190            'signalToNoiseRatio_1', 
     191 
     192            'satellitePRN_2', 
     193            'elevation_2', 
     194            'azimuth_2', 
     195            'signalToNoiseRatio_2', 
     196 
     197            'satellitePRN_3', 
     198            'elevation_3', 
     199            'azimuth_3', 
     200            'signalToNoiseRatio_3', 
     201        ], 
     202 
     203        'GPGLL': [ 
     204            'latitudeFloat', 
     205            'latitudeHemisphere', 
     206            'longitudeFloat', 
     207            'longitudeHemisphere', 
     208            'timestamp', 
     209            'dataMode', 
     210        ], 
     211 
     212        'GPHDT': [ 
     213            'trueHeading', 
     214        ], 
     215 
     216        'GPTRF': [ 
     217            'datestamp', 
     218            'timestamp', 
     219 
     220            'latitudeFloat', 
     221            'latitudeHemisphere', 
     222            'longitudeFloat', 
     223            'longitudeHemisphere', 
     224 
     225            'elevation', 
     226            'numberOfIterations', # unused 
     227            'numberOfDopplerIntervals', # unused  
     228            'updateDistanceInNauticalMiles', # unused 
     229            'satellitePRN', 
     230        ], 
     231 
     232        'GPGSA': [ 
     233            'dataMode', 
     234            'fixType', 
     235 
     236            'usedSatellitePRN_0', 
     237            'usedSatellitePRN_1', 
     238            'usedSatellitePRN_2', 
     239            'usedSatellitePRN_3', 
     240            'usedSatellitePRN_4', 
     241            'usedSatellitePRN_5', 
     242            'usedSatellitePRN_6', 
     243            'usedSatellitePRN_7', 
     244            'usedSatellitePRN_8', 
     245            'usedSatellitePRN_9', 
     246            'usedSatellitePRN_10', 
     247            'usedSatellitePRN_11', 
     248 
     249            'positionDilutionOfPrecision', 
     250            'horizontalDilutionOfPrecision', 
     251            'verticalDilutionOfPrecision', 
     252        ] 
     253    } 
     254 
     255 
     256class NMEASentence(base.BaseSentence): 
     257    """ 
     258    An object representing an NMEA sentence. 
     259 
     260    The attributes of this objects are raw NMEA protocol data, which 
     261    are all ASCII bytestrings. 
     262 
     263    This object contains all the raw NMEA protocol data in a single 
     264    sentence.  Not all of these necessarily have to be present in the 
     265    sentence. Missing attributes are None when accessed. 
     266 
     267    Sentence-specific junk: 
     268 
     269    @ivar type: The sentence type ("GPGGA", "GPGSV"...). 
     270    @ivar numberOfGSVSentences: The total number of GSV sentences in a 
     271        sequence. 
     272    @ivar GSVSentenceIndex: The index of this GSV sentence in the GSV 
     273        sequence. 
     274 
     275    Time-related attributes: 
     276 
     277    @ivar timestamp: A timestamp. ("123456" -> 12:34:56Z) 
     278    @ivar datestamp: A datestamp. ("230394" -> 23 Mar 1994) 
     279 
     280    Location-related attributes: 
     281 
     282    @ivar latitudeFloat: Latitude value. (for example: "1234.567" -> 
     283        12 degrees, 34.567 minutes). 
     284    @ivar latitudeHemisphere: Latitudinal hemisphere ("N" or "S"). 
     285    @ivar longitudeFloat: Longitude value. See C{latitudeFloat} for an 
     286        example. 
     287    @ivar longitudeHemisphere: Longitudinal hemisphere ("E" or "W"). 
     288    @ivar altitude: The altitude above mean sea level. 
     289    @ivar altitudeUnits: Units in which altitude is expressed. (Always 
     290        "M" for meters.) 
     291    @ivar heightOfGeoidAboveWGS84: The local height of the geoid above 
     292        the WGS84 ellipsoid model. 
     293    @ivar heightOfGeoidAboveWGS84Units: The units in which the height 
     294        above the geoid is expressed. (Always "M" for meters.) 
     295 
     296    Attributes related to direction and movement: 
     297 
     298    @ivar trueHeading: The true heading. 
     299    @ivar magneticVariation: The magnetic variation. 
     300    @ivar magneticVariationDirection: The direction of the magnetic 
     301        variation. One of C{"E"} or C{"W"}. 
     302    @ivar speedInKnots: The ground speed, expressed in knots. 
     303 
     304    Attributes related to fix and data quality: 
     305 
     306    @ivar fixQuality: The quality of the fix. This is a single digit 
     307        from C{"0"} to C{"8"}. The important ones are C{"0"} (invalid 
     308        fix), C{"1"} (GPS fix) and C{"2"} (DGPS fix). 
     309    @ivar dataMode: Signals if the data is usable or not. One of 
     310        L{DATA_ACTIVE} or L{DATA_VOID}. 
     311    @ivar numberOfSatellitesSeen: The number of satellites seen by the 
     312        receiver. 
     313    @ivar numberOfSatellitesUsed: The number of satellites used in 
     314        computing the fix. 
     315 
     316    Attributes related to precision: 
     317 
     318    @ivar horizontalDilutionOfPrecision: The dilution of the precision of the 
     319        position on a plane tangential to the geoid. (HDOP) 
     320    @ivar verticalDilutionOfPrecision: As C{horizontalDilutionOfPrecision}, 
     321        but for a position on a plane perpendicular to the geoid. (VDOP) 
     322    @ivar positionDilutionOfPrecision: Euclidian norm of HDOP and VDOP. 
     323 
     324    Attributes related to satellite-specific data: 
     325 
     326    @ivar C{satellitePRN}: The unique identifcation number of a particular 
     327        satelite. Optionally suffixed with C{_N} if multiple satellites are 
     328        referenced in a sentence, where C{N in range(4)}. 
     329    @ivar C{elevation}: The elevation of a satellite in decimal degrees. 
     330        Optionally suffixed with C{_N}, as with C{satellitePRN}. 
     331    @ivar C{azimuth}: The azimuth of a satellite in decimal degrees. 
     332        Optionally suffixed with C{_N}, as with C{satellitePRN}. 
     333    @ivar C{signalToNoiseRatio}: The SNR of a satellite signal, in decibels. 
     334        Optionally suffixed with C{_N}, as with C{satellitePRN}. 
     335    @ivar C{usedSatellitePRN_N}: Where C{int(N) in range(12)}. The PRN 
     336        of a satelite used in computing the fix. 
     337 
     338    """ 
     339    ALLOWED_ATTRIBUTES = NMEAProtocol.getSentenceAttributes() 
     340     
     341    def _isFirstGSVSentence(self): 
     342        """ 
     343        Tests if this current GSV sentence is the first one in a sequence. 
     344        """ 
     345        return self.GSVSentenceIndex == "1" 
     346 
     347 
     348    def _isLastGSVSentence(self): 
     349        """ 
     350        Tests if this current GSV sentence is the final one in a sequence. 
     351        """ 
     352        return self.GSVSentenceIndex == self.numberOfGSVSentences 
     353 
     354 
     355 
     356class NMEAAdapter(object): 
     357    """ 
     358    An adapter from NMEAProtocol receivers to positioning receivers. 
     359 
     360    @cvar DATESTAMP_HANDLING: Determines the way incomplete (two-digit) NMEA 
     361        datestamps are handled.. One of L{INTELLIGENT_DATESTAMPS} (default, 
     362        assumes dates are twenty-first century if the two-digit date is below 
     363        the L{INTELLIGENT_DATE_THRESHOLD}, twentieth century otherwise), 
     364        L{DATESTAMPS_FROM_20XX} (assumes all dates are twenty-first century), 
     365        L{DATESTAMPS_FROM_19XX} (assumes all dates are twentieth century). 
     366        All of these are class attributes of this class. 
     367 
     368    @cvar INTELLIGENT_DATE_THRESHOLD: The threshold that determines which 
     369        century we guess a year is in. If the year value in a sentence is above 
     370        this value, assumes the 20th century (19xx), otherwise assumes the 
     371        twenty-first century (20xx). 
     372    @type INTELLIGENT_DATE_THRESHOLD: L{int} 
     373    """ 
     374    implements(ipositioning.INMEAReceiver) 
     375 
     376 
     377    def __init__(self, receiver): 
     378        """ 
     379        Initializes a new NMEA adapter. 
     380 
     381        @param receiver: The receiver for positioning sentences. 
     382        @type receiver: L{twisted.positioning.IPositioningReceiver} 
     383        """ 
     384        self._state = {} 
     385        self._sentenceData = {} 
     386        self._receiver = receiver 
     387 
     388 
     389    def _fixTimestamp(self): 
     390        """ 
     391        Turns the NMEAProtocol timestamp notation into a datetime.time object. 
     392        The time in this object is expressed as Zulu time. 
     393        """ 
     394        timestamp = self.currentSentence.timestamp.split('.')[0] 
     395        timeObject = datetime.datetime.strptime(timestamp, '%H%M%S').time() 
     396        self._sentenceData['_time'] = timeObject 
     397 
     398 
     399    INTELLIGENT_DATESTAMPS = 0 
     400    DATESTAMPS_FROM_20XX = 1 
     401    DATESTAMPS_FROM_19XX = 2 
     402 
     403    DATESTAMP_HANDLING = INTELLIGENT_DATESTAMPS 
     404    INTELLIGENT_DATE_THRESHOLD = 80 
     405 
     406 
     407    def _fixDatestamp(self): 
     408        """ 
     409        Turns an NMEA datestamp format into a C{datetime.date} object. 
     410        """ 
     411        datestamp = self.currentSentence.datestamp 
     412 
     413        day, month, year = [int(ordinalString) for ordinalString in 
     414                            (datestamp[0:2], datestamp[2:4], datestamp[4:6])] 
     415 
     416        if self.DATESTAMP_HANDLING == self.INTELLIGENT_DATESTAMPS: 
     417            if year > self.INTELLIGENT_DATE_THRESHOLD: 
     418                year = int('19%02d' % year) 
     419            else: 
     420                year = int('20%02d' % year) 
     421 
     422        elif self.DATESTAMP_HANDLING == self.DATESTAMPS_FROM_20XX: 
     423            year = int('20%02d' % year) 
     424 
     425        elif self.DATESTAMP_HANDLING == self.DATESTAMPS_FROM_19XX: 
     426            year = int('19%02d' % year) 
     427 
     428        else: 
     429            raise ValueError("unknown datestamp handling method (%s)" 
     430                             % (self.DATESTAMP_HANDLING,)) 
     431 
     432        self._sentenceData['_date'] = datetime.date(year, month, day) 
     433 
     434 
     435    def _fixCoordinateFloat(self, coordinateType): 
     436        """ 
     437        Turns the NMEAProtocol coordinate format into Python float. 
     438 
     439        @param coordinateType: The coordinate type. Should be L{base.LATITUDE} 
     440            or L{base.LONGITUDE}. 
     441        """ 
     442        coordinateName = base.Coordinate.ANGLE_TYPE_NAMES[coordinateType] 
     443        key = coordinateName + 'Float' 
     444        nmeaCoordinate = getattr(self.currentSentence, key) 
     445 
     446        left, right = nmeaCoordinate.split('.') 
     447 
     448        degrees, minutes = int(left[:-2]), float("%s.%s" % (left[-2:], right)) 
     449        angle = degrees + minutes/60 
     450        coordinate = base.Coordinate(angle, coordinateType) 
     451        self._sentenceData[coordinateName] = coordinate 
     452 
     453 
     454    def _fixHemisphereSign(self, coordinateType, sentenceDataKey=None): 
     455        """ 
     456        Fixes the sign for a hemisphere. 
     457 
     458        This method must be called after the magnitude for the thing it 
     459        determines the sign of has been set. This is done by the following 
     460        functions: 
     461 
     462            - C{self.FIXERS['magneticVariation']} 
     463            - C{self.FIXERS['latitudeFloat']} 
     464            - C{self.FIXERS['longitudeFloat']} 
     465 
     466        @param coordinateType: Coordinate type. One of L{base.LATITUDE}, 
     467            L{base.LONGITUDE} or L{base.VARIATION}. 
     468        """ 
     469        sentenceDataKey = sentenceDataKey or coordinateType 
     470        sign = self._getHemisphereSign(coordinateType) 
     471        self._sentenceData[sentenceDataKey].setSign(sign) 
     472 
     473 
     474    COORDINATE_SIGNS = { 
     475        NMEA_NORTH: 1, 
     476        NMEA_EAST: 1, 
     477        NMEA_SOUTH: -1, 
     478        NMEA_WEST: -1 
     479    } 
     480 
     481 
     482    def _getHemisphereSign(self, coordinateType): 
     483        """ 
     484        Returns the hemisphere sign for a given coordinate type. 
     485 
     486        @param coordinateType: Coordinate type. One of L{base.LATITUDE}, 
     487            L{base.LONGITUDE} or L{base.VARIATION}. 
     488        """ 
     489        if coordinateType in (LATITUDE, LONGITUDE): 
     490            hemisphereKey = (base.Coordinate.ANGLE_TYPE_NAMES[coordinateType] 
     491                             + 'Hemisphere') 
     492        elif coordinateType == VARIATION: 
     493            hemisphereKey = 'magneticVariationDirection' 
     494        else: 
     495            raise ValueError("unknown coordinate type %s" % (coordinateType,)) 
     496 
     497        hemisphere = getattr(self.currentSentence, hemisphereKey) 
     498 
     499        try:  
     500           return self.COORDINATE_SIGNS[hemisphere.upper()] 
     501        except KeyError: 
     502            raise ValueError("bad hemisphere/direction: %s" % hemisphere) 
     503 
     504 
     505    def _convert(self, sourceKey, converter=float, destinationKey=None): 
     506        """ 
     507        A simple conversion fix. 
     508 
     509        @param sourceKey: The attribute name of the value to fix. 
     510        @type sourceKey: C{str} (Python identifier) 
     511 
     512        @param converter: The function that converts the value. 
     513        @type converter: unary callable 
     514 
     515        @param destinationKey: The target attribute key. If unset or 
     516            C{None}, same as C{sourceKey}. 
     517        @type destinationKey: C{str} (Python identifier) 
     518        """ 
     519        currentValue = getattr(self.currentSentence, sourceKey) 
     520 
     521        if destinationKey is None: 
     522            destinationKey = sourceKey 
     523 
     524        self._sentenceData[destinationKey] = converter(currentValue) 
     525 
     526 
     527 
     528    STATEFUL_UPDATE = { 
     529        # sentenceKey: (stateKey, factory, attributeName, converter), 
     530        'trueHeading': 
     531            ('heading', base.Heading, '_angle', float), 
     532        'magneticVariation': 
     533            ('heading', base.Heading, 'variation', 
     534             lambda angle: base.Angle(float(angle), VARIATION)), 
     535 
     536        'horizontalDilutionOfPrecision': 
     537            ('positionError', base.PositionError, 'hdop', float), 
     538        'verticalDilutionOfPrecision': 
     539            ('positionError', base.PositionError, 'vdop', float), 
     540        'positionDilutionOfPrecision': 
     541            ('positionError', base.PositionError, 'pdop', float), 
     542 
     543    } 
     544 
     545 
     546    def _statefulUpdate(self, sentenceKey): 
     547        """ 
     548        Does a stateful update of a particular positioning attribute. 
     549 
     550        @param sentenceKey: The name of the key in the sentence attributes, 
     551            C{NMEAAdapter.STATEFUL_UPDATE} dictionary and the adapter state. 
     552        @type sentenceKey: C{str} 
     553        """ 
     554        state, factory, attr, converter = self.STATEFUL_UPDATE[sentenceKey] 
     555 
     556        if state not in self._sentenceData: 
     557            self._sentenceData[state] = self._state.get(state, factory()) 
     558 
     559        newValue = converter(getattr(self.currentSentence, sentenceKey)) 
     560        setattr(self._sentenceData[state], attr, newValue) 
     561 
     562 
     563    ACCEPTABLE_UNITS = frozenset(['M']) 
     564    UNIT_CONVERTERS = { 
     565        'N': lambda inKnots: base.Speed(float(inKnots) * base.MPS_PER_KNOT), 
     566        'K': lambda inKPH: base.Speed(float(inKPH) * base.MPS_PER_KPH), 
     567    } 
     568 
     569 
     570    def _fixUnits(self, unitKey=None, valueKey=None, sourceKey=None, unit=None): 
     571        """ 
     572        Fixes the units of a certain value. 
     573 
     574        @param unit: The unit that is being converted I{from}. If unspecified 
     575            or None, asks the current sentence for the C{unitKey}. If that also 
     576            fails, raises C{AttributeError}. 
     577        @type unit: C{str} 
     578        @param unitKey: The name of the key/attribute under which the unit can 
     579            be found in the current sentence. If the C{unit} parameter is set, 
     580            this parameter is not used. 
     581        @type unitKey: C{str} 
     582        @param sourceKey: The name of the key/attribute that contains the 
     583            current value to be converted (expressed in units as defined 
     584            according to the the C{unit} parameter). If unset, will use the 
     585            same key as the value key. 
     586        @type sourceKey: C{str} 
     587        @param valueKey: The key name in which the data will be stored in the 
     588            C{_sentenceData} instance attribute. If unset, attempts to strip 
     589            "Units" from the C{unitKey} parameter. 
     590        @type valueKey: C{str} 
     591 
     592        None of the keys are allowed to be the empty string. 
     593        """ 
     594        unit = unit or getattr(self.currentSentence, unitKey) 
     595        valueKey = valueKey or unitKey.strip('Units') 
     596        sourceKey = sourceKey or valueKey 
     597 
     598        if unit not in self.ACCEPTABLE_UNITS: 
     599            converter = self.UNIT_CONVERTERS[unit] 
     600            currentValue = getattr(self.currentSentence, sourceKey) 
     601            self._sentenceData[valueKey] = converter(currentValue) 
     602 
     603 
     604    GSV_KEYS = "satellitePRN", "azimuth", "elevation", "signalToNoiseRatio" 
     605 
     606 
     607    def _fixGSV(self): 
     608        """ 
     609        Parses partial visible satellite information from a GSV sentence. 
     610        """ 
     611        # To anyone who knows NMEA, this method's name should raise a chuckle's 
     612        # worth of schadenfreude. 'Fix' GSV? Hah! Ludicrous. 
     613        self._sentenceData['_partialBeaconInformation'] = base.BeaconInformation() 
     614 
     615        for index in range(4): 
     616            keys = ["%s_%i" % (key, index) for key in self.GSV_KEYS] 
     617            values = [getattr(self.currentSentence, k) for k in keys] 
     618            prn, azimuth, elevation, snr = values 
     619 
     620            if prn is None or snr is None: 
     621                # The peephole optimizer optimizes the jump away, meaning that 
     622                # coverage.py isn't covered. It is. Replace it with break and 
     623                # watch the test case fail. 
     624                # ML thread about this issue: http://goo.gl/1KNUi 
     625                # Related CPython bug: http://bugs.python.org/issue2506 
     626                continue # pragma: no cover 
     627 
     628            satellite = base.Satellite(prn, azimuth, elevation, snr) 
     629            bi = self._sentenceData['_partialBeaconInformation'] 
     630            bi.beacons.add(satellite) 
     631 
     632 
     633    def _fixGSA(self): 
     634        """ 
     635        Extracts the information regarding which satellites were used in 
     636        obtaining the GPS fix from a GSA sentence. 
     637 
     638        @precondition: A GSA sentence was fired. 
     639        @postcondition: The current sentence data (C{self._sentenceData} will 
     640            contain a set of the currently used PRNs (under the key 
     641            C{_usedPRNs}. 
     642        """ 
     643        self._sentenceData['_usedPRNs'] = set() 
     644        for key in ("usedSatellitePRN_%d" % x for x in range(12)): 
     645            prn = getattr(self.currentSentence, key, None) 
     646            if prn is not None: 
     647                self._sentenceData['_usedPRNs'].add(int(prn)) 
     648 
     649 
     650    SPECIFIC_SENTENCE_FIXES = { 
     651        'GPGSV': _fixGSV, 
     652        'GPGSA': _fixGSA, 
     653    } 
     654 
     655 
     656    def _sentenceSpecificFix(self): 
     657        """ 
     658        Executes a fix for a specific type of sentence. 
     659        """ 
     660        fixer = self.SPECIFIC_SENTENCE_FIXES.get(self.currentSentence.type) 
     661        if fixer is not None: 
     662            fixer(self) 
     663 
     664 
     665    FIXERS = { 
     666        'type': 
     667            lambda self: self._sentenceSpecificFix(), 
     668 
     669        'timestamp': 
     670            lambda self: self._fixTimestamp(), 
     671        'datestamp': 
     672            lambda self: self._fixDatestamp(), 
     673 
     674        'latitudeFloat': 
     675            lambda self: self._fixCoordinateFloat(LATITUDE), 
     676        'latitudeHemisphere': 
     677            lambda self: self._fixHemisphereSign(LATITUDE, 'latitude'), 
     678        'longitudeFloat': 
     679            lambda self: self._fixCoordinateFloat(LONGITUDE), 
     680        'longitudeHemisphere': 
     681            lambda self: self._fixHemisphereSign(LONGITUDE, 'longitude'), 
     682 
     683        'altitude': 
     684            lambda self: self._convert('altitude', 
     685                converter=lambda strRepr: base.Altitude(float(strRepr))), 
     686        'altitudeUnits': 
     687            lambda self: self._fixUnits(unitKey='altitudeUnits'), 
     688 
     689        'heightOfGeoidAboveWGS84': 
     690            lambda self: self._convert('heightOfGeoidAboveWGS84', 
     691                converter=lambda strRepr: base.Altitude(float(strRepr))), 
     692        'heightOfGeoidAboveWGS84Units': 
     693            lambda self: self._fixUnits( 
     694                unitKey='heightOfGeoidAboveWGS84Units'), 
     695 
     696        'trueHeading': 
     697            lambda self: self._statefulUpdate('trueHeading'), 
     698        'magneticVariation': 
     699            lambda self: self._statefulUpdate('magneticVariation'), 
     700 
     701        'magneticVariationDirection': 
     702            lambda self: self._fixHemisphereSign(VARIATION, 
     703                                                 'heading'), 
     704 
     705        'speedInKnots': 
     706            lambda self: self._fixUnits(valueKey='speed', 
     707                                        sourceKey='speedInKnots', 
     708                                        unit='N'), 
     709 
     710        'positionDilutionOfPrecision': 
     711            lambda self: self._statefulUpdate('positionDilutionOfPrecision'), 
     712        'horizontalDilutionOfPrecision': 
     713            lambda self: self._statefulUpdate('horizontalDilutionOfPrecision'), 
     714        'verticalDilutionOfPrecision': 
     715            lambda self: self._statefulUpdate('verticalDilutionOfPrecision'), 
     716    } 
     717 
     718 
     719    def clear(self): 
     720        """ 
     721        Resets this adapter. 
     722 
     723        This will empty the adapter state and the current sentence data. 
     724        """ 
     725        self._state = {} 
     726        self._sentenceData = {} 
     727 
     728 
     729    def sentenceReceived(self, sentence): 
     730        """ 
     731        Called when a sentence is received. 
     732 
     733        Will clean the received NMEAProtocol sentence up, and then update the 
     734        adapter's state, followed by firing the callbacks. 
     735 
     736        If the received sentence was invalid, the state will be cleared. 
     737 
     738        @param sentence: The sentence that is received. 
     739        @type sentence: L{NMEASentence} 
     740        """ 
     741        self.currentSentence = sentence 
     742 
     743        try: 
     744            self._validateCurrentSentence() 
     745            self._cleanCurrentSentence() 
     746        except base.InvalidSentence: 
     747            self.clear() 
     748 
     749        self._updateSentence() 
     750        self._fireSentenceCallbacks() 
     751 
     752 
     753    def _validateCurrentSentence(self): 
     754        """ 
     755        Tests if a sentence contains a valid fix. 
     756        """ 
     757        if (self.currentSentence.fixQuality == GGA_INVALID_FIX 
     758            or self.currentSentence.dataMode == DATA_VOID 
     759            or self.currentSentence.fixType == GSA_NO_FIX): 
     760            raise base.InvalidSentence("bad sentence") 
     761 
     762 
     763    def _cleanCurrentSentence(self): 
     764        """ 
     765        Cleans the current sentence. 
     766        """ 
     767        for key in sorted(self.currentSentence.presentAttributes): 
     768            fixer = self.FIXERS.get(key, None) 
     769 
     770            if fixer is not None: 
     771                fixer(self) 
     772 
     773 
     774    def _updateSentence(self): 
     775        """ 
     776        Updates the current state with the new information from the sentence. 
     777        """ 
     778        self._updateBeaconInformation() 
     779        self._combineDateAndTime() 
     780        self._state.update(self._sentenceData) 
     781 
     782 
     783    def _updateBeaconInformation(self): 
     784        """ 
     785        Updates existing beacon information state with new data. 
     786        """ 
     787        new = self._sentenceData.get('_partialBeaconInformation') 
     788        if new is None: 
     789            return 
     790 
     791        usedPRNs = (self._state.get('_usedPRNs') 
     792                    or self._sentenceData.get('_usedPRNs')) 
     793        if usedPRNs is not None: 
     794            for beacon in new.beacons: 
     795                beacon.isUsed = (beacon.identifier in usedPRNs) 
     796 
     797        old = self._state.get('_partialBeaconInformation') 
     798        if old is not None: 
     799            new.beacons.update(old.beacons) 
     800 
     801        if self.currentSentence._isLastGSVSentence(): 
     802            if not self.currentSentence._isFirstGSVSentence(): 
     803                # not a 1-sentence sequence, get rid of partial information 
     804                del self._state['_partialBeaconInformation'] 
     805            bi = self._sentenceData.pop('_partialBeaconInformation') 
     806            self._sentenceData['beaconInformation'] = bi 
     807 
     808 
     809    def _combineDateAndTime(self): 
     810        """ 
     811        Combines a C{datetime.date} object and a C{datetime.time} object, 
     812        collected from one or more NMEA sentences, into a single 
     813        C{datetime.datetime} object suitable for sending to the 
     814        L{IPositioningReceiver}. 
     815        """ 
     816        if not ('_date' in self._sentenceData or '_time' in self._sentenceData): 
     817            return 
     818 
     819        date, time = [self._sentenceData.get(key) or self._state.get(key) 
     820                      for key in ('_date', '_time')] 
     821 
     822        if date is None or time is None: 
     823            return 
     824 
     825        dt = datetime.datetime.combine(date, time) 
     826        self._sentenceData['time'] = dt 
     827 
     828 
     829    def _fireSentenceCallbacks(self): 
     830        """ 
     831        Fires sentence callbacks for the current sentence. 
     832 
     833        A callback will only fire if all of the keys it requires are present in 
     834        the current state and at least one such field was altered in the 
     835        current sentence. 
     836 
     837        The callbacks will only be fired with data from L{self._state}. 
     838        """ 
     839        for callbackName, requiredFields in self.REQUIRED_CALLBACK_FIELDS.items(): 
     840            callback = getattr(self._receiver, callbackName) 
     841 
     842            kwargs = {} 
     843            atLeastOnePresentInSentence = False 
     844 
     845            try: 
     846                for field in requiredFields: 
     847                    if field in self._sentenceData: 
     848                        atLeastOnePresentInSentence = True 
     849                    kwargs[field] = self._state[field] 
     850            except KeyError: 
     851                continue 
     852 
     853            if atLeastOnePresentInSentence: 
     854                callback(**kwargs) 
     855 
     856 
     857 
     858NMEAAdapter.REQUIRED_CALLBACK_FIELDS = dict( 
     859    (name, method.positional) for name, method 
     860    in ipositioning.IPositioningReceiver.namesAndDescriptions()) 
  • new file w/twisted/positioning/test/__init__.py

    diff --git c/twisted/positioning/test/__init__.py w/twisted/positioning/test/__init__.py
    new file mode 100644
    index 0000000..fcd5611
    - +  
     1# Copyright (c) 2009-2011 Twisted Matrix Laboratories. 
     2# See LICENSE for details. 
     3""" 
     4Tests for the Twisted positioning framework. 
     5""" 
  • new file w/twisted/positioning/test/test_base.py

    diff --git c/twisted/positioning/test/test_base.py w/twisted/positioning/test/test_base.py
    new file mode 100644
    index 0000000..95fdba6
    - +  
     1# Copyright (c) 2009-2011 Twisted Matrix Laboratories. 
     2# See LICENSE for details. 
     3""" 
     4Test cases for positioning primitives. 
     5""" 
     6from twisted.trial.unittest import TestCase 
     7from twisted.positioning import base 
     8from twisted.positioning.base import LATITUDE, LONGITUDE 
     9from twisted.positioning.base import NORTH, EAST, SOUTH, WEST 
     10 
     11 
     12class AngleTests(TestCase): 
     13    """ 
     14    Tests for the L{twisted.positioning.base.Angle} class. 
     15    """ 
     16    def test_empty(self): 
     17        """ 
     18        Tests the repr of an empty angle. 
     19        """ 
     20        a = base.Angle() 
     21        self.assertEquals("<Angle of unknown type (unknown value)>", repr(a)) 
     22 
     23 
     24    def test_variation(self): 
     25        """ 
     26        Tests the repr of an empty variation. 
     27        """ 
     28        a = base.Angle(angleType=base.VARIATION) 
     29        self.assertEquals("<Variation (unknown value)>", repr(a)) 
     30 
     31 
     32    def test_unknownType(self): 
     33        """ 
     34        Tests the repr of an unknown angle of a 1 decimal degree value. 
     35        """ 
     36        a = base.Angle(1.0) 
     37        self.assertEquals("<Angle of unknown type (1.0 degrees)>", repr(a)) 
     38 
     39 
     40 
     41class HeadingTests(TestCase): 
     42    """ 
     43    Tests for the L{twisted.positioning.base.Heading} class. 
     44    """ 
     45    def test_simple(self): 
     46        """ 
     47        Tests some of the basic features of a very simple heading. 
     48        """ 
     49        h = base.Heading(1.) 
     50        self.assertEquals(h.inDecimalDegrees, 1.) 
     51        self.assertEquals(h.variation, None) 
     52        self.assertEquals(h.correctedHeading, None) 
     53        self.assertEquals(float(h), 1.) 
     54 
     55 
     56    def test_headingWithoutVariationRepr(self): 
     57        """ 
     58        Tests the repr of a heading without a variation. 
     59        """ 
     60        h = base.Heading(1.) 
     61        self.assertEquals(repr(h), "<Heading (1.0 degrees, unknown variation)>") 
     62 
     63 
     64    def test_headingWithVariationRepr(self): 
     65        """ 
     66        Tests the repr of a heading with a variation. 
     67        """ 
     68        angle, variation = 1.0, -10.0 
     69        h = base.Heading.fromFloats(angle, variationValue=variation) 
     70 
     71        variationRepr = '<Variation (%s degrees)>' % (variation,) 
     72        expectedRepr = '<Heading (%s degrees, %s)>' % (angle, variationRepr) 
     73        self.assertEquals(repr(h), expectedRepr) 
     74 
     75 
     76    def test_equality(self): 
     77        """ 
     78        Tests if equal headings compare equal. 
     79        """ 
     80        self.assertEquals(base.Heading(1.), base.Heading(1.)) 
     81 
     82 
     83    def test_inequality(self): 
     84        """ 
     85        Tests if unequal headings compare unequal. 
     86        """ 
     87        self.assertNotEquals(base.Heading(1.), base.Heading(2.)) 
     88 
     89 
     90    def test_edgeCases(self): 
     91        """ 
     92        Tests that the two edge cases of a heading value of zero and a heading 
     93        value of zero with a variation of C{180.0} don't fail. 
     94        """ 
     95        base.Heading(0) 
     96        base.Heading(0, 180) 
     97 
     98 
     99    def _badValueTest(self, **kw): 
     100        """ 
     101        Helper function for verifying that bad values raise C{ValueError}. 
     102 
     103        Passes C{**kw} to L{base.Heading.fromFloats}, and checks if that raises. 
     104        """ 
     105        self.assertRaises(ValueError, base.Heading.fromFloats, **kw) 
     106 
     107 
     108    def test_badAngleValueEdgeCase(self): 
     109        """ 
     110        Tests that a heading with value C{360.0} fails. 
     111        """ 
     112        self._badValueTest(angleValue=360.0) 
     113 
     114 
     115    def test_badVariationEdgeCase(self): 
     116        """ 
     117        Tests that a variation of C{-180.0} fails. 
     118        """ 
     119        self._badValueTest(variationValue=-180.0) 
     120 
     121 
     122    def test_negativeHeading(self): 
     123        """ 
     124        Tests that negative heading values cause C{ValueError}. 
     125        """ 
     126        self._badValueTest(angleValue=-10.0) 
     127 
     128 
     129    def test_headingTooLarge(self): 
     130        """ 
     131        Tests that an angle value larger than C{360.0} raises C{ValueError}. 
     132        """ 
     133        self._badValueTest(angleValue=370.0) 
     134 
     135 
     136    def test_variationTooNegative(self): 
     137        """ 
     138        Tests that variation values less than C{-180.0} fail. 
     139        """ 
     140        self._badValueTest(variationValue=-190.0) 
     141 
     142 
     143    def test_variationTooPositive(self): 
     144        """ 
     145        Tests that variation values greater than C{-180.0} fail. 
     146        """ 
     147        self._badValueTest(variationValue=190.0) 
     148 
     149 
     150    def test_correctedHeading(self): 
     151        """ 
     152        Simple test for a corrected heading. 
     153        """ 
     154        h = base.Heading.fromFloats(1., variationValue=-10.) 
     155        self.assertEquals(h.correctedHeading, base.Angle(11., base.HEADING)) 
     156 
     157 
     158    def test_correctedHeadingOverflow(self): 
     159        """ 
     160        Tests that a corrected heading that comes out above 360 degrees is 
     161        correctly handled. 
     162        """ 
     163        h = base.Heading.fromFloats(359., variationValue=-2.) 
     164        self.assertEquals(h.correctedHeading, base.Angle(1., base.HEADING)) 
     165 
     166 
     167    def test_correctedHeadingOverflowEdgeCase(self): 
     168        """ 
     169        Tests that a corrected heading that comes out to exactly 360 degrees 
     170        is correctly handled. 
     171        """ 
     172        h = base.Heading.fromFloats(359., variationValue=-1.) 
     173        self.assertEquals(h.correctedHeading, base.Angle(0., base.HEADING)) 
     174 
     175 
     176    def test_correctedHeadingUnderflow(self): 
     177        """ 
     178        Tests that a corrected heading that comes out under 0 degrees is 
     179        correctly handled. 
     180        """ 
     181        h = base.Heading.fromFloats(1., variationValue=2.) 
     182        self.assertEquals(h.correctedHeading, base.Angle(359., base.HEADING)) 
     183 
     184 
     185    def test_correctedHeadingUnderflowEdgeCase(self): 
     186        """ 
     187        Tests that a corrected heading that comes out under 0 degrees is 
     188        correctly handled. 
     189        """ 
     190        h = base.Heading.fromFloats(1., variationValue=1.) 
     191        self.assertEquals(h.correctedHeading, base.Angle(0., base.HEADING)) 
     192 
     193 
     194    def test_setVariationSign(self): 
     195        """ 
     196        Tests that setting the sign on a variation works. 
     197        """ 
     198        h = base.Heading.fromFloats(1., variationValue=1.) 
     199        h.setSign(1) 
     200        self.assertEquals(h.variation.inDecimalDegrees, 1.) 
     201        h.setSign(-1) 
     202        self.assertEquals(h.variation.inDecimalDegrees, -1.) 
     203 
     204 
     205    def test_setBadVariationSign(self): 
     206        """ 
     207        Tests that setting invalid sign values on a variation fails 
     208        predictably. 
     209        """ 
     210        h = base.Heading.fromFloats(1., variationValue=1.) 
     211        self.assertRaises(ValueError, h.setSign, -50) 
     212        self.assertEquals(h.variation.inDecimalDegrees, 1.) 
     213 
     214        self.assertRaises(ValueError, h.setSign, 0) 
     215        self.assertEquals(h.variation.inDecimalDegrees, 1.) 
     216 
     217        self.assertRaises(ValueError, h.setSign, 50) 
     218        self.assertEquals(h.variation.inDecimalDegrees, 1.) 
     219 
     220 
     221    def test_setUnknownVariationSign(self): 
     222        """ 
     223        Tests that setting an otherwise correct sign on an unknown variation 
     224        fails predictably. 
     225        """ 
     226        h = base.Heading.fromFloats(1.) 
     227        self.assertEquals(None, h.variation.inDecimalDegrees) 
     228        self.assertRaises(ValueError, h.setSign, 1) 
     229 
     230 
     231 
     232class CoordinateTests(TestCase): 
     233    def test_simple(self): 
     234        """ 
     235        Test that coordinates are convertible into a float, and verifies the 
     236        generic coordinate repr. 
     237        """ 
     238        value = 10.0 
     239        c = base.Coordinate(value) 
     240        self.assertEquals(float(c), value) 
     241        expectedRepr = "<Angle of unknown type (%s degrees)>" % (value,)  
     242        self.assertEquals(repr(c), expectedRepr) 
     243 
     244 
     245    def test_positiveLatitude(self): 
     246        """ 
     247        Tests creating positive latitudes and verifies their repr. 
     248        """ 
     249        value = 50.0 
     250        c = base.Coordinate(value, LATITUDE) 
     251        self.assertEquals(repr(c), "<Latitude (%s degrees)>" % value) 
     252 
     253 
     254    def test_negativeLatitude(self): 
     255        """ 
     256        Tests creating negative latitudes and verifies their repr. 
     257        """ 
     258        value = -50.0 
     259        c = base.Coordinate(value, LATITUDE) 
     260        self.assertEquals(repr(c), "<Latitude (%s degrees)>" % value) 
     261 
     262 
     263    def test_positiveLongitude(self): 
     264        """ 
     265        Tests creating positive longitudes and verifies their repr. 
     266        """ 
     267        value = 50.0 
     268        c = base.Coordinate(value, LONGITUDE) 
     269        self.assertEquals(repr(c), "<Longitude (%s degrees)>" % value) 
     270 
     271 
     272    def test_negativeLongitude(self): 
     273        """ 
     274        Tests creating negative longitudes and verifies their repr. 
     275        """ 
     276        value = -50.0 
     277        c = base.Coordinate(value, LONGITUDE) 
     278        self.assertEquals(repr(c), "<Longitude (%s degrees)>" % value) 
     279 
     280 
     281    def test_badCoordinateType(self): 
     282        """ 
     283        Tests that creating coordinates with bogus types raises C{ValueError}. 
     284        """ 
     285        self.assertRaises(ValueError, base.Coordinate, 150.0, "BOGUS") 
     286 
     287 
     288    def test_equality(self): 
     289        """ 
     290        Tests that equal coordinates compare equal. 
     291        """ 
     292        self.assertEquals(base.Coordinate(1.0), base.Coordinate(1.0)) 
     293 
     294 
     295    def test_differentAnglesInequality(self): 
     296        """ 
     297        Tests that coordinates with different angles compare unequal. 
     298        """ 
     299        c1 = base.Coordinate(1.0) 
     300        c2 = base.Coordinate(-1.0) 
     301        self.assertNotEquals(c1, c2) 
     302 
     303 
     304    def test_differentTypesInequality(self): 
     305        """ 
     306        Tests that coordinates with the same angles but different types 
     307        compare unequal. 
     308        """ 
     309        c1 = base.Coordinate(1.0, LATITUDE) 
     310        c2 = base.Coordinate(1.0, LONGITUDE) 
     311        self.assertNotEquals(c1, c2) 
     312 
     313 
     314    def test_sign(self): 
     315        """ 
     316        Tests that setting the sign on a coordinate works. 
     317        """ 
     318        c = base.Coordinate(50., LATITUDE) 
     319        c.setSign(1) 
     320        self.assertEquals(c.inDecimalDegrees, 50.) 
     321        c.setSign(-1) 
     322        self.assertEquals(c.inDecimalDegrees, -50.) 
     323 
     324 
     325    def test_badVariationSign(self): 
     326        """ 
     327        Tests that setting a bogus sign value on a coordinate raises 
     328        C{ValueError} and doesn't affect the coordinate. 
     329        """ 
     330        value = 50.0 
     331        c = base.Coordinate(value, LATITUDE) 
     332 
     333        self.assertRaises(ValueError, c.setSign, -50) 
     334        self.assertEquals(c.inDecimalDegrees, 50.) 
     335 
     336        self.assertRaises(ValueError, c.setSign, 0) 
     337        self.assertEquals(c.inDecimalDegrees, 50.) 
     338 
     339        self.assertRaises(ValueError, c.setSign, 50) 
     340        self.assertEquals(c.inDecimalDegrees, 50.) 
     341 
     342 
     343    def test_hemispheres(self): 
     344        """ 
     345        Checks that coordinates know which hemisphere they're in. 
     346        """ 
     347        coordinatesAndHemispheres = [ 
     348            (base.Coordinate(1.0, LATITUDE), NORTH), 
     349            (base.Coordinate(-1.0, LATITUDE), SOUTH), 
     350            (base.Coordinate(1.0, LONGITUDE), EAST), 
     351            (base.Coordinate(-1.0, LONGITUDE), WEST), 
     352        ] 
     353 
     354        for coordinate, expectedHemisphere in coordinatesAndHemispheres: 
     355            self.assertEquals(expectedHemisphere, coordinate.hemisphere) 
     356 
     357 
     358    def test_badHemisphere(self): 
     359        """ 
     360        Checks that asking for a hemisphere when the coordinate doesn't know 
     361        raises C{ValueError}. 
     362        """ 
     363        c = base.Coordinate(1.0, None) 
     364        self.assertRaises(ValueError, lambda: c.hemisphere) 
     365 
     366 
     367    def test_badLatitudeValues(self): 
     368        """ 
     369        Tests that latitudes outside of M{-90.0 < latitude < 90.0} raise 
     370        C{ValueError}. 
     371        """ 
     372        self.assertRaises(ValueError, base.Coordinate, 150.0, LATITUDE) 
     373        self.assertRaises(ValueError, base.Coordinate, -150.0, LATITUDE) 
     374 
     375 
     376    def test_badLongitudeValues(self): 
     377        """ 
     378        Tests that longitudes outside of M{-180.0 < longitude < 180.0} raise 
     379        C{ValueError}. 
     380        """ 
     381        self.assertRaises(ValueError, base.Coordinate, 250.0, LONGITUDE) 
     382        self.assertRaises(ValueError, base.Coordinate, -250.0, LONGITUDE) 
     383 
     384 
     385    def test_inDegreesMinutesSeconds(self): 
     386        """ 
     387        Tests accessing coordinate values in degrees, minutes and seconds. 
     388        """ 
     389        c = base.Coordinate(50.5, LATITUDE) 
     390        self.assertEquals(c.inDegreesMinutesSeconds, (50, 30, 0)) 
     391 
     392        c = base.Coordinate(50.213, LATITUDE) 
     393        self.assertEquals(c.inDegreesMinutesSeconds, (50, 12, 46)) 
     394 
     395 
     396    def test_unknownAngleInDegreesMinutesSeconds(self): 
     397        """ 
     398        Tests accessing unknown coordinate values in degrees, minutes 
     399        and seconds. 
     400        """ 
     401        c = base.Coordinate(None, None) 
     402        self.assertEquals(c.inDegreesMinutesSeconds, None) 
     403 
     404 
     405 
     406class AltitudeTests(TestCase): 
     407    """ 
     408    Tests for the L{twisted.positioning.base.Altitude} class. 
     409    """ 
     410    def test_simple(self): 
     411        """ 
     412        Tests basic altitude functionality. 
     413        """ 
     414        a = base.Altitude(1.) 
     415        self.assertEquals(float(a), 1.) 
     416        self.assertEquals(a.inMeters, 1.) 
     417        self.assertEquals(a.inFeet, 1./base.METERS_PER_FOOT) 
     418        self.assertEquals(repr(a), "<Altitude (1.0 m)>") 
     419 
     420 
     421    def test_equality(self): 
     422        """ 
     423        Tests that equal altitudes compare equal. 
     424        """ 
     425        a1 = base.Altitude(1.) 
     426        a2 = base.Altitude(1.) 
     427        self.assertEquals(a1, a2) 
     428 
     429 
     430    def test_inequality(self): 
     431        """ 
     432        Tests that unequal altitudes compare unequal. 
     433        """ 
     434        a1 = base.Altitude(1.) 
     435        a2 = base.Altitude(-1.) 
     436        self.assertNotEquals(a1, a2) 
     437 
     438 
     439 
     440class SpeedTests(TestCase): 
     441    """ 
     442    Tests for the L{twisted.positioning.base.Speed} class. 
     443    """ 
     444    def test_simple(self): 
     445        """ 
     446        Tests basic speed functionality. 
     447        """ 
     448        s = base.Speed(50.0) 
     449        self.assertEquals(s.inMetersPerSecond, 50.0) 
     450        self.assertEquals(float(s), 50.0) 
     451        self.assertEquals(repr(s), "<Speed (50.0 m/s)>") 
     452 
     453 
     454    def test_negativeSpeeds(self): 
     455        """ 
     456        Tests that negative speeds raise C{ValueError}. 
     457        """ 
     458        self.assertRaises(ValueError, base.Speed, -1.0) 
     459 
     460 
     461    def test_inKnots(self): 
     462        """ 
     463        Tests that speeds can be converted into knots correctly. 
     464        """ 
     465        s = base.Speed(1.0) 
     466        self.assertEquals(1/base.MPS_PER_KNOT, s.inKnots) 
     467 
     468 
     469    def test_asFloat(self): 
     470        """ 
     471        Tests that speeds can be converted into C{float}s correctly. 
     472        """ 
     473        self.assertEquals(1.0, float(base.Speed(1.0))) 
     474 
     475 
     476 
     477class ClimbTests(TestCase): 
     478    """ 
     479    Tests for L{twisted.positioning.base.Climb}. 
     480    """ 
     481    def test_simple(self): 
     482        """ 
     483        Basic functionality for climb objects. 
     484        """ 
     485        s = base.Climb(42.) 
     486        self.assertEquals(s.inMetersPerSecond, 42.) 
     487        self.assertEquals(float(s), 42.) 
     488        self.assertEquals(repr(s), "<Climb (42.0 m/s)>") 
     489 
     490 
     491    def test_negativeClimbs(self): 
     492        """ 
     493        Tests that creating negative climbs works. 
     494        """ 
     495        base.Climb(-42.) 
     496 
     497 
     498    def test_speedInKnots(self): 
     499        """ 
     500        Tests that climbs can be converted into knots correctly. 
     501        """ 
     502        s = base.Climb(1.0) 
     503        self.assertEquals(1/base.MPS_PER_KNOT, s.inKnots) 
     504 
     505 
     506    def test_asFloat(self): 
     507        """ 
     508        Tests that speeds can be converted into C{float}s correctly. 
     509        """ 
     510        self.assertEquals(1.0, float(base.Climb(1.0))) 
     511 
     512 
     513 
     514class PositionErrorTests(TestCase): 
     515    """ 
     516    Tests for L{twisted.positioning.base.PositionError}. 
     517    """ 
     518    def test_allUnset(self): 
     519        """ 
     520        Tests that creating an empty L{PositionError} works without checking 
     521        the invariant. 
     522        """ 
     523        pe = base.PositionError() 
     524        for x in (pe.pdop, pe.hdop, pe.vdop): 
     525            self.assertEquals(None, x) 
     526 
     527 
     528    def test_allUnsetWithInvariant(self): 
     529        """ 
     530        Tests that creating an empty L{PositionError} works while checking the 
     531        invariant. 
     532        """ 
     533        pe = base.PositionError(testInvariant=True) 
     534        for x in (pe.pdop, pe.hdop, pe.vdop): 
     535            self.assertEquals(None, x) 
     536 
     537 
     538    def test_simpleWithoutInvariant(self): 
     539        """ 
     540        Tests that creating a simple L{PositionError} with just a HDOP without 
     541        checking the invariant works. 
     542        """ 
     543        base.PositionError(hdop=1.0) 
     544 
     545 
     546    def test_simpleWithInvariant(self): 
     547        """ 
     548        Tests that creating a simple L{PositionError} with just a HDOP while 
     549        checking the invariant works. 
     550        """ 
     551        base.PositionError(hdop=1.0, testInvariant=True) 
     552 
     553 
     554    def test_invalidWithoutInvariant(self): 
     555        """ 
     556        Tests that creating a simple L{PositionError} with all values set 
     557        without checking the invariant works. 
     558        """ 
     559        base.PositionError(pdop=1.0, vdop=1.0, hdop=1.0) 
     560 
     561 
     562    def test_invalidWithInvariant(self): 
     563        """ 
     564        Tests that creating a simple L{PositionError} with all values set to 
     565        inconsistent values while checking the invariant raises C{ValueError}. 
     566        """ 
     567        self.assertRaises(ValueError, base.PositionError, 
     568                          pdop=1.0, vdop=1.0, hdop=1.0, testInvariant=True) 
     569 
     570 
     571    def test_setDOPWithoutInvariant(self): 
     572        """ 
     573        Tests that setting the PDOP value (with HDOP and VDOP already known) 
     574        to an inconsistent value without checking the invariant works. 
     575        """ 
     576        pe = base.PositionError(hdop=1.0, vdop=1.0) 
     577        pe.pdop = 100.0 
     578        self.assertEquals(pe.pdop, 100.0) 
     579 
     580 
     581    def test_setDOPWithInvariant(self): 
     582        """ 
     583        Tests that setting the PDOP value (with HDOP and VDOP already known) 
     584        to an inconsistent value while checking the invariant raises 
     585        C{ValueError}. 
     586        """ 
     587        pe = base.PositionError(hdop=1.0, vdop=1.0, testInvariant=True) 
     588        pdop = pe.pdop 
     589 
     590        def setPDOP(pe): 
     591            pe.pdop = 100.0 
     592 
     593        self.assertRaises(ValueError, setPDOP, pe) 
     594        self.assertEqual(pe.pdop, pdop) 
     595 
     596 
     597    REPR_TEMPLATE = "<PositionError (pdop: %s, hdop: %s, vdop: %s)>" 
     598 
     599 
     600    def _testDOP(self, pe, pdop, hdop, vdop): 
     601        """ 
     602        Tests the DOP values in a position error, and the repr of that 
     603        position error. 
     604        """ 
     605        self.assertEquals(pe.pdop, pdop) 
     606        self.assertEquals(pe.hdop, hdop) 
     607        self.assertEquals(pe.vdop, vdop) 
     608        self.assertEquals(repr(pe), self.REPR_TEMPLATE % (pdop, hdop, vdop)) 
     609 
     610 
     611    def test_positionAndHorizontalSet(self): 
     612        """ 
     613        Tests that the VDOP is correctly determined from PDOP and HDOP. 
     614        """ 
     615        pdop, hdop = 2.0, 1.0 
     616        vdop = (pdop**2 - hdop**2)**.5 
     617        pe = base.PositionError(pdop=pdop, hdop=hdop) 
     618        self._testDOP(pe, pdop, hdop, vdop) 
     619 
     620 
     621    def test_positionAndVerticalSet(self): 
     622        """ 
     623        Tests that the HDOP is correctly determined from PDOP and VDOP. 
     624        """ 
     625        pdop, vdop = 2.0, 1.0 
     626        hdop = (pdop**2 - vdop**2)**.5 
     627        pe = base.PositionError(pdop=pdop, vdop=vdop) 
     628        self._testDOP(pe, pdop, hdop, vdop) 
     629 
     630 
     631    def test_horizontalAndVerticalSet(self): 
     632        """ 
     633        Tests that the PDOP is correctly determined from HDOP and VDOP. 
     634        """ 
     635        hdop, vdop = 1.0, 1.0 
     636        pdop = (hdop**2 + vdop**2)**.5 
     637        pe = base.PositionError(hdop=hdop, vdop=vdop) 
     638        self._testDOP(pe, pdop, hdop, vdop) 
     639 
     640 
     641 
     642class BeaconInformationTests(TestCase): 
     643    """ 
     644    Tests for L{twisted.positioning.base.BeaconInformation}. 
     645    """ 
     646    def test_minimal(self): 
     647        """ 
     648        Tests some basic features of a minimal beacon information object. 
     649 
     650        Tests the number of used beacons is zero, the total number of 
     651        beacons (the number of seen beacons) is zero, and the repr of 
     652        the object. 
     653        """ 
     654        bi = base.BeaconInformation() 
     655        self.assertEquals(len(list(bi.usedBeacons)), 0) 
     656        self.assertEquals(len(list(bi)), 0) 
     657        self.assertEquals(repr(bi), 
     658            "<BeaconInformation (seen: 0, used: 0, beacons: {})>") 
     659 
     660 
     661    satelliteKwargs = {"azimuth": 1, "elevation": 1, "signalToNoiseRatio": 1.} 
     662 
     663 
     664    def test_simple(self): 
     665        """ 
     666        Tests a beacon information with a bunch of satellites, none of 
     667        which used in computing a fix. 
     668        """ 
     669        def _buildSatellite(**kw): 
     670            kwargs = dict(self.satelliteKwargs) 
     671            kwargs.update(kw) 
     672            return base.Satellite(isUsed=None, **kwargs) 
     673 
     674        beacons = set() 
     675        for prn in range(1, 10): 
     676            beacons.add(_buildSatellite(identifier=prn)) 
     677 
     678        bi = base.BeaconInformation(beacons) 
     679 
     680        self.assertEquals(len(list(bi.usedBeacons)), 0) 
     681        self.assertEquals(bi.used, None) 
     682        self.assertEquals(len(list(bi)), 9) 
     683        self.assertEquals(repr(bi), 
     684            "<BeaconInformation (seen: 9, used: ?, beacons: {" 
     685            "<Satellite (1), azimuth: 1, elevation: 1, snr: 1.0, used: ?>, " 
     686            "<Satellite (2), azimuth: 1, elevation: 1, snr: 1.0, used: ?>, " 
     687            "<Satellite (3), azimuth: 1, elevation: 1, snr: 1.0, used: ?>, " 
     688            "<Satellite (4), azimuth: 1, elevation: 1, snr: 1.0, used: ?>, " 
     689            "<Satellite (5), azimuth: 1, elevation: 1, snr: 1.0, used: ?>, " 
     690            "<Satellite (6), azimuth: 1, elevation: 1, snr: 1.0, used: ?>, " 
     691            "<Satellite (7), azimuth: 1, elevation: 1, snr: 1.0, used: ?>, " 
     692            "<Satellite (8), azimuth: 1, elevation: 1, snr: 1.0, used: ?>, " 
     693            "<Satellite (9), azimuth: 1, elevation: 1, snr: 1.0, used: ?>" 
     694            "})>") 
     695 
     696 
     697    def test_someSatellitesUsed(self): 
     698        """ 
     699        Tests a beacon information with a bunch of satellites, some of 
     700        them used in computing a fix. 
     701        """ 
     702        def _buildSatellite(**kw): 
     703            kwargs = dict(self.satelliteKwargs) 
     704            kwargs.update(kw) 
     705            return base.Satellite(**kwargs) 
     706 
     707        beacons = set() 
     708        for prn in range(1, 10): 
     709            isUsed = bool(prn % 2) 
     710            satellite = _buildSatellite(identifier=prn, isUsed=isUsed) 
     711            beacons.add(satellite) 
     712 
     713        bi = base.BeaconInformation(beacons) 
     714 
     715        self.assertEquals(len(list(bi.usedBeacons)), 5) 
     716        self.assertEquals(bi.used, 5) 
     717        self.assertEquals(len(list(bi)), 9) 
     718        self.assertEquals(len(bi.beacons), 9) 
     719        self.assertEquals(bi.seen, 9) 
     720        self.assertEquals(repr(bi), 
     721            "<BeaconInformation (seen: 9, used: 5, beacons: {" 
     722            "<Satellite (1), azimuth: 1, elevation: 1, snr: 1.0, used: Y>, " 
     723            "<Satellite (2), azimuth: 1, elevation: 1, snr: 1.0, used: N>, " 
     724            "<Satellite (3), azimuth: 1, elevation: 1, snr: 1.0, used: Y>, " 
     725            "<Satellite (4), azimuth: 1, elevation: 1, snr: 1.0, used: N>, " 
     726            "<Satellite (5), azimuth: 1, elevation: 1, snr: 1.0, used: Y>, " 
     727            "<Satellite (6), azimuth: 1, elevation: 1, snr: 1.0, used: N>, " 
     728            "<Satellite (7), azimuth: 1, elevation: 1, snr: 1.0, used: Y>, " 
     729            "<Satellite (8), azimuth: 1, elevation: 1, snr: 1.0, used: N>, " 
     730            "<Satellite (9), azimuth: 1, elevation: 1, snr: 1.0, used: Y>" 
     731            "})>") 
     732 
     733 
     734 
     735class PositioningBeaconTests(TestCase): 
     736    """ 
     737    Tests for L{twisted.positioning.base.PositioningBeacon}. 
     738    """ 
     739    def test_usedRepr(self): 
     740        """ 
     741        Tests the repr of a positioning beacon being used. 
     742        """ 
     743        s = base.PositioningBeacon("A", True) 
     744        self.assertEquals(repr(s), "<Beacon (identifier: A, used: Y)>") 
     745 
     746 
     747    def test_unusedRepr(self): 
     748        """ 
     749        Tests the repr of a positioning beacon not being used. 
     750        """ 
     751        s = base.PositioningBeacon("A", False) 
     752        self.assertEquals(repr(s), "<Beacon (identifier: A, used: N)>") 
     753 
     754 
     755    def test_dontKnowIfUsed(self): 
     756        """ 
     757        Tests the repr of a positioning beacon that might be used. 
     758        """ 
     759        s = base.PositioningBeacon("A", None) 
     760        self.assertEquals(repr(s), "<Beacon (identifier: A, used: ?)>") 
     761 
     762 
     763 
     764class SatelliteTests(TestCase): 
     765    """ 
     766    Tests for L{twisted.positioning.base.Satellite}. 
     767    """ 
     768    def test_minimal(self): 
     769        """ 
     770        Tests a minimal satellite that only has a known PRN. 
     771 
     772        Tests that the azimuth, elevation and signal to noise ratios 
     773        are C{None} and verifies the repr. 
     774        """ 
     775        s = base.Satellite(1) 
     776        self.assertEquals(s.identifier, 1) 
     777        self.assertEquals(s.azimuth, None) 
     778        self.assertEquals(s.elevation, None) 
     779        self.assertEquals(s.signalToNoiseRatio, None) 
     780        self.assertEquals(repr(s), "<Satellite (1), azimuth: ?, " 
     781                                   "elevation: ?, snr: ?, used: ?>") 
     782 
     783 
     784    def test_simple(self): 
     785        """ 
     786        Tests a minimal satellite that only has a known PRN. 
     787 
     788        Tests that the azimuth, elevation and signal to noise ratios 
     789        are correct and verifies the repr. 
     790        """ 
     791        s = base.Satellite(identifier=1, 
     792                           azimuth=270., 
     793                           elevation=30., 
     794                           signalToNoiseRatio=25., 
     795                           isUsed=True) 
     796 
     797        self.assertEquals(s.identifier, 1) 
     798        self.assertEquals(s.azimuth, 270.) 
     799        self.assertEquals(s.elevation, 30.) 
     800        self.assertEquals(s.signalToNoiseRatio, 25.) 
     801        self.assertEquals(repr(s), "<Satellite (1), azimuth: 270.0, " 
     802                                   "elevation: 30.0, snr: 25.0, used: Y>") 
  • new file w/twisted/positioning/test/test_nmea.py

    diff --git c/twisted/positioning/test/test_nmea.py w/twisted/positioning/test/test_nmea.py
    new file mode 100644
    index 0000000..d574740
    - +  
     1# Copyright (c) 2009-2011 Twisted Matrix Laboratories. 
     2# See LICENSE for details. 
     3""" 
     4Test cases for using NMEA sentences. 
     5""" 
     6import datetime 
     7from zope.interface import implements 
     8 
     9from twisted.positioning import base, nmea, ipositioning 
     10from twisted.trial.unittest import TestCase 
     11 
     12from twisted.positioning.base import LATITUDE, LONGITUDE 
     13 
     14# Sample sentences 
     15GPGGA = '$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47' 
     16GPRMC = '$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A' 
     17GPGSA = '$GPGSA,A,3,19,28,14,18,27,22,31,39,,,,,1.7,1.0,1.3*34' 
     18GPHDT = '$GPHDT,038.005,T*3B' 
     19GPGLL = '$GPGLL,4916.45,N,12311.12,W,225444,A*31' 
     20GPGLL_PARTIAL = '$GPGLL,3751.65,S,14507.36,E*77' 
     21 
     22GPGSV_SINGLE = '$GPGSV,1,1,11,03,03,111,00,04,15,270,00,06,01,010,00,,,,*4b' 
     23GPGSV_EMPTY_MIDDLE = '$GPGSV,1,1,11,03,03,111,00,,,,,,,,,13,06,292,00*75' 
     24GPGSV_SEQ = GPGSV_FIRST, GPGSV_MIDDLE, GPGSV_LAST = """ 
     25$GPGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,00,13,06,292,00*74 
     26$GPGSV,3,2,11,14,25,170,00,16,57,208,39,18,67,296,40,19,40,246,00*74 
     27$GPGSV,3,3,11,22,42,067,42,24,14,311,43,27,05,244,00,,,,*4D 
     28""".split() 
     29 
     30 
     31 
     32class NMEATestReceiver(object): 
     33    """ 
     34    An NMEA receiver for testing. 
     35 
     36    Remembers the last sentence it has received. 
     37    """ 
     38    implements(ipositioning.INMEAReceiver) 
     39 
     40    def __init__(self): 
     41        self.clear() 
     42 
     43 
     44    def clear(self): 
     45        """ 
     46        Forgets the received sentence (if any), by setting 
     47        C{self.receivedSentence} to C{None}. 
     48        """ 
     49        self.receivedSentence = None 
     50 
     51 
     52    def sentenceReceived(self, sentence): 
     53        self.receivedSentence = sentence 
     54 
     55 
     56 
     57class NMEACallbackTestProtocol(nmea.NMEAProtocol): 
     58    """ 
     59    An NMEA protocol with a bunch of callbacks that remembers when 
     60    those callbacks have been called. 
     61    """ 
     62    def __init__(self): 
     63        nmea.NMEAProtocol.__init__(self, None) 
     64 
     65        for sentenceType in nmea.NMEAProtocol.SENTENCE_CONTENTS: 
     66            self._createCallback(sentenceType) 
     67 
     68        self.clear() 
     69 
     70 
     71    def clear(self): 
     72        """ 
     73        Forgets all of the called methods, by setting C{self.called} to 
     74        C{None}. 
     75        """ 
     76        self.called = {} 
     77 
     78 
     79    SENTENCE_TYPES = list(nmea.NMEAProtocol.SENTENCE_CONTENTS) 
     80 
     81 
     82    def _createCallback(self, sentenceType): 
     83        """ 
     84        Creates a callback for an NMEA sentence. 
     85        """ 
     86        def callback(sentence): 
     87            self.called[sentenceType] = True 
     88 
     89        setattr(self, "nmea_" + sentenceType, callback) 
     90 
     91 
     92 
     93class CallbackTests(TestCase): 
     94    """ 
     95    Tests if callbacks on NMEA protocols are correctly called. 
     96    """ 
     97    def setUp(self): 
     98        self.callbackProtocol = NMEACallbackTestProtocol() 
     99 
     100 
     101    def test_callbacksCalled(self): 
     102        """ 
     103        Tests that the correct callbacks fire, and that *only* those fire. 
     104        """ 
     105        sentencesByType = {'GPGGA': ['$GPGGA*56'], 
     106                           'GPGLL': ['$GPGLL*50'], 
     107                           'GPGSA': ['$GPGSA*42'], 
     108                           'GPGSV': ['$GPGSV*55'], 
     109                           'GPHDT': ['$GPHDT*4f'], 
     110                           'GPRMC': ['$GPRMC*4b']} 
     111 
     112        for calledSentenceType in sentencesByType: 
     113            for sentence in sentencesByType[calledSentenceType]: 
     114                self.callbackProtocol.lineReceived(sentence) 
     115                called = self.callbackProtocol.called 
     116 
     117                for sentenceType in NMEACallbackTestProtocol.SENTENCE_TYPES: 
     118                    if sentenceType == calledSentenceType: 
     119                        self.assertEquals(called[sentenceType], True) 
     120                    else: 
     121                        self.assertNotIn(sentenceType, called) 
     122 
     123                self.callbackProtocol.clear() 
     124 
     125 
     126 
     127class SplitTest(TestCase): 
     128    """ 
     129    Checks splitting of NMEA sentences. 
     130    """ 
     131    def test_withChecksum(self): 
     132        """ 
     133        Tests that an NMEA sentence with a checksum gets split correctly. 
     134        """ 
     135        splitSentence = nmea.split("$GPGGA,spam,eggs*00") 
     136        self.assertEqual(splitSentence, ['GPGGA', 'spam', 'eggs']) 
     137 
     138 
     139    def test_noCheckum(self): 
     140        """ 
     141        Tests that an NMEA sentence without a checksum gets split correctly. 
     142        """ 
     143        splitSentence = nmea.split("$GPGGA,spam,eggs*") 
     144        self.assertEqual(splitSentence, ['GPGGA', 'spam', 'eggs']) 
     145 
     146 
     147 
     148class ChecksumTests(TestCase): 
     149    """ 
     150    NMEA sentence checksum verification tests. 
     151    """ 
     152    def test_valid(self): 
     153        """ 
     154        Tests checkum validation for valid or missing checksums. 
     155        """ 
     156        sentences = [GPGGA, GPGGA[:-2]] 
     157 
     158        for s in sentences: 
     159            nmea.validateChecksum(s) 
     160 
     161 
     162    def test_invalid(self): 
     163        """ 
     164        Tests checksum validation on invalid checksums. 
     165        """ 
     166        bareSentence, checksum = GPGGA.split("*") 
     167        badChecksum = "%x" % (int(checksum, 16) + 1) 
     168        sentences = ["%s*%s" % (bareSentence, badChecksum)] 
     169 
     170        for s in sentences: 
     171            self.assertRaises(base.InvalidChecksum, nmea.validateChecksum, s) 
     172 
     173 
     174 
     175class NMEAReceiverSetup: 
     176    """ 
     177    A mixin for tests that need an NMEA receiver (and a protocol attached to 
     178    it). 
     179 
     180    @ivar receiver: An NMEA receiver that remembers the last sentence. 
     181    @type receiver: L{NMEATestReceiver} 
     182 
     183    @ivar protocol: An NMEA protocol attached to the receiver. 
     184    @type protocol: L{twisted.positioning.nmea.NMEAProtocol} 
     185    """ 
     186    def setUp(self): 
     187        self.receiver = NMEATestReceiver() 
     188        self.protocol = nmea.NMEAProtocol(self.receiver)     
     189 
     190 
     191 
     192class GSVSequenceTests(NMEAReceiverSetup, TestCase): 
     193    """ 
     194    Tests if GSV sentence sequences are identified correctly. 
     195    """ 
     196    def test_firstSentence(self): 
     197        """ 
     198        Tests if the last sentence in a GSV sequence is correctly identified. 
     199        """ 
     200        self.protocol.lineReceived(GPGSV_FIRST) 
     201        sentence = self.receiver.receivedSentence 
     202 
     203        self.assertTrue(sentence._isFirstGSVSentence()) 
     204        self.assertFalse(sentence._isLastGSVSentence()) 
     205 
     206 
     207    def test_middleSentence(self): 
     208        """ 
     209        Tests if a sentence in the middle of a GSV sequence is correctly 
     210        identified (as being neither the last nor the first). 
     211        """ 
     212        self.protocol.lineReceived(GPGSV_MIDDLE) 
     213        sentence = self.receiver.receivedSentence 
     214 
     215        self.assertFalse(sentence._isFirstGSVSentence()) 
     216        self.assertFalse(sentence._isLastGSVSentence()) 
     217 
     218 
     219    def test_lastSentence(self): 
     220        """ 
     221        Tests if the last sentence in a GSV sequence is correctly identified. 
     222        """ 
     223        self.protocol.lineReceived(GPGSV_LAST) 
     224        sentence = self.receiver.receivedSentence 
     225 
     226        self.assertFalse(sentence._isFirstGSVSentence()) 
     227        self.assertTrue(sentence._isLastGSVSentence()) 
     228 
     229 
     230 
     231class BogusSentenceTests(NMEAReceiverSetup, TestCase): 
     232    """ 
     233    Tests for verifying predictable failure for bogus NMEA sentences. 
     234    """ 
     235    def assertRaisesOnSentence(self, exceptionClass, sentence): 
     236        """ 
     237        Asserts that the protocol raises C{exceptionClass} when it receives 
     238        C{sentence}. 
     239 
     240        @param exceptionClass: The exception class expected to be raised. 
     241        @type exceptionClass: C{Exception} subclass 
     242 
     243        @param sentence: The (bogus) NMEA sentence. 
     244        @type sentence: C{str} 
     245        """ 
     246        self.assertRaises(exceptionClass, self.protocol.lineReceived, sentence) 
     247 
     248 
     249    def test_raiseOnUnknownSentenceType(self): 
     250        """ 
     251        Tests that the protocol raises C{ValueError} when you feed it a 
     252        well-formed sentence of unknown type. 
     253        """ 
     254        self.assertRaisesOnSentence(ValueError, "$GPBOGUS*5b") 
     255 
     256 
     257    def test_raiseOnMalformedSentences(self): 
     258        """ 
     259        Tests that the protocol raises L{base.InvalidSentence} when you feed 
     260        it a malformed sentence. 
     261        """ 
     262        self.assertRaisesOnSentence(base.InvalidSentence, "GPBOGUS") 
     263 
     264 
     265 
     266class NMEASentenceTests(NMEAReceiverSetup, TestCase): 
     267    """ 
     268    Tests for L{nmea.NMEASentence} objects. 
     269    """ 
     270    def test_repr(self): 
     271        """ 
     272        Checks that the C{repr} of L{nmea.NMEASentence} objects is 
     273        predictable. 
     274        """ 
     275        sentencesWithExpectedRepr = [ 
     276            (GPGSA, 
     277             "<NMEASentence (GPGSA) {" 
     278             "dataMode: A, " 
     279             "fixType: 3, " 
     280             "horizontalDilutionOfPrecision: 1.0, " 
     281             "positionDilutionOfPrecision: 1.7, " 
     282             "usedSatellitePRN_0: 19, " 
     283             "usedSatellitePRN_1: 28, " 
     284             "usedSatellitePRN_2: 14, " 
     285             "usedSatellitePRN_3: 18, " 
     286             "usedSatellitePRN_4: 27, " 
     287             "usedSatellitePRN_5: 22, " 
     288             "usedSatellitePRN_6: 31, " 
     289             "usedSatellitePRN_7: 39, " 
     290             "verticalDilutionOfPrecision: 1.3" 
     291             "}>"), 
     292        ] 
     293 
     294        for sentence, repr_ in sentencesWithExpectedRepr: 
     295            self.protocol.lineReceived(sentence) 
     296            received = self.receiver.receivedSentence 
     297            self.assertEquals(repr(received), repr_) 
     298 
     299 
     300 
     301class ParsingTests(NMEAReceiverSetup, TestCase): 
     302    """ 
     303    Tests if raw NMEA sentences get parsed correctly. 
     304 
     305    This doesn't really involve any interpretation, just turning ugly raw NMEA 
     306    representations into objects that are more pleasant to work with. 
     307    """ 
     308    def _parserTest(self, sentence, expected): 
     309        """ 
     310        Passes a sentence to the protocol and gets the parsed sentence from 
     311        the receiver. Then verifies that the parsed sentence contains the 
     312        expected data. 
     313        """ 
     314        self.protocol.lineReceived(sentence) 
     315        received = self.receiver.receivedSentence 
     316        self.assertEquals(expected, received._sentenceData) 
     317 
     318 
     319    def test_fullRMC(self): 
     320        """ 
     321        Tests that a full RMC sentence is correctly parsed. 
     322        """ 
     323        expected = { 
     324             'type': 'GPRMC', 
     325             'latitudeFloat': '4807.038', 
     326             'latitudeHemisphere': 'N', 
     327             'longitudeFloat': '01131.000', 
     328             'longitudeHemisphere': 'E', 
     329             'magneticVariation': '003.1', 
     330             'magneticVariationDirection': 'W', 
     331             'speedInKnots': '022.4', 
     332             'timestamp': '123519', 
     333             'datestamp': '230394', 
     334             'trueHeading': '084.4', 
     335             'dataMode': 'A', 
     336        } 
     337        self._parserTest(GPRMC, expected) 
     338 
     339 
     340    def test_fullGGA(self): 
     341        """ 
     342        Tests that a full GGA sentence is correctly parsed. 
     343        """ 
     344        expected = { 
     345            'type': 'GPGGA', 
     346 
     347            'altitude': '545.4', 
     348            'altitudeUnits': 'M', 
     349            'heightOfGeoidAboveWGS84': '46.9', 
     350            'heightOfGeoidAboveWGS84Units': 'M', 
     351 
     352            'horizontalDilutionOfPrecision': '0.9', 
     353 
     354            'latitudeFloat': '4807.038', 
     355            'latitudeHemisphere': 'N', 
     356            'longitudeFloat': '01131.000', 
     357            'longitudeHemisphere': 'E', 
     358 
     359            'numberOfSatellitesSeen': '08', 
     360            'timestamp': '123519', 
     361            'fixQuality': '1', 
     362        } 
     363        self._parserTest(GPGGA, expected) 
     364 
     365 
     366    def test_fullGLL(self): 
     367        """ 
     368        Tests that a full GLL sentence is correctly parsed. 
     369        """ 
     370        expected = { 
     371            'type': 'GPGLL', 
     372 
     373            'latitudeFloat': '4916.45', 
     374            'latitudeHemisphere': 'N', 
     375            'longitudeFloat': '12311.12', 
     376            'longitudeHemisphere': 'W', 
     377 
     378            'timestamp': '225444', 
     379            'dataMode': 'A', 
     380        } 
     381        self._parserTest(GPGLL, expected) 
     382 
     383 
     384    def test_partialGLL(self): 
     385        """ 
     386        Tests that a partial GLL sentence is correctly parsed. 
     387        """ 
     388        expected = { 
     389            'type': 'GPGLL', 
     390 
     391            'latitudeFloat': '3751.65', 
     392            'latitudeHemisphere': 'S', 
     393            'longitudeFloat': '14507.36', 
     394            'longitudeHemisphere': 'E', 
     395        } 
     396        self._parserTest(GPGLL_PARTIAL, expected) 
     397 
     398 
     399    def test_fullGSV(self): 
     400        """ 
     401        Tests that a full GSV sentence is correctly parsed. 
     402        """ 
     403        expected = { 
     404            'type': 'GPGSV', 
     405            'GSVSentenceIndex': '1', 
     406            'numberOfGSVSentences': '3', 
     407            'numberOfSatellitesSeen': '11', 
     408 
     409            'azimuth_0': '111', 
     410            'azimuth_1': '270', 
     411            'azimuth_2': '010', 
     412            'azimuth_3': '292', 
     413 
     414            'elevation_0': '03', 
     415            'elevation_1': '15', 
     416            'elevation_2': '01', 
     417            'elevation_3': '06', 
     418 
     419            'satellitePRN_0': '03', 
     420            'satellitePRN_1': '04', 
     421            'satellitePRN_2': '06', 
     422            'satellitePRN_3': '13', 
     423 
     424            'signalToNoiseRatio_0': '00', 
     425            'signalToNoiseRatio_1': '00', 
     426            'signalToNoiseRatio_2': '00', 
     427            'signalToNoiseRatio_3': '00', 
     428        } 
     429        self._parserTest(GPGSV_FIRST, expected) 
     430 
     431 
     432    def test_partialGSV(self): 
     433        """ 
     434        Tests that a partial GSV sentence is correctly parsed. 
     435        """ 
     436        expected = { 
     437            'type': 'GPGSV', 
     438            'GSVSentenceIndex': '3', 
     439            'numberOfGSVSentences': '3', 
     440            'numberOfSatellitesSeen': '11', 
     441 
     442            'azimuth_0': '067', 
     443            'azimuth_1': '311', 
     444            'azimuth_2': '244', 
     445 
     446            'elevation_0': '42', 
     447            'elevation_1': '14', 
     448            'elevation_2': '05', 
     449 
     450            'satellitePRN_0': '22', 
     451            'satellitePRN_1': '24', 
     452            'satellitePRN_2': '27', 
     453 
     454            'signalToNoiseRatio_0': '42', 
     455            'signalToNoiseRatio_1': '43', 
     456            'signalToNoiseRatio_2': '00', 
     457        } 
     458        self._parserTest(GPGSV_LAST, expected) 
     459 
     460 
     461    def test_fullHDT(self): 
     462        """ 
     463        Tests that a full HDT sentence is correctly parsed. 
     464        """ 
     465        expected = { 
     466            'type': 'GPHDT', 
     467            'trueHeading': '038.005', 
     468        } 
     469        self._parserTest(GPHDT, expected) 
     470 
     471 
     472    def test_typicalGSA(self): 
     473        """ 
     474        Tests that a typical GSA sentence is correctly parsed. 
     475        """ 
     476        expected = { 
     477            'type': 'GPGSA', 
     478 
     479            'dataMode': 'A', 
     480            'fixType': '3', 
     481 
     482            'usedSatellitePRN_0': '19', 
     483            'usedSatellitePRN_1': '28', 
     484            'usedSatellitePRN_2': '14', 
     485            'usedSatellitePRN_3': '18', 
     486            'usedSatellitePRN_4': '27', 
     487            'usedSatellitePRN_5': '22', 
     488            'usedSatellitePRN_6': '31', 
     489            'usedSatellitePRN_7': '39', 
     490 
     491            'positionDilutionOfPrecision': '1.7', 
     492            'horizontalDilutionOfPrecision': '1.0', 
     493            'verticalDilutionOfPrecision': '1.3', 
     494        } 
     495        self._parserTest(GPGSA, expected) 
     496 
     497 
     498 
     499class FixerTestMixin: 
     500    """ 
     501    Mixin for tests for the fixers on L{nmea.NMEAAdapter} that adapt 
     502    from NMEA-specific notations to generic Python objects. 
     503 
     504    @ivar adapter: The NMEA adapter. 
     505    @type adapter: L{nmea.NMEAAdapter} 
     506    """ 
     507    def setUp(self): 
     508        self.adapter = nmea.NMEAAdapter(base.BasePositioningReceiver()) 
     509 
     510 
     511    def _fixerTest(self, sentenceData, expected=None, exceptionClass=None): 
     512        """ 
     513        A generic adapter fixer test. 
     514 
     515        Creates a sentence from the C{sentenceData} and sends that to the 
     516        adapter. If C{exceptionClass} is not passed, this is assumed to work, 
     517        and C{expected} is compared with the adapter's internal state. 
     518        Otherwise, passing the sentence to the adapter is checked to raise 
     519        C{exceptionClass}. 
     520 
     521        @param sentenceData: Raw sentence content. 
     522        @type sentenceData: C{dict} mapping C{str} to C{str} 
     523 
     524        @param expected: The expected state of the adapter. 
     525        @type expected: C{dict} or C{None} 
     526 
     527        @param exceptionClass: The exception to be raised by the adapter. 
     528        @type exceptionClass: subclass of C{Exception} 
     529        """ 
     530        sentence = nmea.NMEASentence(sentenceData) 
     531        def receiveSentence(): 
     532            self.adapter.sentenceReceived(sentence) 
     533 
     534        if exceptionClass is None: 
     535            receiveSentence() 
     536            self.assertEquals(self.adapter._state, expected) 
     537        else: 
     538            self.assertRaises(exceptionClass, receiveSentence) 
     539 
     540        self.adapter.clear() 
     541 
     542 
     543 
     544class TimestampFixerTests(FixerTestMixin, TestCase): 
     545    """ 
     546    Tests conversion from NMEA timestamps to C{datetime.time} objects. 
     547    """ 
     548    def test_simple(self): 
     549        """ 
     550        Tests that a simple timestamp is converted correctly. 
     551        """ 
     552        data = {'timestamp': '123456'} # 12:34:56Z 
     553        expected = {'_time': datetime.time(12, 34, 56)} 
     554        self._fixerTest(data, expected) 
     555 
     556 
     557    def test_broken(self): 
     558        """ 
     559        Tests that a broken timestamp raises C{ValueError}. 
     560        """ 
     561        badTimestamps = '993456', '129956', '123499' 
     562 
     563        for t in badTimestamps: 
     564            self._fixerTest({'timestamp': t}, exceptionClass=ValueError) 
     565 
     566 
     567 
     568class DatestampFixerTests(FixerTestMixin, TestCase): 
     569    def test_intelligent(self): 
     570        """ 
     571        Tests "intelligent" datestamp handling (guess century based on last 
     572        two digits). Also tests that this is the default. 
     573        """ 
     574        self.assertEqual(self.adapter.DATESTAMP_HANDLING, 
     575                         self.adapter.INTELLIGENT_DATESTAMPS) 
     576 
     577        datestring, date = '010199', datetime.date(1999, 1, 1) 
     578        self._fixerTest({'datestamp': datestring}, {'_date': date}) 
     579 
     580        datestring, date = '010109', datetime.date(2009, 1, 1) 
     581        self._fixerTest({'datestamp': datestring}, {'_date': date}) 
     582 
     583 
     584    def test_19xx(self): 
     585        """ 
     586        Tests 20th-century-only datestam handling method. 
     587        """ 
     588        self.adapter.DATESTAMP_HANDLING = self.adapter.DATESTAMPS_FROM_19XX 
     589 
     590        datestring, date = '010199', datetime.date(1999, 1, 1) 
     591        self._fixerTest({'datestamp': datestring}, {'_date': date}) 
     592 
     593        datestring, date = '010109', datetime.date(1909, 1, 1) 
     594        self._fixerTest({'datestamp': datestring}, {'_date': date}) 
     595 
     596 
     597    def test_20xx(self): 
     598        """ 
     599        Tests 21st-century-only datestam handling method. 
     600        """ 
     601        self.adapter.DATESTAMP_HANDLING = self.adapter.DATESTAMPS_FROM_20XX 
     602 
     603        datestring, date = '010199', datetime.date(2099, 1, 1) 
     604        self._fixerTest({'datestamp': datestring}, {'_date': date}) 
     605 
     606        datestring, date = '010109', datetime.date(2009, 1, 1) 
     607        self._fixerTest({'datestamp': datestring}, {'_date': date}) 
     608 
     609 
     610    def test_bogusMethod(self): 
     611        """ 
     612        Tests that using a nonexistent datestamp handling method raises C{ValueError}. 
     613        """ 
     614        self.adapter.DATESTAMP_HANDLING = "BOGUS_VALUE" 
     615        self._fixerTest({'datestamp': '010199'}, exceptionClass=ValueError) 
     616 
     617 
     618    def test_broken(self): 
     619        """ 
     620        Tests that a broken datestring raises C{ValueError}. 
     621        """ 
     622        self._fixerTest({'datestamp': '123456'}, exceptionClass=ValueError) 
     623 
     624 
     625 
     626def _nmeaFloat(degrees, minutes): 
     627    """ 
     628    Builds an NMEA float representation for a given angle in degrees and 
     629    decimal minutes. 
     630 
     631    @param degrees: The integer degrees for this angle. 
     632    @type degrees: C{int} 
     633    @param minutes: The decimal minutes value for this angle. 
     634    @type minutes: C{float} 
     635    @return: The NMEA float representation for this angle. 
     636    @rtype: C{str} 
     637    """ 
     638    return "%i%0.3f" % (degrees, minutes) 
     639 
     640 
     641def _coordinateSign(hemisphere): 
     642    """ 
     643    Return the sign of a coordinate. 
     644 
     645    This is C{1} if the coordinate is in the northern or eastern hemispheres, 
     646    C{-1} otherwise. 
     647 
     648    @param hemisphere: NMEA shorthand for the hemisphere. One of "NESW". 
     649    @type hemisphere: C{str} 
     650 
     651    @return: The sign of the coordinate value. 
     652    @rtype: C{int} 
     653    """ 
     654    return 1 if hemisphere in "NE" else -1 
     655 
     656 
     657def _coordinateType(hemisphere): 
     658    """ 
     659    Return the type of a coordinate. 
     660 
     661    This is L{LATITUDE} if the coordinate is in the northern or southern 
     662    hemispheres, L{LONGITUDE} otherwise. 
     663 
     664    @param hemisphere: NMEA shorthand for the hemisphere. One of "NESW". 
     665    @type hemisphere: C{str} 
     666 
     667    @return: The type of the coordinate (L{LATITUDE} or L{LONGITUDE}) 
     668    """ 
     669    return LATITUDE if hemisphere in "NS" else LONGITUDE 
     670 
     671 
     672 
     673class CoordinateFixerTests(FixerTestMixin, TestCase): 
     674    """ 
     675    Tests turning NMEA coordinate notations into something more pleasant. 
     676    """ 
     677    def _coordinateFixerTest(self, degrees, minutes, hemisphere): 
     678        """ 
     679        Tests that an NMEA representation of a coordinate at the given 
     680        location converts correctly into a L{base.Coordinate}. 
     681        """ 
     682        coordinateType = _coordinateType(hemisphere) 
     683        if coordinateType is LATITUDE: 
     684            typeName = "latitude" 
     685        else: 
     686            typeName = "longitude" 
     687 
     688        sentenceData = {"%sFloat" % typeName: _nmeaFloat(degrees, minutes), 
     689                        "%sHemisphere" % typeName: hemisphere} 
     690 
     691        coordinateValue = _coordinateSign(hemisphere)*(degrees + minutes/60) 
     692        coordinate = base.Coordinate(coordinateValue, coordinateType) 
     693 
     694        self._fixerTest(sentenceData, {typeName: coordinate}) 
     695 
     696 
     697    def test_north(self): 
     698        """ 
     699        Tests that NMEA coordinate representations in the northern hemisphere 
     700        convert correctly. 
     701        """ 
     702        self._coordinateFixerTest(10, 30.0, "N") 
     703 
     704 
     705    def test_south(self): 
     706        """ 
     707        Tests that NMEA coordinate representations in the southern hemisphere 
     708        convert correctly. 
     709        """ 
     710        self._coordinateFixerTest(45, 12.145, "S") 
     711 
     712 
     713    def test_east(self): 
     714        """ 
     715        Tests that NMEA coordinate representations in the eastern hemisphere 
     716        convert correctly. 
     717        """ 
     718        self._coordinateFixerTest(53, 31.513, "E") 
     719 
     720 
     721    def test_west(self): 
     722        """ 
     723        Tests that NMEA coordinate representations in the western hemisphere 
     724        convert correctly. 
     725        """ 
     726        self._coordinateFixerTest(12, 45.120, "W") 
     727 
     728 
     729    def test_badHemisphere(self): 
     730        """ 
     731        Tests that NMEA coordinate representations for nonexistent hemispheres 
     732        raise C{ValueError} when you attempt to parse them. 
     733        """ 
     734        sentenceData = {'longitudeHemisphere': 'Q'} 
     735        self._fixerTest(sentenceData, exceptionClass=ValueError) 
     736 
     737 
     738    def test_badHemisphereSign(self): 
     739        """ 
     740        Tests that NMEA coordinate repesentation parsing fails predictably 
     741        when you pass nonexistent coordinate types (not latitude or 
     742        longitude). 
     743        """ 
     744        getSign = lambda: self.adapter._getHemisphereSign("BOGUS_VALUE") 
     745        self.assertRaises(ValueError, getSign) 
     746 
     747 
     748 
     749class AltitudeFixerTests(FixerTestMixin, TestCase): 
     750    """ 
     751    Tests that NMEA representations of altitudes are correctly converted. 
     752    """ 
     753    def test_fixAltitude(self): 
     754        """ 
     755        Tests that the NMEA representation of an altitude (above mean sea 
     756        level) is correctly converted. 
     757        """ 
     758        key, value = 'altitude', '545.4' 
     759        altitude = base.Altitude(float(value)) 
     760        self._fixerTest({key: value}, {key: altitude}) 
     761 
     762 
     763    def test_heightOfGeoidAboveWGS84(self): 
     764        """ 
     765        Tests that the NMEA representation of an altitude of the geoid (above 
     766        the WGS84 reference level) is correctly converted. 
     767        """ 
     768        key, value = 'heightOfGeoidAboveWGS84', '46.9' 
     769        altitude = base.Altitude(float(value)) 
     770        self._fixerTest({key: value}, {key: altitude}) 
     771 
     772 
     773 
     774class SpeedFixerTests(FixerTestMixin, TestCase): 
     775    """ 
     776    Tests that NMEA representations of speeds are correctly converted. 
     777    """ 
     778    def test_speedInKnots(self): 
     779        """ 
     780        Tests if speeds reported in knots correctly get converted to 
     781        meters per second. 
     782        """ 
     783        key, value, targetKey = "speedInKnots", "10", "speed" 
     784        speed = base.Speed(float(value) * base.MPS_PER_KNOT) 
     785        self._fixerTest({key: value}, {targetKey: speed}) 
     786 
     787 
     788 
     789class VariationFixerTests(FixerTestMixin, TestCase): 
     790    """ 
     791    Tests if the absolute values of magnetic variations on the heading 
     792    and their sign get combined correctly, and if that value gets 
     793    combined with a heading correctly. 
     794    """ 
     795    def test_west(self): 
     796        """ 
     797        Tests westward (negative) magnetic variation. 
     798        """ 
     799        variation, direction = "1.34", "W" 
     800        heading = base.Heading.fromFloats(variationValue=-1*float(variation)) 
     801        sentenceData = {'magneticVariation': variation, 
     802                        'magneticVariationDirection': direction} 
     803 
     804        self._fixerTest(sentenceData, {'heading': heading}) 
     805 
     806 
     807    def test_east(self): 
     808        """ 
     809        Tests eastward (positive) magnetic variation. 
     810        """ 
     811        variation, direction = "1.34", "E" 
     812        heading = base.Heading.fromFloats(variationValue=float(variation)) 
     813        sentenceData = {'magneticVariation': variation, 
     814                        'magneticVariationDirection': direction} 
     815 
     816        self._fixerTest(sentenceData, {'heading': heading}) 
     817 
     818 
     819    def test_withHeading(self): 
     820        """ 
     821        Tests if variation values get combined with headings correctly. 
     822        """ 
     823        trueHeading, variation, direction = "123.12", "1.34", "E" 
     824        sentenceData = {'trueHeading': trueHeading, 
     825                        'magneticVariation': variation, 
     826                        'magneticVariationDirection': direction} 
     827        heading = base.Heading.fromFloats(float(trueHeading), 
     828                                          variationValue=float(variation)) 
     829        self._fixerTest(sentenceData, {'heading': heading}) 
     830 
     831 
     832 
     833class PositionErrorFixerTests(FixerTestMixin, TestCase): 
     834    """ 
     835    Position errors in NMEA are passed as dilutions of precision (DOP). This 
     836    is a measure relative to some specified value of the GPS device as its 
     837    "reference" precision. Unfortunately, there are very few ways of figuring 
     838    this out from just the device (sans manual). 
     839 
     840    There are two basic DOP values: vertical and horizontal. HDOP tells you 
     841    how precise your location is on the face of the earth (pretending it's 
     842    flat, at least locally). VDOP tells you how precise your altitude is 
     843    known. PDOP (position DOP) is a dependent value defined as the Nuclidean 
     844    norm of those two, and gives you a more generic "goodness of fix" value. 
     845    """ 
     846    def test_simple(self): 
     847        self._fixerTest( 
     848            {'horizontalDilutionOfPrecision': '11'}, 
     849            {'positionError': base.PositionError(hdop=11.)}) 
     850 
     851 
     852    def test_mixing(self): 
     853        pdop, hdop, vdop = "1", "1", "1" 
     854        positionError = base.PositionError(pdop=float(pdop), 
     855                                           hdop=float(hdop), 
     856                                           vdop=float(vdop)) 
     857        sentenceData = {'positionDilutionOfPrecision': pdop, 
     858                        'horizontalDilutionOfPrecision': hdop, 
     859                        'verticalDilutionOfPrecision': vdop} 
     860        self._fixerTest(sentenceData, {"positionError": positionError}) 
     861 
     862 
     863class ValidFixTests(FixerTestMixin, TestCase): 
     864    """ 
     865    Tests that data reported from a valid fix is used. 
     866    """ 
     867    def test_GGA(self): 
     868        """ 
     869        Tests that GGA data with a valid fix is used. 
     870        """ 
     871        sentenceData = {'type': 'GPGGA', 
     872                        'altitude': '545.4', 
     873                        'fixQuality': nmea.GGA_GPS_FIX} 
     874        expectedState = {'altitude': base.Altitude(545.4)} 
     875 
     876        self._fixerTest(sentenceData, expectedState) 
     877 
     878 
     879    def test_GLL(self): 
     880        """ 
     881        Tests that GLL data with a valid data mode is used. 
     882        """ 
     883        sentenceData = {'type': 'GPGLL', 
     884                        'altitude': '545.4', 
     885                        'dataMode': nmea.DATA_ACTIVE} 
     886        expectedState = {'altitude': base.Altitude(545.4)} 
     887 
     888        self._fixerTest(sentenceData, expectedState) 
     889 
     890 
     891 
     892class InvalidFixTests(FixerTestMixin, TestCase): 
     893    """ 
     894    Tests that data being reported from a bad or incomplete fix isn't 
     895    used. Although the specification dictates that GPSes shouldn't produce 
     896    NMEA sentences with real-looking values for altitude or position in them 
     897    unless they have at least some semblance of a GPS fix, this is widely 
     898    ignored. 
     899    """ 
     900    def _invalidFixTest(self, sentenceData): 
     901        """ 
     902        Tests that sentences with an invalid fix or data mode result in empty 
     903        state (ie, the data isn't used). 
     904        """ 
     905        self._fixerTest(sentenceData, {}) 
     906 
     907 
     908    def test_GGA(self): 
     909        """ 
     910        Tests that GGA sentence data is unused when there is no fix. 
     911        """ 
     912        sentenceData = {'type': 'GPGGA', 
     913                        'altitude': '545.4', 
     914                        'fixQuality': nmea.GGA_INVALID_FIX} 
     915 
     916        self._invalidFixTest(sentenceData) 
     917 
     918 
     919    def test_GLL(self): 
     920        """ 
     921        Tests that GLL sentence data is unused when the data is flagged as 
     922        void. 
     923        """ 
     924        sentenceData = {'type': 'GPGLL', 
     925                        'altitude': '545.4', 
     926                        'dataMode': nmea.DATA_VOID} 
     927 
     928        self._invalidFixTest(sentenceData) 
     929 
     930 
     931    def test_badGSADataMode(self): 
     932        """ 
     933        Tests that GSA sentence data is not used when there is no GPS fix, but 
     934        the data mode claims the data is "active". Some GPSes do do this, 
     935        unfortunately, and that means you shouldn't use the data. 
     936        """ 
     937        sentenceData = {'type': 'GPGSA', 
     938                        'altitude': '545.4', 
     939                        'dataMode': nmea.DATA_ACTIVE, 
     940                        'fixType': nmea.GSA_NO_FIX} 
     941        self._invalidFixTest(sentenceData) 
     942 
     943 
     944    def test_badGSAFixType(self): 
     945        """ 
     946        Tests that GSA sentence data is not used when the fix claims to be 
     947        valid (albeit only 2D), but the data mode says the data is void. Some 
     948        GPSes do do this, unfortunately, and that means you shouldn't use the 
     949        data. 
     950        """ 
     951        sentenceData = {'type': 'GPGSA', 
     952                        'altitude': '545.4', 
     953                        'dataMode': nmea.DATA_VOID, 
     954                        'fixType': nmea.GSA_2D_FIX} 
     955        self._invalidFixTest(sentenceData) 
     956 
     957 
     958    def test_badGSADataModeAndFixType(self): 
     959        """ 
     960        Tests that GSA sentence data is not use when neither the fix nor the 
     961        data mode is any good. 
     962        """ 
     963        sentenceData = {'type': 'GPGSA', 
     964                        'altitude': '545.4', 
     965                        'dataMode': nmea.DATA_VOID, 
     966                        'fixType': nmea.GSA_NO_FIX} 
     967        self._invalidFixTest(sentenceData) 
     968 
     969 
     970 
     971class MockNMEAReceiver(base.BasePositioningReceiver): 
     972    """ 
     973    A mock NMEA receiver. 
     974 
     975    Mocks all the L{IPositioningReceiver} methods with stubs that don't do 
     976    anything but register that they were called. 
     977    """ 
     978    def __init__(self): 
     979        self.clear() 
     980 
     981        for methodName in ipositioning.IPositioningReceiver: 
     982            self._addCallback(methodName) 
     983 
     984 
     985    def clear(self): 
     986        """ 
     987        Forget all the methods that have been called on this receiver, by 
     988        emptying C{self.called}. 
     989        """ 
     990        self.called = {} 
     991 
     992 
     993    def _addCallback(self, name): 
     994        def callback(*a, **kw): 
     995            self.called[name] = True 
     996 
     997        setattr(self, name, callback) 
     998 
     999 
     1000 
     1001class NMEAReceiverTest(TestCase): 
     1002    """ 
     1003    Tests for the NMEA receiver. 
     1004    """ 
     1005    def setUp(self): 
     1006        self.receiver = MockNMEAReceiver() 
     1007        self.adapter = nmea.NMEAAdapter(self.receiver) 
     1008        self.protocol = nmea.NMEAProtocol(self.adapter) 
     1009 
     1010 
     1011    def _receiverTest(self, sentences, expectedFired=(), extraTest=None): 
     1012        """ 
     1013        A generic test for NMEA receiver behavior. 
     1014 
     1015        @param sentences: The sequence of sentences to simulate receiving. 
     1016        @type sentences: iterable of C{str} 
     1017        @param expectedFired: The names of the callbacks expected to fire. 
     1018        @type expectedFired: iterable of C{str} 
     1019        @param extraTest: An optional extra test hook. 
     1020        @type extraTest: nullary callable 
     1021        """ 
     1022        for sentence in sentences: 
     1023            self.protocol.lineReceived(sentence) 
     1024 
     1025        actuallyFired = self.receiver.called.keys() 
     1026        self.assertEquals(set(actuallyFired), set(expectedFired)) 
     1027 
     1028        if extraTest is not None: 
     1029            extraTest() 
     1030 
     1031        self.receiver.clear() 
     1032        self.adapter.clear() 
     1033 
     1034 
     1035    def test_positionErrorUpdateAcrossStates(self): 
     1036        """ 
     1037        Tests that the positioning error is updated across multiple states. 
     1038        """ 
     1039        sentences = [GPGSA] + GPGSV_SEQ 
     1040        callbacksFired = ['positionErrorReceived', 'beaconInformationReceived'] 
     1041 
     1042        def checkBeaconInformation(): 
     1043            beaconInformation = self.adapter._state['beaconInformation'] 
     1044            self.assertEqual(beaconInformation.seen, 11) 
     1045            self.assertEqual(beaconInformation.used, 5) 
     1046 
     1047        self._receiverTest(sentences, callbacksFired, checkBeaconInformation) 
     1048 
     1049 
     1050    def test_emptyMiddleGSV(self): 
     1051        """ 
     1052        Tests that a GSV sentence with empty entries in any position 
     1053        does not mean the entries in subsequent positions are ignored. 
     1054        """ 
     1055        sentences = [GPGSV_EMPTY_MIDDLE] 
     1056        callbacksFired = ['beaconInformationReceived'] 
     1057 
     1058        def checkBeaconInformation(): 
     1059            beaconInformation = self.adapter._state['beaconInformation'] 
     1060            self.assertEqual(beaconInformation.seen, 2) 
     1061            prns = [satellite.identifier for satellite in beaconInformation] 
     1062            self.assertIn(13, prns) 
     1063 
     1064        self._receiverTest(sentences, callbacksFired, checkBeaconInformation) 
     1065 
     1066    def test_GGASentences(self): 
     1067        """ 
     1068        Tests that a sequence of GGA sentences fires C{positionReceived}, 
     1069        C{positionErrorReceived} and C{altitudeReceived}. 
     1070        """ 
     1071        sentences = [GPGGA] 
     1072        callbacksFired = ['positionReceived', 
     1073                          'positionErrorReceived', 
     1074                          'altitudeReceived'] 
     1075 
     1076        self._receiverTest(sentences, callbacksFired) 
     1077 
     1078 
     1079    def test_RMCSentences(self): 
     1080        """ 
     1081        Tests that a sequence of RMC sentences fires C{positionReceived}, 
     1082        C{speedReceived}, C{headingReceived} and C{timeReceived}. 
     1083        """ 
     1084        sentences = [GPRMC] 
     1085        callbacksFired = ['headingReceived', 
     1086                          'speedReceived', 
     1087                          'positionReceived', 
     1088                          'timeReceived'] 
     1089 
     1090        self._receiverTest(sentences, callbacksFired) 
     1091 
     1092 
     1093    def test_GSVSentences(self): 
     1094        """ 
     1095        Verifies that a complete sequence of GSV sentences fires 
     1096        C{beaconInformationReceived}. 
     1097        """ 
     1098        sentences = [GPGSV_FIRST, GPGSV_MIDDLE, GPGSV_LAST] 
     1099        callbacksFired = ['beaconInformationReceived'] 
     1100 
     1101        def checkPartialInformation(): 
     1102            self.assertNotIn('_partialBeaconInformation', self.adapter._state) 
     1103 
     1104        self._receiverTest(sentences, callbacksFired, checkPartialInformation) 
     1105 
     1106 
     1107    def test_emptyMiddleEntriesGSVSequence(self): 
     1108        """ 
     1109        Verifies that a complete sequence of GSV sentences with empty entries 
     1110        in the middle still fires C{beaconInformationReceived}. 
     1111        """ 
     1112        sentences = [GPGSV_EMPTY_MIDDLE] 
     1113        self._receiverTest(sentences, ["beaconInformationReceived"]) 
     1114 
     1115 
     1116    def test_incompleteGSVSequence(self): 
     1117        """ 
     1118        Verifies that an incomplete sequence of GSV sentences does not fire. 
     1119        """ 
     1120        sentences = [GPGSV_FIRST] 
     1121        self._receiverTest(sentences) 
     1122 
     1123 
     1124    def test_singleSentenceGSVSequence(self): 
     1125        """ 
     1126        Verifies that the parser does not fail badly when the sequence consists 
     1127        of only one sentence (but is otherwise complete). 
     1128        """ 
     1129        sentences = [GPGSV_SINGLE] 
     1130        self._receiverTest(sentences, ["beaconInformationReceived"]) 
     1131 
     1132 
     1133    def test_GLLSentences(self): 
     1134        """ 
     1135        Verfies that GLL sentences fire C{positionReceived}. 
     1136        """ 
     1137        sentences = [GPGLL_PARTIAL, GPGLL] 
     1138        self._receiverTest(sentences,  ['positionReceived']) 
     1139 
     1140 
     1141    def test_HDTSentences(self): 
     1142        """ 
     1143        Verfies that HDT sentences fire C{headingReceived}. 
     1144        """ 
     1145        sentences = [GPHDT] 
     1146        self._receiverTest(sentences, ['headingReceived']) 
     1147 
     1148 
     1149    def test_mixedSentences(self): 
     1150        """ 
     1151        Verifies that a mix of sentences fires the correct callbacks. 
     1152        """ 
     1153        sentences = [GPRMC, GPGGA] 
     1154        callbacksFired = ['altitudeReceived', 
     1155                          'speedReceived', 
     1156                          'positionReceived', 
     1157                          'positionErrorReceived', 
     1158                          'timeReceived', 
     1159                          'headingReceived'] 
     1160 
     1161        def checkTime(): 
     1162            expectedDateTime = datetime.datetime(1994, 3, 23, 12, 35, 19) 
     1163            self.assertEquals(self.adapter._state['time'], expectedDateTime) 
     1164 
     1165        self._receiverTest(sentences, callbacksFired, checkTime) 
     1166 
     1167 
     1168    def test_lotsOfMixedSentences(self): 
     1169        """ 
     1170        Tests for an entire gamut of sentences. These are more than you'd 
     1171        expect from your average consumer GPS device. They have most of the 
     1172        important information, including beacon information and visibility. 
     1173        """ 
     1174        sentences = [GPGSA] + GPGSV_SEQ + [GPRMC, GPGGA, GPGLL] 
     1175 
     1176        callbacksFired = ['headingReceived', 
     1177                          'beaconInformationReceived', 
     1178                          'speedReceived', 
     1179                          'positionReceived', 
     1180                          'timeReceived', 
     1181                          'altitudeReceived', 
     1182                          'positionErrorReceived'] 
     1183 
     1184        self._receiverTest(sentences, callbacksFired) 
  • new file w/twisted/positioning/test/test_sentence.py

    diff --git c/twisted/positioning/test/test_sentence.py w/twisted/positioning/test/test_sentence.py
    new file mode 100644
    index 0000000..25d0474
    - +  
     1# Copyright (c) 2009-2011 Twisted Matrix Laboratories. 
     2# See LICENSE for details. 
     3""" 
     4Tests for positioning sentences. 
     5""" 
     6import itertools 
     7from zope.interface import classProvides 
     8 
     9from twisted.positioning import base, ipositioning 
     10from twisted.trial.unittest import TestCase 
     11 
     12 
     13sentinelValueOne = "someStringValue" 
     14sentinelValueTwo = "someOtherStringValue" 
     15 
     16 
     17 
     18class DummyProtocol(object): 
     19    """ 
     20    A simple, fake protocol. 
     21    """ 
     22    classProvides(ipositioning.IPositioningSentenceProducer) 
     23 
     24    @staticmethod 
     25    def getSentenceAttributes(): 
     26        return ["type", sentinelValueOne, sentinelValueTwo] 
     27 
     28 
     29 
     30class DummySentence(base.BaseSentence): 
     31    """ 
     32    A sentence for L{DummyProtocol}. 
     33    """ 
     34    ALLOWED_ATTRIBUTES = DummyProtocol.getSentenceAttributes() 
     35 
     36 
     37 
     38class MixinProtocol(base.PositioningSentenceProducerMixin): 
     39    """ 
     40    A simple, fake protocol that declaratively tells you the sentences 
     41    it produces using L{base.PositioningSentenceProducerMixin}. 
     42    """ 
     43    SENTENCE_CONTENTS = { 
     44        None: [ 
     45            sentinelValueOne, 
     46            sentinelValueTwo, 
     47            None # see MixinTests.test_noNoneInSentenceAttributes 
     48        ], 
     49    } 
     50 
     51 
     52 
     53class MixinSentence(base.BaseSentence): 
     54    """ 
     55    A sentence for L{MixinProtocol}. 
     56    """ 
     57    ALLOWED_ATTRIBUTES = MixinProtocol.getSentenceAttributes() 
     58 
     59 
     60 
     61class SentenceTestsMixin: 
     62    """ 
     63    Tests for positioning protocols and their respective sentences. 
     64    """ 
     65    def test_attributeAccess(self): 
     66        """ 
     67        Tests that accessing a sentence attribute gets the correct value, and 
     68        accessing an unset attribute (which is specified as being a valid 
     69        sentence attribute) gets C{None}. 
     70        """ 
     71        thisSentinel = object() 
     72        sentence = self.sentenceClass({sentinelValueOne: thisSentinel}) 
     73        self.assertEquals(getattr(sentence, sentinelValueOne), thisSentinel) 
     74        self.assertEquals(getattr(sentence, sentinelValueTwo), None) 
     75 
     76 
     77    def test_raiseOnMissingAttributeAccess(self): 
     78        """ 
     79        Tests that accessing a nonexistant attribute raises C{AttributeError}. 
     80        """ 
     81        sentence = self.sentenceClass({}) 
     82        self.assertRaises(AttributeError, getattr, sentence, "BOGUS") 
     83 
     84 
     85    def test_raiseOnBadAttributeAccess(self): 
     86        """ 
     87        Tests that accessing bogus attributes raises C{AttributeError}, *even* 
     88        when that attribute actually is in the sentence data. 
     89        """ 
     90        sentence = self.sentenceClass({"BOGUS": None}) 
     91        self.assertRaises(AttributeError, getattr, sentence, "BOGUS") 
     92 
     93 
     94    sentenceType = "tummies" 
     95    reprTemplate = "<%s (%s) {%s}>" 
     96 
     97 
     98    def _expectedRepr(self, sentenceType="unknown type", dataRepr=""): 
     99        """ 
     100        Builds the expected repr for a sentence. 
     101        """ 
     102        clsName = self.sentenceClass.__name__ 
     103        return self.reprTemplate % (clsName, sentenceType, dataRepr) 
     104 
     105 
     106    def test_unknownTypeRepr(self): 
     107        """ 
     108        Test the repr of an empty sentence of unknown type. 
     109        """ 
     110        sentence = self.sentenceClass({}) 
     111        expectedRepr = self._expectedRepr() 
     112        self.assertEqual(repr(sentence), expectedRepr) 
     113 
     114 
     115    def test_knownTypeRepr(self): 
     116        """ 
     117        Test the repr of an empty sentence of known type. 
     118        """ 
     119        sentence = self.sentenceClass({"type": self.sentenceType}) 
     120        expectedRepr = self._expectedRepr(self.sentenceType) 
     121        self.assertEqual(repr(sentence), expectedRepr) 
     122 
     123 
     124 
     125class DummyTests(TestCase, SentenceTestsMixin): 
     126    """ 
     127    Tests for protocol classes that implement the appropriate interface 
     128    (L{ipositioning.IPositioningSentenceProducer}) manually. 
     129    """ 
     130    def setUp(self): 
     131        self.protocol = DummyProtocol() 
     132        self.sentenceClass = DummySentence 
     133 
     134 
     135 
     136class MixinTests(TestCase, SentenceTestsMixin): 
     137    """ 
     138    Tests for protocols deriving from L{base.PositioningSentenceProducerMixin} 
     139    and their sentences. 
     140    """ 
     141    def setUp(self): 
     142        self.protocol = MixinProtocol() 
     143        self.sentenceClass = MixinSentence 
     144 
     145 
     146    def test_noNoneInSentenceAttributes(self): 
     147        """ 
     148        Tests that C{None} does not appear in the sentence attributes of the 
     149        protocol, even though it's in the specification. 
     150 
     151        This is because C{None} is a placeholder for parts of the sentence you 
     152        don't really need or want, but there are some bits later on in the 
     153        sentence that you do want. The alternative would be to have to specify 
     154        things like "_UNUSED0", "_UNUSED1"... which would end up cluttering 
     155        the sentence data and eventually adapter state. 
     156        """ 
     157        sentenceAttributes = self.protocol.getSentenceAttributes() 
     158        self.assertNotIn(None, sentenceAttributes) 
     159 
     160        sentenceContents = self.protocol.SENTENCE_CONTENTS 
     161        sentenceSpecAttributes = itertools.chain(*sentenceContents.values()) 
     162        self.assertIn(None, sentenceSpecAttributes)