123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602 |
- #!@PYSHEBANG@
- # -*- coding: UTF-8
- # @GENERATED@
- '''
- xgps -- test client for gpsd
- usage: xgps [-?] [-D level] [-h] [-l degmfmt] [-r rotation] [-u units] [-V]
- [server[:port[:device]]]
- -? Print help and exit.
- -D lvl Set debug level to lvl
- -h Print help and exit.
- -l {d|m|s} Select lat/lon format
- d = DD.dddddd (default)
- m = DD MM.mmmm'
- s = DD MM' SS.sss"
- -r rotation Set rotation
- -u units Set units to Imperial, Nautical or Metric
- -V Print version and exit.
- Options can be placed in the XGPSOPTS environment variable.
- XGPSOPTS is processed before the CLI options.
- '''
- # ENVIRONMENT:
- # Options in the XGPSOPTS environment variable will be parsed before
- # the CLI options. A handy place to put your '-l m -u m '
- #
- # This file is Copyright 2010 by the GPSD project
- # SPDX-License-Identifier: BSD-2-clause
- #
- # This code runs compatibly under Python 2 and 3.x for x >= 2.
- # Preserve this property!
- from __future__ import absolute_import, print_function, division
- import cairo
- import getopt
- import math
- import os
- import socket
- import sys
- import time
- # Gtk3 imports. Gtk3 requires the require_version(), which then causes
- # pylint to complain about the subsequent "non-top" imports.
- # On gentoo these are from the dev-python/pygobject package.
- # "Python bindings for GObject Introspection"
- # It looks like PyGTK, but it is not. PyGTK is unmaintained.
- try:
- import gi
- gi.require_version('Gtk', '3.0')
- except ImportError as err:
- # ModuleNotFoundError needs Python 3.6
- sys.stderr.write("xgps: ERROR %s. Probably missing package pygobject.\n" % err)
- sys.exit(1)
- except ValueError as err:
- # Gtk2 may be installed, has no require_version()
- sys.stderr.write("xgps: ERROR %s\n" % err)
- sys.exit(1)
- from gi.repository import Gtk # pylint: disable=wrong-import-position
- from gi.repository import Gdk # pylint: disable=wrong-import-position
- from gi.repository import GdkPixbuf # pylint: disable=wrong-import-position
- from gi.repository import GLib # pylint: disable=wrong-import-position
- # pylint wants local modules last
- try:
- import gps
- import gps.clienthelpers
- except ImportError as e:
- sys.stderr.write(
- "xgps: can't load Python gps libraries -- check PYTHONPATH.\n")
- sys.stderr.write("%s\n" % e)
- sys.exit(1)
- gps_version = '@VERSION@'
- if gps.__version__ != gps_version:
- sys.stderr.write("xgps: ERROR: need gps module version %s, got %s\n" %
- (gps_version, gps.__version__))
- sys.exit(1)
- # MAXCHANNELS, from gps.h, currently 120
- MAXCHANNELS = 120
- # MAXCHANDISP, max channels to display
- # Use our own MAXCHANDISP value, due to the tradeoff between max sats and
- # the window size. Ideally, this should be dynamic.
- MAXCHANDISP = 28
- # how to sort the Satellite List
- # some of ("PRN","el","az","ss","used") with optional '-' to reverse sort
- # by default, used at the top, then sort PRN
- SKY_VIEW_SORT_FIELDS = ('-used', 'PRN')
- # Each GNSS constellation reuses the same PRNs. To differentiate they are
- # all mushed into the PRN. Different GPS mush differently. gpsd should
- # have untangled and put in gnssid:svid
- def gnssid_str(sat):
- "convert gnssid:svid to short and long strings"
- # gnssid:svid appeared in gpsd 3.18
- # allow for old servers
- if 'gnssid' not in sat or 'svid' not in sat:
- return ' '
- if 0 >= sat.svid:
- return [' ', '']
- if 0 == sat.gnssid:
- return ['GP', 'GPS']
- if 1 == sat.gnssid:
- return ['SB', 'SBAS']
- if 2 == sat.gnssid:
- return ['GA', 'Galileo']
- if 3 == sat.gnssid:
- return ['BD', 'BeiDou']
- if 4 == sat.gnssid:
- return ['IM', 'IMES']
- if 5 == sat.gnssid:
- return ['QZ', 'QZSS']
- if 6 == sat.gnssid:
- return ['GL', 'GLONASS']
- if 7 == sat.gnssid:
- return ['IR', 'IRNSS']
- return ' '
- class unit_adjustments(object):
- "Encapsulate adjustments for unit systems."
- def __init__(self, units=None):
- "Initialize class unit_adjustments"
- self.altfactor = gps.METERS_TO_FEET
- self.altunits = "ft"
- self.speedfactor = gps.MPS_TO_MPH
- self.speedunits = "mph"
- if units is None:
- units = gps.clienthelpers.gpsd_units()
- if units in (gps.clienthelpers.unspecified, gps.clienthelpers.imperial,
- "imperial", "i"):
- pass
- elif units in (gps.clienthelpers.nautical, "nautical", "n"):
- self.altfactor = gps.METERS_TO_FEET
- self.altunits = "ft"
- self.speedfactor = gps.MPS_TO_KNOTS
- self.speedunits = "knots"
- elif units in (gps.clienthelpers.metric, "metric", "m"):
- self.altfactor = 1.0
- self.altunits = "m"
- self.speedfactor = gps.MPS_TO_KPH
- self.speedunits = "kph"
- else:
- raise ValueError # Should never happen
- def fit_to_grid(x, y, line_width):
- "Adjust coordinates to produce sharp lines."
- if line_width % 1.0 != 0:
- # Can't have sharp lines for non-integral line widths.
- return float(x), float(y) # Be consistent about returning floats
- if line_width % 2 == 0:
- # Round to a pixel corner.
- return round(x), round(y)
- # Round to a pixel center.
- return int(x) + 0.5, int(y) + 0.5
- def fit_circle_to_grid(x, y, radius, line_width):
- """Adjust circle coordinates and radius to produce sharp horizontal
- and vertical tangents."""
- r = radius
- x1, y1 = fit_to_grid(x - r, y - r, line_width)
- x2, y2 = fit_to_grid(x + r, y + r, line_width)
- x, y = (x1 + x2) / 2, (y1 + y2) / 2
- r = (x2 - x1 + y2 - y1) / 4
- return x, y, r
- class SkyView(Gtk.DrawingArea):
- "Satellite skyview, encapsulates pygtk's draw-on-expose behavior."
- # See <http://faq.pygtk.org/index.py?req=show&file=faq18.008.htp>
- HORIZON_PAD = 50 # How much whitespace to leave around horizon
- SAT_RADIUS = 5 # Diameter of satellite circle
- def __init__(self, rotation=None):
- "Initialize class SkyView"
- Gtk.DrawingArea.__init__(self)
- # GObject.GObject.__init__(self)
- self.set_size_request(400, 400)
- self.cr = None # New cairo context for each expose event
- self.step_of_grid = 45 # default step of polar grid
- self.connect('size-allocate', self.on_size_allocate)
- self.connect('draw', self.on_draw)
- self.satellites = []
- self.sat_xy = []
- self.center_x = self.center_y = self.radius = None
- self.rotate = rotation
- if self.rotate is None:
- self.rotate = 0
- self.connect('motion_notify_event', self.popup)
- self.popover = None
- self.pop_xy = (None, None)
- def popdown(self):
- "See if need to popdown the sat details"
- if self.popover:
- self.popover.popdown()
- self.popover = None
- self.pop_xy = (None, None)
- def popup(self, skyview, event):
- "See if need to popup the sat details"
- for (x, y, sat) in self.sat_xy:
- if ((SkyView.SAT_RADIUS >= abs(x - event.x) and
- SkyView.SAT_RADIUS >= abs(y - event.y))):
- # got a sat match under the mouse
- # print((x, y))
- if ((self.pop_xy[0] and self.pop_xy[1] and
- self.pop_xy == (int(x), int(y)))):
- # popup already up here, ignore event
- # print("(%d, %d)" % (x, y))
- return
- if self.popover:
- # remove any old, no longer current popup
- # this never happens?
- self.popdown()
- # mouse is over a satellite, do popup
- self.pop_xy = (int(x), int(y))
- self.popover = Gtk.Popover()
- if "gnssid" in sat and "svid" in sat:
- # gnssid:svid in gpsd 3.18 and up
- constellation = gnssid_str(sat)[1]
- gnss_str = "%-8s %4d\n" % (constellation, sat.svid)
- else:
- gnss_str = ''
- if 'health' not in sat:
- health = "Unk"
- elif 1 == sat.health:
- health = "OK"
- elif 2 == sat.health:
- health = "Bad"
- else:
- health = "Unk"
- label = Gtk.Label()
- s = ("<span font_desc='monospace 10'>PRN %10d\n"
- "%s"
- "Elevation %4.1f\n"
- "Azimuth %5.1f\n"
- "SNR %4.1f\n"
- "Used %9s\n"
- "Health %7s</span>" %
- (sat.PRN, gnss_str,
- sat.el, sat.az, sat.ss, 'Yes' if sat.used else 'No',
- health))
- label.set_markup(s)
- rectangle = Gdk.Rectangle()
- rectangle.x = x - 25
- rectangle.y = y - 25
- rectangle.width = 50
- rectangle.height = 50
- self.popover.set_modal(False)
- self.popover.set_relative_to(self)
- self.popover.set_position(Gtk.PositionType.TOP)
- self.popover.set_pointing_to(rectangle)
- self.popover.add(label)
- self.popover.popup()
- self.popover.show_all()
- # remove popup after 15 seconds
- GLib.timeout_add(15000, self.popdown)
- return
- if self.popover:
- # remove any old, no longer current popup
- # this never happens?
- self.popdown()
- def on_size_allocate(self, _unused, allocation):
- "Adjust SkyView on size change"
- width = allocation.width
- height = allocation.height
- x = width // 2
- y = height // 2
- r = (min(width, height) - SkyView.HORIZON_PAD) // 2
- x, y, r = fit_circle_to_grid(x, y, r, 1)
- self.center_x = x
- self.center_y = y
- self.radius = r
- def set_color(self, r, g, b):
- """Set foreground color for drawing. rgb: 0 to 255"""
- # Gdk.color_parse() deprecated in GDK 3.14
- # gdkcolor = Gdk.color_parse(spec)
- r = r / 255.0
- g = g / 255.0
- b = b / 255.0
- self.cr.set_source_rgb(r, g, b)
- def draw_circle(self, x, y, radius, filled=False):
- "Draw a circle centered on the specified midpoint."
- lw = self.cr.get_line_width()
- r = int(2 * radius + 0.5) // 2
- x, y, r = fit_circle_to_grid(x, y, radius, lw)
- self.cr.arc(x, y, r, 0, math.pi * 2.0)
- self.cr.close_path()
- if filled:
- self.cr.fill()
- else:
- self.cr.stroke()
- def draw_line(self, x1, y1, x2, y2):
- "Draw a line between specified points."
- lw = self.cr.get_line_width()
- x1, y1 = fit_to_grid(x1, y1, lw)
- x2, y2 = fit_to_grid(x2, y2, lw)
- self.cr.move_to(x1, y1)
- self.cr.line_to(x2, y2)
- self.cr.stroke()
- def draw_square(self, x, y, radius, filled, flip):
- "Draw a square centered on the specified midpoint."
- lw = self.cr.get_line_width()
- if 0 == flip:
- x1, y1 = fit_to_grid(x - radius, y - radius, lw)
- x2, y2 = fit_to_grid(x + radius, y + radius, lw)
- self.cr.rectangle(x1, y1, x2 - x1, y2 - y1)
- else:
- self.cr.move_to(x, y + radius)
- self.cr.line_to(x + radius, y)
- self.cr.line_to(x, y - radius)
- self.cr.line_to(x - radius, y)
- self.cr.close_path()
- if filled:
- self.cr.fill()
- else:
- self.cr.stroke()
- def draw_string(self, x, y, text, centered=True):
- "Draw a text on the skyview."
- self.cr.select_font_face("Sans", cairo.FONT_SLANT_NORMAL,
- cairo.FONT_WEIGHT_BOLD)
- self.cr.set_font_size(10)
- if centered:
- extents = self.cr.text_extents(text)
- # width / 2 + x_bearing
- x -= extents[2] / 2 + extents[0]
- # height / 2 + y_bearing
- y -= extents[3] / 2 + extents[1]
- self.cr.move_to(x, y)
- self.cr.show_text(text)
- self.cr.new_path()
- def draw_triangle(self, x, y, radius, filled, flip):
- "Draw a triangle centered on the specified midpoint."
- lw = self.cr.get_line_width()
- if flip in (0, 1):
- if 0 == flip:
- # down
- ytop = y + radius
- ybot = y - radius
- elif 1 == flip:
- # up
- ytop = y - radius
- ybot = y + radius
- x1, y1 = fit_to_grid(x, ytop, lw)
- x2, y2 = fit_to_grid(x + radius, ybot, lw)
- x3, y3 = fit_to_grid(x - radius, ybot, lw)
- else:
- # right
- ytop = y + radius
- ybot = y - radius
- x1, y1 = fit_to_grid(x - radius, ytop, lw)
- x2, y2 = fit_to_grid(x - radius, ybot, lw)
- x3, y3 = fit_to_grid(x + radius, y, lw)
- self.cr.move_to(x1, y1)
- self.cr.line_to(x2, y2)
- self.cr.line_to(x3, y3)
- self.cr.close_path()
- if filled:
- self.cr.fill()
- else:
- self.cr.stroke()
- def pol2cart(self, az, el):
- "Polar to Cartesian coordinates within the horizon circle."
- az = (az - self.rotate) % 360.0
- az *= (math.pi / 180) # Degrees to radians
- # Exact spherical projection would be like this:
- # el = sin((90.0 - el) * DEG_2_RAD);
- el = ((90.0 - el) / 90.0)
- xout = self.center_x + math.sin(az) * el * self.radius
- yout = self.center_y - math.cos(az) * el * self.radius
- return (xout, yout)
- def on_draw(self, widget, _unused):
- "Draw the skyview"
- window = widget.get_window()
- region = window.get_clip_region()
- context = window.begin_draw_frame(region)
- self.cr = context.get_cairo_context()
- self.cr.set_line_width(1)
- self.cr.set_source_rgb(0, 0, 0)
- self.cr.paint()
- self.cr.set_source_rgb(1, 1, 1)
- # The zenith marker
- self.draw_circle(self.center_x, self.center_y, 6, filled=False)
- # The horizon circle
- if self.step_of_grid == 45:
- # The circle corresponding to 45 degrees elevation.
- # There are two ways we could plot this. Projecting the sphere
- # on the display plane, the circle would have a diameter of
- # sin(45) ~ 0.7. But the naive linear mapping, just splitting
- # the horizon diameter in half, seems to work better visually.
- self.draw_circle(self.center_x, self.center_y, self.radius / 2,
- filled=False)
- elif self.step_of_grid == 30:
- self.draw_circle(self.center_x, self.center_y, self.radius * 2 / 3,
- filled=False)
- self.draw_circle(self.center_x, self.center_y, self.radius / 3,
- filled=False)
- self.draw_circle(self.center_x, self.center_y, self.radius,
- filled=False)
- (x1, y1) = self.pol2cart(0, 0)
- (x2, y2) = self.pol2cart(180, 0)
- self.draw_line(x1, y1, x2, y2)
- (x1, y1) = self.pol2cart(90, 0)
- (x2, y2) = self.pol2cart(270, 0)
- self.draw_line(x1, y1, x2, y2)
- # The compass-point letters
- (x, y) = self.pol2cart(0, -5)
- self.draw_string(x, y, "N")
- (x, y) = self.pol2cart(90, -5)
- self.draw_string(x, y, "E")
- (x, y) = self.pol2cart(180, -5)
- self.draw_string(x, y, "S")
- (x, y) = self.pol2cart(270, -5)
- self.draw_string(x, y, "W")
- # place an invisible space above to allow sats below horizon
- (x, y) = self.pol2cart(0, -10)
- self.draw_string(x, y, "")
- # The satellites
- self.cr.set_line_width(2)
- self.sat_xy = []
- for sat in self.satellites:
- if not 1 <= sat.PRN <= 437:
- # Bad PRN, skip. NMEA uses up to 437
- continue
- if not 0 <= sat.az <= 359:
- # Bad azimuth, skip.
- continue
- if not -10 <= sat.el <= 90:
- # Bad elevation, skip. Allow just below horizon
- continue
- # The Navika-100 reports el/az of 0/0 for SBAS satellites,
- # causing them to appear inappropriately at the "north point".
- # Although this value isn't technically illegal (and hence not
- # filtered above), excluding this one specific case has a very
- # low probability of excluding legitimate cases, while avoiding
- # the improper display in this case.
- # Note that this only excludes them from the map, not the list.
- if sat.az == 0 and sat.el == 0:
- continue
- (x, y) = self.pol2cart(sat.az, sat.el)
- # colorize by signal to noise ratio
- # RINEX 3 uses 9 steps: 1 to 9. Corresponding to
- # <12, 12-17, 18-23, 24-29, 30-35, 36-41, 42-47, 48-53, >= 54
- if sat.ss < 12:
- self.set_color(190, 190, 190) # gray
- elif sat.ss < 30:
- self.set_color(255, 0, 0) # red
- elif sat.ss < 36:
- # RINEX 3 says 30 is "threshold for good tracking"
- self.set_color(255, 255, 0) # yellow
- elif sat.ss < 42:
- self.set_color(0, 205, 0) # green3
- else:
- self.set_color(0, 255, 180) # green and some blue
- # shape by constellation
- constellation = gnssid_str(sat)[0]
- if constellation in ('GP', ' '):
- self.draw_circle(x, y, SkyView.SAT_RADIUS, sat.used)
- elif constellation == 'SB':
- self.draw_square(x, y, SkyView.SAT_RADIUS, sat.used, 0)
- elif constellation == 'GA':
- self.draw_triangle(x, y, SkyView.SAT_RADIUS, sat.used, 0)
- elif constellation == 'BD':
- self.draw_triangle(x, y, SkyView.SAT_RADIUS, sat.used, 1)
- elif constellation == 'GL':
- self.draw_square(x, y, SkyView.SAT_RADIUS, sat.used, 1)
- else:
- # QZSS, IMES, unknown or other
- self.draw_triangle(x, y, SkyView.SAT_RADIUS, sat.used, 2)
- self.sat_xy.append((x, y, sat))
- self.cr.set_source_rgb(1, 1, 1)
- self.draw_string(x + SkyView.SAT_RADIUS,
- y + (SkyView.SAT_RADIUS * 2), str(sat.PRN),
- centered=False)
- self.cr = None
- window.end_draw_frame(context)
- def redraw(self, satellites):
- "Redraw the skyview."
- self.satellites = satellites
- self.queue_draw()
- class NoiseView(object):
- "Encapsulate view object for watching noise statistics."
- COLUMNS = 2
- ROWS = 4
- noisefields = (
- # First column
- ("Time", "time"),
- ("Latitude", "lat"),
- ("Longitude", "lon"),
- ("Altitude", "alt"),
- # Second column
- ("RMS", "rms"),
- ("Major", "major"),
- ("Minor", "minor"),
- ("Orient", "orient"),
- )
- def __init__(self):
- "Initialize class NoiseView"
- self.widget = Gtk.Grid()
- self.noisewidgets = []
- for i in range(len(NoiseView.noisefields)):
- colbase = (i // NoiseView.ROWS) * 2
- label = Gtk.Label()
- label.set_markup("<span font_desc='sans 10'> %s:</span>" %
- NoiseView.noisefields[i][0])
- # force right alignment
- label.set_halign(Gtk.Align.END)
- self.widget.attach(label, colbase, i % NoiseView.ROWS, 1, 1)
- entry = Gtk.Label()
- # span gets lost later
- entry.set_markup("<span font_desc='monospace 10'> n/a </span>")
- self.widget.attach_next_to(entry, label,
- Gtk.PositionType.RIGHT, 1, 1)
- self.noisewidgets.append((NoiseView.noisefields[i][1], entry))
- def update(self, noise):
- "Update the GPGST data fields."
- markup = "<span font_desc='monospace 10'>%s </span>"
- for (attrname, widget) in self.noisewidgets:
- if hasattr(noise, attrname):
- s = str(getattr(noise, attrname))
- else:
- s = " n/a "
- widget.set_markup(markup % s)
- class AISView(object):
- "Encapsulate store and view objects for watching AIS data."
- AIS_ENTRIES = 10
- DWELLTIME = 360
- def __init__(self, deg_type):
- "Initialize the store and view."
- self.deg_type = deg_type
- self.name_to_mmsi = {}
- self.named = {}
- self.store = Gtk.ListStore(int, str, str, str, str, str)
- self.widget = Gtk.ScrolledWindow()
- self.widget.set_policy(Gtk.PolicyType.AUTOMATIC,
- Gtk.PolicyType.AUTOMATIC)
- self.view = Gtk.TreeView(model=self.store)
- self.widget.set_size_request(-1, 300)
- self.widget.add(self.view)
- for (i, label) in enumerate(('#', 'Name:', 'Callsign:',
- 'Destination:', "Lat/Lon:",
- "Information")):
- column = Gtk.TreeViewColumn(label)
- renderer = Gtk.CellRendererText()
- column.pack_start(renderer, expand=True)
- column.add_attribute(renderer, 'text', i)
- self.view.append_column(column)
- def enter(self, ais, name):
- "Add a named object (ship or station) to the store."
- if ais.mmsi in self.named:
- return False
- ais.entry_time = time.time()
- self.named[ais.mmsi] = ais
- self.name_to_mmsi[name] = ais.mmsi
- # Garbage-collect old entries
- try:
- for i in range(len(self.store)):
- here = self.store.get_iter(i)
- name = self.store.get_value(here, 1)
- mmsi = self.name_to_mmsi[name]
- if ((self.named[mmsi].entry_time <
- time.time() - AISView.DWELLTIME)):
- del self.named[mmsi]
- if name in self.name_to_mmsi:
- del self.name_to_mmsi[name]
- self.store.remove(here)
- except (ValueError, KeyError): # Invalid TreeIters throw these
- pass
- return True
- def latlon(self, lat, lon):
- "Latitude/longitude display in nice format."
- if lat < 0:
- latsuff = "S"
- elif lat > 0:
- latsuff = "N"
- else:
- latsuff = ""
- lat = gps.clienthelpers.deg_to_str(self.deg_type, lat)
- if lon < 0:
- lonsuff = "W"
- elif lon > 0:
- lonsuff = "E"
- else:
- lonsuff = ""
- lon = gps.clienthelpers.deg_to_str(self.deg_type, lon)
- return lat + latsuff + "/" + lon + lonsuff
- def update(self, ais):
- "Update the AIS data fields."
- if ais.type in (1, 2, 3, 18):
- if ais.mmsi in self.named:
- for i in range(len(self.store)):
- here = self.store.get_iter(i)
- name = self.store.get_value(here, 1)
- if name in self.name_to_mmsi:
- mmsi = self.name_to_mmsi[name]
- if mmsi == ais.mmsi:
- latlon = self.latlon(ais.lat, ais.lon)
- self.store.set_value(here, 4, latlon)
- elif ais.type == 4:
- if self.enter(ais, ais.mmsi):
- where = self.latlon(ais.lat, ais.lon)
- self.store.prepend(
- (ais.type, str(ais.mmsi), "(shore)", ais.timestamp, where,
- ais.epfd_text))
- elif ais.type == 5:
- if self.enter(ais, ais.shipname):
- self.store.prepend(
- (ais.type, ais.shipname, ais.callsign, ais.destination,
- "", str(ais.shiptype)))
- elif ais.type == 12:
- sender = ais.mmsi
- if sender in self.named:
- sender = self.named[sender].shipname
- recipient = ais.dest_mmsi
- if ((recipient in self.named and
- hasattr(self.named[recipient], "shipname"))):
- recipient = self.named[recipient].shipname
- self.store.prepend(
- (ais.type, sender, "", recipient, "", ais.text))
- elif ais.type == 14:
- sender = ais.mmsi
- if sender in self.named:
- sender = self.named[sender].shipname
- self.store.prepend(
- (ais.type, sender, "", "(broadcast)", "", ais.text))
- elif ais.type in (19, 24):
- if self.enter(ais, ais.shipname):
- self.store.prepend(
- (ais.type, ais.shipname, "(class B)", "", "",
- ais.shiptype_text))
- elif ais.type == 21:
- if self.enter(ais, ais.name):
- where = self.latlon(ais.lat, ais.lon)
- self.store.prepend(
- (ais.type, ais.name, "(%s navaid)" % ais.epfd_text,
- "", where, ais.aid_type_text))
- class Base(object):
- "Base class for all the output"
- ROWS = 9
- gpsfields = (
- # First column
- ("Time", lambda s, r: s.update_time(r)),
- ("Latitude", lambda s, r: s.update_latitude(r)),
- ("Longitude", lambda s, r: s.update_longitude(r)),
- ("Altitude HAE", lambda s, r: s.update_altitude(r, 0)),
- ("Altitude MSL", lambda s, r: s.update_altitude(r, 1)),
- ("Speed", lambda s, r: s.update_speed(r)),
- ("Climb", lambda s, r: s.update_climb(r)),
- ("Track True", lambda s, r: s.update_track(r, 0)),
- ("Track Mag", lambda s, r: s.update_track(r, 1)),
- # Second column
- ("Status", lambda s, r: s.update_status(r, 0)),
- ("For", lambda s, r: s.update_status(r, 1)),
- ("EPX", lambda s, r: s.update_err(r, "epx")),
- ("EPY", lambda s, r: s.update_err(r, "epy")),
- ("EPV", lambda s, r: s.update_err(r, "epv")),
- ("EPS", lambda s, r: s.update_err_speed(r, "eps")),
- ("EPC", lambda s, r: s.update_err_speed(r, "epc")),
- ("EPD", lambda s, r: s.update_err_degrees(r, "epd")),
- ("Mag Dec", lambda s, r: s.update_mag_dec(r)),
- # third column
- ("ECEF X", lambda s, r: s.update_ecef(r, "ecefx")),
- ("ECEF Y", lambda s, r: s.update_ecef(r, "ecefy")),
- ("ECEF Z", lambda s, r: s.update_ecef(r, "ecefz")),
- ("ECEF pAcc", lambda s, r: s.update_ecef(r, "ecefpAcc")),
- ("ECEF VX", lambda s, r: s.update_ecef(r, "ecefvx", "/s")),
- ("ECEF VY", lambda s, r: s.update_ecef(r, "ecefvy", "/s")),
- ("ECEF VZ", lambda s, r: s.update_ecef(r, "ecefvz", "/s")),
- ("ECEF vAcc", lambda s, r: s.update_ecef(r, "ecefvAcc", "/s")),
- ('Grid', lambda s, r: s.update_maidenhead(r)),
- # fourth column
- ("Sats Seen", lambda s, r: s.update_seen(r, 0)),
- ("Sats Used", lambda s, r: s.update_seen(r, 1)),
- ("XDOP", lambda s, r: s.update_dop(r, "xdop")),
- ("YDOP", lambda s, r: s.update_dop(r, "ydop")),
- ("HDOP", lambda s, r: s.update_dop(r, "hdop")),
- ("VDOP", lambda s, r: s.update_dop(r, "vdop")),
- ("PDOP", lambda s, r: s.update_dop(r, "pdop")),
- ("TDOP", lambda s, r: s.update_dop(r, "tdop")),
- ("GDOP", lambda s, r: s.update_dop(r, "gdop")),
- )
- def about(self, _unused):
- "Show about dialog"
- about = Gtk.AboutDialog()
- about.set_program_name("xgps")
- about.set_version("Versions:\n"
- "xgps %s\n"
- "PyGObject Version %d.%d.%d" %
- (gps_version, gi.version_info[0],
- gi.version_info[1], gi.version_info[2]))
- about.set_copyright("Copyright 2004 by The GPSD Project")
- about.set_website("https://www.gpsd.io")
- about.set_website_label("https://www.gpsd.io")
- about.set_license("BSD-2-clause")
- iconpath = gps.__iconpath__ + '/gpsd-logo.png'
- if os.access(iconpath, os.R_OK):
- pixbuf = GdkPixbuf.Pixbuf.new_from_file(iconpath)
- about.set_logo(pixbuf)
- about.run()
- about.destroy()
- def __init__(self, deg_type, rotation=None, title=""):
- "Initialize class Base"
- self.deg_type = deg_type
- self.rotate = rotation
- self.conversions = unit_adjustments()
- self.saved_mode = -1
- self.ais_latch = False
- self.noise_latch = False
- self.last_transition = 0.0
- self.daemon = None
- self.device = None
- self.window = Gtk.Window(type=Gtk.WindowType.TOPLEVEL)
- if not self.window.get_display():
- raise Exception("Can't open display")
- if title:
- title = " " + title
- self.window.set_title("xgps" + title)
- self.window.connect("delete-event", self.delete_event)
- self.window.set_resizable(False)
- # do the CSS thing
- style_provider = Gtk.CssProvider()
- css = b"""
- frame * {
- background-color: #FFF;
- color: #000;
- }
- """
- # font-desc: "Comic Sans 12";
- style_provider.load_from_data(css)
- Gtk.StyleContext.add_provider_for_screen(
- Gdk.Screen.get_default(),
- style_provider,
- Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
- )
- vbox = Gtk.VBox(homogeneous=False, spacing=0)
- self.window.add(vbox)
- self.window.connect("destroy", lambda _unused: Gtk.main_quit())
- menubar = Gtk.MenuBar()
- agr = Gtk.AccelGroup()
- self.window.add_accel_group(agr)
- # File
- topmenu = Gtk.MenuItem(label="File")
- menubar.append(topmenu)
- submenu = Gtk.Menu()
- topmenu.set_submenu(submenu)
- menui = Gtk.MenuItem(label="Connect")
- # key, mod = Gtk.accelerator_parse("<Control>Q")
- # menui.add_accelerator("activate", agr, key, mod,
- # Gtk.AccelFlags.VISIBLE)
- # menui.connect("activate", Gtk.main_quit)
- submenu.append(menui)
- menui = Gtk.MenuItem(label="Disconnect")
- # key, mod = Gtk.accelerator_parse("<Control>Q")
- # menui.add_accelerator("activate", agr, key, mod,
- # Gtk.AccelFlags.VISIBLE)
- # menui.connect("activate", Gtk.main_quit)
- submenu.append(menui)
- menui = Gtk.MenuItem(label="Quit")
- key, mod = Gtk.accelerator_parse("<Control>Q")
- menui.add_accelerator("activate", agr, key, mod,
- Gtk.AccelFlags.VISIBLE)
- menui.connect("activate", Gtk.main_quit)
- submenu.append(menui)
- # View
- topmenu = Gtk.MenuItem(label="View")
- menubar.append(topmenu)
- submenu = Gtk.Menu()
- topmenu.set_submenu(submenu)
- views = [["Skyview", True, "<Control>S", "Skyview"],
- ["Responses", True, "<Control>R", "Responses"],
- ["GPS Data", True, "<Control>G", "GPS"],
- ["Noise Statistics", False, "<Control>N", "Noise"],
- ["AIS Data", False, "<Control>A", "AIS"],
- ]
- self.menumap = {}
- for name, active, acc, handle in views:
- menui = Gtk.CheckMenuItem(label=name)
- self.menumap[handle] = menui
- menui.set_active(active)
- menui.connect("activate", self.view_toggle, handle)
- if acc:
- key, mod = Gtk.accelerator_parse(acc)
- menui.add_accelerator("activate", agr, key, mod,
- Gtk.AccelFlags.VISIBLE)
- submenu.append(menui)
- # Units
- topmenu = Gtk.MenuItem(label="Units")
- menubar.append(topmenu)
- submenu = Gtk.Menu()
- topmenu.set_submenu(submenu)
- units = [["Imperial", True, "i", 'i'],
- ["Nautical", False, "n", 'n'],
- ["Metric", False, "m", 'm'],
- ]
- menui = None
- for name, active, acc, handle in units:
- menui = Gtk.RadioMenuItem(group=menui, label=name)
- menui.set_active(active)
- menui.connect("activate", self.set_units, handle)
- if acc:
- key, mod = Gtk.accelerator_parse(acc)
- menui.add_accelerator("activate", agr, key, mod,
- Gtk.AccelFlags.VISIBLE)
- submenu.append(menui)
- submenu.append(Gtk.SeparatorMenuItem())
- units = [["DD.dd", True, "0", gps.clienthelpers.deg_dd],
- ["DD MM.mm", False, "1", gps.clienthelpers.deg_ddmm],
- ["DD MM SS.ss", False, "2", gps.clienthelpers.deg_ddmmss],
- ]
- menui = None
- for name, active, acc, handle in units:
- menui = Gtk.RadioMenuItem(group=menui, label=name)
- menui.set_active(active)
- menui.connect("activate", self.set_deg, handle)
- if acc:
- key, mod = Gtk.accelerator_parse(acc)
- menui.add_accelerator("activate", agr, key, mod,
- Gtk.AccelFlags.VISIBLE)
- submenu.append(menui)
- # Step of Grid
- topmenu = Gtk.MenuItem(label="Step of Grid")
- menubar.append(topmenu)
- submenu = Gtk.Menu()
- topmenu.set_submenu(submenu)
- grid = [["30 deg", False, "3", 30],
- ["45 deg", True, "4", 45],
- ["Off", False, "5", 0],
- ]
- menui = None
- for name, active, acc, handle in grid:
- menui = Gtk.RadioMenuItem(group=menui, label=name)
- menui.set_active(active)
- menui.connect("activate", self.set_step_of_grid, handle)
- if acc:
- key, mod = Gtk.accelerator_parse(acc)
- menui.add_accelerator("activate", agr, key, mod,
- Gtk.AccelFlags.VISIBLE)
- submenu.append(menui)
- submenu.append(Gtk.SeparatorMenuItem())
- skymr = [["Mag North Up", True, "6", None],
- ["Track Up", False, "7", True],
- ["True North Up", False, "8", 0],
- ]
- menui = None
- for name, active, acc, handle in skymr:
- menui = Gtk.RadioMenuItem(group=menui, label=name)
- menui.set_active(active)
- menui.connect("activate", self.set_skyview_n, handle)
- if acc:
- key, mod = Gtk.accelerator_parse(acc)
- menui.add_accelerator("activate", agr, key, mod,
- Gtk.AccelFlags.VISIBLE)
- submenu.append(menui)
- # Help
- topmenu = Gtk.MenuItem(label="Help")
- menubar.append(topmenu)
- submenu = Gtk.Menu()
- topmenu.set_submenu(submenu)
- menui = Gtk.MenuItem(label="About")
- menui.connect("activate", self.about)
- submenu.append(menui)
- vbox.pack_start(menubar, expand=False, fill=True, padding=0)
- self.satbox = Gtk.HBox(homogeneous=False, spacing=0)
- vbox.add(self.satbox)
- skyframe = Gtk.Frame(label="Satellite List")
- self.satbox.add(skyframe)
- self.satlist = Gtk.ListStore(str, str, str, str, str, str, str)
- view = Gtk.TreeView(model=self.satlist)
- satcols = [['', 0],
- ['svid', 1],
- ['PRN', 1],
- ['Elev', 1],
- ['Azim', 1],
- ['SNR', 1],
- ['Used', 0],
- ]
- for (i, satcol) in enumerate(satcols):
- renderer = Gtk.CellRendererText(xalign=satcol[1])
- column = Gtk.TreeViewColumn(satcol[0], renderer)
- column.add_attribute(renderer, 'text', i)
- view.append_column(column)
- self.row_iters = []
- for i in range(MAXCHANDISP):
- self.satlist.append(["", "", "", "", "", "", ""])
- self.row_iters.append(self.satlist.get_iter(i))
- skyframe.add(view)
- viewframe = Gtk.Frame(label="Skyview")
- self.satbox.add(viewframe)
- self.skyview = SkyView(self.rotate)
- try:
- # mouseovers fail with remote DISPLAY
- self.skyview.set_property('events',
- Gdk.EventMask.POINTER_MOTION_MASK)
- except NotImplementedError:
- # keep going anyway, w/o popups
- sys.stderr.write("xgps: WARNING: failed to grab mouse events, "
- "popups disabled\n")
- viewframe.add(self.skyview)
- # Display area for incoming JSON
- self.rawdisplay = Gtk.Entry()
- self.rawdisplay.set_editable(False)
- vbox.add(self.rawdisplay)
- # Display area for GPS Data
- self.dataframe = Gtk.Frame(label="GPS Data")
- # print("GPS Data css:", self.dataframe.get_css_name())
- datatable = Gtk.Grid()
- self.dataframe.add(datatable)
- gpswidgets = []
- # min col widths
- widths = [0, 25, 0, 20, 0, 23, 0, 8]
- for i in range(len(Base.gpsfields)):
- colbase = (i // Base.ROWS) * 2
- label = Gtk.Label()
- label.set_markup("<span font_desc='sans 10'> %s:</span>" %
- Base.gpsfields[i][0])
- # force right alignment
- label.set_halign(Gtk.Align.END)
- datatable.attach(label, colbase, i % Base.ROWS, 1, 1)
- entry = Gtk.Label()
- if 0 < widths[colbase + 1]:
- entry.set_width_chars(widths[colbase + 1])
- entry.set_selectable(True)
- # span gets lost later
- entry.set_markup("<span font_desc='monospace 10'> n/a </span>")
- datatable.attach_next_to(entry, label,
- Gtk.PositionType.RIGHT, 1, 1)
- gpswidgets.append(entry)
- vbox.add(self.dataframe)
- # Add noise box
- self.noisebox = Gtk.HBox(homogeneous=False, spacing=0)
- vbox.add(self.noisebox)
- noiseframe = Gtk.Frame(label="Noise Statistics")
- self.noisebox.add(noiseframe)
- self.noiseview = NoiseView()
- noiseframe.add(self.noiseview.widget)
- self.aisbox = Gtk.HBox(homogeneous=False, spacing=0)
- vbox.add(self.aisbox)
- aisframe = Gtk.Frame(label="AIS Data")
- self.aisbox.add(aisframe)
- self.aisview = AISView(self.deg_type)
- aisframe.add(self.aisview.widget)
- self.window.show_all()
- # Hide the Noise Statistics window until user selects it.
- self.noisebox.hide()
- # Hide the AIS window until user selects it.
- self.aisbox.hide()
- self.view_name_to_widget = {
- "Skyview": self.satbox,
- "Responses": self.rawdisplay,
- "GPS": self.dataframe,
- "Noise": self.noisebox,
- "AIS": self.aisbox}
- # Discard field labels and associate data hooks with their widgets
- Base.gpsfields = [(label_hook_widget[0][1], label_hook_widget[1])
- for label_hook_widget
- in zip(Base.gpsfields, gpswidgets)]
- def view_toggle(self, action, name):
- "Toggle widget view"
- # print("View toggle:", action.get_active(), name)
- if hasattr(self, 'view_name_to_widget'):
- if action.get_active():
- self.view_name_to_widget[name].show()
- else:
- self.view_name_to_widget[name].hide()
- # The effect we're after is to make the top-level window
- # resize itself to fit when we show or hide widgets.
- # This is undocumented magic to do that.
- self.window.resize(1, 1)
- def set_satlist_field(self, row, column, value):
- "Set a specified field in the satellite list."
- try:
- self.satlist.set_value(self.row_iters[row], column, str(value))
- except IndexError:
- sys.stderr.write("xgps: channel = %d, MAXCHANDISP = %d\n"
- % (row, MAXCHANDISP))
- def delete_event(self, _widget, _event, _data=None):
- "Say goodbye nicely"
- Gtk.main_quit()
- return False
- # State updates
- def update_time(self, data):
- "Update time"
- if hasattr(data, "time"):
- # str() just in case we get an old-style float.
- ret = str(data.time)
- else:
- ret = "n/a"
- if hasattr(data, "leapseconds"):
- ret += " (%u)" % data.leapseconds
- return ret
- def update_latitude(self, data):
- "Update latitude"
- if data.mode >= gps.MODE_2D and hasattr(data, "lat"):
- lat = gps.clienthelpers.deg_to_str(self.deg_type, data.lat)
- if data.lat < 0:
- ns = 'S'
- else:
- ns = 'N'
- return "%14s %s" % (lat, ns)
- return "n/a"
- def update_longitude(self, data):
- "Update longitude"
- if data.mode >= gps.MODE_2D and hasattr(data, "lon"):
- lon = gps.clienthelpers.deg_to_str(self.deg_type, data.lon)
- if data.lon < 0:
- ew = 'W'
- else:
- ew = 'E'
- return "%14s %s" % (lon, ew)
- return "n/a"
- def update_altitude(self, data, item):
- "Update altitude"
- ret = "n/a"
- if data.mode >= gps.MODE_3D:
- if 0 == item and hasattr(data, "altHAE"):
- ret = ("%10.3f %s" %
- ((data.altHAE * self.conversions.altfactor),
- self.conversions.altunits))
- if 1 == item and hasattr(data, "altMSL"):
- ret = ("%10.3f %s" %
- ((data.altMSL * self.conversions.altfactor),
- self.conversions.altunits))
- return ret
- def update_speed(self, data):
- "Update speed"
- if hasattr(data, "speed"):
- return "%9.3f %s" % (
- data.speed * self.conversions.speedfactor,
- self.conversions.speedunits)
- return "n/a"
- def update_climb(self, data):
- "Update climb"
- if hasattr(data, "climb"):
- return "%9.3f %s" % (
- data.climb * self.conversions.speedfactor,
- self.conversions.speedunits)
- return "n/a"
- def update_track(self, data, item):
- "Update track"
- if 0 == item and hasattr(data, "track"):
- return "%14s " % (
- gps.clienthelpers.deg_to_str(self.deg_type, data.track))
- if 1 == item and hasattr(data, "magtrack"):
- return "%14s " % (
- gps.clienthelpers.deg_to_str(self.deg_type, data.magtrack))
- return "n/a"
- def update_seen(self, data, item):
- "Update sats seen"
- # update sats seen/used in the GPS Data window
- if 0 == item and hasattr(data, 'satellites_seen'):
- return getattr(data, 'satellites_seen')
- if 1 == item and hasattr(data, 'satellites_used'):
- return getattr(data, 'satellites_used')
- return "n/a"
- def update_dop(self, data, doptype):
- "update a DOP in the GPS Data window"
- if hasattr(data, doptype):
- return "%5.2f" % getattr(data, doptype)
- return "n/a"
- def update_ecef(self, data, eceftype, speedunit=''):
- "update a ECEF in the GPS Data window"
- if hasattr(data, eceftype):
- value = getattr(data, eceftype)
- return ("% 14.3f %s%s" %
- (value * self.conversions.altfactor,
- self.conversions.altunits, speedunit))
- return "n/a"
- def update_err(self, data, errtype):
- "update a error estimate in the GPS Data window"
- if hasattr(data, errtype):
- return "%8.3f %s" % (
- getattr(data, errtype) * self.conversions.altfactor,
- self.conversions.altunits)
- return "n/a"
- def update_err_speed(self, data, errtype):
- "update speed error estimate in the GPS Data window"
- if hasattr(data, errtype):
- return "%8.3f %s" % (
- getattr(data, errtype) * self.conversions.speedfactor,
- self.conversions.speedunits)
- return "n/a"
- def update_err_degrees(self, data, errtype):
- "update heading error estimate in the GPS Data window"
- if hasattr(data, errtype):
- return ("%s " %
- (gps.clienthelpers.deg_to_str(self.deg_type,
- getattr(data, errtype))))
- return "n/a"
- def update_mag_dec(self, data):
- "update magnetic declination in the GPS Data window"
- if ((data.mode >= gps.MODE_2D and
- hasattr(data, "lat") and
- hasattr(data, "lon"))):
- off = gps.clienthelpers.mag_var(data.lat, data.lon)
- off2 = gps.clienthelpers.deg_to_str(self.deg_type, off)
- return off2
- return "n/a"
- def update_maidenhead(self, data):
- "update maidenhead grid square in the GPS Data window"
- if ((data.mode >= gps.MODE_2D and
- hasattr(data, "lat") and
- hasattr(data, "lon"))):
- return gps.clienthelpers.maidenhead(data.lat, data.lon)
- return "n/a"
- def update_status(self, data, item):
- "Update the status window"
- if 1 == item:
- return "%d secs" % (time.time() - self.last_transition)
- sub_status = ''
- if hasattr(data, 'status'):
- if gps.STATUS_DGPS_FIX == data.status:
- sub_status = " DGPS"
- elif gps.STATUS_RTK_FIX == data.status:
- sub_status = " RTKfix"
- elif gps.STATUS_RTK_FLT == data.status:
- sub_status = " RTKflt"
- elif gps.STATUS_DR == data.status:
- sub_status = " DR"
- elif gps.STATUS_GNSSDR == data.status:
- sub_status = " GNSSDR"
- elif gps.STATUS_TIME == data.status:
- sub_status = " FIXED"
- elif gps.STATUS_SIM == data.status:
- sub_status = " SIM"
- elif gps.STATUS_PPS_FIX == data.status:
- sub_status = " PPS"
- if data.mode == gps.MODE_2D:
- status = "2D%s FIX" % sub_status
- elif data.mode == gps.MODE_3D:
- if hasattr(data, 'status') and gps.STATUS_TIME == data.status:
- status = "FIXED SURVEYED"
- else:
- status = "3D%s FIX" % sub_status
- else:
- status = "NO FIX"
- if data.mode != self.saved_mode:
- self.last_transition = time.time()
- self.saved_mode = data.mode
- return status
- def update_gpsdata(self, tpv):
- "Update the GPS data fields."
- # the first 28 fields are updated using TPV data
- # the next 9 fields are updated using SKY data
- markup = "<span font_desc='monospace 10'>%s </span>"
- for (hook, widget) in Base.gpsfields[:27]:
- if hook: # Remove this guard when we have all hooks
- widget.set_markup(markup % hook(self, tpv))
- if self.skyview:
- if ((self.rotate is None
- and hasattr(tpv, 'lat') and hasattr(tpv, 'lon'))):
- self.skyview.rotate = gps.clienthelpers.mag_var(tpv.lat,
- tpv.lon)
- elif self.rotate is True and 'track' in tpv:
- self.skyview.rotate = tpv.track
- def update_version(self, ver):
- "Update the Version"
- if ver.release != gps_version:
- sys.stderr.write("%s: WARNING gpsd version %s different than "
- "expected %s\n" %
- (sys.argv[0], ver.release, gps_version))
- if ((ver.proto_major != gps.api_version_major or
- ver.proto_minor != gps.api_version_minor)):
- sys.stderr.write("%s: WARNING API version %s.%s different than "
- "expected %s.%s\n" %
- (sys.argv[0], ver.proto_major, ver.proto_minor,
- gps.api_version_major, gps.api_version_minor))
- def _int_to_str(self, value, min_val, max_val):
- "test val in range min to max, or return"
- if min_val <= value <= max_val:
- return '%3d' % value
- return 'n/a'
- def _tenth_to_str(self, value, min_val, max_val):
- "test val in range min to max, or return"
- if min_val <= value <= max_val:
- return '%5.1f' % value
- return 'n/a'
- def update_skyview(self, data):
- "Update the satellite list and skyview."
- data.satellites_seen = 0
- data.satellites_used = 0
- if hasattr(data, 'satellites'):
- satellites = data.satellites
- for fld in reversed(SKY_VIEW_SORT_FIELDS):
- rev = (fld[0] == '-')
- if rev:
- fld = fld[1:]
- satellites = sorted(
- satellites[:],
- key=lambda x: x[fld], reverse=rev)
- # print("Sats: ", satellites)
- for (i, satellite) in enumerate(satellites):
- yesno = 'N'
- data.satellites_seen += 1
- if satellite.used:
- yesno = 'Y'
- data.satellites_used += 1
- if 'health' not in satellite:
- yesno = ' ' + yesno
- elif 2 == satellite.health:
- yesno = ' u' + yesno
- else:
- yesno = ' ' + yesno
- if i >= MAXCHANDISP:
- # more than can be displaced
- continue
- self.set_satlist_field(i, 0, gnssid_str(satellite)[0])
- if 'svid' in satellite:
- # SBAS is in the 100's...
- self.set_satlist_field(i, 1,
- self._int_to_str(satellite.svid,
- 1, 199))
- # NMEA uses PRN up to 437
- self.set_satlist_field(i, 2,
- self._int_to_str(satellite.PRN, 1, 437))
- # allow satellites 10 degree below horizon
- self.set_satlist_field(i, 3,
- self._tenth_to_str(satellite.el,
- -10, 90))
- self.set_satlist_field(i, 4,
- self._tenth_to_str(satellite.az,
- 0, 359))
- self.set_satlist_field(i, 5,
- self._tenth_to_str(satellite.ss,
- 0, 100))
- self.set_satlist_field(i, 6, yesno)
- # clear rest of the list
- for i in range(data.satellites_seen, MAXCHANDISP):
- for j in range(0, 7):
- self.set_satlist_field(i, j, "")
- else:
- # clear all of the list
- for i in range(0, MAXCHANDISP):
- for j in range(0, 7):
- self.set_satlist_field(i, j, "")
- satellites = ()
- # repaint Skyview
- self.skyview.redraw(satellites)
- markup = "<span font_desc='monospace 10'>%s </span>"
- # the first 27 fields are updated using TPV data
- # the next 9 fields are updated using SKY data
- for (hook, widget) in Base.gpsfields[27:36]:
- if hook: # Remove this guard when we have all hooks
- widget.set_markup(markup % hook(self, data))
- # Preferences
- def set_skyview_n(self, system, handle):
- "Change the grid orientation."
- self.rotate = handle
- if handle is not None:
- self.skyview.rotate = handle
- def set_step_of_grid(self, system, handle):
- "Change the step of grid."
- # print("set_step_of_grid:", system, handle)
- if hasattr(self, 'skyview') and self.skyview is not None:
- self.skyview.step_of_grid = handle
- def set_deg(self, _unused, handle):
- "Change the degree format."
- # print("set_deg:", _unused, handle)
- self.deg_type = handle
- if hasattr(self, 'mvview') and self.mvview is not None:
- self.mvview.deg_type = handle
- def set_units(self, _unused, handle):
- "Change the display units."
- # print("set_units:", handle)
- self.conversions = unit_adjustments(handle)
- # I/O monitoring and gtk housekeeping
- def watch(self, daem, dev):
- "Set up monitoring of a daemon instance."
- self.daemon = daem
- self.device = dev
- GLib.io_add_watch(daemon.sock, GLib.PRIORITY_DEFAULT,
- GLib.IO_IN, self.handle_response)
- GLib.io_add_watch(daemon.sock, GLib.PRIORITY_DEFAULT,
- GLib.IO_ERR, self.handle_hangup)
- GLib.io_add_watch(daemon.sock, GLib.PRIORITY_DEFAULT,
- GLib.IO_HUP, self.handle_hangup)
- def handle_response(self, source, condition):
- "Handle ordinary I/O ready condition from the daemon."
- if self.daemon.read() == -1:
- self.handle_hangup(source, condition)
- if self.daemon.valid & gps.PACKET_SET:
- if ((self.device and
- "device" in self.daemon.data and
- self.device != self.daemon.data["device"])):
- return True
- self.rawdisplay.set_text(self.daemon.response.strip())
- if self.daemon.data["class"] == "VERSION":
- self.update_version(self.daemon.version)
- elif self.daemon.data["class"] == "SKY":
- self.update_skyview(self.daemon.data)
- elif self.daemon.data["class"] == "TPV":
- self.update_gpsdata(self.daemon.data)
- elif self.daemon.data["class"] == "GST":
- self.noiseview.update(self.daemon.data)
- if not self.noise_latch:
- self.noise_latch = True
- self.menumap['Noise'].set_active(True)
- self.noisebox.show()
- elif self.daemon.data["class"] == "AIS":
- self.aisview.update(self.daemon.data)
- if not self.ais_latch:
- self.ais_latch = True
- self.menumap['AIS'].set_active(True)
- self.aisbox.show()
- return True
- def handle_hangup(self, _source, _condition):
- "Handle hangup condition from the daemon."
- win = Gtk.MessageDialog(parent=self.window,
- message_type=Gtk.MessageType.ERROR,
- destroy_with_parent=True,
- buttons=Gtk.ButtonsType.CANCEL)
- win.connect("destroy", lambda _unused: Gtk.main_quit())
- win.set_markup("gpsd has stopped sending data.")
- win.run()
- Gtk.main_quit()
- return True
- def main(self):
- "The main routine"
- Gtk.main()
- if __name__ == "__main__":
- try:
- if 'XGPSOPTS' in os.environ:
- # grab the XGPSOPTS environment variable for options
- options = os.environ['XGPSOPTS'].split(' ') + sys.argv[1:]
- else:
- options = sys.argv[1:]
- (options, arguments) = getopt.getopt(options, "D:hl:u:r:V?",
- ['verbose'])
- debug = 0
- degreefmt = 'd'
- unit_system = None
- rotate = None
- for (opt, val) in options:
- if opt in '-D':
- debug = int(val)
- elif opt == '-l':
- degreeformat = val
- elif opt == '-u':
- unit_system = val
- elif opt == '-r':
- try:
- rotate = float(val)
- except ValueError:
- rotate = None
- elif opt in ('-?', '-h', '--help'):
- print(__doc__)
- sys.exit(0)
- elif opt == '-V':
- sys.stderr.write("xgps: Version %s\n" % gps_version)
- sys.exit(0)
- degreefmt = {'d': gps.clienthelpers.deg_dd,
- 'm': gps.clienthelpers.deg_ddmm,
- 's': gps.clienthelpers.deg_ddmmss}[degreefmt]
- (host, port, device) = ("localhost", gps.GPSD_PORT, None)
- if arguments:
- args = arguments[0].split(":")
- if len(args) >= 1 and args[0]:
- host = args[0]
- if len(args) >= 2 and args[1]:
- port = args[1]
- if len(args) >= 3:
- device = args[2]
- target = ":".join(arguments[0:])
- else:
- target = ""
- if 'DISPLAY' not in os.environ:
- sys.stderr.write("xgps: ERROR: DISPLAY not set\n")
- sys.exit(1)
- base = Base(deg_type=degreefmt, rotation=rotate, title=target)
- # If we're debuuging, stop here so we can set breakpoints
- pdb_module = sys.modules.get('pdb')
- if pdb_module:
- pdb_module.set_trace()
- base.set_units(None, unit_system)
- try:
- sys.stderr.write("xgps: host %s port %s\n" % (host, port))
- daemon = gps.gps(host=host,
- port=port,
- mode=(gps.WATCH_ENABLE | gps.WATCH_JSON |
- gps.WATCH_SCALED),
- verbose=debug)
- base.watch(daemon, device)
- base.main()
- except socket.error:
- w = Gtk.MessageDialog(parent=base.window,
- message_type=Gtk.MessageType.ERROR,
- destroy_with_parent=True,
- buttons=Gtk.ButtonsType.CANCEL)
- w.set_markup("gpsd is not running on host %s port %s" %
- (host, port))
- w.run()
- w.destroy()
- except KeyboardInterrupt:
- pass
- # vim: set expandtab shiftwidth=4
|