xgps 57 KB

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