Commit c75ddf82 authored by Maxime Perrotin's avatar Maxime Perrotin

TASTE Speedometer widget

parents
all: compile-all
compile-all:
@pyside-rcc speedometer.qrc -o speedometer_resources.py
install: compile-all
@mkdir -p speedometer
@for f in speedometer.py speedometer_resources.py __init__.py; \
do echo Installing $$f && cp $$f speedometer ; \
done
@python setup.py install --record install.record
clean:
@rm *.pyc
.PHONY: all compile-all install clean
TASTE speedometer widget - can also work as a standalone application
Install it using:
$ sudo make install
or
$ sudo python setup.py install
Displays a "speedometer" which value can be dynamically updated (values taken from a file or stdin)
To be used for streaming with the TASTE Peekpoke component
Requires python-pyside (apt-get install python-pyside)
Syntax:
speedometer <window name> <rangeMin> <rangeMax>
The range is optional, if not set the default range is [0, 120]
LICENCE:
This tool is licensed under the BSD license
It contains parts of code originating from the Dial example that is
distributed with the Qt/QML Toolkit from Nokia.
Modifications done for TASTE are the following:
- background image was modified (no more "hardcoded" range values in the drawing)
- Removal of the slider object
- Support for multiple ranges with automatic update of the picture
- Input is read from stdin
- Fix of the angle calculation which was incorrect in the original code
The original BSD license as it appears in the original QML code is applicable,
as follows:
************************************************************************
** You may use this file under the terms of the BSD license as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
** * Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** * Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in
** the documentation and/or other materials provided with the
** distribution.
** * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
** the names of its contributors may be used to endorse or promote
** products derived from this software without specific prior written
** permission.
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
** $QT_END_LICENSE$
**
****************************************************************************/
TASTE speedometer widget - can also work as a standalone application
Install it using:
$ sudo make install
or
$ sudo python setup.py install
Displays a "speedometer" which value can be dynamically updated (values taken from a file or stdin)
To be used for streaming with the TASTE Peekpoke component
Requires python-pyside (apt-get install python-pyside)
Syntax:
speedometer <window name> <rangeMin> <rangeMax>
The range is optional, if not set the default range is [0, 120]
LICENCE:
This tool is licensed under the BSD license
It contains parts of code originating from the Dial example that is
distributed with the Qt/QML Toolkit from Nokia.
Modifications done for TASTE are the following:
- background image was modified (no more "hardcoded" range values in the drawing)
- Removal of the slider object
- Support for multiple ranges with automatic update of the picture
- Input is read from stdin
- Fix of the angle calculation which was incorrect in the original code
The original BSD license as it appears in the original QML code is applicable,
as follows:
************************************************************************
** You may use this file under the terms of the BSD license as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
** * Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** * Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in
** the documentation and/or other materials provided with the
** distribution.
** * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
** the names of its contributors may be used to endorse or promote
** products derived from this software without specific prior written
** permission.
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
** $QT_END_LICENSE$
**
****************************************************************************/
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
TASTE Speedometer - a simple widget allowing to display
a manometer/speedometer, with configurable range, and
remote feeding (from pipes or from a python application)
Contain both standalone application (for pipe control) and
Pyside widget.
"""
from speedometer import Speedometer, __version__
/****************************************************************************
**
** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
** All rights reserved.
** Contact: Nokia Corporation (qt-info@nokia.com)
**
** This file is part of the examples of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** You may use this file under the terms of the BSD license as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
** * Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** * Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in
** the documentation and/or other materials provided with the
** distribution.
** * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
** the names of its contributors may be used to endorse or promote
** products derived from this software without specific prior written
** permission.
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
** $QT_END_LICENSE$
**
****************************************************************************/
import QtQuick 1.0
import Taste 1.0
Item {
id: root
objectName: "tasteDial"
property real value : 0
property real minR : 0.0
property real maxR : 0.0
property real mapF : 0.0
property string background : "qrc:///content/background.png"
property string warning: "qrc:///content/warning.png"
width: 210; height: 210
Image { source: root.background }
//! [taste meter range]
MeterRange {
id: range
minR : root.minR
maxR : root.maxR
mapF : root.mapF
anchors.centerIn: parent
}
//! [taste meter range]
//! [needle_shadow]
Image {
x: 96
y: 35
source: "qrc:///content/needle_shadow.png"
transform: Rotation {
origin.x: 9; origin.y: 67
angle: needleRotation.angle
}
}
//! [needle_shadow]
//! [needle]
Image {
id: needle
x: 98; y: 33
smooth: true
source: "qrc:///content/needle.png"
transform: Rotation {
id: needleRotation
origin.x: 5; origin.y: 65
//! [needle angle]
// We rotate on three quarters of a circle (270 degrees: from -135 to 135 degrees)
// The original range is 0-120, so we must multiply by 2.25 to map to 0-270
// Note: I had to move the computation of the mapFactor to the Python code because
// it failed to compute directly here (the 120.0/.. gave no result) - prossibly a QML bug?
angle: Math.min(Math.max(-135, root.mapF*(root.value-root.minR)*2.25 - 135), 135);
Behavior on angle {
SpringAnimation {
spring: 1.4
damping: .15
}
}
//! [needle angle]
}
}
//! [needle]
//! [overlay]
Image { x: 21; y: 18; source: "qrc:///content/overlay.png" }
//! [overlay]
//! [range input]
Grid {
columns: 5
spacing: 8
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.margins: -10
Text {color: "white"; text: "Range:"}
TextInput {
id: minRangeEdit
selectByMouse: true
validator: DoubleValidator {}
text : root.minR
onAccepted: {
root.minR = text
root.mapF = range.mapF
dial.background = ""; dial.background = "qrc:///content/background.png"
}
}
Text{color: "white"; text: "-"}
TextInput {
id: maxRangeEdit
selectByMouse: true
validator: DoubleValidator {}
text : root.maxR
onAccepted: {
root.maxR = text
root.mapF = range.mapF
dial.background = ""; dial.background = "qrc:///content/background.png"
}
}
Image {
width:20; height:20; fillMode: Image.PreserveAspectFit; source: "qrc:///content/tick.png"
MouseArea {
anchors.fill : parent
onClicked: {
root.minR = minRangeEdit.text;
root.maxR = maxRangeEdit.text;
root.mapF = range.mapF
dial.background = ""; dial.background = "qrc:///content/background.png"
}
}
}
}
//! [range input]
//! [blinking warning icon]
Image {
id: out_of_range
anchors.top: parent.top
anchors.right: parent.right
anchors.margins:-10
source: ""
}
Timer {
id: warning_timer
interval:500; running: root.value>Math.max(root.maxR, root.minR) || root.value < Math.min(root.minR, root.maxR) ; repeat: true
onTriggered: {if (out_of_range.source == "") out_of_range.source = root.warning; else out_of_range.source=""; }
onRunningChanged: {out_of_range.source = "";}
}
//! [blinking warning icon]
}
/****************************************************************************
**
** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
** All rights reserved.
** Contact: Nokia Corporation (qt-info@nokia.com)
**
** This file is part of the examples of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:BSD$
** You may use this file under the terms of the BSD license as follows:
**
** "Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions are
** met:
** * Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** * Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in
** the documentation and/or other materials provided with the
** distribution.
** * Neither the name of Nokia Corporation and its Subsidiary(-ies) nor
** the names of its contributors may be used to endorse or promote
** products derived from this software without specific prior written
** permission.
**
** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
** $QT_END_LICENSE$
**
****************************************************************************/
//! [imports]
import QtQuick 1.0
import "content"
//! [imports]
//! [0]
Rectangle {
color: "#545454"
// border.color: "white"
// border.width: 5
// radius : 20
width: 250; height: 250
// Dial is defined in contents/dial.qml
Dial {
id: dial
anchors.centerIn: parent
}
// updateVal can be called by the Python code
function updateValue(val) {
dial.value = val
}
function setMinRange(val) {
dial.minR = val
}
function setMaxRange(val) {
dial.maxR = val
}
function setMapFactor(val) {
dial.mapF = val
}
}
//! [0]
/usr/local/lib/python2.7/dist-packages/speedometer-1.0-py2.7.egg
/usr/local/bin/speedometer
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
Setup file for Linux distribution
Usage: python setup.py sdist --> to create a tarball
python setup.py install --> to install in python directory
'''
# from distutils.core import setup
from setuptools import setup, find_packages
import speedometer
setup(
name='speedometer',
version=speedometer.__version__,
packages=find_packages(),
author='Maxime Perrotin',
author_email='maxime.perrotin@esa.int',
description='A simple, remotely feedable speedometer',
long_description=open('README').read(),
include_package_data=True,
url='http://taste.tuxfamily.org',
classifiers=[
'Programming Language :: Python',
'License :: OSI Approved :: BSD',
'Operating System :: OS Independent',
'Programming Language :: Python :: 2.7'
],
entry_points={
'console_scripts': [
'speedometer = speedometer.speedometer:speedometer_cli'
]
},
)
#!/usr/bin/python
# -*- coding: utf-8 -*-
''' Speedometer widget
(c) European Space Agency
Maxime Perrotin (2012)
'''
import sys
import signal
from PySide.QtCore import(Property, QObject, Signal, QThread, QUrl, Qt, QRectF,
QPointF, QPoint)
from PySide.QtGui import(QPen, QPainterPath, QPainter, QFont, QApplication,
QGraphicsItem, QColor, QFontMetrics, QDockWidget)
#from PySide.QtUiTools import *
from PySide.QtDeclarative import (QDeclarativeItem, QDeclarativeView,
qmlRegisterType)
import speedometer_resources
__author__ = "Maxime Perrotin"
__license__ = "BSD"
__version__ = "1.0"
__url__ = "http://taste.tuxfamily.org"
__all__ = ['Speedometer']
class MeterRange(QDeclarativeItem):
''' Class MeterRange is reponsible for drawing the cicle part of the meter
(including all the pins and the text around it)
'''
def __init__(self, parent=None):
QDeclarativeItem.__init__(self, parent)
# need to disable this flag to draw inside a QDeclarativeItem
self.setFlag(QGraphicsItem.ItemHasNoContents, False)
self._name = u''
# Create an circle path
# (not possible in QML - it supports only Bezier curves)
self.path = QPainterPath()
# Define a rectangle that contains the circle, and move
# to the starting point of the curve at angle -135 (or 225) degrees
# This corresponds to the point of the lower range.
self.path.arcMoveTo(QRectF(-67, -70, 128, 128), -135)
self.path.arcTo(QRectF(-67, -70, 128, 128), -135, -360)
self.min_range = 0
self.max_range = 0
self.map_factor = 0
self.text = []
def paint(self, painter, options, widget):
_, _ = options, widget
pen = QPen(QColor('grey'), 1)
painter.setPen(pen)
painter.setRenderHints(QPainter.Antialiasing, True)
font = QFont()
font.setPointSize(8)
font.setStretch(QFont.Condensed)
painter.setFont(font)
path_len = self.path.length()
start_point = 6
# Draw the graduation needles
for i in xrange(121):
percent = i * (0.75 / 120.)
if (i % 10) == 0:
painter.setPen(QColor('black'))
start_point = 3
elif (i % 5) == 0:
start_point = 4
else:
painter.setPen(QColor('grey'))
start_point = 6
point = QPointF(self.path.pointAtPercent(percent))
angle = -self.path.angleAtPercent(percent)
painter.save()
painter.translate(point)
painter.rotate(angle)
painter.drawLine(0, start_point, 0, 10)
painter.restore()
painter.setPen(QColor('black'))
skip_next = False
enable_skip = False
# Check if numbers may overlap
# display every other one in that case
for elem in self.text:
if len(elem) > 3:
enable_skip = True
for i in xrange(13):
pos = (i * (0.75 / 12.)) * path_len - \
QFontMetrics(font).width(self.text[i]) / 2
if pos < 0:
pos = 0
percent = pos / path_len
if skip_next == True:
skip_next = False
continue
for j in xrange(0, len(self.text[i])):
if percent > 1:
break
point = QPointF(self.path.pointAtPercent(percent))
angle = -self.path.angleAtPercent(percent)
painter.save()
painter.translate(point)
painter.rotate(angle)
painter.drawText(QPoint(0, 0), self.text[i][j])
painter.restore()
pos = percent * path_len + \
QFontMetrics(font).width(self.text[i][j])
percent = pos / path_len
if enable_skip == True:
skip_next = True
def update_range(self):
# Just ignore absurd range:
if self.min_range == self.max_range:
return
# The angle of the main needle is calculated based on a 0-120 range
# Mapping it to the current range
self.setMapFactor(120.0 / (float(self.max_range) -
float(self.min_range)))
range_step = (self.max_range - self.min_range) / 12.
self.text = []
# Format the number (keep at most 2 decimals)
for i in xrange(0, 13):
realValue = self.min_range + float(i) * range_step
if (float.is_integer(range_step) \
and float.is_integer(self.min_range) \
and float.is_integer(self.max_range)) \
or float.is_integer(float(realValue)):
val = "%d" % int(realValue)
else:
val = "%.2f" % realValue
self.text.append(val)
def get_min_range(self):
return self.min_range
def get_max_range(self):
return self.max_range
def set_min_range(self, val):
self.min_range = val
self.update_range()
def set_max_range(self, val):
self.max_range = val
self.update_range()
def getMapFactor(self):
return self.map_factor
def setMapFactor(self, val):
self.map_factor = val
minR = Property(float, get_min_range, set_min_range)
maxR = Property(float, get_max_range, set_max_range)
mapF = Property(float, getMapFactor, setMapFactor)
# Our main window
class TASTE_FancyDisplay(QDeclarativeView):
def __init__(self, parent=None, title='Taste-o-meter'):
super(TASTE_FancyDisplay, self).__init__(parent)
self.setWindowTitle(title)
# Renders 'dialcontrol.qml'
#self.setSource(QUrl.fromLocalFile('dialcontrol.qml'))
self.setSource(QUrl('qrc:/dialcontrol.qml'))
# QML resizes to main window
self.setResizeMode(QDeclarativeView.SizeRootObjectToView)
self.running = True
def closeEvent(self, event):
''' Detect a Meter window closed event '''
self.running = False
class MySignal(QObject):
sig = Signal(str)
class MyThread(QThread):
''' Thread used to read inputs '''
def __init__(self, parent=None):
QThread.__init__(self, parent)
self.signal = MySignal()
def run(self):
while True:
# read from stdin without any buffering
line = sys.stdin.readline()
if len(line) == 0:
break
elif line[0] == 'q':
self.quit()
else:
# emit input
self.signal.sig.emit(line)
class Speedometer(object):
def __init__(self, parent=None, title='TASTE-o-meter',
min_range=0, max_range=120, geom=None):
''' Create a new Speedometer window '''
# Map the MeterRange class to a new QML Control
qmlRegisterType(MeterRange, 'Taste', 1, 0, 'MeterRange')
self.window = TASTE_FancyDisplay(title=title)
# Check if user defined his own geometry parameters
# (WIDTHxHEIGHT+XOFS+YOFS)
if geom != None:
self.geom = geom.replace('x', '+').split('+')
self.window.move(int(geom[2]), int(geom[3]))
self.window.resize(int(geom[0]), int(geom[1]))
# Get QML root object to access its slots
self.root = self.window.rootObject()
# Set Min and Max range values
self.root.setMinRange(min_range)
self.root.setMaxRange(max_range)
self.root.setMapFactor(120.0 / (float(max_range) - float(min_range)))