Devuan fork of gpsd
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.
 
 
 
 
 
 

398 lines
12 KiB

  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. """webgps.py
  4. This is a Python port of webgps.c
  5. from http://www.wireless.org.au/~jhecker/gpsd/
  6. by Beat Bolli <me+gps@drbeat.li>
  7. It creates a skyview of the currently visible GPS satellites and their tracks
  8. over a time period.
  9. Usage:
  10. ./webgps.py [duration]
  11. duration may be
  12. - a number of seconds
  13. - a number followed by a time unit ('s' for secinds, 'm' for minutes,
  14. 'h' for hours or 'd' for days, e.g. '4h' for a duration of four hours)
  15. - the letter 'c' for continuous operation
  16. If duration is missing, the current skyview is generated and webgps.py exits
  17. immediately. This is the same as giving a duration of 0.
  18. If a duration is given, webgps.py runs for this duration and generates the
  19. tracks of the GPS satellites in view. If the duration is the letter 'c',
  20. the script never exits and continuously updates the skyview.
  21. webgps.py generates two files: a HTML5 file that can be browsed, and a
  22. JavaScript file that contains the drawing commands for the skyview. The HTML5
  23. file auto-refreshes every five minutes. The generated file names are
  24. "gpsd-<duration>.html" and "gpsd-<duration>.js".
  25. If webgps.py is interrupted with Ctrl-C before the duration is over, it saves
  26. the current tracks into the file "tracks.p". This is a Python "pickle" file.
  27. If this file is present on start of webgps.py, it is loaded. This allows to
  28. restart webgps.py without losing accumulated satellite tracks.
  29. """
  30. # This file is Copyright (c) 2010-2018 by the GPSD project
  31. # SPDX-License-Identifier: BSD-2-clause
  32. from __future__ import absolute_import, print_function, division
  33. import math
  34. import os
  35. import pickle
  36. import sys
  37. import time
  38. from gps import *
  39. gps_version = '3.20'
  40. if gps.__version__ != gps_version:
  41. sys.stderr.write("webgps.py: ERROR: need gps module version %s, got %s\n" %
  42. (gps_version, gps.__version__))
  43. sys.exit(1)
  44. TRACKMAX = 1024
  45. STALECOUNT = 10
  46. DIAMETER = 200
  47. def polartocart(el, az):
  48. radius = DIAMETER * (1 - el / 90.0) # * math.cos(Deg2Rad(float(el)))
  49. theta = Deg2Rad(float(az - 90))
  50. return (
  51. # Changed this back to normal orientation - fw
  52. int(radius * math.cos(theta) + 0.5),
  53. int(radius * math.sin(theta) + 0.5)
  54. )
  55. class Track:
  56. '''Store the track of one satellite.'''
  57. def __init__(self, prn):
  58. self.prn = prn
  59. self.stale = 0
  60. self.posn = [] # list of (x, y) tuples
  61. def add(self, x, y):
  62. pos = (x, y)
  63. self.stale = STALECOUNT
  64. if not self.posn or self.posn[-1] != pos:
  65. self.posn.append(pos)
  66. if len(self.posn) > TRACKMAX:
  67. self.posn = self.posn[-TRACKMAX:]
  68. return 1
  69. return 0
  70. def track(self):
  71. '''Return the track as canvas drawing operations.'''
  72. return('M(%d,%d); ' % self.posn[0] + ''.join(['L(%d,%d); ' %
  73. p for p in self.posn[1:]]))
  74. class SatTracks(gps):
  75. '''gpsd client writing HTML5 and <canvas> output.'''
  76. def __init__(self):
  77. super(SatTracks, self).__init__()
  78. self.sattrack = {} # maps PRNs to Tracks
  79. self.state = None
  80. self.statetimer = time.time()
  81. self.needsupdate = 0
  82. def html(self, fh, jsfile):
  83. fh.write("""<!DOCTYPE html>
  84. <html>
  85. <head>
  86. \t<meta http-equiv=Refresh content=300>
  87. \t<meta charset='utf-8'>
  88. \t<title>GPSD Satellite Positions and Readings</title>
  89. \t<style type='text/css'>
  90. \t\t.num td { text-align: right; }
  91. \t\tth { text-align: left; }
  92. \t</style>
  93. \t<script src='%s'></script>
  94. </head>
  95. <body>
  96. \t<table border=1>
  97. \t\t<tr>
  98. \t\t\t<td>
  99. \t\t\t\t<table border=0 class=num>
  100. \t\t\t\t\t<tr><th>PRN:</th><th>Elev:</th><th>Azim:</th><th>SNR:</th>
  101. <th>Used:</th></tr>
  102. """ % jsfile)
  103. sats = self.satellites[:]
  104. sats.sort(key=lambda x: x.PRN)
  105. for s in sats:
  106. fh.write("\t\t\t\t\t<tr><td>%d</td><td>%d</td><td>%d</td>"
  107. "<td>%d</td><td>%s</td></tr>\n" %
  108. (s.PRN, s.elevation, s.azimuth, s.ss,
  109. s.used and 'Y' or 'N'))
  110. fh.write("\t\t\t\t</table>\n\t\t\t\t<table border=0>\n")
  111. def row(l, v):
  112. fh.write("\t\t\t\t\t<tr><th>%s:</th><td>%s</td></tr>\n" % (l, v))
  113. def deg_to_str(a, hemi):
  114. return '%.6f %c' % (abs(a), hemi[a < 0])
  115. row('Time', self.utc or 'N/A')
  116. if self.fix.mode >= MODE_2D:
  117. row('Latitude', deg_to_str(self.fix.latitude, 'SN'))
  118. row('Longitude', deg_to_str(self.fix.longitude, 'WE'))
  119. row('Altitude', self.fix.mode == MODE_3D and "%f m" %
  120. self.fix.altitude or 'N/A')
  121. row('Speed', isfinite(self.fix.speed) and "%f m/s" %
  122. self.fix.speed or 'N/A')
  123. row('Course', isfinite(self.fix.track) and "%f°" %
  124. self.fix.track or 'N/A')
  125. else:
  126. row('Latitude', 'N/A')
  127. row('Longitude', 'N/A')
  128. row('Altitude', 'N/A')
  129. row('Speed', 'N/A')
  130. row('Course', 'N/A')
  131. row('EPX', isfinite(self.fix.epx) and "%f m" % self.fix.epx or 'N/A')
  132. row('EPY', isfinite(self.fix.epy) and "%f m" % self.fix.epy or 'N/A')
  133. row('EPV', isfinite(self.fix.epv) and "%f m" % self.fix.epv or 'N/A')
  134. row('Climb', self.fix.mode == MODE_3D and isfinite(self.fix.climb) and
  135. "%f m/s" % self.fix.climb or 'N/A')
  136. state = "INIT"
  137. if not (self.valid & ONLINE_SET):
  138. newstate = 0
  139. state = "OFFLINE"
  140. else:
  141. newstate = self.fix.mode
  142. if newstate == MODE_2D:
  143. state = "2D FIX"
  144. elif newstate == MODE_3D:
  145. state = "3D FIX"
  146. else:
  147. state = "NO FIX"
  148. if newstate != self.state:
  149. self.statetimer = time.time()
  150. self.state = newstate
  151. row('State', "%s (%d secs)" % (state, time.time() - self.statetimer))
  152. fh.write("""\t\t\t\t</table>
  153. \t\t\t</td>
  154. \t\t\t<td>
  155. \t\t\t\t<canvas id=satview width=425 height=425>
  156. \t\t\t\t\t<p>Your browser needs HTML5 &lt;canvas> support to display
  157. the satellite view correctly.</p>
  158. \t\t\t\t</canvas>
  159. \t\t\t\t<script>draw_satview();</script>
  160. \t\t\t</td>
  161. \t\t</tr>
  162. \t</table>
  163. </body>
  164. </html>
  165. """)
  166. def js(self, fh):
  167. fh.write("""// draw the satellite view
  168. function draw_satview() {
  169. var c = document.getElementById('satview');
  170. if (!c.getContext) return;
  171. var ctx = c.getContext('2d');
  172. if (!ctx) return;
  173. var circle = Math.PI * 2,
  174. M = function (x, y) { ctx.moveTo(x, y); },
  175. L = function (x, y) { ctx.lineTo(x, y); };
  176. ctx.save();
  177. ctx.clearRect(0, 0, c.width, c.height);
  178. ctx.translate(210, 210);
  179. // grid and labels
  180. ctx.strokeStyle = 'black';
  181. ctx.beginPath();
  182. ctx.arc(0, 0, 200, 0, circle, 0);
  183. ctx.stroke();
  184. ctx.beginPath();
  185. ctx.strokeText('N', -4, -202);
  186. ctx.strokeText('W', -210, 4);
  187. ctx.strokeText('E', 202, 4);
  188. ctx.strokeText('S', -4, 210);
  189. ctx.strokeStyle = 'grey';
  190. ctx.beginPath();
  191. ctx.arc(0, 0, 100, 0, circle, 0);
  192. M(2, 0);
  193. ctx.arc(0, 0, 2, 0, circle, 0);
  194. ctx.stroke();
  195. ctx.strokeStyle = 'lightgrey';
  196. ctx.save();
  197. ctx.beginPath();
  198. M(0, -200); L(0, 200); ctx.rotate(circle / 8);
  199. M(0, -200); L(0, 200); ctx.rotate(circle / 8);
  200. M(0, -200); L(0, 200); ctx.rotate(circle / 8);
  201. M(0, -200); L(0, 200);
  202. ctx.stroke();
  203. ctx.restore();
  204. // tracks
  205. ctx.lineWidth = 0.6;
  206. ctx.strokeStyle = 'red';
  207. """)
  208. # Draw the tracks
  209. for t in self.sattrack.values():
  210. if t.posn:
  211. fh.write(" ctx.globalAlpha = %s; ctx.beginPath(); "
  212. "%sctx.stroke();\n" %
  213. (t.stale == 0 and '0.66' or '1', t.track()))
  214. fh.write("""
  215. // satellites
  216. ctx.lineWidth = 1;
  217. ctx.strokeStyle = 'black';
  218. """)
  219. # Draw the satellites
  220. for s in self.satellites:
  221. el, az = s.elevation, s.azimuth
  222. if el == 0 and az == 0:
  223. continue # Skip satellites with unknown position
  224. x, y = polartocart(el, az)
  225. fill = not s.used and 'lightgrey' or \
  226. s.ss < 30 and 'red' or \
  227. s.ss < 35 and 'yellow' or \
  228. s.ss < 40 and 'green' or 'lime'
  229. # Center PRNs in the marker
  230. offset = s.PRN < 10 and 3 or s.PRN >= 100 and -3 or 0
  231. fh.write(" ctx.beginPath(); ctx.fillStyle = '%s'; " % fill)
  232. if s.PRN > 32: # Draw a square for SBAS satellites
  233. fh.write("ctx.rect(%d, %d, 16, 16); " % (x - 8, y - 8))
  234. else:
  235. fh.write("ctx.arc(%d, %d, 8, 0, circle, 0); " % (x, y))
  236. fh.write("ctx.fill(); ctx.stroke(); "
  237. "ctx.strokeText('%s', %d, %d);\n" %
  238. (s.PRN, x - 6 + offset, y + 4))
  239. fh.write("""
  240. ctx.restore();
  241. }
  242. """)
  243. def make_stale(self):
  244. for t in self.sattrack.values():
  245. if t.stale:
  246. t.stale -= 1
  247. def delete_stale(self):
  248. stales = []
  249. for prn in self.sattrack.keys():
  250. if self.sattrack[prn].stale == 0:
  251. stales.append(prn)
  252. self.needsupdate = 1
  253. for prn in stales:
  254. del self.sattrack[prn]
  255. def insert_sat(self, prn, x, y):
  256. try:
  257. t = self.sattrack[prn]
  258. except KeyError:
  259. self.sattrack[prn] = t = Track(prn)
  260. if t.add(x, y):
  261. self.needsupdate = 1
  262. def update_tracks(self):
  263. self.make_stale()
  264. for s in self.satellites:
  265. x, y = polartocart(s.elevation, s.azimuth)
  266. self.insert_sat(s.PRN, x, y)
  267. self.delete_stale()
  268. def generate_html(self, htmlfile, jsfile):
  269. fh = open(htmlfile, 'w')
  270. self.html(fh, jsfile)
  271. fh.close()
  272. def generate_js(self, jsfile):
  273. fh = open(jsfile, 'w')
  274. self.js(fh)
  275. fh.close()
  276. def run(self, suffix, period):
  277. jsfile = 'gpsd' + suffix + '.js'
  278. htmlfile = 'gpsd' + suffix + '.html'
  279. if period is not None:
  280. end = time.time() + period
  281. self.needsupdate = 1
  282. self.stream(WATCH_ENABLE | WATCH_NEWSTYLE)
  283. for report in self:
  284. if report['class'] not in ('TPV', 'SKY'):
  285. continue
  286. self.update_tracks()
  287. if self.needsupdate:
  288. self.generate_js(jsfile)
  289. self.needsupdate = 0
  290. self.generate_html(htmlfile, jsfile)
  291. if period is not None and (
  292. period <= 0 and self.fix.mode >= MODE_2D or
  293. period > 0 and time.time() > end
  294. ):
  295. break
  296. def main():
  297. argv = sys.argv[1:]
  298. factors = {
  299. 's': 1, 'm': 60, 'h': 60 * 60, 'd': 24 * 60 * 60
  300. }
  301. arg = argv and argv[0] or '0'
  302. if arg[-1:] in factors.keys():
  303. period = int(arg[:-1]) * factors[arg[-1]]
  304. elif arg == 'c':
  305. period = None
  306. else:
  307. period = int(arg)
  308. prefix = '-' + arg
  309. sat = SatTracks()
  310. # restore the tracks
  311. pfile = 'tracks.p'
  312. if os.path.isfile(pfile):
  313. p = open(pfile, 'rb')
  314. try:
  315. sat.sattrack = pickle.load(p)
  316. except ValueError:
  317. print("Ignoring incompatible tracks file.", file=sys.stderr)
  318. p.close()
  319. try:
  320. sat.run(prefix, period)
  321. except KeyboardInterrupt:
  322. # save the tracks
  323. p = open(pfile, 'wb')
  324. # No protocol is backward-compatible from Python 3 to Python 2,
  325. # so we just use the default and punt at load time if needed.
  326. pickle.dump(sat.sattrack, p)
  327. p.close()
  328. if __name__ == '__main__':
  329. main()