En esta ocasión me basaré en la clase QwtDial para crear algo parecido a un indicador de actitud.
De la guía de usuario de qwt:
“La clase QwtDial proporciona un control de intervalo redondeado. QwtDial esta diseñado para ser una clase base para los indicadores (dial) como velocímetros, brújulas, relojes:
Un dial contiene una escala y una aguja indicando el valor actual del dual. Dependiendo del Modo uno de ellos es fijo y el otro esta rotando…”
De la descripción anterior podemos sacar la conclusión de que un dial se compone de dos elementos básicos: una escala y una aguja. Estos elementos se dibujan “a mano”, es decir, las líneas que componen el dial, los números, las figuras, los colores. No se puede escoger directamente que indicador queremos, sino que tenemos que dibujarlo. Esto es algo complicado pues hay que tener los conceptos básicos de geometría presentes y dominarlos.
La otra opción es tomar las clases de los ejemplos incluidos y ajustarlas a nuestras necesidades, que es lo que hice en este caso. Copié a un archivo clases.py las clases que necesitaba para el indicador de actitud (el tercer indicador de la imagen anterior) que son: EnumList: Función que es llamada dentro de las clases necesarias. AttitudeIndicator: Esta clase dibuja el círculo y las escalas del indicador. AttitudeIndicatorNeedle: Esta clase dibuja la aguja central, que para el indicador de actitud, es un triangulo rojo.
Interfaz gráfica
La interfaz gráfica contendrá un indicador de actitud y una imagen que indicará la condición de caída libre. Un vertical layout contendrá al indicador de actitud. Un frame contendrá un labelFree fall, un label con la imagen y el indicador de actitud. Todo esta hecho en qtdesigner:
Código en Python
Clases.py
'''
Las siguientes lineas son un copy/paste del ejemplo DialDemo.py de las librerias pyqwt.
Lo que hice fue copiar las clases AttitudeIndicator, AttitudeindIndicatorNeedle y la funcion enumList.
Con pequenias modificaciones.
'''
import math
from PyQt4 import Qt
import PyQt4.Qwt5 as Qwt
def enumList(enum, sentinel):
'''
'''
return [enum(i) for i in range(sentinel)]
colorGroupList = enumList(
Qt.QPalette.ColorGroup, Qt.QPalette.NColorGroups)
colorRoleList = enumList(
Qt.QPalette.ColorRole, Qt.QPalette.NColorRoles)
handList = enumList(
Qwt.QwtAnalogClock.Hand, Qwt.QwtAnalogClock.NHands)
class AttitudeIndicator(Qwt.QwtDial):
def __init__(self, *args):
Qwt.QwtDial.__init__(self, *args)
self.__gradient = 0.0
self.setMode(Qwt.QwtDial.RotateScale)
self.setWrapping(True)
self.setOrigin(270.0)
self.setScaleOptions(Qwt.QwtDial.ScaleTicks)
self.setScale(0, 0, 30.0)
self.setNeedle(AttitudeIndicatorNeedle(
self.palette().color(Qt.QPalette.Text)))
# __init__()
def angle(self):
return self.value()
# angle()
def setAngle(self, angle):
self.setValue(angle)
# setAngle()
def gradient(self):
return self.__gradient
# gradient()
def setGradient(self, gradient):
self.__gradient = gradient
# setGradient()
def keyPressEvent(self, event):
if event.key() == Qt.Qt.Key_Plus:
self.setGradient(self.gradient() + 0.05)
elif event.key() == Qt.Qt.Key_Minus:
self.setGradient(self.gradient() - 0.05)
else:
Qwt.QwtDial.keyPressEvent(self, event)
# keyPressEvent()
def drawScale(self, painter, center, radius, origin, minArc, maxArc):
dir = (360.0 - origin) * math.pi / 180.0
offset = 4
p0 = Qwt.qwtPolar2Pos(center, offset, dir + math.pi)
w = self.contentsRect().width()
# clip region to swallow 180 - 360 degrees
pa = []
pa.append(Qwt.qwtPolar2Pos(p0, w, dir - math.pi/2))
pa.append(Qwt.qwtPolar2Pos(pa[-1], 2 * w, dir + math.pi/2))
pa.append(Qwt.qwtPolar2Pos(pa[-1], w, dir))
pa.append(Qwt.qwtPolar2Pos(pa[-1], 2 * w, dir - math.pi/2))
painter.save()
#setclipregion asigna una area de dibujado, lo que este fuera de esta area no se dibuja , si es un circulo, todo lo que este
#fuera del circulo no se dibujara
painter.setClipRegion(Qt.QRegion(Qt.QPolygon(pa)))
Qwt.QwtDial.drawScale(
self, painter, center, radius, origin, minArc, maxArc)
painter.restore()
# drawScale()
def drawScaleContents(self, painter, center, radius):
#para entenderlo mejor chord de un circle - http://www.mathwarehouse.com/geometry/circle/chord-of-circle.php
#que hace drawchordhttp://doc.trolltech.com/4.6/qpainter.html#drawChord
dir = 360 - int(round(self.origin() - self.value()))
arc = 90 + int(round(self.gradient() * 90))
skyColor = Qt.QColor(38, 151, 221)
painter.save()
painter.setBrush(skyColor)
#lo siguiente a sido modificado para asemejar el widget a un indicador de altitud real
#al cambiar de qrect a coordenas x,y,width,height puedo agregarle es espacio perdido por las lineas de medida de los angulos
rectQt = self.scaleContentsRect()
rect = rectQt.getRect()
painter.drawChord(
rect[0]-10, rect[1]-10, rect[2]+20, rect[3]+20, (dir - arc)*16, 2*arc*16)
painter.restore()
# drawScaleContents()
# class AttitudeIndicator
class AttitudeIndicatorNeedle(Qwt.QwtDialNeedle):
def __init__(self, color):
Qwt.QwtDialNeedle.__init__(self)
palette = Qt.QPalette()
for colourGroup in colorGroupList:
palette.setColor(colourGroup, Qt.QPalette.Text, color)
self.setPalette(palette)
# __init__()
def draw(self, painter, center, length, direction, cg):
direction *= math.pi / 180.0
triangleSize = int(round(length * 0.1))
painter.save()
p0 = Qt.QPoint(center.x() + 1, center.y() + 1)
p1 = Qwt.qwtPolar2Pos(p0, length - 2 * triangleSize - 2, direction)
pa = Qt.QPolygon([
Qwt.qwtPolar2Pos(p1, 2 * triangleSize, direction),
Qwt.qwtPolar2Pos(p1, triangleSize, direction + math.pi/2),
Qwt.qwtPolar2Pos(p1, triangleSize, direction - math.pi/2),
])
color = self.palette().color(cg, Qt.QPalette.Text)
painter.setBrush(color)
painter.drawPolygon(pa)
painter.setPen(Qt.QPen(color, 3))
painter.drawLine(
Qwt.qwtPolar2Pos(p0, length - 2, direction + math.pi/2),
Qwt.qwtPolar2Pos(p0, length - 2, direction - math.pi/2))
painter.restore()
testanalogdial.py
import sys, time , serial #, winsound
import math
from PyQt4 import QtCore, QtGui
from Ui_testanalogdialui import Ui_MainWindow
import PyQt4.Qwt5 as Qwt
import clases
class testAnalogDial(QtGui.QMainWindow):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
#para crear los beeps en linux
#open('/dev/dsp','w').write(''.join(chr(128 * (1 + math.sin(math.pi * 440 * i / 100.0))) for i in xrange(1000)))
#self.ptoSerial = serial.Serial('/dev/ttyUSB0', 9600, timeout = 1)
self.ptoSerial = serial.Serial('COM3', 9600, timeout = 1)
self.timer = QtCore.QTimer()
self.connect(self.timer, QtCore.SIGNAL("timeout()"), self.timerEvent)
#se llama a la funcion que dibuja el dial incluida en el archivo clases
self.ind = clases.AttitudeIndicator(self)
#se modifican las propiedades
#si es readonly no acepta el mouse ni el keypress
self.ind.setReadOnly(True)
self.ind.scaleDraw().setPenWidth(3)
self.ind.setLineWidth(4)
self.ind.setFrameShadow(Qwt.QwtDial.Sunken)
#se dibuja el dial en el widget
self.ui.vlDial.addWidget(self.ind)
#el gradient lo que hace es indicar el nivel de "cielo" si es uno es que la punta esta hasta arriba
#si es -1 es que la punta apunta totalmenta hacia tierra
self.ind.setGradient(0)
self.ind.setAngle(50)
self.timer.start(10)
def timerEvent(self):
angX, gradY, freeFall = self.decodeData()
self.ind.setAngle(angX)
self.ind.setGradient(gradY)
if freeFall == True:
self.ui.lblImg.setPixmap(QtGui.QPixmap(":/img/imagenes/circle_red.png"))
#beeps
#winsound.Beep(1000, 50)
#winsound.Beep(1000, 50)
#winsound.Beep(1000, 50)
#open('/dev/dsp','w').write(''.join(chr(128 * (1 + math.sin(math.pi * 440 * i / 100.0))) for i in xrange(1000)))
#open('/dev/dsp','w').write(''.join(chr(128 * (1 + math.sin(math.pi * 440 * i / 100.0))) for i in xrange(1000)))
#open('/dev/dsp','w').write(''.join(chr(128 * (1 + math.sin(math.pi * 440 * i / 100.0))) for i in xrange(1000)))
else:
self.ui.lblImg.setPixmap(QtGui.QPixmap(":/img/imagenes/circle_grey.png"))
def decodeData(self):
data = self.ptoSerial.readline()
if (len(data) <7):
print "Error"
return 0, 0, False
x = ord(data[1])
y = ord(data[3])
z = ord(data[5])
#el minimo es 71 y el maximo es 195 . 195-71 = 123. 123 / 180 grados = 1.463 por grado
#en reposo esta en 143 pero debe de estar en 123, el problema es que esta mal soldado, no estan los pines a 90 grados
x = x - 20
angX = (1.463 * x) -180
#par y aproximadamente el minimo es de 65 y el maximo es de 191. 191-65 = 126. va de -1.00 a 1.00.
#65 *1.587 aprox 103 minimo equivale al -1.00 (hacia el cielo)
#191 * 1.587 aprox 303 maximo equivale al 1.00 (hacia la tierra)
#diferencia entre 303-103 es igual a 200
#para que la punta cuando suba apunte al cielo
gradY = ((1.587 * y) - 200) / 100
#el eje z es el detector de freefall si es mayor a 200 entonces esta en caida libre
if z > 200 :
freeFall = True
else:
freeFall = False
return angX, -gradY, freeFall
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
myapp = testAnalogDial()
myapp.show()
sys.exit(app.exec_())
Primero creamos el objeto AttitudeIndicator que dibuja el dial con las escalas y la aguja definidas en clases.py. Al ponerlo a solo lectura, no acepta los eventos del teclado y del mouse. El gradiente es la cantidad de cielo o tierra que muestra, si es –1 apunta totalmente hacia la tierra. Ajustando el ángulo, cambiamos el ángulo de la línea horizontal que parte en dos al indicador.
Decodificando los datos
def decodeData(self):
data = self.ptoSerial.readline()
if (len(data) <7):
print "Error"
return 0, 0, False
x = ord(data[1])
y = ord(data[3])
z = ord(data[5])
x = x - 20
angX = (1.463 * x) -180
gradY = ((1.587 * y) - 200) / 100
if z > 200 :
freeFall = True
else:
freeFall = False
return angX, -gradY, freeFall
El formato de los datos que se esperan, es el mismo que el de ejemplos anteriores. El firmware es el mismo. La variable angX define el ángulo al que se mueve la línea horizontal (avion miniatura no dibujado para mantenerlo simple). La fórmula se obtiene con una regla de tres:
Los valores de aceleración se almacenan en variables de 8 bits (0-255).
El mínimo de aceleración normal (sin que haya un golpe fuerte, solo moviendo el acelerómetro) es de 71.
El máximo de aceleración normal es de 195.
Para saber el valor de cada grado se saca la diferencia entre el máximo y el mínimo y se divide entre 180, que es el rango de grados de –X (-90° a 0°) a X (0° a 90°). El resultado es de 1.463. Un grado equivale a 1.463 unidades de aceleración (en este caso inclinación).
La variable gradY define la altura de la línea horizontal (horizonte de referencia) que divide al cielo de la tierra. La formula se obtiene de la siguiente manera:
El valor mínimo de aceleración normal es de 65.
El valor máximo de aceleración normal es de 191.
La diferencia entre el máximo y el mínimo es de 126.
Los valores posibles de gradY van de –1.00 a 1.00.
Suponiendo que de –1.00 a 1.00 hay 200 valores. Dividiendo 200/126 tenemos 1.587.
El –gradY es por que para invertir el movimiento del horizonte.
Ahora ¿por que el –180 del primero y el –200 /100 del segundo?. Eso lo saque a prueba y error.
Para saber si el sensor esta en caída libre, solo fijamos un valor umbral para el eje z. En este caso 200.
Al ejecutar el programa obtenemos la siguiente ventana:
0 comentarios:
Publicar un comentario