9ed4ff938b6fa8aa27c9a60d8be317de3c9da2e4
[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 import argparse
25
26 from PyQt5 import QtCore
27 from PyQt5.QtCore import (pyqtSlot, QTimer, QRect, QPoint, Qt, QByteArray,
28                           QSizeF, QRectF)
29 from PyQt5.QtGui import (QPainter, QColor, QPen, QBrush, QLinearGradient, QFont,
30                          QPainterPath, QPolygonF)
31 from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QAction,
32                              QActionGroup, QGraphicsScene, QGraphicsView)
33 from PyQt5.QtMultimediaWidgets import (QGraphicsVideoItem)
34 from PyQt5.QtMultimedia import (QCamera, QAbstractVideoSurface,
35                                 QAbstractVideoBuffer)
36
37 import serialfpv
38 import qtosd_ui
39
40 try:
41     _fromUtf8 = QtCore.QString.fromUtf8
42 except AttributeError:
43     def _fromUtf8(s):
44         return s
45
46 try:
47     _encoding = QApplication.UnicodeUTF8
48     def _translate(context, text, disambig):
49         return QApplication.translate(context, text, disambig, _encoding)
50 except AttributeError:
51     def _translate(context, text, disambig):
52         return QApplication.translate(context, text, disambig)
53
54 class OSDWidget(QWidget):
55     def __init__(self, mode = "camera", filename = None):
56         super(OSDWidget, self).__init__()
57         # init parameters
58         self.mode = mode
59         self.fpv = None
60         # parameters that will be modified by the user
61         self.user_pitch = 0
62         self.user_roll = 0
63         self.user_yaw = 0
64         self.user_speed = 0
65         self.user_alt = 0
66         self.user_rthome = 0
67         self.left_txt = "14.8v / 34A\n1.5Ah" # XXX
68         self.right_txt = "23:03 since take-off\n1.4 km to home\nRSSI 60dB" # XXX
69         self.user_frame_cb = None
70         # filtered parameters
71         self.pitch = 0
72         self.roll = 0
73         self.yaw = 0
74         self.speed = 0
75         self.alt = 0
76         self.rthome = 0
77         # filtered parameters (0 = no filter, 1 = infinite filter)
78         self.pitch_filter_coef = 0.8
79         self.roll_filter_coef = 0.8
80         self.yaw_filter_coef = 0.8
81         self.speed_filter_coef = 0.8
82         self.alt_filter_coef = 0.8
83         self.rthome_filter_coef = 0.8
84         # QRect representing the viewport
85         self.dev = None
86         # QRect representing the viewport, adjusted to a square
87         self.adjdev = None
88         self.setMinimumSize(250, 250)
89
90         self.setStyleSheet("background-color: transparent;")
91
92         # how many degrees between pitch reference lines
93         if mode == "round":
94             self.CAM_ANGLE = 90.
95             self.PITCH_REFLINES_STEP_ANGLE = 10.
96             self.PITCH_REFLINES_BOLD_STEP_ANGLE = 30.
97             self.PITCH_REFLINES_NUM_LINES = 6
98         else:
99             self.CAM_ANGLE = 40.
100             self.PITCH_REFLINES_STEP_ANGLE = 5.
101             self.PITCH_REFLINES_BOLD_STEP_ANGLE = 10.
102             self.PITCH_REFLINES_NUM_LINES = 4
103
104         self.PITCH_REFLINES_TOTAL_ANGLE = (self.PITCH_REFLINES_STEP_ANGLE *
105                                            self.PITCH_REFLINES_NUM_LINES)
106         # in fraction of radius
107         self.FONT_SIZE = 0.06
108         # how many degrees between yaw reference lines
109         self.YAW_REFLINES_STEP_ANGLE = 15.
110         self.YAW_REFLINES_NUM_LINES = 12
111         self.YAW_REFLINES_TOTAL_ANGLE = (self.YAW_REFLINES_STEP_ANGLE *
112                                          self.YAW_REFLINES_NUM_LINES)
113         self.YAW_REFLINES_SIZE_FACTOR = 0.5
114
115         self.SPEED_REFLINES_STEP = 10.
116         self.SPEED_REFLINES_BOLD_STEP = 50.
117         self.SPEED_REFLINES_NUM_LINES = 10
118         self.SPEED_REFLINES_TOTAL = (self.SPEED_REFLINES_STEP *
119                                      self.SPEED_REFLINES_NUM_LINES)
120         self.SPEED_REFLINES_SIZE_FACTOR = 0.7
121
122         self.ALT_REFLINES_STEP = 100.
123         self.ALT_REFLINES_BOLD_STEP = 500.
124         self.ALT_REFLINES_NUM_LINES = 10
125         self.ALT_REFLINES_TOTAL = (self.ALT_REFLINES_STEP *
126                                    self.ALT_REFLINES_NUM_LINES)
127         self.ALT_REFLINES_SIZE_FACTOR = 0.7
128
129         if filename:
130             self.fpv = serialfpv.SerialFPV()
131             self.fpv.open_file(filename)
132
133         self.FPS = 50.
134         self.timer = QTimer(self)
135         self.timer.timeout.connect(self.frameTimerCb)
136         self.timer.start(1000. / self.FPS)
137
138     @pyqtSlot(name = "paintEvent")
139     def paintEvent(self, evt):
140         """Paint callback, this is the entry point for all drawings."""
141         painter = QPainter()
142         painter.begin(self)
143         painter.setRenderHint(QPainter.Antialiasing)
144         self.dev = evt.rect()
145         self.min_dim = min(self.dev.right(), self.dev.bottom())
146         self.max_dim = max(self.dev.right(), self.dev.bottom())
147         self.adjdev = QRect(0, 0, self.min_dim, self.min_dim)
148         self.adjdev.moveCenter(self.dev.center())
149         self.draw(painter)
150         painter.end()
151
152     @pyqtSlot(name = "frameTimerCb")
153     def frameTimerCb(self):
154         # read from SerialFPV object
155         if self.fpv:
156             self.fpv.update_state()
157             self.user_speed = self.user_speed + 1
158             self.setRoll(self.fpv.roll)
159             self.setPitch(self.fpv.pitch)
160             self.setYaw(self.fpv.yaw)
161
162         # avoid filter bugs when changing between 180 and -180
163         self.pitch += round((self.user_pitch - self.pitch) / 360.) * 360
164         self.pitch = (self.pitch * self.pitch_filter_coef +
165                     self.user_pitch * (1. - self.pitch_filter_coef))
166         self.roll += round((self.user_roll - self.roll) / 360.) * 360
167         self.roll = (self.roll * self.roll_filter_coef +
168                     self.user_roll * (1. - self.roll_filter_coef))
169         self.yaw += round((self.user_yaw - self.yaw) / 360.) * 360
170         self.yaw = (self.yaw * self.yaw_filter_coef +
171                     self.user_yaw * (1. - self.yaw_filter_coef))
172         self.speed = (self.speed * self.speed_filter_coef +
173                     self.user_speed * (1. - self.speed_filter_coef))
174         self.alt = (self.alt * self.alt_filter_coef +
175                     self.user_alt * (1. - self.alt_filter_coef))
176         self.rthome += round((self.user_rthome - self.rthome) / 360.) * 360
177         self.rthome = (self.rthome * self.rthome_filter_coef +
178                     self.user_rthome * (1. - self.rthome_filter_coef))
179         self.update()
180
181     def draw(self, painter):
182         """Draw the widget."""
183         if self.mode == "round":
184             self.drawHorizonRound(painter)
185         elif self.mode == "rectangle":
186             self.drawHorizon(painter)
187         self.drawPitchGraduation(painter)
188         self.drawRollGraduation(painter)
189         self.drawYaw(painter)
190         self.drawCenterRef(painter)
191         if self.mode != "round":
192             self.drawSpeed(painter)
193             self.drawAlt(painter)
194             self.drawReturnToHome(painter)
195             self.drawTxtInfo(painter)
196
197     def getSkyBrush(self):
198         """return the color gradient for the sky (blue)"""
199         sky_gradient = QLinearGradient(self.adjdev.topLeft(),
200                                        self.adjdev.bottomRight())
201         color1 = Qt.blue
202         color2 = Qt.darkBlue
203         sky_gradient.setColorAt(0, color1)
204         sky_gradient.setColorAt(.8, color2)
205         return sky_gradient
206
207     def getGroundBrush(self):
208         """return the color gradient for the ground (marron)"""
209         ground_gradient = QLinearGradient(self.adjdev.topLeft(),
210                                           self.adjdev.bottomRight())
211         color1 = QColor(140, 100, 80)
212         color2 = QColor(140, 100, 40)
213         ground_gradient.setColorAt(0, color1)
214         ground_gradient.setColorAt(.8, color2)
215         return ground_gradient
216
217     def getCenterRadius(self):
218         """Return the radius of the widget circle"""
219         if self.mode == "round":
220             return self.adjdev.width() / 2.
221         else:
222             return self.adjdev.width() / 3.5
223
224     def drawText(self, painter, center, s):
225         r = self.getCenterRadius()
226         painter.save()
227         pen = QPen(Qt.white, r / 150., Qt.SolidLine)
228         brush = QBrush(Qt.white)
229         painter.setBrush(brush)
230         pen.setColor(Qt.black);
231         painter.setPen(pen)
232         font = QFont("Meiryo UI", 0, QFont.Bold)
233         font.setPointSizeF(self.FONT_SIZE * r)
234         painter.setFont(font)
235         path = QPainterPath()
236         for l in s.split("\n"):
237             path.addText(center, font, l)
238             center += QPoint(0, self.FONT_SIZE * r * 1.5)
239         bounding = path.boundingRect()
240         path.translate(-bounding.width() / 2., bounding.height() / 2.)
241         painter.drawPath(path)
242         painter.restore()
243
244     def drawHorizonRound(self, painter):
245         """Draw the horizon for round widget: the sky in blue,
246            the ground in marron."""
247
248         # set local pitch and roll
249         pitch = self.pitch
250         roll = self.roll
251         if pitch > 90.:
252             pitch = 180. - pitch
253             roll += 180.
254             if roll > 180.:
255                 roll -= 360.
256         if pitch < -90.:
257             pitch = -180. - pitch
258             roll += 180.
259             if roll > 180.:
260                 roll -= 360.
261
262         # we have to draw a partial circle delimited by its chord, define
263         # where the chord starts
264         start_angle = math.degrees(math.asin(pitch / 90.)) - roll
265         span = 2 * math.degrees(math.asin(pitch / 90.))
266
267         # draw the sky
268         painter.setBrush(self.getSkyBrush())
269         # startAngle and spanAngle must be specified in 1/16th of a degree
270         painter.drawChord(self.adjdev, 16 * start_angle, 16 * (180. - span))
271
272         # draw the ground
273         painter.setBrush(self.getGroundBrush())
274         # startAngle and spanAngle must be specified in 1/16th of a degree
275         painter.drawChord(self.adjdev, 16 * start_angle, -16 * (180. + span))
276
277     def drawHorizon(self, painter):
278         """Draw the horizon: the sky in blue, the ground in marron."""
279         painter.save()
280         painter.setBrush(self.getSkyBrush())
281         painter.drawRect(self.dev)
282         center = self.adjdev.center()
283         r = self.getCenterRadius()
284         # radius of the adjusted screen (same than r if roundWidget = True)
285         dev_r = self.adjdev.width() / 2.
286         roll = self.roll
287         if self.pitch < -90.:
288             pitch = -180 - self.pitch
289         elif self.pitch < 90.:
290             pitch = self.pitch
291         else:
292             pitch = 180 - self.pitch
293         y_off = (pitch / self.CAM_ANGLE) * dev_r
294         painter.translate(center.x(), center.y())
295         painter.rotate(roll)
296         ground_rect = QRect(0, 0, self.max_dim * 5., self.max_dim * 5.)
297         if self.pitch < 90. and self.pitch > -90.:
298             ground_rect.moveCenter(QPoint(0, -y_off + ground_rect.width()/2.))
299         else:
300             ground_rect.moveCenter(QPoint(0, y_off - ground_rect.width()/2.))
301         painter.setBrush(self.getGroundBrush())
302         painter.drawRect(ground_rect)
303         painter.restore()
304
305     def drawCenterRef(self, painter):
306         """Draw the cross on the middle of the OSD"""
307         center = self.adjdev.center()
308         r = self.getCenterRadius()
309         pen = QPen(Qt.red, r / 100., Qt.SolidLine)
310         painter.setPen(pen)
311         pt1 = QPoint(center.x() - 0.05 * r, center.y())
312         pt2 = QPoint(center.x() + 0.05 * r, center.y())
313         painter.drawLine(pt1, pt2)
314         pt1 = QPoint(center.x(), center.y() + -0.025 * r)
315         pt2 = QPoint(center.x(), center.y() + 0.025 * r)
316         painter.drawLine(pt1, pt2)
317
318     def drawPitchGraduation(self, painter):
319         """Draw the pitch graduations."""
320         # change the reference
321         painter.save()
322         center = self.adjdev.center()
323         r = self.getCenterRadius()
324         # radius of the adjusted screen (same than r if roundWidget = True)
325         dev_r = self.adjdev.width() / 2.
326         roll = self.roll
327         x_off = (self.pitch / self.CAM_ANGLE) * dev_r * math.sin(math.radians(roll))
328         y_off = (self.pitch / self.CAM_ANGLE) * dev_r * math.cos(math.radians(roll))
329         painter.translate(center.x() + x_off, center.y() - y_off)
330         painter.rotate(roll)
331
332         # set font and pen
333         pen = QPen(Qt.white, r / 100., Qt.SolidLine)
334         painter.setPen(pen)
335
336         # round to nearest angle that is a multiple of step
337         a = self.pitch / self.PITCH_REFLINES_STEP_ANGLE
338         a = round(a)
339         a = int(a * self.PITCH_REFLINES_STEP_ANGLE)
340         a -= self.PITCH_REFLINES_STEP_ANGLE * (self.PITCH_REFLINES_NUM_LINES / 2.)
341         angles = [ a + i * self.PITCH_REFLINES_STEP_ANGLE
342                    for i in range(self.PITCH_REFLINES_NUM_LINES + 1) ]
343         for a in angles:
344             # thin line
345             if int(a) % int(self.PITCH_REFLINES_BOLD_STEP_ANGLE) != 0:
346                 pen = QPen(Qt.white, r / 100., Qt.SolidLine)
347                 painter.setPen(pen)
348                 pt1 = QPoint(-0.05 * r, dev_r / self.CAM_ANGLE * a)
349                 pt2 = QPoint(0.05 * r, dev_r / self.CAM_ANGLE * a)
350                 painter.drawLine(pt1, pt2)
351                 continue
352
353             # bold line
354             pen = QPen(Qt.white, r / 50., Qt.SolidLine)
355             painter.setPen(pen)
356             pt1 = QPoint(-0.2 * r, dev_r / self.CAM_ANGLE * a)
357             pt2 = QPoint(0.2 * r, dev_r / self.CAM_ANGLE * a)
358             painter.drawLine(pt1, pt2)
359
360             # the left text
361             disp_val = -a
362             if disp_val > 90.:
363                 disp_val = 180. - disp_val
364             if disp_val < -90.:
365                 disp_val = -180. - disp_val
366             disp_val = str(int(disp_val))
367             lefttxt_pt = pt1 - QPoint(0.2 * r, 0)
368             self.drawText(painter, lefttxt_pt, disp_val)
369
370             # flip the right text
371             painter.save()
372             painter.translate(pt2 + QPoint(0.2 * r, 0))
373             painter.rotate(180.)
374             painter.translate(-pt2 - QPoint(0.2 * r, 0))
375             righttxt_pt = pt2 + QPoint(0.2 * r, 0)
376             self.drawText(painter, righttxt_pt, disp_val)
377             painter.restore()
378
379         painter.restore()
380
381     def drawOneRollGraduation(self, painter, deg, disp_text):
382         # draw the graduiation
383         r = self.getCenterRadius()
384         center = self.adjdev.center()
385         x = center.x() - math.cos(math.radians(deg)) * r
386         y = center.y() - math.sin(math.radians(deg)) * r
387         pt = QPoint(x, y)
388         path = QPainterPath()
389         path.moveTo(pt)
390         path.lineTo(center)
391         pt2 = path.pointAtPercent(0.075) # graduation len is 7.5% of the radius
392         painter.drawLine(pt, pt2)
393         # then draw the text
394         if disp_text == True:
395             pt_txt = path.pointAtPercent(0.2)
396             disp_val = deg
397             if disp_val > 90:
398                 disp_val = 180. - disp_val
399             disp_val = str(int(disp_val))
400             self.drawText(painter, pt_txt, disp_val)
401
402     def drawRollGraduation(self, painter):
403         """Draw the roll graduations."""
404         center = self.adjdev.center()
405         r = self.getCenterRadius()
406
407         # draw the red reference lines (pitch 0)
408         painter.save()
409         painter.translate(center.x(), center.y())
410         painter.rotate(self.roll)
411         pen = QPen(Qt.red, r / 100., Qt.SolidLine)
412         painter.setPen(pen)
413         pt1 = QPoint(-0.925 * r, 0)
414         pt2 = QPoint(-0.85 * r, 0)
415         painter.drawLine(pt1, pt2)
416         pt1 = QPoint(0.925 * r, 0)
417         pt2 = QPoint(0.85 * r, 0)
418         painter.drawLine(pt1, pt2)
419         painter.restore()
420
421         pen = QPen(Qt.white, r / 50., Qt.SolidLine)
422         painter.setPen(pen)
423         deg = 0
424         while deg <= 180:
425             if deg % 30 == 0:
426                 w = r / 50.
427                 disp_text = True
428             else:
429                 w = r / 100.
430                 disp_text = False
431             pen.setWidth(w)
432             painter.setPen(pen)
433             self.drawOneRollGraduation(painter, deg, disp_text)
434             deg += 10
435
436     def drawYaw(self, painter):
437         center = self.adjdev.center()
438         r = self.getCenterRadius()
439         pen = QPen(Qt.red, r / 100., Qt.SolidLine)
440         painter.setPen(pen)
441         if self.mode == "round":
442             y_txt = center.y() + r * 0.6
443             y1 = center.y() + r * 0.7
444             y2 = center.y() + r * 0.8
445             y3 = center.y() + r * 0.85
446         else:
447             y_txt = center.y() + r * 1.
448             y1 = center.y() + r * 1.1
449             y2 = center.y() + r * 1.2
450             y3 = center.y() + r * 1.25
451         pt1 = QPoint(center.x(), y2)
452         pt2 = QPoint(center.x(), y3)
453         painter.drawLine(pt1, pt2)
454
455         # round to nearest angle multiple of step
456         a = self.yaw / self.YAW_REFLINES_STEP_ANGLE
457         a = round(a)
458         a = int(a * self.YAW_REFLINES_STEP_ANGLE)
459         a -= self.YAW_REFLINES_STEP_ANGLE * (self.YAW_REFLINES_NUM_LINES / 2.)
460         angles = [ a + i * self.YAW_REFLINES_STEP_ANGLE
461                    for i in range(self.YAW_REFLINES_NUM_LINES + 1) ]
462         for a in angles:
463             # text (N, S, E, W)
464             if int(a) % 90 == 0:
465                 disp_text = True
466                 pen = QPen(Qt.white, r / 50., Qt.SolidLine)
467                 painter.setPen(pen)
468             else:
469                 disp_text = False
470                 pen = QPen(Qt.white, r / 100., Qt.SolidLine)
471                 painter.setPen(pen)
472             # the line
473             x = center.x() - ((r / (self.YAW_REFLINES_TOTAL_ANGLE / 2.)) *
474                               (self.yaw - a) * self.YAW_REFLINES_SIZE_FACTOR)
475             pt_txt = QPoint(x, y_txt)
476             pt1 = QPoint(x, y1)
477             pt2 = QPoint(x, y2)
478             painter.drawLine(pt1, pt2)
479             if disp_text == False:
480                 continue
481             disp_val = ["N", "E", "S", "W"][(int(a)/90)%4]
482             self.drawText(painter, pt_txt, disp_val)
483
484     def drawSpeed(self, painter):
485         center = self.adjdev.center()
486         r = self.getCenterRadius()
487         pen = QPen(Qt.red, r / 100., Qt.SolidLine)
488         painter.setPen(pen)
489         x1 =  center.x() - 1.5 * r
490         x2 =  center.x() - 1.6 * r
491         pt1 = QPoint(center.x() - 1.45 * r, center.y())
492         pt2 = QPoint(x2, center.y())
493         painter.drawLine(pt1, pt2)
494
495         # round to nearest angle multiple of step
496         s = self.speed / self.SPEED_REFLINES_STEP
497         s = round(s)
498         s = int(s * self.SPEED_REFLINES_STEP)
499         s -= self.SPEED_REFLINES_STEP * (self.SPEED_REFLINES_NUM_LINES / 2.)
500         speeds = [ s + i * self.SPEED_REFLINES_STEP
501                    for i in range(self.SPEED_REFLINES_NUM_LINES + 1) ]
502         for s in speeds:
503             if int(s) % int(self.SPEED_REFLINES_BOLD_STEP) == 0:
504                 disp_text = True
505                 pen = QPen(Qt.white, r / 50., Qt.SolidLine)
506                 painter.setPen(pen)
507             else:
508                 disp_text = False
509                 pen = QPen(Qt.white, r / 100., Qt.SolidLine)
510                 painter.setPen(pen)
511             # the line
512             y = center.y() + ((r / (self.SPEED_REFLINES_TOTAL/2.)) *
513                               (self.speed - s) * self.SPEED_REFLINES_SIZE_FACTOR)
514             pt_txt = QPoint(center.x() + r * -1.75, y)
515             pt1 = QPoint(x1, y)
516             pt2 = QPoint(x2, y)
517             painter.drawLine(pt1, pt2)
518             if disp_text == False:
519                 continue
520             disp_val = str(int(s))
521             self.drawText(painter, pt_txt, disp_val)
522
523     def drawAlt(self, painter):
524         center = self.adjdev.center()
525         r = self.getCenterRadius()
526         pen = QPen(Qt.red, r / 100., Qt.SolidLine)
527         painter.setPen(pen)
528         x1 =  center.x() + 1.5 * r
529         x2 =  center.x() + 1.6 * r
530         pt1 = QPoint(center.x() + 1.45 * r, center.y())
531         pt2 = QPoint(x2, center.y())
532         painter.drawLine(pt1, pt2)
533
534         # round to nearest angle multiple of step
535         a = self.alt / self.ALT_REFLINES_STEP
536         a = round(a)
537         a = int(a * self.ALT_REFLINES_STEP)
538         a -= self.ALT_REFLINES_STEP * (self.ALT_REFLINES_NUM_LINES / 2.)
539         alts = [ a + i * self.ALT_REFLINES_STEP
540                    for i in range(self.ALT_REFLINES_NUM_LINES + 1) ]
541         for a in alts:
542             if int(a) % int(self.ALT_REFLINES_BOLD_STEP) == 0:
543                 disp_text = True
544                 pen = QPen(Qt.white, r / 50., Qt.SolidLine)
545                 painter.setPen(pen)
546             else:
547                 disp_text = False
548                 pen = QPen(Qt.white, r / 100., Qt.SolidLine)
549                 painter.setPen(pen)
550             # the line
551             y = center.y() + ((r / (self.ALT_REFLINES_TOTAL / 2.)) *
552                               (self.alt - a) * self.ALT_REFLINES_SIZE_FACTOR)
553             pt_txt = QPoint(center.x() + r * 1.75, y)
554             pt1 = QPoint(x1, y)
555             pt2 = QPoint(x2, y)
556             painter.drawLine(pt1, pt2)
557             if disp_text == False:
558                 continue
559             disp_val = str(int(a))
560             self.drawText(painter, pt_txt, disp_val)
561
562     def drawReturnToHome(self, painter):
563         center = self.adjdev.center()
564         r = self.getCenterRadius()
565         dev_r = self.adjdev.width() / 2.
566         painter.save()
567         painter.translate(center.x(), center.y() - 0.9 * dev_r)
568         painter.rotate(self.yaw) # XXX
569         pen = QPen(Qt.white, r / 100., Qt.SolidLine)
570         painter.setPen(pen)
571         poly = QPolygonF()
572         poly.append(QPoint(0., -0.08 * r))
573         poly.append(QPoint(0.04 * r, 0.08 * r))
574         poly.append(QPoint(-0.04 * r, 0.08 * r))
575         poly.append(QPoint(0., -0.08 * r))
576         path = QPainterPath()
577         path.addPolygon(poly)
578         brush = QBrush(Qt.darkGray)
579         painter.setBrush(brush)
580         painter.drawPath(path)
581         painter.restore()
582
583     def drawTxtInfo(self, painter):
584         center = self.adjdev.center()
585         dev_r = self.adjdev.width() / 2.
586         r = self.getCenterRadius()
587         pen = QPen(Qt.white, r / 100., Qt.SolidLine)
588         painter.setPen(pen)
589
590         pt_txt = QPoint(center.x() + dev_r * -0.95,
591                                center.y() + dev_r * -0.95)
592         self.drawText(painter, pt_txt, self.right_txt)
593
594         pt_txt = QPoint(center.x() + dev_r * 0.95,
595                                center.y() + dev_r * -0.95)
596         self.drawText(painter, pt_txt, self.right_txt)
597
598     def setPitch(self, pitch):
599         """set the pitch in degrees, between -180 and 180"""
600         pitch = pitch % 360.
601         if pitch > 180.:
602             pitch -= 360.
603         self.user_pitch = pitch
604
605     def setRoll(self, roll):
606         """set the roll in degrees, between -180 and 180"""
607         roll = roll % 360.
608         if roll > 180.:
609             roll -= 360.
610         self.user_roll = roll
611
612     def setYaw(self, yaw):
613         """set the yaw in degrees, between 0 and 360"""
614         yaw = yaw % 360.
615         self.user_yaw = yaw
616
617     def setSpeed(self, speed):
618         """set the speed"""
619         self.user_speed = speed
620
621     def setAlt(self, alt):
622         """set the alt"""
623         self.user_alt = alt
624
625     def setReturnToHomeAngle(self, angle):
626         """set the left text"""
627         self.user_rthome = angle
628
629     def setLeftTxt(self, txt):
630         """set the right text"""
631         self.left_txt = txt
632
633     def setRightTxt(self, txt):
634         """set the left text"""
635         self.right_txt = txt
636
637 class OSDGraphicsView(QGraphicsView):
638     def __init__(self, parent=None):
639         super(OSDGraphicsView, self).__init__(parent)
640         self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
641         self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
642         self.setMinimumSize(200, 150)
643     def resizeEvent(self, event):
644         # use item 0 to resize automatically
645         self.fitInView(self.items()[0], Qt.KeepAspectRatio)
646         super(OSDGraphicsView, self).resizeEvent(event)
647
648 class Ui_OSD(QMainWindow):
649     def __init__(self, parent = None, mode = "camera", filename = None):
650         super(Ui_OSD, self).__init__(parent)
651         self.ui = qtosd_ui.Ui_MainWindow()
652         self.ui.setupUi(self)
653
654         self.mode = mode
655         self.filename = filename
656
657         self.osd = OSDWidget(mode = self.mode,
658                              filename = self.filename)
659         self.osd.setObjectName("osd")
660         self.ui.pitchSlider.valueChanged[int].connect(self.changePitch)
661         self.ui.rollSlider.valueChanged[int].connect(self.changeRoll)
662         self.ui.yawSlider.valueChanged[int].connect(self.changeYaw)
663         self.ui.actionExit.triggered.connect(self.close)
664
665         self.scene = QGraphicsScene(self)
666         self.graphicsView = OSDGraphicsView(self.scene)
667
668         if self.mode == "camera":
669             self.videoItem = QGraphicsVideoItem()
670             self.videoItem.setSize(QSizeF(640, 480)) # XXX
671             self.scene.addItem(self.videoItem)
672
673             x = self.videoItem.boundingRect().width() / 2.0
674             y = self.videoItem.boundingRect().height() / 2.0
675             #self.videoItem.setTransform(
676             #        QTransform().translate(x, y).rotate(70).translate(-x, -y))
677
678             self.initCamera()
679
680         self.scene.addWidget(self.osd)
681         self.ui.gridLayout.addWidget(self.graphicsView, 0, 1)
682
683     def initCamera(self):
684         # find camera devices and add them in the menu
685         cameraDevice = QByteArray()
686         videoDevicesGroup = QActionGroup(self)
687         videoDevicesGroup.setExclusive(True)
688         for deviceName in QCamera.availableDevices():
689             description = QCamera.deviceDescription(deviceName)
690             videoDeviceAction = QAction(description, videoDevicesGroup)
691             videoDeviceAction.setCheckable(True)
692             videoDeviceAction.setData(deviceName)
693             if cameraDevice.isEmpty():
694                 cameraDevice = deviceName
695                 videoDeviceAction.setChecked(True)
696             self.ui.menuDevices.addAction(videoDeviceAction)
697         videoDevicesGroup.triggered.connect(self.updateCameraDevice)
698         # select the first camera
699         self.setCamera(cameraDevice)
700
701     def setCamera(self, cameraDevice):
702         if cameraDevice.isEmpty():
703             self.camera = QCamera()
704         else:
705             self.camera = QCamera(cameraDevice)
706
707         self.camera.stateChanged.connect(self.updateCameraState)
708         self.camera.error.connect(self.displayCameraError)
709
710         #self.ui.exposureCompensation.valueChanged.connect(
711         #        self.setExposureCompensation)
712
713         self.camera.setCaptureMode(QCamera.CaptureViewfinder)
714         self.camera.setViewfinder(self.videoItem)
715         self.updateCameraState(self.camera.state())
716         self.camera.start()
717
718     #XXX stop camera? remove from scene?
719
720     def updateCameraDevice(self, action):
721         print "updateCameraDevice"
722         self.setCamera(action.data())
723
724     def updateCameraState(self, state):
725         print "updateCameraState %s"%(str(state))
726
727     def displayCameraError(self):
728         print "displayCameraError"
729         QMessageBox.warning(self, "Camera error", self.camera.errorString())
730
731     def keyPressEvent(self, event):
732         key = event.key()
733         if key == Qt.Key_J:
734             self.osd.setRoll(self.osd.user_roll + 2)
735             event.accept()
736         elif key == Qt.Key_L:
737             self.osd.setRoll(self.osd.user_roll - 2)
738             event.accept()
739         elif key == Qt.Key_I:
740             self.osd.setPitch(self.osd.user_pitch + 2)
741             event.accept()
742         elif key == Qt.Key_K:
743             self.osd.setPitch(self.osd.user_pitch - 2)
744             event.accept()
745         elif key == Qt.Key_Q:
746             self.close()
747         elif not event.isAutoRepeat():
748             if key == Qt.Key_CameraFocus:
749                 self.camera.searchAndLock()
750                 event.accept()
751             else:
752                 super(Ui_OSD, self).keyPressEvent(event)
753
754     def keyReleaseEvent(self, event):
755         key = event.key()
756         if event.isAutoRepeat():
757             return
758         if key == Qt.Key_CameraFocus:
759             self.camera.unlock()
760         else:
761             super(Ui_OSD, self).keyReleaseEvent(event)
762
763     def retranslateUi(self, MainWindow):
764         MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow", None))
765
766     @pyqtSlot(int, name = "changePitch")
767     def changePitch(self, value):
768         self.osd.setPitch(value)
769
770     @pyqtSlot(int, name = "changeRoll")
771     def changeRoll(self, value):
772         self.osd.setRoll(value)
773
774     @pyqtSlot(int, name = "changeYaw")
775     def changeYaw(self, value):
776         self.osd.setYaw(value)
777
778 if __name__ == "__main__":
779     import sys
780
781     parser = argparse.ArgumentParser(description='OSD written in Qt.')
782     parser.add_argument('--mode', '-m', action="store",
783                         choices=["round", "rectangle", "camera"],
784                         help='display the widget as a round attitude meter')
785     parser.add_argument('--filename', '-f',
786                         help='specify the log file')
787     args = parser.parse_args()
788
789     app = QApplication(sys.argv)
790     ui = Ui_OSD(filename = args.filename, mode = args.mode)
791     ui.show()
792     sys.exit(app.exec_())
793