mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-22 14:11:08 -05:00
okay fine
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
# -*- test-case-name: twisted.positioning.test -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
"""
|
||||
Twisted Positioning: Framework for applications that make use of positioning.
|
||||
|
||||
@since: 14.0
|
||||
"""
|
||||
@@ -0,0 +1,118 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
"""
|
||||
Generic sentence handling tools: hopefully reusable.
|
||||
"""
|
||||
from typing import Set
|
||||
|
||||
|
||||
class _BaseSentence:
|
||||
"""
|
||||
A base sentence class for a particular protocol.
|
||||
|
||||
Using this base class, specific sentence classes can almost automatically
|
||||
be created for a particular protocol.
|
||||
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 presentAttributes: 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[str] = 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 L{None})
|
||||
"""
|
||||
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 = f"{className} sentences have no {name} attributes"
|
||||
raise AttributeError(msg)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Returns a textual representation of this sentence.
|
||||
|
||||
@return: A textual representation of this sentence.
|
||||
@rtype: C{str}
|
||||
"""
|
||||
items = self._sentenceData.items()
|
||||
data = [f"{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 f"<{className} ({typeRepr}) {{{dataRepr}}}>"
|
||||
|
||||
|
||||
class _PositioningSentenceProducerMixin:
|
||||
"""
|
||||
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 = {"type"}
|
||||
for attributeList in cls._SENTENCE_CONTENTS.values():
|
||||
for attribute in attributeList:
|
||||
if attribute is None:
|
||||
continue
|
||||
attributes.add(attribute)
|
||||
|
||||
return attributes
|
||||
926
.venv/lib/python3.12/site-packages/twisted/positioning/base.py
Normal file
926
.venv/lib/python3.12/site-packages/twisted/positioning/base.py
Normal file
@@ -0,0 +1,926 @@
|
||||
# -*- 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 typing import ClassVar, Sequence
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from constantly import NamedConstant, Names
|
||||
|
||||
from twisted.positioning import ipositioning
|
||||
from twisted.python.util import FancyEqMixin
|
||||
|
||||
MPS_PER_KNOT = 0.5144444444444444
|
||||
MPS_PER_KPH = 0.27777777777777777
|
||||
METERS_PER_FOOT = 0.3048
|
||||
|
||||
|
||||
class Angles(Names):
|
||||
"""
|
||||
The types of angles.
|
||||
|
||||
@cvar LATITUDE: Angle representing a latitude of an object.
|
||||
@type LATITUDE: L{NamedConstant}
|
||||
|
||||
@cvar LONGITUDE: Angle representing the longitude of an object.
|
||||
@type LONGITUDE: L{NamedConstant}
|
||||
|
||||
@cvar HEADING: Angle representing the heading of an object.
|
||||
@type HEADING: L{NamedConstant}
|
||||
|
||||
@cvar VARIATION: Angle representing a magnetic variation.
|
||||
@type VARIATION: L{NamedConstant}
|
||||
|
||||
"""
|
||||
|
||||
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:
|
||||
"""
|
||||
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.positionErrorReceived} 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(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: ClassVar[Sequence[str]] = (
|
||||
"angleType",
|
||||
"inDecimalDegrees",
|
||||
)
|
||||
|
||||
def __init__(self, angle=None, angleType=None):
|
||||
"""
|
||||
Initializes an angle.
|
||||
|
||||
@param angle: The value of the angle in decimal degrees. (L{None} if
|
||||
unknown).
|
||||
@type angle: C{float} or L{None}
|
||||
|
||||
@param angleType: A symbolic constant describing the angle type. Should
|
||||
be one of L{Angles} 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 L{None} if the
|
||||
angle is unknown.
|
||||
@rtype: C{float} (or L{None})
|
||||
"""
|
||||
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. L{None} if
|
||||
the angle is unknown.
|
||||
@rtype: 3-C{tuple} of C{int} (or L{None})
|
||||
"""
|
||||
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) -> str:
|
||||
"""
|
||||
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 L{None}.
|
||||
@type variation: C{Angle} or L{None}.
|
||||
@ivar correctedHeading: The heading, corrected for variation. If the
|
||||
variation is unknown (L{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 an 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 L{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 L{None}.
|
||||
@rtype: C{float} or L{None}
|
||||
"""
|
||||
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) -> str:
|
||||
"""
|
||||
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 "<{} ({}, {})>".format(
|
||||
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 L{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(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) -> str:
|
||||
"""
|
||||
Returns a string representation of this altitude.
|
||||
|
||||
@return: The string representation.
|
||||
@rtype: C{str}
|
||||
"""
|
||||
return f"<Altitude ({self._altitude} m)>"
|
||||
|
||||
|
||||
class _BaseSpeed(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) -> str:
|
||||
"""
|
||||
Returns a string representation of this speed object.
|
||||
|
||||
@return: The string representation.
|
||||
@rtype: C{str}
|
||||
"""
|
||||
speedValue = round(self.inMetersPerSecond, 2)
|
||||
return f"<{self.__class__.__name__} ({speedValue} m/s)>"
|
||||
|
||||
|
||||
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(f"negative speed: {speed!r}")
|
||||
|
||||
_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(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. L{None} if unknown.
|
||||
@type pdop: C{float} or L{None}
|
||||
@ivar hdop: The horizontal dilution of precision. L{None} if unknown.
|
||||
@type hdop: C{float} or L{None}
|
||||
@ivar vdop: The vertical dilution of precision. L{None} if unknown.
|
||||
@type vdop: C{float} or L{None}
|
||||
"""
|
||||
|
||||
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. L{None} if unknown.
|
||||
@type pdop: C{float} or L{None}
|
||||
@param hdop: The horizontal dilution of precision. L{None} if unknown.
|
||||
@type hdop: C{float} or L{None}
|
||||
@param vdop: The vertical dilution of precision. L{None} if unknown.
|
||||
@type vdop: C{float} or L{None}
|
||||
@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: L{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) ** 0.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) ** 0.5,
|
||||
],
|
||||
"hdop": [
|
||||
lambda self: float(self._hdop),
|
||||
lambda self: (self._pdop**2 - self._vdop**2) ** 0.5,
|
||||
],
|
||||
"vdop": [
|
||||
lambda self: float(self._vdop),
|
||||
lambda self: (self._pdop**2 - self._hdop**2) ** 0.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, L{None} otherwise.
|
||||
@rtype: C{float} or L{None}
|
||||
"""
|
||||
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
|
||||
|
||||
@property
|
||||
def pdop(self):
|
||||
return self._getDOP("pdop")
|
||||
|
||||
@pdop.setter
|
||||
def pdop(self, value):
|
||||
return self._setDOP("pdop", value)
|
||||
|
||||
@property
|
||||
def hdop(self):
|
||||
return self._getDOP("hdop")
|
||||
|
||||
@hdop.setter
|
||||
def hdop(self, value):
|
||||
return self._setDOP("hdop", value)
|
||||
|
||||
@property
|
||||
def vdop(self):
|
||||
return self._getDOP("vdop")
|
||||
|
||||
@vdop.setter
|
||||
def vdop(self, value):
|
||||
return self._setDOP("vdop", value)
|
||||
|
||||
_REPR_TEMPLATE = "<PositionError (pdop: %s, hdop: %s, vdop: %s)>"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
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: A 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) -> str:
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
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) -> str:
|
||||
"""
|
||||
Returns a string representation of this beacon.
|
||||
|
||||
@return: The string representation.
|
||||
@rtype: C{str}
|
||||
"""
|
||||
return f"<Beacon ({self.identifier})>"
|
||||
|
||||
|
||||
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) -> str:
|
||||
"""
|
||||
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,113 @@
|
||||
# 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 positionError: The position error.
|
||||
@type positionError: 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 dependent 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"]
|
||||
932
.venv/lib/python3.12/site-packages/twisted/positioning/nmea.py
Normal file
932
.venv/lib/python3.12/site-packages/twisted/positioning/nmea.py
Normal file
@@ -0,0 +1,932 @@
|
||||
# -*- 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 datetime
|
||||
import operator
|
||||
from functools import reduce
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from constantly import ValueConstant, Values
|
||||
|
||||
from twisted.positioning import _sentence, base, ipositioning
|
||||
from twisted.positioning.base import Angles
|
||||
from twisted.protocols.basic import LineReceiver
|
||||
from twisted.python.compat import iterbytes, nativeString
|
||||
|
||||
|
||||
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.
|
||||
|
||||
>>> _split(b"$GPGGA,spam,eggs*00")
|
||||
[b'GPGGA', b'spam', b'eggs']
|
||||
|
||||
@param sentence: The NMEA sentence to split.
|
||||
@type sentence: C{bytes}
|
||||
"""
|
||||
if sentence[-3:-2] == b"*": # Sentence with checksum
|
||||
return sentence[1:-3].split(b",")
|
||||
elif sentence[-1:] == b"*": # Sentence without checksum
|
||||
return sentence[1:-1].split(b",")
|
||||
else:
|
||||
raise base.InvalidSentence(f"malformed sentence {sentence}")
|
||||
|
||||
|
||||
def _validateChecksum(sentence):
|
||||
"""
|
||||
Validates the checksum of an NMEA sentence.
|
||||
|
||||
@param sentence: The NMEA sentence to check the checksum of.
|
||||
@type sentence: C{bytes}
|
||||
|
||||
@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:-2] == b"*": # Sentence has a checksum
|
||||
reference, source = int(sentence[-2:], 16), sentence[1:-3]
|
||||
computed = reduce(operator.xor, [ord(x) for x in iterbytes(source)])
|
||||
if computed != reference:
|
||||
raise base.InvalidChecksum(f"{computed:02x} != {reference:02x}")
|
||||
|
||||
|
||||
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{bytes}
|
||||
"""
|
||||
sentence = rawSentence.strip()
|
||||
|
||||
_validateChecksum(sentence)
|
||||
splitSentence = _split(sentence)
|
||||
|
||||
sentenceType = nativeString(splitSentence[0])
|
||||
contents = [nativeString(x) for x in splitSentence[1:]]
|
||||
|
||||
try:
|
||||
keys = self._SENTENCE_CONTENTS[sentenceType]
|
||||
except KeyError:
|
||||
raise ValueError("unknown sentence type %s" % sentenceType)
|
||||
|
||||
sentenceData = {"type": sentenceType}
|
||||
for key, value in zip(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 L{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: Euclidean norm of HDOP and VDOP.
|
||||
@ivar satellitePRN: The unique identifcation number of a particular
|
||||
satellite. 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 satellite 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:
|
||||
"""
|
||||
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(f"{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 L{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(f"unknown coordinate type {coordinateType}")
|
||||
|
||||
hemisphere = getattr(self.currentSentence, hemisphereKey).upper()
|
||||
|
||||
if hemisphere in "NE":
|
||||
return 1
|
||||
elif hemisphere in "SW":
|
||||
return -1
|
||||
else:
|
||||
raise ValueError(f"bad hemisphere/direction: {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 L{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 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 newBeaconInformation: The beacon information object that beacon
|
||||
information will be merged into (if necessary).
|
||||
@type newBeaconInformation: 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{_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,45 @@
|
||||
# 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,863 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
"""
|
||||
Test cases for positioning primitives.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from zope.interface import verify
|
||||
|
||||
from twisted.positioning import base
|
||||
from twisted.positioning.base import Angles, Directions
|
||||
from twisted.positioning.ipositioning import IPositioningBeacon
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
class AngleTests(TestCase):
|
||||
"""
|
||||
Tests for the L{twisted.positioning.base.Angle} class.
|
||||
"""
|
||||
|
||||
def test_empty(self) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
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 L{None}.
|
||||
"""
|
||||
h = base.Heading(1.0)
|
||||
self.assertEqual(h.inDecimalDegrees, 1.0)
|
||||
self.assertEqual(float(h), 1.0)
|
||||
self.assertIsNone(h.variation)
|
||||
self.assertIsNone(h.correctedHeading)
|
||||
|
||||
def test_headingWithoutVariationRepr(self) -> None:
|
||||
"""
|
||||
A repr of a heading with no variation reports its value and that the
|
||||
variation is unknown.
|
||||
"""
|
||||
heading = base.Heading(1.0)
|
||||
expectedRepr = "<Heading (1.0 degrees, unknown variation)>"
|
||||
self.assertEqual(repr(heading), expectedRepr)
|
||||
|
||||
def test_headingWithVariationRepr(self) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
Headings with the same values compare equal.
|
||||
"""
|
||||
self.assertEqual(base.Heading(1.0), base.Heading(1.0))
|
||||
|
||||
def test_valueInequality(self) -> None:
|
||||
"""
|
||||
Headings with different values compare unequal.
|
||||
"""
|
||||
self.assertNotEqual(base.Heading(1.0), base.Heading(2.0))
|
||||
|
||||
def test_zeroHeadingEdgeCase(self) -> None:
|
||||
"""
|
||||
Headings can be instantiated with a value of 0 and no variation.
|
||||
"""
|
||||
base.Heading(0)
|
||||
|
||||
def test_zeroHeading180DegreeVariationEdgeCase(self) -> None:
|
||||
"""
|
||||
Headings can be instantiated with a value of 0 and a variation of 180
|
||||
degrees.
|
||||
"""
|
||||
base.Heading(0, 180)
|
||||
|
||||
def _badValueTest(self, **kw: float) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
Headings can not be instantiated with a value of 360 degrees.
|
||||
"""
|
||||
self._badValueTest(angleValue=360.0)
|
||||
|
||||
def test_badVariationEdgeCase(self) -> None:
|
||||
"""
|
||||
Headings can not be instantiated with a variation of -180 degrees.
|
||||
"""
|
||||
self._badValueTest(variationValue=-180.0)
|
||||
|
||||
def test_negativeHeading(self) -> None:
|
||||
"""
|
||||
Negative heading values raise C{ValueError}.
|
||||
"""
|
||||
self._badValueTest(angleValue=-10.0)
|
||||
|
||||
def test_headingTooLarge(self) -> None:
|
||||
"""
|
||||
Heading values greater than C{360.0} raise C{ValueError}.
|
||||
"""
|
||||
self._badValueTest(angleValue=370.0)
|
||||
|
||||
def test_variationTooNegative(self) -> None:
|
||||
"""
|
||||
Variation values less than C{-180.0} raise C{ValueError}.
|
||||
"""
|
||||
self._badValueTest(variationValue=-190.0)
|
||||
|
||||
def test_variationTooPositive(self) -> None:
|
||||
"""
|
||||
Variation values greater than C{180.0} raise C{ValueError}.
|
||||
"""
|
||||
self._badValueTest(variationValue=190.0)
|
||||
|
||||
def test_correctedHeading(self) -> None:
|
||||
"""
|
||||
A heading with a value and a variation has a corrected heading.
|
||||
"""
|
||||
h = base.Heading.fromFloats(1.0, variationValue=-10.0)
|
||||
self.assertEqual(h.correctedHeading, base.Angle(11.0, Angles.HEADING))
|
||||
|
||||
def test_correctedHeadingOverflow(self) -> None:
|
||||
"""
|
||||
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.0, variationValue=-2.0)
|
||||
self.assertEqual(h.correctedHeading, base.Angle(1.0, Angles.HEADING))
|
||||
|
||||
def test_correctedHeadingOverflowEdgeCase(self) -> None:
|
||||
"""
|
||||
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.0, variationValue=-1.0)
|
||||
self.assertEqual(h.correctedHeading, base.Angle(0.0, Angles.HEADING))
|
||||
|
||||
def test_correctedHeadingUnderflow(self) -> None:
|
||||
"""
|
||||
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.0, variationValue=2.0)
|
||||
self.assertEqual(h.correctedHeading, base.Angle(359.0, Angles.HEADING))
|
||||
|
||||
def test_correctedHeadingUnderflowEdgeCase(self) -> None:
|
||||
"""
|
||||
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.0, variationValue=1.0)
|
||||
self.assertEqual(h.correctedHeading, base.Angle(0.0, Angles.HEADING))
|
||||
|
||||
def test_setVariationSign(self) -> None:
|
||||
"""
|
||||
Setting the sign of a heading changes the variation sign.
|
||||
"""
|
||||
h = base.Heading.fromFloats(1.0, variationValue=1.0)
|
||||
h.setSign(1)
|
||||
self.assertEqual(h.variation.inDecimalDegrees, 1.0)
|
||||
h.setSign(-1)
|
||||
self.assertEqual(h.variation.inDecimalDegrees, -1.0)
|
||||
|
||||
def test_setBadVariationSign(self) -> None:
|
||||
"""
|
||||
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.0, variationValue=1.0)
|
||||
self.assertRaises(ValueError, h.setSign, -50)
|
||||
self.assertEqual(h.variation.inDecimalDegrees, 1.0)
|
||||
|
||||
self.assertRaises(ValueError, h.setSign, 0)
|
||||
self.assertEqual(h.variation.inDecimalDegrees, 1.0)
|
||||
|
||||
self.assertRaises(ValueError, h.setSign, 50)
|
||||
self.assertEqual(h.variation.inDecimalDegrees, 1.0)
|
||||
|
||||
def test_setUnknownVariationSign(self) -> None:
|
||||
"""
|
||||
Setting the sign on a heading with unknown variation raises
|
||||
C{ValueError}.
|
||||
"""
|
||||
h = base.Heading.fromFloats(1.0)
|
||||
self.assertIsNone(h.variation.inDecimalDegrees)
|
||||
self.assertRaises(ValueError, h.setSign, 1)
|
||||
|
||||
|
||||
class CoordinateTests(TestCase):
|
||||
def test_float(self) -> None:
|
||||
"""
|
||||
Coordinates can be converted to floats.
|
||||
"""
|
||||
coordinate = base.Coordinate(10.0)
|
||||
self.assertEqual(float(coordinate), 10.0)
|
||||
|
||||
def test_repr(self) -> None:
|
||||
"""
|
||||
Coordinates that aren't explicitly latitudes or longitudes have an
|
||||
appropriate repr.
|
||||
"""
|
||||
coordinate = base.Coordinate(10.0)
|
||||
expectedRepr = f"<Angle of unknown type ({10.0} degrees)>"
|
||||
self.assertEqual(repr(coordinate), expectedRepr)
|
||||
|
||||
def test_positiveLatitude(self) -> None:
|
||||
"""
|
||||
Positive latitudes have a repr that specifies their type and value.
|
||||
"""
|
||||
coordinate = base.Coordinate(10.0, Angles.LATITUDE)
|
||||
expectedRepr = f"<Latitude ({10.0} degrees)>"
|
||||
self.assertEqual(repr(coordinate), expectedRepr)
|
||||
|
||||
def test_negativeLatitude(self) -> None:
|
||||
"""
|
||||
Negative latitudes have a repr that specifies their type and value.
|
||||
"""
|
||||
coordinate = base.Coordinate(-50.0, Angles.LATITUDE)
|
||||
expectedRepr = f"<Latitude ({-50.0} degrees)>"
|
||||
self.assertEqual(repr(coordinate), expectedRepr)
|
||||
|
||||
def test_positiveLongitude(self) -> None:
|
||||
"""
|
||||
Positive longitudes have a repr that specifies their type and value.
|
||||
"""
|
||||
longitude = base.Coordinate(50.0, Angles.LONGITUDE)
|
||||
expectedRepr = f"<Longitude ({50.0} degrees)>"
|
||||
self.assertEqual(repr(longitude), expectedRepr)
|
||||
|
||||
def test_negativeLongitude(self) -> None:
|
||||
"""
|
||||
Negative longitudes have a repr that specifies their type and value.
|
||||
"""
|
||||
longitude = base.Coordinate(-50.0, Angles.LONGITUDE)
|
||||
expectedRepr = f"<Longitude ({-50.0} degrees)>"
|
||||
self.assertEqual(repr(longitude), expectedRepr)
|
||||
|
||||
def test_bogusCoordinateType(self) -> None:
|
||||
"""
|
||||
Creating coordinates with bogus types rasies C{ValueError}.
|
||||
"""
|
||||
self.assertRaises(ValueError, base.Coordinate, 150.0, "BOGUS")
|
||||
|
||||
def test_angleTypeNotCoordinate(self) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
Coordinates with the same value and type are equal.
|
||||
"""
|
||||
|
||||
def makeCoordinate() -> base.Coordinate:
|
||||
return base.Coordinate(1.0, Angles.LONGITUDE)
|
||||
|
||||
self.assertEqual(makeCoordinate(), makeCoordinate())
|
||||
|
||||
def test_differentAnglesInequality(self) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
Setting the sign on a coordinate sets the sign of the value of the
|
||||
coordinate.
|
||||
"""
|
||||
c = base.Coordinate(50.0, Angles.LATITUDE)
|
||||
c.setSign(1)
|
||||
self.assertEqual(c.inDecimalDegrees, 50.0)
|
||||
c.setSign(-1)
|
||||
self.assertEqual(c.inDecimalDegrees, -50.0)
|
||||
|
||||
def test_badVariationSign(self) -> None:
|
||||
"""
|
||||
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.0)
|
||||
|
||||
self.assertRaises(ValueError, c.setSign, 0)
|
||||
self.assertEqual(c.inDecimalDegrees, 50.0)
|
||||
|
||||
self.assertRaises(ValueError, c.setSign, 50)
|
||||
self.assertEqual(c.inDecimalDegrees, 50.0)
|
||||
|
||||
def test_northernHemisphere(self) -> None:
|
||||
"""
|
||||
Positive latitudes are in the northern hemisphere.
|
||||
"""
|
||||
coordinate = base.Coordinate(1.0, Angles.LATITUDE)
|
||||
self.assertEqual(coordinate.hemisphere, Directions.NORTH)
|
||||
|
||||
def test_easternHemisphere(self) -> None:
|
||||
"""
|
||||
Positive longitudes are in the eastern hemisphere.
|
||||
"""
|
||||
coordinate = base.Coordinate(1.0, Angles.LONGITUDE)
|
||||
self.assertEqual(coordinate.hemisphere, Directions.EAST)
|
||||
|
||||
def test_southernHemisphere(self) -> None:
|
||||
"""
|
||||
Negative latitudes are in the southern hemisphere.
|
||||
"""
|
||||
coordinate = base.Coordinate(-1.0, Angles.LATITUDE)
|
||||
self.assertEqual(coordinate.hemisphere, Directions.SOUTH)
|
||||
|
||||
def test_westernHemisphere(self) -> None:
|
||||
"""
|
||||
Negative longitudes are in the western hemisphere.
|
||||
"""
|
||||
coordinate = base.Coordinate(-1.0, Angles.LONGITUDE)
|
||||
self.assertEqual(coordinate.hemisphere, Directions.WEST)
|
||||
|
||||
def test_badHemisphere(self) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
If the vaue of a coordinate is L{None}, its values in degrees,
|
||||
minutes, seconds is also L{None}.
|
||||
"""
|
||||
c = base.Coordinate(None, None)
|
||||
self.assertIsNone(c.inDegreesMinutesSeconds)
|
||||
|
||||
|
||||
def _makeLatitude(value: float) -> base.Coordinate:
|
||||
"""
|
||||
Builds and returns a latitude of given value.
|
||||
"""
|
||||
return base.Coordinate(value, Angles.LATITUDE)
|
||||
|
||||
|
||||
def _makeLongitude(value: float) -> base.Coordinate:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
Altitudes can be instantiated and reports the correct value in
|
||||
meters and feet, as well as when converted to float.
|
||||
"""
|
||||
altitude = base.Altitude(1.0)
|
||||
self.assertEqual(float(altitude), 1.0)
|
||||
self.assertEqual(altitude.inMeters, 1.0)
|
||||
self.assertEqual(altitude.inFeet, 1.0 / base.METERS_PER_FOOT)
|
||||
|
||||
def test_repr(self) -> None:
|
||||
"""
|
||||
Altitudes report their type and value in their repr.
|
||||
"""
|
||||
altitude = base.Altitude(1.0)
|
||||
self.assertEqual(repr(altitude), "<Altitude (1.0 m)>")
|
||||
|
||||
def test_equality(self) -> None:
|
||||
"""
|
||||
Altitudes with equal values compare equal.
|
||||
"""
|
||||
firstAltitude = base.Altitude(1.0)
|
||||
secondAltitude = base.Altitude(1.0)
|
||||
self.assertEqual(firstAltitude, secondAltitude)
|
||||
|
||||
def test_inequality(self) -> None:
|
||||
"""
|
||||
Altitudes with different values don't compare equal.
|
||||
"""
|
||||
firstAltitude = base.Altitude(1.0)
|
||||
secondAltitude = base.Altitude(-1.0)
|
||||
self.assertNotEqual(firstAltitude, secondAltitude)
|
||||
|
||||
|
||||
class SpeedTests(TestCase):
|
||||
"""
|
||||
Tests for the L{twisted.positioning.base.Speed} class.
|
||||
"""
|
||||
|
||||
def test_value(self) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
Creating a negative speed raises C{ValueError}.
|
||||
"""
|
||||
self.assertRaises(ValueError, base.Speed, -1.0)
|
||||
|
||||
def test_inKnots(self) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
Speeds can be instantiated, and report their value in meters
|
||||
per second, and can be converted to floats.
|
||||
"""
|
||||
climb = base.Climb(42.0)
|
||||
self.assertEqual(climb.inMetersPerSecond, 42.0)
|
||||
self.assertEqual(float(climb), 42.0)
|
||||
|
||||
def test_repr(self) -> None:
|
||||
"""
|
||||
Climbs report their type and value in their repr.
|
||||
"""
|
||||
climb = base.Climb(42.0)
|
||||
self.assertEqual(repr(climb), "<Climb (42.0 m/s)>")
|
||||
|
||||
def test_negativeClimbs(self) -> None:
|
||||
"""
|
||||
Climbs can have negative values, and still report that value
|
||||
in meters per second and when converted to floats.
|
||||
"""
|
||||
climb = base.Climb(-42.0)
|
||||
self.assertEqual(climb.inMetersPerSecond, -42.0)
|
||||
self.assertEqual(float(climb), -42.0)
|
||||
|
||||
def test_speedInKnots(self) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
In an empty L{base.PositionError} with no invariant testing, all
|
||||
dilutions of positions are L{None}.
|
||||
"""
|
||||
positionError = base.PositionError()
|
||||
self.assertIsNone(positionError.pdop)
|
||||
self.assertIsNone(positionError.hdop)
|
||||
self.assertIsNone(positionError.vdop)
|
||||
|
||||
def test_allUnsetWithInvariant(self) -> None:
|
||||
"""
|
||||
In an empty L{base.PositionError} with invariant testing, all
|
||||
dilutions of positions are L{None}.
|
||||
"""
|
||||
positionError = base.PositionError(testInvariant=True)
|
||||
self.assertIsNone(positionError.pdop)
|
||||
self.assertIsNone(positionError.hdop)
|
||||
self.assertIsNone(positionError.vdop)
|
||||
|
||||
def test_withoutInvariant(self) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
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: base.PositionError) -> None:
|
||||
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: base.PositionError,
|
||||
pdop: float | None,
|
||||
hdop: float | None,
|
||||
vdop: float | None,
|
||||
) -> None:
|
||||
"""
|
||||
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 L{None}
|
||||
@param hdop: The expected horizontal dilution of precision.
|
||||
@type hdop: C{float} or L{None}
|
||||
@param vdop: The expected vertical dilution of precision.
|
||||
@type vdop: C{float} or L{None}
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
The VDOP is correctly determined from PDOP and HDOP.
|
||||
"""
|
||||
pdop, hdop = 2.0, 1.0
|
||||
vdop = (pdop**2 - hdop**2) ** 0.5
|
||||
pe = base.PositionError(pdop=pdop, hdop=hdop)
|
||||
self._testDOP(pe, pdop, hdop, vdop)
|
||||
|
||||
def test_positionAndVerticalSet(self) -> None:
|
||||
"""
|
||||
The HDOP is correctly determined from PDOP and VDOP.
|
||||
"""
|
||||
pdop, vdop = 2.0, 1.0
|
||||
hdop = (pdop**2 - vdop**2) ** 0.5
|
||||
pe = base.PositionError(pdop=pdop, vdop=vdop)
|
||||
self._testDOP(pe, pdop, hdop, vdop)
|
||||
|
||||
def test_horizontalAndVerticalSet(self) -> None:
|
||||
"""
|
||||
The PDOP is correctly determined from HDOP and VDOP.
|
||||
"""
|
||||
hdop, vdop = 1.0, 1.0
|
||||
pdop = (hdop**2 + vdop**2) ** 0.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) -> None:
|
||||
"""
|
||||
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.0}
|
||||
|
||||
def test_simple(self) -> None:
|
||||
"""
|
||||
Tests a beacon information with a bunch of satellites, none of
|
||||
which used in computing a fix.
|
||||
"""
|
||||
|
||||
def _buildSatellite(**kw: float) -> base.Satellite:
|
||||
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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
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) -> None:
|
||||
"""
|
||||
Tests a minimal satellite that only has a known PRN.
|
||||
|
||||
Tests that the azimuth, elevation and signal to noise ratios
|
||||
are L{None} and verifies the repr.
|
||||
"""
|
||||
s = base.Satellite(1)
|
||||
self.assertEqual(s.identifier, 1)
|
||||
self.assertIsNone(s.azimuth)
|
||||
self.assertIsNone(s.elevation)
|
||||
self.assertIsNone(s.signalToNoiseRatio)
|
||||
self.assertEqual(
|
||||
repr(s), "<Satellite (1), azimuth: None, " "elevation: None, snr: None>"
|
||||
)
|
||||
|
||||
def test_simple(self) -> None:
|
||||
"""
|
||||
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.0, elevation=30.0, signalToNoiseRatio=25.0
|
||||
)
|
||||
|
||||
self.assertEqual(s.identifier, 1)
|
||||
self.assertEqual(s.azimuth, 270.0)
|
||||
self.assertEqual(s.elevation, 30.0)
|
||||
self.assertEqual(s.signalToNoiseRatio, 25.0)
|
||||
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,149 @@
|
||||
# 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:
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
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 L{None}.
|
||||
"""
|
||||
thisSentinel = object()
|
||||
sentence = self.sentenceClass({sentinelValueOne: thisSentinel})
|
||||
self.assertEqual(getattr(sentence, sentinelValueOne), thisSentinel)
|
||||
self.assertIsNone(getattr(sentence, sentinelValueTwo))
|
||||
|
||||
def test_raiseOnMissingAttributeAccess(self):
|
||||
"""
|
||||
Accessing a nonexistent 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 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):
|
||||
"""
|
||||
L{None} does not appear in the sentence attributes of the
|
||||
protocol, even though it's in the specification.
|
||||
|
||||
This is because L{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)
|
||||
Reference in New Issue
Block a user