add first version of qt-osd
[fpv.git] / qtosd / qtosd.py
1 # -*- coding: utf-8 -*-
2
3 # OSD (on screen display) written in Qt
4 # Copyright (C) 2015 Olivier Matz <zer0@droids-corp.org>
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
18
19 # Inspired from QcGauge
20 # Copyright (C) 2015 Hadj Tahar Berrima
21 # http://pytricity.com/qt-artificial-horizon-custom-widget/
22
23 import math
24 from PyQt4 import QtCore, QtGui
25
26 import serialfpv
27
28 try:
29     _fromUtf8 = QtCore.QString.fromUtf8
30 except AttributeError:
31     def _fromUtf8(s):
32         return s
33
34 try:
35     _encoding = QtGui.QApplication.UnicodeUTF8
36     def _translate(context, text, disambig):
37         return QtGui.QApplication.translate(context, text, disambig, _encoding)
38 except AttributeError:
39     def _translate(context, text, disambig):
40         return QtGui.QApplication.translate(context, text, disambig)
41
42 class OSDWidget(QtGui.QWidget):
43     def __init__(self, roundWidget = False):
44         super(OSDWidget, self).__init__()
45         # init parameters
46         self.roundWidget = roundWidget
47         # parameters that will be modified by the user
48         self.user_pitch = 0
49         self.user_roll = 0
50         self.user_yaw = 0
51         self.user_speed = 0
52         self.user_alt = 0
53         self.user_rthome = 0
54         self.left_txt = "14.8v / 34A\n1.5Ah" # XXX
55         self.right_txt = "23:03 since take-off\n1.4 km to home\nRSSI 60dB" # XXX
56         self.user_frame_cb = None
57         # filtered parameters
58         self.pitch = 0
59         self.roll = 0
60         self.yaw = 0
61         self.speed = 0
62         self.alt = 0
63         self.rthome = 0
64         # filtered parameters (0 = no filter, 1 = infinite filter)
65         self.pitch_filter_coef = 0.8
66         self.roll_filter_coef = 0.8
67         self.yaw_filter_coef = 0.8
68         self.speed_filter_coef = 0.8
69         self.alt_filter_coef = 0.8
70         self.rthome_filter_coef = 0.8
71         # QRect representing the viewport
72         self.dev = None
73         # QRect representing the viewport, adjusted to a square
74         self.adjdev = None
75         self.setMinimumSize(250, 250)
76
77         # how many degrees between pitch reference lines
78         if roundWidget:
79             self.CAM_ANGLE = 90.
80             self.PITCH_REFLINES_STEP_ANGLE = 10.
81             self.PITCH_REFLINES_BOLD_STEP_ANGLE = 30.
82             self.PITCH_REFLINES_NUM_LINES = 6
83         else:
84             self.CAM_ANGLE = 40.
85             self.PITCH_REFLINES_STEP_ANGLE = 5.
86             self.PITCH_REFLINES_BOLD_STEP_ANGLE = 10.
87             self.PITCH_REFLINES_NUM_LINES = 4
88
89         self.PITCH_REFLINES_TOTAL_ANGLE = (self.PITCH_REFLINES_STEP_ANGLE *
90                                            self.PITCH_REFLINES_NUM_LINES)
91         # in fraction of radius
92         self.FONT_SIZE = 0.06
93         # how many degrees between yaw reference lines
94         self.YAW_REFLINES_STEP_ANGLE = 15.
95         self.YAW_REFLINES_NUM_LINES = 12
96         self.YAW_REFLINES_TOTAL_ANGLE = (self.YAW_REFLINES_STEP_ANGLE *
97                                          self.YAW_REFLINES_NUM_LINES)
98         self.YAW_REFLINES_SIZE_FACTOR = 0.5
99
100         self.SPEED_REFLINES_STEP = 10.
101         self.SPEED_REFLINES_BOLD_STEP = 50.
102         self.SPEED_REFLINES_NUM_LINES = 10
103         self.SPEED_REFLINES_TOTAL = (self.SPEED_REFLINES_STEP *
104                                      self.SPEED_REFLINES_NUM_LINES)
105         self.SPEED_REFLINES_SIZE_FACTOR = 0.7
106
107         self.ALT_REFLINES_STEP = 100.
108         self.ALT_REFLINES_BOLD_STEP = 500.
109         self.ALT_REFLINES_NUM_LINES = 10
110         self.ALT_REFLINES_TOTAL = (self.ALT_REFLINES_STEP *
111                                    self.ALT_REFLINES_NUM_LINES)
112         self.ALT_REFLINES_SIZE_FACTOR = 0.7
113
114         self.FPS = 50.
115         self.timer = QtCore.QTimer(self)
116         self.connect(self.timer, QtCore.SIGNAL("timeout()"), self.frameTimerCb)
117         self.timer.start(1000. / self.FPS)
118
119     def paintEvent(self, evt):
120         """Paint callback, this is the entry point for all drawings."""
121         painter = QtGui.QPainter()
122         painter.begin(self)
123         painter.setRenderHint(QtGui.QPainter.Antialiasing)
124         self.dev = evt.rect()
125         self.min_dim = min(self.dev.right(), self.dev.bottom())
126         self.max_dim = max(self.dev.right(), self.dev.bottom())
127         self.adjdev = QtCore.QRect(0, 0, self.min_dim, self.min_dim)
128         self.adjdev.moveCenter(self.dev.center())
129         self.draw(painter)
130         painter.end()
131
132     def frameTimerCb(self):
133         """called periodically, every frame, it calls the user_frame_cb function,
134            updates the filter and updates the widget"""
135         if self.user_frame_cb != None:
136             self.user_frame_cb()
137
138         # avoid filter bugs when changing between 180 and -180
139         self.pitch += round((self.user_pitch - self.pitch) / 360.) * 360
140         self.pitch = (self.pitch * self.pitch_filter_coef +
141                     self.user_pitch * (1. - self.pitch_filter_coef))
142         self.roll += round((self.user_roll - self.roll) / 360.) * 360
143         self.roll = (self.roll * self.roll_filter_coef +
144                     self.user_roll * (1. - self.roll_filter_coef))
145         self.yaw += round((self.user_yaw - self.yaw) / 360.) * 360
146         self.yaw = (self.yaw * self.yaw_filter_coef +
147                     self.user_yaw * (1. - self.yaw_filter_coef))
148         self.speed = (self.speed * self.speed_filter_coef +
149                     self.user_speed * (1. - self.speed_filter_coef))
150         self.alt = (self.alt * self.alt_filter_coef +
151                     self.user_alt * (1. - self.alt_filter_coef))
152         self.rthome += round((self.user_rthome - self.rthome) / 360.) * 360
153         self.rthome = (self.rthome * self.rthome_filter_coef +
154                     self.user_rthome * (1. - self.rthome_filter_coef))
155         self.update()
156
157     def draw(self, painter):
158         """Draw the widget."""
159         if self.roundWidget:
160             self.drawHorizonRound(painter)
161         else:
162             self.drawHorizon(painter)
163         self.drawPitchGraduation(painter)
164         self.drawRollGraduation(painter)
165         self.drawYaw(painter)
166         self.drawCenterRef(painter)
167         if self.roundWidget == False:
168             self.drawSpeed(painter)
169             self.drawAlt(painter)
170             self.drawReturnToHome(painter)
171             self.drawTxtInfo(painter)
172
173     def getSkyBrush(self):
174         """return the color gradient for the sky (blue)"""
175         sky_gradient = QtGui.QLinearGradient(self.adjdev.topLeft(),
176                                              self.adjdev.bottomRight())
177         color1 = QtCore.Qt.blue
178         color2 = QtCore.Qt.darkBlue
179         sky_gradient.setColorAt(0, color1)
180         sky_gradient.setColorAt(.8, color2)
181         return sky_gradient
182
183     def getGroundBrush(self):
184         """return the color gradient for the ground (marron)"""
185         ground_gradient = QtGui.QLinearGradient(self.adjdev.topLeft(),
186                                                 self.adjdev.bottomRight())
187         color1 = QtGui.QColor(140, 100, 80)
188         color2 = QtGui.QColor(140, 100, 40)
189         ground_gradient.setColorAt(0, color1)
190         ground_gradient.setColorAt(.8, color2)
191         return ground_gradient
192
193     def getCenterRadius(self):
194         """Return the radius of the widget circle"""
195         if self.roundWidget:
196             return self.adjdev.width() / 2.
197         else:
198             return self.adjdev.width() / 3.5
199
200     def drawHorizonRound(self, painter):
201         """Draw the horizon for round widget: the sky in blue,
202            the ground in marron."""
203
204         # set local pitch and roll
205         pitch = self.pitch
206         roll = self.roll
207         if pitch > 90.:
208             pitch = 180. - pitch
209             roll += 180.
210             if roll > 180.:
211                 roll -= 360.
212         if pitch < -90.:
213             pitch = -180. - pitch
214             roll += 180.
215             if roll > 180.:
216                 roll -= 360.
217
218         # we have to draw a partial circle delimited by its chord, define
219         # where the chord starts
220         start_angle = math.degrees(math.asin(pitch / 90.)) - roll
221         span = 2 * math.degrees(math.asin(pitch / 90.))
222
223         # draw the sky
224         painter.setBrush(self.getSkyBrush())
225         # startAngle and spanAngle must be specified in 1/16th of a degree
226         painter.drawChord(self.adjdev, 16 * start_angle, 16 * (180. - span))
227
228         # draw the ground
229         painter.setBrush(self.getGroundBrush())
230         # startAngle and spanAngle must be specified in 1/16th of a degree
231         painter.drawChord(self.adjdev, 16 * start_angle, -16 * (180. + span))
232
233     def drawHorizon(self, painter):
234         """Draw the horizon: the sky in blue, the ground in marron."""
235         painter.save()
236         painter.setBrush(self.getSkyBrush())
237         painter.drawRect(self.dev)
238         center = self.adjdev.center()
239         r = self.getCenterRadius()
240         # radius of the adjusted screen (same than r if roundWidget = True)
241         dev_r = self.adjdev.width() / 2.
242         roll = self.roll
243         if self.pitch < -90.:
244             pitch = -180 - self.pitch
245         elif self.pitch < 90.:
246             pitch = self.pitch
247         else:
248             pitch = 180 - self.pitch
249         y_off = (pitch / self.CAM_ANGLE) * dev_r
250         painter.translate(center.x(), center.y())
251         painter.rotate(roll)
252         ground_rect = QtCore.QRect(0, 0, self.max_dim * 5., self.max_dim * 5.)
253         if self.pitch < 90. and self.pitch > -90.:
254             ground_rect.moveCenter(QtCore.QPoint(0, -y_off + ground_rect.width()/2.))
255         else:
256             ground_rect.moveCenter(QtCore.QPoint(0, y_off - ground_rect.width()/2.))
257         painter.setBrush(self.getGroundBrush())
258         painter.drawRect(ground_rect)
259         painter.restore()
260
261     def drawCenterRef(self, painter):
262         """Draw the cross on the middle of the OSD"""
263         center = self.adjdev.center()
264         r = self.getCenterRadius()
265         pen = QtGui.QPen(QtCore.Qt.red, r / 100., QtCore.Qt.SolidLine)
266         painter.setPen(pen)
267         pt1 = QtCore.QPoint(center.x() - 0.05 * r, center.y())
268         pt2 = QtCore.QPoint(center.x() + 0.05 * r, center.y())
269         painter.drawLine(pt1, pt2)
270         pt1 = QtCore.QPoint(center.x(), center.y() + -0.025 * r)
271         pt2 = QtCore.QPoint(center.x(), center.y() + 0.025 * r)
272         painter.drawLine(pt1, pt2)
273
274     def drawPitchGraduation(self, painter):
275         """Draw the pitch graduations."""
276         # change the reference
277         painter.save()
278         center = self.adjdev.center()
279         r = self.getCenterRadius()
280         # radius of the adjusted screen (same than r if roundWidget = True)
281         dev_r = self.adjdev.width() / 2.
282         roll = self.roll
283         x_off = (self.pitch / self.CAM_ANGLE) * dev_r * math.sin(math.radians(roll))
284         y_off = (self.pitch / self.CAM_ANGLE) * dev_r * math.cos(math.radians(roll))
285         painter.translate(center.x() + x_off, center.y() - y_off)
286         painter.rotate(roll)
287
288         # set font and pen
289         pen = QtGui.QPen(QtCore.Qt.white, r / 100., QtCore.Qt.SolidLine)
290         painter.setPen(pen)
291         font = QtGui.QFont("Meiryo UI", 0, QtGui.QFont.Bold)
292         font.setPointSizeF(self.FONT_SIZE * r)
293         painter.setFont(font)
294
295         # round to nearest angle that is a multiple of step
296         a = self.pitch / self.PITCH_REFLINES_STEP_ANGLE
297         a = round(a)
298         a = int(a * self.PITCH_REFLINES_STEP_ANGLE)
299         a -= self.PITCH_REFLINES_STEP_ANGLE * (self.PITCH_REFLINES_NUM_LINES / 2.)
300         angles = [ a + i * self.PITCH_REFLINES_STEP_ANGLE
301                    for i in range(self.PITCH_REFLINES_NUM_LINES + 1) ]
302         for a in angles:
303             # thin line
304             if int(a) % int(self.PITCH_REFLINES_BOLD_STEP_ANGLE) != 0:
305                 pen = QtGui.QPen(QtCore.Qt.white, r / 100., QtCore.Qt.SolidLine)
306                 painter.setPen(pen)
307                 pt1 = QtCore.QPoint(-0.05 * r, dev_r / self.CAM_ANGLE * a)
308                 pt2 = QtCore.QPoint(0.05 * r, dev_r / self.CAM_ANGLE * a)
309                 painter.drawLine(pt1, pt2)
310                 continue
311
312             # bold line
313             pen = QtGui.QPen(QtCore.Qt.white, r / 50., QtCore.Qt.SolidLine)
314             painter.setPen(pen)
315             pt1 = QtCore.QPoint(-0.2 * r, dev_r / self.CAM_ANGLE * a)
316             pt2 = QtCore.QPoint(0.2 * r, dev_r / self.CAM_ANGLE * a)
317             painter.drawLine(pt1, pt2)
318
319             # the left text
320             disp_val = -a
321             if disp_val > 90.:
322                 disp_val = 180. - disp_val
323             if disp_val < -90.:
324                 disp_val = -180. - disp_val
325             disp_val = str(int(disp_val))
326             metrics = painter.fontMetrics()
327             sz = metrics.size(QtCore.Qt.TextSingleLine, disp_val)
328             lefttxt = QtCore.QRect(QtCore.QPoint(0, 0), sz)
329             lefttxt.moveCenter(pt1 - QtCore.QPoint(0.2 * r, 0))
330             #pen.setWidth(1);
331             #brush = QtGui.QBrush(QtCore.Qt.white)
332             #painter.setBrush(brush)
333             #pen.setColor(QtCore.Qt.black);
334             #painter.setPen(pen)
335             #path = QtGui.QPainterPath()
336             #path.addText(lefttxt.center(), font, disp_val)
337             #painter.drawPath(path)
338             painter.drawText(lefttxt, QtCore.Qt.TextSingleLine, disp_val)
339
340             # flip the right text
341             painter.save()
342             painter.translate(pt2 + QtCore.QPoint(0.2 * r, 0))
343             painter.rotate(180.)
344             painter.translate(-pt2 - QtCore.QPoint(0.2 * r, 0))
345             righttxt = QtCore.QRect(QtCore.QPoint(0, 0), sz)
346             righttxt.moveCenter(pt2 + QtCore.QPoint(0.2 * r, 0))
347             #path = QtGui.QPainterPath()
348             #path.addText(righttxt.center(), font, disp_val)
349             #painter.drawPath(path)
350             painter.drawText(righttxt, QtCore.Qt.TextSingleLine, disp_val)
351             painter.restore()
352
353         painter.restore()
354
355     def drawOneRollGraduation(self, painter, deg, disp_text):
356         # draw the graduiation
357         r = self.getCenterRadius()
358         center = self.adjdev.center()
359         x = center.x() - math.cos(math.radians(deg)) * r
360         y = center.y() - math.sin(math.radians(deg)) * r
361         pt = QtCore.QPoint(x, y)
362         path = QtGui.QPainterPath()
363         path.moveTo(pt)
364         path.lineTo(center)
365         pt2 = path.pointAtPercent(0.075) # graduation len is 7.5% of the radius
366         painter.drawLine(pt, pt2)
367         # then draw the text
368         if disp_text == True:
369             pt_txt = path.pointAtPercent(0.2)
370             font = QtGui.QFont("Meiryo UI", 0, QtGui.QFont.Bold)
371             font.setPointSizeF(self.FONT_SIZE * r)
372             painter.setFont(font)
373             disp_val = deg
374             if disp_val > 90:
375                 disp_val = 180. - disp_val
376             disp_val = str(int(disp_val))
377             metrics = painter.fontMetrics()
378             sz = metrics.size(QtCore.Qt.TextSingleLine, disp_val)
379             txt = QtCore.QRect(QtCore.QPoint(0, 0), sz)
380             txt.moveCenter(pt_txt.toPoint())
381             painter.drawText(txt, QtCore.Qt.TextSingleLine, disp_val)
382
383     def drawRollGraduation(self, painter):
384         """Draw the roll graduations."""
385         center = self.adjdev.center()
386         r = self.getCenterRadius()
387
388         # draw the red reference lines (pitch 0)
389         painter.save()
390         painter.translate(center.x(), center.y())
391         painter.rotate(self.roll)
392         pen = QtGui.QPen(QtCore.Qt.red, r / 100., QtCore.Qt.SolidLine)
393         painter.setPen(pen)
394         pt1 = QtCore.QPoint(-0.925 * r, 0)
395         pt2 = QtCore.QPoint(-0.85 * r, 0)
396         painter.drawLine(pt1, pt2)
397         pt1 = QtCore.QPoint(0.925 * r, 0)
398         pt2 = QtCore.QPoint(0.85 * r, 0)
399         painter.drawLine(pt1, pt2)
400         painter.restore()
401
402         pen = QtGui.QPen(QtCore.Qt.white, r / 50., QtCore.Qt.SolidLine)
403         painter.setPen(pen)
404         deg = 0
405         while deg <= 180:
406             if deg % 30 == 0:
407                 w = r / 50.
408                 disp_text = True
409             else:
410                 w = r / 100.
411                 disp_text = False
412             pen.setWidth(w)
413             painter.setPen(pen)
414             self.drawOneRollGraduation(painter, deg, disp_text)
415             deg += 10
416
417     def drawYaw(self, painter):
418         center = self.adjdev.center()
419         r = self.getCenterRadius()
420         pen = QtGui.QPen(QtCore.Qt.red, r / 100., QtCore.Qt.SolidLine)
421         painter.setPen(pen)
422         font = QtGui.QFont("Meiryo UI", 0, QtGui.QFont.Bold)
423         font.setPointSizeF(self.FONT_SIZE * r)
424         painter.setFont(font)
425         if self.roundWidget == True:
426             y_txt = center.y() + r * 0.6
427             y1 = center.y() + r * 0.7
428             y2 = center.y() + r * 0.8
429             y3 = center.y() + r * 0.85
430         else:
431             y_txt = center.y() + r * 1.
432             y1 = center.y() + r * 1.1
433             y2 = center.y() + r * 1.2
434             y3 = center.y() + r * 1.25
435         pt1 = QtCore.QPoint(center.x(), y2)
436         pt2 = QtCore.QPoint(center.x(), y3)
437         painter.drawLine(pt1, pt2)
438
439         # round to nearest angle multiple of step
440         a = self.yaw / self.YAW_REFLINES_STEP_ANGLE
441         a = round(a)
442         a = int(a * self.YAW_REFLINES_STEP_ANGLE)
443         a -= self.YAW_REFLINES_STEP_ANGLE * (self.YAW_REFLINES_NUM_LINES / 2.)
444         angles = [ a + i * self.YAW_REFLINES_STEP_ANGLE
445                    for i in range(self.YAW_REFLINES_NUM_LINES + 1) ]
446         for a in angles:
447             # text (N, S, E, W)
448             if int(a) % 90 == 0:
449                 disp_text = True
450                 pen = QtGui.QPen(QtCore.Qt.white, r / 50., QtCore.Qt.SolidLine)
451                 painter.setPen(pen)
452             else:
453                 disp_text = False
454                 pen = QtGui.QPen(QtCore.Qt.white, r / 100., QtCore.Qt.SolidLine)
455                 painter.setPen(pen)
456             # the line
457             x = center.x() - ((r / (self.YAW_REFLINES_TOTAL_ANGLE / 2.)) *
458                               (self.yaw - a) * self.YAW_REFLINES_SIZE_FACTOR)
459             pt_txt = QtCore.QPoint(x, y_txt)
460             pt1 = QtCore.QPoint(x, y1)
461             pt2 = QtCore.QPoint(x, y2)
462             painter.drawLine(pt1, pt2)
463             if disp_text == False:
464                 continue
465             disp_val = ["N", "E", "S", "W"][(int(a)/90)%4]
466             metrics = painter.fontMetrics()
467             sz = metrics.size(QtCore.Qt.TextSingleLine, disp_val)
468             txt = QtCore.QRect(QtCore.QPoint(0, 0), sz)
469             txt.moveCenter(pt_txt)
470             painter.drawText(txt, QtCore.Qt.TextSingleLine, disp_val)
471
472     def drawSpeed(self, painter):
473         center = self.adjdev.center()
474         r = self.getCenterRadius()
475         pen = QtGui.QPen(QtCore.Qt.red, r / 100., QtCore.Qt.SolidLine)
476         painter.setPen(pen)
477         font = QtGui.QFont("Meiryo UI", 0, QtGui.QFont.Bold)
478         font.setPointSizeF(self.FONT_SIZE * r)
479         painter.setFont(font)
480         x1 =  center.x() - 1.5 * r
481         x2 =  center.x() - 1.6 * r
482         pt1 = QtCore.QPoint(center.x() - 1.45 * r, center.y())
483         pt2 = QtCore.QPoint(x2, center.y())
484         painter.drawLine(pt1, pt2)
485
486         # round to nearest angle multiple of step
487         s = self.speed / self.SPEED_REFLINES_STEP
488         s = round(s)
489         s = int(s * self.SPEED_REFLINES_STEP)
490         s -= self.SPEED_REFLINES_STEP * (self.SPEED_REFLINES_NUM_LINES / 2.)
491         speeds = [ s + i * self.SPEED_REFLINES_STEP
492                    for i in range(self.SPEED_REFLINES_NUM_LINES + 1) ]
493         for s in speeds:
494             if int(s) % int(self.SPEED_REFLINES_BOLD_STEP) == 0:
495                 disp_text = True
496                 pen = QtGui.QPen(QtCore.Qt.white, r / 50., QtCore.Qt.SolidLine)
497                 painter.setPen(pen)
498             else:
499                 disp_text = False
500                 pen = QtGui.QPen(QtCore.Qt.white, r / 100., QtCore.Qt.SolidLine)
501                 painter.setPen(pen)
502             # the line
503             y = center.y() + ((r / (self.SPEED_REFLINES_TOTAL/2.)) *
504                               (self.speed - s) * self.SPEED_REFLINES_SIZE_FACTOR)
505             pt_txt = QtCore.QPoint(center.x() + r * -1.75, y)
506             pt1 = QtCore.QPoint(x1, y)
507             pt2 = QtCore.QPoint(x2, y)
508             painter.drawLine(pt1, pt2)
509             if disp_text == False:
510                 continue
511             disp_val = str(int(s))
512             metrics = painter.fontMetrics()
513             sz = metrics.size(QtCore.Qt.TextSingleLine, disp_val)
514             txt = QtCore.QRect(QtCore.QPoint(0, 0), sz)
515             txt.moveCenter(pt_txt)
516             painter.drawText(txt, QtCore.Qt.TextSingleLine, disp_val)
517
518     def drawAlt(self, painter):
519         center = self.adjdev.center()
520         r = self.getCenterRadius()
521         pen = QtGui.QPen(QtCore.Qt.red, r / 100., QtCore.Qt.SolidLine)
522         painter.setPen(pen)
523         font = QtGui.QFont("Meiryo UI", 0, QtGui.QFont.Bold)
524         font.setPointSizeF(self.FONT_SIZE * r)
525         painter.setFont(font)
526         x1 =  center.x() + 1.5 * r
527         x2 =  center.x() + 1.6 * r
528         pt1 = QtCore.QPoint(center.x() + 1.45 * r, center.y())
529         pt2 = QtCore.QPoint(x2, center.y())
530         painter.drawLine(pt1, pt2)
531
532         # round to nearest angle multiple of step
533         a = self.alt / self.ALT_REFLINES_STEP
534         a = round(a)
535         a = int(a * self.ALT_REFLINES_STEP)
536         a -= self.ALT_REFLINES_STEP * (self.ALT_REFLINES_NUM_LINES / 2.)
537         alts = [ a + i * self.ALT_REFLINES_STEP
538                    for i in range(self.ALT_REFLINES_NUM_LINES + 1) ]
539         for a in alts:
540             if int(a) % int(self.ALT_REFLINES_BOLD_STEP) == 0:
541                 disp_text = True
542                 pen = QtGui.QPen(QtCore.Qt.white, r / 50., QtCore.Qt.SolidLine)
543                 painter.setPen(pen)
544             else:
545                 disp_text = False
546                 pen = QtGui.QPen(QtCore.Qt.white, r / 100., QtCore.Qt.SolidLine)
547                 painter.setPen(pen)
548             # the line
549             y = center.y() + ((r / (self.ALT_REFLINES_TOTAL / 2.)) *
550                               (self.alt - a) * self.ALT_REFLINES_SIZE_FACTOR)
551             pt_txt = QtCore.QPoint(center.x() + r * 1.75, y)
552             pt1 = QtCore.QPoint(x1, y)
553             pt2 = QtCore.QPoint(x2, y)
554             painter.drawLine(pt1, pt2)
555             if disp_text == False:
556                 continue
557             disp_val = str(int(a))
558             metrics = painter.fontMetrics()
559             sz = metrics.size(QtCore.Qt.TextSingleLine, disp_val)
560             txt = QtCore.QRect(QtCore.QPoint(0, 0), sz)
561             txt.moveCenter(pt_txt)
562             painter.drawText(txt, QtCore.Qt.TextSingleLine, disp_val)
563
564     def drawReturnToHome(self, painter):
565         center = self.adjdev.center()
566         r = self.getCenterRadius()
567         dev_r = self.adjdev.width() / 2.
568         painter.save()
569         painter.translate(center.x(), center.y() - 0.9 * dev_r)
570         painter.rotate(self.yaw) # XXX
571         pen = QtGui.QPen(QtCore.Qt.white, r / 100., QtCore.Qt.SolidLine)
572         painter.setPen(pen)
573         poly = QtGui.QPolygonF()
574         poly.append(QtCore.QPoint(0., -0.08 * r))
575         poly.append(QtCore.QPoint(0.04 * r, 0.08 * r))
576         poly.append(QtCore.QPoint(-0.04 * r, 0.08 * r))
577         poly.append(QtCore.QPoint(0., -0.08 * r))
578         path = QtGui.QPainterPath()
579         path.addPolygon(poly)
580         brush = QtGui.QBrush(QtCore.Qt.darkGray)
581         painter.setBrush(brush)
582         painter.drawPath(path)
583         painter.restore()
584
585     def drawTxtInfo(self, painter):
586         center = self.adjdev.center()
587         dev_r = self.adjdev.width() / 2.
588         r = self.getCenterRadius()
589         pen = QtGui.QPen(QtCore.Qt.white, r / 100., QtCore.Qt.SolidLine)
590         painter.setPen(pen)
591         font = QtGui.QFont("Meiryo UI", 0, QtGui.QFont.Bold)
592         font.setPointSizeF(self.FONT_SIZE * r)
593         metrics = painter.fontMetrics()
594
595         sz = metrics.size(QtCore.Qt.AlignLeft, self.left_txt)
596         txt = QtCore.QRect(QtCore.QPoint(0, 0), sz)
597         pt_txt = QtCore.QPoint(center.x() + dev_r * -0.95,
598                                center.y() + dev_r * -0.95)
599         txt.moveTopLeft(pt_txt)
600         painter.drawText(txt, QtCore.Qt.AlignLeft, self.left_txt)
601
602         sz = metrics.size(QtCore.Qt.AlignRight, self.right_txt)
603         txt = QtCore.QRect(QtCore.QPoint(0, 0), sz)
604         pt_txt = QtCore.QPoint(center.x() + dev_r * 0.95,
605                                center.y() + dev_r * -0.95)
606         txt.moveTopRight(pt_txt)
607         painter.drawText(txt, QtCore.Qt.AlignRight, self.right_txt)
608
609     def setPitch(self, pitch):
610         """set the pitch in degrees, between -180 and 180"""
611         pitch = pitch % 360.
612         if pitch > 180.:
613             pitch -= 360.
614         self.user_pitch = pitch
615
616     def setRoll(self, roll):
617         """set the roll in degrees, between -180 and 180"""
618         roll = roll % 360.
619         if roll > 180.:
620             roll -= 360.
621         self.user_roll = roll
622
623     def setYaw(self, yaw):
624         """set the yaw in degrees, between 0 and 360"""
625         yaw = yaw % 360.
626         self.user_yaw = yaw
627
628     def setSpeed(self, speed):
629         """set the speed"""
630         self.user_speed = speed
631
632     def setAlt(self, alt):
633         """set the alt"""
634         self.user_alt = alt
635
636     def setReturnToHomeAngle(self, angle):
637         """set the left text"""
638         self.user_rthome = angle
639
640     def setLeftTxt(self, txt):
641         """set the right text"""
642         self.left_txt = txt
643
644     def setRightTxt(self, txt):
645         """set the left text"""
646         self.right_txt = txt
647
648     def setFrameCb(self, user_frame_cb):
649         """set a function that is called every frame, before updating the widget"""
650         self.user_frame_cb = user_frame_cb
651
652 class Ui_MainWindow(object):
653     def __init__(self, filename = None, roundWidget = False):
654         self.roundWidget = roundWidget
655         self.filename = filename
656
657     def update_fpv_info(self):
658         self.fpv.update()
659         self.osd.user_speed = self.osd.user_speed + 1
660         self.osd.setRoll(self.fpv.roll)
661         self.osd.setPitch(self.fpv.pitch)
662         self.osd.setYaw(self.fpv.yaw)
663
664     def setupUi(self, MainWindow):
665         MainWindow.setObjectName(_fromUtf8("MainWindow"))
666         MainWindow.resize(400, 300)
667         self.centralWidget = QtGui.QWidget(MainWindow)
668         self.centralWidget.setObjectName(_fromUtf8("centralWidget"))
669         self.gridLayout = QtGui.QGridLayout(self.centralWidget)
670         self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
671
672         self.pitchSlider = QtGui.QSlider(self.centralWidget)
673         self.pitchSlider.setMinimum(-180)
674         self.pitchSlider.setMaximum(180)
675         self.pitchSlider.setOrientation(QtCore.Qt.Vertical)
676         self.pitchSlider.setObjectName(_fromUtf8("pitchSlider"))
677         self.pitchSlider.valueChanged[int].connect(self.changePitch)
678         self.gridLayout.addWidget(self.pitchSlider, 0, 0)
679
680         self.osd = OSDWidget(self.roundWidget)
681         if self.filename:
682             self.fpv = serialfpv.SerialFPV()
683             self.fpv.open_file(self.filename)
684             self.osd.setFrameCb(self.update_fpv_info)
685         self.gridLayout.addWidget(self.osd, 0, 1)
686
687         self.rollSlider = QtGui.QSlider(self.centralWidget)
688         self.rollSlider.setMinimum(-180)
689         self.rollSlider.setMaximum(180)
690         self.rollSlider.setOrientation(QtCore.Qt.Horizontal)
691         self.rollSlider.setObjectName(_fromUtf8("rollSlider"))
692         self.rollSlider.valueChanged[int].connect(self.changeRoll)
693         self.gridLayout.addWidget(self.rollSlider, 1, 1)
694
695         self.yawSlider = QtGui.QSlider(self.centralWidget)
696         self.yawSlider.setMaximum(360)
697         self.yawSlider.setProperty("value", 180)
698         self.osd.setYaw(180)
699         self.yawSlider.setOrientation(QtCore.Qt.Horizontal)
700         self.yawSlider.setObjectName(_fromUtf8("yawSlider"))
701         self.yawSlider.valueChanged[int].connect(self.changeYaw)
702         self.gridLayout.addWidget(self.yawSlider, 2, 1)
703
704         MainWindow.setCentralWidget(self.centralWidget)
705         self.menuBar = QtGui.QMenuBar(MainWindow)
706         self.menuBar.setGeometry(QtCore.QRect(0, 0, 400, 23))
707         self.menuBar.setObjectName(_fromUtf8("menuBar"))
708         MainWindow.setMenuBar(self.menuBar)
709         self.mainToolBar = QtGui.QToolBar(MainWindow)
710         self.mainToolBar.setObjectName(_fromUtf8("mainToolBar"))
711         MainWindow.addToolBar(QtCore.Qt.TopToolBarArea, self.mainToolBar)
712         self.statusBar = QtGui.QStatusBar(MainWindow)
713         self.statusBar.setObjectName(_fromUtf8("statusBar"))
714         MainWindow.setStatusBar(self.statusBar)
715
716         self.retranslateUi(MainWindow)
717         QtCore.QMetaObject.connectSlotsByName(MainWindow)
718
719     def retranslateUi(self, MainWindow):
720         MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow", None))
721
722     def changePitch(self, value):
723         self.osd.setPitch(value)
724         self.osd.update()
725
726     def changeRoll(self, value):
727         self.osd.setRoll(value)
728         self.osd.update()
729
730     def changeYaw(self, value):
731         self.osd.setYaw(value)
732         self.osd.update()
733
734 if __name__ == "__main__":
735     import sys
736     app = QtGui.QApplication(sys.argv)
737     if "round" in sys.argv:
738         roundWidget = True
739     else:
740         roundWidget = False
741     MainWindow = QtGui.QMainWindow()
742     if len(sys.argv) > 1:
743         ui = Ui_MainWindow(sys.argv[1], roundWidget)
744     else:
745         ui = Ui_MainWindow(roundWidget = roundWidget)
746     ui.setupUi(MainWindow)
747     MainWindow.show()
748     sys.exit(app.exec_())
749