Open Media Library Platform
This commit is contained in:
commit
411ad5b16f
5849 changed files with 1778641 additions and 0 deletions
|
|
@ -0,0 +1,8 @@
|
|||
# -*- test-case-name: twisted.positioning.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
"""
|
||||
The Twisted positioning framework.
|
||||
|
||||
@since: 14.0
|
||||
"""
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
"""
|
||||
Generic sentence handling tools: hopefully reusable.
|
||||
"""
|
||||
class _BaseSentence(object):
|
||||
"""
|
||||
A base sentence class for a particular protocol.
|
||||
|
||||
Using this base class, specific sentence classes can almost automatically
|
||||
be created for a particular protocol (except for the documentation of
|
||||
course) if that protocol implements the L{IPositioningSentenceProducer}
|
||||
interface. To do this, fill the ALLOWED_ATTRIBUTES class attribute using
|
||||
the C{getSentenceAttributes} class method of the producer::
|
||||
|
||||
class FooSentence(BaseSentence):
|
||||
\"\"\"
|
||||
A sentence for integalactic transmodulator sentences.
|
||||
|
||||
@ivar transmogrificationConstant: The value used in the
|
||||
transmogrifier while producing this sentence, corrected for
|
||||
gravitational fields.
|
||||
@type transmogrificationConstant: C{Tummy}
|
||||
\"\"\"
|
||||
ALLOWED_ATTRIBUTES = FooProtocol.getSentenceAttributes()
|
||||
|
||||
@ivar presentAttribues: An iterable containing the names of the
|
||||
attributes that are present in this sentence.
|
||||
@type presentAttributes: iterable of C{str}
|
||||
|
||||
@cvar ALLOWED_ATTRIBUTES: A set of attributes that are allowed in this
|
||||
sentence.
|
||||
@type ALLOWED_ATTRIBUTES: C{set} of C{str}
|
||||
"""
|
||||
ALLOWED_ATTRIBUTES = set()
|
||||
|
||||
|
||||
def __init__(self, sentenceData):
|
||||
"""
|
||||
Initializes a sentence with parsed sentence data.
|
||||
|
||||
@param sentenceData: The parsed sentence data.
|
||||
@type sentenceData: C{dict} (C{str} -> C{str} or C{NoneType})
|
||||
"""
|
||||
self._sentenceData = sentenceData
|
||||
|
||||
|
||||
@property
|
||||
def presentAttributes(self):
|
||||
"""
|
||||
An iterable containing the names of the attributes that are present in
|
||||
this sentence.
|
||||
|
||||
@return: The iterable of names of present attributes.
|
||||
@rtype: iterable of C{str}
|
||||
"""
|
||||
return iter(self._sentenceData)
|
||||
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""
|
||||
Gets an attribute of this sentence.
|
||||
"""
|
||||
if name in self.ALLOWED_ATTRIBUTES:
|
||||
return self._sentenceData.get(name, None)
|
||||
else:
|
||||
className = self.__class__.__name__
|
||||
msg = "%s sentences have no %s attributes" % (className, name)
|
||||
raise AttributeError(msg)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Returns a textual representation of this sentence.
|
||||
|
||||
@return: A textual representation of this sentence.
|
||||
@rtype: C{str}
|
||||
"""
|
||||
items = self._sentenceData.items()
|
||||
data = ["%s: %s" % (k, v) for k, v in sorted(items) if k != "type"]
|
||||
dataRepr = ", ".join(data)
|
||||
|
||||
typeRepr = self._sentenceData.get("type") or "unknown type"
|
||||
className = self.__class__.__name__
|
||||
|
||||
return "<%s (%s) {%s}>" % (className, typeRepr, dataRepr)
|
||||
|
||||
|
||||
|
||||
class _PositioningSentenceProducerMixin(object):
|
||||
"""
|
||||
A mixin for certain protocols that produce positioning sentences.
|
||||
|
||||
This mixin helps protocols that store the layout of sentences that they
|
||||
consume in a C{_SENTENCE_CONTENTS} class variable provide all sentence
|
||||
attributes that can ever occur. It does this by providing a class method,
|
||||
C{getSentenceAttributes}, which iterates over all sentence types and
|
||||
collects the possible sentence attributes.
|
||||
"""
|
||||
@classmethod
|
||||
def getSentenceAttributes(cls):
|
||||
"""
|
||||
Returns a set of all attributes that might be found in the sentences
|
||||
produced by this protocol.
|
||||
|
||||
This is basically a set of all the attributes of all the sentences that
|
||||
this protocol can produce.
|
||||
|
||||
@return: The set of all possible sentence attribute names.
|
||||
@rtype: C{set} of C{str}
|
||||
"""
|
||||
attributes = set(["type"])
|
||||
for attributeList in cls._SENTENCE_CONTENTS.values():
|
||||
for attribute in attributeList:
|
||||
if attribute is None:
|
||||
continue
|
||||
attributes.add(attribute)
|
||||
|
||||
return attributes
|
||||
929
Linux/lib/python2.7/site-packages/twisted/positioning/base.py
Normal file
929
Linux/lib/python2.7/site-packages/twisted/positioning/base.py
Normal file
|
|
@ -0,0 +1,929 @@
|
|||
# -*- test-case-name: twisted.positioning.test.test_base,twisted.positioning.test.test_sentence -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
"""
|
||||
Generic positioning base classes.
|
||||
|
||||
@since: 14.0
|
||||
"""
|
||||
from functools import partial
|
||||
from operator import attrgetter
|
||||
from zope.interface import implementer
|
||||
from twisted.python.constants import Names, NamedConstant
|
||||
from twisted.python.util import FancyEqMixin
|
||||
from twisted.positioning import ipositioning
|
||||
|
||||
|
||||
MPS_PER_KNOT = 0.5144444444444444
|
||||
MPS_PER_KPH = 0.27777777777777777
|
||||
METERS_PER_FOOT = 0.3048
|
||||
|
||||
|
||||
|
||||
class Angles(Names):
|
||||
"""
|
||||
The types of angles.
|
||||
"""
|
||||
LATITUDE = NamedConstant()
|
||||
LONGITUDE = NamedConstant()
|
||||
HEADING = NamedConstant()
|
||||
VARIATION = NamedConstant()
|
||||
|
||||
|
||||
|
||||
class Directions(Names):
|
||||
"""
|
||||
The four cardinal directions (north, east, south, west).
|
||||
"""
|
||||
NORTH = NamedConstant()
|
||||
EAST = NamedConstant()
|
||||
SOUTH = NamedConstant()
|
||||
WEST = NamedConstant()
|
||||
|
||||
|
||||
|
||||
@implementer(ipositioning.IPositioningReceiver)
|
||||
class BasePositioningReceiver(object):
|
||||
"""
|
||||
A base positioning receiver.
|
||||
|
||||
This class would be a good base class for building positioning
|
||||
receivers. It implements the interface (so you don't have to) with stub
|
||||
methods.
|
||||
|
||||
People who want to implement positioning receivers should subclass this
|
||||
class and override the specific callbacks they want to handle.
|
||||
"""
|
||||
def timeReceived(self, time):
|
||||
"""
|
||||
Implements L{IPositioningReceiver.timeReceived} stub.
|
||||
"""
|
||||
|
||||
|
||||
def headingReceived(self, heading):
|
||||
"""
|
||||
Implements L{IPositioningReceiver.headingReceived} stub.
|
||||
"""
|
||||
|
||||
|
||||
def speedReceived(self, speed):
|
||||
"""
|
||||
Implements L{IPositioningReceiver.speedReceived} stub.
|
||||
"""
|
||||
|
||||
|
||||
def climbReceived(self, climb):
|
||||
"""
|
||||
Implements L{IPositioningReceiver.climbReceived} stub.
|
||||
"""
|
||||
|
||||
|
||||
def positionReceived(self, latitude, longitude):
|
||||
"""
|
||||
Implements L{IPositioningReceiver.positionReceived} stub.
|
||||
"""
|
||||
|
||||
|
||||
def positionErrorReceived(self, positionError):
|
||||
"""
|
||||
Implements L{IPositioningReceiver.positioningErrorReceived} stub.
|
||||
"""
|
||||
|
||||
|
||||
def altitudeReceived(self, altitude):
|
||||
"""
|
||||
Implements L{IPositioningReceiver.altitudeReceived} stub.
|
||||
"""
|
||||
|
||||
|
||||
def beaconInformationReceived(self, beaconInformation):
|
||||
"""
|
||||
Implements L{IPositioningReceiver.beaconInformationReceived} stub.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class InvalidSentence(Exception):
|
||||
"""
|
||||
An exception raised when a sentence is invalid.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class InvalidChecksum(Exception):
|
||||
"""
|
||||
An exception raised when the checksum of a sentence is invalid.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class Angle(object, FancyEqMixin):
|
||||
"""
|
||||
An object representing an angle.
|
||||
|
||||
@cvar _RANGE_EXPRESSIONS: A collection of expressions for the allowable
|
||||
range for the angular value of a particular coordinate value.
|
||||
@type _RANGE_EXPRESSIONS: C{dict} of L{Angles} constants to callables
|
||||
@cvar _ANGLE_TYPE_NAMES: English names for angle types.
|
||||
@type _ANGLE_TYPE_NAMES: C{dict} of L{Angles} constants to C{str}
|
||||
"""
|
||||
_RANGE_EXPRESSIONS = {
|
||||
Angles.LATITUDE: lambda latitude: -90.0 < latitude < 90.0,
|
||||
Angles.LONGITUDE: lambda longitude: -180.0 < longitude < 180.0,
|
||||
Angles.HEADING: lambda heading: 0 <= heading < 360,
|
||||
Angles.VARIATION: lambda variation: -180 < variation <= 180,
|
||||
}
|
||||
|
||||
|
||||
_ANGLE_TYPE_NAMES = {
|
||||
Angles.LATITUDE: "Latitude",
|
||||
Angles.LONGITUDE: "Longitude",
|
||||
Angles.VARIATION: "Variation",
|
||||
Angles.HEADING: "Heading",
|
||||
}
|
||||
|
||||
|
||||
compareAttributes = 'angleType', 'inDecimalDegrees'
|
||||
|
||||
|
||||
def __init__(self, angle=None, angleType=None):
|
||||
"""
|
||||
Initializes an angle.
|
||||
|
||||
@param angle: The value of the angle in decimal degrees. (C{None} if
|
||||
unknown).
|
||||
@type angle: C{float} or C{NoneType}
|
||||
@param angleType: A symbolic constant describing the angle type. Should
|
||||
be one of L{AngleTypes} or {None} if unknown.
|
||||
|
||||
@raises ValueError: If the angle type is not the default argument,
|
||||
but it is an unknown type (not in C{Angle._RANGE_EXPRESSIONS}),
|
||||
or it is a known type but the supplied value was out of the
|
||||
allowable range for said type.
|
||||
"""
|
||||
if angleType is not None and angleType not in self._RANGE_EXPRESSIONS:
|
||||
raise ValueError("Unknown angle type")
|
||||
|
||||
if angle is not None and angleType is not None:
|
||||
rangeExpression = self._RANGE_EXPRESSIONS[angleType]
|
||||
if not rangeExpression(angle):
|
||||
template = "Angle {0} not in allowed range for type {1}"
|
||||
raise ValueError(template.format(angle, angleType))
|
||||
|
||||
self.angleType = angleType
|
||||
self._angle = angle
|
||||
|
||||
|
||||
@property
|
||||
def inDecimalDegrees(self):
|
||||
"""
|
||||
The value of this angle in decimal degrees. This value is immutable.
|
||||
|
||||
@return: This angle expressed in decimal degrees, or C{None} if the
|
||||
angle is unknown.
|
||||
@rtype: C{float} (or C{NoneType})
|
||||
"""
|
||||
return self._angle
|
||||
|
||||
|
||||
@property
|
||||
def inDegreesMinutesSeconds(self):
|
||||
"""
|
||||
The value of this angle as a degrees, minutes, seconds tuple. This
|
||||
value is immutable.
|
||||
|
||||
@return: This angle expressed in degrees, minutes, seconds. C{None} if
|
||||
the angle is unknown.
|
||||
@rtype: 3-C{tuple} of C{int} (or C{NoneType})
|
||||
"""
|
||||
if self._angle is None:
|
||||
return None
|
||||
|
||||
degrees = abs(int(self._angle))
|
||||
fractionalDegrees = abs(self._angle - int(self._angle))
|
||||
decimalMinutes = 60 * fractionalDegrees
|
||||
|
||||
minutes = int(decimalMinutes)
|
||||
fractionalMinutes = decimalMinutes - int(decimalMinutes)
|
||||
decimalSeconds = 60 * fractionalMinutes
|
||||
|
||||
return degrees, minutes, int(decimalSeconds)
|
||||
|
||||
|
||||
def setSign(self, sign):
|
||||
"""
|
||||
Sets the sign of this angle.
|
||||
|
||||
@param sign: The new sign. C{1} for positive and C{-1} for negative
|
||||
signs, respectively.
|
||||
@type sign: C{int}
|
||||
|
||||
@raise ValueError: If the C{sign} parameter is not C{-1} or C{1}.
|
||||
"""
|
||||
if sign not in (-1, 1):
|
||||
raise ValueError("bad sign (got %s, expected -1 or 1)" % sign)
|
||||
|
||||
self._angle = sign * abs(self._angle)
|
||||
|
||||
|
||||
def __float__(self):
|
||||
"""
|
||||
Returns this angle as a float.
|
||||
|
||||
@return: The float value of this angle, expressed in degrees.
|
||||
@rtype: C{float}
|
||||
"""
|
||||
return self._angle
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Returns a string representation of this angle.
|
||||
|
||||
@return: The string representation.
|
||||
@rtype: C{str}
|
||||
"""
|
||||
return "<{s._angleTypeNameRepr} ({s._angleValueRepr})>".format(s=self)
|
||||
|
||||
|
||||
@property
|
||||
def _angleValueRepr(self):
|
||||
"""
|
||||
Returns a string representation of the angular value of this angle.
|
||||
|
||||
This is a helper function for the actual C{__repr__}.
|
||||
|
||||
@return: The string representation.
|
||||
@rtype: C{str}
|
||||
"""
|
||||
if self.inDecimalDegrees is not None:
|
||||
return "%s degrees" % round(self.inDecimalDegrees, 2)
|
||||
else:
|
||||
return "unknown value"
|
||||
|
||||
|
||||
@property
|
||||
def _angleTypeNameRepr(self):
|
||||
"""
|
||||
Returns a string representation of the type of this angle.
|
||||
|
||||
This is a helper function for the actual C{__repr__}.
|
||||
|
||||
@return: The string representation.
|
||||
@rtype: C{str}
|
||||
"""
|
||||
try:
|
||||
return self._ANGLE_TYPE_NAMES[self.angleType]
|
||||
except KeyError:
|
||||
return "Angle of unknown type"
|
||||
|
||||
|
||||
|
||||
class Heading(Angle):
|
||||
"""
|
||||
The heading of a mobile object.
|
||||
|
||||
@ivar variation: The (optional) magnetic variation.
|
||||
The sign of the variation is positive for variations towards the east
|
||||
(clockwise from north), and negative for variations towards the west
|
||||
(counterclockwise from north).
|
||||
If the variation is unknown or not applicable, this is C{None}.
|
||||
@type variation: C{Angle} or C{NoneType}.
|
||||
@ivar correctedHeading: The heading, corrected for variation. If the
|
||||
variation is unknown (C{None}), is None. This attribute is read-only
|
||||
(its value is determined by the angle and variation attributes). The
|
||||
value is coerced to being between 0 (inclusive) and 360 (exclusive).
|
||||
"""
|
||||
def __init__(self, angle=None, variation=None):
|
||||
"""
|
||||
Initializes a angle with an optional variation.
|
||||
"""
|
||||
Angle.__init__(self, angle, Angles.HEADING)
|
||||
self.variation = variation
|
||||
|
||||
|
||||
@classmethod
|
||||
def fromFloats(cls, angleValue=None, variationValue=None):
|
||||
"""
|
||||
Constructs a Heading from the float values of the angle and variation.
|
||||
|
||||
@param angleValue: The angle value of this heading.
|
||||
@type angleValue: C{float}
|
||||
@param variationValue: The value of the variation of this heading.
|
||||
@type variationValue: C{float}
|
||||
@return A C{Heading } with the given values.
|
||||
"""
|
||||
variation = Angle(variationValue, Angles.VARIATION)
|
||||
return cls(angleValue, variation)
|
||||
|
||||
|
||||
@property
|
||||
def correctedHeading(self):
|
||||
"""
|
||||
Corrects the heading by the given variation. This is sometimes known as
|
||||
the true heading.
|
||||
|
||||
@return: The heading, corrected by the variation. If the variation or
|
||||
the angle are unknown, returns C{None}.
|
||||
@rtype: C{float} or C{NoneType}
|
||||
"""
|
||||
if self._angle is None or self.variation is None:
|
||||
return None
|
||||
|
||||
angle = (self.inDecimalDegrees - self.variation.inDecimalDegrees) % 360
|
||||
return Angle(angle, Angles.HEADING)
|
||||
|
||||
|
||||
def setSign(self, sign):
|
||||
"""
|
||||
Sets the sign of the variation of this heading.
|
||||
|
||||
@param sign: The new sign. C{1} for positive and C{-1} for negative
|
||||
signs, respectively.
|
||||
@type sign: C{int}
|
||||
|
||||
@raise ValueError: If the C{sign} parameter is not C{-1} or C{1}.
|
||||
"""
|
||||
if self.variation.inDecimalDegrees is None:
|
||||
raise ValueError("can't set the sign of an unknown variation")
|
||||
|
||||
self.variation.setSign(sign)
|
||||
|
||||
|
||||
compareAttributes = list(Angle.compareAttributes) + ["variation"]
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Returns a string representation of this angle.
|
||||
|
||||
@return: The string representation.
|
||||
@rtype: C{str}
|
||||
"""
|
||||
if self.variation is None:
|
||||
variationRepr = "unknown variation"
|
||||
else:
|
||||
variationRepr = repr(self.variation)
|
||||
|
||||
return "<%s (%s, %s)>" % (
|
||||
self._angleTypeNameRepr, self._angleValueRepr, variationRepr)
|
||||
|
||||
|
||||
|
||||
class Coordinate(Angle):
|
||||
"""
|
||||
A coordinate.
|
||||
|
||||
@ivar angle: The value of the coordinate in decimal degrees, with the usual
|
||||
rules for sign (northern and eastern hemispheres are positive, southern
|
||||
and western hemispheres are negative).
|
||||
@type angle: C{float}
|
||||
"""
|
||||
def __init__(self, angle, coordinateType=None):
|
||||
"""
|
||||
Initializes a coordinate.
|
||||
|
||||
@param angle: The angle of this coordinate in decimal degrees. The
|
||||
hemisphere is determined by the sign (north and east are positive).
|
||||
If this coordinate describes a latitude, this value must be within
|
||||
-90.0 and +90.0 (exclusive). If this value describes a longitude,
|
||||
this value must be within -180.0 and +180.0 (exclusive).
|
||||
@type angle: C{float}
|
||||
@param coordinateType: The coordinate type. One of L{Angles.LATITUDE},
|
||||
L{Angles.LONGITUDE} or C{None} if unknown.
|
||||
"""
|
||||
if coordinateType not in [Angles.LATITUDE, Angles.LONGITUDE, None]:
|
||||
raise ValueError("coordinateType must be one of Angles.LATITUDE, "
|
||||
"Angles.LONGITUDE or None, was {!r}"
|
||||
.format(coordinateType))
|
||||
|
||||
Angle.__init__(self, angle, coordinateType)
|
||||
|
||||
|
||||
@property
|
||||
def hemisphere(self):
|
||||
"""
|
||||
Gets the hemisphere of this coordinate.
|
||||
|
||||
@return: A symbolic constant representing a hemisphere (one of
|
||||
L{Angles})
|
||||
"""
|
||||
|
||||
if self.angleType is Angles.LATITUDE:
|
||||
if self.inDecimalDegrees < 0:
|
||||
return Directions.SOUTH
|
||||
else:
|
||||
return Directions.NORTH
|
||||
elif self.angleType is Angles.LONGITUDE:
|
||||
if self.inDecimalDegrees < 0:
|
||||
return Directions.WEST
|
||||
else:
|
||||
return Directions.EAST
|
||||
else:
|
||||
raise ValueError("unknown coordinate type (cant find hemisphere)")
|
||||
|
||||
|
||||
|
||||
class Altitude(object, FancyEqMixin):
|
||||
"""
|
||||
An altitude.
|
||||
|
||||
@ivar inMeters: The altitude represented by this object, in meters. This
|
||||
attribute is read-only.
|
||||
@type inMeters: C{float}
|
||||
|
||||
@ivar inFeet: As above, but expressed in feet.
|
||||
@type inFeet: C{float}
|
||||
"""
|
||||
compareAttributes = 'inMeters',
|
||||
|
||||
def __init__(self, altitude):
|
||||
"""
|
||||
Initializes an altitude.
|
||||
|
||||
@param altitude: The altitude in meters.
|
||||
@type altitude: C{float}
|
||||
"""
|
||||
self._altitude = altitude
|
||||
|
||||
|
||||
@property
|
||||
def inFeet(self):
|
||||
"""
|
||||
Gets the altitude this object represents, in feet.
|
||||
|
||||
@return: The altitude, expressed in feet.
|
||||
@rtype: C{float}
|
||||
"""
|
||||
return self._altitude / METERS_PER_FOOT
|
||||
|
||||
|
||||
@property
|
||||
def inMeters(self):
|
||||
"""
|
||||
Returns the altitude this object represents, in meters.
|
||||
|
||||
@return: The altitude, expressed in feet.
|
||||
@rtype: C{float}
|
||||
"""
|
||||
return self._altitude
|
||||
|
||||
|
||||
def __float__(self):
|
||||
"""
|
||||
Returns the altitude represented by this object expressed in meters.
|
||||
|
||||
@return: The altitude represented by this object, expressed in meters.
|
||||
@rtype: C{float}
|
||||
"""
|
||||
return self._altitude
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Returns a string representation of this altitude.
|
||||
|
||||
@return: The string representation.
|
||||
@rtype: C{str}
|
||||
"""
|
||||
return "<Altitude (%s m)>" % (self._altitude,)
|
||||
|
||||
|
||||
|
||||
class _BaseSpeed(object, FancyEqMixin):
|
||||
"""
|
||||
An object representing the abstract concept of the speed (rate of
|
||||
movement) of a mobile object.
|
||||
|
||||
This primarily has behavior for converting between units and comparison.
|
||||
"""
|
||||
compareAttributes = 'inMetersPerSecond',
|
||||
|
||||
def __init__(self, speed):
|
||||
"""
|
||||
Initializes a speed.
|
||||
|
||||
@param speed: The speed that this object represents, expressed in
|
||||
meters per second.
|
||||
@type speed: C{float}
|
||||
|
||||
@raises ValueError: Raised if value was invalid for this particular
|
||||
kind of speed. Only happens in subclasses.
|
||||
"""
|
||||
self._speed = speed
|
||||
|
||||
|
||||
@property
|
||||
def inMetersPerSecond(self):
|
||||
"""
|
||||
The speed that this object represents, expressed in meters per second.
|
||||
This attribute is immutable.
|
||||
|
||||
@return: The speed this object represents, in meters per second.
|
||||
@rtype: C{float}
|
||||
"""
|
||||
return self._speed
|
||||
|
||||
|
||||
@property
|
||||
def inKnots(self):
|
||||
"""
|
||||
Returns the speed represented by this object, expressed in knots. This
|
||||
attribute is immutable.
|
||||
|
||||
@return: The speed this object represents, in knots.
|
||||
@rtype: C{float}
|
||||
"""
|
||||
return self._speed / MPS_PER_KNOT
|
||||
|
||||
|
||||
def __float__(self):
|
||||
"""
|
||||
Returns the speed represented by this object expressed in meters per
|
||||
second.
|
||||
|
||||
@return: The speed represented by this object, expressed in meters per
|
||||
second.
|
||||
@rtype: C{float}
|
||||
"""
|
||||
return self._speed
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Returns a string representation of this speed object.
|
||||
|
||||
@return: The string representation.
|
||||
@rtype: C{str}
|
||||
"""
|
||||
speedValue = round(self.inMetersPerSecond, 2)
|
||||
return "<%s (%s m/s)>" % (self.__class__.__name__, speedValue)
|
||||
|
||||
|
||||
|
||||
class Speed(_BaseSpeed):
|
||||
"""
|
||||
The speed (rate of movement) of a mobile object.
|
||||
"""
|
||||
def __init__(self, speed):
|
||||
"""
|
||||
Initializes a L{Speed} object.
|
||||
|
||||
@param speed: The speed that this object represents, expressed in
|
||||
meters per second.
|
||||
@type speed: C{float}
|
||||
|
||||
@raises ValueError: Raised if C{speed} is negative.
|
||||
"""
|
||||
if speed < 0:
|
||||
raise ValueError("negative speed: %r" % (speed,))
|
||||
|
||||
_BaseSpeed.__init__(self, speed)
|
||||
|
||||
|
||||
|
||||
class Climb(_BaseSpeed):
|
||||
"""
|
||||
The climb ("vertical speed") of an object.
|
||||
"""
|
||||
def __init__(self, climb):
|
||||
"""
|
||||
Initializes a L{Climb} object.
|
||||
|
||||
@param climb: The climb that this object represents, expressed in
|
||||
meters per second.
|
||||
@type climb: C{float}
|
||||
"""
|
||||
_BaseSpeed.__init__(self, climb)
|
||||
|
||||
|
||||
|
||||
class PositionError(object, FancyEqMixin):
|
||||
"""
|
||||
Position error information.
|
||||
|
||||
@cvar _ALLOWABLE_THRESHOLD: The maximum allowable difference between PDOP
|
||||
and the geometric mean of VDOP and HDOP. That difference is supposed
|
||||
to be zero, but can be non-zero because of rounding error and limited
|
||||
reporting precision. You should never have to change this value.
|
||||
@type _ALLOWABLE_THRESHOLD: C{float}
|
||||
@cvar _DOP_EXPRESSIONS: A mapping of DOP types (C[hvp]dop) to a list of
|
||||
callables that take self and return that DOP type, or raise
|
||||
C{TypeError}. This allows a DOP value to either be returned directly
|
||||
if it's know, or computed from other DOP types if it isn't.
|
||||
@type _DOP_EXPRESSIONS: C{dict} of C{str} to callables
|
||||
@ivar pdop: The position dilution of precision. C{None} if unknown.
|
||||
@type pdop: C{float} or C{NoneType}
|
||||
@ivar hdop: The horizontal dilution of precision. C{None} if unknown.
|
||||
@type hdop: C{float} or C{NoneType}
|
||||
@ivar vdop: The vertical dilution of precision. C{None} if unknown.
|
||||
@type vdop: C{float} or C{NoneType}
|
||||
"""
|
||||
compareAttributes = 'pdop', 'hdop', 'vdop'
|
||||
|
||||
def __init__(self, pdop=None, hdop=None, vdop=None, testInvariant=False):
|
||||
"""
|
||||
Initializes a positioning error object.
|
||||
|
||||
@param pdop: The position dilution of precision. C{None} if unknown.
|
||||
@type pdop: C{float} or C{NoneType}
|
||||
@param hdop: The horizontal dilution of precision. C{None} if unknown.
|
||||
@type hdop: C{float} or C{NoneType}
|
||||
@param vdop: The vertical dilution of precision. C{None} if unknown.
|
||||
@type vdop: C{float} or C{NoneType}
|
||||
@param testInvariant: Flag to test if the DOP invariant is valid or
|
||||
not. If C{True}, the invariant (PDOP = (HDOP**2 + VDOP**2)*.5) is
|
||||
checked at every mutation. By default, this is false, because the
|
||||
vast majority of DOP-providing devices ignore this invariant.
|
||||
@type testInvariant: c{bool}
|
||||
"""
|
||||
self._pdop = pdop
|
||||
self._hdop = hdop
|
||||
self._vdop = vdop
|
||||
|
||||
self._testInvariant = testInvariant
|
||||
self._testDilutionOfPositionInvariant()
|
||||
|
||||
|
||||
_ALLOWABLE_TRESHOLD = 0.01
|
||||
|
||||
|
||||
def _testDilutionOfPositionInvariant(self):
|
||||
"""
|
||||
Tests if this positioning error object satisfies the dilution of
|
||||
position invariant (PDOP = (HDOP**2 + VDOP**2)*.5), unless the
|
||||
C{self._testInvariant} instance variable is C{False}.
|
||||
|
||||
@return: C{None} if the invariant was not satisfied or not tested.
|
||||
@raises ValueError: Raised if the invariant was tested but not
|
||||
satisfied.
|
||||
"""
|
||||
if not self._testInvariant:
|
||||
return
|
||||
|
||||
for x in (self.pdop, self.hdop, self.vdop):
|
||||
if x is None:
|
||||
return
|
||||
|
||||
delta = abs(self.pdop - (self.hdop**2 + self.vdop**2)**.5)
|
||||
if delta > self._ALLOWABLE_TRESHOLD:
|
||||
raise ValueError("invalid combination of dilutions of precision: "
|
||||
"position: %s, horizontal: %s, vertical: %s"
|
||||
% (self.pdop, self.hdop, self.vdop))
|
||||
|
||||
|
||||
_DOP_EXPRESSIONS = {
|
||||
'pdop': [
|
||||
lambda self: float(self._pdop),
|
||||
lambda self: (self._hdop**2 + self._vdop**2)**.5,
|
||||
],
|
||||
|
||||
'hdop': [
|
||||
lambda self: float(self._hdop),
|
||||
lambda self: (self._pdop**2 - self._vdop**2)**.5,
|
||||
],
|
||||
|
||||
'vdop': [
|
||||
lambda self: float(self._vdop),
|
||||
lambda self: (self._pdop**2 - self._hdop**2)**.5,
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _getDOP(self, dopType):
|
||||
"""
|
||||
Gets a particular dilution of position value.
|
||||
|
||||
@param dopType: The type of dilution of position to get. One of
|
||||
('pdop', 'hdop', 'vdop').
|
||||
@type dopType: C{str}
|
||||
@return: The DOP if it is known, C{None} otherwise.
|
||||
@rtype: C{float} or C{NoneType}
|
||||
"""
|
||||
for dopExpression in self._DOP_EXPRESSIONS[dopType]:
|
||||
try:
|
||||
return dopExpression(self)
|
||||
except TypeError:
|
||||
continue
|
||||
|
||||
|
||||
def _setDOP(self, dopType, value):
|
||||
"""
|
||||
Sets a particular dilution of position value.
|
||||
|
||||
@param dopType: The type of dilution of position to set. One of
|
||||
('pdop', 'hdop', 'vdop').
|
||||
@type dopType: C{str}
|
||||
|
||||
@param value: The value to set the dilution of position type to.
|
||||
@type value: C{float}
|
||||
|
||||
If this position error tests dilution of precision invariants,
|
||||
it will be checked. If the invariant is not satisfied, the
|
||||
assignment will be undone and C{ValueError} is raised.
|
||||
"""
|
||||
attributeName = "_" + dopType
|
||||
|
||||
oldValue = getattr(self, attributeName)
|
||||
setattr(self, attributeName, float(value))
|
||||
|
||||
try:
|
||||
self._testDilutionOfPositionInvariant()
|
||||
except ValueError:
|
||||
setattr(self, attributeName, oldValue)
|
||||
raise
|
||||
|
||||
|
||||
pdop = property(fget=lambda self: self._getDOP('pdop'),
|
||||
fset=lambda self, value: self._setDOP('pdop', value))
|
||||
|
||||
|
||||
hdop = property(fget=lambda self: self._getDOP('hdop'),
|
||||
fset=lambda self, value: self._setDOP('hdop', value))
|
||||
|
||||
|
||||
vdop = property(fget=lambda self: self._getDOP('vdop'),
|
||||
fset=lambda self, value: self._setDOP('vdop', value))
|
||||
|
||||
|
||||
_REPR_TEMPLATE = "<PositionError (pdop: %s, hdop: %s, vdop: %s)>"
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Returns a string representation of positioning information object.
|
||||
|
||||
@return: The string representation.
|
||||
@rtype: C{str}
|
||||
"""
|
||||
return self._REPR_TEMPLATE % (self.pdop, self.hdop, self.vdop)
|
||||
|
||||
|
||||
|
||||
class BeaconInformation(object):
|
||||
"""
|
||||
Information about positioning beacons (a generalized term for the reference
|
||||
objects that help you determine your position, such as satellites or cell
|
||||
towers).
|
||||
|
||||
@ivar seenBeacons: A set of visible beacons. Note that visible beacons are not
|
||||
necessarily used in acquiring a positioning fix.
|
||||
@type seenBeacons: C{set} of L{IPositioningBeacon}
|
||||
@ivar usedBeacons: An set of the beacons that were used in obtaining a
|
||||
positioning fix. This only contains beacons that are actually used, not
|
||||
beacons for which it is unknown if they are used or not.
|
||||
@type usedBeacons: C{set} of L{IPositioningBeacon}
|
||||
"""
|
||||
def __init__(self, seenBeacons=()):
|
||||
"""
|
||||
Initializes a beacon information object.
|
||||
|
||||
@param seenBeacons: A collection of beacons that are currently seen.
|
||||
@type seenBeacons: iterable of L{IPositioningBeacon}s
|
||||
"""
|
||||
self.seenBeacons = set(seenBeacons)
|
||||
self.usedBeacons = set()
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Returns a string representation of this beacon information object.
|
||||
|
||||
The beacons are sorted by their identifier.
|
||||
|
||||
@return: The string representation.
|
||||
@rtype: C{str}
|
||||
"""
|
||||
sortedBeacons = partial(sorted, key=attrgetter("identifier"))
|
||||
|
||||
usedBeacons = sortedBeacons(self.usedBeacons)
|
||||
unusedBeacons = sortedBeacons(self.seenBeacons - self.usedBeacons)
|
||||
|
||||
template = ("<BeaconInformation ("
|
||||
"used beacons ({numUsed}): {usedBeacons}, "
|
||||
"unused beacons: {unusedBeacons})>")
|
||||
|
||||
formatted = template.format(numUsed=len(self.usedBeacons),
|
||||
usedBeacons=usedBeacons,
|
||||
unusedBeacons=unusedBeacons)
|
||||
|
||||
return formatted
|
||||
|
||||
|
||||
|
||||
@implementer(ipositioning.IPositioningBeacon)
|
||||
class PositioningBeacon(object):
|
||||
"""
|
||||
A positioning beacon.
|
||||
|
||||
@ivar identifier: The unique identifier for this beacon. This is usually
|
||||
an integer. For GPS, this is also known as the PRN.
|
||||
@type identifier: Pretty much anything that can be used as a unique
|
||||
identifier. Depends on the implementation.
|
||||
"""
|
||||
def __init__(self, identifier):
|
||||
"""
|
||||
Initializes a positioning beacon.
|
||||
|
||||
@param identifier: The identifier for this beacon.
|
||||
@type identifier: Can be pretty much anything (see ivar documentation).
|
||||
"""
|
||||
self.identifier = identifier
|
||||
|
||||
|
||||
def __hash__(self):
|
||||
"""
|
||||
Returns the hash of the identifier for this beacon.
|
||||
|
||||
@return: The hash of the identifier. (C{hash(self.identifier)})
|
||||
@rtype: C{int}
|
||||
"""
|
||||
return hash(self.identifier)
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Returns a string representation of this beacon.
|
||||
|
||||
@return: The string representation.
|
||||
@rtype: C{str}
|
||||
"""
|
||||
return "<Beacon ({s.identifier})>".format(s=self)
|
||||
|
||||
|
||||
|
||||
class Satellite(PositioningBeacon):
|
||||
"""
|
||||
A satellite.
|
||||
|
||||
@ivar azimuth: The azimuth of the satellite. This is the heading (positive
|
||||
angle relative to true north) where the satellite appears to be to the
|
||||
device.
|
||||
@ivar elevation: The (positive) angle above the horizon where this
|
||||
satellite appears to be to the device.
|
||||
@ivar signalToNoiseRatio: The signal to noise ratio of the signal coming
|
||||
from this satellite.
|
||||
"""
|
||||
def __init__(self,
|
||||
identifier,
|
||||
azimuth=None,
|
||||
elevation=None,
|
||||
signalToNoiseRatio=None):
|
||||
"""
|
||||
Initializes a satellite object.
|
||||
|
||||
@param identifier: The PRN (unique identifier) of this satellite.
|
||||
@type identifier: C{int}
|
||||
@param azimuth: The azimuth of the satellite (see instance variable
|
||||
documentation).
|
||||
@type azimuth: C{float}
|
||||
@param elevation: The elevation of the satellite (see instance variable
|
||||
documentation).
|
||||
@type elevation: C{float}
|
||||
@param signalToNoiseRatio: The signal to noise ratio of the connection
|
||||
to this satellite (see instance variable documentation).
|
||||
@type signalToNoiseRatio: C{float}
|
||||
"""
|
||||
PositioningBeacon.__init__(self, int(identifier))
|
||||
|
||||
self.azimuth = azimuth
|
||||
self.elevation = elevation
|
||||
self.signalToNoiseRatio = signalToNoiseRatio
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Returns a string representation of this Satellite.
|
||||
|
||||
@return: The string representation.
|
||||
@rtype: C{str}
|
||||
"""
|
||||
template = ("<Satellite ({s.identifier}), "
|
||||
"azimuth: {s.azimuth}, "
|
||||
"elevation: {s.elevation}, "
|
||||
"snr: {s.signalToNoiseRatio}>")
|
||||
|
||||
return template.format(s=self)
|
||||
|
||||
|
||||
|
||||
__all__ = [
|
||||
'Altitude',
|
||||
'Angle',
|
||||
'Angles',
|
||||
'BasePositioningReceiver',
|
||||
'BeaconInformation',
|
||||
'Climb',
|
||||
'Coordinate',
|
||||
'Directions',
|
||||
'Heading',
|
||||
'InvalidChecksum',
|
||||
'InvalidSentence',
|
||||
'METERS_PER_FOOT',
|
||||
'MPS_PER_KNOT',
|
||||
'MPS_PER_KPH',
|
||||
'PositionError',
|
||||
'PositioningBeacon',
|
||||
'Satellite',
|
||||
'Speed'
|
||||
]
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
"""
|
||||
Positioning interfaces.
|
||||
|
||||
@since: 14.0
|
||||
"""
|
||||
from zope.interface import Attribute, Interface
|
||||
|
||||
|
||||
class IPositioningReceiver(Interface):
|
||||
"""
|
||||
An interface for positioning providers.
|
||||
"""
|
||||
def positionReceived(latitude, longitude):
|
||||
"""
|
||||
Method called when a position is received.
|
||||
|
||||
@param latitude: The latitude of the received position.
|
||||
@type latitude: L{twisted.positioning.base.Coordinate}
|
||||
@param longitude: The longitude of the received position.
|
||||
@type longitude: L{twisted.positioning.base.Coordinate}
|
||||
"""
|
||||
|
||||
|
||||
def positionErrorReceived(positionError):
|
||||
"""
|
||||
Method called when position error is received.
|
||||
|
||||
@param positioningError: The position error.
|
||||
@type positioningError: L{twisted.positioning.base.PositionError}
|
||||
"""
|
||||
|
||||
def timeReceived(time):
|
||||
"""
|
||||
Method called when time and date information arrives.
|
||||
|
||||
@param time: The date and time (expressed in UTC unless otherwise
|
||||
specified).
|
||||
@type time: L{datetime.datetime}
|
||||
"""
|
||||
|
||||
|
||||
def headingReceived(heading):
|
||||
"""
|
||||
Method called when a true heading is received.
|
||||
|
||||
@param heading: The heading.
|
||||
@type heading: L{twisted.positioning.base.Heading}
|
||||
"""
|
||||
|
||||
|
||||
def altitudeReceived(altitude):
|
||||
"""
|
||||
Method called when an altitude is received.
|
||||
|
||||
@param altitude: The altitude.
|
||||
@type altitude: L{twisted.positioning.base.Altitude}
|
||||
"""
|
||||
|
||||
|
||||
def speedReceived(speed):
|
||||
"""
|
||||
Method called when the speed is received.
|
||||
|
||||
@param speed: The speed of a mobile object.
|
||||
@type speed: L{twisted.positioning.base.Speed}
|
||||
"""
|
||||
|
||||
|
||||
def climbReceived(climb):
|
||||
"""
|
||||
Method called when the climb is received.
|
||||
|
||||
@param climb: The climb of the mobile object.
|
||||
@type climb: L{twisted.positioning.base.Climb}
|
||||
"""
|
||||
|
||||
def beaconInformationReceived(beaconInformation):
|
||||
"""
|
||||
Method called when positioning beacon information is received.
|
||||
|
||||
@param beaconInformation: The beacon information.
|
||||
@type beaconInformation: L{twisted.positioning.base.BeaconInformation}
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class IPositioningBeacon(Interface):
|
||||
"""
|
||||
A positioning beacon.
|
||||
"""
|
||||
identifier = Attribute(
|
||||
"""
|
||||
A unique identifier for this beacon. The type is dependant on the
|
||||
implementation, but must be immutable.
|
||||
""")
|
||||
|
||||
|
||||
|
||||
class INMEAReceiver(Interface):
|
||||
"""
|
||||
An object that can receive NMEA data.
|
||||
"""
|
||||
def sentenceReceived(sentence):
|
||||
"""
|
||||
Method called when a sentence is received.
|
||||
|
||||
@param sentence: The received NMEA sentence.
|
||||
@type L{twisted.positioning.nmea.NMEASentence}
|
||||
"""
|
||||
|
||||
|
||||
|
||||
__all__ = [
|
||||
"IPositioningReceiver",
|
||||
"IPositioningBeacon",
|
||||
"INMEAReceiver"
|
||||
]
|
||||
980
Linux/lib/python2.7/site-packages/twisted/positioning/nmea.py
Normal file
980
Linux/lib/python2.7/site-packages/twisted/positioning/nmea.py
Normal file
|
|
@ -0,0 +1,980 @@
|
|||
# -*- test-case-name: twisted.positioning.test.test_nmea -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
"""
|
||||
Classes for working with NMEA 0183 sentence producing devices.
|
||||
This standard is generally just called "NMEA", which is actually the
|
||||
name of the body that produces the standard, not the standard itself..
|
||||
|
||||
For more information, read the blog post on NMEA by ESR (the gpsd
|
||||
maintainer) at U{http://esr.ibiblio.org/?p=801}. Unfortunately,
|
||||
official specifications on NMEA 0183 are only available at a cost.
|
||||
|
||||
More information can be found on the Wikipedia page:
|
||||
U{https://en.wikipedia.org/wiki/NMEA_0183}.
|
||||
|
||||
The official standard may be obtained through the NMEA's website:
|
||||
U{http://www.nmea.org/content/nmea_standards/nmea_0183_v_410.asp}.
|
||||
|
||||
@since: 14.0
|
||||
"""
|
||||
import itertools
|
||||
import operator
|
||||
import datetime
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.positioning import base, ipositioning, _sentence
|
||||
from twisted.positioning.base import Angles
|
||||
from twisted.protocols.basic import LineReceiver
|
||||
from twisted.python.constants import Values, ValueConstant
|
||||
from twisted.python.compat import reduce
|
||||
|
||||
|
||||
class GPGGAFixQualities(Values):
|
||||
"""
|
||||
The possible fix quality indications for GPGGA sentences.
|
||||
|
||||
@cvar INVALID_FIX: The fix is invalid.
|
||||
@cvar GPS_FIX: There is a fix, acquired using GPS.
|
||||
@cvar DGPS_FIX: There is a fix, acquired using differential GPS (DGPS).
|
||||
@cvar PPS_FIX: There is a fix, acquired using the precise positioning
|
||||
service (PPS).
|
||||
@cvar RTK_FIX: There is a fix, acquired using fixed real-time
|
||||
kinematics. This means that there was a sufficient number of shared
|
||||
satellites with the base station, usually yielding a resolution in
|
||||
the centimeter range. This was added in NMEA 0183 version 3.0. This
|
||||
is also called Carrier-Phase Enhancement or CPGPS, particularly when
|
||||
used in combination with GPS.
|
||||
@cvar FLOAT_RTK_FIX: There is a fix, acquired using floating real-time
|
||||
kinematics. The same comments apply as for a fixed real-time
|
||||
kinematics fix, except that there were insufficient shared satellites
|
||||
to acquire it, so instead you got a slightly less good floating fix.
|
||||
Typical resolution in the decimeter range.
|
||||
@cvar DEAD_RECKONING: There is currently no more fix, but this data was
|
||||
computed using a previous fix and some information about motion
|
||||
(either from that fix or from other sources) using simple dead
|
||||
reckoning. Not particularly reliable, but better-than-nonsense data.
|
||||
@cvar MANUAL: There is no real fix from this device, but the location has
|
||||
been manually entered, presumably with data obtained from some other
|
||||
positioning method.
|
||||
@cvar SIMULATED: There is no real fix, but instead it is being simulated.
|
||||
"""
|
||||
INVALID_FIX = "0"
|
||||
GPS_FIX = "1"
|
||||
DGPS_FIX = "2"
|
||||
PPS_FIX = "3"
|
||||
RTK_FIX = "4"
|
||||
FLOAT_RTK_FIX = "5"
|
||||
DEAD_RECKONING = "6"
|
||||
MANUAL = "7"
|
||||
SIMULATED = "8"
|
||||
|
||||
|
||||
|
||||
class GPGLLGPRMCFixQualities(Values):
|
||||
"""
|
||||
The possible fix quality indications in GPGLL and GPRMC sentences.
|
||||
|
||||
Unfortunately, these sentences only indicate whether data is good or void.
|
||||
They provide no other information, such as what went wrong if the data is
|
||||
void, or how good the data is if the data is not void.
|
||||
|
||||
@cvar ACTIVE: The data is okay.
|
||||
@cvar VOID: The data is void, and should not be used.
|
||||
"""
|
||||
ACTIVE = ValueConstant("A")
|
||||
VOID = ValueConstant("V")
|
||||
|
||||
|
||||
|
||||
class GPGSAFixTypes(Values):
|
||||
"""
|
||||
The possible fix types of a GPGSA sentence.
|
||||
|
||||
@cvar GSA_NO_FIX: The sentence reports no fix at all.
|
||||
@cvar GSA_2D_FIX: The sentence reports a 2D fix: position but no altitude.
|
||||
@cvar GSA_3D_FIX: The sentence reports a 3D fix: position with altitude.
|
||||
"""
|
||||
GSA_NO_FIX = ValueConstant("1")
|
||||
GSA_2D_FIX = ValueConstant("2")
|
||||
GSA_3D_FIX = ValueConstant("3")
|
||||
|
||||
|
||||
|
||||
def _split(sentence):
|
||||
"""
|
||||
Returns the split version of an NMEA sentence, minus header
|
||||
and checksum.
|
||||
|
||||
@param sentence: The NMEA sentence to split.
|
||||
@type sentence: C{str}
|
||||
|
||||
>>> _split("$GPGGA,spam,eggs*00")
|
||||
['GPGGA', 'spam', 'eggs']
|
||||
"""
|
||||
if sentence[-3] == "*": # Sentence with checksum
|
||||
return sentence[1:-3].split(',')
|
||||
elif sentence[-1] == "*": # Sentence without checksum
|
||||
return sentence[1:-1].split(',')
|
||||
else:
|
||||
raise base.InvalidSentence("malformed sentence %s" % (sentence,))
|
||||
|
||||
|
||||
|
||||
def _validateChecksum(sentence):
|
||||
"""
|
||||
Validates the checksum of an NMEA sentence.
|
||||
|
||||
@param sentence: The NMEA sentence to check the checksum of.
|
||||
@type sentence: C{str}
|
||||
|
||||
@raise ValueError: If the sentence has an invalid checksum.
|
||||
|
||||
Simply returns on sentences that either don't have a checksum,
|
||||
or have a valid checksum.
|
||||
"""
|
||||
if sentence[-3] == '*': # Sentence has a checksum
|
||||
reference, source = int(sentence[-2:], 16), sentence[1:-3]
|
||||
computed = reduce(operator.xor, (ord(x) for x in source))
|
||||
if computed != reference:
|
||||
raise base.InvalidChecksum("%02x != %02x" % (computed, reference))
|
||||
|
||||
|
||||
|
||||
class NMEAProtocol(LineReceiver, _sentence._PositioningSentenceProducerMixin):
|
||||
"""
|
||||
A protocol that parses and verifies the checksum of an NMEA sentence (in
|
||||
string form, not L{NMEASentence}), and delegates to a receiver.
|
||||
|
||||
It receives lines and verifies these lines are NMEA sentences. If
|
||||
they are, verifies their checksum and unpacks them into their
|
||||
components. It then wraps them in L{NMEASentence} objects and
|
||||
calls the appropriate receiver method with them.
|
||||
|
||||
@cvar _SENTENCE_CONTENTS: Has the field names in an NMEA sentence for each
|
||||
sentence type (in order, obviously).
|
||||
@type _SENTENCE_CONTENTS: C{dict} of bytestrings to C{list}s of C{str}
|
||||
@param _receiver: A receiver for NMEAProtocol sentence objects.
|
||||
@type _receiver: L{INMEAReceiver}
|
||||
@param _sentenceCallback: A function that will be called with a new
|
||||
L{NMEASentence} when it is created. Useful for massaging data from
|
||||
particularly misbehaving NMEA receivers.
|
||||
@type _sentenceCallback: unary callable
|
||||
"""
|
||||
def __init__(self, receiver, sentenceCallback=None):
|
||||
"""
|
||||
Initializes an NMEAProtocol.
|
||||
|
||||
@param receiver: A receiver for NMEAProtocol sentence objects.
|
||||
@type receiver: L{INMEAReceiver}
|
||||
@param sentenceCallback: A function that will be called with a new
|
||||
L{NMEASentence} when it is created. Useful for massaging data from
|
||||
particularly misbehaving NMEA receivers.
|
||||
@type sentenceCallback: unary callable
|
||||
"""
|
||||
self._receiver = receiver
|
||||
self._sentenceCallback = sentenceCallback
|
||||
|
||||
|
||||
def lineReceived(self, rawSentence):
|
||||
"""
|
||||
Parses the data from the sentence and validates the checksum.
|
||||
|
||||
@param rawSentence: The NMEA positioning sentence.
|
||||
@type rawSentence: C{str}
|
||||
"""
|
||||
sentence = rawSentence.strip()
|
||||
|
||||
_validateChecksum(sentence)
|
||||
splitSentence = _split(sentence)
|
||||
|
||||
sentenceType, contents = splitSentence[0], splitSentence[1:]
|
||||
|
||||
try:
|
||||
keys = self._SENTENCE_CONTENTS[sentenceType]
|
||||
except KeyError:
|
||||
raise ValueError("unknown sentence type %s" % sentenceType)
|
||||
|
||||
sentenceData = {"type": sentenceType}
|
||||
for key, value in itertools.izip(keys, contents):
|
||||
if key is not None and value != "":
|
||||
sentenceData[key] = value
|
||||
|
||||
sentence = NMEASentence(sentenceData)
|
||||
|
||||
if self._sentenceCallback is not None:
|
||||
self._sentenceCallback(sentence)
|
||||
|
||||
self._receiver.sentenceReceived(sentence)
|
||||
|
||||
|
||||
_SENTENCE_CONTENTS = {
|
||||
'GPGGA': [
|
||||
'timestamp',
|
||||
|
||||
'latitudeFloat',
|
||||
'latitudeHemisphere',
|
||||
'longitudeFloat',
|
||||
'longitudeHemisphere',
|
||||
|
||||
'fixQuality',
|
||||
'numberOfSatellitesSeen',
|
||||
'horizontalDilutionOfPrecision',
|
||||
|
||||
'altitude',
|
||||
'altitudeUnits',
|
||||
'heightOfGeoidAboveWGS84',
|
||||
'heightOfGeoidAboveWGS84Units',
|
||||
|
||||
# The next parts are DGPS information, currently unused.
|
||||
None, # Time since last DGPS update
|
||||
None, # DGPS reference source id
|
||||
],
|
||||
|
||||
'GPRMC': [
|
||||
'timestamp',
|
||||
|
||||
'dataMode',
|
||||
|
||||
'latitudeFloat',
|
||||
'latitudeHemisphere',
|
||||
'longitudeFloat',
|
||||
'longitudeHemisphere',
|
||||
|
||||
'speedInKnots',
|
||||
|
||||
'trueHeading',
|
||||
|
||||
'datestamp',
|
||||
|
||||
'magneticVariation',
|
||||
'magneticVariationDirection',
|
||||
],
|
||||
|
||||
'GPGSV': [
|
||||
'numberOfGSVSentences',
|
||||
'GSVSentenceIndex',
|
||||
|
||||
'numberOfSatellitesSeen',
|
||||
|
||||
'satellitePRN_0',
|
||||
'elevation_0',
|
||||
'azimuth_0',
|
||||
'signalToNoiseRatio_0',
|
||||
|
||||
'satellitePRN_1',
|
||||
'elevation_1',
|
||||
'azimuth_1',
|
||||
'signalToNoiseRatio_1',
|
||||
|
||||
'satellitePRN_2',
|
||||
'elevation_2',
|
||||
'azimuth_2',
|
||||
'signalToNoiseRatio_2',
|
||||
|
||||
'satellitePRN_3',
|
||||
'elevation_3',
|
||||
'azimuth_3',
|
||||
'signalToNoiseRatio_3',
|
||||
],
|
||||
|
||||
'GPGLL': [
|
||||
'latitudeFloat',
|
||||
'latitudeHemisphere',
|
||||
'longitudeFloat',
|
||||
'longitudeHemisphere',
|
||||
'timestamp',
|
||||
'dataMode',
|
||||
],
|
||||
|
||||
'GPHDT': [
|
||||
'trueHeading',
|
||||
],
|
||||
|
||||
'GPTRF': [
|
||||
'datestamp',
|
||||
'timestamp',
|
||||
|
||||
'latitudeFloat',
|
||||
'latitudeHemisphere',
|
||||
'longitudeFloat',
|
||||
'longitudeHemisphere',
|
||||
|
||||
'elevation',
|
||||
'numberOfIterations', # Unused
|
||||
'numberOfDopplerIntervals', # Unused
|
||||
'updateDistanceInNauticalMiles', # Unused
|
||||
'satellitePRN',
|
||||
],
|
||||
|
||||
'GPGSA': [
|
||||
'dataMode',
|
||||
'fixType',
|
||||
|
||||
'usedSatellitePRN_0',
|
||||
'usedSatellitePRN_1',
|
||||
'usedSatellitePRN_2',
|
||||
'usedSatellitePRN_3',
|
||||
'usedSatellitePRN_4',
|
||||
'usedSatellitePRN_5',
|
||||
'usedSatellitePRN_6',
|
||||
'usedSatellitePRN_7',
|
||||
'usedSatellitePRN_8',
|
||||
'usedSatellitePRN_9',
|
||||
'usedSatellitePRN_10',
|
||||
'usedSatellitePRN_11',
|
||||
|
||||
'positionDilutionOfPrecision',
|
||||
'horizontalDilutionOfPrecision',
|
||||
'verticalDilutionOfPrecision',
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
class NMEASentence(_sentence._BaseSentence):
|
||||
"""
|
||||
An object representing an NMEA sentence.
|
||||
|
||||
The attributes of this objects are raw NMEA protocol data, which
|
||||
are all ASCII bytestrings.
|
||||
|
||||
This object contains all the raw NMEA protocol data in a single
|
||||
sentence. Not all of these necessarily have to be present in the
|
||||
sentence. Missing attributes are C{None} when accessed.
|
||||
|
||||
@ivar type: The sentence type (C{"GPGGA"}, C{"GPGSV"}...).
|
||||
@ivar numberOfGSVSentences: The total number of GSV sentences in a
|
||||
sequence.
|
||||
@ivar GSVSentenceIndex: The index of this GSV sentence in the GSV
|
||||
sequence.
|
||||
@ivar timestamp: A timestamp. (C{"123456"} -> 12:34:56Z)
|
||||
@ivar datestamp: A datestamp. (C{"230394"} -> 23 Mar 1994)
|
||||
@ivar latitudeFloat: Latitude value. (for example: C{"1234.567"} ->
|
||||
12 degrees, 34.567 minutes).
|
||||
@ivar latitudeHemisphere: Latitudinal hemisphere (C{"N"} or C{"S"}).
|
||||
@ivar longitudeFloat: Longitude value. See C{latitudeFloat} for an
|
||||
example.
|
||||
@ivar longitudeHemisphere: Longitudinal hemisphere (C{"E"} or C{"W"}).
|
||||
@ivar altitude: The altitude above mean sea level.
|
||||
@ivar altitudeUnits: Units in which altitude is expressed. (Always
|
||||
C{"M"} for meters.)
|
||||
@ivar heightOfGeoidAboveWGS84: The local height of the geoid above
|
||||
the WGS84 ellipsoid model.
|
||||
@ivar heightOfGeoidAboveWGS84Units: The units in which the height
|
||||
above the geoid is expressed. (Always C{"M"} for meters.)
|
||||
@ivar trueHeading: The true heading.
|
||||
@ivar magneticVariation: The magnetic variation.
|
||||
@ivar magneticVariationDirection: The direction of the magnetic
|
||||
variation. One of C{"E"} or C{"W"}.
|
||||
@ivar speedInKnots: The ground speed, expressed in knots.
|
||||
@ivar fixQuality: The quality of the fix.
|
||||
@type fixQuality: One of L{GPGGAFixQualities}.
|
||||
@ivar dataMode: Signals if the data is usable or not.
|
||||
@type dataMode: One of L{GPGLLGPRMCFixQualities}.
|
||||
@ivar numberOfSatellitesSeen: The number of satellites seen by the
|
||||
receiver.
|
||||
@ivar numberOfSatellitesUsed: The number of satellites used in
|
||||
computing the fix.
|
||||
@ivar horizontalDilutionOfPrecision: The dilution of the precision of the
|
||||
position on a plane tangential to the geoid. (HDOP)
|
||||
@ivar verticalDilutionOfPrecision: As C{horizontalDilutionOfPrecision},
|
||||
but for a position on a plane perpendicular to the geoid. (VDOP)
|
||||
@ivar positionDilutionOfPrecision: Euclidian norm of HDOP and VDOP.
|
||||
@ivar satellitePRN: The unique identifcation number of a particular
|
||||
satelite. Optionally suffixed with C{_N} if multiple satellites are
|
||||
referenced in a sentence, where C{N in range(4)}.
|
||||
@ivar elevation: The elevation of a satellite in decimal degrees.
|
||||
Optionally suffixed with C{_N}, as with C{satellitePRN}.
|
||||
@ivar azimuth: The azimuth of a satellite in decimal degrees.
|
||||
Optionally suffixed with C{_N}, as with C{satellitePRN}.
|
||||
@ivar signalToNoiseRatio: The SNR of a satellite signal, in decibels.
|
||||
Optionally suffixed with C{_N}, as with C{satellitePRN}.
|
||||
@ivar usedSatellitePRN_N: Where C{int(N) in range(12)}. The PRN
|
||||
of a satelite used in computing the fix.
|
||||
"""
|
||||
ALLOWED_ATTRIBUTES = NMEAProtocol.getSentenceAttributes()
|
||||
|
||||
def _isFirstGSVSentence(self):
|
||||
"""
|
||||
Tests if this current GSV sentence is the first one in a sequence.
|
||||
|
||||
@return: C{True} if this is the first GSV sentence.
|
||||
@rtype: C{bool}
|
||||
"""
|
||||
return self.GSVSentenceIndex == "1"
|
||||
|
||||
|
||||
def _isLastGSVSentence(self):
|
||||
"""
|
||||
Tests if this current GSV sentence is the final one in a sequence.
|
||||
|
||||
@return: C{True} if this is the last GSV sentence.
|
||||
@rtype: C{bool}
|
||||
"""
|
||||
return self.GSVSentenceIndex == self.numberOfGSVSentences
|
||||
|
||||
|
||||
|
||||
@implementer(ipositioning.INMEAReceiver)
|
||||
class NMEAAdapter(object):
|
||||
"""
|
||||
An adapter from NMEAProtocol receivers to positioning receivers.
|
||||
|
||||
@cvar _STATEFUL_UPDATE: Information on how to update partial information
|
||||
in the sentence data or internal adapter state. For more information,
|
||||
see C{_statefulUpdate}'s docstring.
|
||||
@type _STATEFUL_UPDATE: See C{_statefulUpdate}'s docstring
|
||||
@cvar _ACCEPTABLE_UNITS: A set of NMEA notations of units that are
|
||||
already acceptable (metric), and therefore don't need to be converted.
|
||||
@type _ACCEPTABLE_UNITS: C{frozenset} of bytestrings
|
||||
@cvar _UNIT_CONVERTERS: Mapping of NMEA notations of units that are not
|
||||
acceptable (not metric) to converters that take a quantity in that
|
||||
unit and produce a metric quantity.
|
||||
@type _UNIT_CONVERTERS: C{dict} of bytestrings to unary callables
|
||||
@cvar _SPECIFIC_SENTENCE_FIXES: A mapping of sentece types to specific
|
||||
fixes that are required to extract useful information from data from
|
||||
those sentences.
|
||||
@type _SPECIFIC_SENTENCE_FIXES: C{dict} of sentence types to callables
|
||||
that take self and modify it in-place
|
||||
@cvar _FIXERS: Set of unary callables that take an NMEAAdapter instance
|
||||
and extract useful data from the sentence data, usually modifying the
|
||||
adapter's sentence data in-place.
|
||||
@type _FIXERS: C{dict} of native strings to unary callables
|
||||
@ivar yearThreshold: The earliest possible year that data will be
|
||||
interpreted as. For example, if this value is C{1990}, an NMEA
|
||||
0183 two-digit year of "96" will be interpreted as 1996, and
|
||||
a two-digit year of "13" will be interpreted as 2013.
|
||||
@type yearThreshold: L{int}
|
||||
@ivar _state: The current internal state of the receiver.
|
||||
@type _state: C{dict}
|
||||
@ivar _sentenceData: The data present in the sentence currently being
|
||||
processed. Starts empty, is filled as the sentence is parsed.
|
||||
@type _sentenceData: C{dict}
|
||||
@ivar _receiver: The positioning receiver that will receive parsed data.
|
||||
@type _receiver: L{ipositioning.IPositioningReceiver}
|
||||
"""
|
||||
def __init__(self, receiver):
|
||||
"""
|
||||
Initializes a new NMEA adapter.
|
||||
|
||||
@param receiver: The receiver for positioning sentences.
|
||||
@type receiver: L{ipositioning.IPositioningReceiver}
|
||||
"""
|
||||
self._state = {}
|
||||
self._sentenceData = {}
|
||||
self._receiver = receiver
|
||||
|
||||
|
||||
def _fixTimestamp(self):
|
||||
"""
|
||||
Turns the NMEAProtocol timestamp notation into a datetime.time object.
|
||||
The time in this object is expressed as Zulu time.
|
||||
"""
|
||||
timestamp = self.currentSentence.timestamp.split('.')[0]
|
||||
timeObject = datetime.datetime.strptime(timestamp, '%H%M%S').time()
|
||||
self._sentenceData['_time'] = timeObject
|
||||
|
||||
|
||||
yearThreshold = 1980
|
||||
|
||||
|
||||
def _fixDatestamp(self):
|
||||
"""
|
||||
Turns an NMEA datestamp format into a C{datetime.date} object.
|
||||
|
||||
@raise ValueError: When the day or month value was invalid, e.g. 32nd
|
||||
day, or 13th month, or 0th day or month.
|
||||
"""
|
||||
date = self.currentSentence.datestamp
|
||||
day, month, year = map(int, [date[0:2], date[2:4], date[4:6]])
|
||||
|
||||
year += self.yearThreshold - (self.yearThreshold % 100)
|
||||
if year < self.yearThreshold:
|
||||
year += 100
|
||||
|
||||
self._sentenceData['_date'] = datetime.date(year, month, day)
|
||||
|
||||
|
||||
def _fixCoordinateFloat(self, coordinateType):
|
||||
"""
|
||||
Turns the NMEAProtocol coordinate format into Python float.
|
||||
|
||||
@param coordinateType: The coordinate type.
|
||||
@type coordinateType: One of L{Angles.LATITUDE} or L{Angles.LONGITUDE}.
|
||||
"""
|
||||
if coordinateType is Angles.LATITUDE:
|
||||
coordinateName = "latitude"
|
||||
else: # coordinateType is Angles.LONGITUDE
|
||||
coordinateName = "longitude"
|
||||
nmeaCoordinate = getattr(self.currentSentence, coordinateName + "Float")
|
||||
|
||||
left, right = nmeaCoordinate.split('.')
|
||||
|
||||
degrees, minutes = int(left[:-2]), float("%s.%s" % (left[-2:], right))
|
||||
angle = degrees + minutes/60
|
||||
coordinate = base.Coordinate(angle, coordinateType)
|
||||
self._sentenceData[coordinateName] = coordinate
|
||||
|
||||
|
||||
def _fixHemisphereSign(self, coordinateType, sentenceDataKey=None):
|
||||
"""
|
||||
Fixes the sign for a hemisphere.
|
||||
|
||||
This method must be called after the magnitude for the thing it
|
||||
determines the sign of has been set. This is done by the following
|
||||
functions:
|
||||
|
||||
- C{self.FIXERS['magneticVariation']}
|
||||
- C{self.FIXERS['latitudeFloat']}
|
||||
- C{self.FIXERS['longitudeFloat']}
|
||||
|
||||
@param coordinateType: Coordinate type. One of L{Angles.LATITUDE},
|
||||
L{Angles.LONGITUDE} or L{Angles.VARIATION}.
|
||||
@param sentenceDataKey: The key name of the hemisphere sign being
|
||||
fixed in the sentence data. If unspecified, C{coordinateType} is
|
||||
used.
|
||||
@type sentenceDataKey: C{str} (unless C{None})
|
||||
"""
|
||||
sentenceDataKey = sentenceDataKey or coordinateType
|
||||
sign = self._getHemisphereSign(coordinateType)
|
||||
self._sentenceData[sentenceDataKey].setSign(sign)
|
||||
|
||||
|
||||
def _getHemisphereSign(self, coordinateType):
|
||||
"""
|
||||
Returns the hemisphere sign for a given coordinate type.
|
||||
|
||||
@param coordinateType: The coordinate type to find the hemisphere for.
|
||||
@type coordinateType: L{Angles.LATITUDE}, L{Angles.LONGITUDE} or
|
||||
L{Angles.VARIATION}.
|
||||
@return: The sign of that hemisphere (-1 or 1).
|
||||
@rtype: C{int}
|
||||
"""
|
||||
if coordinateType is Angles.LATITUDE:
|
||||
hemisphereKey = "latitudeHemisphere"
|
||||
elif coordinateType is Angles.LONGITUDE:
|
||||
hemisphereKey = "longitudeHemisphere"
|
||||
elif coordinateType is Angles.VARIATION:
|
||||
hemisphereKey = 'magneticVariationDirection'
|
||||
else:
|
||||
raise ValueError("unknown coordinate type %s" % (coordinateType,))
|
||||
|
||||
hemisphere = getattr(self.currentSentence, hemisphereKey).upper()
|
||||
|
||||
if hemisphere in "NE":
|
||||
return 1
|
||||
elif hemisphere in "SW":
|
||||
return -1
|
||||
else:
|
||||
raise ValueError("bad hemisphere/direction: %s" % (hemisphere,))
|
||||
|
||||
|
||||
def _convert(self, key, converter):
|
||||
"""
|
||||
A simple conversion fix.
|
||||
|
||||
@param key: The attribute name of the value to fix.
|
||||
@type key: native string (Python identifier)
|
||||
|
||||
@param converter: The function that converts the value.
|
||||
@type converter: unary callable
|
||||
"""
|
||||
currentValue = getattr(self.currentSentence, key)
|
||||
self._sentenceData[key] = converter(currentValue)
|
||||
|
||||
|
||||
_STATEFUL_UPDATE = {
|
||||
# sentenceKey: (stateKey, factory, attributeName, converter),
|
||||
'trueHeading': ('heading', base.Heading, '_angle', float),
|
||||
'magneticVariation':
|
||||
('heading', base.Heading, 'variation',
|
||||
lambda angle: base.Angle(float(angle), Angles.VARIATION)),
|
||||
|
||||
'horizontalDilutionOfPrecision':
|
||||
('positionError', base.PositionError, 'hdop', float),
|
||||
'verticalDilutionOfPrecision':
|
||||
('positionError', base.PositionError, 'vdop', float),
|
||||
'positionDilutionOfPrecision':
|
||||
('positionError', base.PositionError, 'pdop', float),
|
||||
|
||||
}
|
||||
|
||||
|
||||
def _statefulUpdate(self, sentenceKey):
|
||||
"""
|
||||
Does a stateful update of a particular positioning attribute.
|
||||
Specifically, this will mutate an object in the current sentence data.
|
||||
|
||||
Using the C{sentenceKey}, this will get a tuple containing, in order,
|
||||
the key name in the current state and sentence data, a factory for
|
||||
new values, the attribute to update, and a converter from sentence
|
||||
data (in NMEA notation) to something useful.
|
||||
|
||||
If the sentence data doesn't have this data yet, it is grabbed from
|
||||
the state. If that doesn't have anything useful yet either, the
|
||||
factory is called to produce a new, empty object. Either way, the
|
||||
object ends up in the sentence data.
|
||||
|
||||
@param sentenceKey: The name of the key in the sentence attributes,
|
||||
C{NMEAAdapter._STATEFUL_UPDATE} dictionary and the adapter state.
|
||||
@type sentenceKey: C{str}
|
||||
"""
|
||||
key, factory, attr, converter = self._STATEFUL_UPDATE[sentenceKey]
|
||||
|
||||
if key not in self._sentenceData:
|
||||
try:
|
||||
self._sentenceData[key] = self._state[key]
|
||||
except KeyError: # state does not have this partial data yet
|
||||
self._sentenceData[key] = factory()
|
||||
|
||||
newValue = converter(getattr(self.currentSentence, sentenceKey))
|
||||
setattr(self._sentenceData[key], attr, newValue)
|
||||
|
||||
|
||||
_ACCEPTABLE_UNITS = frozenset(['M'])
|
||||
_UNIT_CONVERTERS = {
|
||||
'N': lambda inKnots: base.Speed(float(inKnots) * base.MPS_PER_KNOT),
|
||||
'K': lambda inKPH: base.Speed(float(inKPH) * base.MPS_PER_KPH),
|
||||
}
|
||||
|
||||
|
||||
def _fixUnits(self, unitKey=None, valueKey=None, sourceKey=None,
|
||||
unit=None):
|
||||
"""
|
||||
Fixes the units of a certain value. If the units are already
|
||||
acceptable (metric), does nothing.
|
||||
|
||||
None of the keys are allowed to be the empty string.
|
||||
|
||||
@param unit: The unit that is being converted I{from}. If unspecified
|
||||
or C{None}, asks the current sentence for the C{unitKey}. If that
|
||||
also fails, raises C{AttributeError}.
|
||||
@type unit: C{str}
|
||||
@param unitKey: The name of the key/attribute under which the unit can
|
||||
be found in the current sentence. If the C{unit} parameter is set,
|
||||
this parameter is not used.
|
||||
@type unitKey: C{str}
|
||||
@param sourceKey: The name of the key/attribute that contains the
|
||||
current value to be converted (expressed in units as defined
|
||||
according to the the C{unit} parameter). If unset, will use the
|
||||
same key as the value key.
|
||||
@type sourceKey: C{str}
|
||||
@param valueKey: The key name in which the data will be stored in the
|
||||
C{_sentenceData} instance attribute. If unset, attempts to remove
|
||||
"Units" from the end of the C{unitKey} parameter. If that fails,
|
||||
raises C{ValueError}.
|
||||
@type valueKey: C{str}
|
||||
"""
|
||||
if unit is None:
|
||||
unit = getattr(self.currentSentence, unitKey)
|
||||
if valueKey is None:
|
||||
if unitKey is not None and unitKey.endswith("Units"):
|
||||
valueKey = unitKey[:-5]
|
||||
else:
|
||||
raise ValueError("valueKey unspecified and couldn't be guessed")
|
||||
if sourceKey is None:
|
||||
sourceKey = valueKey
|
||||
|
||||
if unit not in self._ACCEPTABLE_UNITS:
|
||||
converter = self._UNIT_CONVERTERS[unit]
|
||||
currentValue = getattr(self.currentSentence, sourceKey)
|
||||
self._sentenceData[valueKey] = converter(currentValue)
|
||||
|
||||
|
||||
def _fixGSV(self):
|
||||
"""
|
||||
Parses partial visible satellite information from a GSV sentence.
|
||||
"""
|
||||
# To anyone who knows NMEA, this method's name should raise a chuckle's
|
||||
# worth of schadenfreude. 'Fix' GSV? Hah! Ludicrous.
|
||||
beaconInformation = base.BeaconInformation()
|
||||
self._sentenceData['_partialBeaconInformation'] = beaconInformation
|
||||
|
||||
keys = "satellitePRN", "azimuth", "elevation", "signalToNoiseRatio"
|
||||
for index in range(4):
|
||||
prn, azimuth, elevation, snr = [getattr(self.currentSentence, attr)
|
||||
for attr in ("%s_%i" % (key, index) for key in keys)]
|
||||
|
||||
if prn is None or snr is None:
|
||||
# The peephole optimizer optimizes the jump away, meaning that
|
||||
# coverage.py thinks it isn't covered. It is. Replace it with
|
||||
# break, and watch the test case fail.
|
||||
# ML thread about this issue: http://goo.gl/1KNUi
|
||||
# Related CPython bug: http://bugs.python.org/issue2506
|
||||
continue
|
||||
|
||||
satellite = base.Satellite(prn, azimuth, elevation, snr)
|
||||
beaconInformation.seenBeacons.add(satellite)
|
||||
|
||||
|
||||
def _fixGSA(self):
|
||||
"""
|
||||
Extracts the information regarding which satellites were used in
|
||||
obtaining the GPS fix from a GSA sentence.
|
||||
|
||||
Precondition: A GSA sentence was fired. Postcondition: The current
|
||||
sentence data (C{self._sentenceData} will contain a set of the
|
||||
currently used PRNs (under the key C{_usedPRNs}.
|
||||
"""
|
||||
self._sentenceData['_usedPRNs'] = set()
|
||||
for key in ("usedSatellitePRN_%d" % (x,) for x in range(12)):
|
||||
prn = getattr(self.currentSentence, key, None)
|
||||
if prn is not None:
|
||||
self._sentenceData['_usedPRNs'].add(int(prn))
|
||||
|
||||
|
||||
_SPECIFIC_SENTENCE_FIXES = {
|
||||
'GPGSV': _fixGSV,
|
||||
'GPGSA': _fixGSA,
|
||||
}
|
||||
|
||||
|
||||
def _sentenceSpecificFix(self):
|
||||
"""
|
||||
Executes a fix for a specific type of sentence.
|
||||
"""
|
||||
fixer = self._SPECIFIC_SENTENCE_FIXES.get(self.currentSentence.type)
|
||||
if fixer is not None:
|
||||
fixer(self)
|
||||
|
||||
|
||||
_FIXERS = {
|
||||
'type':
|
||||
lambda self: self._sentenceSpecificFix(),
|
||||
|
||||
'timestamp':
|
||||
lambda self: self._fixTimestamp(),
|
||||
'datestamp':
|
||||
lambda self: self._fixDatestamp(),
|
||||
|
||||
'latitudeFloat':
|
||||
lambda self: self._fixCoordinateFloat(Angles.LATITUDE),
|
||||
'latitudeHemisphere':
|
||||
lambda self: self._fixHemisphereSign(Angles.LATITUDE, 'latitude'),
|
||||
'longitudeFloat':
|
||||
lambda self: self._fixCoordinateFloat(Angles.LONGITUDE),
|
||||
'longitudeHemisphere':
|
||||
lambda self: self._fixHemisphereSign(Angles.LONGITUDE, 'longitude'),
|
||||
|
||||
'altitude':
|
||||
lambda self: self._convert('altitude',
|
||||
converter=lambda strRepr: base.Altitude(float(strRepr))),
|
||||
'altitudeUnits':
|
||||
lambda self: self._fixUnits(unitKey='altitudeUnits'),
|
||||
|
||||
'heightOfGeoidAboveWGS84':
|
||||
lambda self: self._convert('heightOfGeoidAboveWGS84',
|
||||
converter=lambda strRepr: base.Altitude(float(strRepr))),
|
||||
'heightOfGeoidAboveWGS84Units':
|
||||
lambda self: self._fixUnits(
|
||||
unitKey='heightOfGeoidAboveWGS84Units'),
|
||||
|
||||
'trueHeading':
|
||||
lambda self: self._statefulUpdate('trueHeading'),
|
||||
'magneticVariation':
|
||||
lambda self: self._statefulUpdate('magneticVariation'),
|
||||
|
||||
'magneticVariationDirection':
|
||||
lambda self: self._fixHemisphereSign(Angles.VARIATION,
|
||||
'heading'),
|
||||
|
||||
'speedInKnots':
|
||||
lambda self: self._fixUnits(valueKey='speed',
|
||||
sourceKey='speedInKnots',
|
||||
unit='N'),
|
||||
|
||||
'positionDilutionOfPrecision':
|
||||
lambda self: self._statefulUpdate('positionDilutionOfPrecision'),
|
||||
'horizontalDilutionOfPrecision':
|
||||
lambda self: self._statefulUpdate('horizontalDilutionOfPrecision'),
|
||||
'verticalDilutionOfPrecision':
|
||||
lambda self: self._statefulUpdate('verticalDilutionOfPrecision'),
|
||||
}
|
||||
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Resets this adapter.
|
||||
|
||||
This will empty the adapter state and the current sentence data.
|
||||
"""
|
||||
self._state = {}
|
||||
self._sentenceData = {}
|
||||
|
||||
|
||||
def sentenceReceived(self, sentence):
|
||||
"""
|
||||
Called when a sentence is received.
|
||||
|
||||
Will clean the received NMEAProtocol sentence up, and then update the
|
||||
adapter's state, followed by firing the callbacks.
|
||||
|
||||
If the received sentence was invalid, the state will be cleared.
|
||||
|
||||
@param sentence: The sentence that is received.
|
||||
@type sentence: L{NMEASentence}
|
||||
"""
|
||||
self.currentSentence = sentence
|
||||
self._sentenceData = {}
|
||||
|
||||
try:
|
||||
self._validateCurrentSentence()
|
||||
self._cleanCurrentSentence()
|
||||
except base.InvalidSentence:
|
||||
self.clear()
|
||||
|
||||
self._updateState()
|
||||
self._fireSentenceCallbacks()
|
||||
|
||||
|
||||
def _validateCurrentSentence(self):
|
||||
"""
|
||||
Tests if a sentence contains a valid fix.
|
||||
"""
|
||||
if (self.currentSentence.fixQuality is GPGGAFixQualities.INVALID_FIX
|
||||
or self.currentSentence.dataMode is GPGLLGPRMCFixQualities.VOID
|
||||
or self.currentSentence.fixType is GPGSAFixTypes.GSA_NO_FIX):
|
||||
raise base.InvalidSentence("bad sentence")
|
||||
|
||||
|
||||
def _cleanCurrentSentence(self):
|
||||
"""
|
||||
Cleans the current sentence.
|
||||
"""
|
||||
for key in sorted(self.currentSentence.presentAttributes):
|
||||
fixer = self._FIXERS.get(key, None)
|
||||
|
||||
if fixer is not None:
|
||||
fixer(self)
|
||||
|
||||
|
||||
def _updateState(self):
|
||||
"""
|
||||
Updates the current state with the new information from the sentence.
|
||||
"""
|
||||
self._updateBeaconInformation()
|
||||
self._combineDateAndTime()
|
||||
self._state.update(self._sentenceData)
|
||||
|
||||
|
||||
def _updateBeaconInformation(self):
|
||||
"""
|
||||
Updates existing beacon information state with new data.
|
||||
"""
|
||||
new = self._sentenceData.get('_partialBeaconInformation')
|
||||
if new is None:
|
||||
return
|
||||
|
||||
self._updateUsedBeacons(new)
|
||||
self._mergeBeaconInformation(new)
|
||||
|
||||
if self.currentSentence._isLastGSVSentence():
|
||||
if not self.currentSentence._isFirstGSVSentence():
|
||||
# not a 1-sentence sequence, get rid of partial information
|
||||
del self._state['_partialBeaconInformation']
|
||||
bi = self._sentenceData.pop('_partialBeaconInformation')
|
||||
self._sentenceData['beaconInformation'] = bi
|
||||
|
||||
|
||||
def _updateUsedBeacons(self, beaconInformation):
|
||||
"""
|
||||
Searches the adapter state and sentence data for information about
|
||||
which beacons where used, then adds it to the provided beacon
|
||||
information object.
|
||||
|
||||
If no new beacon usage information is available, does nothing.
|
||||
|
||||
@param beaconInformation: The beacon information object that beacon
|
||||
usage information will be added to (if necessary).
|
||||
@type beaconInformation: L{twisted.positioning.base.BeaconInformation}
|
||||
"""
|
||||
for source in [self._state, self._sentenceData]:
|
||||
usedPRNs = source.get("_usedPRNs")
|
||||
if usedPRNs is not None:
|
||||
break
|
||||
else: # No used PRN info to update
|
||||
return
|
||||
|
||||
for beacon in beaconInformation.seenBeacons:
|
||||
if beacon.identifier in usedPRNs:
|
||||
beaconInformation.usedBeacons.add(beacon)
|
||||
|
||||
|
||||
def _mergeBeaconInformation(self, newBeaconInformation):
|
||||
"""
|
||||
Merges beacon information in the adapter state (if it exists) into
|
||||
the provided beacon information. Specifically, this merges used and
|
||||
seen beacons.
|
||||
|
||||
If the adapter state has no beacon information, does nothing.
|
||||
|
||||
@param beaconInformation: The beacon information object that beacon
|
||||
information will be merged into (if necessary).
|
||||
@type beaconInformation: L{twisted.positioning.base.BeaconInformation}
|
||||
"""
|
||||
old = self._state.get('_partialBeaconInformation')
|
||||
if old is None:
|
||||
return
|
||||
|
||||
for attr in ["seenBeacons", "usedBeacons"]:
|
||||
getattr(newBeaconInformation, attr).update(getattr(old, attr))
|
||||
|
||||
|
||||
def _combineDateAndTime(self):
|
||||
"""
|
||||
Combines a C{datetime.date} object and a C{datetime.time} object,
|
||||
collected from one or more NMEA sentences, into a single
|
||||
C{datetime.datetime} object suitable for sending to the
|
||||
L{IPositioningReceiver}.
|
||||
"""
|
||||
if not any(k in self._sentenceData for k in ["_date", "_time"]):
|
||||
# If the sentence has neither date nor time, there's
|
||||
# nothing new to combine here.
|
||||
return
|
||||
|
||||
date, time = [self._sentenceData.get(key) or self._state.get(key)
|
||||
for key in ('_date', '_time')]
|
||||
|
||||
if date is None or time is None:
|
||||
return
|
||||
|
||||
dt = datetime.datetime.combine(date, time)
|
||||
self._sentenceData['time'] = dt
|
||||
|
||||
|
||||
def _fireSentenceCallbacks(self):
|
||||
"""
|
||||
Fires sentence callbacks for the current sentence.
|
||||
|
||||
A callback will only fire if all of the keys it requires are present
|
||||
in the current state and at least one such field was altered in the
|
||||
current sentence.
|
||||
|
||||
The callbacks will only be fired with data from L{self._state}.
|
||||
"""
|
||||
iface = ipositioning.IPositioningReceiver
|
||||
for name, method in iface.namesAndDescriptions():
|
||||
callback = getattr(self._receiver, name)
|
||||
|
||||
kwargs = {}
|
||||
atLeastOnePresentInSentence = False
|
||||
|
||||
try:
|
||||
for field in method.positional:
|
||||
if field in self._sentenceData:
|
||||
atLeastOnePresentInSentence = True
|
||||
kwargs[field] = self._state[field]
|
||||
except KeyError:
|
||||
continue
|
||||
|
||||
if atLeastOnePresentInSentence:
|
||||
callback(**kwargs)
|
||||
|
||||
|
||||
|
||||
__all__ = [
|
||||
"NMEAProtocol",
|
||||
"NMEASentence",
|
||||
"NMEAAdapter"
|
||||
]
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
"""
|
||||
Tests for the Twisted positioning framework.
|
||||
"""
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
"""
|
||||
Receivers for use in tests.
|
||||
"""
|
||||
from twisted.positioning import base, ipositioning
|
||||
|
||||
|
||||
class MockPositioningReceiver(base.BasePositioningReceiver):
|
||||
"""
|
||||
A mock positioning receiver.
|
||||
|
||||
Mocks all the L{IPositioningReceiver} methods with stubs that don't do
|
||||
anything but register that they were called.
|
||||
|
||||
@ivar called: A mapping of names of callbacks that have been called to
|
||||
C{True}.
|
||||
@type called: C{dict}
|
||||
"""
|
||||
def __init__(self):
|
||||
self.clear()
|
||||
|
||||
for methodName in ipositioning.IPositioningReceiver:
|
||||
self._addCallback(methodName)
|
||||
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Forget all the methods that have been called on this receiver, by
|
||||
emptying C{self.called}.
|
||||
"""
|
||||
self.called = {}
|
||||
|
||||
|
||||
def _addCallback(self, name):
|
||||
"""
|
||||
Adds a callback of the given name, setting C{self.called[name]} to
|
||||
C{True} when called.
|
||||
"""
|
||||
def callback(*a, **kw):
|
||||
self.called[name] = True
|
||||
|
||||
setattr(self, name, callback)
|
||||
|
|
@ -0,0 +1,917 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
"""
|
||||
Test cases for positioning primitives.
|
||||
"""
|
||||
from twisted.trial.unittest import TestCase
|
||||
from twisted.positioning import base
|
||||
from twisted.positioning.base import Angles, Directions
|
||||
from twisted.positioning.ipositioning import IPositioningBeacon
|
||||
from zope.interface import verify
|
||||
|
||||
|
||||
class AngleTests(TestCase):
|
||||
"""
|
||||
Tests for the L{twisted.positioning.base.Angle} class.
|
||||
"""
|
||||
def test_empty(self):
|
||||
"""
|
||||
The repr of an empty angle says that is of unknown type and unknown
|
||||
value.
|
||||
"""
|
||||
a = base.Angle()
|
||||
self.assertEqual("<Angle of unknown type (unknown value)>", repr(a))
|
||||
|
||||
|
||||
def test_variation(self):
|
||||
"""
|
||||
The repr of an empty variation says that it is a variation of unknown
|
||||
value.
|
||||
"""
|
||||
a = base.Angle(angleType=Angles.VARIATION)
|
||||
self.assertEqual("<Variation (unknown value)>", repr(a))
|
||||
|
||||
|
||||
def test_unknownType(self):
|
||||
"""
|
||||
The repr of an angle of unknown type but a given value displays that
|
||||
type and value in its repr.
|
||||
"""
|
||||
a = base.Angle(1.0)
|
||||
self.assertEqual("<Angle of unknown type (1.0 degrees)>", repr(a))
|
||||
|
||||
|
||||
def test_bogusType(self):
|
||||
"""
|
||||
Trying to create an angle with a bogus type raises C{ValueError}.
|
||||
"""
|
||||
self.assertRaises(ValueError, base.Angle, angleType="BOGUS")
|
||||
|
||||
|
||||
|
||||
class HeadingTests(TestCase):
|
||||
"""
|
||||
Tests for the L{twisted.positioning.base.Heading} class.
|
||||
"""
|
||||
def test_simple(self):
|
||||
"""
|
||||
Tests that a simple heading has a value in decimal degrees, which is
|
||||
also its value when converted to a float. Its variation, and by
|
||||
consequence its corrected heading, is C{None}.
|
||||
"""
|
||||
h = base.Heading(1.)
|
||||
self.assertEqual(h.inDecimalDegrees, 1.)
|
||||
self.assertEqual(float(h), 1.)
|
||||
self.assertEqual(h.variation, None)
|
||||
self.assertEqual(h.correctedHeading, None)
|
||||
|
||||
|
||||
def test_headingWithoutVariationRepr(self):
|
||||
"""
|
||||
A repr of a heading with no variation reports its value and that the
|
||||
variation is unknown.
|
||||
"""
|
||||
heading = base.Heading(1.)
|
||||
expectedRepr = "<Heading (1.0 degrees, unknown variation)>"
|
||||
self.assertEqual(repr(heading), expectedRepr)
|
||||
|
||||
|
||||
def test_headingWithVariationRepr(self):
|
||||
"""
|
||||
A repr of a heading with known variation reports its value and the
|
||||
value of that variation.
|
||||
"""
|
||||
angle, variation = 1.0, -10.0
|
||||
heading = base.Heading.fromFloats(angle, variationValue=variation)
|
||||
reprTemplate = '<Heading ({0} degrees, <Variation ({1} degrees)>)>'
|
||||
self.assertEqual(repr(heading), reprTemplate.format(angle, variation))
|
||||
|
||||
|
||||
def test_valueEquality(self):
|
||||
"""
|
||||
Headings with the same values compare equal.
|
||||
"""
|
||||
self.assertEqual(base.Heading(1.), base.Heading(1.))
|
||||
|
||||
|
||||
def test_valueInequality(self):
|
||||
"""
|
||||
Headings with different values compare unequal.
|
||||
"""
|
||||
self.assertNotEquals(base.Heading(1.), base.Heading(2.))
|
||||
|
||||
|
||||
def test_zeroHeadingEdgeCase(self):
|
||||
"""
|
||||
Headings can be instantiated with a value of 0 and no variation.
|
||||
"""
|
||||
base.Heading(0)
|
||||
|
||||
|
||||
def test_zeroHeading180DegreeVariationEdgeCase(self):
|
||||
"""
|
||||
Headings can be instantiated with a value of 0 and a variation of 180
|
||||
degrees.
|
||||
"""
|
||||
base.Heading(0, 180)
|
||||
|
||||
|
||||
def _badValueTest(self, **kw):
|
||||
"""
|
||||
Helper function for verifying that bad values raise C{ValueError}.
|
||||
|
||||
@param kw: The keyword arguments passed to L{base.Heading.fromFloats}.
|
||||
"""
|
||||
self.assertRaises(ValueError, base.Heading.fromFloats, **kw)
|
||||
|
||||
|
||||
def test_badAngleValueEdgeCase(self):
|
||||
"""
|
||||
Headings can not be instantiated with a value of 360 degrees.
|
||||
"""
|
||||
self._badValueTest(angleValue=360.0)
|
||||
|
||||
|
||||
def test_badVariationEdgeCase(self):
|
||||
"""
|
||||
Headings can not be instantiated with a variation of -180 degrees.
|
||||
"""
|
||||
self._badValueTest(variationValue=-180.0)
|
||||
|
||||
|
||||
def test_negativeHeading(self):
|
||||
"""
|
||||
Negative heading values raise C{ValueError}.
|
||||
"""
|
||||
self._badValueTest(angleValue=-10.0)
|
||||
|
||||
|
||||
def test_headingTooLarge(self):
|
||||
"""
|
||||
Heading values greater than C{360.0} raise C{ValueError}.
|
||||
"""
|
||||
self._badValueTest(angleValue=370.0)
|
||||
|
||||
|
||||
def test_variationTooNegative(self):
|
||||
"""
|
||||
Variation values less than C{-180.0} raise C{ValueError}.
|
||||
"""
|
||||
self._badValueTest(variationValue=-190.0)
|
||||
|
||||
|
||||
def test_variationTooPositive(self):
|
||||
"""
|
||||
Variation values greater than C{180.0} raise C{ValueError}.
|
||||
"""
|
||||
self._badValueTest(variationValue=190.0)
|
||||
|
||||
|
||||
def test_correctedHeading(self):
|
||||
"""
|
||||
A heading with a value and a variation has a corrected heading.
|
||||
"""
|
||||
h = base.Heading.fromFloats(1., variationValue=-10.)
|
||||
self.assertEqual(h.correctedHeading, base.Angle(11., Angles.HEADING))
|
||||
|
||||
|
||||
def test_correctedHeadingOverflow(self):
|
||||
"""
|
||||
A heading with a value and a variation has the appropriate corrected
|
||||
heading value, even when the variation puts it across the 360 degree
|
||||
boundary.
|
||||
"""
|
||||
h = base.Heading.fromFloats(359., variationValue=-2.)
|
||||
self.assertEqual(h.correctedHeading, base.Angle(1., Angles.HEADING))
|
||||
|
||||
|
||||
def test_correctedHeadingOverflowEdgeCase(self):
|
||||
"""
|
||||
A heading with a value and a variation has the appropriate corrected
|
||||
heading value, even when the variation puts it exactly at the 360
|
||||
degree boundary.
|
||||
"""
|
||||
h = base.Heading.fromFloats(359., variationValue=-1.)
|
||||
self.assertEqual(h.correctedHeading, base.Angle(0., Angles.HEADING))
|
||||
|
||||
|
||||
def test_correctedHeadingUnderflow(self):
|
||||
"""
|
||||
A heading with a value and a variation has the appropriate corrected
|
||||
heading value, even when the variation puts it under the 0 degree
|
||||
boundary.
|
||||
"""
|
||||
h = base.Heading.fromFloats(1., variationValue=2.)
|
||||
self.assertEqual(h.correctedHeading, base.Angle(359., Angles.HEADING))
|
||||
|
||||
|
||||
def test_correctedHeadingUnderflowEdgeCase(self):
|
||||
"""
|
||||
A heading with a value and a variation has the appropriate corrected
|
||||
heading value, even when the variation puts it exactly at the 0
|
||||
degree boundary.
|
||||
"""
|
||||
h = base.Heading.fromFloats(1., variationValue=1.)
|
||||
self.assertEqual(h.correctedHeading, base.Angle(0., Angles.HEADING))
|
||||
|
||||
|
||||
def test_setVariationSign(self):
|
||||
"""
|
||||
Setting the sign of a heading changes the variation sign.
|
||||
"""
|
||||
h = base.Heading.fromFloats(1., variationValue=1.)
|
||||
h.setSign(1)
|
||||
self.assertEqual(h.variation.inDecimalDegrees, 1.)
|
||||
h.setSign(-1)
|
||||
self.assertEqual(h.variation.inDecimalDegrees, -1.)
|
||||
|
||||
|
||||
def test_setBadVariationSign(self):
|
||||
"""
|
||||
Setting the sign of a heading to values that aren't C{-1} or C{1}
|
||||
raises C{ValueError} and does not affect the heading.
|
||||
"""
|
||||
h = base.Heading.fromFloats(1., variationValue=1.)
|
||||
self.assertRaises(ValueError, h.setSign, -50)
|
||||
self.assertEqual(h.variation.inDecimalDegrees, 1.)
|
||||
|
||||
self.assertRaises(ValueError, h.setSign, 0)
|
||||
self.assertEqual(h.variation.inDecimalDegrees, 1.)
|
||||
|
||||
self.assertRaises(ValueError, h.setSign, 50)
|
||||
self.assertEqual(h.variation.inDecimalDegrees, 1.)
|
||||
|
||||
|
||||
def test_setUnknownVariationSign(self):
|
||||
"""
|
||||
Setting the sign on a heading with unknown variation raises
|
||||
C{ValueError}.
|
||||
"""
|
||||
h = base.Heading.fromFloats(1.)
|
||||
self.assertIdentical(None, h.variation.inDecimalDegrees)
|
||||
self.assertRaises(ValueError, h.setSign, 1)
|
||||
|
||||
|
||||
|
||||
class CoordinateTests(TestCase):
|
||||
def test_float(self):
|
||||
"""
|
||||
Coordinates can be converted to floats.
|
||||
"""
|
||||
coordinate = base.Coordinate(10.0)
|
||||
self.assertEqual(float(coordinate), 10.0)
|
||||
|
||||
|
||||
def test_repr(self):
|
||||
"""
|
||||
Coordinates that aren't explicitly latitudes or longitudes have an
|
||||
appropriate repr.
|
||||
"""
|
||||
coordinate = base.Coordinate(10.0)
|
||||
expectedRepr = "<Angle of unknown type ({0} degrees)>".format(10.0)
|
||||
self.assertEqual(repr(coordinate), expectedRepr)
|
||||
|
||||
|
||||
def test_positiveLatitude(self):
|
||||
"""
|
||||
Positive latitudes have a repr that specifies their type and value.
|
||||
"""
|
||||
coordinate = base.Coordinate(10.0, Angles.LATITUDE)
|
||||
expectedRepr = "<Latitude ({0} degrees)>".format(10.0)
|
||||
self.assertEqual(repr(coordinate), expectedRepr)
|
||||
|
||||
|
||||
def test_negativeLatitude(self):
|
||||
"""
|
||||
Negative latitudes have a repr that specifies their type and value.
|
||||
"""
|
||||
coordinate = base.Coordinate(-50.0, Angles.LATITUDE)
|
||||
expectedRepr = "<Latitude ({0} degrees)>".format(-50.0)
|
||||
self.assertEqual(repr(coordinate), expectedRepr)
|
||||
|
||||
|
||||
def test_positiveLongitude(self):
|
||||
"""
|
||||
Positive longitudes have a repr that specifies their type and value.
|
||||
"""
|
||||
longitude = base.Coordinate(50.0, Angles.LONGITUDE)
|
||||
expectedRepr = "<Longitude ({0} degrees)>".format(50.0)
|
||||
self.assertEqual(repr(longitude), expectedRepr)
|
||||
|
||||
|
||||
def test_negativeLongitude(self):
|
||||
"""
|
||||
Negative longitudes have a repr that specifies their type and value.
|
||||
"""
|
||||
longitude = base.Coordinate(-50.0, Angles.LONGITUDE)
|
||||
expectedRepr = "<Longitude ({0} degrees)>".format(-50.0)
|
||||
self.assertEqual(repr(longitude), expectedRepr)
|
||||
|
||||
|
||||
def test_bogusCoordinateType(self):
|
||||
"""
|
||||
Creating coordinates with bogus types rasies C{ValueError}.
|
||||
"""
|
||||
self.assertRaises(ValueError, base.Coordinate, 150.0, "BOGUS")
|
||||
|
||||
|
||||
def test_angleTypeNotCoordinate(self):
|
||||
"""
|
||||
Creating coordinates with angle types that aren't coordinates raises
|
||||
C{ValueError}.
|
||||
"""
|
||||
self.assertRaises(ValueError, base.Coordinate, 150.0, Angles.HEADING)
|
||||
|
||||
|
||||
def test_equality(self):
|
||||
"""
|
||||
Coordinates with the same value and type are equal.
|
||||
"""
|
||||
def makeCoordinate():
|
||||
return base.Coordinate(1.0, Angles.LONGITUDE)
|
||||
self.assertEqual(makeCoordinate(), makeCoordinate())
|
||||
|
||||
|
||||
def test_differentAnglesInequality(self):
|
||||
"""
|
||||
Coordinates with different values aren't equal.
|
||||
"""
|
||||
c1 = base.Coordinate(1.0)
|
||||
c2 = base.Coordinate(-1.0)
|
||||
self.assertNotEqual(c1, c2)
|
||||
|
||||
|
||||
def test_differentTypesInequality(self):
|
||||
"""
|
||||
Coordinates with the same values but different types aren't equal.
|
||||
"""
|
||||
c1 = base.Coordinate(1.0, Angles.LATITUDE)
|
||||
c2 = base.Coordinate(1.0, Angles.LONGITUDE)
|
||||
self.assertNotEqual(c1, c2)
|
||||
|
||||
|
||||
def test_sign(self):
|
||||
"""
|
||||
Setting the sign on a coordinate sets the sign of the value of the
|
||||
coordinate.
|
||||
"""
|
||||
c = base.Coordinate(50., Angles.LATITUDE)
|
||||
c.setSign(1)
|
||||
self.assertEqual(c.inDecimalDegrees, 50.)
|
||||
c.setSign(-1)
|
||||
self.assertEqual(c.inDecimalDegrees, -50.)
|
||||
|
||||
|
||||
def test_badVariationSign(self):
|
||||
"""
|
||||
Setting a bogus sign value (not -1 or 1) on a coordinate raises
|
||||
C{ValueError} and doesn't affect the coordinate.
|
||||
"""
|
||||
value = 50.0
|
||||
c = base.Coordinate(value, Angles.LATITUDE)
|
||||
|
||||
self.assertRaises(ValueError, c.setSign, -50)
|
||||
self.assertEqual(c.inDecimalDegrees, 50.)
|
||||
|
||||
self.assertRaises(ValueError, c.setSign, 0)
|
||||
self.assertEqual(c.inDecimalDegrees, 50.)
|
||||
|
||||
self.assertRaises(ValueError, c.setSign, 50)
|
||||
self.assertEqual(c.inDecimalDegrees, 50.)
|
||||
|
||||
|
||||
def test_northernHemisphere(self):
|
||||
"""
|
||||
Positive latitudes are in the northern hemisphere.
|
||||
"""
|
||||
coordinate = base.Coordinate(1.0, Angles.LATITUDE)
|
||||
self.assertEqual(coordinate.hemisphere, Directions.NORTH)
|
||||
|
||||
|
||||
def test_easternHemisphere(self):
|
||||
"""
|
||||
Positive longitudes are in the eastern hemisphere.
|
||||
"""
|
||||
coordinate = base.Coordinate(1.0, Angles.LONGITUDE)
|
||||
self.assertEqual(coordinate.hemisphere, Directions.EAST)
|
||||
|
||||
|
||||
def test_southernHemisphere(self):
|
||||
"""
|
||||
Negative latitudes are in the southern hemisphere.
|
||||
"""
|
||||
coordinate = base.Coordinate(-1.0, Angles.LATITUDE)
|
||||
self.assertEqual(coordinate.hemisphere, Directions.SOUTH)
|
||||
|
||||
|
||||
def test_westernHemisphere(self):
|
||||
"""
|
||||
Negative longitudes are in the western hemisphere.
|
||||
"""
|
||||
coordinate = base.Coordinate(-1.0, Angles.LONGITUDE)
|
||||
self.assertEqual(coordinate.hemisphere, Directions.WEST)
|
||||
|
||||
|
||||
def test_badHemisphere(self):
|
||||
"""
|
||||
Accessing the hemisphere for a coordinate that can't compute it
|
||||
raises C{ValueError}.
|
||||
"""
|
||||
coordinate = base.Coordinate(1.0, None)
|
||||
self.assertRaises(ValueError, lambda: coordinate.hemisphere)
|
||||
|
||||
|
||||
def test_latitudeTooLarge(self):
|
||||
"""
|
||||
Creating a latitude with a value greater than or equal to 90 degrees
|
||||
raises C{ValueError}.
|
||||
"""
|
||||
self.assertRaises(ValueError, _makeLatitude, 150.0)
|
||||
self.assertRaises(ValueError, _makeLatitude, 90.0)
|
||||
|
||||
|
||||
def test_latitudeTooSmall(self):
|
||||
"""
|
||||
Creating a latitude with a value less than or equal to -90 degrees
|
||||
raises C{ValueError}.
|
||||
"""
|
||||
self.assertRaises(ValueError, _makeLatitude, -150.0)
|
||||
self.assertRaises(ValueError, _makeLatitude, -90.0)
|
||||
|
||||
|
||||
def test_longitudeTooLarge(self):
|
||||
"""
|
||||
Creating a longitude with a value greater than or equal to 180 degrees
|
||||
raises C{ValueError}.
|
||||
"""
|
||||
self.assertRaises(ValueError, _makeLongitude, 250.0)
|
||||
self.assertRaises(ValueError, _makeLongitude, 180.0)
|
||||
|
||||
|
||||
def test_longitudeTooSmall(self):
|
||||
"""
|
||||
Creating a longitude with a value less than or equal to -180 degrees
|
||||
raises C{ValueError}.
|
||||
"""
|
||||
self.assertRaises(ValueError, _makeLongitude, -250.0)
|
||||
self.assertRaises(ValueError, _makeLongitude, -180.0)
|
||||
|
||||
|
||||
def test_inDegreesMinutesSeconds(self):
|
||||
"""
|
||||
Coordinate values can be accessed in degrees, minutes, seconds.
|
||||
"""
|
||||
c = base.Coordinate(50.5, Angles.LATITUDE)
|
||||
self.assertEqual(c.inDegreesMinutesSeconds, (50, 30, 0))
|
||||
|
||||
c = base.Coordinate(50.213, Angles.LATITUDE)
|
||||
self.assertEqual(c.inDegreesMinutesSeconds, (50, 12, 46))
|
||||
|
||||
|
||||
def test_unknownAngleInDegreesMinutesSeconds(self):
|
||||
"""
|
||||
If the vaue of a coordinate is C{None}, its values in degrees,
|
||||
minutes, seconds is also C{None}.
|
||||
"""
|
||||
c = base.Coordinate(None, None)
|
||||
self.assertEqual(c.inDegreesMinutesSeconds, None)
|
||||
|
||||
|
||||
|
||||
def _makeLatitude(value):
|
||||
"""
|
||||
Builds and returns a latitude of given value.
|
||||
"""
|
||||
return base.Coordinate(value, Angles.LATITUDE)
|
||||
|
||||
|
||||
|
||||
def _makeLongitude(value):
|
||||
"""
|
||||
Builds and returns a longitude of given value.
|
||||
"""
|
||||
return base.Coordinate(value, Angles.LONGITUDE)
|
||||
|
||||
|
||||
|
||||
class AltitudeTests(TestCase):
|
||||
"""
|
||||
Tests for the L{twisted.positioning.base.Altitude} class.
|
||||
"""
|
||||
def test_value(self):
|
||||
"""
|
||||
Altitudes can be instantiated and reports the correct value in
|
||||
meters and feet, as well as when converted to float.
|
||||
"""
|
||||
altitude = base.Altitude(1.)
|
||||
self.assertEqual(float(altitude), 1.)
|
||||
self.assertEqual(altitude.inMeters, 1.)
|
||||
self.assertEqual(altitude.inFeet, 1./base.METERS_PER_FOOT)
|
||||
|
||||
|
||||
def test_repr(self):
|
||||
"""
|
||||
Altitudes report their type and value in their repr.
|
||||
"""
|
||||
altitude = base.Altitude(1.)
|
||||
self.assertEqual(repr(altitude), "<Altitude (1.0 m)>")
|
||||
|
||||
|
||||
def test_equality(self):
|
||||
"""
|
||||
Altitudes with equal values compare equal.
|
||||
"""
|
||||
firstAltitude = base.Altitude(1.)
|
||||
secondAltitude = base.Altitude(1.)
|
||||
self.assertEqual(firstAltitude, secondAltitude)
|
||||
|
||||
|
||||
def test_inequality(self):
|
||||
"""
|
||||
Altitudes with different values don't compare equal.
|
||||
"""
|
||||
firstAltitude = base.Altitude(1.)
|
||||
secondAltitude = base.Altitude(-1.)
|
||||
self.assertNotEquals(firstAltitude, secondAltitude)
|
||||
|
||||
|
||||
|
||||
class SpeedTests(TestCase):
|
||||
"""
|
||||
Tests for the L{twisted.positioning.base.Speed} class.
|
||||
"""
|
||||
def test_value(self):
|
||||
"""
|
||||
Speeds can be instantiated, and report their value in meters
|
||||
per second, and can be converted to floats.
|
||||
"""
|
||||
speed = base.Speed(50.0)
|
||||
self.assertEqual(speed.inMetersPerSecond, 50.0)
|
||||
self.assertEqual(float(speed), 50.0)
|
||||
|
||||
|
||||
def test_repr(self):
|
||||
"""
|
||||
Speeds report their type and value in their repr.
|
||||
"""
|
||||
speed = base.Speed(50.0)
|
||||
self.assertEqual(repr(speed), "<Speed (50.0 m/s)>")
|
||||
|
||||
|
||||
def test_negativeSpeeds(self):
|
||||
"""
|
||||
Creating a negative speed raises C{ValueError}.
|
||||
"""
|
||||
self.assertRaises(ValueError, base.Speed, -1.0)
|
||||
|
||||
|
||||
def test_inKnots(self):
|
||||
"""
|
||||
A speed can be converted into its value in knots.
|
||||
"""
|
||||
speed = base.Speed(1.0)
|
||||
self.assertEqual(1/base.MPS_PER_KNOT, speed.inKnots)
|
||||
|
||||
|
||||
def test_asFloat(self):
|
||||
"""
|
||||
A speed can be converted into a C{float}.
|
||||
"""
|
||||
self.assertEqual(1.0, float(base.Speed(1.0)))
|
||||
|
||||
|
||||
|
||||
class ClimbTests(TestCase):
|
||||
"""
|
||||
Tests for L{twisted.positioning.base.Climb}.
|
||||
"""
|
||||
def test_simple(self):
|
||||
"""
|
||||
Speeds can be instantiated, and report their value in meters
|
||||
per second, and can be converted to floats.
|
||||
"""
|
||||
climb = base.Climb(42.)
|
||||
self.assertEqual(climb.inMetersPerSecond, 42.)
|
||||
self.assertEqual(float(climb), 42.)
|
||||
|
||||
|
||||
def test_repr(self):
|
||||
"""
|
||||
Climbs report their type and value in their repr.
|
||||
"""
|
||||
climb = base.Climb(42.)
|
||||
self.assertEqual(repr(climb), "<Climb (42.0 m/s)>")
|
||||
|
||||
|
||||
def test_negativeClimbs(self):
|
||||
"""
|
||||
Climbs can have negative values, and still report that value
|
||||
in meters per second and when converted to floats.
|
||||
"""
|
||||
climb = base.Climb(-42.)
|
||||
self.assertEqual(climb.inMetersPerSecond, -42.)
|
||||
self.assertEqual(float(climb), -42.)
|
||||
|
||||
|
||||
def test_speedInKnots(self):
|
||||
"""
|
||||
A climb can be converted into its value in knots.
|
||||
"""
|
||||
climb = base.Climb(1.0)
|
||||
self.assertEqual(1/base.MPS_PER_KNOT, climb.inKnots)
|
||||
|
||||
|
||||
def test_asFloat(self):
|
||||
"""
|
||||
A climb can be converted into a C{float}.
|
||||
"""
|
||||
self.assertEqual(1.0, float(base.Climb(1.0)))
|
||||
|
||||
|
||||
|
||||
class PositionErrorTests(TestCase):
|
||||
"""
|
||||
Tests for L{twisted.positioning.base.PositionError}.
|
||||
"""
|
||||
def test_allUnset(self):
|
||||
"""
|
||||
In an empty L{base.PositionError} with no invariant testing, all
|
||||
dilutions of positions are C{None}.
|
||||
"""
|
||||
positionError = base.PositionError()
|
||||
self.assertEqual(positionError.pdop, None)
|
||||
self.assertEqual(positionError.hdop, None)
|
||||
self.assertEqual(positionError.vdop, None)
|
||||
|
||||
|
||||
def test_allUnsetWithInvariant(self):
|
||||
"""
|
||||
In an empty L{base.PositionError} with invariant testing, all
|
||||
dilutions of positions are C{None}.
|
||||
"""
|
||||
positionError = base.PositionError(testInvariant=True)
|
||||
self.assertEqual(positionError.pdop, None)
|
||||
self.assertEqual(positionError.hdop, None)
|
||||
self.assertEqual(positionError.vdop, None)
|
||||
|
||||
|
||||
def test_withoutInvariant(self):
|
||||
"""
|
||||
L{base.PositionError}s can be instantiated with just a HDOP.
|
||||
"""
|
||||
positionError = base.PositionError(hdop=1.0)
|
||||
self.assertEqual(positionError.hdop, 1.0)
|
||||
|
||||
|
||||
def test_withInvariant(self):
|
||||
"""
|
||||
Creating a simple L{base.PositionError} with just a HDOP while
|
||||
checking the invariant works.
|
||||
"""
|
||||
positionError = base.PositionError(hdop=1.0, testInvariant=True)
|
||||
self.assertEqual(positionError.hdop, 1.0)
|
||||
|
||||
|
||||
def test_invalidWithoutInvariant(self):
|
||||
"""
|
||||
Creating a L{base.PositionError} with values set to an impossible
|
||||
combination works if the invariant is not checked.
|
||||
"""
|
||||
error = base.PositionError(pdop=1.0, vdop=1.0, hdop=1.0)
|
||||
self.assertEqual(error.pdop, 1.0)
|
||||
self.assertEqual(error.hdop, 1.0)
|
||||
self.assertEqual(error.vdop, 1.0)
|
||||
|
||||
|
||||
def test_invalidWithInvariant(self):
|
||||
"""
|
||||
Creating a L{base.PositionError} with values set to an impossible
|
||||
combination raises C{ValueError} if the invariant is being tested.
|
||||
"""
|
||||
self.assertRaises(ValueError, base.PositionError,
|
||||
pdop=1.0, vdop=1.0, hdop=1.0, testInvariant=True)
|
||||
|
||||
|
||||
def test_setDOPWithoutInvariant(self):
|
||||
"""
|
||||
You can set the PDOP value to value inconsisted with HDOP and VDOP
|
||||
when not checking the invariant.
|
||||
"""
|
||||
pe = base.PositionError(hdop=1.0, vdop=1.0)
|
||||
pe.pdop = 100.0
|
||||
self.assertEqual(pe.pdop, 100.0)
|
||||
|
||||
|
||||
def test_setDOPWithInvariant(self):
|
||||
"""
|
||||
Attempting to set the PDOP value to value inconsisted with HDOP and
|
||||
VDOP when checking the invariant raises C{ValueError}.
|
||||
"""
|
||||
pe = base.PositionError(hdop=1.0, vdop=1.0, testInvariant=True)
|
||||
pdop = pe.pdop
|
||||
|
||||
def setPDOP(pe):
|
||||
pe.pdop = 100.0
|
||||
|
||||
self.assertRaises(ValueError, setPDOP, pe)
|
||||
self.assertEqual(pe.pdop, pdop)
|
||||
|
||||
|
||||
REPR_TEMPLATE = "<PositionError (pdop: %s, hdop: %s, vdop: %s)>"
|
||||
|
||||
|
||||
def _testDOP(self, pe, pdop, hdop, vdop):
|
||||
"""
|
||||
Tests the DOP values in a position error, and the repr of that
|
||||
position error.
|
||||
|
||||
@param pe: The position error under test.
|
||||
@type pe: C{PositionError}
|
||||
@param pdop: The expected position dilution of precision.
|
||||
@type pdop: C{float} or C{NoneType}
|
||||
@param hdop: The expected horizontal dilution of precision.
|
||||
@type hdop: C{float} or C{NoneType}
|
||||
@param vdop: The expected vertical dilution of precision.
|
||||
@type vdop: C{float} or C{NoneType}
|
||||
"""
|
||||
self.assertEqual(pe.pdop, pdop)
|
||||
self.assertEqual(pe.hdop, hdop)
|
||||
self.assertEqual(pe.vdop, vdop)
|
||||
self.assertEqual(repr(pe), self.REPR_TEMPLATE % (pdop, hdop, vdop))
|
||||
|
||||
|
||||
def test_positionAndHorizontalSet(self):
|
||||
"""
|
||||
The VDOP is correctly determined from PDOP and HDOP.
|
||||
"""
|
||||
pdop, hdop = 2.0, 1.0
|
||||
vdop = (pdop**2 - hdop**2)**.5
|
||||
pe = base.PositionError(pdop=pdop, hdop=hdop)
|
||||
self._testDOP(pe, pdop, hdop, vdop)
|
||||
|
||||
|
||||
def test_positionAndVerticalSet(self):
|
||||
"""
|
||||
The HDOP is correctly determined from PDOP and VDOP.
|
||||
"""
|
||||
pdop, vdop = 2.0, 1.0
|
||||
hdop = (pdop**2 - vdop**2)**.5
|
||||
pe = base.PositionError(pdop=pdop, vdop=vdop)
|
||||
self._testDOP(pe, pdop, hdop, vdop)
|
||||
|
||||
|
||||
def test_horizontalAndVerticalSet(self):
|
||||
"""
|
||||
The PDOP is correctly determined from HDOP and VDOP.
|
||||
"""
|
||||
hdop, vdop = 1.0, 1.0
|
||||
pdop = (hdop**2 + vdop**2)**.5
|
||||
pe = base.PositionError(hdop=hdop, vdop=vdop)
|
||||
self._testDOP(pe, pdop, hdop, vdop)
|
||||
|
||||
|
||||
|
||||
class BeaconInformationTests(TestCase):
|
||||
"""
|
||||
Tests for L{twisted.positioning.base.BeaconInformation}.
|
||||
"""
|
||||
def test_minimal(self):
|
||||
"""
|
||||
For an empty beacon information object, the number of used
|
||||
beacons is zero, the number of seen beacons is zero, and the
|
||||
repr of the object reflects that.
|
||||
"""
|
||||
bi = base.BeaconInformation()
|
||||
self.assertEqual(len(bi.usedBeacons), 0)
|
||||
expectedRepr = ("<BeaconInformation ("
|
||||
"used beacons (0): [], "
|
||||
"unused beacons: [])>")
|
||||
self.assertEqual(repr(bi), expectedRepr)
|
||||
|
||||
|
||||
satelliteKwargs = {"azimuth": 1, "elevation": 1, "signalToNoiseRatio": 1.}
|
||||
|
||||
|
||||
def test_simple(self):
|
||||
"""
|
||||
Tests a beacon information with a bunch of satellites, none of
|
||||
which used in computing a fix.
|
||||
"""
|
||||
def _buildSatellite(**kw):
|
||||
kwargs = dict(self.satelliteKwargs)
|
||||
kwargs.update(kw)
|
||||
return base.Satellite(**kwargs)
|
||||
|
||||
beacons = set()
|
||||
for prn in range(1, 10):
|
||||
beacons.add(_buildSatellite(identifier=prn))
|
||||
|
||||
bi = base.BeaconInformation(beacons)
|
||||
|
||||
self.assertEqual(len(bi.seenBeacons), 9)
|
||||
self.assertEqual(len(bi.usedBeacons), 0)
|
||||
self.assertEqual(repr(bi),
|
||||
"<BeaconInformation (used beacons (0): [], "
|
||||
"unused beacons: ["
|
||||
"<Satellite (1), azimuth: 1, elevation: 1, snr: 1.0>, "
|
||||
"<Satellite (2), azimuth: 1, elevation: 1, snr: 1.0>, "
|
||||
"<Satellite (3), azimuth: 1, elevation: 1, snr: 1.0>, "
|
||||
"<Satellite (4), azimuth: 1, elevation: 1, snr: 1.0>, "
|
||||
"<Satellite (5), azimuth: 1, elevation: 1, snr: 1.0>, "
|
||||
"<Satellite (6), azimuth: 1, elevation: 1, snr: 1.0>, "
|
||||
"<Satellite (7), azimuth: 1, elevation: 1, snr: 1.0>, "
|
||||
"<Satellite (8), azimuth: 1, elevation: 1, snr: 1.0>, "
|
||||
"<Satellite (9), azimuth: 1, elevation: 1, snr: 1.0>"
|
||||
"])>")
|
||||
|
||||
|
||||
def test_someSatellitesUsed(self):
|
||||
"""
|
||||
Tests a beacon information with a bunch of satellites, some of
|
||||
them used in computing a fix.
|
||||
"""
|
||||
bi = base.BeaconInformation()
|
||||
|
||||
for prn in range(1, 10):
|
||||
satellite = base.Satellite(identifier=prn, **self.satelliteKwargs)
|
||||
bi.seenBeacons.add(satellite)
|
||||
if prn % 2:
|
||||
bi.usedBeacons.add(satellite)
|
||||
|
||||
self.assertEqual(len(bi.seenBeacons), 9)
|
||||
self.assertEqual(len(bi.usedBeacons), 5)
|
||||
|
||||
self.assertEqual(repr(bi),
|
||||
"<BeaconInformation (used beacons (5): ["
|
||||
"<Satellite (1), azimuth: 1, elevation: 1, snr: 1.0>, "
|
||||
"<Satellite (3), azimuth: 1, elevation: 1, snr: 1.0>, "
|
||||
"<Satellite (5), azimuth: 1, elevation: 1, snr: 1.0>, "
|
||||
"<Satellite (7), azimuth: 1, elevation: 1, snr: 1.0>, "
|
||||
"<Satellite (9), azimuth: 1, elevation: 1, snr: 1.0>], "
|
||||
"unused beacons: ["
|
||||
"<Satellite (2), azimuth: 1, elevation: 1, snr: 1.0>, "
|
||||
"<Satellite (4), azimuth: 1, elevation: 1, snr: 1.0>, "
|
||||
"<Satellite (6), azimuth: 1, elevation: 1, snr: 1.0>, "
|
||||
"<Satellite (8), azimuth: 1, elevation: 1, snr: 1.0>])>")
|
||||
|
||||
|
||||
|
||||
class PositioningBeaconTests(TestCase):
|
||||
"""
|
||||
Tests for L{base.PositioningBeacon}.
|
||||
"""
|
||||
def test_interface(self):
|
||||
"""
|
||||
Tests that L{base.PositioningBeacon} implements L{IPositioningBeacon}.
|
||||
"""
|
||||
implements = IPositioningBeacon.implementedBy(base.PositioningBeacon)
|
||||
self.assertTrue(implements)
|
||||
verify.verifyObject(IPositioningBeacon, base.PositioningBeacon(1))
|
||||
|
||||
|
||||
def test_repr(self):
|
||||
"""
|
||||
Tests the repr of a positioning beacon.
|
||||
"""
|
||||
self.assertEqual(repr(base.PositioningBeacon("A")), "<Beacon (A)>")
|
||||
|
||||
|
||||
|
||||
class SatelliteTests(TestCase):
|
||||
"""
|
||||
Tests for L{twisted.positioning.base.Satellite}.
|
||||
"""
|
||||
def test_minimal(self):
|
||||
"""
|
||||
Tests a minimal satellite that only has a known PRN.
|
||||
|
||||
Tests that the azimuth, elevation and signal to noise ratios
|
||||
are C{None} and verifies the repr.
|
||||
"""
|
||||
s = base.Satellite(1)
|
||||
self.assertEqual(s.identifier, 1)
|
||||
self.assertEqual(s.azimuth, None)
|
||||
self.assertEqual(s.elevation, None)
|
||||
self.assertEqual(s.signalToNoiseRatio, None)
|
||||
self.assertEqual(repr(s), "<Satellite (1), azimuth: None, "
|
||||
"elevation: None, snr: None>")
|
||||
|
||||
|
||||
def test_simple(self):
|
||||
"""
|
||||
Tests a minimal satellite that only has a known PRN.
|
||||
|
||||
Tests that the azimuth, elevation and signal to noise ratios
|
||||
are correct and verifies the repr.
|
||||
"""
|
||||
s = base.Satellite(identifier=1,
|
||||
azimuth=270.,
|
||||
elevation=30.,
|
||||
signalToNoiseRatio=25.)
|
||||
|
||||
self.assertEqual(s.identifier, 1)
|
||||
self.assertEqual(s.azimuth, 270.)
|
||||
self.assertEqual(s.elevation, 30.)
|
||||
self.assertEqual(s.signalToNoiseRatio, 25.)
|
||||
self.assertEqual(repr(s), "<Satellite (1), azimuth: 270.0, "
|
||||
"elevation: 30.0, snr: 25.0>")
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,166 @@
|
|||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
"""
|
||||
Tests for positioning sentences.
|
||||
"""
|
||||
import itertools
|
||||
|
||||
from twisted.positioning import _sentence
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
sentinelValueOne = "someStringValue"
|
||||
sentinelValueTwo = "someOtherStringValue"
|
||||
|
||||
|
||||
|
||||
class DummyProtocol(object):
|
||||
"""
|
||||
A simple, fake protocol.
|
||||
"""
|
||||
@staticmethod
|
||||
def getSentenceAttributes():
|
||||
return ["type", sentinelValueOne, sentinelValueTwo]
|
||||
|
||||
|
||||
|
||||
class DummySentence(_sentence._BaseSentence):
|
||||
"""
|
||||
A sentence for L{DummyProtocol}.
|
||||
"""
|
||||
ALLOWED_ATTRIBUTES = DummyProtocol.getSentenceAttributes()
|
||||
|
||||
|
||||
|
||||
class MixinProtocol(_sentence._PositioningSentenceProducerMixin):
|
||||
"""
|
||||
A simple, fake protocol that declaratively tells you the sentences
|
||||
it produces using L{base.PositioningSentenceProducerMixin}.
|
||||
"""
|
||||
_SENTENCE_CONTENTS = {
|
||||
None: [
|
||||
sentinelValueOne,
|
||||
sentinelValueTwo,
|
||||
None # See MixinTests.test_noNoneInSentenceAttributes
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
|
||||
class MixinSentence(_sentence._BaseSentence):
|
||||
"""
|
||||
A sentence for L{MixinProtocol}.
|
||||
"""
|
||||
ALLOWED_ATTRIBUTES = MixinProtocol.getSentenceAttributes()
|
||||
|
||||
|
||||
|
||||
class SentenceTestsMixin(object):
|
||||
"""
|
||||
Tests for positioning protocols and their respective sentences.
|
||||
"""
|
||||
def test_attributeAccess(self):
|
||||
"""
|
||||
A sentence attribute gets the correct value, and accessing an
|
||||
unset attribute (which is specified as being a valid sentence
|
||||
attribute) gets C{None}.
|
||||
"""
|
||||
thisSentinel = object()
|
||||
sentence = self.sentenceClass({sentinelValueOne: thisSentinel})
|
||||
self.assertEqual(getattr(sentence, sentinelValueOne), thisSentinel)
|
||||
self.assertEqual(getattr(sentence, sentinelValueTwo), None)
|
||||
|
||||
|
||||
def test_raiseOnMissingAttributeAccess(self):
|
||||
"""
|
||||
Accessing a nonexistant attribute raises C{AttributeError}.
|
||||
"""
|
||||
sentence = self.sentenceClass({})
|
||||
self.assertRaises(AttributeError, getattr, sentence, "BOGUS")
|
||||
|
||||
|
||||
def test_raiseOnBadAttributeAccess(self):
|
||||
"""
|
||||
Accessing bogus attributes raises C{AttributeError}, *even*
|
||||
when that attribute actually is in the sentence data.
|
||||
"""
|
||||
sentence = self.sentenceClass({"BOGUS": None})
|
||||
self.assertRaises(AttributeError, getattr, sentence, "BOGUS")
|
||||
|
||||
|
||||
sentenceType = "tummies"
|
||||
reprTemplate = "<%s (%s) {%s}>"
|
||||
|
||||
|
||||
def _expectedRepr(self, sentenceType="unknown type", dataRepr=""):
|
||||
"""
|
||||
Builds the expected repr for a sentence.
|
||||
|
||||
@param sentenceType: The name of the sentence type (e.g "GPGGA").
|
||||
@type sentenceType: C{str}
|
||||
@param dataRepr: The repr of the data in the sentence.
|
||||
@type dataRepr: C{str}
|
||||
@return: The expected repr of the sentence.
|
||||
@rtype: C{str}
|
||||
"""
|
||||
clsName = self.sentenceClass.__name__
|
||||
return self.reprTemplate % (clsName, sentenceType, dataRepr)
|
||||
|
||||
|
||||
def test_unknownTypeRepr(self):
|
||||
"""
|
||||
Test the repr of an empty sentence of unknown type.
|
||||
"""
|
||||
sentence = self.sentenceClass({})
|
||||
expectedRepr = self._expectedRepr()
|
||||
self.assertEqual(repr(sentence), expectedRepr)
|
||||
|
||||
|
||||
def test_knownTypeRepr(self):
|
||||
"""
|
||||
Test the repr of an empty sentence of known type.
|
||||
"""
|
||||
sentence = self.sentenceClass({"type": self.sentenceType})
|
||||
expectedRepr = self._expectedRepr(self.sentenceType)
|
||||
self.assertEqual(repr(sentence), expectedRepr)
|
||||
|
||||
|
||||
|
||||
class DummyTests(TestCase, SentenceTestsMixin):
|
||||
"""
|
||||
Tests for protocol classes that implement the appropriate interface
|
||||
(L{ipositioning.IPositioningSentenceProducer}) manually.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.protocol = DummyProtocol()
|
||||
self.sentenceClass = DummySentence
|
||||
|
||||
|
||||
|
||||
class MixinTests(TestCase, SentenceTestsMixin):
|
||||
"""
|
||||
Tests for protocols deriving from L{base.PositioningSentenceProducerMixin}
|
||||
and their sentences.
|
||||
"""
|
||||
def setUp(self):
|
||||
self.protocol = MixinProtocol()
|
||||
self.sentenceClass = MixinSentence
|
||||
|
||||
|
||||
def test_noNoneInSentenceAttributes(self):
|
||||
"""
|
||||
C{None} does not appear in the sentence attributes of the
|
||||
protocol, even though it's in the specification.
|
||||
|
||||
This is because C{None} is a placeholder for parts of the sentence you
|
||||
don't really need or want, but there are some bits later on in the
|
||||
sentence that you do want. The alternative would be to have to specify
|
||||
things like "_UNUSED0", "_UNUSED1"... which would end up cluttering
|
||||
the sentence data and eventually adapter state.
|
||||
"""
|
||||
sentenceAttributes = self.protocol.getSentenceAttributes()
|
||||
self.assertNotIn(None, sentenceAttributes)
|
||||
|
||||
sentenceContents = self.protocol._SENTENCE_CONTENTS
|
||||
sentenceSpecAttributes = itertools.chain(*sentenceContents.values())
|
||||
self.assertIn(None, sentenceSpecAttributes)
|
||||
Loading…
Add table
Add a link
Reference in a new issue