123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538 |
- """webgps.py
- This is a Python port of webgps.c
- from http://www.wireless.org.au/~jhecker/gpsd/
- by Beat Bolli <me+gps@drbeat.li>
- It creates a skyview of the currently visible GPS satellites and their tracks
- over a time period.
- Usage:
- ./webgps.py [duration]
- duration may be
- - a number of seconds
- - a number followed by a time unit ('s' for seconds, 'm' for minutes,
- 'h' for hours or 'd' for days, e.g. '4h' for a duration of four hours)
- - the letter 'c' for continuous operation
- If duration is missing, the current skyview is generated and webgps.py exits
- immediately. This is the same as giving a duration of 0.
- If a duration is given, webgps.py runs for this duration and generates the
- tracks of the GPS satellites in view. If the duration is the letter 'c',
- the script never exits and continuously updates the skyview.
- webgps.py generates two files: a HTML5 file that can be browsed, and a
- JavaScript file that contains the drawing commands for the skyview. The HTML5
- file auto-refreshes every five minutes. The generated file names are
- "gpsd-<duration>.html" and "gpsd-<duration>.js".
- If webgps.py is interrupted with Ctrl-C before the duration is over, it saves
- the current tracks into the file "tracks.j". This is a JSON file. If this file
- is present on start of webgps.py, it is loaded. This allows to restart
- webgps.py without losing accumulated satellite tracks.
- """
- from __future__ import absolute_import, print_function, division
- import math
- import os
- import sys
- import time
- from gps import *
- gps_version = '@VERSION@'
- if gps.__version__ != gps_version:
- sys.stderr.write("webgps.py: ERROR: need gps module version %s, got %s\n" %
- (gps_version, gps.__version__))
- sys.exit(1)
- TRACKMAX = 1024
- STALECOUNT = 10
- DIAMETER = 200
- def polartocart(el, az):
- """Polartocart."""
- radius = DIAMETER * (1 - el / 90.0)
- theta = Deg2Rad(float(az - 90))
- return (
-
- int(radius * math.cos(theta) + 0.5),
- int(radius * math.sin(theta) + 0.5)
- )
- class Track:
- """Store the track of one satellite."""
- def __init__(self, prn):
- """Init class track."""
- self.prn = prn
- self.stale = 0
- self.posn = []
- def add(self, x, y):
- """Add track."""
- pos = (x, y)
- self.stale = STALECOUNT
- if not self.posn or self.posn[-1] != pos:
- self.posn.append(pos)
- if len(self.posn) > TRACKMAX:
- self.posn = self.posn[-TRACKMAX:]
- return 1
- return 0
- def track(self):
- """Return the track as canvas drawing operations."""
- mid_thing = ');L('
- return('M(%s);' % mid_thing.join(
- map(lambda x: '%d,%d' % (x[0], x[1]), self.posn)))
- class SatTracks(gps):
- """gpsd client writing HTML5 and <canvas> output."""
- def __init__(self):
- """Init class SatTracks."""
- super(SatTracks, self).__init__()
- self.backing = {}
- self.sattrack = {}
- self.state = None
- self.statetimer = time.time()
- self.needsupdate = 0
- def html(self, fh, jsfile):
- """Writh HTML."""
- fh.write("""<!DOCTYPE html>
- <html lang="en"><head>
- \t<meta http-equiv="Refresh" content="300">
- \t<meta charset='utf-8'>
- \t<title>GPSD Satellite Positions and Readings</title>
- \t<style>
- \t\t.num td { text-align: right; }
- \t\tth { text-align: left; }
- \t\ttable {
- \t\t\tborder: none;
- \t\t\tborder-collapse: collapse;
- \t\t}
- \t\tbody {
- \t\t\tcolor: #839496;
- \t\t\tbackground: #fdf6e3;
- \t\t\tdisplay: grid;
- \t\t\tgrid-gap: 10px;
- \t\t}
- \t\t.dark {
- \t\t\tcolor: #839496;
- \t\t\tbackground: #002b36;
- \t\t}
- \t\t@media only screen and (min-width: 900px) {
- \t\t\tbody {
- \t\t\t\tgrid-template-areas:
- \t\t\t\t\t'tpv satview sky'
- \t\t\t\t\t'misc satview sky'
- \t\t\t\t\t'light ep dop';
- \t\t\t}
- \t\t}
- \t\t@media only screen and (min-width: 680px) and (max-width: 899px) {
- \t\t\tbody {
- \t\t\t\tgrid-template-areas:
- \t\t\t\t'sky satview'
- \t\t\t\t'tpv satview'
- \t\t\t\t'dop ep'
- \t\t\t\t'light misc';
- \t\t\t}
- \t\t}
- \t\t@media only screen and (min-width: 460px) and (max-width: 679px) {
- \t\t\tbody {
- \t\t\t\tgrid-template-areas:
- \t\t\t\t'satview'
- \t\t\t\t'tpv'
- \t\t\t\t'sky'
- \t\t\t\t'misc'
- \t\t\t\t'ep'
- \t\t\t\t'dop'
- \t\t\t\t'light';
- \t\t\t}
- \t\t}
- \t\t@media only screen and (max-width: 459px) {
- \t\t\t#satview {
- \t\t\t\tdisplay: none
- \t\t\t}
- \t\t\tbody {
- \t\t\t\tgrid-template-areas:
- \t\t\t\t'tpv'
- \t\t\t\t'sky'
- \t\t\t\t'misc'
- \t\t\t\t'ep'
- \t\t\t\t'dop'
- \t\t\t\t'light';
- \t\t\t}
- \t\t}
- \t\t.wide td:nth-child(odd),
- \t\t.tall tr:nth-child(6n),
- \t\t.tall tr:nth-child(6n+4),
- \t\t.tall tr:nth-child(6n+5) {
- \t\t\tbackground: rgba(131,148,150,0.25);
- \t\t}
- \t</style>
- \t<script src='%s'></script>
- </head><body onload="draw_satview();">
- \t<div style="grid-area: sky;"><table class="num tall">
- \t\t<tr><th>PRN:</th><th>Elev:</th><th>Azim:</th><th>SNR:</th><th>Used:</th></tr>
- """ % jsfile)
- sats = self.satellites[:]
- sats.sort(key=lambda x: x.PRN)
- for s in sats:
- fh.write("\t\t<tr><td>%d</td><td>%s</td><td>%s</td>"
- "<td>%d</td><td>%s</td></tr>\n" %
- (s.PRN,
- (-10 <= s.elevation <= 90) and s.elevation or 'N/A',
- (0 <= s.azimuth < 360) and s.azimuth or 'N/A',
- s.ss, s.used and 'Y' or 'N'))
- fh.write('\t</table></div>\n')
- def fgetnans(parent, children):
- """Fgetnans."""
- result = []
- for child in children:
- k = parent.get(child, float('nan'))
- result.append(isfinite(k) and str(k) or 'N/A')
- return result
- s = dict.get(self.backing, 'SKY', None)
- if s:
- dops = 'x y h v p t g'.split(" ")
- fh.write('\t<div style="grid-area: dop;"><table class="wide">\n'
- '\t\t<tr><th colspan="7">Dilution Of Precision</th>'
- '</tr>\n')
- fh.write('\t\t<tr><th>' + '</th><th>'.join(dops) + '</th></tr>\n')
- result = fgetnans(s, map(lambda e: e+"dop", dops))
- fh.write('\t\t<tr class="num"><td>%s</td></tr>\n' %
- '</td><td>'.join(result))
- fh.write('\t</table></div>\n')
- s = dict.get(self.backing, 'TPV', None)
- if s:
- eps = 'x y v c s d t'.split(" ")
- fh.write('\t<div style="grid-area: ep;"><table class="wide">\n')
- fh.write('\t\t<tr><th colspan="7">Estimated Precision</th></tr>\n')
- fh.write('\t\t<tr><th>' + '</th><th>'.join(eps) + '</th></tr>\n')
- result = fgetnans(s, map(lambda e: "ep"+e, eps))
- fh.write('\t\t<tr class="num"><td>%s m</td><td>%s m</td>'
- '<td>%s m/s</td><td>%s m/s</td>'
- '<td>%s m</td><td>%s deg</td><td>%s !s</td></tr>\n' %
- (result[0], result[1], result[2], result[3], result[4],
- result[5], result[6]))
- fh.write('\t</table></div>\n')
- fh.write('\t<div style="grid-area: misc;">\n')
- if 'TOFF' in self.backing:
- s = self.backing['TOFF']
- ns = int((int(s.real_sec) - int(s.clock_sec)) * 1000000000
- + (int(s.real_nsec) - int(s.clock_nsec)))
- fh.write('\t\tTime OFFset: %f ms<br/>\n' % (float(ns)/1.0e6))
- if 'PPS' in self.backing:
- s = self.backing['PPS']
- ns = int((int(s.real_sec) - int(s.clock_sec)) * 1000000000
- + (int(s.real_nsec) - int(s.clock_nsec)))
- fh.write('\t\tPPS offset: %d us<br/>\n' % (float(ns)/1.0e3))
- fh.write('\t\tPPS precision: %f<br/>\n' % s.precision)
- qerr = s.get('qErr', float('nan'))
- fh.write(isfinite(qerr) and ("\t\tPPS sawtooth %fps<br/>\n"
- % qerr) or "")
- if 'DEVICES' in self.backing:
- for (index, value) in enumerate(
- self.backing['DEVICES']['devices']):
- for key in value:
- fh.write('\t\t[%d]%s: %s<br/>\n' %
- (index, key, value[key]))
- if 'OSC' in self.backing:
- s = self.backing['OSC']
- fh.write('\t\tOscillator')
- fh.write(': %s running' % (s['running'] and '' or 'not'))
- fh.write(' with%s GPS PPS' % (s['reference'] and '' or 'out'))
- fh.write('and is %s disciplined' %
- (s['disciplined'] and '' or 'not'))
- fh.write('<br/>\n')
- fh.write('Oscillator delta: %fns<br>\n' % s['delta'])
- fh.write('\t</div>\n')
- def row(ln, v):
- """Write raw."""
- fh.write("\t\t<tr><th>%s:</th><td>%s</td></tr>\n" % (ln, v))
- def deg_to_str(a, hemi):
- """deg_to_str."""
- return '%.6f %c' % (abs(a), hemi[a < 0])
- def moderows(mode, table):
- """Moderows."""
- for line in table:
- if line[0] > mode or not isfinite(line[3]):
- row(line[1], 'N/A')
- continue
- row(line[1], line[2] % line[3])
- fh.write('\t<div style="grid-area: tpv;"><table class="tall">\n')
- row('Time', self.utc or 'N/A')
- if self.fix.mode >= MODE_2D:
- row('Latitude', deg_to_str(self.fix.latitude, 'SN'))
- row('Longitude', deg_to_str(self.fix.longitude, 'WE'))
- else:
- row('Latitude', 'N/A')
- row('Longitude', 'N/A')
- moderows(self.fix.mode, [
- [3, 'altHAE', '%f m', self.fix.altHAE],
- [3, 'altMSL', '%f m', self.fix.altMSL],
- [2, 'Speed', '%f m/s', self.fix.speed],
- [2, 'Course', '%f°', self.fix.track],
- [3, 'Climb', '%f m/s', self.fix.climb],
- ])
- state = "INIT"
- if not (self.valid & ONLINE_SET):
- newstate = 0
- state = "OFFLINE"
- else:
- newstate = self.fix.mode
- if newstate == MODE_2D:
- state = "2D FIX"
- elif newstate == MODE_3D:
- state = "3D FIX"
- else:
- state = "NO FIX"
- if newstate != self.state:
- self.statetimer = time.time()
- self.state = newstate
- row('State', "%s (%d secs)" % (state, time.time() - self.statetimer))
- fh.write("""\t</table></div>
- \t<div style="grid-area: satview;"
- ><canvas id="satview" width="425" height="425">
- \t\t<p>Your browser needs HTML5 <canvas> support to display
- \t\tthe satellite view correctly.</p>
- \t</canvas></div>
- \t<div style="grid-area: light">
- \t\t<button onclick="document.body.classList.toggle('dark')"
- >light switch</button>
- \t</div>
- </body></html>
- """)
- def js(self, fh):
- """Write the js."""
- fh.write("""// draw the satellite view
- function draw_satview() {
- var c = document.getElementById('satview');
- if (!c.getContext) return;
- var ctx = c.getContext('2d');
- if (!ctx) return;
- var circle = Math.PI * 2,
- M = function (x, y) { ctx.moveTo(x, y); },
- L = function (x, y) { ctx.lineTo(x, y); };
- ctx.save();
- ctx.clearRect(0, 0, c.width, c.height);
- ctx.translate(210, 210);
- // grid and labels
- ctx.strokeStyle = '#839496';
- ctx.beginPath();
- ctx.arc(0, 0, 200, 0, circle, 0);
- ctx.stroke();
- ctx.beginPath();
- ctx.strokeText('N', -4, -202);
- ctx.strokeText('W', -210, 4);
- ctx.strokeText('E', 202, 4);
- ctx.strokeText('S', -4, 210);
- ctx.beginPath();
- ctx.arc(0, 0, 100, 0, circle, 0);
- M(2, 0);
- ctx.arc(0, 0, 2, 0, circle, 0);
- ctx.stroke();
- ctx.save();
- ctx.beginPath();
- M(0, -200); L(0, 200);
- M(-200, 0); L(200, 0); ctx.rotate(circle / 8);
- M(0, -200); L(0, 200);
- M(-200, 0); L(200, 0);
- ctx.stroke();
- ctx.restore();
- // tracks
- ctx.lineWidth = 0.6;
- ctx.strokeStyle = 'red';
- """)
-
- for t in self.sattrack.values():
- if t.posn:
- fh.write(" ctx.globalAlpha = %s; ctx.beginPath(); "
- "%sctx.stroke();\n" %
- (t.stale == 0 and '0.66' or '1', t.track()))
- fh.write("""
- // satellites
- ctx.lineWidth = 1;
- ctx.strokeStyle = '#839496';
- """)
-
- for s in self.satellites:
- el, az = s.elevation, s.azimuth
- if el == 0 and az == 0:
- continue
- x, y = polartocart(el, az)
- fill = not s.used and 'lightgrey' or \
- s.ss < 30 and 'red' or \
- s.ss < 35 and 'yellow' or \
- s.ss < 40 and 'green' or 'lime'
-
- offset = s.PRN < 10 and 3 or s.PRN >= 100 and -3 or 0
- fh.write(" ctx.beginPath(); ctx.fillStyle = '%s'; " % fill)
- if s.PRN > 32:
- fh.write("ctx.rect(%d, %d, 16, 16); " % (x - 8, y - 8))
- else:
- fh.write("ctx.arc(%d, %d, 8, 0, circle, 0); " % (x, y))
- fh.write("ctx.fill(); ctx.stroke(); "
- "ctx.strokeText('%s', %d, %d);\n" %
- (s.PRN, x - 6 + offset, y + 4))
- fh.write("""
- ctx.restore();
- }
- """)
- def make_stale(self):
- """Nake_stale."""
- for t in self.sattrack.values():
- if t.stale:
- t.stale -= 1
- def delete_stale(self):
- """"Delete_stale."""
- stales = []
- for prn in self.sattrack.keys():
- if self.sattrack[prn].stale == 0:
- stales.append(prn)
- self.needsupdate = 1
- for prn in stales:
- del self.sattrack[prn]
- def insert_sat(self, prn, x, y):
- """Insert_sat."""
- try:
- t = self.sattrack[prn]
- except KeyError:
- self.sattrack[prn] = t = Track(prn)
- if t.add(x, y):
- self.needsupdate = 1
- def update_tracks(self):
- """update_tracks."""
- self.make_stale()
- for s in self.satellites:
- x, y = polartocart(s.elevation, s.azimuth)
- self.insert_sat(s.PRN, x, y)
- self.delete_stale()
- def run(self, suffix, period):
- """Run."""
- jsfile = 'gpsd' + suffix + '.js'
- htmlfile = 'gpsd' + suffix + '.html'
- if period is not None:
- end = time.time() + period
- self.needsupdate = 1
- self.stream(WATCH_ENABLE | WATCH_NEWSTYLE | WATCH_PPS)
- for report in self:
- self.backing[report['class']] = report
- if report['class'] not in ('TPV', 'SKY'):
- continue
- self.update_tracks()
- if self.needsupdate:
- with open(jsfile, 'w') as jfh:
- self.js(jfh)
- self.needsupdate = 0
- with open(htmlfile, 'w') as hfh:
- self.html(hfh, jsfile)
- if period is not None and (
- period <= 0 and self.fix.mode >= MODE_2D or
- period > 0 and time.time() > end
- ):
- break
- def main():
- """Main."""
- argv = sys.argv[1:]
- factors = {
- 's': 1, 'm': 60, 'h': 60 * 60, 'd': 24 * 60 * 60
- }
- arg = argv and argv[0] or '0'
- if arg[-1:] in factors.keys():
- period = int(arg[:-1]) * factors[arg[-1]]
- elif arg == 'c':
- period = None
- else:
- period = int(arg)
- prefix = '-' + arg
- sat = SatTracks()
-
- jfile = 'tracks.j'
- if os.path.isfile(jfile):
- with open(jfile, 'r') as j:
- try:
- dictionary = json.load(j)
- for t in dictionary.values():
- prn = t['prn']
- sat.sattrack[prn] = Track(prn)
- sat.sattrack[prn].stale = t['stale']
- sat.sattrack[prn].posn = t['posn']
- except ValueError:
- print("tracker.py WARNING: Ignoring incompatible tracks file.",
- file=sys.stderr)
- try:
- sat.run(prefix, period)
- except KeyboardInterrupt:
-
- with open(jfile, 'w') as j:
- dictionary = {}
- for t in sat.sattrack.values():
- dictionary[t.prn] = dict(prn=t.prn, stale=t.stale, posn=t.posn)
- json.dump(dictionary, j)
- print("tracker.py INFORMATION: saving state", file=sys.stderr)
- if __name__ == '__main__':
- main()
|