xgps.py.in 59 KB

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