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

xgpsspeed 33KB

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