From e699d7d926144078f4247a8ce2999de9f9c129b4 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 28 Jul 2020 11:21:10 +0200 Subject: [PATCH 1/6] Changed column headers in log-config Changed the headers to display the ctype and the size of the type (in bytes). --- src/cfclient/ui/dialogs/logconfigdialogue.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/cfclient/ui/dialogs/logconfigdialogue.py b/src/cfclient/ui/dialogs/logconfigdialogue.py index fae9f326..8456eb0d 100644 --- a/src/cfclient/ui/dialogs/logconfigdialogue.py +++ b/src/cfclient/ui/dialogs/logconfigdialogue.py @@ -32,6 +32,7 @@ """ import logging +import struct import cfclient from PyQt5 import Qt, QtWidgets, uic @@ -51,8 +52,10 @@ NAME_FIELD = 0 ID_FIELD = 1 -PTYPE_FIELD = 2 -CTYPE_FIELD = 3 +TYPE_FIELD = 2 +SIZE_FIELD = 3 +MAX_LOG_SIZE = 26 +COLOR_GREEN = '#7cdb37' class LogConfigDialogue(QtWidgets.QWidget, logconfig_widget_class): @@ -62,8 +65,8 @@ def __init__(self, helper, *args): self.setupUi(self) self.helper = helper - self.logTree.setHeaderLabels(['Name', 'ID', 'Unpack', 'Storage']) - self.varTree.setHeaderLabels(['Name', 'ID', 'Unpack', 'Storage']) + self.logTree.setHeaderLabels(['Name', 'ID', 'Type', 'Size']) + self.varTree.setHeaderLabels(['Name', 'ID', 'Type', 'Size']) self.addButton.clicked.connect(lambda: self.moveNode(self.logTree, self.varTree)) @@ -222,10 +225,10 @@ def updateToc(self): item.setData(NAME_FIELD, Qt.DisplayRole, param) item.setData(ID_FIELD, Qt.DisplayRole, toc.toc[group][param].ident) - item.setData(PTYPE_FIELD, Qt.DisplayRole, - toc.toc[group][param].pytype) - item.setData(CTYPE_FIELD, Qt.DisplayRole, + item.setData(TYPE_FIELD, Qt.DisplayRole, toc.toc[group][param].ctype) + item.setData(SIZE_FIELD, Qt.DisplayRole, + struct.calcsize(toc.toc[group][param].pytype)) groupItem.addChild(item) self.logTree.addTopLevelItem(groupItem) @@ -281,7 +284,7 @@ def createConfigFromSelection(self): parentName = node.text(NAME_FIELD) for leaf in self.getNodeChildren(node): varName = leaf.text(NAME_FIELD) - varType = str(leaf.text(CTYPE_FIELD)) + varType = str(leaf.text(TYPE_FIELD)) completeName = "%s.%s" % (parentName, varName) logconfig.add_variable(completeName, varType) return logconfig From 0063a59fb7c3a7308cdd67a55c740103b0d9666a Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 28 Jul 2020 11:22:52 +0200 Subject: [PATCH 2/6] Changed color of progressbar in log-config window, and allow it to go red once the config is full. Also added a text displaying how many bytes the configuration contains --- src/cfclient/ui/dialogs/logconfigdialogue.py | 20 +++++++--- src/cfclient/ui/dialogs/logconfigdialogue.ui | 40 ++++++++++++++++++++ 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/cfclient/ui/dialogs/logconfigdialogue.py b/src/cfclient/ui/dialogs/logconfigdialogue.py index 8456eb0d..fc435fb4 100644 --- a/src/cfclient/ui/dialogs/logconfigdialogue.py +++ b/src/cfclient/ui/dialogs/logconfigdialogue.py @@ -78,8 +78,8 @@ def __init__(self, helper, *args): self.loggingPeriod.textChanged.connect(self.periodChanged) - self.packetSize.setMaximum(26) self.currentSize = 0 + self.packetSize.setMaximum(100) self.packetSize.setValue(0) self.period = 0 @@ -118,15 +118,23 @@ def updatePacketSizeBar(self): for node in self.getNodeChildren(self.varTree.invisibleRootItem()): for leaf in self.getNodeChildren(node): self.currentSize = (self.currentSize + - self.decodeSize(leaf.text(CTYPE_FIELD))) - if self.currentSize > 26: - self.packetSize.setMaximum(self.currentSize / 26.0 * 100.0) + int(leaf.text(SIZE_FIELD))) + + self.packetSizeText.setText('%s/%s bytes' % (self.currentSize, + MAX_LOG_SIZE)) + + if self.currentSize > MAX_LOG_SIZE: + self.packetSize.setMaximum(self.currentSize / MAX_LOG_SIZE * 100) self.packetSize.setFormat("%v%") - self.packetSize.setValue(self.currentSize / 26.0 * 100.0) + self.packetSize.setValue(self.currentSize / MAX_LOG_SIZE * 100) + self.packetSize.setStyleSheet( + 'QProgressBar::chunk { background: red;}') else: - self.packetSize.setMaximum(26) + self.packetSize.setMaximum(MAX_LOG_SIZE) self.packetSize.setFormat("%p%") self.packetSize.setValue(self.currentSize) + self.packetSize.setStyleSheet( + 'QProgressBar::chunk { background: %s;}' % COLOR_GREEN) def addNewVar(self, logTreeItem, target): parentName = logTreeItem.parent().text(NAME_FIELD) diff --git a/src/cfclient/ui/dialogs/logconfigdialogue.ui b/src/cfclient/ui/dialogs/logconfigdialogue.ui index d0cc7228..171468b4 100644 --- a/src/cfclient/ui/dialogs/logconfigdialogue.ui +++ b/src/cfclient/ui/dialogs/logconfigdialogue.ui @@ -179,11 +179,51 @@ + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + 24 + + Qt::AlignCenter + From b00cca24b0cf975272ffc5f0efdfeac864d50cc0 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 28 Jul 2020 11:23:09 +0200 Subject: [PATCH 3/6] Removed unused function --- src/cfclient/ui/dialogs/logconfigdialogue.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/cfclient/ui/dialogs/logconfigdialogue.py b/src/cfclient/ui/dialogs/logconfigdialogue.py index fc435fb4..82b7e933 100644 --- a/src/cfclient/ui/dialogs/logconfigdialogue.py +++ b/src/cfclient/ui/dialogs/logconfigdialogue.py @@ -83,20 +83,6 @@ def __init__(self, helper, *args): self.packetSize.setValue(0) self.period = 0 - def decodeSize(self, s): - size = 0 - if ("16" in s): - size = 2 - if ("float" in s): - size = 4 - if ("8" in s): - size = 1 - if ("FP16" in s): - size = 2 - if ("32" in s): - size = 4 - return size - def sortTrees(self): self.varTree.invisibleRootItem().sortChildren(NAME_FIELD, Qt.AscendingOrder) From e33f04bb492defce44b000979fd9a457a8027756 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 28 Jul 2020 12:43:48 +0200 Subject: [PATCH 4/6] merge from master --- .travis.yml | 2 +- docs/installation/macports.md | 5 +- examples/zmqsrvtest.py | 2 - src/cfclient/ui/dialogs/logconfigdialogue.py | 389 +++++++++++++++++-- src/cfclient/ui/dialogs/logconfigdialogue.ui | 179 ++++----- src/cfclient/ui/icons/create.png | Bin 0 -> 1361 bytes src/cfclient/ui/icons/delete.png | Bin 0 -> 387 bytes src/cfclient/{ => ui/icons}/icon-256.png | Bin src/cfclient/ui/main.py | 7 +- src/cfclient/ui/pluginhelper.py | 1 + src/cfclient/ui/tabs/LogBlockTab.py | 1 + src/cfclient/ui/tabs/LogTab.py | 3 +- src/cfclient/ui/tabs/ParamTab.py | 1 - src/cfclient/ui/tabs/PlotTab.py | 6 +- src/cfclient/utils/logconfigreader.py | 251 +++++++++++- 15 files changed, 691 insertions(+), 156 deletions(-) create mode 100644 src/cfclient/ui/icons/create.png create mode 100644 src/cfclient/ui/icons/delete.png rename src/cfclient/{ => ui/icons}/icon-256.png (100%) diff --git a/.travis.yml b/.travis.yml index 4d7186f6..84e6fb1e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ language: python git: depth: 150 python: - - "2.7" + - "3.6" services: - docker before_install: diff --git a/docs/installation/macports.md b/docs/installation/macports.md index fc40f228..f3681fc6 100644 --- a/docs/installation/macports.md +++ b/docs/installation/macports.md @@ -1,12 +1,15 @@ --- title: Installing Crazyflie client with macPorts -page_id: macports +page_id: macports --- This procedure has been tested on a just-installed Yosemite installation. Commands in code blocks have to be executed in a terminal window. +**Note:** This instruction is outdated and should be updated to use python 3. + + Prerequisite ------------ diff --git a/examples/zmqsrvtest.py b/examples/zmqsrvtest.py index 100b2741..2837491e 100644 --- a/examples/zmqsrvtest.py +++ b/examples/zmqsrvtest.py @@ -31,8 +31,6 @@ NOTE! If connected to a Crazyflie this will power on the motors! """ -from __future__ import print_function - from threading import Thread import signal import time diff --git a/src/cfclient/ui/dialogs/logconfigdialogue.py b/src/cfclient/ui/dialogs/logconfigdialogue.py index 82b7e933..8ac32d91 100644 --- a/src/cfclient/ui/dialogs/logconfigdialogue.py +++ b/src/cfclient/ui/dialogs/logconfigdialogue.py @@ -35,10 +35,11 @@ import struct import cfclient -from PyQt5 import Qt, QtWidgets, uic -from PyQt5.QtCore import * # noqa -from PyQt5.QtWidgets import * # noqa -from PyQt5.Qt import * # noqa +from PyQt5 import QtWidgets, uic, QtGui +from PyQt5.QtCore import pyqtSlot, Qt, QTimer +#from PyQt5.QtCore import * # noqa +#from PyQt5.QtWidgets import * # noqa +#from PyQt5.Qt import * # noqa from cflib.crazyflie.log import LogConfig @@ -67,15 +68,26 @@ def __init__(self, helper, *args): self.logTree.setHeaderLabels(['Name', 'ID', 'Type', 'Size']) self.varTree.setHeaderLabels(['Name', 'ID', 'Type', 'Size']) + self.categoryTree.setHeaderLabels(['Cathegories']) + self.logTree.setSortingEnabled(True) + self.varTree.setSortingEnabled(True) + + # Item-click callbacks. self.addButton.clicked.connect(lambda: self.moveNode(self.logTree, self.varTree)) self.removeButton.clicked.connect(lambda: self.moveNode(self.varTree, self.logTree)) - self.cancelButton.clicked.connect(self.close) - self.loadButton.clicked.connect(self.loadConfig) self.saveButton.clicked.connect(self.saveConfig) + self.categoryTree.itemClicked.connect(self._on_item_click) + self.categoryTree.itemPressed.connect(self._on_item_press) + self.categoryTree.itemChanged.connect(self._config_changed) + + # Add/remove item on doubleclick. + self.logTree.itemDoubleClicked.connect(self.itemDoubleClicked) + self.varTree.itemDoubleClicked.connect(lambda: self.moveNode( + self.varTree, self.logTree)) self.loggingPeriod.textChanged.connect(self.periodChanged) self.currentSize = 0 @@ -83,15 +95,283 @@ def __init__(self, helper, *args): self.packetSize.setValue(0) self.period = 0 + # Used when renaming a config/category + self._last_pressed_item = None + + # set icons + save_icon, delete_icon = self.helper.logConfigReader.get_icons() + self.createCategoryBtn.setIcon(save_icon) + self.createConfigBtn.setIcon(save_icon) + self.deleteBtn.setIcon(delete_icon) + + # bind buttons + self.createCategoryBtn.clicked.connect(self._create_category) + self.createConfigBtn.clicked.connect(self._create_config) + self.deleteBtn.clicked.connect(self._delete_config) + + # set tooltips + self.createCategoryBtn.setToolTip('Create a new category') + self.createConfigBtn.setToolTip('Create a new log-config') + self.deleteBtn.setToolTip('Delete category') + + # enable right-click context-menu + self.categoryTree.setContextMenuPolicy(Qt.CustomContextMenu) + self.categoryTree.customContextMenuRequested.connect( + self.menuContextTree) + + # keyboard shortcuts + shortcut_delete = QtWidgets.QShortcut(QtGui.QKeySequence("Delete"), + self) + shortcut_delete.activated.connect(self._delete_config) + + shortcut_f2 = QtWidgets.QShortcut(QtGui.QKeySequence("F2"), self) + shortcut_f2.activated.connect(self._edit_name) + + self._config_saved_timer = QTimer() + self._config_saved_timer.timeout.connect(self._config_saved_status) + + self.closeOnSave.setChecked(True) + + def itemDoubleClicked(self): + if self.categoryTree.selectedItems(): + self.moveNode(self.logTree, self.varTree) + + def _config_saved_status(self): + self.statusText.setText('') + self._config_saved_timer.stop() + + def _on_item_press(self, item): + self._last_pressed_item = item, item.text(0) + + def _create_config(self): + """ Creates a new log-configuration in the chosen + category. If no category is selected, the + configuration is stored in the 'Default' category. + """ + items = self.categoryTree.selectedItems() + + if items: + config = items[0] + parent = config.parent() + if parent: + category = parent.text(0) + else: + category = config.text(0) + + conf_name = self.helper.logConfigReader.create_empty_log_conf( + category) + self._reload() + # Load the newly created log-config. + self._select_item(conf_name, category) + self._edit_name() + + def _create_category(self): + """ Creates a new category and enables editing the name. """ + category_name = self.helper.logConfigReader.create_category() + self._load_saved_configs() + self.sortTrees() + self._select_category(category_name) + self._edit_name() + + def _delete_config(self): + + """ Deletes a category or a configuration + depending on if the item has a parent or not. + """ + + items = self.categoryTree.selectedItems() + if items: + config = items[0] + parent = config.parent() + + if parent: + # Delete a configuration in the given category. + category = parent.text(0) + self.helper.logConfigReader.delete_config(config.text(0), + category) + self._reload() + else: + # Delete a category and all its log-configurations + category = config.text(0) + if category != 'Default': + self.helper.logConfigReader.delete_category(category) + self._reload() + + def _config_changed(self, config): + """ Changes the name for a log-configuration or a category. + This is a callback function that gets called when an item + is changed. + """ + item, old_name = self._last_pressed_item + + parent = config.parent() + if parent: + # Change name for a log-config, inside of the category. + new_conf_name = item.text(0) + category = parent.text(0) + self.helper.logConfigReader.change_name_config(old_name, + new_conf_name, + category) + else: + # Change name for the category. + category = config.text(0) + self.helper.logConfigReader.change_name_category(old_name, + category) + + def _edit_name(self): + """ Enables editing the clicked item. + When the edit is saved, a callback is fired. + """ + items = self.categoryTree.selectedItems() + if items: + item_clicked = items[0] + self.categoryTree.editItem(item_clicked, 0) + + def _reload(self): + self.resetTrees() + self._load_saved_configs() + self.sortTrees() + + def menuContextTree(self, point): + + menu = QtWidgets.QMenu() + + createConfig = None + createCategory = None + delete = None + edit = None + + item = self.categoryTree.itemAt(point) + if item: + createConfig = menu.addAction('Create new log configuration') + edit = menu.addAction('Edit name') + + if item.parent(): + delete = menu.addAction('Delete config') + else: + delete = menu.addAction('Delete category') + else: + createCategory = menu.addAction('Create new Category') + + action = menu.exec_(self.categoryTree.mapToGlobal(point)) + + if action == createConfig: + self._create_config() + elif createCategory: + self._create_category() + elif action == delete: + self._delete_config() + elif action == edit: + self._edit_name() + + def _select_category(self, category): + items = self.categoryTree.findItems(category, + Qt.MatchFixedString + | Qt.MatchRecursive) + if items: + category = items[0] + self.categoryTree.setCurrentItem(category) + self._last_pressed_item = category, category.text(0) + + def _select_item(self, conf_name, category): + """ loads the given config in the correct category """ + items = self.categoryTree.findItems(conf_name, + Qt.MatchFixedString + | Qt.MatchRecursive) + for item in items: + if item.parent().text(0) == category: + self._last_pressed_item = item, conf_name + self._loadConfig(category, conf_name) + self.categoryTree.setCurrentItem(item) + + @pyqtSlot(QtWidgets.QTreeWidgetItem, int) + def _on_item_click(self, it, col): + """ Opens the log configuration of the pressed + item in the category-tree. """ + log_conf_name = it.text(col) + category = it.parent() + # if category is None, it's the category that's clicked + if category: + self._loadConfig(category.text(0), log_conf_name) + + def _load_saved_configs(self): + """ Read saved log-configs and display them on + the left-side category-tree. """ + + config = None + config = self.helper.logConfigReader._getLogConfigs() + + if (config is None): + logger.warning("Could not load config") + else: + self.categoryTree.clear() + # Create category-tree. + for conf_category in config: + category = QtWidgets.QTreeWidgetItem() + category.setData(NAME_FIELD, Qt.DisplayRole, conf_category) + category.setFlags(category.flags() | Qt.ItemIsEditable) + + # Copulate category-tree with log configurations. + for conf in config[conf_category]: + item = QtWidgets.QTreeWidgetItem() + + # Check if name contains category/config-name. + # This is only true is a new config has been added + # during a session, and the window re-opened. + if '/' in conf.name: + conf_name = conf.name.split('/')[1] + else: + conf_name = conf.name + + item.setData(NAME_FIELD, Qt.DisplayRole, conf_name) + category.addChild(item) + + # Enable item-editing. + item.setFlags(item.flags() | Qt.ItemIsEditable) + + self.categoryTree.addTopLevelItem(category) + self.categoryTree.expandItem(category) + + self.sortTrees() + + def _loadConfig(self, category, config_name): + configs = self.helper.logConfigReader._getLogConfigs()[category] + + if (configs is None): + logger.warning("Could not load config") + + else: + for config in configs: + if config.name == config_name: + self.resetTrees() + self.loggingPeriod.setText("%d" % config.period_in_ms) + self.period = config.period_in_ms + for v in config.variables: + if (v.is_toc_variable()): + parts = v.name.split(".") + varParent = parts[0] + varName = parts[1] + if self.moveNodeByName( + self.logTree, self.varTree, varParent, + varName) is False: + logger.warning("Could not find node %s.%s!!", + varParent, varName) + else: + logger.warning("Error: Mem vars not supported!") + + self.sortTrees() + + def resetTrees(self): + self.varTree.clear() + self.logTree.clear() + self.updateToc() + def sortTrees(self): - self.varTree.invisibleRootItem().sortChildren(NAME_FIELD, - Qt.AscendingOrder) - for node in self.getNodeChildren(self.varTree.invisibleRootItem()): - node.sortChildren(NAME_FIELD, Qt.AscendingOrder) - self.logTree.invisibleRootItem().sortChildren(NAME_FIELD, - Qt.AscendingOrder) - for node in self.getNodeChildren(self.logTree.invisibleRootItem()): - node.sortChildren(NAME_FIELD, Qt.AscendingOrder) + """ Sorts all trees by their name. """ + for tree in [self.logTree, self.varTree, self.categoryTree]: + tree.sortItems(NAME_FIELD, Qt.AscendingOrder) + for node in self.getNodeChildren(tree.invisibleRootItem()): + node.sortChildren(NAME_FIELD, Qt.AscendingOrder) def getNodeChildren(self, treeNode): children = [] @@ -179,17 +459,7 @@ def moveNodeByName(self, source, target, parentName, itemName): return False def showEvent(self, event): - self.updateToc() - self.populateDropDown() - toc = self.helper.cf.log.toc - if (len(list(toc.toc.keys())) > 0): - self.configNameCombo.setEnabled(True) - else: - self.configNameCombo.setEnabled(False) - - def resetTrees(self): - self.varTree.clear() - self.updateToc() + self._load_saved_configs() def periodChanged(self, value): try: @@ -208,7 +478,6 @@ def showErrorPopup(self, caption, message): def updateToc(self): self.logTree.clear() - toc = self.helper.cf.log.toc for group in list(toc.toc.keys()): @@ -226,8 +495,6 @@ def updateToc(self): groupItem.addChild(item) self.logTree.addTopLevelItem(groupItem) - self.logTree.expandItem(groupItem) - self.sortTrees() def populateDropDown(self): self.configNameCombo.clear() @@ -263,22 +530,68 @@ def loadConfig(self): logger.warning("Error: Mem vars not supported!") def saveConfig(self): - updatedConfig = self.createConfigFromSelection() - try: - self.helper.logConfigReader.saveLogConfigFile(updatedConfig) - self.close() - except Exception as e: - self.showErrorPopup("Error when saving file", "Error: %s" % e) + + items = self.categoryTree.selectedItems() + + if items: + config = items[0] + parent = config.parent() + + if parent: + category = parent.text(NAME_FIELD) + config_name = config.text(NAME_FIELD) + updatedConfig = self.createConfigFromSelection(config_name) + + if category != 'Default': + plot_tab_name = '%s/%s' % (category, config_name) + else: + plot_tab_name = config_name + + try: + self.helper.logConfigReader.saveLogConfigFile( + category, + updatedConfig) + self.statusText.setText('Log config succesfully saved!') + self._config_saved_timer.start(4000) + + if self.closeOnSave.isChecked(): + self.close() + + except Exception as e: + self.showErrorPopup("Error when saving file", + "Error: %s" % e) + + # The name of the config is changed due to displaying + # it as category/config-name in the plotter-tab. + # The config is however saved with only the config-name. + updatedConfig.name = plot_tab_name + + # If we're just updating a config, we want to delete the old one first + self._delete_from_plottab(config_name) + self.helper.cf.log.add_config(updatedConfig) - def createConfigFromSelection(self): - logconfig = LogConfig(str(self.configNameCombo.currentText()), - self.period) - for node in self.getNodeChildren(self.varTree.invisibleRootItem()): + def _delete_from_plottab(self, config_name): + for logconfig in self.helper.cf.log.log_blocks: + if logconfig.name == config_name: + self.helper.plotTab.remove_config(logconfig) + self.helper.cf.log.log_blocks.remove(logconfig) + logconfig.delete() + + def _get_node_children(self): + root_item = self.varTree.invisibleRootItem() + return [root_item.child(i) for i in range(root_item.childCount())] + + def createConfigFromSelection(self, config): + logconfig = LogConfig(config, self.period) + + for node in self._get_node_children(): parentName = node.text(NAME_FIELD) + for leaf in self.getNodeChildren(node): varName = leaf.text(NAME_FIELD) varType = str(leaf.text(TYPE_FIELD)) completeName = "%s.%s" % (parentName, varName) logconfig.add_variable(completeName, varType) + return logconfig diff --git a/src/cfclient/ui/dialogs/logconfigdialogue.ui b/src/cfclient/ui/dialogs/logconfigdialogue.ui index 171468b4..0dbb01a3 100644 --- a/src/cfclient/ui/dialogs/logconfigdialogue.ui +++ b/src/cfclient/ui/dialogs/logconfigdialogue.ui @@ -9,8 +9,8 @@ 0 0 - 925 - 447 + 1179 + 558 @@ -43,12 +43,36 @@ + + + + 0 + 0 + + + + + 1 + + + + + + + + 0 + 0 + + + + true + 4 - - 100 + + 57 @@ -72,7 +96,7 @@ - + @@ -90,7 +114,7 @@ - + 4 @@ -125,12 +149,39 @@ - + + + + 0 + 0 + + + + Create category + + + + + + + Create config + + + + + + + Delete + + + + + Qt::Horizontal - QSizePolicy::Fixed + QSizePolicy::Minimum @@ -140,7 +191,7 @@ - + Logging period @@ -165,7 +216,7 @@ - + Qt::Horizontal @@ -177,6 +228,23 @@ + + + + Close window on save + + + + + + + false + + + Save + + + @@ -227,93 +295,10 @@ - - - - - Config name - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - false - - - Delete - - - - - - - Cancel - - - - - - - true - - - - 0 - 0 - - - - - 400 - 0 - - - - - 16777215 - 16777215 - - - - true - - - - - - - false - - - Save - - - - - - - false - - - Load - - - - + + + + diff --git a/src/cfclient/ui/icons/create.png b/src/cfclient/ui/icons/create.png new file mode 100644 index 0000000000000000000000000000000000000000..d6ad5cdfc5b189f8ad515d5f287dcc9088a21001 GIT binary patch literal 1361 zcmV-X1+MyuP)WFU8GbZ8()Nlj2>E@cM*00gy3L_t(|+U=c7ZWB!u zg-_uK5CX(Yh-47LV*_Nui~)iju!}4Mi5(z@?133uU<(WYArMHs{uj{?#KoW;(dpQZ zySu9UcHi%lEK80b?YgJ#J@rUg0ssI2000000D#U2lA@$1v-|Sn;DFb&z*leCMS#Nk zC-xbmW3+#-yk9{CQPSC5LLdX!?fLu936*$-6<$XE(w;23`GXWTc@X`;oGH-Mql^I0R8r^lah89{B3;RjR-X?gT#&JLZX|iIQiJX)}#M&`n7}%W4iGmeN@7Jay zg)TZF9-!bx;+h?6wso@QVM;)=p8v6G=@>*fxB1W3tNUt<8kdnQ8Ez z0b++@JL+?jv^OTO-Z;N2rbh%jS!Y84N3$jA@1DxdyjmYpe-I^LF`JU!81Nou-rSP_ zmaXcIMg$0;f5X?S2=D-hnRp340yxZuq+ecwj{pvGUD7*ar6>FRJ|3Xpf~3zzf*&fu zj|d?Dcsm*lzRCJB#th)b@ZkXtr~M%YKj~7uR{>l%RP*Z?zL+|Tc`N}NhTMK#!Q z76Al9*D$Dk5o0O=fot$3fO49>*HZvYPVaUx#_s!oh)}tn?>cz$){gQVy_Ws_e(UXT zK>+&={aA;OUn`%%cT3X4j)L!|{jFqxWr4j?dG6CpB8VO{u!Y8WutLwgBlF+KmIV*g*67m0j9Uyk3s0It;x;9C_tfXh4^h~Yy3bGX_dcpoa| z;IyPWl3orh_*vS2Xp2D?C4Fk}x-YXikMotscU9~cT=sn}&G5P35Yjf&e48VJJxM=R z{G^-p89r76TXJsJh~-{CQL%UMKjXo7JrfvK`24!Joyuoy8*=-2fDGw>*>xY&pR@VO zfw#7vpS=CoNNNX;ZLj|9+UO6#9+C&x%Pazyzk0GZpk4~jGXTPJD1eYT@YM~R2*BvD z^G7CO-O+$Ha9ZeLvB%z;`g>0@8_Piq1HE{Fe^t!;mycZE6POpk*_N;WI=jCq>6G_$ zKg1Fc8$3UBa9xxDqYV=(4dT|_dlSHZkk@D5$<=gj8-~6XFqQO|ic>#fqym`E$yA?I zocFh0$^i6qXY&9aYrDJUU`(8I6G&QT6aWYS5C9+mKmdRM0096300aOC01yBm06+kM z0003%eNt`+VeOs_00;mO03ZNB05AlaY`Mnmk0Xu%000000000000000Flv4S^9afl TrNW&r00000NkvXXu0mjfo}OCb literal 0 HcmV?d00001 diff --git a/src/cfclient/ui/icons/delete.png b/src/cfclient/ui/icons/delete.png new file mode 100644 index 0000000000000000000000000000000000000000..841f502d7da41dda06c747cfd54b476e6d78ef68 GIT binary patch literal 387 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmPVyC|==!bOE+)j%QH%#er@=ltB<)VvZPmw~~#C^fMp zHASI3vm`^o-P1Q9MK6^dDE{2j#WAGf*4x_`xehDvuwF>}@qhoDd#4v{nKkR-$!N`- zcNMLHl7urztEnWSg(1KI_XPZ8XTwu=o){y0$w1s~QS93(?`&fnQWpzuFztp{CnUiew z`<4*ca5Q3p*nvvn;%{Hyc-<7;C-vn;t#^Xzfva|PVX3w5tL+@T7bqQ=?Wa=lG=2YtJRXpkr>mdKI;Vst0Ir9LYXATM literal 0 HcmV?d00001 diff --git a/src/cfclient/icon-256.png b/src/cfclient/ui/icons/icon-256.png similarity index 100% rename from src/cfclient/icon-256.png rename to src/cfclient/ui/icons/icon-256.png diff --git a/src/cfclient/ui/main.py b/src/cfclient/ui/main.py index 193bcdc5..4e00c9b2 100644 --- a/src/cfclient/ui/main.py +++ b/src/cfclient/ui/main.py @@ -63,6 +63,7 @@ from .dialogs.inputconfigdialogue import InputConfigDialogue from .dialogs.logconfigdialogue import LogConfigDialogue + __author__ = 'Bitcraze AB' __all__ = ['MainUI'] @@ -315,11 +316,15 @@ def __init__(self, *args): self.tabsMenuItem = QMenu("Tabs", self.menuView, enabled=True) self.menuView.addMenu(self.tabsMenuItem) - # self.tabsMenuItem.setMenu(QtWidgets.QMenu()) tabItems = {} self.loadedTabs = [] for tabClass in cfclient.ui.tabs.available: tab = tabClass(self.tabs, cfclient.ui.pluginhelper) + + # Set reference for plot-tab. + if isinstance(tab, cfclient.ui.tabs.PlotTab): + cfclient.ui.pluginhelper.plotTab = tab + item = QtWidgets.QAction(tab.getMenuName(), self, checkable=True) item.toggled.connect(tab.toggleVisibility) self.tabsMenuItem.addAction(item) diff --git a/src/cfclient/ui/pluginhelper.py b/src/cfclient/ui/pluginhelper.py index 9d080991..34bfedae 100644 --- a/src/cfclient/ui/pluginhelper.py +++ b/src/cfclient/ui/pluginhelper.py @@ -41,3 +41,4 @@ def __init__(self): self.menu = None self.logConfigReader = None self.mainUI = None + self.plotTab = None diff --git a/src/cfclient/ui/tabs/LogBlockTab.py b/src/cfclient/ui/tabs/LogBlockTab.py index de4b0408..d74764af 100644 --- a/src/cfclient/ui/tabs/LogBlockTab.py +++ b/src/cfclient/ui/tabs/LogBlockTab.py @@ -176,6 +176,7 @@ def __init__(self, view, parent=None): def add_block(self, block, connected_ts): self._nodes.append(LogBlockItem(block, self, connected_ts)) self.layoutChanged.emit() + self._nodes.sort(key=lambda conf: conf.name.lower()) def refresh(self): """Force a refresh of the view though the model""" diff --git a/src/cfclient/ui/tabs/LogTab.py b/src/cfclient/ui/tabs/LogTab.py index 7db1f804..ed9785d3 100644 --- a/src/cfclient/ui/tabs/LogTab.py +++ b/src/cfclient/ui/tabs/LogTab.py @@ -60,6 +60,8 @@ def __init__(self, tabWidget, helper, *args): # Init the tree widget self.logTree.setHeaderLabels(['Name', 'ID', 'Unpack', 'Storage']) + self.logTree.setSortingEnabled(True) + self.logTree.sortItems(0, Qt.AscendingOrder) self.cf.connected.add_callback(self.connectedSignal.emit) self.connectedSignal.connect(self.connected) @@ -90,4 +92,3 @@ def connected(self, linkURI): groupItem.addChild(item) self.logTree.addTopLevelItem(groupItem) - self.logTree.expandItem(groupItem) diff --git a/src/cfclient/ui/tabs/ParamTab.py b/src/cfclient/ui/tabs/ParamTab.py index a3c97891..ef7f361c 100644 --- a/src/cfclient/ui/tabs/ParamTab.py +++ b/src/cfclient/ui/tabs/ParamTab.py @@ -253,7 +253,6 @@ def __init__(self, tabWidget, helper, *args): def _connected(self, link_uri): self._model.set_toc(self.cf.param.toc.toc, self.helper.cf) - self.paramTree.expandAll() def _disconnected(self, link_uri): self._model.beginResetModel() diff --git a/src/cfclient/ui/tabs/PlotTab.py b/src/cfclient/ui/tabs/PlotTab.py index 42cbb02a..f6ac5140 100644 --- a/src/cfclient/ui/tabs/PlotTab.py +++ b/src/cfclient/ui/tabs/PlotTab.py @@ -60,6 +60,7 @@ def __init__(self, parent=None): def add_block(self, block): self._nodes.append(block) self.layoutChanged.emit() + self._nodes.sort(key=lambda conf: conf.name.lower()) def parent(self, index): """Re-implemented method to get the parent of the given index""" @@ -67,7 +68,7 @@ def parent(self, index): def remove_block(self, block): """Remove a block from the view""" - raise NotImplementedError() + self._nodes.remove(block) def columnCount(self, parent): """Re-implemented method to get the number of columns""" @@ -245,6 +246,9 @@ def _config_added(self, logconfig): logger.debug("Callback for new config [%s]", logconfig.name) self._model.add_block(logconfig) + def remove_config(self, logconfig): + self._model.remove_block(logconfig) + def _logging_error(self, log_conf, msg): """Callback from the log layer when an error occurs""" QMessageBox.about( diff --git a/src/cfclient/utils/logconfigreader.py b/src/cfclient/utils/logconfigreader.py index c548dcf4..fd812d42 100644 --- a/src/cfclient/utils/logconfigreader.py +++ b/src/cfclient/utils/logconfigreader.py @@ -39,21 +39,29 @@ import json import logging import os +import re import shutil import cfclient from cflib.crazyflie.log import LogVariable, LogConfig +from PyQt5 import QtGui + __author__ = 'Bitcraze AB' __all__ = ['LogVariable', 'LogConfigReader'] logger = logging.getLogger(__name__) +DEFAULT_CONF_NAME = 'log_config' +DEFAULT_CATEGORY_NAME = 'category' + class LogConfigReader(): """Reads logging configurations from file""" def __init__(self, crazyflie): + + self._log_configs = {} self.dsList = [] # Check if user config exists, otherwise copy files if (not os.path.exists(cfclient.config_path + "/log")): @@ -65,21 +73,226 @@ def __init__(self, crazyflie): self._cf = crazyflie self._cf.connected.add_callback(self._connected) + def get_icons(self): + client_path = os.path.abspath(os.path.join(os.path.dirname(__file__), + os.pardir)) + icon_path = os.path.join(client_path, 'ui', 'icons') + save_icon = QtGui.QIcon(os.path.join(icon_path, 'create.png')) + delete_icon = QtGui.QIcon(os.path.join(icon_path, 'delete.png')) + return save_icon, delete_icon + + def create_empty_log_conf(self, category): + """ Creates an empty log-configuration with a default name """ + log_path = self._get_log_path(category) + conf_name = self._get_default_conf_name(log_path) + file_path = os.path.join(log_path, conf_name) + '.json' + + if not os.path.exists(file_path): + with open(file_path, 'w') as f: + f.write(json.dumps( + { + 'logconfig': { + 'logblock': { + 'variables': [], + 'name': conf_name, + 'period': 100 + } + } + }, indent=2)) + + self._log_configs[category].append(LogConfig(conf_name, 100)) + return conf_name + + def create_category(self): + """ Creates a new category (dir in filesystem), with a unique name """ + log_path = os.path.join(cfclient.config_path, 'log') + category = self._get_default_category(log_path) + dir_path = os.path.join(log_path, category) + + # This should never be false, but just to be safe. + if not os.path.exists(dir_path): + os.mkdir(dir_path) + self._log_configs[category] = [] + + return category + + def delete_category(self, category): + """ Removes the directory on file-system and recursively removes + all the logging configurations. + """ + log_path = self._get_log_path(category) + if os.path.exists(log_path): + shutil.rmtree(log_path) + self._log_configs.pop(category) + + def delete_config(self, conf_name, category): + """ Deletes a configuration from file system. """ + log_path = self._get_log_path(category) + conf_path = os.path.join(log_path, conf_name) + '.json' + + if not os.path.exists(conf_path): + # Check if we can find the file with lowercase first letter. + conf_path = os.path.join(log_path, + conf_name[0].lower() + conf_name[1:] + + '.json') + if not os.path.exists(conf_path): + # Cant' find the config-file + logger.warning('Failed to find log-config %s' % conf_path) + return + + os.remove(conf_path) + for conf in self._log_configs[category]: + if conf.name == conf_name: + self._log_configs[category].remove(conf) + + def change_name_config(self, old_name, new_name, category): + """ Changes name to the configuration and updates the + file in the file system. + """ + configs = self._log_configs[category] + + for conf in configs: + if conf.name == old_name: + conf.name = new_name + + log_path = self._get_log_path(category) + old_path = os.path.join(log_path, old_name) + '.json' + new_path = os.path.join(log_path, new_name) + '.json' + + # File should exist but just to be extra safe + if os.path.exists(old_path): + with open(old_path, 'r+') as f: + data = json.load(f) + data['logconfig']['logblock']['name'] = new_name + f.seek(0) + f.truncate() + f.write(json.dumps(data, indent=2)) + + os.rename(old_path, new_path) + + def change_name_category(self, old_name, new_name): + """ Renames the directory on file system and the config dict """ + if old_name in self._log_configs: + self._log_configs[new_name] = self._log_configs.pop(old_name) + os.rename(self._get_log_path(old_name), + self._get_log_path(new_name)) + + def _get_log_path(self, category): + """ Helper method """ + category_dir = '' if category == 'Default' else '/' + category + return os.path.join(cfclient.config_path, + 'log' + category_dir) + + def _get_default_category(self, log_path): + """ Creates a name for the category, ending with a unique number. """ + dirs = [dir_ for dir_ in os.listdir(log_path) if os.path.isdir( + os.path.join(log_path, dir_) + )] + config_nbrs = re.findall(r'(?<=%s)\d*' % DEFAULT_CATEGORY_NAME, + ' '.join(dirs)) + config_nbrs = list(filter(len, config_nbrs)) + + if config_nbrs: + return DEFAULT_CATEGORY_NAME + str( + max([int(nbr) for nbr in config_nbrs]) + 1) + else: + return DEFAULT_CATEGORY_NAME + '1' + + def _read_config_categories(self): + """Read and parse log configurations""" + + self._log_configs = {'Default': []} + log_path = os.path.join(cfclient.config_path, 'log') + + for cathegory in os.listdir(log_path): + + cathegory_path = os.path.join(log_path, cathegory) + + try: + if (os.path.isdir(cathegory_path)): + # create a new cathegory + self._log_configs[cathegory] = [] + for conf in os.listdir(cathegory_path): + if conf.endswith('.json'): + conf_path = os.path.join(cathegory_path, conf) + log_conf = self._get_conf(conf_path) + + # add the log configuration to the cathegory + self._log_configs[cathegory].append(log_conf) + + else: + # if it's not a directory, the log config is placed + # in the 'Default' cathegory + if cathegory_path.endswith('.json'): + log_conf = self._get_conf(cathegory_path) + self._log_configs['Default'].append(log_conf) + + except Exception as e: + logger.warning("Failed to open log config %s", e) + + def _get_default_conf_name(self, log_path): + config_nbrs = re.findall(r'(?<=%s)\d*(?!=\.json)' % DEFAULT_CONF_NAME, + ' '.join(os.listdir(log_path))) + config_nbrs = list(filter(len, config_nbrs)) + + if config_nbrs: + return DEFAULT_CONF_NAME + str( + max([int(nbr) for nbr in config_nbrs]) + 1) + else: + return DEFAULT_CONF_NAME + '1' + + def _get_conf(self, conf_path): + with open(conf_path) as f: + data = json.load(f) + infoNode = data["logconfig"]["logblock"] + + logConf = LogConfig(infoNode["name"], + int(infoNode["period"])) + for v in data["logconfig"]["logblock"]["variables"]: + if v["type"] == "TOC": + logConf.add_variable(str(v["name"]), v["fetch_as"]) + else: + logConf.add_variable("Mem", v["fetch_as"], + v["stored_as"], + int(v["address"], 16)) + return logConf + + def _get_configpaths_recursively(self): + """ Reads all configuration files from the log path and + returns a list of tuples with format: + (category/conf-name, absolute path). + """ + logpath = os.path.join(cfclient.config_path, 'log') + filepaths = [] + + for files in os.listdir(logpath): + abspath = os.path.join(logpath, files) + if os.path.isdir(abspath): + for config in os.listdir(abspath): + if config.endswith('.json'): + filepaths.append(('/'.join([files, config]), + os.path.join(abspath, config))) + else: + if files.endswith('.json'): + filepaths.append((files, os.path.join(abspath))) + + return filepaths + def _read_config_files(self): """Read and parse log configurations""" - configsfound = [os.path.basename(f) for f in - glob.glob(cfclient.config_path + - "/log/[A-Za-z_-]*.json")] + + configsfound = self._get_configpaths_recursively() + new_dsList = [] for conf in configsfound: try: - logger.info("Parsing [%s]", conf) - json_data = open(cfclient.config_path + "/log/%s" % conf) + logger.info("Parsing [%s]", conf[0]) + json_data = open(conf[1]) self.data = json.load(json_data) infoNode = self.data["logconfig"]["logblock"] + logConfName = conf[0].replace('.json', '') - logConf = LogConfig(infoNode["name"], - int(infoNode["period"])) + logConf = LogConfig(logConfName, int(infoNode["period"])) for v in self.data["logconfig"]["logblock"]["variables"]: if v["type"] == "TOC": logConf.add_variable(str(v["name"]), v["fetch_as"]) @@ -97,6 +310,7 @@ def _connected(self, link_uri): """Callback that is called once Crazyflie is connected""" self._read_config_files() + self._read_config_categories() # Just add all the configurations. Via callbacks other parts of the # application will pick up these configurations and use them for d in self.dsList: @@ -111,10 +325,15 @@ def getLogConfigs(self): """Return the log configurations""" return self.dsList - def saveLogConfigFile(self, logconfig): + def _getLogConfigs(self): + """Return the log configurations""" + return self._log_configs + + def saveLogConfigFile(self, category, logconfig): """Save a log configuration to file""" - filename = cfclient.config_path + "/log/" + logconfig.name + ".json" - logger.info("Saving config for [%s]", filename) + log_path = self._get_log_path(category) + file_path = os.path.join(log_path, logconfig.name) + '.json' + logger.info("Saving config for [%s]", file_path) # Build tree for JSON saveConfig = {} @@ -133,6 +352,12 @@ def saveLogConfigFile(self, logconfig): saveConfig['logconfig'] = logconf - json_data = open(filename, 'w') - json_data.write(json.dumps(saveConfig, indent=2)) - json_data.close() + for old_conf in self._log_configs[category]: + if old_conf.name == logconfig.name: + self._log_configs[category].remove(old_conf) + self._log_configs[category].append(logconfig) + + with open(file_path, 'w') as f: + f.write(json.dumps(saveConfig, indent=2)) + + self._read_config_files() From 0981bbc051118db39f78b0a9bbbc39e716980151 Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 28 Jul 2020 13:17:51 +0200 Subject: [PATCH 5/6] fixed wildcard import issue --- src/cfclient/ui/dialogs/logconfigdialogue.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/cfclient/ui/dialogs/logconfigdialogue.py b/src/cfclient/ui/dialogs/logconfigdialogue.py index 8ac32d91..f4f02a35 100644 --- a/src/cfclient/ui/dialogs/logconfigdialogue.py +++ b/src/cfclient/ui/dialogs/logconfigdialogue.py @@ -37,9 +37,6 @@ import cfclient from PyQt5 import QtWidgets, uic, QtGui from PyQt5.QtCore import pyqtSlot, Qt, QTimer -#from PyQt5.QtCore import * # noqa -#from PyQt5.QtWidgets import * # noqa -#from PyQt5.Qt import * # noqa from cflib.crazyflie.log import LogConfig @@ -469,7 +466,7 @@ def periodChanged(self, value): self.period = 0 def showErrorPopup(self, caption, message): - self.box = QMessageBox() # noqa + self.box = QtWidgets.QMessageBox() # noqa self.box.setWindowTitle(caption) self.box.setText(message) # self.box.setButtonText(1, "Ok") From d499103bb57daa910d00ae909142e7193ad2098d Mon Sep 17 00:00:00 2001 From: victor Date: Tue, 28 Jul 2020 13:18:15 +0200 Subject: [PATCH 6/6] Added a text-label that was missing in the log-config tab --- src/cfclient/ui/dialogs/logconfigdialogue.ui | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/cfclient/ui/dialogs/logconfigdialogue.ui b/src/cfclient/ui/dialogs/logconfigdialogue.ui index 0dbb01a3..790dd364 100644 --- a/src/cfclient/ui/dialogs/logconfigdialogue.ui +++ b/src/cfclient/ui/dialogs/logconfigdialogue.ui @@ -215,6 +215,26 @@ + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + +