eric6/MicroPython/MicroPythonGraphWidget.py

Tue, 16 Jul 2019 20:12:53 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Tue, 16 Jul 2019 20:12:53 +0200
branch
micropython
changeset 7065
e3d04faced34
child 7066
e3d034e65afc
permissions
-rw-r--r--

Continued implementing the MicroPython support.

# -*- coding: utf-8 -*-

# Copyright (c) 2019 Detlev Offenbach <detlev@die-offenbachs.de>
#

"""
Module implementing the MicroPython graph widget.
"""

from __future__ import unicode_literals

from collections import deque
import bisect

from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import (
    QWidget, QHBoxLayout, QVBoxLayout, QToolButton, QSizePolicy, QSpacerItem
)
from PyQt5.QtChart import QChartView, QChart, QLineSeries, QValueAxis

import UI.PixmapCache


class MicroPythonGraphWidget(QWidget):
    """
    Class implementing the MicroPython graph widget.
    
    @signal dataFlood emitted to indicate, that too much data is received
    """
    dataFlood = pyqtSignal()
    
    def __init__(self, parent=None):
        """
        Constructor
        
        @param parent reference to the parent widget
        @type QWidget
        """
        super(MicroPythonGraphWidget, self).__init__(parent)
        
        self.__layout = QHBoxLayout()
        self.__layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(self.__layout)
        
        self.__chartView = QChartView(self)
        self.__chartView.setSizePolicy(
            QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.__layout.addWidget(self.__chartView)
        
        self.__buttonsLayout = QVBoxLayout()
        self.__buttonsLayout.setContentsMargins(0, 0, 0, 0)
        self.__layout.addLayout(self.__buttonsLayout)
        
        self.__saveButton = QToolButton(self)
        self.__saveButton.setIcon(UI.PixmapCache.getIcon("fileSave"))
        self.__saveButton.setToolTip(self.tr("Press to save the raw data"))
        self.__saveButton.clicked.connect(self.on_saveButton_clicked)
        self.__buttonsLayout.addWidget(self.__saveButton)
        
        spacerItem = QSpacerItem(20, 20, QSizePolicy.Minimum,
                                 QSizePolicy.Expanding)
        self.__buttonsLayout.addItem(spacerItem)
        
        # holds the data to be checked for plotable data
        self.__inputBuffer = []
        # holds the raw data
        self.__rawData = []
        
        self.__maxX = 100
        self.__maxY = 1000
        self.__flooded = False  # flag indicating a data flood
        
        self.__data = [deque([0] * self.__maxX)]
        self.__series = [QLineSeries()]
        
        # Y-axis ranges
        self.__yRanges = [1, 5, 10, 25, 50, 100, 250, 500, 1000]
        
        # setup the chart
        self.__chart = QChart()
        self.__chart.legend().hide()
        self.__chart.addSeries(self.__series[0])
        self.__axisX = QValueAxis()
        self.__axisX.setRange(0, self.__maxX)
        self.__axisX.setLabelFormat("time")
        self.__axisY = QValueAxis()
        self.__axisY.setRange(-self.__maxY, self.__maxY)
        self.__axisY.setLabelFormat("%d")
        self.__chart.setAxisX(self.__axisX, self.__series[0])
        self.__chart.setAxisY(self.__axisY, self.__series[0])
        self.__chartView.setChart(self.__chart)
        self.__chartView.setRenderHint(QPainter.Antialiasing)
    
    @pyqtSlot(bytes)
    def processData(self, data):
        """
        Public slot to process the raw data.
        
        It takes raw bytes, checks the data for a valid tuple of ints or
        floats and adds the data to the graph. If the the length of the bytes
        data is greater than 1024 then a dataFlood signal is emitted to ensure
        eric can take action to remain responsive.
        
        @param data raw data received from the connected device via the main
            device widget
        @type bytes
        """
        # flooding guard
        if self.__flooded:
            return
        
        if len(data) > 1024:
            self.__flooded = True
            self.dataFlood.emit()
            return
        
        data = data.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
        self.__inputBuffer.append(data)
        
        # check if the data contains a Python tuple containing numbers (int
        # or float) on a single line
        inputBytes = b"".join(self.__inputBuffer)
        lines = inputBytes.splitlines(True)
        for line in lines:
            if not line.endswith(b"\n"):
                # incomplete line (last line); skip it
                continue
            
            line = line.strip()
            if line.startswith(b"(") and line.endswith(b")"):
                # it may be a tuple we are interested in
                rawValues = [val.strip() for val in line[1:-1].split(b",")]
                values = []
                for raw in rawValues:
                    try:
                        values.append(int(raw))
                        # ok, it is an integer
                        continue
                    except ValueError:
                        # test for a float
                        pass
                    try:
                        values.append(float(raw))
                    except ValueError:
                        # it is not an int or float, ignore it
                        continue
                if values:
                    self.__addData(tuple(values))
        
        self.__inputBuffer = []
        if lines[-1] and not lines[-1].endswith(b"\n"):
            # Append any left over bytes for processing next time data is
            # received.
            self.__inputBuffer.append(lines[-1])
    
    def __addData(self, values):
        """
        Private method to add a tuple of values to the graph.
        
        It ensures there are the required number of line series, adds the data
        to the line series and updates the range of the chart so the chart
        displays nicely.
        
        @param values tuple containing the data to be added
        @type tuple of int or float
        """
        # store incoming data to be able to dump it as CSV upon request
        self.__rawData.append(values)
        
        # check number of incoming values and adjust line series accordingly
        if len(values) != len(self.__series):
            valuesLen = len(values)
            seriesLen = len(self.__series)
            if valuesLen > seriesLen:
                # add a nwe line series
                for _index in range(valuesLen - seriesLen):
                    newSeries = QLineSeries()
                    self.__chart.addSeries(newSeries)
                    self.__chart.setAxisX(self.__axisX, newSeries)
                    self.__chart.setAxisY(self.__axisY, newSeries)
                    self.__series.append(newSeries)
                    self.__data.append(deque([0] * self.__maxX))
            else:
                # remove obsolete line series
                for oldSeries in self.__series[valuesLen:]:
                    self.__chart.removeSeries(oldSeries)
                self.__series = self.__series[:valuesLen]
                self.__data = self.__data[:valuesLen]
        
        # add the new values to the display and compute the maximum range
        maxRanges = []
        for index, value in enumerate(values):
            self.__data[index].appendleft(value)
            maxRanges.append(max([max(self.__data[index]),
                                  abs(min(self.__data[index]))]))
            if len(self.__data[index]) > self.__maxX:
                self.__data[index].pop()
        
        # re-scale the y-axis
        maxYRange = max(maxRanges)
        yRange = bisect.bisect_left(self.__yRanges, maxYRange)
        if yRange < len(self.__yRanges):
            self.__maxY = self.__yRanges[yRange]
        elif maxYRange > self.__maxY:
            self.__maxY += self.__maxY
        elif maxYRange < self.__maxY / 2:
            self.__maxY /= 2
        self.__axisY.setRange(-self.__maxY, self.__maxY)
        
        # ensure that floats are used to label the y-axis if the range is small
        if self.__maxY <= 5:
            self.__axisY.setLabelFormat("%2.2f")
        else:
            self.__axisY.setLabelFormat("%d")
        
        # update the line series
        for index, series in enumerate(self.__series):
            series.clear()
            xyValues = []
            for x in range(self.__maxX):
                value = self.__data[index][self.__maxX - 1 - x]
                xyValues.append((x, value))
            for xy in xyValues:
                series.append(*xy)
    
    @pyqtSlot()
    def on_saveButton_clicked(self):
        """
        Private slot to save the raw data to a CSV file.
        """
        self.saveData()
    
    def hasData(self):
        """
        Public method to check, if the chart contains some valid data.
        
        @return flag indicating valid data
        @rtype bool
        """
        # TODO: not implemented yet
    
    def saveData(self):
        """
        Public method to save the dialog's raw data.
        
        @return flag indicating success
        @rtype bool
        """
        # TODO: not implemented yet

eric ide

mercurial