12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643 |
- #!@PYSHEBANG@
- # -*- coding: UTF-8
- # @GENERATED@
- """xgps -- test client for gpsd.
- 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!
- # Codacy D203 and D211 conflict, I choose D203
- # Codacy D203 and D211 conflict, I choose D203
- from __future__ import absolute_import, print_function, division
- import argparse
- import cairo
- 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, default max channels to display
- 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 ' '
- 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):
- """Fit cicrle to grid.
- 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:
- # square
- 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:
- # diamond
- self.cr.move_to(x, y + radius + 1) # top
- self.cr.line_to(x + radius - 1, y) # right
- self.cr.line_to(x, y - radius - 1) # bottom
- self.cr.line_to(x - radius + 1, y) # left
- 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:
- ytop = y + radius
- ybot = y - radius
- if 2 == flip:
- # right
- 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)
- else:
- # left
- 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)
- elif constellation == 'QZ':
- self.draw_triangle(x, y, SkyView.SAT_RADIUS, sat.used, 2)
- else:
- # IRNSS, IMES, unknown or other
- self.draw_triangle(x, y, SkyView.SAT_RADIUS, sat.used, 3)
- 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 2010 by The GPSD Project")
- about.set_website("@WEBSITE@")
- about.set_website_label("@WEBSITE@")
- 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 = gps.clienthelpers.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(options.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, options.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):
- """Rest 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 >= options.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, options.maxchandisp):
- for j in range(0, 7):
- self.set_satlist_field(i, j, "")
- else:
- # clear all of the list
- for i in range(0, options.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 = gps.clienthelpers.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__":
- usage = '%(prog)s [OPTIONS] [host[:port[:device]]]'
- epilog = ('Options can be placed in the XGPSOPTS environment variable.\n'
- 'Default units can be placed in the GPSD_UNITS environment'
- ' variabla.\n\ne'
- 'BSD terms apply: see the file COPYING in the distribution root'
- ' for details.')
- # get default units from the environment
- # GPSD_UNITS, LC_MEASUREMENT and LANG
- default_units = gps.clienthelpers.unit_adjustments()
- parser = argparse.ArgumentParser(usage=usage, epilog=epilog)
- parser.add_argument(
- '-?',
- action="help",
- help='show this help message and exit'
- )
- parser.add_argument(
- '-D',
- '--debug',
- dest='debug',
- default=0,
- type=int,
- help='Set level of debug. Must be integer. [Default %(default)s)]',
- )
- parser.add_argument(
- '--device',
- dest='device',
- default='',
- help='The device to connect. [Default %(default)s)]',
- )
- parser.add_argument(
- '--host',
- dest='host',
- default='localhost',
- help='The host to connect. [Default %(default)s)]',
- )
- parser.add_argument(
- '-l',
- choices=['d', 'm', 's'],
- dest='degreefmt',
- default='d',
- help=("Select lat/lon format. d = DD.dddddd, m = DD MM.mmmm'"
- "s = DD MM' SS.sss\" [Default %(default)s)]"),
- )
- parser.add_argument(
- '--port',
- dest='port',
- default=gps.GPSD_PORT,
- help='The port to connect. [Default %(default)s)]',
- )
- parser.add_argument(
- '-r',
- '--rotate',
- dest='rotate',
- default=0,
- type=float,
- help='Rotation of skyview ("up" direction) in degrees. '
- ' [Default %(default)s)]',
- )
- parser.add_argument(
- '-s',
- '--sats',
- dest='maxchandisp',
- default=MAXCHANDISP,
- type=int,
- help=('Set number of sats to display in sat window. '
- '[Default %(default)s)]'),
- )
- parser.add_argument(
- '-u, --units',
- choices=['i', 'imperial', 'm', 'metric', 'n', 'nautical'],
- dest='units',
- default=default_units.name,
- help='The unit of speed. [Default %(default)s)]',
- )
- parser.add_argument(
- '-V', '--version',
- action='version',
- version="%(prog)s: Version " + gps_version + "\n",
- help='Output version to stderr, then exit',
- )
- parser.add_argument(
- 'target',
- nargs='?',
- help='[host[:port[:device]]]',
- )
- 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 = parser.parse_args(options)
- options.degreefmt = {
- 'd': gps.clienthelpers.deg_dd,
- 'm': gps.clienthelpers.deg_ddmm,
- 's': gps.clienthelpers.deg_ddmmss}[options.degreefmt]
- # the options host, port, device are set by the defaults
- if options.target:
- # override with target
- arg = options.target.split(':')
- len_arg = len(arg)
- if len_arg == 1:
- (options.host,) = arg
- elif len_arg == 2:
- (options.host, options.port) = arg
- elif len_arg == 3:
- (options.host, options.port, options.device) = arg
- else:
- parser.print_help()
- sys.exit(0)
- if not options.port:
- options.port = gps.GPSD_PORT
- ltarget = [options.host, options.port, options.device]
- target = ':'.join(ltarget)
- if 'DISPLAY' not in os.environ or not os.environ['DISPLAY']:
- sys.stderr.write("xgps: ERROR: DISPLAY not set\n")
- sys.exit(1)
- base = Base(deg_type=options.degreefmt, rotation=options.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, options.units)
- try:
- sys.stderr.write("xgps: host %s port %s\n" %
- (options.host, options.port))
- daemon = gps.gps(host=options.host,
- port=options.port,
- mode=(gps.WATCH_ENABLE | gps.WATCH_JSON |
- gps.WATCH_SCALED),
- verbose=options.debug)
- base.watch(daemon, options.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" %
- (options.host, options.port))
- w.run()
- w.destroy()
- except KeyboardInterrupt:
- # ^C, die
- pass
- # vim: set expandtab shiftwidth=4
|