Ticket #3926: positioning.diff

File positioning.diff, 75.2 KB (added by lvh, 5 years ago)
  • twisted/positioning/base.py

    === added directory 'twisted/positioning'
    === added file 'twisted/positioning/__init__.py'
    === added file 'twisted/positioning/base.py'
     
     1# -*- encoding: utf-8 -*- 
     2# -*- test-case-name: twisted.positioning.test.test_base -*- 
     3# Copyright (c) 2009 Twisted Matrix Laboratories. 
     4# See LICENSE for details. 
     5""" 
     6Generic positioning base classes. 
     7""" 
     8from zope.interface import implements 
     9from twisted.positioning import ipositioning 
     10 
     11MPS_PER_KNOT = 0.5144444444444444 
     12MPS_PER_KPH = 0.27777777777777777 
     13METERS_PER_FOOT = 0.3048 
     14 
     15 
     16class BasePositioningReceiver(object): 
     17    implements(ipositioning.IPositioningReceiver) 
     18    def headingReceived(self, heading): 
     19        """ 
     20        Implements C{IPositioningReceiver.headingReceived} stub. 
     21        """ 
     22 
     23 
     24    def speedReceived(self, speed): 
     25        """ 
     26        Implements C{IPositioningReceiver.speedReceived} stub. 
     27        """ 
     28 
     29 
     30    def climbReceived(self, climb): 
     31        """ 
     32        Implements C{IPositioningReceiver.climbReceived} stub. 
     33        """ 
     34 
     35 
     36    def positionReceived(self, latitude, longitude): 
     37        """ 
     38        Implements C{IPositioningReceiver.positionReceived} stub. 
     39        """ 
     40 
     41 
     42    def positioningErrorReceived(self, positioningError): 
     43        """ 
     44        Implements C{IPositioningReceiver.positioningErrorReceived} stub. 
     45        """ 
     46 
     47 
     48    def altitudeReceived(self, altitude): 
     49        """ 
     50        Implements C{IPositioningReceiver.altitudeReceived} stub. 
     51        """ 
     52 
     53         
     54    def beaconInformationReceived(self, beaconInformation): 
     55        """ 
     56        Implements C{IPositioningReceiver.beaconInformationReceived} stub. 
     57        """ 
     58 
     59 
     60 
     61class InvalidSentence(Exception): 
     62    """ 
     63    An exception raised when a sentence is invalid. 
     64    """ 
     65 
     66 
     67 
     68class InvalidChecksum(Exception): 
     69    """ 
     70    An exception raised when the checksum of a sentence is invalid. 
     71    """ 
     72 
     73 
     74class Heading(object): 
     75    """ 
     76    The heading of a mobile object. 
     77 
     78    @ivar heading: The heading of a mobile object. C{None} if unknown. 
     79    @type heading: C{float} or C{NoneType} 
     80    @ivar variation: The (optional) variation. 
     81        The sign of the variation is positive for variations towards the east 
     82        (clockwise from north), and negative for variations towards the west 
     83        (counterclockwise from north). 
     84        If the variation is unknown or not applicable, this is C{None}. 
     85    @type variation: C{float} or C{NoneType}. 
     86    """ 
     87    def __init__(self, heading=None, variation=None): 
     88        """ 
     89        Initializes a heading with an optional variation. 
     90        """ 
     91        self.heading = heading 
     92        self.variation = variation 
     93 
     94        if self.heading is not None and not 0 <= self.heading < 360: 
     95            raise ValueError("Bad heading (%s)" % (self.heading,)) 
     96        if self.variation is not None and not -180 < self.variation < 180: 
     97            raise ValueError("Bad variation (%s)" % (self.variation,)) 
     98 
     99         
     100    #@staticmethod 
     101    def _clipAngle(angle): 
     102        """ 
     103        TODO: document 
     104        """ 
     105        while angle <= 0: 
     106            angle += 360 
     107 
     108        while angle >= 360: 
     109            angle -= 360 
     110 
     111        return angle 
     112 
     113     
     114    _clipAngle = staticmethod(_clipAngle) 
     115     
     116 
     117    def _getCorrectedHeading(self): 
     118        """ 
     119        Corrects the heading by the given variation. This is sometimes known as 
     120        the true heading. 
     121 
     122        @return: The heading, corrected by the variation. If the variation is 
     123            unknown, returns None. 
     124        @rtype: C{float} or C{NoneType} 
     125        """ 
     126        if self.variation is None: 
     127            return None 
     128 
     129        return self._clipAngle(self.heading - self.variation) 
     130         
     131 
     132    correctedHeading=property(fget=_getCorrectedHeading, doc= 
     133        """ 
     134        Returns the heading, corrected for variation. 
     135 
     136        If the variation is None, returns None. 
     137 
     138        This calculates the raw heading minus the variation and then coerces 
     139        the value to something between 0 and 360. 
     140 
     141        @return The corrected heading. If it is unknown or not applicable to 
     142            this heading object, returns C{None}. 
     143        @rtype: C{float} or C{NoneType} 
     144        """) 
     145 
     146 
     147    def __float__(self): 
     148        """ 
     149        Returns this heading as a float. 
     150 
     151        @return: The float value of this heading. 
     152        @rtype: C{float} 
     153        """ 
     154        return self.heading 
     155 
     156 
     157    def __imul__(self, factor): 
     158        """ 
     159        Multiplies the variation in place. 
     160 
     161        Note that this multiplies the variation, not the actual heading 
     162        itself. This is because multiplying the angle generally has no meaning, 
     163        wheras multiplying the variation is commonly useful (for example, for 
     164        setting the sign of the variation). 
     165         
     166        @param factor: The factor to multiply with. 
     167        @type factor: C{float} 
     168 
     169        @return: The mutated C{self}. 
     170        @post: The variation will be equal to the current value, multiplied by 
     171            the parameter value. 
     172        """ 
     173        self.variation *= factor 
     174        return self 
     175 
     176     
     177    def __repr__(self): 
     178        """ 
     179        Returns a debugging representation of a heading object. 
     180        """ 
     181        return "<Heading (%s, var: %s)>" % (self.heading, self.variation) 
     182 
     183 
     184    def __eq__(self, other): 
     185        """ 
     186        Compares two heading objects for equality. 
     187        """ 
     188        return self.heading == other.heading and \ 
     189               self.variation == other.variation 
     190 
     191 
     192 
     193class Altitude(object): 
     194    """ 
     195    An altitude. 
     196 
     197    @ivar altitude: The altitude represented by this object, in meters. 
     198    @type altitude: C{float} 
     199 
     200    @ivar altitudeInFeet: As above, but expressed in feet. 
     201    @type altitudeInFeet: C{float} 
     202 
     203    The value in meters is the default. For ease of use, this value is used 
     204    when converting this object to a float: 
     205 
     206    >>> altitude = Altitude(100) # 100 meters 
     207    >>> float(altitude) 
     208    100.0 
     209    >>> "The altitude is %.1f meters." % (altitude,) 
     210    'The altitude is 100.0 meters.' 
     211    """ 
     212    def __init__(self, altitude): 
     213        """ 
     214        Initializes an altitude. 
     215 
     216        @param altitude: The altitude in meters. 
     217        @type altitude: C{float} 
     218        """ 
     219        self.altitude = float(altitude) 
     220 
     221 
     222    def _getAltitudeInFeet(self): 
     223        """ 
     224        Gets the altitude of this altitude in feet. 
     225        """ 
     226        return self.altitude / METERS_PER_FOOT 
     227 
     228 
     229    inFeet = property(fget = _getAltitudeInFeet, doc= 
     230        """ 
     231        Returns the altitude represented by this object expressed in feet. 
     232 
     233        @return: The altitude represented by this object expressed in feet. 
     234        @rtype: C{float} 
     235        """) 
     236 
     237 
     238    def __float__(self): 
     239        """ 
     240        Returns the altitude represented by this object expressed in meters. 
     241        """ 
     242        return self.altitude 
     243 
     244 
     245    def __eq__(self, other): 
     246        """ 
     247        Compares two altitudes for equality 
     248 
     249        @param other: The Altitude to compare to. 
     250        @return: C{True} if the other altitude is equal to this altitude, 
     251            C{False} otherwise. 
     252        @rtype: C{bool} 
     253        """ 
     254        return self.altitude == other.altitude 
     255 
     256 
     257    def __repr__(self): 
     258        """ 
     259        Returns a string representation of this Altitude object. 
     260        """ 
     261        return "<Altitude (%s m)>" % (self.altitude,) 
     262 
     263 
     264 
     265class Speed(object): 
     266    """ 
     267    An object representing the speed of a mobile object. 
     268 
     269    @ivar speed: The speed that this object represents, in meters per second. 
     270    @type speed: C{float} 
     271 
     272    @ivar speedInKnots: Same as above, but in knots. 
     273    @type speedInKnots: C{float} 
     274 
     275    When converted to a float, this object will represent the speed in meters 
     276    per second. 
     277    """ 
     278    def __init__(self, speed): 
     279        """ 
     280        Initializes a speed. 
     281 
     282        @param speed: The speed that this object represents, expressed in 
     283            meters per second. 
     284o        @type speed: C{float} 
     285 
     286        If the provided speed is smaller than zero, raises ValueError. 
     287 
     288        >>> Speed(-1.0) 
     289        Traceback (most recent call last): 
     290            ... 
     291        ValueError: negative speed: -1.0 
     292        """ 
     293        if speed < 0: 
     294            raise ValueError("negative speed: %r" % (speed,)) 
     295 
     296        self.speed = float(speed) 
     297 
     298 
     299    def _getSpeedInKnots(self): 
     300        return self.speed / MPS_PER_KNOT 
     301 
     302 
     303    speedInKnots = property(fget=_getSpeedInKnots, doc= 
     304        """ 
     305        Gets the speed represented by this object, expressed in knots. 
     306        """) 
     307 
     308 
     309    def __eq__(self, other): 
     310        """ 
     311        Compares two speeds for equality. 
     312        """ 
     313        return self.speed == other.speed 
     314 
     315 
     316    def __float__(self): 
     317        """ 
     318        Returns speed that this object represents, in meters per second. 
     319        """ 
     320        return self.speed 
     321 
     322 
     323    def __repr__(self): 
     324        """ 
     325        Returns a string representation of this Speed object. 
     326        """ 
     327        return "<Speed (%s m/s)>" % round(self.speed, 2) 
     328 
     329 
     330 
     331class Coordinate(object): 
     332    """ 
     333    A coordinate. 
     334 
     335    @ivar angle: The value of the coordinate in decimal degrees, with the usual 
     336        rules for sign (northern and eastern hemispheres are positive, southern 
     337        and western hemispheres are negative). 
     338    @type angle: C{float} 
     339 
     340    @ivar type: An optional description of the type of coordinate 
     341        this object. Should be one of ("latitude", "longitude"). 
     342    @type type: C{str} 
     343    """ 
     344    def __init__(self, angle, type=None): 
     345        """ 
     346        Initializes a coordinate. 
     347 
     348        @param angle: The angle of this coordinate in decimal degrees. The 
     349            hemisphere is determined by the sign (north and east are positive). 
     350        @type angle: C{float} 
     351 
     352        @param type: One of ("latitude", "longitude"). Used to return 
     353            hemisphere names. 
     354        @type type: C{str} 
     355        """ 
     356        self.angle = angle 
     357        self.type = type 
     358 
     359 
     360    def _getDMS(self): 
     361        """ 
     362        Gets the angle of this coordinate as a degrees, minutes, seconds tuple. 
     363        """ 
     364        degrees = abs(int(self.angle)) 
     365        fractionalDegrees = abs(self.angle - int(self.angle)) 
     366        decimalMinutes = 60 * fractionalDegrees 
     367 
     368        minutes = int(decimalMinutes) 
     369        fractionalMinutes = decimalMinutes - int(decimalMinutes) 
     370        decimalSeconds = 60 * fractionalMinutes 
     371 
     372        return degrees, minutes, int(round(decimalSeconds)) 
     373 
     374 
     375    inDegreesMinutesSeconds = property(fget=_getDMS, doc= 
     376        """ 
     377        Returns the angle of this coordinate in degrees, minutes, seconds. 
     378        """) 
     379 
     380 
     381    def _getHemisphere(self): 
     382        """ 
     383        Gets the hemisphere of this coordinate (one of ('N', 'E', 'S', 'W')). 
     384        """ 
     385        # REVIEW: switch inner and outer jmps, then use lookup dict? :-) 
     386        # (Or something evil like abusing indexing with a boolean (True == 1)) 
     387        if self.type == "latitude": 
     388            if self.angle > 0: 
     389                return "N" 
     390            else: 
     391                return "S" 
     392        elif self.type == "longitude": 
     393            if self.angle > 0: 
     394                return 'E' 
     395            else: 
     396                return "W" 
     397        else: 
     398            raise ValueError("Unknown coordinate type %s" % (self.type,)) 
     399 
     400 
     401    hemisphere = property(fget=_getHemisphere, doc= 
     402        """ 
     403        The hemisphere of this coordinate (one of ('N', 'E', 'S', 'W')). 
     404 
     405        The hemisphere is determined from the coordinate type (latitude or 
     406        longitude) and signage of the angle in decimal degrees. 
     407 
     408        >>> Coordinate(1.0, "latitude").hemisphere 
     409        'N' 
     410        >>> Coordinate(-1.0, "latitude").hemisphere 
     411        'S' 
     412        >>> Coordinate(1.0, "longitude").hemisphere 
     413        'E' 
     414        >>> Coordinate(-1.0, "longitude").hemisphere 
     415        'W' 
     416        """) 
     417 
     418 
     419    def setSign(self, sign): 
     420        """ 
     421        Sets the sign of this coordinate. 
     422 
     423        @param sign: The sign to set, 1 for positive, -1 for negative signs. 
     424        @type sign: C{int} 
     425        """ 
     426        if sign not in (-1, 1): 
     427            raise ValueError("bad sign (got %s, expected -1 or 1)" % sign) 
     428 
     429        self.angle = sign * abs(self.angle) 
     430 
     431 
     432    def __eq__(self, other): 
     433        """ 
     434        Compares two coordinates for equality. 
     435        """ 
     436        return self.type == other.type \ 
     437            and self.angle == other.angle 
     438 
     439 
     440    def __imul__(self, factor): 
     441        """ 
     442        Multiplies this coordinate in place. 
     443 
     444        This is particularly useful for setting the sign of a coordinate 
     445        (multiplying by -1 or 1). 
     446         
     447        @param factor: The factor to multiply with. 
     448        @type factor: numeric 
     449 
     450        @return: The mutated C{self}. 
     451        @post: The coordinate will be equal to the current value, multiplied by 
     452            the parameter value. 
     453        """ 
     454        self.angle *= factor 
     455        return self 
     456 
     457     
     458    def __repr__(self): 
     459        """ 
     460        Returns a string representation of this coordinate. 
     461 
     462        @return: The string representation. 
     463        @rtype: C{str} 
     464        """ 
     465        if self.type in ('latitude', 'longitude'): 
     466            coordinateType = self.type.title() 
     467        else: 
     468            coordinateType = "Coordinate of unknown type %s" % self.type 
     469 
     470        return "<%s (%s degrees)>" % (coordinateType, round(self.angle, 2),) 
     471 
     472 
     473 
     474class PositioningError(object): 
     475    """ 
     476    Positioning error information. 
     477    """ 
     478 
     479    ALLOWABLE_TRESHOLD = 0.01 
     480    def __init__(self, pdop=None, hdop=None, vdop=None, testInvariant=False): 
     481        """ 
     482        Initializes a positioning error object. 
     483 
     484        @param pdop: The position dilution of precision. 
     485        @type pdop: C{float} 
     486        @param hdop: The horizontal dilution of precision. 
     487        @type hdop: C{float} 
     488        @param vdop: The vertical dilution of precision. 
     489        @type vdop: C{float} 
     490        @param testInvariant: Flag to test if the DOP invariant is valid or 
     491            not. If C{True}, the invariant (PDOP = (HDOP**2 + VDOP**2)*.5) is 
     492            checked at every mutation. By default, this is false, because the 
     493            vast majority of DOP-providing devices ignore this invariant. 
     494        @type testInvariant: c{bool} 
     495        """ 
     496        self._pdop = pdop 
     497        self._hdop = hdop 
     498        self._vdop = vdop 
     499 
     500        self._testInvariant = testInvariant 
     501        self._testDilutionOfPositionInvariant() 
     502 
     503 
     504    def _testDilutionOfPositionInvariant(self): 
     505        """ 
     506        Tests if this positioning error object satisfies the dilution of 
     507        position invariant (PDOP = (HDOP**2 + VDOP**2)*.5), unless the 
     508        C{self._testInvariant} instance variable is C{False}. 
     509 
     510        @return: C{None} if the invariant was not satisifed or not tested. 
     511        @raises: C{ValueError} if the invariant was not satisfied. 
     512        """ 
     513        if not self._testInvariant: 
     514            return 
     515 
     516        for x in (self.pdop, self.hdop, self.vdop): 
     517            if x is None: 
     518                return 
     519 
     520        delta = abs(self.pdop - (self.hdop**2 + self.vdop**2)**.5) 
     521        if delta > self.ALLOWABLE_TRESHOLD: 
     522            raise ValueError("invalid combination of dilutions of precision: " 
     523                             "position: %s, horizontal: %s, vertical: %s" 
     524                             % (self.pdop, self.hdop, self.vdop)) 
     525 
     526 
     527    DOP_EXPRESSIONS = { 
     528        '_pdop': lambda self: (self._hdop**2 + self._vdop**2)**.5, 
     529        '_hdop': lambda self: (self._pdop**2 - self._vdop**2)**.5, 
     530        '_vdop': lambda self: (self._pdop**2 - self._hdop**2)**.5, 
     531    } 
     532 
     533 
     534    def _getDOP(self, dopType): 
     535        """ 
     536        Gets a particular dilution of position value 
     537        """ 
     538        attributeName = "_" + dopType 
     539         
     540        if getattr(self, attributeName) is not None: 
     541            # known 
     542            return getattr(self, attributeName) 
     543 
     544 
     545        # REVIEW: perhaps we should replace this with a simple try/except? 
     546        others = (dop for dop in self.DOP_EXPRESSIONS if dop != attributeName) 
     547        for dop in others: 
     548            if getattr(self, dop) is None: 
     549                # At least one other DOP is None, can't calculate the last one. 
     550                return None 
     551 
     552        # !known && calculable 
     553        return self.DOP_EXPRESSIONS[attributeName](self) 
     554 
     555 
     556    def _setDOP(self, dopType, value): 
     557        """ 
     558        Sets a particular dilution of position value. 
     559 
     560        @param dopType: The type of dilution of position to set. One of 
     561            ('pdop', 'hdop', 'vdop'). 
     562        @type dopType: C{str} 
     563 
     564        @param value: The value to set the dilution of position type to. 
     565        @type value: C{float} 
     566        """ 
     567        attributeName = "_" + dopType 
     568        setattr(self, attributeName, float(value)) 
     569        self._testDilutionOfPositionInvariant() 
     570 
     571 
     572    pdop = property( 
     573        fget=lambda self: self._getDOP('pdop'), 
     574        fset=lambda self, value: self._setDOP('pdop', value), 
     575        doc=""" 
     576        Returns (or calculates, if not directly available) the position 
     577        dilution of precision. 
     578 
     579        @return: The position dilution of precision. 
     580        @rtype: C{float} or C{NoneType} if unknown 
     581        """) 
     582 
     583 
     584    hdop = property( 
     585        fget=lambda self: self._getDOP('hdop'), 
     586        fset=lambda self, value: self._setDOP('hdop', value), 
     587        doc=""" 
     588        Returns (or calculates, if not directly available) the horizontal 
     589        dilution of precision. 
     590 
     591        @return: The horizontal dilution of precision. 
     592        @rtype: C{float} or C{NoneType} if unknown 
     593        """) 
     594 
     595 
     596    vdop = property( 
     597        fget=lambda self: self._getDOP('vdop'), 
     598        fset=lambda self, value: self._setDOP('vdop', value), 
     599        doc=""" 
     600        Returns (or calculates, if not directly available) the vertical 
     601        dilution of precision. 
     602 
     603        @return: The vertical dilution of precision. 
     604        @rtype: C{float} or C{NoneType} if unknown 
     605        """) 
     606 
     607    def __eq__(self, other): 
     608        """ 
     609        Compares two PositionErrors for equality. 
     610 
     611        @return C{True} if the two positioning errors are equal (all the 
     612           relevant attributes are equal), C{False} otherwise. 
     613        @rtype: C{bool} 
     614        """ 
     615        return self.pdop == other.pdop \ 
     616            and self.hdop == other.hdop \ 
     617            and self.vdop == other.vdop 
     618 
     619 
     620    def __repr__(self): 
     621        """ 
     622        Returns a debugging representation of a positioning error. 
     623        """ 
     624        return "<PositioningError (pdop: %s, hdop: %s, vdop: %s)>" \ 
     625            % (self.pdop, self.hdop, self.vdop) 
     626 
     627 
     628 
     629class BeaconInformation(object): 
     630    """ 
     631    Information about positioning beacons (a generalized term for the reference 
     632    objects that help you determine your position, such as satellites or cell 
     633    towers). 
     634 
     635    @ivar beacons: A set of visible beacons. Note that visible beacons are not 
     636        necessarily used in acquiring a postioning fix. 
     637    @type beacons: C{set} of C{IPositioningBeacon} 
     638 
     639    TODO: document seen/used ivars 
     640     
     641    This object may be iterated to yield these beacons. 
     642    """ 
     643    def __init__(self, seen=0, used=0, beacons=None): 
     644        """ 
     645        Initializes a beacon information object. 
     646 
     647        @ivar beacons: A collection of beacons in this beacon information 
     648            object. 
     649        @type beacons: iterable 
     650        """ 
     651        self.seen = int(seen) 
     652        self.used = int(used) 
     653        self.beacons = set(beacons or []) 
     654 
     655 
     656    def _getUsedBeacons(self): 
     657        return (x for x in self.beacons.value if x.isUsed) 
     658 
     659 
     660    usedBeacons = property(fget=_getUsedBeacons, doc= 
     661        """ 
     662        Yields the used beacons in this BeaconInformation object. 
     663 
     664        This is different from BeaconInformation.__iter__ because it only 
     665        yields beacons that are actually used in obtaining the fix. 
     666        """) 
     667 
     668 
     669    def __iter__(self): 
     670        """ 
     671        Yields the beacons in this beacon information object. 
     672        """ 
     673        for beacon in self.beacons: 
     674            yield beacon 
     675 
     676 
     677    def __repr__(self): 
     678        """ 
     679        Returns a string representation of this beacon information object. 
     680 
     681        @return: The string representation. 
     682        @rtype: C{str} 
     683        """ 
     684        beaconReprs = "".join(repr(beacon) for beacon in self.beacons) 
     685        return "<BeaconInformation {%s}>" % beaconReprs 
     686 
     687 
     688 
     689class PositioningBeacon(object): 
     690    """ 
     691    A positioning beacon. 
     692 
     693    @ivar identifier: The unqiue identifier for this satellite. This is usually 
     694        an integer. For GPS, this is also known as the PRN. 
     695    @type identifier: Pretty much anything that can be used as a unique 
     696        identifier. Depends on the implementation. 
     697    @ivar isUsed: True if the satellite is currently being used to acchieve a 
     698        fix, False if it is not currently being used, None if unknown. 
     699    @type isUsed: c{bool} or C{None} 
     700    """ 
     701    def __init__(self, identifier, isUsed=None): 
     702        self.identifier = identifier 
     703        self.isUsed = isUsed 
     704 
     705 
     706    def __hash__(self): 
     707        """ 
     708        Returns the identifier for this beacon. 
     709        """ 
     710        return self.identifier 
     711 
     712 
     713    def _usedRepr(self): 
     714        """ 
     715        Returns a single character representation of the status of this 
     716        satellite in terms of being used for attaining a positioning fix. 
     717 
     718        @return: One of ("Y", "N", "?") depending on the status of the 
     719            satellite. 
     720        @rval: C{str} 
     721        """ 
     722        return {True: "Y", False: "N", None: "?"}[self.isUsed]         
     723     
     724 
     725    def __repr__(self): 
     726        """ 
     727        Returns a string representation of this beacon. 
     728 
     729        @return: The string representation. 
     730        @rtype: C{str} 
     731        """ 
     732        return "<Beacon (identifier: %s, used: %s)>" \ 
     733            % (self.identifier, self._usedRepr()) 
     734 
     735 
     736class Satellite(PositioningBeacon): 
     737    """ 
     738    A satellite. 
     739 
     740    @ivar azimuth: The azimuth of the satellite. This is the heading (positive 
     741        angle relative to true north) where the satellite appears to be to the 
     742        device. 
     743    @ivar elevation: The (positive) angle above the horizon where this 
     744        satellite appears to be to the device. 
     745    @ivar snr: The signal to noise ratio of the signal coming from this 
     746        satellite. 
     747    """ 
     748    def __init__(self, identifier, azimuth, elevation, snr, isUsed=None): 
     749        """ 
     750        Initializes a satellite object. 
     751 
     752        @param azimuth: The azimuth of the satellite (see instance variable 
     753            documentation). 
     754        @type azimuth: C{float} 
     755        @param elevation: The elevation of the satellite (see instance variable 
     756            documentation). 
     757        @type elevation: C{float} 
     758        @param snr: The signal to noise ratio of the connection to this 
     759            satellite (see instance variable documentation). 
     760        @type snr: C{float} 
     761 
     762        """ 
     763        super(Satellite, self).__init__(int(identifier), isUsed) 
     764 
     765        # TODO: remove these float casts, make default arguments None 
     766        self.azimuth = float(azimuth) 
     767        self.elevation = float(elevation) 
     768        self.signalToNoiseRatio = float(snr) 
     769 
     770 
     771    def __repr__(self): 
     772        """ 
     773        Returns a string representation of this Satellite. 
     774 
     775        @return: The string representation. 
     776        @rtype: C{str} 
     777        """ 
     778        return "<Satellite (%s), azimuth: %s, elevation: %s, used: %s>" \ 
     779            % (self.identifier, self.azimuth, self.elevation, self._usedRepr()) 
  • twisted/positioning/ipositioning.py

    === added file 'twisted/positioning/ipositioning.py'
     
     1# -*- encoding: utf-8 -*- 
     2# Copyright (c) 2009 Twisted Matrix Laboratories. 
     3# See LICENSE for details. 
     4""" 
     5Positioning interface. 
     6""" 
     7from zope.interface import Interface 
     8 
     9 
     10class IPositioningReceiver(Interface): 
     11    """ 
     12    An interface for positioning providers. 
     13    """ 
     14    def positionReceived(latitude, longitude): 
     15        """ 
     16        Method called when a position is received. 
     17 
     18        @param latitude: The latitude of the received position. 
     19        @type latitude: C{Coordinate} 
     20        @param longitude: The longitude of the received position. 
     21        @type longitude: C{Coordinate} 
     22        """ 
     23 
     24 
     25    def positioningErrorReceived(positioningError): 
     26        """ 
     27        Method called when positioning error is received. 
     28 
     29        @param positioningError: The positioning error. 
     30        TODO: Create and document type. 
     31        """ 
     32 
     33    def timeReceived(time, date): 
     34        """ 
     35        Method called when time and date information arrives. 
     36 
     37        @param time: The time (in UTC unless otherwise specified). 
     38        @type time: C{datetime.time} 
     39        @param date: The date. 
     40        @type date: C{datetime.date} 
     41        """ 
     42 
     43    def headingReceived(heading): 
     44        """ 
     45        Method called when a true heading is received. 
     46 
     47        @param heading: The heading. 
     48        @type heading: C{Heading} 
     49        """ 
     50 
     51 
     52    def altitudeReceived(altitude): 
     53        """ 
     54        Method called when an altitude is received. 
     55 
     56        @param altitude: The altitude. 
     57        @type altitude: C{twisted.positioning.base.Altitude} 
     58        """ 
     59 
     60 
     61    def speedReceived(speed): 
     62        """ 
     63        Method called when the speed is received. 
     64 
     65        @param speed: The speed of a mobile object. 
     66        @type speed: C{twisted.positioning.base.Speed} 
     67        """ 
     68 
     69 
     70    def climbReceived(climb): 
     71        """ 
     72        Method called when the climb is received. 
     73 
     74        @param climb: The climb of the mobile object. 
     75        TODO: Create and document type. 
     76        """ 
     77 
     78    def beaconInformationReceived(beaconInformation): 
     79        """ 
     80        Method called when positioning beacon information is received. 
     81 
     82        @param beaconInformation: The beacon information. 
     83        @type C{twisted.positioning.base.BeaconInformation} 
     84        """ 
     85 
     86 
     87class INMEAReceiver(Interface): 
     88    """ 
     89    An object that can receive NMEA data. 
     90    """ 
     91    def sentenceReceived(sentence): 
     92        """ 
     93        Method called when a sentence is received. 
     94 
     95        @param sentence: The received NMEA sentence. 
     96        @type C{twisted.positioning.nmea.NMEASentence} 
     97        """ 
  • twisted/positioning/nmea.py

    === added file 'twisted/positioning/nmea.py'
     
     1# -*- encoding: utf-8 -*- 
     2# -*- test-case-name: twisted.positioning.test.test_nmea -*- 
     3# Copyright (c) 2009 Twisted Matrix Laboratories. 
     4# See LICENSE for details. 
     5""" 
     6Classes for using NMEAProtocol sentences. 
     7""" 
     8import itertools 
     9import operator 
     10import datetime 
     11from zope.interface import implements 
     12 
     13import twisted.protocols.basic 
     14from twisted.positioning import base, ipositioning 
     15 
     16 
     17class NMEAProtocol(twisted.protocols.basic.LineReceiver): 
     18    """ 
     19    A protocol that parses and verifies the checksum of an NMEAProtocol sentence, and 
     20    delegates to a receiver. 
     21 
     22    Responsibilities: 
     23        - receiving lines (which are hopefully sentences) 
     24        - verifying their checksum 
     25        - unpacking them (mapping of sentence element keys to their values) 
     26        - creating C{NMEASentence} objects 
     27        - passing them to the receiver. 
     28    """ 
     29    def __init__(self, receiver): 
     30        """ 
     31        Initializes an receiver for NMEAProtocol sentences. 
     32 
     33        @param receiver: A receiver for NMEAProtocol sentence objects. 
     34        @type receiver: L{INMEAReceiver} 
     35        """ 
     36        self.receiver = receiver 
     37 
     38 
     39    METHOD_PREFIX = "nmea_" 
     40 
     41 
     42    def lineReceived(self, rawSentence): 
     43        """ 
     44        Parses the data from the sentence and validates the checksum. 
     45 
     46        @param rawSentence: The raw positioning sentence. 
     47        @type rawSentence: C{str} (bytestring encoded in ascii) 
     48        """ 
     49        sentence = rawSentence.strip() 
     50 
     51        self.validateChecksum(sentence) 
     52        splitSentence = self.splitSentence(sentence) 
     53 
     54        sentenceType, contents = splitSentence[0], splitSentence[1:] 
     55 
     56        keys = self.SENTENCE_CONTENTS.get(sentenceType, None) 
     57        callback = getattr(self, self.METHOD_PREFIX + sentenceType, None) 
     58 
     59        if keys is None or callback is None: 
     60            raise ValueError("unknown sentence type %s" % sentenceType) 
     61 
     62        sentenceData = {"type": sentenceType} 
     63        for key, value in itertools.izip(keys, contents): 
     64            if key is not None and value != "": 
     65                sentenceData[key] = value 
     66 
     67        sentence = NMEASentence(sentenceData) 
     68 
     69        callback(sentence) 
     70 
     71        if self.receiver is not None: 
     72            self.receiver.sentenceReceived(sentence) 
     73 
     74 
     75    #@staticmethod 
     76    def validateChecksum(sentence): 
     77        """ 
     78        Validates the checksum of an NMEAProtocol sentence. 
     79 
     80        Does nothing (except implicitly return None, of course) on sentences 
     81        with valid checksums. 
     82 
     83        >>> NMEASentence.validateChecksum( 
     84        ... '$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47' 
     85        ... ) 
     86 
     87        Same thing on sentences missing a checksum: 
     88 
     89        >>> NMEASentence.validateChecksum( 
     90        ... '$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*') 
     91 
     92        Will raise an exception on sentences with a missing checksum: 
     93 
     94        >>> NMEASentence.validateChecksum( 
     95        ... '$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*46' 
     96        ... ) 
     97        Traceback (most recent call last): 
     98            ... 
     99        InvalidChecksum: 71 != 70 
     100        """ 
     101        if sentence[-3] == '*': # sentence has a checksum 
     102            reference, source = int(sentence[-2:], 16), sentence[1:-3] 
     103            computed = reduce(operator.xor, (ord(x) for x in source)) 
     104            if computed != reference: 
     105                raise base.InvalidChecksum("%02x != %02x" 
     106                                           % (computed, reference)) 
     107 
     108             
     109    validateChecksum = staticmethod(validateChecksum) 
     110 
     111 
     112    #@staticmethod 
     113    def splitSentence(sentence): 
     114        """ 
     115        Returns the split version of the sentence, minus header and checksum. 
     116 
     117        >>> NMEASentence.splitSentence("$GPGGA,spam,eggs*00") 
     118        ['GPGGA', 'spam', 'eggs'] 
     119        """ 
     120        if sentence[-3] == "*": # sentence with checksum 
     121            return sentence[1:-3].split(',') 
     122        elif sentence[-1] == "*": # sentence without checksum 
     123            return sentence[1:-1].split(',') 
     124        else: 
     125            raise base.InvalidSentence("malformed sentence %s" % sentence) 
     126 
     127 
     128    splitSentence = staticmethod(splitSentence) 
     129 
     130 
     131    def nmea_GPGGA(self, sentence): 
     132        """ 
     133        Callback called when a GGA sentence is received. 
     134        """ 
     135 
     136 
     137    def nmea_GPRMC(self, sentence): 
     138        """ 
     139        Callback called when an RMC sentence is received. 
     140        """ 
     141 
     142 
     143    def nmea_GPGSV(self, sentence): 
     144        """ 
     145        Callback called when a GSV sentence is received. 
     146        """ 
     147 
     148 
     149    def nmea_GPGLL(self, sentence): 
     150        """ 
     151        Callback called when a GGL sentence is received. 
     152        """ 
     153 
     154 
     155    def nmea_GPHDT(self, sentence): 
     156        """ 
     157        Callback called when an HDT sentence is received. 
     158        """ 
     159 
     160 
     161    def nmea_GPGSA(self, sentence): 
     162        """ 
     163        Callback called when a GSA sentence is received. 
     164        """ 
     165 
     166 
     167 
     168    SENTENCE_CONTENTS = { 
     169        'GPGGA': [ 
     170            'timestamp', 
     171 
     172            'latitudeFloat', 
     173            'latitudeHemisphere', 
     174            'longitudeFloat', 
     175            'longitudeHemisphere', 
     176 
     177            'validGGA', 
     178            'numberOfSatellitesSeen', 
     179            'horizontalDilutionOfPrecision', 
     180 
     181            'altitude', 
     182            'altitudeUnits', 
     183            'heightOfGeoidAboveWGS84', 
     184            'heightOfGeoidAboveWGS84Units', 
     185 
     186            # TODO: DGPS information 
     187        ], 
     188 
     189        'GPRMC': [ 
     190            'timestamp', 
     191 
     192            'validRMC', 
     193 
     194            'latitudeFloat', 
     195            'latitudeHemisphere', 
     196            'longitudeFloat', 
     197            'longitudeHemisphere', 
     198 
     199            'speedInKnots', 
     200 
     201            'trueHeading', 
     202 
     203            'datestamp', 
     204 
     205            'magneticVariation', 
     206            'magneticVariationDirection', 
     207        ], 
     208 
     209        'GPGSV': [ 
     210            'numberOfGSVSentences', 
     211            'GSVSentenceIndex', 
     212 
     213            'numberOfSatellitesSeen', 
     214 
     215            'satellitePRN_0', 
     216            'elevation_0', 
     217            'azimuth_0', 
     218            'signalToNoiseRatio_0', 
     219 
     220            'satellitePRN_1', 
     221            'elevation_1', 
     222            'azimuth_1', 
     223            'signalToNoiseRatio_1', 
     224 
     225            'satellitePRN_2', 
     226            'elevation_2', 
     227            'azimuth_2', 
     228            'signalToNoiseRatio_2', 
     229 
     230            'satellitePRN_3', 
     231            'elevation_3', 
     232            'azimuth_3', 
     233            'signalToNoiseRatio_3', 
     234        ], 
     235 
     236        'GPGLL': [ 
     237            'latitudeFloat', 
     238            'latitudeHemisphere', 
     239            'longitudeFloat', 
     240            'longitudeHemisphere', 
     241            'timestamp', 
     242            'validGLL', 
     243        ], 
     244 
     245        'GPHDT': [ 
     246            'trueHeading', 
     247        ], 
     248 
     249        'GPTRF': [ 
     250            'datestamp', 
     251            'timestamp', 
     252 
     253            'latitudeFloat', 
     254            'latitudeHemisphere', 
     255            'longitudeFloat', 
     256            'longitudeHemisphere', 
     257 
     258            # TODO: actually use these: 
     259            'elevation', 
     260            'numberOfIterations', 
     261            'numberOfDopplerIntervals', 
     262            'updateDistanceInNauticalMiles', 
     263            'satellitePRN', 
     264        ], 
     265 
     266        'GPGSA': [ 
     267            None, # like GPRMCMode 
     268            None, # like GPGGAFixQuality 
     269 
     270            'usedSatellitePRN_0', 
     271            'usedSatellitePRN_1', 
     272            'usedSatellitePRN_2', 
     273            'usedSatellitePRN_3', 
     274            'usedSatellitePRN_4', 
     275            'usedSatellitePRN_5', 
     276            'usedSatellitePRN_6', 
     277            'usedSatellitePRN_7', 
     278            'usedSatellitePRN_8', 
     279            'usedSatellitePRN_9', 
     280            'usedSatellitePRN_10', 
     281            'usedSatellitePRN_11', 
     282 
     283            'positionDilutionOfPrecision', 
     284            'horizontalDilutionOfPrecision', 
     285            'verticalDilutionOfPrecision', 
     286        ] 
     287    } 
     288 
     289 
     290 
     291class NMEASentence(object): 
     292    """ 
     293    An object representing an NMEAProtocol sentence. 
     294 
     295    The attributes of this objects are raw NMEAProtocol representations, which 
     296    are bytestrings encoded in ASCII. 
     297 
     298    @ivar type: The sentence type ("GPGGA", "GPGSV"...). 
     299 
     300    This object contains the raw NMEAProtocol representations in a sentence. 
     301    Not all of these necessarily have to be present in the sentence. Missing 
     302    attributes are None when accessed. 
     303 
     304    @ivar timestamp: An NMEAProtocol timestamp. ("123456" -> 12:34:56Z) 
     305 
     306    @ivar latitudeFloat: The NMEAProtocol angular representation of a latitude 
     307        (for example: "1234.567" -> 12 degrees, 34.567 minutes). 
     308    @ivar latitudeHemisphere: The NMEAProtocol representation of a latitudinal 
     309        hemisphere ("N" or "S"). 
     310    @ivar longitudeFloat: The NMEAProtocol angular representation of a 
     311        longitude. See C{latitudeFloat} for an example. 
     312    @ivar longitudeHemisphere: The NMEAProtocol representation of a 
     313        longitudinal hemisphere ("E" or "W"). 
     314 
     315    TODO: finish documenting these attributes 
     316 
     317    """ 
     318    def __init__(self, sentenceData): 
     319        """ 
     320        Initializes an NMEAProtocol sentence from parsed sentence data. 
     321        """ 
     322        super(NMEASentence, self).__init__() 
     323        self._sentenceData = sentenceData 
     324 
     325    ## REVIEW: would properties like this be okay? Does that mean we can remove 
     326    ## the ivar docs in the class docstring? Would a bunch of getters, like: 
     327    ## 
     328    ##     def _getLatitudeFloat(self): 
     329    ##         return self._sentenceData['latitudeFloat'] 
     330    ## 
     331    ## and then property(fget=_getLatitudeFloat) be preferable? That seems like 
     332    ## a lot of repetitive code. 
     333    latitudeFloat = property( 
     334        fget=lambda self: self._sentenceData['latitudeFloat'], 
     335        doc=""" 
     336        The NMEAProtocol angular representation of a latitude. 
     337 
     338        For example: "1234.567" -> 12 degrees, 34.567 minutes. 
     339        """) 
     340 
     341    def __getattr__(self, name): 
     342        """ 
     343        Gets an attribute from the internal sentence dictionary. 
     344        """ 
     345        # TODO: remove 
     346        return self._sentenceData.get(name, None) 
     347 
     348 
     349    def _isFirstGSVSentence(self): 
     350        """ 
     351        Tests if this current GSV sentence is the first one in a sequence. 
     352        """ 
     353        return self.GSVSentenceIndex == "1" 
     354 
     355 
     356    def _isLastGSVSentence(self): 
     357        """ 
     358        Tests if this current GSV sentence is the final one in a sequence. 
     359        """ 
     360        return self.GSVSentenceIndex == self.numberOfGSVSentences 
     361 
     362 
     363    keys = property(fget=lambda self: iter(self._sentenceData), doc= 
     364        """ 
     365        Returns an iterator that iterates over the names of attributes present 
     366        in this sentence. 
     367        """) 
     368 
     369 
     370    def __repr__(self): 
     371        """ 
     372        Returns a textual representation of this NMEA sentence. 
     373 
     374        Note that this object represents a sentence that has already been 
     375        parsed -- this method does not return the raw serialized NMEA sentence. 
     376        """ 
     377        return "<NMEASentence {%s}>" % (repr(self._sentenceData),) 
     378 
     379     
     380 
     381MODE_AUTO, MODE_MANUAL = 'A', 'M' 
     382RMC_INVALID, RMC_VALID = 'V', 'A' 
     383GLL_INVALID, GLL_VALID = 'V', 'A' 
     384GGA_INVALID, GGA_GPS_FIX, GGA_DGPS_FIX = 1, 2, 3 
     385 
     386 
     387 
     388class NMEAAdapter(object): 
     389    """ 
     390    An adapter from NMEAProtocol receivers to positioning receivers. 
     391 
     392    @cvar DATESTAMP_HANDLING: Way to handle dates. One of (C{'intelligent'} 
     393        (default, if the last two digits are greater than the intelligent data 
     394        threshold,  assumes twentieth century, otherwise assumes twenty-first 
     395        century.), C{'19xx'} (assumes dates start with '19'), or C{'20xx'} 
     396        (assumes dates start with '20'). 
     397    @type DATESTAMP_HANDLING: C{str} 
     398 
     399    @cvar INTELLIGENT_DATE_THRESHOLD: The threshold that determines which 
     400        century we guess a year is in. If the year value in a sentence is above 
     401        this value, assumes the 20th century (19xx), otherwise assumes the 
     402        twenty-first century (20xx). 
     403    @type INTELLIGENT_DATE_THRESHOLD: C{int} 
     404    """ 
     405    implements(ipositioning.INMEAReceiver) 
     406 
     407     
     408    def __init__(self, positioningReceiver): 
     409        """ 
     410        Initializes a new NMEA adapter. 
     411 
     412        @param positioningReceiver: The receiver for positioning sentences. 
     413        @type positioningReceiver: C{ipositioning.IPositioningReceiver} 
     414        """ 
     415        self._state = {} 
     416        self._sentenceData = {} 
     417        self._receiver = positioningReceiver 
     418 
     419 
     420    def _fixTimestamp(self): 
     421        """ 
     422        Turns the NMEAProtocol timestamp notation into a datetime.time object. 
     423        The time in this object is expressed as Zulu time. 
     424        """ 
     425        timestamp = self.currentSentence.timestamp.split('.')[0] 
     426        timeObject = datetime.datetime.strptime(timestamp, '%H%M%S').time() 
     427        self._sentenceData['time'] = timeObject 
     428 
     429 
     430    DATESTAMP_HANDLING = 'intelligent' 
     431    INTELLIGENT_DATE_THRESHOLD = 80 
     432 
     433     
     434    def _fixDatestamp(self): 
     435        """ 
     436        Turns an NMEA datestamp format into a Python datetime.date object. 
     437        """ 
     438        datestamp = self.currentSentence.datestamp 
     439 
     440        day, month, year = [int(ordinalString) for ordinalString in 
     441                            (datestamp[0:2], datestamp[2:4], datestamp[4:6])] 
     442 
     443        # REVIEW: this can be optimized into a dict lookup + call, if I could 
     444        # have ternaries in 2.3 to do the intelligent datestamp handling. 
     445        if self.DATESTAMP_HANDLING == 'intelligent': 
     446            if year > self.INTELLIGENT_DATE_THRESHOLD: 
     447                year = int('19%02d' % year) 
     448            else: 
     449                year = int('20%02d' % year) 
     450 
     451        elif self.DATESTAMP_HANDLING == '20xx': 
     452            year = int('20%02d' % year) 
     453 
     454        elif self.DATESTAMP_HANDLING == '19xx': 
     455            year = int('19%02d' % year) 
     456 
     457        else: 
     458            raise ValueError("unknown datestamp handling method (%s)" 
     459                             % (self.DATESTAMP_HANDLING,)) 
     460 
     461        self._sentenceData['date'] = datetime.date(year, month, day) 
     462 
     463 
     464    def _fixCoordinateFloat(self, coordinate): 
     465        """ 
     466        Turns the NMEAProtocol coordinate format into Python float. 
     467 
     468        @param coordinate: The coordinate type: 'latitude' or 'longitude'. 
     469        @type coordinate: C{str} 
     470        """ 
     471        nmeaCoordinate = getattr(self.currentSentence, coordinate + 'Float') 
     472        left, right = nmeaCoordinate.split('.') 
     473        degrees, minutes = int(left[:-2]), float("%s.%s" % (left[-2:], right)) 
     474         
     475        self._sentenceData[coordinate] = base.Coordinate(degrees + minutes/60, 
     476                                                         coordinate) 
     477 
     478         
     479    ALLOWED_HEMISPHERE_LETTERS = { 
     480        "latitude": "NS", 
     481        'longitude': "EW", 
     482        "magneticVariation": "EW", 
     483    } 
     484 
     485     
     486    def _fixHemisphereSign(self, coordinate, sentenceDataKey=None): 
     487        """ 
     488        Fixes the sign for a hemisphere. 
     489 
     490        @param coordinate: Coordinate type (latitude, longitude or 
     491            magneticVariation). 
     492        @type coordinate: C{str} 
     493 
     494        This method must be called after the magnitude for the thing it 
     495        determines the sign of has been set. This is done by the following 
     496        functions: 
     497 
     498            - C{self.FIXERS['magneticVariation']} 
     499            - C{self.FIXERS['latitudeFloat']} 
     500            - C{self.FIXERS['longitudeFloat']} 
     501        """ 
     502        sentenceDataKey = sentenceDataKey or coordinate 
     503        sign = self._getHemisphereSign(coordinate) 
     504        self._sentenceData[sentenceDataKey] *= sign 
     505 
     506 
     507    COORDINATE_SIGNS = { 
     508        'N': 1, 
     509        'E': 1, 
     510        'S': -1, 
     511        'W': -1     
     512    } 
     513 
     514     
     515    def _getHemisphereSign(self, coordinate): 
     516        """ 
     517        Returns the hemisphere sign for a given coordinate. 
     518 
     519        @param coordinate: Coordinate type (latitude, longitude or 
     520            magneticVariation). 
     521        @type coordinate: C{str} 
     522        """ 
     523        if coordinate in ('latitude', 'longitude'): 
     524            hemisphereKey = coordinate + 'Hemisphere' 
     525        else: 
     526            hemisphereKey = coordinate + 'Direction' 
     527 
     528        hemisphere = getattr(self.currentSentence, hemisphereKey) 
     529 
     530        try: 
     531            return self.COORDINATE_SIGNS[hemisphere.upper()] 
     532        except KeyError: 
     533            raise ValueError("bad hemisphere/direction: %s" % hemisphere) 
     534             
     535 
     536    def _convert(self, sourceKey, converter=float, destinationKey=None): 
     537        """ 
     538        A simple conversion fix. 
     539        """ 
     540        currentValue = getattr(self.currentSentence, sourceKey) 
     541 
     542        if destinationKey is None: 
     543            destinationKey = sourceKey 
     544 
     545        self._sentenceData[destinationKey] = converter(currentValue) 
     546 
     547 
     548 
     549    STATEFUL_UPDATE = { 
     550        # sentenceKey: (stateKey, factory, attributeName, converter), 
     551        'trueHeading': 
     552            ('heading', base.Heading, 'heading', float), 
     553        'magneticVariation': 
     554            ('heading', base.Heading, 'variation', float), 
     555 
     556        'horizontalDilutionOfPrecision': 
     557            ('positioningError', base.PositioningError, 'hdop', float), 
     558        'verticalDilutionOfPrecision': 
     559            ('positioningError', base.PositioningError, 'vdop', float), 
     560        'positionDilutionOfPrecision': 
     561            ('positioningError', base.PositioningError, 'pdop', float), 
     562 
     563    } 
     564 
     565     
     566    def _statefulUpdate(self, sentenceKey): 
     567        """ 
     568        Does a stateful update of a particular positioning attribute. 
     569 
     570        @param stateKey: The name of the key in the sentence attributes, the 
     571            adapter state, and the NMEAAdapter.STATEFUL_UPDATE dict. 
     572        @type stateKey: C{str} 
     573        """ 
     574        stateKey, factory, attributeName, converter \ 
     575            = self.STATEFUL_UPDATE[sentenceKey] 
     576 
     577        if stateKey not in self._sentenceData: 
     578            self._sentenceData[stateKey] = self._state.get(stateKey, factory()) 
     579 
     580        newValue = converter(getattr(self.currentSentence, sentenceKey)) 
     581        setattr(self._sentenceData[stateKey], attributeName, newValue) 
     582             
     583         
     584    ACCEPTABLE_UNITS = frozenset(['M']) 
     585    UNIT_CONVERTERS = { 
     586        'N': lambda inKnots: base.Speed(float(inKnots) * base.MPS_PER_KNOT), 
     587        'K': lambda inKPH: base.Speed(float(inKPH) * base.MPS_PER_KPH), 
     588    } 
     589 
     590 
     591    def _fixUnits(self, unitKey=None, valueKey=None, sourceKey=None, unit=None): 
     592        """ 
     593        Fixes the units of a certain value. 
     594 
     595        At least one of C{unit}, C{unitKey} must be provided. 
     596 
     597        If the C{valueKey} is not provided, will attempt to strip "Units" from 
     598        the C{unitKey}. 
     599 
     600        If the C{sourceKey} is not provided. will store the new data in the 
     601        same place as the C{valueKey}. 
     602        """ 
     603        unit = unit or getattr(self.currentSentence, unitKey) 
     604        valueKey = valueKey or unitKey.strip('Units') 
     605        sourceKey = sourceKey or valueKey 
     606 
     607        if unit not in self.ACCEPTABLE_UNITS: 
     608            converter = self.UNIT_CONVERTERS[unit] 
     609            currentValue = getattr(self.currentSentence, sourceKey) 
     610            self._sentenceData[valueKey] = converter(currentValue) 
     611 
     612 
     613    GSV_KEYS = "satellitePRN", "azimuth", "elevation", "signalToNoiseRatio" 
     614 
     615 
     616    def _fixGSV(self): 
     617        """ 
     618        Parses partial visible satellite information from a GSV sentence. 
     619        """ 
     620        # To anyone who knows NMEA, this method's name should raise a chuckle's 
     621        # worth of schadenfreude. 'Fix' GSV? Hah! Ludicrous. 
     622        self._sentenceData['_partialBeaconInformation'] = base.BeaconInformation() 
     623 
     624        for index in range(4): 
     625            prn, azimuth, elevation, snr = \ 
     626                [getattr(self.currentSentence, "%s_%s" % (gsvKey, index)) 
     627                 for gsvKey in self.GSV_KEYS] 
     628 
     629            if prn is None or snr is None: 
     630                continue # continue not break, to accomodate for some bad gpses 
     631 
     632            satellite = base.Satellite(prn, azimuth, elevation, snr) 
     633            self._sentenceData['_partialBeaconInformation'].beacons.add(satellite) 
     634 
     635 
     636    def _fixBeacons(self, predicate): 
     637        """ 
     638        TODO: document 
     639         
     640        @param predicate: One of C{"seen"} or C{"used"}. 
     641        @type predicate: C{str} 
     642        """ 
     643        ## TODO: refactor mercilessly to a statefulUpdate 
     644        if "beaconInformation" not in self._state: 
     645            self._state["beaconInformation"] = base.BeaconInformation() 
     646 
     647        # TODO: Write tests for this!!! -- lvh 
     648        informationKey = predicate.lower() 
     649        sentenceKey = "beacons" + predicate.title() 
     650 
     651        setattr(self._state["beaconInformation"], informationKey, 
     652                int(getattr(self.currentSentence, sentenceKey))) 
     653 
     654 
     655    def _fixGSA(self): 
     656        """ 
     657        Extracts the information regarding which satellites were used in 
     658        obtaining the GPS fix from a GSA sentence. 
     659 
     660        @pre: A GSA sentence was fired. 
     661        @post: The current sentence data (C{self._sentenceData} will contain a 
     662            set of the currently used PRNs (under the key C{_usedPRNs}. 
     663        """ 
     664        self._sentenceData['_usedPRNs'] = set() 
     665        for key in ("usedSatellitePRN_%d" % x for x in range(12)): 
     666            prn = getattr(self.currentSentence, key, None) 
     667            if prn is not None: 
     668                self._sentenceData['_usedPRNs'].add(int(prn)) 
     669 
     670 
     671                 
     672    SENTENCE_INVALIDITY = { 
     673        "RMC": RMC_INVALID, 
     674        "GGA": GGA_INVALID, 
     675        "GLL": GLL_INVALID, 
     676    } 
     677 
     678 
     679    def _validate(self, sentenceType): 
     680        """ 
     681        Tests if a sentence contains a valid fix. 
     682 
     683        Some sentences (GGA, RMC...) contain information on the validity of the 
     684        fix. 
     685        """ 
     686        invalidValue = self.SENTENCE_INVALIDITY[sentenceType] 
     687        thisValue = getattr(self.currentSentence, "valid" + sentenceType) 
     688 
     689        if thisValue == invalidValue: 
     690            raise base.InvalidSentence("bad %s validity" % sentenceType) 
     691 
     692 
     693    SPECIFIC_SENTENCE_FIXES = { 
     694        'GPGSV': _fixGSV, 
     695        'GPGSA': _fixGSA, 
     696    } 
     697 
     698 
     699    def _sentenceSpecificFix(self): 
     700        """ 
     701        Executes a fix for a specific type of sentence. 
     702        """ 
     703        fixer = self.SPECIFIC_SENTENCE_FIXES.get(self.currentSentence.type) 
     704        if fixer is not None: 
     705            fixer(self) 
     706 
     707 
     708    FIXERS = { 
     709        'type': 
     710            lambda self: self._sentenceSpecificFix(), 
     711 
     712        'timestamp': 
     713            lambda self: self._fixTimestamp(), 
     714        'datestamp': 
     715            lambda self: self._fixDatestamp(), 
     716 
     717        'latitudeFloat': 
     718            lambda self: self._fixCoordinateFloat(coordinate='latitude'), 
     719        'latitudeHemisphere': 
     720            lambda self: self._fixHemisphereSign('latitude'), 
     721        'longitudeFloat': 
     722            lambda self: self._fixCoordinateFloat(coordinate='longitude'), 
     723        'longitudeHemisphere': 
     724            lambda self: self._fixHemisphereSign('longitude'), 
     725 
     726        'altitude': 
     727            lambda self: self._convert('altitude', 
     728                                       converter=base.Altitude), 
     729        'altitudeUnits': 
     730            lambda self: self._fixUnits(unitKey='altitudeUnits'), 
     731 
     732        'heightOfGeoidAboveWGS84': 
     733            lambda self: self._convert('heightOfGeoidAboveWGS84', 
     734                                       converter=base.Altitude), 
     735        'heightOfGeoidAboveWGS84Units': 
     736            lambda self: self._fixUnits( 
     737                unitKey='heightOfGeoidAboveWGS84Units'), 
     738 
     739        'trueHeading': 
     740            lambda self: self._statefulUpdate('trueHeading'), 
     741        'magneticVariation': 
     742            lambda self: self._statefulUpdate('magneticVariation'), 
     743 
     744        'magneticVariationDirection': 
     745            lambda self: self._fixHemisphereSign('magneticVariation', 
     746                                                 'heading'), 
     747 
     748        'speedInKnots': 
     749            lambda self: self._fixUnits(valueKey='speed', 
     750                                        sourceKey='speedInKnots', 
     751                                        unit='N'), 
     752 
     753        'validGGA': 
     754            lambda self: self._validate('GGA'), 
     755        'validRMC': 
     756            lambda self: self._validate('RMC'), 
     757        'validGLL': 
     758            lambda self: self._validate('GLL'), 
     759 
     760        'numberOfBeaconsUsed': 
     761            lambda self: self._fixBeacons('used'), 
     762        'numberOfBeaconsSeen': 
     763            lambda self: self._fixBeacons('seen'), 
     764 
     765        'positionDilutionOfPrecision': 
     766            lambda self: self._statefulUpdate('positionDilutionOfPrecision'), 
     767        'horizontalDilutionOfPrecision': 
     768            lambda self: self._statefulUpdate('horizontalDilutionOfPrecision'), 
     769        'verticalDilutionOfPrecision': 
     770            lambda self: self._statefulUpdate('verticalDilutionOfPrecision'), 
     771    } 
     772 
     773 
     774    def clear(self): 
     775        """ 
     776        Resets this adapter. 
     777 
     778        This will empty the adapter state and the current sentence data. 
     779        """ 
     780        self._state = {} 
     781        self._sentenceData = {} 
     782 
     783 
     784    def sentenceReceived(self, sentence): 
     785        """ 
     786        Called when a sentence is received. 
     787 
     788        Will clean the received NMEAProtocol sentence up, and then update the 
     789        adapter's state, followed by firing the callbacks. 
     790 
     791        If the received sentence was invalid, the state will be cleared. 
     792 
     793        @param sentence: The sentence that is received. 
     794        @type sentence: C{NMEASentence} 
     795        """ 
     796        self.currentSentence = sentence 
     797 
     798        try: 
     799            self._cleanCurrentSentence() 
     800            self._updateSentence() 
     801            self._fireSentenceCallbacks() 
     802        except base.InvalidSentence: 
     803            self.clear() 
     804 
     805 
     806    def _cleanCurrentSentence(self): 
     807        """ 
     808        Cleans the current sentence. 
     809        """ 
     810        for key in sorted(self.currentSentence.keys): 
     811            fixer = self.FIXERS.get(key, None) 
     812 
     813            if fixer is not None: 
     814                fixer(self) 
     815 
     816 
     817    def _updateSentence(self): 
     818        """ 
     819        Updates the current state with the new information from the sentence. 
     820        """ 
     821        self._updateBeaconInformation() 
     822        self._state.update(self._sentenceData) 
     823 
     824 
     825    def _updateBeaconInformation(self): 
     826        """ 
     827        Updates existing beacon information state with new data. 
     828        """ 
     829        new = self._sentenceData.get('_partialBeaconInformation') 
     830        if new is None: 
     831            return 
     832 
     833        usedPRNs = self._state.get('_usedPRNs') \ 
     834            or self._sentenceData.get('_usedPRNs') 
     835        if usedPRNs is not None: 
     836            for beacon in new.beacons: 
     837                beacon.isUsed = (beacon.identifier in usedPRNs) 
     838 
     839        old = self._state.get('_partialBeaconInformation') 
     840        if old is not None: 
     841            new.beacons.update(old.beacons) 
     842                 
     843        if self.currentSentence._isLastGSVSentence(): 
     844            del self._state['_partialBeaconInformation'] 
     845            bi = self._sentenceData.pop('_partialBeaconInformation') 
     846            self._sentenceData['beaconInformation'] = bi 
     847 
     848 
     849    def _fireSentenceCallbacks(self): 
     850        """ 
     851        Fires sentence callbacks for the current sentence. 
     852 
     853        A callback will only fire if all of the keys it requires are present in 
     854        the current state and at least one such field was altered in the 
     855        current sentence. 
     856 
     857        The callbacks will only be fired with data from C{self._state}. 
     858        """ 
     859        for callbackName, requiredFields in self.REQUIRED_CALLBACK_FIELDS.items(): 
     860            callback = getattr(self._receiver, callbackName, None) 
     861 
     862            if callback is None: 
     863                continue 
     864 
     865            kwargs = {} 
     866            atLeastOnePresentInSentence = False 
     867 
     868            try: 
     869                for field in requiredFields: 
     870                    if field in self._sentenceData: 
     871                        atLeastOnePresentInSentence = True 
     872                        kwargs[field] = self._state[field] 
     873            except KeyError: 
     874                continue 
     875 
     876            if atLeastOnePresentInSentence: 
     877                callback(**kwargs) 
     878 
     879 
     880NMEAAdapter.REQUIRED_CALLBACK_FIELDS = dict((name, method.positional) 
     881     for name, method 
     882     in ipositioning.IPositioningReceiver.namesAndDescriptions()) 
  • twisted/positioning/test/test_nmea.py

    === added directory 'twisted/positioning/test'
    === added file 'twisted/positioning/test/__init__.py'
    === added file 'twisted/positioning/test/test_nmea.py'
     
     1# -*- encoding: utf-8 -*- 
     2# Copyright (c) 2009 Twisted Matrix Laboratories. 
     3# See LICENSE for details. 
     4""" 
     5Test cases for using NMEA sentences. 
     6""" 
     7import datetime 
     8from zope.interface import implements 
     9 
     10from twisted.positioning import base, nmea, ipositioning 
     11from twisted.trial.unittest import TestCase 
     12 
     13class NMEATestReceiver(object): 
     14    implements(ipositioning.INMEAReceiver) 
     15 
     16    def __init__(self): 
     17        self.clear() 
     18 
     19 
     20    def clear(self): 
     21        self.receivedSentence = None 
     22 
     23 
     24    def sentenceReceived(self, sentence): 
     25        self.receivedSentence = sentence 
     26 
     27 
     28 
     29class CallbackTestNMEAProtocol(nmea.NMEAProtocol): 
     30    """ 
     31    A class that tests that the correct callbacks are called. 
     32    """ 
     33    def __init__(self): 
     34        nmea.NMEAProtocol.__init__(self, None) 
     35 
     36        for sentenceType in nmea.NMEAProtocol.SENTENCE_CONTENTS: 
     37            self._createCallback(sentenceType) 
     38 
     39        self.clear() 
     40 
     41 
     42    def clear(self): 
     43        self.sentenceReceived = None 
     44        self.called = {} 
     45 
     46 
     47    SENTENCE_TYPES = [x for x in nmea.NMEAProtocol.SENTENCE_CONTENTS] 
     48 
     49    def _createCallback(self, sentenceType): 
     50        """ 
     51        Creates a callback for an NMEA sentence. 
     52        """ 
     53        def callback(sentence): 
     54            self.sentenceReceived = sentence 
     55            self.called[sentenceType] = True 
     56 
     57        setattr(self, "nmea_" + sentenceType, callback) 
     58 
     59 
     60 
     61class NMEATests(TestCase): 
     62    def setUp(self): 
     63        self.callbackProtocol = CallbackTestNMEAProtocol() 
     64 
     65        self.receiver = NMEATestReceiver() 
     66        self.receiverProtocol = nmea.NMEAProtocol(self.receiver) 
     67 
     68 
     69    def test_callbacksCalled(self): 
     70        """ 
     71        Tests that the correct callbacks fire, and that *only* those fire. 
     72        """ 
     73        sentencesByType = { 
     74            "GPGGA": [ 
     75                "$GPGGA*56", 
     76            ], 
     77 
     78            "GPRMC": [ 
     79                "$GPRMC*4b", 
     80            ], 
     81 
     82            "GPGSV": [ 
     83                "$GPGSV*55", 
     84            ], 
     85 
     86            "GPGLL": [ 
     87                "$GPGLL*50", 
     88            ], 
     89 
     90            "GPHDT": [ 
     91                "$GPHDT*4f", 
     92            ], 
     93 
     94            "GPGSA": [ 
     95                "$GPGSA*42", 
     96            ], 
     97 
     98        } 
     99 
     100        for calledSentenceType in sentencesByType: 
     101            for sentence in sentencesByType[calledSentenceType]: 
     102                self.callbackProtocol.lineReceived(sentence) 
     103                called = self.callbackProtocol.called 
     104 
     105                for sentenceType in CallbackTestNMEAProtocol.SENTENCE_TYPES: 
     106                    self.assertEquals(sentenceType == calledSentenceType, 
     107                                      called.get(sentenceType, False)) 
     108 
     109                self.callbackProtocol.clear() 
     110 
     111                self.receiverProtocol.lineReceived(sentence) 
     112                self.assertTrue(self.receiver.receivedSentence) 
     113 
     114 
     115 
     116    def test_validateGoodChecksum(self): 
     117        """ 
     118        Tests checkum validation for good or missing checksums. 
     119        """ 
     120        sentences = [ 
     121          '$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47', 
     122          '$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*', 
     123        ] 
     124 
     125        for sentence in sentences: 
     126            nmea.NMEAProtocol.validateChecksum(sentence) 
     127 
     128 
     129    def test_validateBadChecksum(self): 
     130        """ 
     131        Tests checksum validation on bad checksums. 
     132        """ 
     133        sentences = [ 
     134          '$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*46', 
     135        ] 
     136 
     137        for sentence in sentences: 
     138            self.assertRaises(base.InvalidChecksum, 
     139                              nmea.NMEAProtocol.validateChecksum, sentence) 
     140 
     141 
     142    def test_GSVFirstSequence(self): 
     143        """ 
     144        Tests if the last sentence in a GSV sequence is correctly identified. 
     145        """ 
     146        string = '$GPGSV,3,1,11,22,42,067,42,24,14,311,43,27,05,244,00,,,,*4F' 
     147        self.callbackProtocol.lineReceived(string) 
     148        sentence = self.callbackProtocol.sentenceReceived 
     149 
     150        self.assertTrue(sentence._isFirstGSVSentence()) 
     151        self.assertFalse(sentence._isLastGSVSentence()) 
     152 
     153 
     154    def test_GSVLastSentence(self): 
     155        """ 
     156        Tests if the last sentence in a GSV sequence is correctly identified. 
     157        """ 
     158        string = '$GPGSV,3,3,11,22,42,067,42,24,14,311,43,27,05,244,00,,,,*4D' 
     159        self.callbackProtocol.lineReceived(string) 
     160        sentence = self.callbackProtocol.sentenceReceived 
     161 
     162        self.assertFalse(sentence._isFirstGSVSentence()) 
     163        self.assertTrue(sentence._isLastGSVSentence()) 
     164 
     165 
     166    def test_parsing(self): 
     167        """ 
     168        Tests the parsing of a few sentences. 
     169        """ 
     170        sentences = { 
     171        # Full GPRMC sentence. 
     172        '$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A': { 
     173             'type': 'GPRMC', 
     174             'latitudeFloat': '4807.038', 
     175             'latitudeHemisphere': 'N', 
     176             'longitudeFloat': '01131.000', 
     177             'longitudeHemisphere': 'E', 
     178             'magneticVariation': '003.1', 
     179             'magneticVariationDirection': 'W', 
     180             'speedInKnots': '022.4', 
     181             'timestamp': '123519', 
     182             'datestamp': '230394', 
     183             'trueHeading': '084.4', 
     184             'validRMC': 'A', 
     185        }, 
     186 
     187        # Full GPGGA sentence. 
     188        '$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47': { 
     189            'type': 'GPGGA', 
     190 
     191            'altitude': '545.4', 
     192            'altitudeUnits': 'M', 
     193            'heightOfGeoidAboveWGS84': '46.9', 
     194            'heightOfGeoidAboveWGS84Units': 'M', 
     195 
     196            'horizontalDilutionOfPrecision': '0.9', 
     197 
     198            'latitudeFloat': '4807.038', 
     199            'latitudeHemisphere': 'N', 
     200            'longitudeFloat': '01131.000', 
     201            'longitudeHemisphere': 'E', 
     202 
     203            'numberOfSatellitesSeen': '08', 
     204            'timestamp': '123519', 
     205            'validGGA': '1', 
     206        }, 
     207 
     208        # Partial GPGLL sentence. 
     209        '$GPGLL,3751.65,S,14507.36,E*77': { 
     210            'type': 'GPGLL', 
     211 
     212            'latitudeFloat': '3751.65', 
     213            'latitudeHemisphere': 'S', 
     214            'longitudeFloat': '14507.36', 
     215            'longitudeHemisphere': 'E', 
     216        }, 
     217 
     218        # Full GPGLL sentence. 
     219        '$GPGLL,4916.45,N,12311.12,W,225444,A*31': { 
     220            'type': 'GPGLL', 
     221 
     222            'latitudeFloat': '4916.45', 
     223            'latitudeHemisphere': 'N', 
     224            'longitudeFloat': '12311.12', 
     225            'longitudeHemisphere': 'W', 
     226 
     227            'timestamp': '225444', 
     228            'validGLL': 'A', 
     229        }, 
     230 
     231        # Full GPGSV sentence. 
     232        '$GPGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,00,13,06,292,00*74': { 
     233            'type': 'GPGSV', 
     234            'GSVSentenceIndex': '1', 
     235            'numberOfGSVSentences': '3', 
     236            'numberOfSatellitesSeen': '11', 
     237 
     238            'azimuth_0': '111', 
     239            'azimuth_1': '270', 
     240            'azimuth_2': '010', 
     241            'azimuth_3': '292', 
     242 
     243            'elevation_0': '03', 
     244            'elevation_1': '15', 
     245            'elevation_2': '01', 
     246            'elevation_3': '06', 
     247 
     248            'satellitePRN_0': '03', 
     249            'satellitePRN_1': '04', 
     250            'satellitePRN_2': '06', 
     251            'satellitePRN_3': '13', 
     252 
     253            'signalToNoiseRatio_0': '00', 
     254            'signalToNoiseRatio_1': '00', 
     255            'signalToNoiseRatio_2': '00', 
     256            'signalToNoiseRatio_3': '00', 
     257        }, 
     258 
     259        # Partially empty GSV sentence support. 
     260        '$GPGSV,3,3,11,22,42,067,42,24,14,311,43,27,05,244,00,,,,*4D': { 
     261            'type': 'GPGSV', 
     262            'GSVSentenceIndex': '3', 
     263            'numberOfGSVSentences': '3', 
     264            'numberOfSatellitesSeen': '11', 
     265 
     266            'azimuth_0': '067', 
     267            'azimuth_1': '311', 
     268            'azimuth_2': '244', 
     269 
     270            'elevation_0': '42', 
     271            'elevation_1': '14', 
     272            'elevation_2': '05', 
     273 
     274            'satellitePRN_0': '22', 
     275            'satellitePRN_1': '24', 
     276            'satellitePRN_2': '27', 
     277 
     278            'signalToNoiseRatio_0': '42', 
     279            'signalToNoiseRatio_1': '43', 
     280            'signalToNoiseRatio_2': '00', 
     281        }, 
     282 
     283        # Full HDT sentence. 
     284        '$GPHDT,038.005,T*3B': { 
     285            'type': 'GPHDT', 
     286            'trueHeading': '038.005', 
     287        }, 
     288 
     289        # Full TRG sentence. 
     290        # TODO: fill 
     291 
     292        # Typical GPGSA sentence. 
     293        '$GPGSA,A,3,19,28,14,18,27,22,31,39,,,,,1.7,1.0,1.3*34': { 
     294            'type': 'GPGSA', 
     295 
     296            'usedSatellitePRN_0': '19', 
     297            'usedSatellitePRN_1': '28', 
     298            'usedSatellitePRN_2': '14', 
     299            'usedSatellitePRN_3': '18', 
     300            'usedSatellitePRN_4': '27', 
     301            'usedSatellitePRN_5': '22', 
     302            'usedSatellitePRN_6': '31', 
     303            'usedSatellitePRN_7': '39', 
     304 
     305            'positionDilutionOfPrecision': '1.7', 
     306            'horizontalDilutionOfPrecision': '1.0', 
     307            'verticalDilutionOfPrecision': '1.3', 
     308        }, 
     309 
     310 
     311        } 
     312 
     313        for sentence, expected in sentences.iteritems(): 
     314            self.callbackProtocol.lineReceived(sentence) 
     315            received = self.callbackProtocol.sentenceReceived 
     316            self.assertEquals(expected, received._sentenceData) 
     317            self.callbackProtocol.clear() 
     318 
     319 
     320 
     321class NMEAAdapterConverterTests(TestCase): 
     322    """ 
     323    Tests for the converters on an NMEA adapter. 
     324    """ 
     325    def setUp(self): 
     326        self.adapter = nmea.NMEAAdapter(None) 
     327 
     328 
     329    def test_fixTimestamp(self): 
     330        self._genericFixerTest( 
     331            {'timestamp': '123456'}, # 12:34:56Z 
     332            {'time': datetime.time(12, 34, 56)}) 
     333 
     334 
     335    def test_fixBrokenTimestamp(self): 
     336        self._genericFixerRaisingTest( 
     337            {'timestamp': '993456'}, ValueError) 
     338        self._genericFixerRaisingTest( 
     339            {'timestamp': '129956'}, ValueError) 
     340        self._genericFixerRaisingTest( 
     341            {'timestamp': '123499'}, ValueError) 
     342 
     343 
     344    def test_fixDatestamp_intelligent(self): 
     345        self._genericFixerTest( 
     346            {'datestamp': '010199'}, 
     347            {'date': datetime.date(1999, 1, 1)}) 
     348 
     349        self._genericFixerTest( 
     350            {'datestamp': '010109'}, 
     351            {'date': datetime.date(2009, 1, 1)}) 
     352 
     353 
     354    def test_fixDatestamp_19xx(self): 
     355        self.adapter.DATESTAMP_HANDLING = '19xx' 
     356 
     357        self._genericFixerTest( 
     358            {'datestamp': '010199'}, 
     359            {'date': datetime.date(1999, 1, 1)}) 
     360 
     361        self._genericFixerTest( 
     362            {'datestamp': '010109'}, 
     363            {'date': datetime.date(1909, 1, 1)}) 
     364 
     365 
     366    def test_fixDatestamp_20xx(self): 
     367        self.adapter.DATESTAMP_HANDLING = '20xx' 
     368 
     369        self._genericFixerTest( 
     370            {'datestamp': '010199'}, 
     371            {'date': datetime.date(2099, 1, 1)}) 
     372 
     373        self._genericFixerTest( 
     374            {'datestamp': '010109'}, 
     375            {'date': datetime.date(2009, 1, 1)}) 
     376 
     377 
     378    def test_fixBrokenDatestamp(self): 
     379        self._genericFixerRaisingTest({'datestamp': '123456'}, ValueError) 
     380 
     381 
     382    def test_coordinate_north(self): 
     383        self._genericFixerTest( 
     384            {'latitudeFloat': '1030.000', 'latitudeHemisphere': 'N'}, 
     385            {'latitude': base.Coordinate(10+30.000/60, 'latitude')}) 
     386 
     387         
     388    def test_coordinate_south(self): 
     389        self._genericFixerTest( 
     390            {'latitudeFloat': '4512.145', 'latitudeHemisphere': 'S'}, 
     391            {'latitude': base.Coordinate(-(45 + 12.145/60), 'latitude')}) 
     392 
     393 
     394    def test_coordinate_east(self): 
     395        self._genericFixerTest( 
     396            {'longitudeFloat': '5331.513', 'longitudeHemisphere': 'E'}, 
     397            {'longitude': base.Coordinate(53 + 31.513/60, 'longitude')}) 
     398 
     399         
     400    def test_coordinate_west(self): 
     401        self._genericFixerTest( 
     402            {'longitudeFloat': '1245.120', 'longitudeHemisphere': 'W'}, 
     403            {'longitude': base.Coordinate(-(12 + 45.12/60), 'longitude')}) 
     404 
     405 
     406    def test_fixHemisphereSignBadHemispheres(self): 
     407        self._genericFixerRaisingTest({'longitudeHemisphere': 'Q'}, 
     408                                      ValueError) 
     409 
     410 
     411    def test_fixAltitude(self): 
     412        self._genericFixerTest( 
     413            {'altitude': '545.4'}, 
     414            {'altitude': base.Altitude(545.4)}) 
     415 
     416        self._genericFixerTest( 
     417            {'heightOfGeoidAboveWGS84': '46.9'}, 
     418            {'heightOfGeoidAboveWGS84': base.Altitude(46.9)}) 
     419 
     420 
     421    def test_validation(self): 
     422        """ 
     423        Tests validation of sentences. 
     424 
     425        Invalid sentences will cause the state to be cleared. The altitude is 
     426        added so we have junk data (that will hopefully be removed, since the 
     427        GPS is telling us that this data is invalid). 
     428        """ 
     429        self._genericFixerTest( 
     430            {'altitude': '545.4', 'validGGA': nmea.GGA_GPS_FIX}, 
     431            {'altitude': base.Altitude(545.4)}) 
     432        self._genericFixerTest( 
     433            {'altitude': '545.4', 'validGGA': nmea.GGA_INVALID}, 
     434            {}) 
     435 
     436        self._genericFixerTest( 
     437            {'altitude': '545.4', 'validGLL': nmea.GLL_VALID}, 
     438            {'altitude': base.Altitude(545.4)}) 
     439        self._genericFixerTest( 
     440            {'altitude': '545.4', 'validGLL': nmea.GLL_INVALID}, 
     441            {}) 
     442 
     443 
     444    def test_speedInKnots(self): 
     445        self._genericFixerTest( 
     446            {'speedInKnots': '10'}, 
     447            {'speed': base.Speed(10 * base.MPS_PER_KNOT)}) 
     448 
     449 
     450    def test_magneticVariation_west(self): 
     451        self._genericFixerTest( 
     452            {'magneticVariation': '1.34', 'magneticVariationDirection': 'W'}, 
     453            {'heading': base.Heading(variation=-1.34)}) 
     454 
     455         
     456    def test_magneticVariation_east(self): 
     457        self._genericFixerTest( 
     458            {'magneticVariation': '1.34', 'magneticVariationDirection': 'E'}, 
     459            {'heading': base.Heading(variation=1.34)}) 
     460 
     461 
     462    def test_headingPlusMagneticVariation(self): 
     463 
     464        self._genericFixerTest( 
     465            {'trueHeading': '123.12', 
     466             'magneticVariation': '1.34', 'magneticVariationDirection': 'E'}, 
     467            {'heading': base.Heading(123.12, variation=1.34)}) 
     468 
     469 
     470    def test_positioningError(self): 
     471        self._genericFixerTest( 
     472            {'horizontalDilutionOfPrecision': '11'}, 
     473            {'positioningError': base.PositioningError(hdop=11.)}) 
     474 
     475 
     476    def test_positioningError_mixing(self): 
     477        self._genericFixerTest( 
     478            {'positionDilutionOfPrecision': '1', 
     479             'horizontalDilutionOfPrecision': '1', 
     480             'verticalDilutionOfPrecision': '1'}, 
     481            {'positioningError': base.PositioningError(pdop=1., hdop=1., vdop=1.)}) 
     482 
     483 
     484    def _genericFixerTest(self, sentenceData, expected): 
     485        sentence = nmea.NMEASentence(sentenceData) 
     486        self.adapter.sentenceReceived(sentence) 
     487        self.assertEquals(self.adapter._state, expected) 
     488        self.adapter.clear() 
     489 
     490 
     491    def _genericFixerRaisingTest(self, sentenceData, exceptionClass): 
     492        sentence = nmea.NMEASentence(sentenceData) 
     493        self.assertRaises(exceptionClass, 
     494        self.adapter.sentenceReceived, sentence) 
     495        self.adapter.clear() 
     496 
     497 
     498class MockNMEAReceiver(base.BasePositioningReceiver): 
     499    """ 
     500    A mock NMEA receiver. 
     501    """ 
     502    def __init__(self): 
     503        self.clear() 
     504 
     505        for methodName in ipositioning.IPositioningReceiver: 
     506            setattr(self, methodName, self._callbackForName(methodName)) 
     507 
     508 
     509    def clear(self): 
     510        self.called = {} 
     511 
     512 
     513    def _callbackForName(self, name): 
     514        def setter(**_): 
     515            self.called[name] = True 
     516        return setter 
     517 
     518 
     519 
     520class NMEAReceiverTest(TestCase): 
     521    """ 
     522    Tests for NMEAReceivers. 
     523    """ 
     524    def setUp(self): 
     525        self.receiver = MockNMEAReceiver() 
     526        self.adapter = nmea.NMEAAdapter(self.receiver) 
     527        self.protocol = nmea.NMEAProtocol(self.adapter) 
     528 
     529 
     530    def test_positioningErrorUpdateAcrossStates(self): 
     531        """ 
     532        Tests that the positioning error is updated across multiple states. 
     533        """ 
     534        sentences = [ 
     535        '$GPGSA,A,3,19,28,14,18,27,22,31,39,,,,,1.7,1.0,1.3*34', 
     536        '$GPGSV,3,1,11,19,03,111,00,04,15,270,00,06,01,010,00,31,06,292,00*7f', 
     537        '$GPGSV,3,2,11,28,25,170,00,14,57,208,39,18,67,296,40,39,40,246,00*7b', 
     538        '$GPGSV,3,3,11,22,42,067,42,27,14,311,43,27,05,244,00,,,,*4e', 
     539        ] 
     540        callbacksFired = set(['positioningErrorReceived', 
     541                              'beaconInformationReceived']) 
     542 
     543        self._genericSentenceReceivedTest(sentences, callbacksFired) 
     544 
     545 
     546    def test_GGASentences(self): 
     547        sentences = [ 
     548        '$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47', 
     549        ] 
     550        callbacksFired = set(['positionReceived', 
     551                              'positioningErrorReceived', 
     552                              'altitudeReceived', 
     553                              'timeReceived']) 
     554        self._genericSentenceReceivedTest(sentences, callbacksFired) 
     555 
     556 
     557    def test_RMCSentences(self): 
     558        sentences = [ 
     559        '$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A', 
     560        ] 
     561        callbacksFired = set(['headingReceived', 
     562                              'speedReceived', 
     563                              'positionReceived', 
     564                              'timeReceived']) 
     565 
     566        self._genericSentenceReceivedTest(sentences, callbacksFired) 
     567 
     568 
     569    def test_GSVSentences_incompleteSequence(self): 
     570        """ 
     571        Verifies that an incomplete sequence of GSV sentences does not fire. 
     572        """ 
     573        sentences=[ 
     574        '$GPGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,00,13,06,292,00*74', 
     575        ] 
     576 
     577        self._genericSentenceReceivedTest(sentences, set([])) 
     578 
     579 
     580    def test_GSVSentences(self): 
     581        """ 
     582        Verifies that a complete sequence of GSV sentences fires. 
     583        """ 
     584        sentences=[ 
     585        '$GPGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,00,13,06,292,00*74', 
     586        '$GPGSV,3,2,11,14,25,170,00,16,57,208,39,18,67,296,40,19,40,246,00*74', 
     587        '$GPGSV,3,3,11,22,42,067,42,24,14,311,43,27,05,244,00,,,,*4D', 
     588        ] 
     589 
     590        callbacksFired = set(['beaconInformationReceived']) 
     591 
     592        lambda self: self.assertNotIn( 
     593            '_partialBeaconInformation', self.adapter._state) 
     594 
     595 
     596        self._genericSentenceReceivedTest(sentences, callbacksFired, 
     597            beforeClearCondition=lambda self: self.assertNotIn( 
     598                '_partialBeaconInformation', self.adapter._state)) 
     599 
     600 
     601 
     602    def test_GLLSentences(self): 
     603        sentences=[ 
     604            '$GPGLL,3751.65,S,14507.36,E*77', 
     605            '$GPGLL,4916.45,N,12311.12,W,225444,A*31', 
     606        ] 
     607        callbacksFired = set(['positionReceived', 'timeReceived']) 
     608 
     609        self._genericSentenceReceivedTest(sentences, callbacksFired) 
     610 
     611 
     612    def test_HDTSentences(self): 
     613        sentences=[ 
     614            "$GPHDT,038.005,T*3B", 
     615        ] 
     616        callbacksFired = set(['headingReceived']) 
     617 
     618        self._genericSentenceReceivedTest(sentences, callbacksFired) 
     619 
     620 
     621    def test_mixedSentences(self): 
     622        sentences = [ 
     623        '$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A', 
     624        '$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47', 
     625        ] 
     626        callbacksFired = set(['altitudeReceived', 
     627                              'speedReceived', 
     628                              'positionReceived', 
     629                              'positioningErrorReceived', 
     630                              'timeReceived', 
     631                              'headingReceived']) 
     632        self._genericSentenceReceivedTest(sentences, callbacksFired) 
     633 
     634 
     635    def test_mixesSentences_withBeaconInformationAndVisibility(self): 
     636        sentences = [ 
     637        '$GPGSA,A,3,16,4,13,18,27,22,31,39,,,,,1.7,1.0,1.3*', 
     638        '$GPGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,00,13,06,292,00*74', 
     639        '$GPGSV,3,2,11,14,25,170,00,16,57,208,39,18,67,296,40,19,40,246,00*74', 
     640        '$GPGSV,3,3,11,22,42,067,42,24,14,311,43,27,05,244,00,,,,*4D', 
     641        '$GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A', 
     642        '$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47', 
     643        '$GPGLL,4916.45,N,12311.12,W,225444,A*31', 
     644        ] 
     645        callbacksFired = set(['headingReceived', 
     646                              'beaconInformationReceived', 
     647                              'speedReceived', 
     648                              'positionReceived', 
     649                              'timeReceived', 
     650                              'altitudeReceived', 
     651                              'positioningErrorReceived']) 
     652        self._genericSentenceReceivedTest(sentences, callbacksFired) 
     653 
     654 
     655    def _genericSentenceReceivedTest(self, sentences, callbacksFired, 
     656                                     beforeClearCondition=lambda _: None): 
     657        for sentence in sentences: 
     658            self.protocol.lineReceived(sentence) 
     659 
     660        self.assertEquals(set(self.receiver.called.keys()), callbacksFired) 
     661 
     662        beforeClearCondition(self) 
     663 
     664        self.receiver.clear() 
     665        self.adapter.clear()