xgpsspeed 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976
  1. #!/usr/bin/env python
  2. #
  3. # by
  4. # Robin Wittler <real@the-real.org> (speedometer mode)
  5. # and
  6. # Chen Wei <weichen302@gmx.com> (nautical mode)
  7. #
  8. # SPDX-License-Identifier: BSD-2-clause
  9. # This code runs compatibly under Python 2 and 3.x for x >= 2.
  10. # Preserve this property!
  11. from __future__ import absolute_import, print_function, division
  12. import argparse
  13. import cairo
  14. from math import pi
  15. from math import cos
  16. from math import sin
  17. from math import sqrt
  18. from math import radians
  19. import os
  20. from socket import error as SocketError
  21. import sys
  22. # Gtk3 imports. Gtk3 requires the require_version(), which then causes
  23. # pylint to complain about the subsequent "non-top" imports.
  24. import gi
  25. try:
  26. gi.require_version('Gtk', '3.0')
  27. except ValueError:
  28. # Gtk2 may be installed, has no require_version()
  29. sys.stderr.write("xgps: ERROR Unsupported Gtk version\n")
  30. exit(1)
  31. from gi.repository import Gtk # pylint: disable=wrong-import-position
  32. from gi.repository import Gdk # pylint: disable=wrong-import-position
  33. from gi.repository import GLib # pylint: disable=wrong-import-position
  34. # pylint wants local modules last
  35. try:
  36. import gps
  37. except ImportError as e:
  38. sys.stderr.write(
  39. "xgpsspeed: can't load Python gps libraries -- check PYTHONPATH.\n")
  40. sys.stderr.write("%s\n" % e)
  41. sys.exit(1)
  42. gps_version = '3.19.1~dev'
  43. if gps.__version__ != gps_version:
  44. sys.stderr.write("xgpsspeed: ERROR: need gps module version %s, got %s\n" %
  45. (gps_version, gps.__version__))
  46. sys.exit(1)
  47. class Speedometer(Gtk.DrawingArea):
  48. "Speedometer class"
  49. def __init__(self, speed_unit=None):
  50. "Init Speedometer class"
  51. Gtk.DrawingArea.__init__(self)
  52. self.MPH_UNIT_LABEL = 'mph'
  53. self.KPH_UNIT_LABEL = 'kmh'
  54. self.KNOTS_UNIT_LABEL = 'knots'
  55. self.conversions = {
  56. self.MPH_UNIT_LABEL: gps.MPS_TO_MPH,
  57. self.KPH_UNIT_LABEL: gps.MPS_TO_KPH,
  58. self.KNOTS_UNIT_LABEL: gps.MPS_TO_KNOTS
  59. }
  60. self.speed_unit = speed_unit or self.MPH_UNIT_LABEL
  61. if self.speed_unit not in self.conversions:
  62. raise TypeError(
  63. '%s is not a valid speed unit'
  64. % (repr(speed_unit))
  65. )
  66. class LandSpeedometer(Speedometer):
  67. "LandSpeedometer class"
  68. def __init__(self, speed_unit=None):
  69. "Init LandSpeedometer class"
  70. Speedometer.__init__(self, speed_unit)
  71. self.connect('size-allocate', self.on_size_allocate)
  72. self.width = self.height = 0
  73. self.connect('draw', self.draw_s)
  74. self.long_ticks = (2, 1, 0, -1, -2, -3, -4, -5, -6, -7, -8)
  75. self.short_ticks = (0.1, 0.2, 0.3, 0.4, 0.6, 0.7, 0.8, 0.9)
  76. self.long_inset = lambda x: 0.1 * x
  77. self.middle_inset = lambda x: self.long_inset(x) / 1.5
  78. self.short_inset = lambda x: self.long_inset(x) / 3
  79. self.res_div = 10.0
  80. self.res_div_mul = 1
  81. self.last_speed = 0
  82. self.nums = {
  83. -8: 0,
  84. -7: 10,
  85. -6: 20,
  86. -5: 30,
  87. -4: 40,
  88. -3: 50,
  89. -2: 60,
  90. -1: 70,
  91. 0: 80,
  92. 1: 90,
  93. 2: 100
  94. }
  95. def on_size_allocate(self, _unused, allocation):
  96. self.width = allocation.width
  97. self.height = allocation.height
  98. def draw_s(self, widget, _event, _empty=None):
  99. "Top level draw"
  100. window = widget.get_window()
  101. region = window.get_clip_region()
  102. context = window.begin_draw_frame(region)
  103. self.cr = context.get_cairo_context()
  104. self.cr.rectangle(0, 0, self.width, self.height)
  105. self.cr.clip()
  106. x, y = self.get_x_y()
  107. width, height = self.get_window().get_geometry()[2:4]
  108. radius = self.get_radius(width, height)
  109. self.cr.set_line_width(radius / 100)
  110. self.draw_arc_and_ticks(width, height, radius, x, y)
  111. self.draw_needle(self.last_speed, radius, x, y)
  112. self.draw_speed_text(self.last_speed, radius, x, y)
  113. self.cr = None
  114. window.end_draw_frame(context)
  115. def draw_arc_and_ticks(self, width, height, radius, x, y):
  116. self.cr.set_source_rgb(1.0, 1.0, 1.0)
  117. self.cr.rectangle(0, 0, width, height)
  118. self.cr.fill()
  119. self.cr.set_source_rgb(0.0, 0.0, 0.0)
  120. # draw the speedometer arc
  121. self.cr.arc_negative(x, y, radius, radians(60), radians(120))
  122. self.cr.stroke()
  123. long_inset = self.long_inset(radius)
  124. middle_inset = self.middle_inset(radius)
  125. short_inset = self.short_inset(radius)
  126. # draw the ticks
  127. for i in self.long_ticks:
  128. self.cr.move_to(
  129. x + (radius - long_inset) * cos(i * pi / 6.0),
  130. y + (radius - long_inset) * sin(i * pi / 6.0)
  131. )
  132. self.cr.line_to(
  133. (x + (radius + (self.cr.get_line_width() / 2)) *
  134. cos(i * pi / 6.0)),
  135. (y + (radius + (self.cr.get_line_width() / 2)) *
  136. sin(i * pi / 6.0))
  137. )
  138. self.cr.select_font_face(
  139. 'Georgia',
  140. cairo.FONT_SLANT_NORMAL,
  141. )
  142. self.cr.set_font_size(radius / 10)
  143. self.cr.save()
  144. _num = str(self.nums.get(i) * self.res_div_mul)
  145. (
  146. _x_bearing,
  147. _y_bearing,
  148. t_width,
  149. t_height,
  150. _x_advance,
  151. _y_advance
  152. ) = self.cr.text_extents(_num)
  153. if i in (-8, -7, -6, -5, -4):
  154. self.cr.move_to(
  155. (x + (radius - long_inset - (t_width / 2)) *
  156. cos(i * pi / 6.0)),
  157. (y + (radius - long_inset - (t_height * 2)) *
  158. sin(i * pi / 6.0))
  159. )
  160. elif i in (-2, -1, 0, 2, 1):
  161. self.cr.move_to(
  162. (x + (radius - long_inset - (t_width * 1.5)) *
  163. cos(i * pi / 6.0)),
  164. (y + (radius - long_inset - (t_height * 2)) *
  165. sin(i * pi / 6.0))
  166. )
  167. elif i in (-3,):
  168. self.cr.move_to(
  169. (x - t_width / 2),
  170. (y - radius + self.long_inset(radius) * 2 + t_height)
  171. )
  172. self.cr.show_text(_num)
  173. self.cr.restore()
  174. if i != self.long_ticks[0]:
  175. self.cr.move_to(
  176. x + (radius - middle_inset) * cos((i + 0.5) * pi / 6.0),
  177. y + (radius - middle_inset) * sin((i + 0.5) * pi / 6.0)
  178. )
  179. self.cr.line_to(
  180. x + (radius + (self.cr.get_line_width() / 2)) *
  181. cos((i + 0.5) * pi / 6.0),
  182. y + (radius + (self.cr.get_line_width() / 2)) *
  183. sin((i + 0.5) * pi / 6.0)
  184. )
  185. for z in self.short_ticks:
  186. w_half = self.cr.get_line_width() / 2
  187. if i < 0:
  188. self.cr.move_to(
  189. x + (radius - short_inset) * cos((i + z) * pi / 6.0),
  190. y + (radius - short_inset) * sin((i + z) * pi / 6.0)
  191. )
  192. self.cr.line_to(
  193. x + (radius + w_half) * cos((i + z) * pi / 6.0),
  194. y + (radius + w_half) * sin((i + z) * pi / 6.0)
  195. )
  196. else:
  197. self.cr.move_to(
  198. x + (radius - short_inset) * cos((i - z) * pi / 6.0),
  199. y + (radius - short_inset) * sin((i - z) * pi / 6.0)
  200. )
  201. self.cr.line_to(
  202. x + (radius + w_half) * cos((i - z) * pi / 6.0),
  203. y + (radius + w_half) * sin((i - z) * pi / 6.0)
  204. )
  205. self.cr.stroke()
  206. def draw_needle(self, speed, radius, x, y):
  207. self.cr.save()
  208. inset = self.long_inset(radius)
  209. speed = speed * self.conversions.get(self.speed_unit)
  210. speed = speed / (self.res_div * self.res_div_mul)
  211. actual = self.long_ticks[-1] + speed
  212. if actual > self.long_ticks[0]:
  213. self.res_div_mul += 1
  214. speed = speed / (self.res_div * self.res_div_mul)
  215. actual = self.long_ticks[-1] + speed
  216. self.cr.move_to(x, y)
  217. self.cr.line_to(
  218. x + (radius - (2 * inset)) * cos(actual * pi / 6.0),
  219. y + (radius - (2 * inset)) * sin(actual * pi / 6.0)
  220. )
  221. self.cr.stroke()
  222. self.cr.restore()
  223. def draw_speed_text(self, speed, radius, x, y):
  224. self.cr.save()
  225. speed = '%.2f %s' % (
  226. speed * self.conversions.get(self.speed_unit),
  227. self.speed_unit
  228. )
  229. self.cr.select_font_face(
  230. 'Georgia',
  231. cairo.FONT_SLANT_NORMAL,
  232. # cairo.FONT_WEIGHT_BOLD
  233. )
  234. self.cr.set_font_size(radius / 10)
  235. _x_bearing, _y_bearing, t_width, _t_height = \
  236. self.cr.text_extents(speed)[:4]
  237. self.cr.move_to((x - t_width / 2),
  238. (y + radius) - self.long_inset(radius))
  239. self.cr.show_text(speed)
  240. self.cr.restore()
  241. def get_x_y(self):
  242. rect = self.get_allocation()
  243. x = (rect.x + rect.width / 2.0)
  244. y = (rect.y + rect.height / 2.0) - 20
  245. return x, y
  246. def get_radius(self, width, height):
  247. return min(width / 2.0, height / 2.0) - 20
  248. class NauticalSpeedometer(Speedometer):
  249. "NauticalSpeedometer class"
  250. HEADING_SAT_GAP = 0.8
  251. SAT_SIZE = 10 # radius of the satellite circle in skyview
  252. def __init__(self, speed_unit=None, maxspeed=100, rotate=0.0):
  253. Speedometer.__init__(self, speed_unit)
  254. self.connect('size-allocate', self.on_size_allocate)
  255. self.width = self.height = 0
  256. self.connect('draw', self.draw_s)
  257. self.long_inset = lambda x: 0.05 * x
  258. self.mid_inset = lambda x: self.long_inset(x) / 1.5
  259. self.short_inset = lambda x: self.long_inset(x) / 3
  260. self.last_speed = 0
  261. self.satellites = []
  262. self.last_heading = 0
  263. self.maxspeed = int(maxspeed)
  264. self.rotate = radians(rotate)
  265. self.cr = None
  266. def polar2xy(self, radius, angle, polex, poley):
  267. '''convert Polar coordinate to Cartesian coordinate system
  268. the y axis in pygtk points downward
  269. Args:
  270. radius:
  271. angle: azimuth from from Polar coordinate system, in radian
  272. polex and poley are the Cartesian coordinate of the pole
  273. return a tuple contains (x, y)'''
  274. return (polex + cos(angle) * radius, poley - sin(angle) * radius)
  275. def polar2xyr(self, radius, angle, polex, poley):
  276. '''Version of polar2xy that includes rotation'''
  277. angle = (angle + self.rotate) % (pi * 2) # Note reversed sense
  278. return self.polar2xy(radius, angle, polex, poley)
  279. def on_size_allocate(self, _unused, allocation):
  280. self.width = allocation.width
  281. self.height = allocation.height
  282. def draw_s(self, widget, _event, _empty=None):
  283. "Top level draw"
  284. window = widget.get_window()
  285. region = window.get_clip_region()
  286. context = window.begin_draw_frame(region)
  287. self.cr = context.get_cairo_context()
  288. self.cr.rectangle(0, 0, self.width, self.height)
  289. self.cr.clip()
  290. x, y = self.get_x_y()
  291. width, height = self.get_window().get_geometry()[2:4]
  292. radius = self.get_radius(width, height)
  293. self.cr.set_line_width(radius / 100)
  294. self.draw_arc_and_ticks(width, height, radius, x, y)
  295. self.draw_heading(20, self.last_heading, radius, x, y)
  296. for sat in self.satellites:
  297. self.draw_sat(sat, radius * NauticalSpeedometer.HEADING_SAT_GAP,
  298. x, y)
  299. self.draw_speed(radius, x, y)
  300. self.cr = None
  301. window.end_draw_frame(context)
  302. def draw_text(self, x, y, text, fontsize=10):
  303. '''draw text at given location
  304. Args:
  305. x, y is the center of textbox'''
  306. txt = str(text)
  307. self.cr.new_sub_path()
  308. self.cr.set_source_rgba(0, 0, 0)
  309. self.cr.select_font_face('Sans',
  310. cairo.FONT_SLANT_NORMAL,
  311. cairo.FONT_WEIGHT_BOLD)
  312. self.cr.set_font_size(fontsize)
  313. (_x_bearing, _y_bearing,
  314. t_width, t_height) = self.cr.text_extents(txt)[:4]
  315. # set the center of textbox
  316. self.cr.move_to(x - t_width / 2, y + t_height / 2)
  317. self.cr.show_text(txt)
  318. def draw_arc_and_ticks(self, width, height, radius, x, y):
  319. '''Draw a serial of circle, with ticks in outmost circle'''
  320. self.cr.set_source_rgb(1.0, 1.0, 1.0)
  321. self.cr.rectangle(0, 0, width, height)
  322. self.cr.fill()
  323. self.cr.set_source_rgba(0, 0, 0)
  324. # draw the speedmeter arc
  325. rspeed = radius + 50
  326. self.cr.arc(x, y, rspeed, 2 * pi / 3, 7 * pi / 3)
  327. self.cr.set_source_rgba(0, 0, 0, 1.0)
  328. self.cr.stroke()
  329. s_long = self.long_inset(rspeed)
  330. s_middle = self.mid_inset(radius)
  331. s_short = self.short_inset(radius)
  332. for i in range(11):
  333. # draw the large ticks
  334. alpha = (8 - i) * pi / 6
  335. self.cr.move_to(*self.polar2xy(rspeed, alpha, x, y))
  336. self.cr.set_line_width(radius / 100)
  337. self.cr.line_to(*self.polar2xy(rspeed - s_long, alpha, x, y))
  338. self.cr.stroke()
  339. self.cr.set_line_width(radius / 200)
  340. xf, yf = self.polar2xy(rspeed + 10, alpha, x, y)
  341. stxt = (self.maxspeed // 10) * i
  342. self.draw_text(xf, yf, stxt, fontsize=radius / 15)
  343. for i in range(1, 11):
  344. # middle tick
  345. alpha = (8 - i) * pi / 6
  346. beta = (17 - 2 * i) * pi / 12
  347. self.cr.move_to(*self.polar2xy(rspeed, beta, x, y))
  348. self.cr.line_to(*self.polar2xy(rspeed - s_middle, beta, x, y))
  349. # short tick
  350. for n in range(10):
  351. gamma = alpha + n * pi / 60
  352. self.cr.move_to(*self.polar2xy(rspeed, gamma, x, y))
  353. self.cr.line_to(*self.polar2xy(rspeed - s_short, gamma, x, y))
  354. # draw the heading arc
  355. self.cr.new_sub_path()
  356. self.cr.arc(x, y, radius, 0, 2 * pi)
  357. self.cr.stroke()
  358. self.cr.arc(x, y, radius - 20, 0, 2 * pi)
  359. self.cr.set_source_rgba(0, 0, 0, 0.20)
  360. self.cr.fill()
  361. self.cr.set_source_rgba(0, 0, 0)
  362. # heading label 90/180/270
  363. for n in range(0, 4):
  364. label = str(n * 90)
  365. # self.cr.set_source_rgba(0, 1, 0)
  366. # radius * (1 + NauticalSpeedometer.HEADING_SAT_GAP),
  367. tbox_x, tbox_y = self.polar2xyr(
  368. radius * 0.88,
  369. (1 - n) * pi / 2,
  370. x, y)
  371. self.draw_text(tbox_x, tbox_y,
  372. label, fontsize=radius / 20)
  373. # draw the satellite arcs
  374. skyradius = radius * NauticalSpeedometer.HEADING_SAT_GAP
  375. self.cr.set_line_width(radius / 200)
  376. self.cr.set_source_rgba(0, 0, 0)
  377. self.cr.arc(x, y, skyradius, 0, 2 * pi)
  378. self.cr.set_source_rgba(1, 1, 1)
  379. self.cr.fill()
  380. self.cr.set_source_rgba(0, 0, 0)
  381. self.cr.arc(x, y, skyradius * 2 / 3, 0, 2 * pi)
  382. self.cr.move_to(x + skyradius / 3, y) # Avoid line connecting circles
  383. self.cr.arc(x, y, skyradius / 3, 0, 2 * pi)
  384. # draw the cross hair
  385. self.cr.move_to(*self.polar2xyr(skyradius, 1.5 * pi, x, y))
  386. self.cr.line_to(*self.polar2xyr(skyradius, 0.5 * pi, x, y))
  387. self.cr.move_to(*self.polar2xyr(skyradius, 0.0, x, y))
  388. self.cr.line_to(*self.polar2xyr(skyradius, pi, x, y))
  389. self.cr.set_line_width(radius / 200)
  390. self.cr.stroke()
  391. long_inset = self.long_inset(radius)
  392. mid_inset = self.mid_inset(radius)
  393. short_inset = self.short_inset(radius)
  394. # draw the large ticks
  395. for i in range(12):
  396. agllong = i * pi / 6
  397. self.cr.move_to(*self.polar2xy(radius - long_inset, agllong, x, y))
  398. self.cr.line_to(*self.polar2xy(radius, agllong, x, y))
  399. self.cr.set_line_width(radius / 100)
  400. self.cr.stroke()
  401. self.cr.set_line_width(radius / 200)
  402. # middle tick
  403. aglmid = (i + 0.5) * pi / 6
  404. self.cr.move_to(*self.polar2xy(radius - mid_inset, aglmid, x, y))
  405. self.cr.line_to(*self.polar2xy(radius, aglmid, x, y))
  406. # short tick
  407. for n in range(1, 10):
  408. aglshrt = agllong + n * pi / 60
  409. self.cr.move_to(*self.polar2xy(radius - short_inset,
  410. aglshrt, x, y))
  411. self.cr.line_to(*self.polar2xy(radius, aglshrt, x, y))
  412. self.cr.stroke()
  413. def draw_heading(self, trig_height, heading, radius, x, y):
  414. hypo = trig_height * 2 / sqrt(3)
  415. h = (pi / 2 - radians(heading) + self.rotate) % (pi * 2) # to xyz
  416. self.cr.set_line_width(2)
  417. self.cr.set_source_rgba(0, 0.3, 0.2, 0.8)
  418. # the triangle pointer
  419. x0 = x + radius * cos(h)
  420. y0 = y - radius * sin(h)
  421. x1 = x0 + hypo * cos(7 * pi / 6 + h)
  422. y1 = y0 - hypo * sin(7 * pi / 6 + h)
  423. x2 = x0 + hypo * cos(5 * pi / 6 + h)
  424. y2 = y0 - hypo * sin(5 * pi / 6 + h)
  425. self.cr.move_to(x0, y0)
  426. self.cr.line_to(x1, y1)
  427. self.cr.line_to(x2, y2)
  428. self.cr.line_to(x0, y0)
  429. self.cr.close_path()
  430. self.cr.fill()
  431. self.cr.stroke()
  432. # heading text
  433. (tbox_x, tbox_y) = self.polar2xy(radius * 1.1, h, x, y)
  434. self.draw_text(tbox_x, tbox_y, int(heading), fontsize=radius / 15)
  435. # the ship shape, based on test and try
  436. shiplen = radius * NauticalSpeedometer.HEADING_SAT_GAP / 4
  437. xh, yh = self.polar2xy(shiplen * 2.3, h, x, y)
  438. xa, ya = self.polar2xy(shiplen * 2.2, h + pi - 0.3, x, y)
  439. xb, yb = self.polar2xy(shiplen * 2.2, h + pi + 0.3, x, y)
  440. xc, yc = self.polar2xy(shiplen * 1.4, h - pi / 5, x, y)
  441. xd, yd = self.polar2xy(shiplen * 1.4, h + pi / 5, x, y)
  442. self.cr.set_source_rgba(0, 0.3, 0.2, 0.5)
  443. self.cr.move_to(xa, ya)
  444. self.cr.line_to(xb, yb)
  445. self.cr.line_to(xc, yc)
  446. self.cr.line_to(xh, yh)
  447. self.cr.line_to(xd, yd)
  448. self.cr.close_path()
  449. self.cr.fill()
  450. # self.cr.stroke()
  451. def set_color(self, spec):
  452. '''Set foreground color for drawing.'''
  453. color = Gdk.RGBA()
  454. color.parse(spec)
  455. Gdk.cairo_set_source_rgba(self.cr, color)
  456. def draw_sat(self, satsoup, radius, x, y):
  457. """Given a sat's elevation, azimuth, SNR, draw it on the skyview
  458. Arg:
  459. satsoup: a dictionary {'el': xx, 'az': xx, 'ss': xx}
  460. """
  461. el, az = satsoup['el'], satsoup['az']
  462. if el == 0 and az == 0:
  463. return # Skip satellites with unknown position
  464. h = pi / 2 - radians(az) # to xy
  465. self.cr.set_line_width(2)
  466. self.cr.set_source_rgb(0, 0, 0)
  467. x0, y0 = self.polar2xyr(radius * (90 - el) // 90, h, x, y)
  468. self.cr.new_sub_path()
  469. if gps.is_sbas(satsoup['PRN']):
  470. self.cr.rectangle(x0 - NauticalSpeedometer.SAT_SIZE,
  471. y0 - NauticalSpeedometer.SAT_SIZE,
  472. NauticalSpeedometer.SAT_SIZE * 2,
  473. NauticalSpeedometer.SAT_SIZE * 2)
  474. else:
  475. self.cr.arc(x0, y0, NauticalSpeedometer.SAT_SIZE, 0, pi * 2.0)
  476. if satsoup['ss'] < 10:
  477. self.set_color('Gray')
  478. elif satsoup['ss'] < 30:
  479. self.set_color('Red')
  480. elif satsoup['ss'] < 35:
  481. self.set_color('Yellow')
  482. elif satsoup['ss'] < 40:
  483. self.set_color('Green3')
  484. else:
  485. self.set_color('Green1')
  486. if satsoup['used']:
  487. self.cr.fill()
  488. else:
  489. self.cr.stroke()
  490. self.draw_text(x0, y0, satsoup['PRN'], fontsize=15)
  491. def draw_speed(self, radius, x, y):
  492. self.cr.new_sub_path()
  493. self.cr.set_line_width(20)
  494. self.cr.set_source_rgba(0, 0, 0, 0.5)
  495. speed = self.last_speed * self.conversions.get(self.speed_unit)
  496. # cariol arc angle start at polar 0, going clockwise
  497. alpha = 4 * pi / 3
  498. beta = 2 * pi - alpha
  499. theta = 5 * pi * speed / (self.maxspeed * 3)
  500. self.cr.arc(x, y, radius + 40, beta, beta + theta)
  501. self.cr.stroke()
  502. # self.cr.close_path()
  503. # self.cr.fill()
  504. label = '%.2f %s' % (speed, self.speed_unit)
  505. self.draw_text(x, y + radius + 40, label, fontsize=20)
  506. def get_x_y(self):
  507. rect = self.get_allocation()
  508. x = (rect.x + rect.width / 2.0)
  509. y = (rect.y + rect.height / 2.0) - 20
  510. return x, y
  511. def get_radius(self, width, height):
  512. return min(width / 2.0, height / 2.0) - 70
  513. class Main(object):
  514. "Main"
  515. def __init__(self, host='localhost', port=gps.GPSD_PORT, device=None,
  516. debug=0, speed_unit=None, maxspeed=0, nautical=False,
  517. rotate=0.0, target=""):
  518. self.host = host
  519. self.port = port
  520. self.device = device
  521. self.debug = debug
  522. self.speed_unit = speed_unit
  523. self.maxspeed = maxspeed
  524. self.nautical = nautical
  525. self.rotate = rotate
  526. self.window = Gtk.Window(type=Gtk.WindowType.TOPLEVEL)
  527. if not self.window.get_display():
  528. raise Exception("Can't open display")
  529. if target:
  530. target = " " + target
  531. self.window.set_title('xgpsspeed' + target)
  532. self.window.connect("delete-event", self.delete_event)
  533. vbox = Gtk.VBox(homogeneous=False, spacing=0)
  534. self.window.add(vbox)
  535. # menubar
  536. menubar = Gtk.MenuBar()
  537. vbox.pack_start(menubar, False, False, 0)
  538. agr = Gtk.AccelGroup()
  539. self.window.add_accel_group(agr)
  540. # need the widget before the menu as the menu building
  541. # calls the widget
  542. self.window.set_size_request(400, 450)
  543. if self.nautical:
  544. self.widget = NauticalSpeedometer(
  545. speed_unit=self.speed_unit,
  546. maxspeed=self.maxspeed,
  547. rotate=self.rotate)
  548. else:
  549. self.widget = LandSpeedometer(speed_unit=self.speed_unit)
  550. self.speedframe = Gtk.Frame()
  551. self.speedframe.add(self.widget)
  552. vbox.add(self.speedframe)
  553. self.window.connect('delete-event', self.delete_event)
  554. self.window.connect('destroy', self.destroy)
  555. self.window.present()
  556. # File
  557. topmenu = Gtk.MenuItem(label="File")
  558. menubar.append(topmenu)
  559. submenu = Gtk.Menu()
  560. topmenu.set_submenu(submenu)
  561. menui = Gtk.MenuItem(label="Quit")
  562. key, mod = Gtk.accelerator_parse("<Control>Q")
  563. menui.add_accelerator("activate", agr, key, mod,
  564. Gtk.AccelFlags.VISIBLE)
  565. menui.connect("activate", Gtk.main_quit)
  566. submenu.append(menui)
  567. # View
  568. topmenu = Gtk.MenuItem(label="View")
  569. menubar.append(topmenu)
  570. submenu = Gtk.Menu()
  571. topmenu.set_submenu(submenu)
  572. views = [["Nautical", False, "0", "Nautical"],
  573. ["Land", False, "1", "Land"],
  574. ]
  575. if self.nautical:
  576. views[0][1] = True
  577. else:
  578. views[1][1] = True
  579. menui = None
  580. for name, active, acc, handle in views:
  581. menui = Gtk.RadioMenuItem(group=menui, label=name)
  582. menui.set_active(active)
  583. menui.connect("activate", self.view_toggle, handle)
  584. if acc:
  585. key, mod = Gtk.accelerator_parse(acc)
  586. menui.add_accelerator("activate", agr, key, mod,
  587. Gtk.AccelFlags.VISIBLE)
  588. submenu.append(menui)
  589. # Units
  590. topmenu = Gtk.MenuItem(label="Units")
  591. menubar.append(topmenu)
  592. submenu = Gtk.Menu()
  593. topmenu.set_submenu(submenu)
  594. units = [["Imperial", True, "i", 'mph'],
  595. ["Nautical", False, "n", 'knots'],
  596. ["Metric", False, "m", 'kmh'],
  597. ]
  598. menui = None
  599. for name, active, acc, handle in units:
  600. menui = Gtk.RadioMenuItem(group=menui, label=name)
  601. menui.set_active(active)
  602. menui.connect("activate", self.set_units, handle)
  603. if acc:
  604. key, mod = Gtk.accelerator_parse(acc)
  605. menui.add_accelerator("activate", agr, key, mod,
  606. Gtk.AccelFlags.VISIBLE)
  607. submenu.append(menui)
  608. # Help
  609. topmenu = Gtk.MenuItem(label="Help")
  610. menubar.append(topmenu)
  611. submenu = Gtk.Menu()
  612. topmenu.set_submenu(submenu)
  613. menui = Gtk.MenuItem(label="About")
  614. menui.connect("activate", self.about)
  615. submenu.append(menui)
  616. # vbox.pack_start(menubar, False, False, 0)
  617. # vbox.add(self.speedframe)
  618. self.window.show_all()
  619. def about(self, _unused):
  620. "Show about dialog"
  621. about = Gtk.AboutDialog()
  622. about.set_program_name("xgpsspeed")
  623. about.set_version("Versions:\n"
  624. "xgpspeed %s\n"
  625. "PyGObject Version %d.%d.%d" %
  626. (gps_version, gi.version_info[0],
  627. gi.version_info[1], gi.version_info[2]))
  628. about.set_copyright("Copyright 2004-2019 by The GPSD Project")
  629. about.set_website("https://www.gpsd.io")
  630. about.set_website_label("https://www.gpsd.io")
  631. about.set_license("BSD-2-clause")
  632. about.run()
  633. about.destroy()
  634. def delete_event(self, _widget, _event, _data=None):
  635. "Say goodbye nicely"
  636. Gtk.main_quit()
  637. return False
  638. def set_units(self, _unused, handle):
  639. "Change the display units."
  640. # print("set_units:", handle, self)
  641. self.widget.speed_unit = handle
  642. def watch(self, daemon, device):
  643. self.daemon = daemon
  644. self.device = device
  645. GLib.io_add_watch(daemon.sock, GLib.PRIORITY_DEFAULT,
  646. GLib.IO_IN, self.handle_response)
  647. GLib.io_add_watch(daemon.sock, GLib.PRIORITY_DEFAULT,
  648. GLib.IO_ERR, self.handle_hangup)
  649. GLib.io_add_watch(daemon.sock, GLib.PRIORITY_DEFAULT,
  650. GLib.IO_HUP, self.handle_hangup)
  651. return True
  652. def view_toggle(self, action, name):
  653. "Toggle widget view"
  654. if not action.get_active() or not name:
  655. # nothing to do
  656. return
  657. parent = self.widget.get_parent()
  658. if 'Nautical' == name:
  659. self.nautical = True
  660. widget = NauticalSpeedometer(
  661. speed_unit=self.speed_unit,
  662. maxspeed=self.maxspeed,
  663. rotate=self.rotate)
  664. else:
  665. self.nautical = False
  666. widget = LandSpeedometer(speed_unit=self.speed_unit)
  667. parent.remove(self.widget)
  668. parent.add(widget)
  669. self.widget = widget
  670. self.widget.show()
  671. def handle_response(self, source, condition):
  672. if self.daemon.read() == -1:
  673. self.handle_hangup(source, condition)
  674. if self.daemon.data['class'] == 'VERSION':
  675. self.update_version(self.daemon.version)
  676. elif self.daemon.data['class'] == 'TPV':
  677. self.update_speed(self.daemon.data)
  678. elif self.nautical and self.daemon.data['class'] == 'SKY':
  679. self.update_skyview(self.daemon.data)
  680. return True
  681. def handle_hangup(self, _dummy, _unused):
  682. w = Gtk.MessageDialog(
  683. parent=self.window,
  684. message_type=Gtk.MessageType.ERROR,
  685. destroy_with_parent=True,
  686. buttons=Gtk.ButtonsType.OK
  687. )
  688. w.connect("destroy", lambda unused: Gtk.main_quit())
  689. w.set_title('gpsd error')
  690. w.set_markup("gpsd has stopped sending data.")
  691. w.run()
  692. Gtk.main_quit()
  693. return True
  694. def update_speed(self, data):
  695. if hasattr(data, 'speed'):
  696. self.widget.last_speed = data.speed
  697. self.widget.queue_draw()
  698. if self.nautical and hasattr(data, 'track'):
  699. self.widget.last_heading = data.track
  700. self.widget.queue_draw()
  701. # Used for NauticalSpeedometer only
  702. def update_skyview(self, data):
  703. "Update the satellite list and skyview."
  704. if hasattr(data, 'satellites'):
  705. self.widget.satellites = data.satellites
  706. self.widget.queue_draw()
  707. def update_version(self, ver):
  708. "Update the Version"
  709. if ver.release != gps_version:
  710. sys.stderr.write("%s: WARNING gpsd version %s different than "
  711. "expected %s\n" %
  712. (sys.argv[0], ver.release, gps_version))
  713. if ((ver.proto_major != gps.api_major_version or
  714. ver.proto_minor != gps.api_minor_version)):
  715. sys.stderr.write("%s: WARNING API version %s.%s different than "
  716. "expected %s.%s\n" %
  717. (sys.argv[0], ver.proto_major, ver.proto_minor,
  718. gps.api_major_version, gps.api_minor_version))
  719. def destroy(self, _unused, _empty=None):
  720. Gtk.main_quit()
  721. def run(self):
  722. try:
  723. daemon = gps.gps(
  724. host=self.host,
  725. port=self.port,
  726. mode=gps.WATCH_ENABLE | gps.WATCH_JSON | gps.WATCH_SCALED,
  727. verbose=self.debug
  728. )
  729. self.watch(daemon, self.device)
  730. Gtk.main()
  731. except SocketError:
  732. w = Gtk.MessageDialog(
  733. parent=self.window,
  734. message_type=Gtk.MessageType.ERROR,
  735. destroy_with_parent=True,
  736. buttons=Gtk.ButtonsType.OK
  737. )
  738. w.set_title('socket error')
  739. w.set_markup(
  740. "could not connect to gpsd socket. make sure gpsd is running."
  741. )
  742. w.run()
  743. w.destroy()
  744. except KeyboardInterrupt:
  745. pass
  746. if __name__ == '__main__':
  747. usage = '%(prog)s [OPTIONS] [host[:port[:device]]]'
  748. epilog = ('BSD terms apply: see the file COPYING in the distribution root'
  749. ' for details.')
  750. parser = argparse.ArgumentParser(usage=usage, epilog=epilog)
  751. parser.add_argument(
  752. '-D',
  753. '--debug',
  754. dest='debug',
  755. default=0,
  756. type=int,
  757. help='Set level of debug. Must be integer. [Default %(default)s)]'
  758. )
  759. parser.add_argument(
  760. '--device',
  761. dest='device',
  762. default='',
  763. help='The device to connect. [Default %(default)s)]'
  764. )
  765. parser.add_argument(
  766. '--host',
  767. dest='host',
  768. default='localhost',
  769. help='The host to connect. [Default %(default)s)]'
  770. )
  771. parser.add_argument(
  772. '--landspeed',
  773. dest='nautical',
  774. default=True,
  775. action='store_false',
  776. help='Enable dashboard-style speedometer.'
  777. )
  778. parser.add_argument(
  779. '--maxspeed',
  780. dest='maxspeed',
  781. default='50',
  782. help='Max speed of the speedmeter [Default %(default)s]'
  783. )
  784. parser.add_argument(
  785. '--nautical',
  786. dest='nautical',
  787. default=True,
  788. action='store_true',
  789. help='Enable nautical-style speed and track display.'
  790. )
  791. parser.add_argument(
  792. '--port',
  793. dest='port',
  794. default=gps.GPSD_PORT,
  795. help='The port to connect. [Default %(default)s)]'
  796. )
  797. parser.add_argument(
  798. '-r',
  799. '--rotate',
  800. dest='rotate',
  801. default=0,
  802. type=float,
  803. help='Rotation of skyview ("up" direction) in degrees. '
  804. ' [Default %(default)s)]'
  805. )
  806. parser.add_argument(
  807. '--speedunits',
  808. dest='speedunits',
  809. default='mph',
  810. choices=['mph', 'kmh', 'knots'],
  811. help='The unit of speed. [Default %(default)s)]'
  812. )
  813. parser.add_argument(
  814. '-V', '--version',
  815. action='version',
  816. version="%(prog)s: Version " + gps_version + "\n",
  817. help='Output version to stderr, then exit'
  818. )
  819. parser.add_argument(
  820. 'target',
  821. nargs='?',
  822. help='[host[:port[:device]]]'
  823. )
  824. options = parser.parse_args()
  825. # the options host, port, device are set by the defaults
  826. if options.target:
  827. # override with target
  828. arg = options.target.split(':')
  829. len_arg = len(arg)
  830. if len_arg == 1:
  831. (options.host,) = arg
  832. elif len_arg == 2:
  833. (options.host, options.port) = arg
  834. elif len_arg == 3:
  835. (options.host, options.port, options.device) = arg
  836. else:
  837. parser.print_help()
  838. sys.exit(0)
  839. target = ':'.join(options.target[0:])
  840. ltarget = [options.host, options.port, options.device]
  841. target = ':'.join(ltarget)
  842. if 'DISPLAY' not in os.environ:
  843. sys.stderr.write("xgps: ERROR: DISPLAY not set\n")
  844. exit(1)
  845. Main(
  846. host=options.host,
  847. port=options.port,
  848. device=options.device,
  849. speed_unit=options.speedunits,
  850. maxspeed=options.maxspeed,
  851. nautical=options.nautical,
  852. debug=options.debug,
  853. rotate=options.rotate,
  854. target=target,
  855. ).run()