Wed, 17 Jul 2019 20:31:47 +0200
Continued implementing the MicroPython support.
eric6/MicroPython/MicroPythonGraphWidget.py | file | annotate | diff | comparison | revisions | |
eric6/MicroPython/MicroPythonReplWidget.py | file | annotate | diff | comparison | revisions |
--- a/eric6/MicroPython/MicroPythonGraphWidget.py Tue Jul 16 20:12:53 2019 +0200 +++ b/eric6/MicroPython/MicroPythonGraphWidget.py Wed Jul 17 20:31:47 2019 +0200 @@ -11,15 +11,22 @@ from collections import deque import bisect +import os +import time +import csv -from PyQt5.QtCore import pyqtSignal, pyqtSlot +from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt from PyQt5.QtGui import QPainter from PyQt5.QtWidgets import ( - QWidget, QHBoxLayout, QVBoxLayout, QToolButton, QSizePolicy, QSpacerItem + QWidget, QHBoxLayout, QVBoxLayout, QToolButton, QSizePolicy, QSpacerItem, + QLabel, QSpinBox ) from PyQt5.QtChart import QChartView, QChart, QLineSeries, QValueAxis +from E5Gui import E5MessageBox + import UI.PixmapCache +import Preferences class MicroPythonGraphWidget(QWidget): @@ -40,7 +47,7 @@ super(MicroPythonGraphWidget, self).__init__(parent) self.__layout = QHBoxLayout() - self.__layout.setContentsMargins(0, 0, 0, 0) + self.__layout.setContentsMargins(2, 2, 2, 2) self.setLayout(self.__layout) self.__chartView = QChartView(self) @@ -48,26 +55,42 @@ 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.__verticalLayout = QVBoxLayout() + self.__verticalLayout.setContentsMargins(0, 0, 0, 0) + self.__layout.addLayout(self.__verticalLayout) 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) + self.__verticalLayout.addWidget(self.__saveButton) + self.__verticalLayout.setAlignment(self.__saveButton, Qt.AlignHCenter) spacerItem = QSpacerItem(20, 20, QSizePolicy.Minimum, QSizePolicy.Expanding) - self.__buttonsLayout.addItem(spacerItem) + self.__verticalLayout.addItem(spacerItem) + + label = QLabel(self.tr("max. X:")) + self.__verticalLayout.addWidget(label) + self.__verticalLayout.setAlignment(label, Qt.AlignHCenter) + + self.__maxX = 100 + self.__maxXSpinBox = QSpinBox() + self.__maxXSpinBox.setMinimum(100) + self.__maxXSpinBox.setMaximum(1000) + self.__maxXSpinBox.setSingleStep(100) + self.__maxXSpinBox.setToolTip(self.tr( + "Enter the maximum number of data points to be plotted.")) + self.__maxXSpinBox.setValue(self.__maxX) + self.__maxXSpinBox.setAlignment(Qt.AlignRight) + self.__verticalLayout.addWidget(self.__maxXSpinBox) # holds the data to be checked for plotable data self.__inputBuffer = [] # holds the raw data self.__rawData = [] + self.__dirty = False - self.__maxX = 100 self.__maxY = 1000 self.__flooded = False # flag indicating a data flood @@ -91,6 +114,8 @@ self.__chart.setAxisY(self.__axisY, self.__series[0]) self.__chartView.setChart(self.__chart) self.__chartView.setRenderHint(QPainter.Antialiasing) + + self.__maxXSpinBox.valueChanged.connect(self.__handleMaxXChanged) @pyqtSlot(bytes) def processData(self, data): @@ -115,6 +140,10 @@ self.dataFlood.emit() return + # disable the inputs while processing data + self.__saveButton.setEnabled(False) + self.__maxXSpinBox.setEnabled(False) + data = data.replace(b"\r\n", b"\n").replace(b"\r", b"\n") self.__inputBuffer.append(data) @@ -153,6 +182,10 @@ # Append any left over bytes for processing next time data is # received. self.__inputBuffer.append(lines[-1]) + + # re-enable the inputs + self.__saveButton.setEnabled(True) + self.__maxXSpinBox.setEnabled(True) def __addData(self, values): """ @@ -167,6 +200,7 @@ """ # store incoming data to be able to dump it as CSV upon request self.__rawData.append(values) + self.__dirty = True # check number of incoming values and adjust line series accordingly if len(values) != len(self.__series): @@ -238,7 +272,16 @@ @return flag indicating valid data @rtype bool """ - # TODO: not implemented yet + return len(self.__rawData) > 0 + + def isDirty(self): + """ + Public method to check, if the chart contains unsaved data. + + @return flag indicating unsaved data + @rtype bool + """ + return self.hasData() and self.__dirty def saveData(self): """ @@ -247,4 +290,56 @@ @return flag indicating success @rtype bool """ - # TODO: not implemented yet + baseDir = (Preferences.getMultiProject("Workspace") or + os.path.expanduser("~")) + dataDir = os.path.join(baseDir, "data_capture") + + if not os.path.exists(dataDir): + os.makedirs(dataDir) + + # save the raw data as a CSV file + fileName = "{0}.csv".format(time.strftime("%Y%m%d-%H%M%S")) + fullPath = os.path.join(dataDir, fileName) + try: + csvFile = open(fullPath, "w") + csvWriter = csv.writer(csvFile) + csvWriter.writerows(self.__rawData) + csvFile.close() + + self.__dirty = False + return True + except (IOError, OSError) as err: + E5MessageBox.critical( + self, + self.tr("Save Chart Data"), + self.tr( + """<p>The chart data could not be saved into file""" + """ <b>{0}</b>.</p><p>Reason: {1}</p>""").format( + fullPath, str(err))) + return False + + @pyqtSlot(int) + def __handleMaxXChanged(self, value): + """ + Private slot handling a change of the max. X spin box. + + @param value value of the spin box + @type int + """ + delta = value - self.__maxX + if delta == 0: + # nothing to change + return + elif delta > 0: + # range must be increased + for deq in self.__data: + deq.extend([0] * delta) + else: + # range must be decreased + data = [] + for deq in self.__data: + data.append(deque(list(deq)[:value])) + self.__data = data + + self.__maxX = value + self.__axisX.setRange(0, self.__maxX)
--- a/eric6/MicroPython/MicroPythonReplWidget.py Tue Jul 16 20:12:53 2019 +0200 +++ b/eric6/MicroPython/MicroPythonReplWidget.py Wed Jul 17 20:31:47 2019 +0200 @@ -14,7 +14,7 @@ from PyQt5.QtCore import ( pyqtSlot, pyqtSignal, Qt, QPoint, QEvent, QIODevice, QTimer ) -from PyQt5.QtGui import QColor, QKeySequence, QTextCursor +from PyQt5.QtGui import QColor, QKeySequence, QTextCursor, QBrush from PyQt5.QtWidgets import ( QWidget, QMenu, QApplication, QHBoxLayout, QSpacerItem, QSizePolicy) try: @@ -55,6 +55,26 @@ dataReceived = pyqtSignal(bytes) + # ANSI Colors + AnsiColors = { + (1, 30): QBrush(Qt.darkGray), + (1, 31): QBrush(Qt.red), + (1, 32): QBrush(Qt.green), + (1, 33): QBrush(Qt.yellow), + (1, 34): QBrush(Qt.blue), + (1, 35): QBrush(Qt.magenta), + (1, 36): QBrush(Qt.cyan), + (1, 37): QBrush(Qt.white), + (2, 30): QBrush(Qt.black), + (2, 31): QBrush(Qt.darkRed), + (2, 32): QBrush(Qt.darkGreen), + (2, 33): QBrush(Qt.darkYellow), + (2, 34): QBrush(Qt.darkBlue), + (2, 35): QBrush(Qt.darkMagenta), + (2, 36): QBrush(Qt.darkCyan), + (2, 37): QBrush(Qt.lightGray), + } + def __init__(self, parent=None): """ Constructor @@ -115,7 +135,7 @@ return self.__vt100Re = re.compile( - r'(?P<count>[\d]*)(;?[\d]*)*(?P<action>[ABCDKm])') + r'(?P<count>\d*);?(?P<color>\d*)(?P<action>[ABCDKm])') self.__populateDeviceTypeComboBox() @@ -127,6 +147,10 @@ self.replEdit.customContextMenuRequested.connect( self.__showContextMenu) + + defaultCharFormat = self.replEdit.textCursor().charFormat() + self.DefaultForeground = defaultCharFormat.foreground() + self.DefaultBackground = defaultCharFormat.background() def __populateDeviceTypeComboBox(self): """ @@ -218,6 +242,7 @@ menu = QMenu(self) menu.addAction(self.tr("Copy"), self.replEdit.copy, copyKeys) menu.addAction(self.tr("Paste"), self.__paste, pasteKeys) + menu.addSeparator() menu.exec_(self.replEdit.mapToGlobal(pos)) def setConnected(self, connected): @@ -310,18 +335,6 @@ self.__closeSerialLink() self.setConnected(False) - def __activatePlotter(self): - """ - Private method to activate a data plotter widget. - """ - # TODO: not implemented yet - - def __deactivatePlotter(self): - """ - Private method to deactivate the plotter widget. - """ - # TODO: not implemented yet - @pyqtSlot() def __paste(self): """ @@ -441,7 +454,26 @@ mode=QTextCursor.KeepAnchor) tc.removeSelectedText() self.replEdit.setTextCursor(tc) - # TODO: add handling of 'm' (colors) + elif action == "m": + print(match.group("count"), match.group("color")) + charFormat = tc.charFormat() + if count == 0 and match.group("color") == "": + # reset color + charFormat.setForeground(self.DefaultForeground) + charFormat.setBackground(self.DefaultBackground) + elif count in (0, 1, 2): + if match.group("color") != "": + color = int(match.group("color")) + if count == 0: + count = 1 + if 30 <= color <= 37: + charFormat.setForeground( + self.AnsiColors[(count, color)]) + elif 40 <= color <= 47: + charFormat.setBackground( + self.AnsiColors[(count, color - 10)]) + tc.setCharFormat(charFormat) + self.replEdit.setTextCursor(tc) elif data[index] == 10: # \n tc.movePosition(QTextCursor.End) self.replEdit.setTextCursor(tc) @@ -633,7 +665,7 @@ self.__ui = e5App().getObject("UserInterface") if self.__plotterRunning: - if self.__chartWidget.hasData(): + if self.__chartWidget.isDirty(): res = E5MessageBox.okToClearData( self, self.tr("Unsaved Chart Data"), @@ -668,7 +700,7 @@ self.__ui.addSideWidget(self.__ui.BottomSide, self.__chartWidget, UI.PixmapCache.getIcon("chart"), - self.tr("Chart")) + self.tr("μPy Chart")) self.__ui.showSideWidget(self.__chartWidget) if not self.__serial: