gpssim.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. # This file is Copyright 2010 by the GPSD project
  2. # SPDX-License-Identifier: BSD-2-clause
  3. """
  4. A GPS simulator.
  5. This is proof-of-concept code, not production ready; some functions are stubs.
  6. """
  7. import math
  8. import random
  9. import sys
  10. import time
  11. # pylint wants local modules last
  12. try:
  13. import gps
  14. except ImportError as e:
  15. sys.stderr.write(
  16. "gpssim.py: can't load Python gps libraries -- check PYTHONPATH.\n")
  17. sys.stderr.write("%s\n" % e)
  18. sys.exit(1)
  19. # First, the mathematics. We simulate a moving viewpoint on the Earth
  20. # and a satellite with specified orbital elements in the sky.
  21. class ksv(object):
  22. "Kinematic state vector."
  23. def __init__(self, time=0, lat=0, lon=0, alt=0, course=0,
  24. speed=0, climb=0, h_acc=0, v_acc=0):
  25. self.time = time # Seconds from epoch
  26. self.lat = lat # Decimal degrees
  27. self.lon = lon # Decimal degrees
  28. self.alt = alt # Meters
  29. self.course = course # Degrees from true North
  30. self.speed = speed # Meters per second
  31. self.climb = climb # Meters per second
  32. self.h_acc = h_acc # Meters per second per second
  33. self.v_acc = v_acc # Meters per second per second
  34. def next(self, quantum=1):
  35. "State after quantum."
  36. self.time += quantum
  37. avspeed = (2 * self.speed + self.h_acc * quantum) / 2
  38. avclimb = (2 * self.climb + self.v_acc * quantum) / 2
  39. self.alt += avclimb * quantum
  40. self.speed += self.h_acc * quantum
  41. self.climb += self.v_acc * quantum
  42. distance = avspeed * quantum
  43. # Formula from <http://williams.best.vwh.net/avform.htm#Rhumb>
  44. # Initial point cannot be a pole, but GPS doesn't work at high.
  45. # latitudes anyway so it would be OK to fail there.
  46. # Seems to assume a spherical Earth, which means it's going
  47. # to have a slight inaccuracy rising towards the poles.
  48. # The if/then avoids 0/0 indeterminacies on E-W courses.
  49. tc = gps.Deg2Rad(self.course)
  50. lat = gps.Deg2Rad(self.lat)
  51. lon = gps.Deg2Rad(self.lon)
  52. lat += distance * math.cos(tc)
  53. dphi = math.log(math.tan(lat / 2 + math.pi / 4) /
  54. math.tan(self.lat / 2 + math.pi / 4))
  55. if abs(lat - self.lat) < math.sqrt(1e-15):
  56. q = math.cos(self.lat)
  57. else:
  58. q = (lat - self.lat) / dphi
  59. dlon = -distance * math.sin(tc) / q
  60. self.lon = gps.Rad2Deg(math.mod(lon + dlon + math.pi, 2 * math.pi) -
  61. math.pi)
  62. self.lat = gps.Rad2Deg(lat)
  63. # Satellite orbital elements are available at:
  64. # <http://www.ngs.noaa.gov/orbits/>
  65. # Orbital theory at:
  66. # <http://www.wolffdata.se/gps/gpshtml/anomalies.html>
  67. class satellite(object):
  68. "Orbital elements of one satellite. PRESENTLY A STUB"
  69. def __init__(self, prn):
  70. self.prn = prn
  71. def position(self, time):
  72. "Return right ascension and declination of satellite,"
  73. return
  74. # Next, the command interpreter. This is an object that takes an
  75. # input source in the track description language, interprets it into
  76. # sammples that might be reported by a GPS, and calls a reporting
  77. # class to generate output.
  78. class gpssimException(BaseException):
  79. def __init__(self, message, filename, lineno):
  80. BaseException.__init__(self)
  81. self.message = message
  82. self.filename = filename
  83. self.lineno = lineno
  84. def __str__(self):
  85. return '"%s", %d:' % (self.filename, self.lineno)
  86. class gpssim(object):
  87. "Simulate a moving sensor, with skyview."
  88. active_PRNs = list(range(1, 24 + 1)) + [134, ]
  89. def __init__(self, outfmt):
  90. self.ksv = ksv()
  91. self.ephemeris = {}
  92. # This sets up satellites at random. Not really what we want.
  93. for prn in gpssim.active_PRNs:
  94. for (prn, _satellite) in list(self.ephemeris.items()):
  95. self.skyview[prn] = (random.randint(-60, +61),
  96. random.randint(0, 359))
  97. self.have_ephemeris = False
  98. self.channels = {}
  99. self.outfmt = outfmt
  100. self.status = gps.STATUS_NO_FIX
  101. self.mode = gps.MODE_NO_FIX
  102. self.validity = "V"
  103. self.satellites_used = 0
  104. self.filename = None
  105. self.lineno = 0
  106. def parse_tdl(self, line):
  107. "Interpret one TDL directive."
  108. line = line.strip()
  109. if "#" in line:
  110. line = line[:line.find("#")]
  111. if line == '':
  112. return
  113. fields = line.split()
  114. command = fields[0]
  115. if command == "time":
  116. self.ksv.time = gps.isotime(fields[1])
  117. elif command == "location":
  118. (self.lat, self.lon, self.alt) = list(map(float, fields[1:]))
  119. elif command == "course":
  120. self.ksv.time = float(fields[1])
  121. elif command == "speed":
  122. self.ksv.speed = float(fields[1])
  123. elif command == "climb":
  124. self.ksv.climb = float(fields[1])
  125. elif command == "acceleration":
  126. (self.ksv.h_acc, self.ksv.h_acc) = list(map(float, fields[1:]))
  127. elif command == "snr":
  128. self.channels[int(fields[1])] = float(fields[2])
  129. elif command == "go":
  130. self.go(int(fields[1]))
  131. elif command == "status":
  132. try:
  133. code = fields[1]
  134. self.status = {"no_fix": 0, "fix": 1, "dgps_fix": 2}[
  135. code.lower()]
  136. except KeyError:
  137. raise gpssimException("invalid status code '%s'" % code,
  138. self.filename, self.lineno)
  139. elif command == "mode":
  140. try:
  141. code = fields[1]
  142. self.status = {"no_fix": 1, "2d": 2, "3d": 3}[code.lower()]
  143. except KeyError:
  144. raise gpssimException("invalid mode code '%s'" % code,
  145. self.filename, self.lineno)
  146. elif command == "satellites":
  147. self.satellites_used = int(fields[1])
  148. elif command == "validity":
  149. self.validity = fields[1]
  150. else:
  151. raise gpssimException("unknown command '%s'" % fields[1],
  152. self.filename, self.lineno)
  153. # FIX-ME: add syntax for ephemeris elements
  154. self.lineno += 1
  155. def filter(self, inp, outp):
  156. "Make this a filter for file-like objects."
  157. self.filename = "WTF"
  158. self.lineno = 1
  159. self.output = outp
  160. for line in inp:
  161. self.execute(line)
  162. def go(self, seconds):
  163. "Run the simulation for a specified number of seconds."
  164. for i in range(seconds):
  165. next(self.ksv)
  166. if self.have_ephemeris:
  167. self.skyview = {}
  168. for (prn, satellite) in list(self.ephemeris.items()):
  169. self.skyview[prn] = satellite.position(i)
  170. self.output.write(self.gpstype.report(self))
  171. # Reporting classes need to have a report() method returning a string
  172. # that is a sentence (or possibly several sentences) reporting the
  173. # state of the simulation. Presently we have only one, for NMEA
  174. # devices, but the point of the architecture is so that we could simulate
  175. # others - SirF, Evermore, whatever.
  176. MPS_TO_KNOTS = 1.9438445 # Meters per second to knots
  177. class NMEA(object):
  178. "NMEA output generator."
  179. def __init__(self):
  180. self.sentences = ("RMC", "GGA",)
  181. self.counter = 0
  182. def add_checksum(self, mstr):
  183. "Concatenate NMEA checksum and trailer to a string"
  184. csum = 0
  185. for (i, c) in enumerate(mstr):
  186. if i == 0 and c == "$":
  187. continue
  188. csum ^= ord(c)
  189. mstr += "*%02X\r\n" % csum
  190. return mstr
  191. def degtodm(self, angle):
  192. "Decimal degrees to GPS-style, degrees first followed by minutes."
  193. (fraction, _integer) = math.modf(angle)
  194. return math.floor(angle) * 100 + fraction * 60
  195. def GGA(self, sim):
  196. "Emit GGA sentence describing the simulation state."
  197. tm = time.gmtime(sim.ksv.time)
  198. gga = "$GPGGA,%02d%02d%02d,%09.4f,%c,%010.4f,%c,%d,%02d," % (
  199. tm.tm_hour,
  200. tm.tm_min,
  201. tm.tm_sec,
  202. self.degtodm(abs(sim.ksv.lat)), "SN"[sim.ksv.lat > 0],
  203. self.degtodm(abs(sim.ksv.lon)), "WE"[sim.ksv.lon > 0],
  204. sim.status,
  205. sim.satellites_used)
  206. # HDOP calculation goes here
  207. gga += ","
  208. if sim.mode == gps.MODE_3D:
  209. gga += "%.1f,M" % self.ksv.lat
  210. gga += ","
  211. gga += "%.3f,M," % gps.wg84_separation(sim.ksv.lat, sim.ksv.lon)
  212. # Magnetic variation goes here
  213. # gga += "%3.2f,M," % mag_var
  214. gga += ",,"
  215. # Time in seconds since last DGPS update goes here
  216. gga += ","
  217. # DGPS station ID goes here
  218. return self.add_checksum(gga)
  219. def GLL(self, sim):
  220. "Emit GLL sentence describing the simulation state."
  221. tm = time.gmtime(sim.ksv.time)
  222. gll = "$GPLL,%09.4f,%c,%010.4f,%c,%02d%02d%02d,%s," % (
  223. self.degtodm(abs(sim.ksv.lat)), "SN"[sim.ksv.lat > 0],
  224. self.degtodm(abs(sim.ksv.lon)), "WE"[sim.ksv.lon > 0],
  225. tm.tm_hour,
  226. tm.tm_min,
  227. tm.tm_sec,
  228. sim.validity, )
  229. # FAA mode indicator could go after these fields.
  230. return self.add_checksum(gll)
  231. def RMC(self, sim):
  232. "Emit RMC sentence describing the simulation state."
  233. tm = time.gmtime(sim.ksv.time)
  234. rmc = \
  235. "GPRMC,%02d%02d%02d,%s,%09.4f,%c,%010.4f,%c,%.1f,%02d%02d%02d," % (
  236. tm.tm_hour,
  237. tm.tm_min,
  238. tm.tm_sec,
  239. sim.validity,
  240. self.degtodm(abs(sim.ksv.lat)), "SN"[sim.ksv.lat > 0],
  241. self.degtodm(abs(sim.ksv.lon)), "WE"[sim.ksv.lon > 0],
  242. sim.course * MPS_TO_KNOTS,
  243. tm.tm_mday,
  244. tm.tm_mon,
  245. tm.tm_year % 100)
  246. # Magnetic variation goes here
  247. # rmc += "%3.2f,M," % mag_var
  248. rmc += ",,"
  249. # FAA mode goes here
  250. return self.add_checksum(rmc)
  251. def ZDA(self, sim):
  252. "Emit ZDA sentence describing the simulation state."
  253. tm = time.gmtime(sim.ksv.time)
  254. zda = "$GPZDA,%02d%2d%02d,%02d,%02d,%04d" % (
  255. tm.tm_hour,
  256. tm.tm_min,
  257. tm.tm_sec,
  258. tm.tm_mday,
  259. tm.tm_mon,
  260. tm.tm_year, )
  261. # Local zone description, 00 to +- 13 hours, goes here
  262. zda += ","
  263. # Local zone minutes description goes here
  264. zda += ","
  265. return self.add_checksum(zda)
  266. def report(self, sim):
  267. "Report the simulation state."
  268. out = ""
  269. for sentence in self.sentences:
  270. if isinstance(sentence, tuple):
  271. (interval, sentence) = sentence
  272. if self.counter % interval:
  273. continue
  274. out += getattr(self, sentence)(*[sim])
  275. self.counter += 1
  276. return out
  277. # The very simple main line.
  278. if __name__ == "__main__":
  279. try:
  280. gpssim(NMEA).filter(sys.stdin, sys.stdout)
  281. except gpssimException as e:
  282. sys.stderr.write(repr(e) + "\n")
  283. # gpssim.py ends here.
  284. # vim: set expandtab shiftwidth=4