Funktionierender Prototyp des Serious Games zur Vermittlung von Wissen zu Software-Engineering-Arbeitsmodellen.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

nmea.py 35KB

1 year ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932
  1. # -*- test-case-name: twisted.positioning.test.test_nmea -*-
  2. # Copyright (c) Twisted Matrix Laboratories.
  3. # See LICENSE for details.
  4. """
  5. Classes for working with NMEA 0183 sentence producing devices.
  6. This standard is generally just called "NMEA", which is actually the
  7. name of the body that produces the standard, not the standard itself..
  8. For more information, read the blog post on NMEA by ESR (the gpsd
  9. maintainer) at U{http://esr.ibiblio.org/?p=801}. Unfortunately,
  10. official specifications on NMEA 0183 are only available at a cost.
  11. More information can be found on the Wikipedia page:
  12. U{https://en.wikipedia.org/wiki/NMEA_0183}.
  13. The official standard may be obtained through the NMEA's website:
  14. U{http://www.nmea.org/content/nmea_standards/nmea_0183_v_410.asp}.
  15. @since: 14.0
  16. """
  17. import datetime
  18. import operator
  19. from functools import reduce
  20. from zope.interface import implementer
  21. from constantly import ValueConstant, Values # type: ignore[import]
  22. from twisted.positioning import _sentence, base, ipositioning
  23. from twisted.positioning.base import Angles
  24. from twisted.protocols.basic import LineReceiver
  25. from twisted.python.compat import iterbytes, nativeString
  26. class GPGGAFixQualities(Values):
  27. """
  28. The possible fix quality indications for GPGGA sentences.
  29. @cvar INVALID_FIX: The fix is invalid.
  30. @cvar GPS_FIX: There is a fix, acquired using GPS.
  31. @cvar DGPS_FIX: There is a fix, acquired using differential GPS (DGPS).
  32. @cvar PPS_FIX: There is a fix, acquired using the precise positioning
  33. service (PPS).
  34. @cvar RTK_FIX: There is a fix, acquired using fixed real-time
  35. kinematics. This means that there was a sufficient number of shared
  36. satellites with the base station, usually yielding a resolution in
  37. the centimeter range. This was added in NMEA 0183 version 3.0. This
  38. is also called Carrier-Phase Enhancement or CPGPS, particularly when
  39. used in combination with GPS.
  40. @cvar FLOAT_RTK_FIX: There is a fix, acquired using floating real-time
  41. kinematics. The same comments apply as for a fixed real-time
  42. kinematics fix, except that there were insufficient shared satellites
  43. to acquire it, so instead you got a slightly less good floating fix.
  44. Typical resolution in the decimeter range.
  45. @cvar DEAD_RECKONING: There is currently no more fix, but this data was
  46. computed using a previous fix and some information about motion
  47. (either from that fix or from other sources) using simple dead
  48. reckoning. Not particularly reliable, but better-than-nonsense data.
  49. @cvar MANUAL: There is no real fix from this device, but the location has
  50. been manually entered, presumably with data obtained from some other
  51. positioning method.
  52. @cvar SIMULATED: There is no real fix, but instead it is being simulated.
  53. """
  54. INVALID_FIX = "0"
  55. GPS_FIX = "1"
  56. DGPS_FIX = "2"
  57. PPS_FIX = "3"
  58. RTK_FIX = "4"
  59. FLOAT_RTK_FIX = "5"
  60. DEAD_RECKONING = "6"
  61. MANUAL = "7"
  62. SIMULATED = "8"
  63. class GPGLLGPRMCFixQualities(Values):
  64. """
  65. The possible fix quality indications in GPGLL and GPRMC sentences.
  66. Unfortunately, these sentences only indicate whether data is good or void.
  67. They provide no other information, such as what went wrong if the data is
  68. void, or how good the data is if the data is not void.
  69. @cvar ACTIVE: The data is okay.
  70. @cvar VOID: The data is void, and should not be used.
  71. """
  72. ACTIVE = ValueConstant("A")
  73. VOID = ValueConstant("V")
  74. class GPGSAFixTypes(Values):
  75. """
  76. The possible fix types of a GPGSA sentence.
  77. @cvar GSA_NO_FIX: The sentence reports no fix at all.
  78. @cvar GSA_2D_FIX: The sentence reports a 2D fix: position but no altitude.
  79. @cvar GSA_3D_FIX: The sentence reports a 3D fix: position with altitude.
  80. """
  81. GSA_NO_FIX = ValueConstant("1")
  82. GSA_2D_FIX = ValueConstant("2")
  83. GSA_3D_FIX = ValueConstant("3")
  84. def _split(sentence):
  85. """
  86. Returns the split version of an NMEA sentence, minus header
  87. and checksum.
  88. >>> _split(b"$GPGGA,spam,eggs*00")
  89. [b'GPGGA', b'spam', b'eggs']
  90. @param sentence: The NMEA sentence to split.
  91. @type sentence: C{bytes}
  92. """
  93. if sentence[-3:-2] == b"*": # Sentence with checksum
  94. return sentence[1:-3].split(b",")
  95. elif sentence[-1:] == b"*": # Sentence without checksum
  96. return sentence[1:-1].split(b",")
  97. else:
  98. raise base.InvalidSentence(f"malformed sentence {sentence}")
  99. def _validateChecksum(sentence):
  100. """
  101. Validates the checksum of an NMEA sentence.
  102. @param sentence: The NMEA sentence to check the checksum of.
  103. @type sentence: C{bytes}
  104. @raise ValueError: If the sentence has an invalid checksum.
  105. Simply returns on sentences that either don't have a checksum,
  106. or have a valid checksum.
  107. """
  108. if sentence[-3:-2] == b"*": # Sentence has a checksum
  109. reference, source = int(sentence[-2:], 16), sentence[1:-3]
  110. computed = reduce(operator.xor, [ord(x) for x in iterbytes(source)])
  111. if computed != reference:
  112. raise base.InvalidChecksum(f"{computed:02x} != {reference:02x}")
  113. class NMEAProtocol(LineReceiver, _sentence._PositioningSentenceProducerMixin):
  114. """
  115. A protocol that parses and verifies the checksum of an NMEA sentence (in
  116. string form, not L{NMEASentence}), and delegates to a receiver.
  117. It receives lines and verifies these lines are NMEA sentences. If
  118. they are, verifies their checksum and unpacks them into their
  119. components. It then wraps them in L{NMEASentence} objects and
  120. calls the appropriate receiver method with them.
  121. @cvar _SENTENCE_CONTENTS: Has the field names in an NMEA sentence for each
  122. sentence type (in order, obviously).
  123. @type _SENTENCE_CONTENTS: C{dict} of bytestrings to C{list}s of C{str}
  124. @param receiver: A receiver for NMEAProtocol sentence objects.
  125. @type receiver: L{INMEAReceiver}
  126. @param sentenceCallback: A function that will be called with a new
  127. L{NMEASentence} when it is created. Useful for massaging data from
  128. particularly misbehaving NMEA receivers.
  129. @type sentenceCallback: unary callable
  130. """
  131. def __init__(self, receiver, sentenceCallback=None):
  132. """
  133. Initializes an NMEAProtocol.
  134. @param receiver: A receiver for NMEAProtocol sentence objects.
  135. @type receiver: L{INMEAReceiver}
  136. @param sentenceCallback: A function that will be called with a new
  137. L{NMEASentence} when it is created. Useful for massaging data from
  138. particularly misbehaving NMEA receivers.
  139. @type sentenceCallback: unary callable
  140. """
  141. self._receiver = receiver
  142. self._sentenceCallback = sentenceCallback
  143. def lineReceived(self, rawSentence):
  144. """
  145. Parses the data from the sentence and validates the checksum.
  146. @param rawSentence: The NMEA positioning sentence.
  147. @type rawSentence: C{bytes}
  148. """
  149. sentence = rawSentence.strip()
  150. _validateChecksum(sentence)
  151. splitSentence = _split(sentence)
  152. sentenceType = nativeString(splitSentence[0])
  153. contents = [nativeString(x) for x in splitSentence[1:]]
  154. try:
  155. keys = self._SENTENCE_CONTENTS[sentenceType]
  156. except KeyError:
  157. raise ValueError("unknown sentence type %s" % sentenceType)
  158. sentenceData = {"type": sentenceType}
  159. for key, value in zip(keys, contents):
  160. if key is not None and value != "":
  161. sentenceData[key] = value
  162. sentence = NMEASentence(sentenceData)
  163. if self._sentenceCallback is not None:
  164. self._sentenceCallback(sentence)
  165. self._receiver.sentenceReceived(sentence)
  166. _SENTENCE_CONTENTS = {
  167. "GPGGA": [
  168. "timestamp",
  169. "latitudeFloat",
  170. "latitudeHemisphere",
  171. "longitudeFloat",
  172. "longitudeHemisphere",
  173. "fixQuality",
  174. "numberOfSatellitesSeen",
  175. "horizontalDilutionOfPrecision",
  176. "altitude",
  177. "altitudeUnits",
  178. "heightOfGeoidAboveWGS84",
  179. "heightOfGeoidAboveWGS84Units",
  180. # The next parts are DGPS information, currently unused.
  181. None, # Time since last DGPS update
  182. None, # DGPS reference source id
  183. ],
  184. "GPRMC": [
  185. "timestamp",
  186. "dataMode",
  187. "latitudeFloat",
  188. "latitudeHemisphere",
  189. "longitudeFloat",
  190. "longitudeHemisphere",
  191. "speedInKnots",
  192. "trueHeading",
  193. "datestamp",
  194. "magneticVariation",
  195. "magneticVariationDirection",
  196. ],
  197. "GPGSV": [
  198. "numberOfGSVSentences",
  199. "GSVSentenceIndex",
  200. "numberOfSatellitesSeen",
  201. "satellitePRN_0",
  202. "elevation_0",
  203. "azimuth_0",
  204. "signalToNoiseRatio_0",
  205. "satellitePRN_1",
  206. "elevation_1",
  207. "azimuth_1",
  208. "signalToNoiseRatio_1",
  209. "satellitePRN_2",
  210. "elevation_2",
  211. "azimuth_2",
  212. "signalToNoiseRatio_2",
  213. "satellitePRN_3",
  214. "elevation_3",
  215. "azimuth_3",
  216. "signalToNoiseRatio_3",
  217. ],
  218. "GPGLL": [
  219. "latitudeFloat",
  220. "latitudeHemisphere",
  221. "longitudeFloat",
  222. "longitudeHemisphere",
  223. "timestamp",
  224. "dataMode",
  225. ],
  226. "GPHDT": [
  227. "trueHeading",
  228. ],
  229. "GPTRF": [
  230. "datestamp",
  231. "timestamp",
  232. "latitudeFloat",
  233. "latitudeHemisphere",
  234. "longitudeFloat",
  235. "longitudeHemisphere",
  236. "elevation",
  237. "numberOfIterations", # Unused
  238. "numberOfDopplerIntervals", # Unused
  239. "updateDistanceInNauticalMiles", # Unused
  240. "satellitePRN",
  241. ],
  242. "GPGSA": [
  243. "dataMode",
  244. "fixType",
  245. "usedSatellitePRN_0",
  246. "usedSatellitePRN_1",
  247. "usedSatellitePRN_2",
  248. "usedSatellitePRN_3",
  249. "usedSatellitePRN_4",
  250. "usedSatellitePRN_5",
  251. "usedSatellitePRN_6",
  252. "usedSatellitePRN_7",
  253. "usedSatellitePRN_8",
  254. "usedSatellitePRN_9",
  255. "usedSatellitePRN_10",
  256. "usedSatellitePRN_11",
  257. "positionDilutionOfPrecision",
  258. "horizontalDilutionOfPrecision",
  259. "verticalDilutionOfPrecision",
  260. ],
  261. }
  262. class NMEASentence(_sentence._BaseSentence):
  263. """
  264. An object representing an NMEA sentence.
  265. The attributes of this objects are raw NMEA protocol data, which
  266. are all ASCII bytestrings.
  267. This object contains all the raw NMEA protocol data in a single
  268. sentence. Not all of these necessarily have to be present in the
  269. sentence. Missing attributes are L{None} when accessed.
  270. @ivar type: The sentence type (C{"GPGGA"}, C{"GPGSV"}...).
  271. @ivar numberOfGSVSentences: The total number of GSV sentences in a
  272. sequence.
  273. @ivar GSVSentenceIndex: The index of this GSV sentence in the GSV
  274. sequence.
  275. @ivar timestamp: A timestamp. (C{"123456"} -> 12:34:56Z)
  276. @ivar datestamp: A datestamp. (C{"230394"} -> 23 Mar 1994)
  277. @ivar latitudeFloat: Latitude value. (for example: C{"1234.567"} ->
  278. 12 degrees, 34.567 minutes).
  279. @ivar latitudeHemisphere: Latitudinal hemisphere (C{"N"} or C{"S"}).
  280. @ivar longitudeFloat: Longitude value. See C{latitudeFloat} for an
  281. example.
  282. @ivar longitudeHemisphere: Longitudinal hemisphere (C{"E"} or C{"W"}).
  283. @ivar altitude: The altitude above mean sea level.
  284. @ivar altitudeUnits: Units in which altitude is expressed. (Always
  285. C{"M"} for meters.)
  286. @ivar heightOfGeoidAboveWGS84: The local height of the geoid above
  287. the WGS84 ellipsoid model.
  288. @ivar heightOfGeoidAboveWGS84Units: The units in which the height
  289. above the geoid is expressed. (Always C{"M"} for meters.)
  290. @ivar trueHeading: The true heading.
  291. @ivar magneticVariation: The magnetic variation.
  292. @ivar magneticVariationDirection: The direction of the magnetic
  293. variation. One of C{"E"} or C{"W"}.
  294. @ivar speedInKnots: The ground speed, expressed in knots.
  295. @ivar fixQuality: The quality of the fix.
  296. @type fixQuality: One of L{GPGGAFixQualities}.
  297. @ivar dataMode: Signals if the data is usable or not.
  298. @type dataMode: One of L{GPGLLGPRMCFixQualities}.
  299. @ivar numberOfSatellitesSeen: The number of satellites seen by the
  300. receiver.
  301. @ivar numberOfSatellitesUsed: The number of satellites used in
  302. computing the fix.
  303. @ivar horizontalDilutionOfPrecision: The dilution of the precision of the
  304. position on a plane tangential to the geoid. (HDOP)
  305. @ivar verticalDilutionOfPrecision: As C{horizontalDilutionOfPrecision},
  306. but for a position on a plane perpendicular to the geoid. (VDOP)
  307. @ivar positionDilutionOfPrecision: Euclidean norm of HDOP and VDOP.
  308. @ivar satellitePRN: The unique identifcation number of a particular
  309. satellite. Optionally suffixed with C{_N} if multiple satellites are
  310. referenced in a sentence, where C{N in range(4)}.
  311. @ivar elevation: The elevation of a satellite in decimal degrees.
  312. Optionally suffixed with C{_N}, as with C{satellitePRN}.
  313. @ivar azimuth: The azimuth of a satellite in decimal degrees.
  314. Optionally suffixed with C{_N}, as with C{satellitePRN}.
  315. @ivar signalToNoiseRatio: The SNR of a satellite signal, in decibels.
  316. Optionally suffixed with C{_N}, as with C{satellitePRN}.
  317. @ivar usedSatellitePRN_N: Where C{int(N) in range(12)}. The PRN
  318. of a satellite used in computing the fix.
  319. """
  320. ALLOWED_ATTRIBUTES = NMEAProtocol.getSentenceAttributes()
  321. def _isFirstGSVSentence(self):
  322. """
  323. Tests if this current GSV sentence is the first one in a sequence.
  324. @return: C{True} if this is the first GSV sentence.
  325. @rtype: C{bool}
  326. """
  327. return self.GSVSentenceIndex == "1"
  328. def _isLastGSVSentence(self):
  329. """
  330. Tests if this current GSV sentence is the final one in a sequence.
  331. @return: C{True} if this is the last GSV sentence.
  332. @rtype: C{bool}
  333. """
  334. return self.GSVSentenceIndex == self.numberOfGSVSentences
  335. @implementer(ipositioning.INMEAReceiver)
  336. class NMEAAdapter:
  337. """
  338. An adapter from NMEAProtocol receivers to positioning receivers.
  339. @cvar _STATEFUL_UPDATE: Information on how to update partial information
  340. in the sentence data or internal adapter state. For more information,
  341. see C{_statefulUpdate}'s docstring.
  342. @type _STATEFUL_UPDATE: See C{_statefulUpdate}'s docstring
  343. @cvar _ACCEPTABLE_UNITS: A set of NMEA notations of units that are
  344. already acceptable (metric), and therefore don't need to be converted.
  345. @type _ACCEPTABLE_UNITS: C{frozenset} of bytestrings
  346. @cvar _UNIT_CONVERTERS: Mapping of NMEA notations of units that are not
  347. acceptable (not metric) to converters that take a quantity in that
  348. unit and produce a metric quantity.
  349. @type _UNIT_CONVERTERS: C{dict} of bytestrings to unary callables
  350. @cvar _SPECIFIC_SENTENCE_FIXES: A mapping of sentece types to specific
  351. fixes that are required to extract useful information from data from
  352. those sentences.
  353. @type _SPECIFIC_SENTENCE_FIXES: C{dict} of sentence types to callables
  354. that take self and modify it in-place
  355. @cvar _FIXERS: Set of unary callables that take an NMEAAdapter instance
  356. and extract useful data from the sentence data, usually modifying the
  357. adapter's sentence data in-place.
  358. @type _FIXERS: C{dict} of native strings to unary callables
  359. @ivar yearThreshold: The earliest possible year that data will be
  360. interpreted as. For example, if this value is C{1990}, an NMEA
  361. 0183 two-digit year of "96" will be interpreted as 1996, and
  362. a two-digit year of "13" will be interpreted as 2013.
  363. @type yearThreshold: L{int}
  364. @ivar _state: The current internal state of the receiver.
  365. @type _state: C{dict}
  366. @ivar _sentenceData: The data present in the sentence currently being
  367. processed. Starts empty, is filled as the sentence is parsed.
  368. @type _sentenceData: C{dict}
  369. @ivar _receiver: The positioning receiver that will receive parsed data.
  370. @type _receiver: L{ipositioning.IPositioningReceiver}
  371. """
  372. def __init__(self, receiver):
  373. """
  374. Initializes a new NMEA adapter.
  375. @param receiver: The receiver for positioning sentences.
  376. @type receiver: L{ipositioning.IPositioningReceiver}
  377. """
  378. self._state = {}
  379. self._sentenceData = {}
  380. self._receiver = receiver
  381. def _fixTimestamp(self):
  382. """
  383. Turns the NMEAProtocol timestamp notation into a datetime.time object.
  384. The time in this object is expressed as Zulu time.
  385. """
  386. timestamp = self.currentSentence.timestamp.split(".")[0]
  387. timeObject = datetime.datetime.strptime(timestamp, "%H%M%S").time()
  388. self._sentenceData["_time"] = timeObject
  389. yearThreshold = 1980
  390. def _fixDatestamp(self):
  391. """
  392. Turns an NMEA datestamp format into a C{datetime.date} object.
  393. @raise ValueError: When the day or month value was invalid, e.g. 32nd
  394. day, or 13th month, or 0th day or month.
  395. """
  396. date = self.currentSentence.datestamp
  397. day, month, year = map(int, [date[0:2], date[2:4], date[4:6]])
  398. year += self.yearThreshold - (self.yearThreshold % 100)
  399. if year < self.yearThreshold:
  400. year += 100
  401. self._sentenceData["_date"] = datetime.date(year, month, day)
  402. def _fixCoordinateFloat(self, coordinateType):
  403. """
  404. Turns the NMEAProtocol coordinate format into Python float.
  405. @param coordinateType: The coordinate type.
  406. @type coordinateType: One of L{Angles.LATITUDE} or L{Angles.LONGITUDE}.
  407. """
  408. if coordinateType is Angles.LATITUDE:
  409. coordinateName = "latitude"
  410. else: # coordinateType is Angles.LONGITUDE
  411. coordinateName = "longitude"
  412. nmeaCoordinate = getattr(self.currentSentence, coordinateName + "Float")
  413. left, right = nmeaCoordinate.split(".")
  414. degrees, minutes = int(left[:-2]), float(f"{left[-2:]}.{right}")
  415. angle = degrees + minutes / 60
  416. coordinate = base.Coordinate(angle, coordinateType)
  417. self._sentenceData[coordinateName] = coordinate
  418. def _fixHemisphereSign(self, coordinateType, sentenceDataKey=None):
  419. """
  420. Fixes the sign for a hemisphere.
  421. This method must be called after the magnitude for the thing it
  422. determines the sign of has been set. This is done by the following
  423. functions:
  424. - C{self.FIXERS['magneticVariation']}
  425. - C{self.FIXERS['latitudeFloat']}
  426. - C{self.FIXERS['longitudeFloat']}
  427. @param coordinateType: Coordinate type. One of L{Angles.LATITUDE},
  428. L{Angles.LONGITUDE} or L{Angles.VARIATION}.
  429. @param sentenceDataKey: The key name of the hemisphere sign being
  430. fixed in the sentence data. If unspecified, C{coordinateType} is
  431. used.
  432. @type sentenceDataKey: C{str} (unless L{None})
  433. """
  434. sentenceDataKey = sentenceDataKey or coordinateType
  435. sign = self._getHemisphereSign(coordinateType)
  436. self._sentenceData[sentenceDataKey].setSign(sign)
  437. def _getHemisphereSign(self, coordinateType):
  438. """
  439. Returns the hemisphere sign for a given coordinate type.
  440. @param coordinateType: The coordinate type to find the hemisphere for.
  441. @type coordinateType: L{Angles.LATITUDE}, L{Angles.LONGITUDE} or
  442. L{Angles.VARIATION}.
  443. @return: The sign of that hemisphere (-1 or 1).
  444. @rtype: C{int}
  445. """
  446. if coordinateType is Angles.LATITUDE:
  447. hemisphereKey = "latitudeHemisphere"
  448. elif coordinateType is Angles.LONGITUDE:
  449. hemisphereKey = "longitudeHemisphere"
  450. elif coordinateType is Angles.VARIATION:
  451. hemisphereKey = "magneticVariationDirection"
  452. else:
  453. raise ValueError(f"unknown coordinate type {coordinateType}")
  454. hemisphere = getattr(self.currentSentence, hemisphereKey).upper()
  455. if hemisphere in "NE":
  456. return 1
  457. elif hemisphere in "SW":
  458. return -1
  459. else:
  460. raise ValueError(f"bad hemisphere/direction: {hemisphere}")
  461. def _convert(self, key, converter):
  462. """
  463. A simple conversion fix.
  464. @param key: The attribute name of the value to fix.
  465. @type key: native string (Python identifier)
  466. @param converter: The function that converts the value.
  467. @type converter: unary callable
  468. """
  469. currentValue = getattr(self.currentSentence, key)
  470. self._sentenceData[key] = converter(currentValue)
  471. _STATEFUL_UPDATE = {
  472. # sentenceKey: (stateKey, factory, attributeName, converter),
  473. "trueHeading": ("heading", base.Heading, "_angle", float),
  474. "magneticVariation": (
  475. "heading",
  476. base.Heading,
  477. "variation",
  478. lambda angle: base.Angle(float(angle), Angles.VARIATION),
  479. ),
  480. "horizontalDilutionOfPrecision": (
  481. "positionError",
  482. base.PositionError,
  483. "hdop",
  484. float,
  485. ),
  486. "verticalDilutionOfPrecision": (
  487. "positionError",
  488. base.PositionError,
  489. "vdop",
  490. float,
  491. ),
  492. "positionDilutionOfPrecision": (
  493. "positionError",
  494. base.PositionError,
  495. "pdop",
  496. float,
  497. ),
  498. }
  499. def _statefulUpdate(self, sentenceKey):
  500. """
  501. Does a stateful update of a particular positioning attribute.
  502. Specifically, this will mutate an object in the current sentence data.
  503. Using the C{sentenceKey}, this will get a tuple containing, in order,
  504. the key name in the current state and sentence data, a factory for
  505. new values, the attribute to update, and a converter from sentence
  506. data (in NMEA notation) to something useful.
  507. If the sentence data doesn't have this data yet, it is grabbed from
  508. the state. If that doesn't have anything useful yet either, the
  509. factory is called to produce a new, empty object. Either way, the
  510. object ends up in the sentence data.
  511. @param sentenceKey: The name of the key in the sentence attributes,
  512. C{NMEAAdapter._STATEFUL_UPDATE} dictionary and the adapter state.
  513. @type sentenceKey: C{str}
  514. """
  515. key, factory, attr, converter = self._STATEFUL_UPDATE[sentenceKey]
  516. if key not in self._sentenceData:
  517. try:
  518. self._sentenceData[key] = self._state[key]
  519. except KeyError: # state does not have this partial data yet
  520. self._sentenceData[key] = factory()
  521. newValue = converter(getattr(self.currentSentence, sentenceKey))
  522. setattr(self._sentenceData[key], attr, newValue)
  523. _ACCEPTABLE_UNITS = frozenset(["M"])
  524. _UNIT_CONVERTERS = {
  525. "N": lambda inKnots: base.Speed(float(inKnots) * base.MPS_PER_KNOT),
  526. "K": lambda inKPH: base.Speed(float(inKPH) * base.MPS_PER_KPH),
  527. }
  528. def _fixUnits(self, unitKey=None, valueKey=None, sourceKey=None, unit=None):
  529. """
  530. Fixes the units of a certain value. If the units are already
  531. acceptable (metric), does nothing.
  532. None of the keys are allowed to be the empty string.
  533. @param unit: The unit that is being converted I{from}. If unspecified
  534. or L{None}, asks the current sentence for the C{unitKey}. If that
  535. also fails, raises C{AttributeError}.
  536. @type unit: C{str}
  537. @param unitKey: The name of the key/attribute under which the unit can
  538. be found in the current sentence. If the C{unit} parameter is set,
  539. this parameter is not used.
  540. @type unitKey: C{str}
  541. @param sourceKey: The name of the key/attribute that contains the
  542. current value to be converted (expressed in units as defined
  543. according to the C{unit} parameter). If unset, will use the
  544. same key as the value key.
  545. @type sourceKey: C{str}
  546. @param valueKey: The key name in which the data will be stored in the
  547. C{_sentenceData} instance attribute. If unset, attempts to remove
  548. "Units" from the end of the C{unitKey} parameter. If that fails,
  549. raises C{ValueError}.
  550. @type valueKey: C{str}
  551. """
  552. if unit is None:
  553. unit = getattr(self.currentSentence, unitKey)
  554. if valueKey is None:
  555. if unitKey is not None and unitKey.endswith("Units"):
  556. valueKey = unitKey[:-5]
  557. else:
  558. raise ValueError("valueKey unspecified and couldn't be guessed")
  559. if sourceKey is None:
  560. sourceKey = valueKey
  561. if unit not in self._ACCEPTABLE_UNITS:
  562. converter = self._UNIT_CONVERTERS[unit]
  563. currentValue = getattr(self.currentSentence, sourceKey)
  564. self._sentenceData[valueKey] = converter(currentValue)
  565. def _fixGSV(self):
  566. """
  567. Parses partial visible satellite information from a GSV sentence.
  568. """
  569. # To anyone who knows NMEA, this method's name should raise a chuckle's
  570. # worth of schadenfreude. 'Fix' GSV? Hah! Ludicrous.
  571. beaconInformation = base.BeaconInformation()
  572. self._sentenceData["_partialBeaconInformation"] = beaconInformation
  573. keys = "satellitePRN", "azimuth", "elevation", "signalToNoiseRatio"
  574. for index in range(4):
  575. prn, azimuth, elevation, snr = (
  576. getattr(self.currentSentence, attr)
  577. for attr in ("%s_%i" % (key, index) for key in keys)
  578. )
  579. if prn is None or snr is None:
  580. # The peephole optimizer optimizes the jump away, meaning that
  581. # coverage.py thinks it isn't covered. It is. Replace it with
  582. # break, and watch the test case fail.
  583. # ML thread about this issue: http://goo.gl/1KNUi
  584. # Related CPython bug: http://bugs.python.org/issue2506
  585. continue
  586. satellite = base.Satellite(prn, azimuth, elevation, snr)
  587. beaconInformation.seenBeacons.add(satellite)
  588. def _fixGSA(self):
  589. """
  590. Extracts the information regarding which satellites were used in
  591. obtaining the GPS fix from a GSA sentence.
  592. Precondition: A GSA sentence was fired. Postcondition: The current
  593. sentence data (C{self._sentenceData} will contain a set of the
  594. currently used PRNs (under the key C{_usedPRNs}.
  595. """
  596. self._sentenceData["_usedPRNs"] = set()
  597. for key in ("usedSatellitePRN_%d" % (x,) for x in range(12)):
  598. prn = getattr(self.currentSentence, key, None)
  599. if prn is not None:
  600. self._sentenceData["_usedPRNs"].add(int(prn))
  601. _SPECIFIC_SENTENCE_FIXES = {
  602. "GPGSV": _fixGSV,
  603. "GPGSA": _fixGSA,
  604. }
  605. def _sentenceSpecificFix(self):
  606. """
  607. Executes a fix for a specific type of sentence.
  608. """
  609. fixer = self._SPECIFIC_SENTENCE_FIXES.get(self.currentSentence.type)
  610. if fixer is not None:
  611. fixer(self)
  612. _FIXERS = {
  613. "type": lambda self: self._sentenceSpecificFix(),
  614. "timestamp": lambda self: self._fixTimestamp(),
  615. "datestamp": lambda self: self._fixDatestamp(),
  616. "latitudeFloat": lambda self: self._fixCoordinateFloat(Angles.LATITUDE),
  617. "latitudeHemisphere": lambda self: self._fixHemisphereSign(
  618. Angles.LATITUDE, "latitude"
  619. ),
  620. "longitudeFloat": lambda self: self._fixCoordinateFloat(Angles.LONGITUDE),
  621. "longitudeHemisphere": lambda self: self._fixHemisphereSign(
  622. Angles.LONGITUDE, "longitude"
  623. ),
  624. "altitude": lambda self: self._convert(
  625. "altitude", converter=lambda strRepr: base.Altitude(float(strRepr))
  626. ),
  627. "altitudeUnits": lambda self: self._fixUnits(unitKey="altitudeUnits"),
  628. "heightOfGeoidAboveWGS84": lambda self: self._convert(
  629. "heightOfGeoidAboveWGS84",
  630. converter=lambda strRepr: base.Altitude(float(strRepr)),
  631. ),
  632. "heightOfGeoidAboveWGS84Units": lambda self: self._fixUnits(
  633. unitKey="heightOfGeoidAboveWGS84Units"
  634. ),
  635. "trueHeading": lambda self: self._statefulUpdate("trueHeading"),
  636. "magneticVariation": lambda self: self._statefulUpdate("magneticVariation"),
  637. "magneticVariationDirection": lambda self: self._fixHemisphereSign(
  638. Angles.VARIATION, "heading"
  639. ),
  640. "speedInKnots": lambda self: self._fixUnits(
  641. valueKey="speed", sourceKey="speedInKnots", unit="N"
  642. ),
  643. "positionDilutionOfPrecision": lambda self: self._statefulUpdate(
  644. "positionDilutionOfPrecision"
  645. ),
  646. "horizontalDilutionOfPrecision": lambda self: self._statefulUpdate(
  647. "horizontalDilutionOfPrecision"
  648. ),
  649. "verticalDilutionOfPrecision": lambda self: self._statefulUpdate(
  650. "verticalDilutionOfPrecision"
  651. ),
  652. }
  653. def clear(self):
  654. """
  655. Resets this adapter.
  656. This will empty the adapter state and the current sentence data.
  657. """
  658. self._state = {}
  659. self._sentenceData = {}
  660. def sentenceReceived(self, sentence):
  661. """
  662. Called when a sentence is received.
  663. Will clean the received NMEAProtocol sentence up, and then update the
  664. adapter's state, followed by firing the callbacks.
  665. If the received sentence was invalid, the state will be cleared.
  666. @param sentence: The sentence that is received.
  667. @type sentence: L{NMEASentence}
  668. """
  669. self.currentSentence = sentence
  670. self._sentenceData = {}
  671. try:
  672. self._validateCurrentSentence()
  673. self._cleanCurrentSentence()
  674. except base.InvalidSentence:
  675. self.clear()
  676. self._updateState()
  677. self._fireSentenceCallbacks()
  678. def _validateCurrentSentence(self):
  679. """
  680. Tests if a sentence contains a valid fix.
  681. """
  682. if (
  683. self.currentSentence.fixQuality is GPGGAFixQualities.INVALID_FIX
  684. or self.currentSentence.dataMode is GPGLLGPRMCFixQualities.VOID
  685. or self.currentSentence.fixType is GPGSAFixTypes.GSA_NO_FIX
  686. ):
  687. raise base.InvalidSentence("bad sentence")
  688. def _cleanCurrentSentence(self):
  689. """
  690. Cleans the current sentence.
  691. """
  692. for key in sorted(self.currentSentence.presentAttributes):
  693. fixer = self._FIXERS.get(key, None)
  694. if fixer is not None:
  695. fixer(self)
  696. def _updateState(self):
  697. """
  698. Updates the current state with the new information from the sentence.
  699. """
  700. self._updateBeaconInformation()
  701. self._combineDateAndTime()
  702. self._state.update(self._sentenceData)
  703. def _updateBeaconInformation(self):
  704. """
  705. Updates existing beacon information state with new data.
  706. """
  707. new = self._sentenceData.get("_partialBeaconInformation")
  708. if new is None:
  709. return
  710. self._updateUsedBeacons(new)
  711. self._mergeBeaconInformation(new)
  712. if self.currentSentence._isLastGSVSentence():
  713. if not self.currentSentence._isFirstGSVSentence():
  714. # not a 1-sentence sequence, get rid of partial information
  715. del self._state["_partialBeaconInformation"]
  716. bi = self._sentenceData.pop("_partialBeaconInformation")
  717. self._sentenceData["beaconInformation"] = bi
  718. def _updateUsedBeacons(self, beaconInformation):
  719. """
  720. Searches the adapter state and sentence data for information about
  721. which beacons where used, then adds it to the provided beacon
  722. information object.
  723. If no new beacon usage information is available, does nothing.
  724. @param beaconInformation: The beacon information object that beacon
  725. usage information will be added to (if necessary).
  726. @type beaconInformation: L{twisted.positioning.base.BeaconInformation}
  727. """
  728. for source in [self._state, self._sentenceData]:
  729. usedPRNs = source.get("_usedPRNs")
  730. if usedPRNs is not None:
  731. break
  732. else: # No used PRN info to update
  733. return
  734. for beacon in beaconInformation.seenBeacons:
  735. if beacon.identifier in usedPRNs:
  736. beaconInformation.usedBeacons.add(beacon)
  737. def _mergeBeaconInformation(self, newBeaconInformation):
  738. """
  739. Merges beacon information in the adapter state (if it exists) into
  740. the provided beacon information. Specifically, this merges used and
  741. seen beacons.
  742. If the adapter state has no beacon information, does nothing.
  743. @param newBeaconInformation: The beacon information object that beacon
  744. information will be merged into (if necessary).
  745. @type newBeaconInformation: L{twisted.positioning.base.BeaconInformation}
  746. """
  747. old = self._state.get("_partialBeaconInformation")
  748. if old is None:
  749. return
  750. for attr in ["seenBeacons", "usedBeacons"]:
  751. getattr(newBeaconInformation, attr).update(getattr(old, attr))
  752. def _combineDateAndTime(self):
  753. """
  754. Combines a C{datetime.date} object and a C{datetime.time} object,
  755. collected from one or more NMEA sentences, into a single
  756. C{datetime.datetime} object suitable for sending to the
  757. L{IPositioningReceiver}.
  758. """
  759. if not any(k in self._sentenceData for k in ["_date", "_time"]):
  760. # If the sentence has neither date nor time, there's
  761. # nothing new to combine here.
  762. return
  763. date, time = (
  764. self._sentenceData.get(key) or self._state.get(key)
  765. for key in ("_date", "_time")
  766. )
  767. if date is None or time is None:
  768. return
  769. dt = datetime.datetime.combine(date, time)
  770. self._sentenceData["time"] = dt
  771. def _fireSentenceCallbacks(self):
  772. """
  773. Fires sentence callbacks for the current sentence.
  774. A callback will only fire if all of the keys it requires are present
  775. in the current state and at least one such field was altered in the
  776. current sentence.
  777. The callbacks will only be fired with data from L{_state}.
  778. """
  779. iface = ipositioning.IPositioningReceiver
  780. for name, method in iface.namesAndDescriptions():
  781. callback = getattr(self._receiver, name)
  782. kwargs = {}
  783. atLeastOnePresentInSentence = False
  784. try:
  785. for field in method.positional:
  786. if field in self._sentenceData:
  787. atLeastOnePresentInSentence = True
  788. kwargs[field] = self._state[field]
  789. except KeyError:
  790. continue
  791. if atLeastOnePresentInSentence:
  792. callback(**kwargs)
  793. __all__ = ["NMEAProtocol", "NMEASentence", "NMEAAdapter"]