Ticket #3926: positioning.diff
| File positioning.diff, 75.2 KB (added by lvh, 4 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 """ 6 Generic positioning base classes. 7 """ 8 from zope.interface import implements 9 from twisted.positioning import ipositioning 10 11 MPS_PER_KNOT = 0.5144444444444444 12 MPS_PER_KPH = 0.27777777777777777 13 METERS_PER_FOOT = 0.3048 14 15 16 class 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 61 class InvalidSentence(Exception): 62 """ 63 An exception raised when a sentence is invalid. 64 """ 65 66 67 68 class InvalidChecksum(Exception): 69 """ 70 An exception raised when the checksum of a sentence is invalid. 71 """ 72 73 74 class 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 193 class 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 265 class 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. 284 o @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 331 class 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 474 class 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 629 class 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 689 class 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 736 class 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 """ 5 Positioning interface. 6 """ 7 from zope.interface import Interface 8 9 10 class 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 87 class 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 """ 6 Classes for using NMEAProtocol sentences. 7 """ 8 import itertools 9 import operator 10 import datetime 11 from zope.interface import implements 12 13 import twisted.protocols.basic 14 from twisted.positioning import base, ipositioning 15 16 17 class 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 291 class 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 381 MODE_AUTO, MODE_MANUAL = 'A', 'M' 382 RMC_INVALID, RMC_VALID = 'V', 'A' 383 GLL_INVALID, GLL_VALID = 'V', 'A' 384 GGA_INVALID, GGA_GPS_FIX, GGA_DGPS_FIX = 1, 2, 3 385 386 387 388 class 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 880 NMEAAdapter.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 """ 5 Test cases for using NMEA sentences. 6 """ 7 import datetime 8 from zope.interface import implements 9 10 from twisted.positioning import base, nmea, ipositioning 11 from twisted.trial.unittest import TestCase 12 13 class 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 29 class 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 61 class 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 321 class 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 498 class 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 520 class 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()
