Open Media Library Platform

This commit is contained in:
j 2013-10-11 19:28:32 +02:00
commit 411ad5b16f
5849 changed files with 1778641 additions and 0 deletions

View file

@ -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
"""

View file

@ -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

View 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'
]

View file

@ -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"
]

View 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"
]

View file

@ -0,0 +1,5 @@
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.
"""
Tests for the Twisted positioning framework.
"""

View file

@ -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)

View file

@ -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

View file

@ -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)