xgps.in 58 KB

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