You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1581 lines
57KB

  1. #!/usr/bin/env python
  2. # -*- coding: UTF-8
  3. '''
  4. xgps -- test client for gpsd
  5. usage: xgps [-?] [-D level] [-h] [-l degmfmt] [-r rotation] [-u units] [-V]
  6. [server[:port[:device]]]
  7. -? Print help and exit.
  8. -D lvl Set debug level to lvl
  9. -h Print help and exit.
  10. -l {d|m|s} Select lat/lon format
  11. d = DD.dddddd (default)
  12. m = DD MM.mmmm'
  13. s = DD MM' SS.sss"
  14. -r rotation Set rotation
  15. -u units Set units to Imperial, Nautical or Metric
  16. -V Print version and exit.
  17. Options can be placed in the XGPSOPTS environment variable.
  18. XGPSOPTS is processed before the CLI options.
  19. '''
  20. # ENVIRONMENT:
  21. # Options in the XGPSOPTS environment variable will be parsed before
  22. # the CLI options. A handy place to put your '-l m -u m '
  23. #
  24. # This file is Copyright (c) 2010 by the GPSD project
  25. # SPDX-License-Identifier: BSD-2-clause
  26. #
  27. # This code runs compatibly under Python 2 and 3.x for x >= 2.
  28. # Preserve this property!
  29. from __future__ import absolute_import, print_function, division
  30. import cairo
  31. import getopt
  32. import math
  33. import os
  34. import socket
  35. import sys
  36. import time
  37. # Gtk3 imports. Gtk3 requires the require_version(), which then causes
  38. # pylint to complain about the subsequent "non-top" imports.
  39. # On gentoo these are from the dev-python/pygobject package.
  40. # "Python bindings for GObject Introspection"
  41. # It looks like PyGTK, but it is not. PyGTK is unmaintained.
  42. try:
  43. import gi
  44. gi.require_version('Gtk', '3.0')
  45. except ImportError as err:
  46. # ModuleNotFoundError neds Python 3.6
  47. sys.stderr.write("xgps: ERROR %s\n" % err)
  48. exit(1)
  49. except ValueError as err:
  50. # Gtk2 may be installed, has no require_version()
  51. sys.stderr.write("xgps: ERROR %s\n" % err)
  52. exit(1)
  53. from gi.repository import Gtk # pylint: disable=wrong-import-position
  54. from gi.repository import Gdk # pylint: disable=wrong-import-position
  55. from gi.repository import GLib # pylint: disable=wrong-import-position
  56. # pylint wants local modules last
  57. try:
  58. import gps
  59. import gps.clienthelpers
  60. except ImportError as e:
  61. sys.stderr.write(
  62. "xgps: can't load Python gps libraries -- check PYTHONPATH.\n")
  63. sys.stderr.write("%s\n" % e)
  64. sys.exit(1)
  65. gps_version = '3.20'
  66. if gps.__version__ != gps_version:
  67. sys.stderr.write("xgps: ERROR: need gps module version %s, got %s\n" %
  68. (gps_version, gps.__version__))
  69. sys.exit(1)
  70. # MAXCHANNELS, from gps.h, currently 120
  71. MAXCHANNELS = 120
  72. # MAXCHANDISP, max channels to display
  73. # Use our own MAXCHANDISP value, due to the tradeoff between max sats and
  74. # the window size. Ideally, this should be dynamic.
  75. MAXCHANDISP = 28
  76. # how to sort the Satellite List
  77. # some of ("PRN","el","az","ss","used") with optional '-' to reverse sort
  78. # by default, used at the top, then sort PRN
  79. SKY_VIEW_SORT_FIELDS = ('-used', 'PRN')
  80. # Each GNSS constellation reuses the same PRNs. To differentiate they are
  81. # all mushed into the PRN. Different GPS mush differently. gpsd should
  82. # have untangled and put in gnssid:svid
  83. def gnssid_str(sat):
  84. "convert gnssid:svid to short and long strings"
  85. # gnssid:svid appeared in gpsd 3.18
  86. # allow for old servers
  87. if 'gnssid' not in sat or 'svid' not in sat:
  88. return ' '
  89. if 0 >= sat.svid:
  90. return [' ', '']
  91. if 0 == sat.gnssid:
  92. return ['GP', 'GPS']
  93. if 1 == sat.gnssid:
  94. return ['SB', 'SBAS']
  95. if 2 == sat.gnssid:
  96. return ['GA', 'Galileo']
  97. if 3 == sat.gnssid:
  98. return ['BD', 'BeiDou']
  99. if 4 == sat.gnssid:
  100. return ['IM', 'IMES']
  101. if 5 == sat.gnssid:
  102. return ['QZ', 'QZSS']
  103. if 6 == sat.gnssid:
  104. return ['GL', 'GLONASS']
  105. return ' '
  106. class unit_adjustments(object):
  107. "Encapsulate adjustments for unit systems."
  108. def __init__(self, units=None):
  109. "Initialize class unit_adjustments"
  110. self.altfactor = gps.METERS_TO_FEET
  111. self.altunits = "ft"
  112. self.speedfactor = gps.MPS_TO_MPH
  113. self.speedunits = "mph"
  114. if units is None:
  115. units = gps.clienthelpers.gpsd_units()
  116. if units in (gps.clienthelpers.unspecified, gps.clienthelpers.imperial,
  117. "imperial", "i"):
  118. pass
  119. elif units in (gps.clienthelpers.nautical, "nautical", "n"):
  120. self.altfactor = gps.METERS_TO_FEET
  121. self.altunits = "ft"
  122. self.speedfactor = gps.MPS_TO_KNOTS
  123. self.speedunits = "knots"
  124. elif units in (gps.clienthelpers.metric, "metric", "m"):
  125. self.altfactor = 1.0
  126. self.altunits = "m"
  127. self.speedfactor = gps.MPS_TO_KPH
  128. self.speedunits = "kph"
  129. else:
  130. raise ValueError # Should never happen
  131. def fit_to_grid(x, y, line_width):
  132. "Adjust coordinates to produce sharp lines."
  133. if line_width % 1.0 != 0:
  134. # Can't have sharp lines for non-integral line widths.
  135. return float(x), float(y) # Be consistent about returning floats
  136. if line_width % 2 == 0:
  137. # Round to a pixel corner.
  138. return round(x), round(y)
  139. # Round to a pixel center.
  140. return int(x) + 0.5, int(y) + 0.5
  141. def fit_circle_to_grid(x, y, radius, line_width):
  142. """Adjust circle coordinates and radius to produce sharp horizontal
  143. and vertical tangents."""
  144. r = radius
  145. x1, y1 = fit_to_grid(x - r, y - r, line_width)
  146. x2, y2 = fit_to_grid(x + r, y + r, line_width)
  147. x, y = (x1 + x2) / 2, (y1 + y2) / 2
  148. r = (x2 - x1 + y2 - y1) / 4
  149. return x, y, r
  150. class SkyView(Gtk.DrawingArea):
  151. "Satellite skyview, encapsulates pygtk's draw-on-expose behavior."
  152. # See <http://faq.pygtk.org/index.py?req=show&file=faq18.008.htp>
  153. HORIZON_PAD = 50 # How much whitespace to leave around horizon
  154. SAT_RADIUS = 5 # Diameter of satellite circle
  155. def __init__(self, rotation=None):
  156. "Initialize class SkyView"
  157. Gtk.DrawingArea.__init__(self)
  158. # GObject.GObject.__init__(self)
  159. self.set_size_request(400, 400)
  160. self.cr = None # New cairo context for each expose event
  161. self.step_of_grid = 45 # default step of polar grid
  162. self.connect('size-allocate', self.on_size_allocate)
  163. self.connect('draw', self.on_draw)
  164. self.satellites = []
  165. self.sat_xy = []
  166. self.center_x = self.center_y = self.radius = None
  167. self.rotate = rotation
  168. if self.rotate is None:
  169. self.rotate = 0
  170. self.connect('motion_notify_event', self.popup)
  171. self.popover = None
  172. self.pop_xy = (None, None)
  173. def popdown(self):
  174. "See if need to popdown the sat details"
  175. if self.popover:
  176. self.popover.popdown()
  177. self.popover = None
  178. self.pop_xy = (None, None)
  179. def popup(self, skyview, event):
  180. "See if need to popup the sat details"
  181. for (x, y, sat) in self.sat_xy:
  182. if ((SkyView.SAT_RADIUS >= abs(x - event.x) and
  183. SkyView.SAT_RADIUS >= abs(y - event.y))):
  184. # got a sat match under the mouse
  185. # print((x, y))
  186. if ((self.pop_xy[0] and self.pop_xy[1] and
  187. self.pop_xy == (int(x), int(y)))):
  188. # popup already up here, ignore event
  189. # print("(%d, %d)" % (x, y))
  190. return
  191. if self.popover:
  192. # remove any old, no longer current popup
  193. # this never happens?
  194. self.popdown()
  195. # mouse is over a satellite, do popup
  196. self.pop_xy = (int(x), int(y))
  197. self.popover = Gtk.Popover()
  198. if "gnssid" in sat and "svid" in sat:
  199. # gnssid:svid in gpsd 3.18 and up
  200. constellation = gnssid_str(sat)[1]
  201. gnss_str = "%-8s %4d\n" % (constellation, sat.svid)
  202. else:
  203. gnss_str = ''
  204. if 'health' not in sat:
  205. health = "Unk"
  206. elif 1 == sat.health:
  207. health = "OK"
  208. elif 2 == sat.health:
  209. health = "Bad"
  210. else:
  211. health = "Unk"
  212. label = Gtk.Label()
  213. s = ("<span font_desc='monospace 10'>PRN %10d\n"
  214. "%s"
  215. "Elevation %4.1f\n"
  216. "Azimuth %5.1f\n"
  217. "SNR %4.1f\n"
  218. "Used %9s\n"
  219. "Health %7s</span>" %
  220. (sat.PRN, gnss_str,
  221. sat.el, sat.az, sat.ss, 'Yes' if sat.used else 'No',
  222. health))
  223. label.set_markup(s)
  224. rectangle = Gdk.Rectangle()
  225. rectangle.x = x - 25
  226. rectangle.y = y - 25
  227. rectangle.width = 50
  228. rectangle.height = 50
  229. self.popover.set_modal(False)
  230. self.popover.set_relative_to(self)
  231. self.popover.set_position(Gtk.PositionType.TOP)
  232. self.popover.set_pointing_to(rectangle)
  233. self.popover.add(label)
  234. self.popover.popup()
  235. self.popover.show_all()
  236. # remove popup after 15 seconds
  237. GLib.timeout_add(15000, self.popdown)
  238. return
  239. if self.popover:
  240. # remove any old, no longer current popup
  241. # this never happens?
  242. self.popdown()
  243. def on_size_allocate(self, _unused, allocation):
  244. "Adjust SkyView on size change"
  245. width = allocation.width
  246. height = allocation.height
  247. x = width // 2
  248. y = height // 2
  249. r = (min(width, height) - SkyView.HORIZON_PAD) // 2
  250. x, y, r = fit_circle_to_grid(x, y, r, 1)
  251. self.center_x = x
  252. self.center_y = y
  253. self.radius = r
  254. def set_color(self, r, g, b):
  255. """Set foreground color for drawing. rgb: 0 to 255"""
  256. # Gdk.color_parse() deprecated in GDK 3.14
  257. # gdkcolor = Gdk.color_parse(spec)
  258. r = r / 255.0
  259. g = g / 255.0
  260. b = b / 255.0
  261. self.cr.set_source_rgb(r, g, b)
  262. def draw_circle(self, x, y, radius, filled=False):
  263. "Draw a circle centered on the specified midpoint."
  264. lw = self.cr.get_line_width()
  265. r = int(2 * radius + 0.5) // 2
  266. x, y, r = fit_circle_to_grid(x, y, radius, lw)
  267. self.cr.arc(x, y, r, 0, math.pi * 2.0)
  268. self.cr.close_path()
  269. if filled:
  270. self.cr.fill()
  271. else:
  272. self.cr.stroke()
  273. def draw_line(self, x1, y1, x2, y2):
  274. "Draw a line between specified points."
  275. lw = self.cr.get_line_width()
  276. x1, y1 = fit_to_grid(x1, y1, lw)
  277. x2, y2 = fit_to_grid(x2, y2, lw)
  278. self.cr.move_to(x1, y1)
  279. self.cr.line_to(x2, y2)
  280. self.cr.stroke()
  281. def draw_square(self, x, y, radius, filled, flip):
  282. "Draw a square centered on the specified midpoint."
  283. lw = self.cr.get_line_width()
  284. if 0 == flip:
  285. x1, y1 = fit_to_grid(x - radius, y - radius, lw)
  286. x2, y2 = fit_to_grid(x + radius, y + radius, lw)
  287. self.cr.rectangle(x1, y1, x2 - x1, y2 - y1)
  288. else:
  289. self.cr.move_to(x, y + radius)
  290. self.cr.line_to(x + radius, y)
  291. self.cr.line_to(x, y - radius)
  292. self.cr.line_to(x - radius, y)
  293. self.cr.close_path()
  294. if filled:
  295. self.cr.fill()
  296. else:
  297. self.cr.stroke()
  298. def draw_string(self, x, y, text, centered=True):
  299. "Draw a text on the skyview."
  300. self.cr.select_font_face("Sans", cairo.FONT_SLANT_NORMAL,
  301. cairo.FONT_WEIGHT_BOLD)
  302. self.cr.set_font_size(10)
  303. if centered:
  304. extents = self.cr.text_extents(text)
  305. # width / 2 + x_bearing
  306. x -= extents[2] / 2 + extents[0]
  307. # height / 2 + y_bearing
  308. y -= extents[3] / 2 + extents[1]
  309. self.cr.move_to(x, y)
  310. self.cr.show_text(text)
  311. self.cr.new_path()
  312. def draw_triangle(self, x, y, radius, filled, flip):
  313. "Draw a triangle centered on the specified midpoint."
  314. lw = self.cr.get_line_width()
  315. if flip in (0, 1):
  316. if 0 == flip:
  317. # down
  318. ytop = y + radius
  319. ybot = y - radius
  320. elif 1 == flip:
  321. # up
  322. ytop = y - radius
  323. ybot = y + radius
  324. x1, y1 = fit_to_grid(x, ytop, lw)
  325. x2, y2 = fit_to_grid(x + radius, ybot, lw)
  326. x3, y3 = fit_to_grid(x - radius, ybot, lw)
  327. else:
  328. # right
  329. ytop = y + radius
  330. ybot = y - radius
  331. x1, y1 = fit_to_grid(x - radius, ytop, lw)
  332. x2, y2 = fit_to_grid(x - radius, ybot, lw)
  333. x3, y3 = fit_to_grid(x + radius, y, lw)
  334. self.cr.move_to(x1, y1)
  335. self.cr.line_to(x2, y2)
  336. self.cr.line_to(x3, y3)
  337. self.cr.close_path()
  338. if filled:
  339. self.cr.fill()
  340. else:
  341. self.cr.stroke()
  342. def pol2cart(self, az, el):
  343. "Polar to Cartesian coordinates within the horizon circle."
  344. az = (az - self.rotate) % 360.0
  345. az *= (math.pi / 180) # Degrees to radians
  346. # Exact spherical projection would be like this:
  347. # el = sin((90.0 - el) * DEG_2_RAD);
  348. el = ((90.0 - el) / 90.0)
  349. xout = self.center_x + math.sin(az) * el * self.radius
  350. yout = self.center_y - math.cos(az) * el * self.radius
  351. return (xout, yout)
  352. def on_draw(self, widget, _unused):
  353. "Draw the skyview"
  354. window = widget.get_window()
  355. region = window.get_clip_region()
  356. context = window.begin_draw_frame(region)
  357. self.cr = context.get_cairo_context()
  358. self.cr.set_line_width(1)
  359. self.cr.set_source_rgb(0, 0, 0)
  360. self.cr.paint()
  361. self.cr.set_source_rgb(1, 1, 1)
  362. # The zenith marker
  363. self.draw_circle(self.center_x, self.center_y, 6, filled=False)
  364. # The horizon circle
  365. if self.step_of_grid == 45:
  366. # The circle corresponding to 45 degrees elevation.
  367. # There are two ways we could plot this. Projecting the sphere
  368. # on the display plane, the circle would have a diameter of
  369. # sin(45) ~ 0.7. But the naive linear mapping, just splitting
  370. # the horizon diameter in half, seems to work better visually.
  371. self.draw_circle(self.center_x, self.center_y, self.radius / 2,
  372. filled=False)
  373. elif self.step_of_grid == 30:
  374. self.draw_circle(self.center_x, self.center_y, self.radius * 2 / 3,
  375. filled=False)
  376. self.draw_circle(self.center_x, self.center_y, self.radius / 3,
  377. filled=False)
  378. self.draw_circle(self.center_x, self.center_y, self.radius,
  379. filled=False)
  380. (x1, y1) = self.pol2cart(0, 0)
  381. (x2, y2) = self.pol2cart(180, 0)
  382. self.draw_line(x1, y1, x2, y2)
  383. (x1, y1) = self.pol2cart(90, 0)
  384. (x2, y2) = self.pol2cart(270, 0)
  385. self.draw_line(x1, y1, x2, y2)
  386. # The compass-point letters
  387. (x, y) = self.pol2cart(0, -5)
  388. self.draw_string(x, y, "N")
  389. (x, y) = self.pol2cart(90, -5)
  390. self.draw_string(x, y, "E")
  391. (x, y) = self.pol2cart(180, -5)
  392. self.draw_string(x, y, "S")
  393. (x, y) = self.pol2cart(270, -5)
  394. self.draw_string(x, y, "W")
  395. # place an invisible space above to allow sats below horizon
  396. (x, y) = self.pol2cart(0, -10)
  397. self.draw_string(x, y, "")
  398. # The satellites
  399. self.cr.set_line_width(2)
  400. self.sat_xy = []
  401. for sat in self.satellites:
  402. if not 1 <= sat.PRN <= 437:
  403. # Bad PRN, skip. NMEA uses up to 437
  404. continue
  405. if not 0 <= sat.az <= 359:
  406. # Bad azimuth, skip.
  407. continue
  408. if not -10 <= sat.el <= 90:
  409. # Bad elevation, skip. Allow just below horizon
  410. continue
  411. # The Navika-100 reports el/az of 0/0 for SBAS satellites,
  412. # causing them to appear inappropriately at the "north point".
  413. # Although this value isn't technically illegal (and hence not
  414. # filtered above), excluding this one specific case has a very
  415. # low probability of excluding legitimate cases, while avoiding
  416. # the improper display in this case.
  417. # Note that this only excludes them from the map, not the list.
  418. if sat.az == 0 and sat.el == 0:
  419. continue
  420. (x, y) = self.pol2cart(sat.az, sat.el)
  421. # colorize by signal to noise ratio
  422. # RINEX 3 uses 9 steps: 1 to 9. Corresponding to
  423. # <12, 12-17, 18-23, 24-29, 30-35, 36-41, 42-47, 48-53, >= 54
  424. if sat.ss < 12:
  425. self.set_color(190, 190, 190) # gray
  426. elif sat.ss < 30:
  427. self.set_color(255, 0, 0) # red
  428. elif sat.ss < 36:
  429. # RINEX 3 says 30 is "threshold for good tracking"
  430. self.set_color(255, 255, 0) # yellow
  431. elif sat.ss < 42:
  432. self.set_color(0, 205, 0) # green3
  433. else:
  434. self.set_color(0, 255, 180) # green and some blue
  435. # shape by constellation
  436. constellation = gnssid_str(sat)[0]
  437. if constellation in ('GP', ' '):
  438. self.draw_circle(x, y, SkyView.SAT_RADIUS, sat.used)
  439. elif constellation == 'SB':
  440. self.draw_square(x, y, SkyView.SAT_RADIUS, sat.used, 0)
  441. elif constellation == 'GA':
  442. self.draw_triangle(x, y, SkyView.SAT_RADIUS, sat.used, 0)
  443. elif constellation == 'BD':
  444. self.draw_triangle(x, y, SkyView.SAT_RADIUS, sat.used, 1)
  445. elif constellation == 'GL':
  446. self.draw_square(x, y, SkyView.SAT_RADIUS, sat.used, 1)
  447. else:
  448. # QZSS, IMES, unknown or other
  449. self.draw_triangle(x, y, SkyView.SAT_RADIUS, sat.used, 2)
  450. self.sat_xy.append((x, y, sat))
  451. self.cr.set_source_rgb(1, 1, 1)
  452. self.draw_string(x + SkyView.SAT_RADIUS,
  453. y + (SkyView.SAT_RADIUS * 2), str(sat.PRN),
  454. centered=False)
  455. self.cr = None
  456. window.end_draw_frame(context)
  457. def redraw(self, satellites):
  458. "Redraw the skyview."
  459. self.satellites = satellites
  460. self.queue_draw()
  461. class NoiseView(object):
  462. "Encapsulate view object for watching noise statistics."
  463. COLUMNS = 2
  464. ROWS = 4
  465. noisefields = (
  466. # First column
  467. ("Time", "time"),
  468. ("Latitude", "lat"),
  469. ("Longitude", "lon"),
  470. ("Altitude", "alt"),
  471. # Second column
  472. ("RMS", "rms"),
  473. ("Major", "major"),
  474. ("Minor", "minor"),
  475. ("Orient", "orient"),
  476. )
  477. def __init__(self):
  478. "Initialize class NoiseView"
  479. self.widget = Gtk.Grid()
  480. self.noisewidgets = []
  481. for i in range(len(NoiseView.noisefields)):
  482. colbase = (i // NoiseView.ROWS) * 2
  483. label = Gtk.Label()
  484. label.set_markup("<span font_desc='sans 10'> %s:</span>" %
  485. NoiseView.noisefields[i][0])
  486. # force right alignment
  487. label.set_halign(Gtk.Align.END)
  488. self.widget.attach(label, colbase, i % NoiseView.ROWS, 1, 1)
  489. entry = Gtk.Label()
  490. # span gets lost later
  491. entry.set_markup("<span font_desc='monospace 10'> n/a </span>")
  492. self.widget.attach_next_to(entry, label,
  493. Gtk.PositionType.RIGHT, 1, 1)
  494. self.noisewidgets.append((NoiseView.noisefields[i][1], entry))
  495. def update(self, noise):
  496. "Update the GPGST data fields."
  497. markup = "<span font_desc='monospace 10'>%s </span>"
  498. for (attrname, widget) in self.noisewidgets:
  499. if hasattr(noise, attrname):
  500. s = str(getattr(noise, attrname))
  501. else:
  502. s = " n/a "
  503. widget.set_markup(markup % s)
  504. class AISView(object):
  505. "Encapsulate store and view objects for watching AIS data."
  506. AIS_ENTRIES = 10
  507. DWELLTIME = 360
  508. def __init__(self, deg_type):
  509. "Initialize the store and view."
  510. self.deg_type = deg_type
  511. self.name_to_mmsi = {}
  512. self.named = {}
  513. self.store = Gtk.ListStore(str, str, str, str, str, str)
  514. self.widget = Gtk.ScrolledWindow()
  515. self.widget.set_policy(Gtk.PolicyType.AUTOMATIC,
  516. Gtk.PolicyType.AUTOMATIC)
  517. self.view = Gtk.TreeView(model=self.store)
  518. self.widget.set_size_request(-1, 300)
  519. self.widget.add(self.view)
  520. for (i, label) in enumerate(('#', 'Name:', 'Callsign:',
  521. 'Destination:', "Lat/Lon:",
  522. "Information")):
  523. column = Gtk.TreeViewColumn(label)
  524. renderer = Gtk.CellRendererText()
  525. column.pack_start(renderer, expand=True)
  526. column.add_attribute(renderer, 'text', i)
  527. self.view.append_column(column)
  528. def enter(self, ais, name):
  529. "Add a named object (ship or station) to the store."
  530. if ais.mmsi in self.named:
  531. return False
  532. ais.entry_time = time.time()
  533. self.named[ais.mmsi] = ais
  534. self.name_to_mmsi[name] = ais.mmsi
  535. # Garbage-collect old entries
  536. try:
  537. for i in range(len(self.store)):
  538. here = self.store.get_iter(i)
  539. name = self.store.get_value(here, 1)
  540. mmsi = self.name_to_mmsi[name]
  541. if ((self.named[mmsi].entry_time <
  542. time.time() - AISView.DWELLTIME)):
  543. del self.named[mmsi]
  544. if name in self.name_to_mmsi:
  545. del self.name_to_mmsi[name]
  546. self.store.remove(here)
  547. except (ValueError, KeyError): # Invalid TreeIters throw these
  548. pass
  549. return True
  550. def latlon(self, lat, lon):
  551. "Latitude/longitude display in nice format."
  552. if lat < 0:
  553. latsuff = "S"
  554. elif lat > 0:
  555. latsuff = "N"
  556. else:
  557. latsuff = ""
  558. lat = gps.clienthelpers.deg_to_str(self.deg_type, lat)
  559. if lon < 0:
  560. lonsuff = "W"
  561. elif lon > 0:
  562. lonsuff = "E"
  563. else:
  564. lonsuff = ""
  565. lon = gps.clienthelpers.deg_to_str(self.deg_type, lon)
  566. return lat + latsuff + "/" + lon + lonsuff
  567. def update(self, ais):
  568. "Update the AIS data fields."
  569. if ais.type in (1, 2, 3, 18):
  570. if ais.mmsi in self.named:
  571. for i in range(len(self.store)):
  572. here = self.store.get_iter(i)
  573. name = self.store.get_value(here, 1)
  574. if name in self.name_to_mmsi:
  575. mmsi = self.name_to_mmsi[name]
  576. if mmsi == ais.mmsi:
  577. latlon = self.latlon(ais.lat, ais.lon)
  578. self.store.set_value(here, 4, latlon)
  579. elif ais.type == 4:
  580. if self.enter(ais, ais.mmsi):
  581. where = self.latlon(ais.lat, ais.lon)
  582. self.store.prepend(
  583. (ais.type, ais.mmsi, "(shore)", ais.timestamp, where,
  584. ais.epfd_text))
  585. elif ais.type == 5:
  586. if self.enter(ais, ais.shipname):
  587. self.store.prepend(
  588. (ais.type, ais.shipname, ais.callsign, ais.destination,
  589. "", ais.shiptype))
  590. elif ais.type == 12:
  591. sender = ais.mmsi
  592. if sender in self.named:
  593. sender = self.named[sender].shipname
  594. recipient = ais.dest_mmsi
  595. if ((recipient in self.named and
  596. hasattr(self.named[recipient], "shipname"))):
  597. recipient = self.named[recipient].shipname
  598. self.store.prepend(
  599. (ais.type, sender, "", recipient, "", ais.text))
  600. elif ais.type == 14:
  601. sender = ais.mmsi
  602. if sender in self.named:
  603. sender = self.named[sender].shipname
  604. self.store.prepend(
  605. (ais.type, sender, "", "(broadcast)", "", ais.text))
  606. elif ais.type in (19, 24):
  607. if self.enter(ais, ais.shipname):
  608. self.store.prepend(
  609. (ais.type, ais.shipname, "(class B)", "", "",
  610. ais.shiptype_text))
  611. elif ais.type == 21:
  612. if self.enter(ais, ais.name):
  613. where = self.latlon(ais.lat, ais.lon)
  614. self.store.prepend(
  615. (ais.type, ais.name, "(%s navaid)" % ais.epfd_text,
  616. "", where, ais.aid_type_text))
  617. class Base(object):
  618. "Base class for all the output"
  619. ROWS = 9
  620. gpsfields = (
  621. # First column
  622. ("Time", lambda s, r: s.update_time(r)),
  623. ("Latitude", lambda s, r: s.update_latitude(r)),
  624. ("Longitude", lambda s, r: s.update_longitude(r)),
  625. ("Altitude HAE", lambda s, r: s.update_altitude(r, 0)),
  626. ("Altitude MSL", lambda s, r: s.update_altitude(r, 1)),
  627. ("Speed", lambda s, r: s.update_speed(r)),
  628. ("Climb", lambda s, r: s.update_climb(r)),
  629. ("Track True", lambda s, r: s.update_track(r, 0)),
  630. ("Track Mag", lambda s, r: s.update_track(r, 1)),
  631. # Second column
  632. ("Status", lambda s, r: s.update_status(r, 0)),
  633. ("For", lambda s, r: s.update_status(r, 1)),
  634. ("EPX", lambda s, r: s.update_err(r, "epx")),
  635. ("EPY", lambda s, r: s.update_err(r, "epy")),
  636. ("EPV", lambda s, r: s.update_err(r, "epv")),
  637. ("EPS", lambda s, r: s.update_err_speed(r, "eps")),
  638. ("EPC", lambda s, r: s.update_err_speed(r, "epc")),
  639. ("EPD", lambda s, r: s.update_err_degrees(r, "epd")),
  640. ("Mag Dec", lambda s, r: s.update_mag_dec(r)),
  641. # third column
  642. ("ECEF X", lambda s, r: s.update_ecef(r, "ecefx")),
  643. ("ECEF Y", lambda s, r: s.update_ecef(r, "ecefy")),
  644. ("ECEF Z", lambda s, r: s.update_ecef(r, "ecefz")),
  645. ("ECEF pAcc", lambda s, r: s.update_ecef(r, "ecefpAcc")),
  646. ("ECEF VX", lambda s, r: s.update_ecef(r, "ecefvx", "/s")),
  647. ("ECEF VY", lambda s, r: s.update_ecef(r, "ecefvy", "/s")),
  648. ("ECEF VZ", lambda s, r: s.update_ecef(r, "ecefvz", "/s")),
  649. ("ECEF vAcc", lambda s, r: s.update_ecef(r, "ecefvAcc", "/s")),
  650. ('Grid', lambda s, r: s.update_maidenhead(r)),
  651. # fourth column
  652. ("Sats Seen", lambda s, r: s.update_seen(r, 0)),
  653. ("Sats Used", lambda s, r: s.update_seen(r, 1)),
  654. ("XDOP", lambda s, r: s.update_dop(r, "xdop")),
  655. ("YDOP", lambda s, r: s.update_dop(r, "ydop")),
  656. ("HDOP", lambda s, r: s.update_dop(r, "hdop")),
  657. ("VDOP", lambda s, r: s.update_dop(r, "vdop")),
  658. ("PDOP", lambda s, r: s.update_dop(r, "pdop")),
  659. ("TDOP", lambda s, r: s.update_dop(r, "tdop")),
  660. ("GDOP", lambda s, r: s.update_dop(r, "gdop")),
  661. )
  662. def about(self, _unused):
  663. "Show about dialog"
  664. about = Gtk.AboutDialog()
  665. about.set_program_name("xgps")
  666. about.set_version("Versions:\n"
  667. "xgps %s\n"
  668. "PyGObject Version %d.%d.%d" %
  669. (gps_version, gi.version_info[0],
  670. gi.version_info[1], gi.version_info[2]))
  671. about.set_copyright("Copyright 2004-2019 by The GPSD Project")
  672. about.set_website("https://www.gpsd.io")
  673. about.set_website_label("https://www.gpsd.io")
  674. about.set_license("BSD-2-clause")
  675. about.run()
  676. about.destroy()
  677. def __init__(self, deg_type, rotation=None, title=""):
  678. "Initialize class Base"
  679. self.deg_type = deg_type
  680. self.rotate = rotation
  681. self.conversions = unit_adjustments()
  682. self.saved_mode = -1
  683. self.ais_latch = False
  684. self.noise_latch = False
  685. self.last_transition = 0.0
  686. self.daemon = None
  687. self.device = None
  688. self.window = Gtk.Window(type=Gtk.WindowType.TOPLEVEL)
  689. if not self.window.get_display():
  690. raise Exception("Can't open display")
  691. if title:
  692. title = " " + title
  693. self.window.set_title("xgps" + title)
  694. self.window.connect("delete-event", self.delete_event)
  695. self.window.set_resizable(False)
  696. # do the CSS thing
  697. style_provider = Gtk.CssProvider()
  698. css = b"""
  699. frame * {
  700. background-color: #FFF;
  701. color: #000;
  702. }
  703. """
  704. # font-desc: "Comic Sans 12";
  705. style_provider.load_from_data(css)
  706. Gtk.StyleContext.add_provider_for_screen(
  707. Gdk.Screen.get_default(),
  708. style_provider,
  709. Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION
  710. )
  711. vbox = Gtk.VBox(homogeneous=False, spacing=0)
  712. self.window.add(vbox)
  713. self.window.connect("destroy", lambda _unused: Gtk.main_quit())
  714. menubar = Gtk.MenuBar()
  715. agr = Gtk.AccelGroup()
  716. self.window.add_accel_group(agr)
  717. # File
  718. topmenu = Gtk.MenuItem(label="File")
  719. menubar.append(topmenu)
  720. submenu = Gtk.Menu()
  721. topmenu.set_submenu(submenu)
  722. menui = Gtk.MenuItem(label="Connect")
  723. # key, mod = Gtk.accelerator_parse("<Control>Q")
  724. # menui.add_accelerator("activate", agr, key, mod,
  725. # Gtk.AccelFlags.VISIBLE)
  726. # menui.connect("activate", Gtk.main_quit)
  727. submenu.append(menui)
  728. menui = Gtk.MenuItem(label="Disconnect")
  729. # key, mod = Gtk.accelerator_parse("<Control>Q")
  730. # menui.add_accelerator("activate", agr, key, mod,
  731. # Gtk.AccelFlags.VISIBLE)
  732. # menui.connect("activate", Gtk.main_quit)
  733. submenu.append(menui)
  734. menui = Gtk.MenuItem(label="Quit")
  735. key, mod = Gtk.accelerator_parse("<Control>Q")
  736. menui.add_accelerator("activate", agr, key, mod,
  737. Gtk.AccelFlags.VISIBLE)
  738. menui.connect("activate", Gtk.main_quit)
  739. submenu.append(menui)
  740. # View
  741. topmenu = Gtk.MenuItem(label="View")
  742. menubar.append(topmenu)
  743. submenu = Gtk.Menu()
  744. topmenu.set_submenu(submenu)
  745. views = [["Skyview", True, "<Control>S", "Skyview"],
  746. ["Responses", True, "<Control>R", "Responses"],
  747. ["GPS Data", True, "<Control>G", "GPS"],
  748. ["Noise Statistics", False, "<Control>N", "Noise"],
  749. ["AIS Data", False, "<Control>A", "AIS"],
  750. ]
  751. for name, active, acc, handle in views:
  752. menui = Gtk.CheckMenuItem(label=name)
  753. menui.set_active(active)
  754. menui.connect("activate", self.view_toggle, handle)
  755. if acc:
  756. key, mod = Gtk.accelerator_parse(acc)
  757. menui.add_accelerator("activate", agr, key, mod,
  758. Gtk.AccelFlags.VISIBLE)
  759. submenu.append(menui)
  760. # Units
  761. topmenu = Gtk.MenuItem(label="Units")
  762. menubar.append(topmenu)
  763. submenu = Gtk.Menu()
  764. topmenu.set_submenu(submenu)
  765. units = [["Imperial", True, "i", 'i'],
  766. ["Nautical", False, "n", 'n'],
  767. ["Metric", False, "m", 'm'],
  768. ]
  769. menui = None
  770. for name, active, acc, handle in units:
  771. menui = Gtk.RadioMenuItem(group=menui, label=name)
  772. menui.set_active(active)
  773. menui.connect("activate", self.set_units, handle)
  774. if acc:
  775. key, mod = Gtk.accelerator_parse(acc)
  776. menui.add_accelerator("activate", agr, key, mod,
  777. Gtk.AccelFlags.VISIBLE)
  778. submenu.append(menui)
  779. submenu.append(Gtk.SeparatorMenuItem())
  780. units = [["DD.dd", True, "0", gps.clienthelpers.deg_dd],
  781. ["DD MM.mm", False, "1", gps.clienthelpers.deg_ddmm],
  782. ["DD MM SS.ss", False, "2", gps.clienthelpers.deg_ddmmss],
  783. ]
  784. menui = None
  785. for name, active, acc, handle in units:
  786. menui = Gtk.RadioMenuItem(group=menui, label=name)
  787. menui.set_active(active)
  788. menui.connect("activate", self.set_deg, handle)
  789. if acc:
  790. key, mod = Gtk.accelerator_parse(acc)
  791. menui.add_accelerator("activate", agr, key, mod,
  792. Gtk.AccelFlags.VISIBLE)
  793. submenu.append(menui)
  794. # Step of Grid
  795. topmenu = Gtk.MenuItem(label="Step of Grid")
  796. menubar.append(topmenu)
  797. submenu = Gtk.Menu()
  798. topmenu.set_submenu(submenu)
  799. grid = [["30 deg", True, "3", 30],
  800. ["45 deg", False, "4", 45],
  801. ["Off", False, "5", 0],
  802. ]
  803. menui = None
  804. for name, active, acc, handle in grid:
  805. menui = Gtk.RadioMenuItem(group=menui, label=name)
  806. menui.set_active(active)
  807. menui.connect("activate", self.set_step_of_grid, handle)
  808. if acc:
  809. key, mod = Gtk.accelerator_parse(acc)
  810. menui.add_accelerator("activate", agr, key, mod,
  811. Gtk.AccelFlags.VISIBLE)
  812. submenu.append(menui)
  813. submenu.append(Gtk.SeparatorMenuItem())
  814. skymr = [["Mag North Up", True, "6", None],
  815. ["Track Up", False, "7", True],
  816. ["True North Up", False, "8", 0],
  817. ]
  818. menui = None
  819. for name, active, acc, handle in skymr:
  820. menui = Gtk.RadioMenuItem(group=menui, label=name)
  821. menui.set_active(active)
  822. menui.connect("activate", self.set_skyview_n, handle)
  823. if acc:
  824. key, mod = Gtk.accelerator_parse(acc)
  825. menui.add_accelerator("activate", agr, key, mod,
  826. Gtk.AccelFlags.VISIBLE)
  827. submenu.append(menui)
  828. # Help
  829. topmenu = Gtk.MenuItem(label="Help")
  830. menubar.append(topmenu)
  831. submenu = Gtk.Menu()
  832. topmenu.set_submenu(submenu)
  833. menui = Gtk.MenuItem(label="About")
  834. menui.connect("activate", self.about)
  835. submenu.append(menui)
  836. vbox.pack_start(menubar, expand=False, fill=True, padding=0)
  837. self.satbox = Gtk.HBox(homogeneous=False, spacing=0)
  838. vbox.add(self.satbox)
  839. skyframe = Gtk.Frame(label="Satellite List")
  840. self.satbox.add(skyframe)
  841. self.satlist = Gtk.ListStore(str, str, str, str, str, str, str)
  842. view = Gtk.TreeView(model=self.satlist)
  843. satcols = [['', 0],
  844. ['svid', 1],
  845. ['PRN', 1],
  846. ['Elev', 1],
  847. ['Azim', 1],
  848. ['SNR', 1],
  849. ['Used', 0],
  850. ]
  851. for (i, satcol) in enumerate(satcols):
  852. renderer = Gtk.CellRendererText(xalign=satcol[1])
  853. column = Gtk.TreeViewColumn(satcol[0], renderer)
  854. column.add_attribute(renderer, 'text', i)
  855. view.append_column(column)
  856. self.row_iters = []
  857. for i in range(MAXCHANDISP):
  858. self.satlist.append(["", "", "", "", "", "", ""])
  859. self.row_iters.append(self.satlist.get_iter(i))
  860. skyframe.add(view)
  861. viewframe = Gtk.Frame(label="Skyview")
  862. self.satbox.add(viewframe)
  863. self.skyview = SkyView(self.rotate)
  864. try:
  865. # mouseovers fail with remote DISPLAY
  866. self.skyview.set_property('events',
  867. Gdk.EventMask.POINTER_MOTION_MASK)
  868. except NotImplementedError:
  869. # keep going anyway, w/o popups
  870. sys.stderr.write("xgps: WARNING: failed to grab mouse events, "
  871. "popups disabled\n")
  872. viewframe.add(self.skyview)
  873. # Display area for incoming JSON
  874. self.rawdisplay = Gtk.Entry()
  875. self.rawdisplay.set_editable(False)
  876. vbox.add(self.rawdisplay)
  877. # Display area for GPS Data
  878. self.dataframe = Gtk.Frame(label="GPS Data")
  879. # print("GPS Data css:", self.dataframe.get_css_name())
  880. datatable = Gtk.Grid()
  881. self.dataframe.add(datatable)
  882. gpswidgets = []
  883. # min col widths
  884. widths = [0, 25, 0, 20, 0, 23, 0, 8]
  885. for i in range(len(Base.gpsfields)):
  886. colbase = (i // Base.ROWS) * 2
  887. label = Gtk.Label()
  888. label.set_markup("<span font_desc='sans 10'> %s:</span>" %
  889. Base.gpsfields[i][0])
  890. # force right alignment
  891. label.set_halign(Gtk.Align.END)
  892. datatable.attach(label, colbase, i % Base.ROWS, 1, 1)
  893. entry = Gtk.Label()
  894. if 0 < widths[colbase + 1]:
  895. entry.set_width_chars(widths[colbase + 1])
  896. entry.set_selectable(True)
  897. # span gets lost later
  898. entry.set_markup("<span font_desc='monospace 10'> n/a </span>")
  899. datatable.attach_next_to(entry, label,
  900. Gtk.PositionType.RIGHT, 1, 1)
  901. gpswidgets.append(entry)
  902. vbox.add(self.dataframe)
  903. # Add noise box
  904. self.noisebox = Gtk.HBox(homogeneous=False, spacing=0)
  905. vbox.add(self.noisebox)
  906. noiseframe = Gtk.Frame(label="Noise Statistics")
  907. self.noisebox.add(noiseframe)
  908. self.noiseview = NoiseView()
  909. noiseframe.add(self.noiseview.widget)
  910. self.aisbox = Gtk.HBox(homogeneous=False, spacing=0)
  911. vbox.add(self.aisbox)
  912. aisframe = Gtk.Frame(label="AIS Data")
  913. self.aisbox.add(aisframe)
  914. self.aisview = AISView(self.deg_type)
  915. aisframe.add(self.aisview.widget)
  916. self.window.show_all()
  917. # Hide the Noise Statistics window until user selects it.
  918. self.noisebox.hide()
  919. # Hide the AIS window until user selects it.
  920. self.aisbox.hide()
  921. self.view_name_to_widget = {
  922. "Skyview": self.satbox,
  923. "Responses": self.rawdisplay,
  924. "GPS": self.dataframe,
  925. "Noise": self.noisebox,
  926. "AIS": self.aisbox}
  927. # Discard field labels and associate data hooks with their widgets
  928. Base.gpsfields = [(label_hook_widget[0][1], label_hook_widget[1])
  929. for label_hook_widget
  930. in zip(Base.gpsfields, gpswidgets)]
  931. def view_toggle(self, action, name):
  932. "Toggle widget view"
  933. # print("View toggle:", action.get_active(), name)
  934. if hasattr(self, 'view_name_to_widget'):
  935. if action.get_active():
  936. self.view_name_to_widget[name].show()
  937. else:
  938. self.view_name_to_widget[name].hide()
  939. # The effect we're after is to make the top-level window
  940. # resize itself to fit when we show or hide widgets.
  941. # This is undocumented magic to do that.
  942. self.window.resize(1, 1)
  943. def set_satlist_field(self, row, column, value):
  944. "Set a specified field in the satellite list."
  945. try:
  946. self.satlist.set_value(self.row_iters[row], column, str(value))
  947. except IndexError:
  948. sys.stderr.write("xgps: channel = %d, MAXCHANDISP = %d\n"
  949. % (row, MAXCHANDISP))
  950. def delete_event(self, _widget, _event, _data=None):
  951. "Say goodbye nicely"
  952. Gtk.main_quit()
  953. return False
  954. # State updates
  955. def update_time(self, data):
  956. "Update time"
  957. if hasattr(data, "time"):
  958. # str() just in case we get an old-style float.
  959. return str(data.time)
  960. return "n/a"
  961. def update_latitude(self, data):
  962. "Update latitude"
  963. if data.mode >= gps.MODE_2D and hasattr(data, "lat"):
  964. lat = gps.clienthelpers.deg_to_str(self.deg_type, data.lat)
  965. if data.lat < 0:
  966. ns = 'S'
  967. else:
  968. ns = 'N'
  969. return "%14s %s" % (lat, ns)
  970. return "n/a"
  971. def update_longitude(self, data):
  972. "Update longitude"
  973. if data.mode >= gps.MODE_2D and hasattr(data, "lon"):
  974. lon = gps.clienthelpers.deg_to_str(self.deg_type, data.lon)
  975. if data.lon < 0:
  976. ew = 'W'
  977. else:
  978. ew = 'E'
  979. return "%14s %s" % (lon, ew)
  980. return "n/a"
  981. def update_altitude(self, data, item):
  982. "Update altitude"
  983. ret = "n/a"
  984. if data.mode >= gps.MODE_3D:
  985. if 0 == item and hasattr(data, "altHAE"):
  986. ret = ("%10.3f %s" %
  987. ((data.altHAE * self.conversions.altfactor),
  988. self.conversions.altunits))
  989. if 1 == item and hasattr(data, "altMSL"):
  990. ret = ("%10.3f %s" %
  991. ((data.altMSL * self.conversions.altfactor),
  992. self.conversions.altunits))
  993. return ret
  994. def update_speed(self, data):
  995. "Update speed"
  996. if hasattr(data, "speed"):
  997. return "%9.3f %s" % (
  998. data.speed * self.conversions.speedfactor,
  999. self.conversions.speedunits)
  1000. return "n/a"
  1001. def update_climb(self, data):
  1002. "Update climb"
  1003. if hasattr(data, "climb"):
  1004. return "%9.3f %s" % (
  1005. data.climb * self.conversions.speedfactor,
  1006. self.conversions.speedunits)
  1007. return "n/a"
  1008. def update_track(self, data, item):
  1009. "Update track"
  1010. if 0 == item and hasattr(data, "track"):
  1011. return "%14s " % (
  1012. gps.clienthelpers.deg_to_str(self.deg_type, data.track))
  1013. if 1 == item and hasattr(data, "magtrack"):
  1014. return "%14s " % (
  1015. gps.clienthelpers.deg_to_str(self.deg_type, data.magtrack))
  1016. return "n/a"
  1017. def update_seen(self, data, item):
  1018. "Update sats seen"
  1019. # update sats seen/used in the GPS Data window
  1020. if 0 == item and hasattr(data, 'satellites_seen'):
  1021. return getattr(data, 'satellites_seen')
  1022. if 1 == item and hasattr(data, 'satellites_used'):
  1023. return getattr(data, 'satellites_used')
  1024. return "n/a"
  1025. def update_dop(self, data, doptype):
  1026. "update a DOP in the GPS Data window"
  1027. if hasattr(data, doptype):
  1028. return "%5.2f" % getattr(data, doptype)
  1029. return "n/a"
  1030. def update_ecef(self, data, eceftype, speedunit=''):
  1031. "update a ECEF in the GPS Data window"
  1032. if hasattr(data, eceftype):
  1033. value = getattr(data, eceftype)
  1034. return ("% 14.3f %s%s" %
  1035. (value * self.conversions.altfactor,
  1036. self.conversions.altunits, speedunit))
  1037. return "n/a"
  1038. def update_err(self, data, errtype):
  1039. "update a error estimate in the GPS Data window"
  1040. if hasattr(data, errtype):
  1041. return "%8.3f %s" % (
  1042. getattr(data, errtype) * self.conversions.altfactor,
  1043. self.conversions.altunits)
  1044. return "n/a"
  1045. def update_err_speed(self, data, errtype):
  1046. "update speed error estimate in the GPS Data window"
  1047. if hasattr(data, errtype):
  1048. return "%8.3f %s" % (
  1049. getattr(data, errtype) * self.conversions.speedfactor,
  1050. self.conversions.speedunits)
  1051. return "n/a"
  1052. def update_err_degrees(self, data, errtype):
  1053. "update heading error estimate in the GPS Data window"
  1054. if hasattr(data, errtype):
  1055. return ("%s " %
  1056. (gps.clienthelpers.deg_to_str(self.deg_type,
  1057. getattr(data, errtype))))
  1058. return "n/a"
  1059. def update_mag_dec(self, data):
  1060. "update magnetic declination in the GPS Data window"
  1061. if ((data.mode >= gps.MODE_2D and
  1062. hasattr(data, "lat") and
  1063. hasattr(data, "lon"))):
  1064. off = gps.clienthelpers.mag_var(data.lat, data.lon)
  1065. off2 = gps.clienthelpers.deg_to_str(self.deg_type, off)
  1066. return off2
  1067. return "n/a"
  1068. def update_maidenhead(self, data):
  1069. "update maidenhead grid square in the GPS Data window"
  1070. if ((data.mode >= gps.MODE_2D and
  1071. hasattr(data, "lat") and
  1072. hasattr(data, "lon"))):
  1073. return gps.clienthelpers.maidenhead(data.lat, data.lon)
  1074. return "n/a"
  1075. def update_status(self, data, item):
  1076. "Update the status window"
  1077. if 1 == item:
  1078. return "%d secs" % (time.time() - self.last_transition)
  1079. sub_status = ''
  1080. if hasattr(data, 'status'):
  1081. if gps.STATUS_DGPS_FIX == data.status:
  1082. sub_status = " DGPS"
  1083. elif gps.STATUS_RTK_FIX == data.status:
  1084. sub_status = " RTKfix"
  1085. elif gps.STATUS_RTK_FLT == data.status:
  1086. sub_status = " RTKflt"
  1087. elif gps.STATUS_DR == data.status:
  1088. sub_status = " DR"
  1089. elif gps.STATUS_GNSSDR == data.status:
  1090. sub_status = " GNSSDR"
  1091. elif gps.STATUS_TIME == data.status:
  1092. sub_status = " FIXED"
  1093. elif gps.STATUS_SIM == data.status:
  1094. sub_status = " SIM"
  1095. elif gps.STATUS_PPS_FIX == data.status:
  1096. sub_status = " PPS"
  1097. if data.mode == gps.MODE_2D:
  1098. status = "2D%s FIX" % sub_status
  1099. elif data.mode == gps.MODE_3D:
  1100. if hasattr(data, 'status') and gps.STATUS_TIME == data.status:
  1101. status = "FIXED SURVEYED"
  1102. else:
  1103. status = "3D%s FIX" % sub_status
  1104. else:
  1105. status = "NO FIX"
  1106. if data.mode != self.saved_mode:
  1107. self.last_transition = time.time()
  1108. self.saved_mode = data.mode
  1109. return status
  1110. def update_gpsdata(self, tpv):
  1111. "Update the GPS data fields."
  1112. # the first 28 fields are updated using TPV data
  1113. # the next 9 fields are updated using SKY data
  1114. markup = "<span font_desc='monospace 10'>%s </span>"
  1115. for (hook, widget) in Base.gpsfields[:27]:
  1116. if hook: # Remove this guard when we have all hooks
  1117. widget.set_markup(markup % hook(self, tpv))
  1118. if self.skyview:
  1119. if ((self.rotate is None
  1120. and hasattr(tpv, 'lat') and hasattr(tpv, 'lon'))):
  1121. self.skyview.rotate = gps.clienthelpers.mag_var(tpv.lat,
  1122. tpv.lon)
  1123. elif self.rotate is True and 'track' in tpv:
  1124. self.skyview.rotate = tpv.track
  1125. def update_version(self, ver):
  1126. "Update the Version"
  1127. if ver.release != gps_version:
  1128. sys.stderr.write("%s: WARNING gpsd version %s different than "
  1129. "expected %s\n" %
  1130. (sys.argv[0], ver.release, gps_version))
  1131. if ((ver.proto_major != gps.api_major_version or
  1132. ver.proto_minor != gps.api_minor_version)):
  1133. sys.stderr.write("%s: WARNING API version %s.%s different than "
  1134. "expected %s.%s\n" %
  1135. (sys.argv[0], ver.proto_major, ver.proto_minor,
  1136. gps.api_major_version, gps.api_minor_version))
  1137. def _int_to_str(self, value, min_val, max_val):
  1138. "test val in range min to max, or return"
  1139. if min_val <= value <= max_val:
  1140. return '%3d' % value
  1141. return 'n/a'
  1142. def _tenth_to_str(self, value, min_val, max_val):
  1143. "test val in range min to max, or return"
  1144. if min_val <= value <= max_val:
  1145. return '%5.1f' % value
  1146. return 'n/a'
  1147. def update_skyview(self, data):
  1148. "Update the satellite list and skyview."
  1149. data.satellites_seen = 0
  1150. data.satellites_used = 0
  1151. if hasattr(data, 'satellites'):
  1152. satellites = data.satellites
  1153. for fld in reversed(SKY_VIEW_SORT_FIELDS):
  1154. rev = (fld[0] == '-')
  1155. if rev:
  1156. fld = fld[1:]
  1157. satellites = sorted(
  1158. satellites[:],
  1159. key=lambda x: x[fld], reverse=rev)
  1160. # print("Sats: ", satellites)
  1161. for (i, satellite) in enumerate(satellites):
  1162. yesno = 'N'
  1163. data.satellites_seen += 1
  1164. if satellite.used:
  1165. yesno = 'Y'
  1166. data.satellites_used += 1
  1167. if 'health' not in satellite:
  1168. yesno = ' ' + yesno
  1169. elif 2 == satellite.health:
  1170. yesno = ' u' + yesno
  1171. else:
  1172. yesno = ' ' + yesno
  1173. if i >= MAXCHANDISP:
  1174. # more than can be displaced
  1175. continue
  1176. self.set_satlist_field(i, 0, gnssid_str(satellite)[0])
  1177. if 'svid' in satellite:
  1178. # SBAS is in the 100's...
  1179. self.set_satlist_field(i, 1,
  1180. self._int_to_str(satellite.svid,
  1181. 1, 199))
  1182. # NMEA uses PRN up to 437
  1183. self.set_satlist_field(i, 2,
  1184. self._int_to_str(satellite.PRN, 1, 437))
  1185. # allow satellites 10 degree below horizon
  1186. self.set_satlist_field(i, 3,
  1187. self._tenth_to_str(satellite.el,
  1188. -10, 90))
  1189. self.set_satlist_field(i, 4,
  1190. self._tenth_to_str(satellite.az,
  1191. 0, 359))
  1192. self.set_satlist_field(i, 5,
  1193. self._tenth_to_str(satellite.ss,
  1194. 0, 100))
  1195. self.set_satlist_field(i, 6, yesno)
  1196. # clear rest of the list
  1197. for i in range(data.satellites_seen, MAXCHANDISP):
  1198. for j in range(0, 7):
  1199. self.set_satlist_field(i, j, "")
  1200. else:
  1201. # clear all of the list
  1202. for i in range(0, MAXCHANDISP):
  1203. for j in range(0, 7):
  1204. self.set_satlist_field(i, j, "")
  1205. satellites = ()
  1206. # repaint Skyview
  1207. self.skyview.redraw(satellites)
  1208. markup = "<span font_desc='monospace 10'>%s </span>"
  1209. # the first 27 fields are updated using TPV data
  1210. # the next 9 fields are updated using SKY data
  1211. for (hook, widget) in Base.gpsfields[27:36]:
  1212. if hook: # Remove this guard when we have all hooks
  1213. widget.set_markup(markup % hook(self, data))
  1214. # Preferences
  1215. def set_skyview_n(self, system, handle):
  1216. "Change the step of grid."
  1217. self.rotate = handle
  1218. if handle is not None:
  1219. self.skyview.rotate = handle
  1220. def set_step_of_grid(self, system, handle):
  1221. "Change the step of grid."
  1222. # print("set_step_of_grid:", system, handle)
  1223. self.skyview.step_of_grid = handle
  1224. def set_deg(self, _unused, handle):
  1225. "Change the degree format."
  1226. # print("set_deg:", _unused, handle)
  1227. self.deg_type = handle
  1228. if hasattr(self, 'mvview') and self.mvview is not None:
  1229. self.mvview.deg_type = handle
  1230. def set_units(self, _unused, handle):
  1231. "Change the display units."
  1232. # print("set_units:", handle)
  1233. self.conversions = unit_adjustments(handle)
  1234. # I/O monitoring and gtk housekeeping
  1235. def watch(self, daem, dev):
  1236. "Set up monitoring of a daemon instance."
  1237. self.daemon = daem
  1238. self.device = dev
  1239. GLib.io_add_watch(daemon.sock, GLib.PRIORITY_DEFAULT,
  1240. GLib.IO_IN, self.handle_response)
  1241. GLib.io_add_watch(daemon.sock, GLib.PRIORITY_DEFAULT,
  1242. GLib.IO_ERR, self.handle_hangup)
  1243. GLib.io_add_watch(daemon.sock, GLib.PRIORITY_DEFAULT,
  1244. GLib.IO_HUP, self.handle_hangup)
  1245. def handle_response(self, source, condition):
  1246. "Handle ordinary I/O ready condition from the daemon."
  1247. if self.daemon.read() == -1:
  1248. self.handle_hangup(source, condition)
  1249. if self.daemon.valid & gps.PACKET_SET:
  1250. if ((self.device and
  1251. "device" in self.daemon.data and
  1252. self.device != self.daemon.data["device"])):
  1253. return True
  1254. self.rawdisplay.set_text(self.daemon.response.strip())
  1255. if self.daemon.data["class"] == "VERSION":
  1256. self.update_version(self.daemon.version)
  1257. elif self.daemon.data["class"] == "SKY":
  1258. self.update_skyview(self.daemon.data)
  1259. elif self.daemon.data["class"] == "TPV":
  1260. self.update_gpsdata(self.daemon.data)
  1261. elif self.daemon.data["class"] == "GST":
  1262. self.noiseview.update(self.daemon.data)
  1263. if not self.noise_latch:
  1264. self.noise_latch = True
  1265. self.uimanager.get_widget(
  1266. '/MenuBar/View/Noise').set_active(True)
  1267. self.noisebox.show()
  1268. elif self.daemon.data["class"] == "AIS":
  1269. self.aisview.update(self.daemon.data)
  1270. if not self.ais_latch:
  1271. self.ais_latch = True
  1272. self.uimanager.get_widget(
  1273. '/MenuBar/View/AIS').set_active(True)
  1274. self.aisbox.show()
  1275. return True
  1276. def handle_hangup(self, _source, _condition):
  1277. "Handle hangup condition from the daemon."
  1278. win = Gtk.MessageDialog(parent=self.window,
  1279. message_type=Gtk.MessageType.ERROR,
  1280. destroy_with_parent=True,
  1281. buttons=Gtk.ButtonsType.CANCEL)
  1282. win.connect("destroy", lambda _unused: Gtk.main_quit())
  1283. win.set_markup("gpsd has stopped sending data.")
  1284. win.run()
  1285. Gtk.main_quit()
  1286. return True
  1287. def main(self):
  1288. "The main routine"
  1289. Gtk.main()
  1290. if __name__ == "__main__":
  1291. try:
  1292. if 'XGPSOPTS' in os.environ:
  1293. # grab the XGPSOPTS environment variable for options
  1294. options = os.environ['XGPSOPTS'].split(' ') + sys.argv[1:]
  1295. else:
  1296. options = sys.argv[1:]
  1297. (options, arguments) = getopt.getopt(options, "D:hl:u:r:V?",
  1298. ['verbose'])
  1299. debug = 0
  1300. degreefmt = 'd'
  1301. unit_system = None
  1302. rotate = None
  1303. for (opt, val) in options:
  1304. if opt in '-D':
  1305. debug = int(val)
  1306. elif opt == '-l':
  1307. degreeformat = val
  1308. elif opt == '-u':
  1309. unit_system = val
  1310. elif opt == '-r':
  1311. try:
  1312. rotate = float(val)
  1313. except ValueError:
  1314. rotate = None
  1315. elif opt in ('-?', '-h', '--help'):
  1316. print(__doc__)
  1317. sys.exit(0)
  1318. elif opt == '-V':
  1319. sys.stderr.write("xgps: Version %s\n" % gps_version)
  1320. sys.exit(0)
  1321. degreefmt = {'d': gps.clienthelpers.deg_dd,
  1322. 'm': gps.clienthelpers.deg_ddmm,
  1323. 's': gps.clienthelpers.deg_ddmmss}[degreefmt]
  1324. (host, port, device) = ("localhost", gps.GPSD_PORT, None)
  1325. if arguments:
  1326. args = arguments[0].split(":")
  1327. if len(args) >= 1 and args[0]:
  1328. host = args[0]
  1329. if len(args) >= 2 and args[1]:
  1330. port = args[1]
  1331. if len(args) >= 3:
  1332. device = args[2]
  1333. target = ":".join(arguments[0:])
  1334. else:
  1335. target = ""
  1336. if 'DISPLAY' not in os.environ:
  1337. sys.stderr.write("xgps: ERROR: DISPLAY not set\n")
  1338. exit(1)
  1339. base = Base(deg_type=degreefmt, rotation=rotate, title=target)
  1340. base.set_units(None, unit_system)
  1341. try:
  1342. sys.stderr.write("xgps: host %s port %s\n" % (host, port))
  1343. daemon = gps.gps(host=host,
  1344. port=port,
  1345. mode=(gps.WATCH_ENABLE | gps.WATCH_JSON |
  1346. gps.WATCH_SCALED),
  1347. verbose=debug)
  1348. base.watch(daemon, device)
  1349. base.main()
  1350. except socket.error:
  1351. w = Gtk.MessageDialog(parent=base.window,
  1352. message_type=Gtk.MessageType.ERROR,
  1353. destroy_with_parent=True,
  1354. buttons=Gtk.ButtonsType.CANCEL)
  1355. w.set_markup("gpsd is not running on host %s port %s" %
  1356. (host, port))
  1357. w.run()
  1358. w.destroy()
  1359. except KeyboardInterrupt:
  1360. pass