webgps.py.in 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. #!@PYSHEBANG@
  2. # encoding: utf-8
  3. # @GENERATED@
  4. # This code runs compatibly under Python 2 and 3.x for x >= 2.
  5. # Preserve this property!
  6. #
  7. # This file is Copyright 2010 by the GPSD project
  8. # SPDX-License-Identifier: BSD-2-clause
  9. # Codacy D203 and D211 conflict, I choose D203
  10. # Codacy D212 and D213 conflict, I choose D212
  11. """webgps.py
  12. This is a Python port of webgps.c
  13. from http://www.wireless.org.au/~jhecker/gpsd/
  14. by Beat Bolli <me+gps@drbeat.li>
  15. It creates a skyview of the currently visible GPS satellites and their tracks
  16. over a time period.
  17. Usage:
  18. ./webgps.py [duration]
  19. duration may be
  20. - a number of seconds
  21. - a number followed by a time unit ('s' for seconds, 'm' for minutes,
  22. 'h' for hours or 'd' for days, e.g. '4h' for a duration of four hours)
  23. - the letter 'c' for continuous operation
  24. If duration is missing, the current skyview is generated and webgps.py exits
  25. immediately. This is the same as giving a duration of 0.
  26. If a duration is given, webgps.py runs for this duration and generates the
  27. tracks of the GPS satellites in view. If the duration is the letter 'c',
  28. the script never exits and continuously updates the skyview.
  29. webgps.py generates two files: a HTML5 file that can be browsed, and a
  30. JavaScript file that contains the drawing commands for the skyview. The HTML5
  31. file auto-refreshes every five minutes. The generated file names are
  32. "gpsd-<duration>.html" and "gpsd-<duration>.js".
  33. If webgps.py is interrupted with Ctrl-C before the duration is over, it saves
  34. the current tracks into the file "tracks.j". This is a JSON file. If this file
  35. is present on start of webgps.py, it is loaded. This allows to restart
  36. webgps.py without losing accumulated satellite tracks.
  37. """
  38. from __future__ import absolute_import, print_function, division
  39. import math
  40. import os
  41. import sys
  42. import time
  43. from gps import *
  44. gps_version = '@VERSION@'
  45. if gps.__version__ != gps_version:
  46. sys.stderr.write("webgps.py: ERROR: need gps module version %s, got %s\n" %
  47. (gps_version, gps.__version__))
  48. sys.exit(1)
  49. TRACKMAX = 1024
  50. STALECOUNT = 10
  51. DIAMETER = 200
  52. def polartocart(el, az):
  53. """Polartocart."""
  54. radius = DIAMETER * (1 - el / 90.0) # * math.cos(Deg2Rad(float(el)))
  55. theta = Deg2Rad(float(az - 90))
  56. return (
  57. # Changed this back to normal orientation - fw
  58. int(radius * math.cos(theta) + 0.5),
  59. int(radius * math.sin(theta) + 0.5)
  60. )
  61. class Track:
  62. """Store the track of one satellite."""
  63. def __init__(self, prn):
  64. """Init class track."""
  65. self.prn = prn
  66. self.stale = 0
  67. self.posn = [] # list of (x, y) tuples
  68. def add(self, x, y):
  69. """Add track."""
  70. pos = (x, y)
  71. self.stale = STALECOUNT
  72. if not self.posn or self.posn[-1] != pos:
  73. self.posn.append(pos)
  74. if len(self.posn) > TRACKMAX:
  75. self.posn = self.posn[-TRACKMAX:]
  76. return 1
  77. return 0
  78. def track(self):
  79. """Return the track as canvas drawing operations."""
  80. mid_thing = ');L('
  81. return('M(%s);' % mid_thing.join(
  82. map(lambda x: '%d,%d' % (x[0], x[1]), self.posn)))
  83. class SatTracks(gps):
  84. """gpsd client writing HTML5 and <canvas> output."""
  85. def __init__(self):
  86. """Init class SatTracks."""
  87. super(SatTracks, self).__init__()
  88. self.backing = {}
  89. self.sattrack = {} # maps PRNs to Tracks
  90. self.state = None
  91. self.statetimer = time.time()
  92. self.needsupdate = 0
  93. def html(self, fh, jsfile):
  94. """Writh HTML."""
  95. fh.write("""<!DOCTYPE html>
  96. <html lang="en"><head>
  97. \t<meta http-equiv="Refresh" content="300">
  98. \t<meta charset='utf-8'>
  99. \t<title>GPSD Satellite Positions and Readings</title>
  100. \t<style>
  101. \t\t.num td { text-align: right; }
  102. \t\tth { text-align: left; }
  103. \t\ttable {
  104. \t\t\tborder: none;
  105. \t\t\tborder-collapse: collapse;
  106. \t\t}
  107. \t\tbody {
  108. \t\t\tcolor: #839496;
  109. \t\t\tbackground: #fdf6e3;
  110. \t\t\tdisplay: grid;
  111. \t\t\tgrid-gap: 10px;
  112. \t\t}
  113. \t\t.dark {
  114. \t\t\tcolor: #839496;
  115. \t\t\tbackground: #002b36;
  116. \t\t}
  117. \t\t@media only screen and (min-width: 900px) {
  118. \t\t\tbody {
  119. \t\t\t\tgrid-template-areas:
  120. \t\t\t\t\t'tpv satview sky'
  121. \t\t\t\t\t'misc satview sky'
  122. \t\t\t\t\t'light ep dop';
  123. \t\t\t}
  124. \t\t}
  125. \t\t@media only screen and (min-width: 680px) and (max-width: 899px) {
  126. \t\t\tbody {
  127. \t\t\t\tgrid-template-areas:
  128. \t\t\t\t'sky satview'
  129. \t\t\t\t'tpv satview'
  130. \t\t\t\t'dop ep'
  131. \t\t\t\t'light misc';
  132. \t\t\t}
  133. \t\t}
  134. \t\t@media only screen and (min-width: 460px) and (max-width: 679px) {
  135. \t\t\tbody {
  136. \t\t\t\tgrid-template-areas:
  137. \t\t\t\t'satview'
  138. \t\t\t\t'tpv'
  139. \t\t\t\t'sky'
  140. \t\t\t\t'misc'
  141. \t\t\t\t'ep'
  142. \t\t\t\t'dop'
  143. \t\t\t\t'light';
  144. \t\t\t}
  145. \t\t}
  146. \t\t@media only screen and (max-width: 459px) {
  147. \t\t\t#satview {
  148. \t\t\t\tdisplay: none
  149. \t\t\t}
  150. \t\t\tbody {
  151. \t\t\t\tgrid-template-areas:
  152. \t\t\t\t'tpv'
  153. \t\t\t\t'sky'
  154. \t\t\t\t'misc'
  155. \t\t\t\t'ep'
  156. \t\t\t\t'dop'
  157. \t\t\t\t'light';
  158. \t\t\t}
  159. \t\t}
  160. \t\t.wide td:nth-child(odd),
  161. \t\t.tall tr:nth-child(6n),
  162. \t\t.tall tr:nth-child(6n+4),
  163. \t\t.tall tr:nth-child(6n+5) {
  164. \t\t\tbackground: rgba(131,148,150,0.25);
  165. \t\t}
  166. \t</style>
  167. \t<script src='%s'></script>
  168. </head><body onload="draw_satview();">
  169. \t<div style="grid-area: sky;"><table class="num tall">
  170. \t\t<tr><th>PRN:</th><th>Elev:</th><th>Azim:</th><th>SNR:</th><th>Used:</th></tr>
  171. """ % jsfile)
  172. sats = self.satellites[:]
  173. sats.sort(key=lambda x: x.PRN)
  174. for s in sats:
  175. fh.write("\t\t<tr><td>%d</td><td>%s</td><td>%s</td>"
  176. "<td>%d</td><td>%s</td></tr>\n" %
  177. (s.PRN,
  178. (-10 <= s.elevation <= 90) and s.elevation or 'N/A',
  179. (0 <= s.azimuth < 360) and s.azimuth or 'N/A',
  180. s.ss, s.used and 'Y' or 'N'))
  181. fh.write('\t</table></div>\n')
  182. def fgetnans(parent, children):
  183. """Fgetnans."""
  184. result = []
  185. for child in children:
  186. k = parent.get(child, float('nan'))
  187. result.append(isfinite(k) and str(k) or 'N/A')
  188. return result
  189. s = dict.get(self.backing, 'SKY', None)
  190. if s:
  191. dops = 'x y h v p t g'.split(" ")
  192. fh.write('\t<div style="grid-area: dop;"><table class="wide">\n'
  193. '\t\t<tr><th colspan="7">Dilution Of Precision</th>'
  194. '</tr>\n')
  195. fh.write('\t\t<tr><th>' + '</th><th>'.join(dops) + '</th></tr>\n')
  196. result = fgetnans(s, map(lambda e: e+"dop", dops))
  197. fh.write('\t\t<tr class="num"><td>%s</td></tr>\n' %
  198. '</td><td>'.join(result))
  199. fh.write('\t</table></div>\n')
  200. s = dict.get(self.backing, 'TPV', None)
  201. if s:
  202. eps = 'x y v c s d t'.split(" ")
  203. fh.write('\t<div style="grid-area: ep;"><table class="wide">\n')
  204. fh.write('\t\t<tr><th colspan="7">Estimated Precision</th></tr>\n')
  205. fh.write('\t\t<tr><th>' + '</th><th>'.join(eps) + '</th></tr>\n')
  206. result = fgetnans(s, map(lambda e: "ep"+e, eps))
  207. fh.write('\t\t<tr class="num"><td>%s m</td><td>%s m</td>'
  208. '<td>%s m/s</td><td>%s m/s</td>'
  209. '<td>%s m</td><td>%s deg</td><td>%s !s</td></tr>\n' %
  210. (result[0], result[1], result[2], result[3], result[4],
  211. result[5], result[6]))
  212. fh.write('\t</table></div>\n')
  213. fh.write('\t<div style="grid-area: misc;">\n')
  214. if 'TOFF' in self.backing:
  215. s = self.backing['TOFF']
  216. ns = int((int(s.real_sec) - int(s.clock_sec)) * 1000000000
  217. + (int(s.real_nsec) - int(s.clock_nsec)))
  218. fh.write('\t\tTime OFFset: %f ms<br/>\n' % (float(ns)/1.0e6))
  219. if 'PPS' in self.backing:
  220. s = self.backing['PPS']
  221. ns = int((int(s.real_sec) - int(s.clock_sec)) * 1000000000
  222. + (int(s.real_nsec) - int(s.clock_nsec)))
  223. fh.write('\t\tPPS offset: %d us<br/>\n' % (float(ns)/1.0e3))
  224. fh.write('\t\tPPS precision: %f<br/>\n' % s.precision)
  225. qerr = s.get('qErr', float('nan'))
  226. fh.write(isfinite(qerr) and ("\t\tPPS sawtooth %fps<br/>\n"
  227. % qerr) or "")
  228. if 'DEVICES' in self.backing:
  229. for (index, value) in enumerate(
  230. self.backing['DEVICES']['devices']):
  231. for key in value:
  232. fh.write('\t\t[%d]%s: %s<br/>\n' %
  233. (index, key, value[key]))
  234. if 'OSC' in self.backing:
  235. s = self.backing['OSC']
  236. fh.write('\t\tOscillator')
  237. fh.write(': %s running' % (s['running'] and '' or 'not'))
  238. fh.write(' with%s GPS PPS' % (s['reference'] and '' or 'out'))
  239. fh.write('and is %s disciplined' %
  240. (s['disciplined'] and '' or 'not'))
  241. fh.write('<br/>\n')
  242. fh.write('Oscillator delta: %fns<br>\n' % s['delta'])
  243. fh.write('\t</div>\n')
  244. def row(ln, v):
  245. """Write raw."""
  246. fh.write("\t\t<tr><th>%s:</th><td>%s</td></tr>\n" % (ln, v))
  247. def deg_to_str(a, hemi):
  248. """deg_to_str."""
  249. return '%.6f %c' % (abs(a), hemi[a < 0])
  250. def moderows(mode, table):
  251. """Moderows."""
  252. for line in table:
  253. if line[0] > mode or not isfinite(line[3]):
  254. row(line[1], 'N/A')
  255. continue
  256. row(line[1], line[2] % line[3])
  257. fh.write('\t<div style="grid-area: tpv;"><table class="tall">\n')
  258. row('Time', self.utc or 'N/A')
  259. if self.fix.mode >= MODE_2D:
  260. row('Latitude', deg_to_str(self.fix.latitude, 'SN'))
  261. row('Longitude', deg_to_str(self.fix.longitude, 'WE'))
  262. else:
  263. row('Latitude', 'N/A')
  264. row('Longitude', 'N/A')
  265. moderows(self.fix.mode, [
  266. [3, 'altHAE', '%f m', self.fix.altHAE],
  267. [3, 'altMSL', '%f m', self.fix.altMSL],
  268. [2, 'Speed', '%f m/s', self.fix.speed],
  269. [2, 'Course', '%f&deg;', self.fix.track],
  270. [3, 'Climb', '%f m/s', self.fix.climb],
  271. ])
  272. state = "INIT"
  273. if not (self.valid & ONLINE_SET):
  274. newstate = 0
  275. state = "OFFLINE"
  276. else:
  277. newstate = self.fix.mode
  278. if newstate == MODE_2D:
  279. state = "2D FIX"
  280. elif newstate == MODE_3D:
  281. state = "3D FIX"
  282. else:
  283. state = "NO FIX"
  284. if newstate != self.state:
  285. self.statetimer = time.time()
  286. self.state = newstate
  287. row('State', "%s (%d secs)" % (state, time.time() - self.statetimer))
  288. fh.write("""\t</table></div>
  289. \t<div style="grid-area: satview;"
  290. ><canvas id="satview" width="425" height="425">
  291. \t\t<p>Your browser needs HTML5 &lt;canvas&gt; support to display
  292. \t\tthe satellite view correctly.</p>
  293. \t</canvas></div>
  294. \t<div style="grid-area: light">
  295. \t\t<button onclick="document.body.classList.toggle('dark')"
  296. >light switch</button>
  297. \t</div>
  298. </body></html>
  299. """)
  300. def js(self, fh):
  301. """Write the js."""
  302. fh.write("""// draw the satellite view
  303. function draw_satview() {
  304. var c = document.getElementById('satview');
  305. if (!c.getContext) return;
  306. var ctx = c.getContext('2d');
  307. if (!ctx) return;
  308. var circle = Math.PI * 2,
  309. M = function (x, y) { ctx.moveTo(x, y); },
  310. L = function (x, y) { ctx.lineTo(x, y); };
  311. ctx.save();
  312. ctx.clearRect(0, 0, c.width, c.height);
  313. ctx.translate(210, 210);
  314. // grid and labels
  315. ctx.strokeStyle = '#839496';
  316. ctx.beginPath();
  317. ctx.arc(0, 0, 200, 0, circle, 0);
  318. ctx.stroke();
  319. ctx.beginPath();
  320. ctx.strokeText('N', -4, -202);
  321. ctx.strokeText('W', -210, 4);
  322. ctx.strokeText('E', 202, 4);
  323. ctx.strokeText('S', -4, 210);
  324. ctx.beginPath();
  325. ctx.arc(0, 0, 100, 0, circle, 0);
  326. M(2, 0);
  327. ctx.arc(0, 0, 2, 0, circle, 0);
  328. ctx.stroke();
  329. ctx.save();
  330. ctx.beginPath();
  331. M(0, -200); L(0, 200);
  332. M(-200, 0); L(200, 0); ctx.rotate(circle / 8);
  333. M(0, -200); L(0, 200);
  334. M(-200, 0); L(200, 0);
  335. ctx.stroke();
  336. ctx.restore();
  337. // tracks
  338. ctx.lineWidth = 0.6;
  339. ctx.strokeStyle = 'red';
  340. """)
  341. # Draw the tracks
  342. for t in self.sattrack.values():
  343. if t.posn:
  344. fh.write(" ctx.globalAlpha = %s; ctx.beginPath(); "
  345. "%sctx.stroke();\n" %
  346. (t.stale == 0 and '0.66' or '1', t.track()))
  347. fh.write("""
  348. // satellites
  349. ctx.lineWidth = 1;
  350. ctx.strokeStyle = '#839496';
  351. """)
  352. # Draw the satellites
  353. for s in self.satellites:
  354. el, az = s.elevation, s.azimuth
  355. if el == 0 and az == 0:
  356. continue # Skip satellites with unknown position
  357. x, y = polartocart(el, az)
  358. fill = not s.used and 'lightgrey' or \
  359. s.ss < 30 and 'red' or \
  360. s.ss < 35 and 'yellow' or \
  361. s.ss < 40 and 'green' or 'lime'
  362. # Center PRNs in the marker
  363. offset = s.PRN < 10 and 3 or s.PRN >= 100 and -3 or 0
  364. fh.write(" ctx.beginPath(); ctx.fillStyle = '%s'; " % fill)
  365. if s.PRN > 32: # Draw a square for SBAS satellites
  366. fh.write("ctx.rect(%d, %d, 16, 16); " % (x - 8, y - 8))
  367. else:
  368. fh.write("ctx.arc(%d, %d, 8, 0, circle, 0); " % (x, y))
  369. fh.write("ctx.fill(); ctx.stroke(); "
  370. "ctx.strokeText('%s', %d, %d);\n" %
  371. (s.PRN, x - 6 + offset, y + 4))
  372. fh.write("""
  373. ctx.restore();
  374. }
  375. """)
  376. def make_stale(self):
  377. """Nake_stale."""
  378. for t in self.sattrack.values():
  379. if t.stale:
  380. t.stale -= 1
  381. def delete_stale(self):
  382. """"Delete_stale."""
  383. stales = []
  384. for prn in self.sattrack.keys():
  385. if self.sattrack[prn].stale == 0:
  386. stales.append(prn)
  387. self.needsupdate = 1
  388. for prn in stales:
  389. del self.sattrack[prn]
  390. def insert_sat(self, prn, x, y):
  391. """Insert_sat."""
  392. try:
  393. t = self.sattrack[prn]
  394. except KeyError:
  395. self.sattrack[prn] = t = Track(prn)
  396. if t.add(x, y):
  397. self.needsupdate = 1
  398. def update_tracks(self):
  399. """update_tracks."""
  400. self.make_stale()
  401. for s in self.satellites:
  402. x, y = polartocart(s.elevation, s.azimuth)
  403. self.insert_sat(s.PRN, x, y)
  404. self.delete_stale()
  405. def run(self, suffix, period):
  406. """Run."""
  407. jsfile = 'gpsd' + suffix + '.js'
  408. htmlfile = 'gpsd' + suffix + '.html'
  409. if period is not None:
  410. end = time.time() + period
  411. self.needsupdate = 1
  412. self.stream(WATCH_ENABLE | WATCH_NEWSTYLE | WATCH_PPS)
  413. for report in self:
  414. self.backing[report['class']] = report
  415. if report['class'] not in ('TPV', 'SKY'):
  416. continue
  417. self.update_tracks()
  418. if self.needsupdate:
  419. with open(jsfile, 'w') as jfh:
  420. self.js(jfh)
  421. self.needsupdate = 0
  422. with open(htmlfile, 'w') as hfh:
  423. self.html(hfh, jsfile)
  424. if period is not None and (
  425. period <= 0 and self.fix.mode >= MODE_2D or
  426. period > 0 and time.time() > end
  427. ):
  428. break
  429. def main():
  430. """Main."""
  431. argv = sys.argv[1:]
  432. factors = {
  433. 's': 1, 'm': 60, 'h': 60 * 60, 'd': 24 * 60 * 60
  434. }
  435. arg = argv and argv[0] or '0'
  436. if arg[-1:] in factors.keys():
  437. period = int(arg[:-1]) * factors[arg[-1]]
  438. elif arg == 'c':
  439. period = None
  440. else:
  441. period = int(arg)
  442. prefix = '-' + arg
  443. sat = SatTracks()
  444. # restore the tracks
  445. jfile = 'tracks.j'
  446. if os.path.isfile(jfile):
  447. with open(jfile, 'r') as j:
  448. try:
  449. dictionary = json.load(j)
  450. for t in dictionary.values():
  451. prn = t['prn']
  452. sat.sattrack[prn] = Track(prn)
  453. sat.sattrack[prn].stale = t['stale']
  454. sat.sattrack[prn].posn = t['posn']
  455. except ValueError:
  456. print("tracker.py WARNING: Ignoring incompatible tracks file.",
  457. file=sys.stderr)
  458. try:
  459. sat.run(prefix, period)
  460. except KeyboardInterrupt:
  461. # save the tracks
  462. with open(jfile, 'w') as j:
  463. dictionary = {}
  464. for t in sat.sattrack.values():
  465. dictionary[t.prn] = dict(prn=t.prn, stale=t.stale, posn=t.posn)
  466. json.dump(dictionary, j)
  467. print("tracker.py INFORMATION: saving state", file=sys.stderr)
  468. if __name__ == '__main__':
  469. main()