mirror of
https://github.com/paparazzi/paparazzi.git
synced 2026-05-09 22:49:53 +08:00
9763a70ca9
This is the result of a student project to develop a new paparazzi center based on Python/Qt. See http://wiki.paparazziuav.org/wiki/Paparazzi_Center/Evolutions for the main idea and development process. It can be tested by running the main paparazzi launcher with the -python option. I more convenient way might be added later. This PR is mostly a way to not loose this work, and other people are encourage to improve it in many aspects (HMI, functionalities, ...).
1769 lines
75 KiB
Python
1769 lines
75 KiB
Python
# Paparazzi center utilities
|
|
#
|
|
# Copyright (C) 2016 ENAC, Florian BITARD (intern student)
|
|
#
|
|
# This file is part of paparazzi.
|
|
#
|
|
# paparazzi is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2, or (at your option)
|
|
# any later version.
|
|
#
|
|
# paparazzi is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with paparazzi; see the file COPYING. If not, write to
|
|
# the Free Software Foundation, 59 Temple Place - Suite 330,
|
|
# Boston, MA 02111-1307, USA.
|
|
|
|
###############################################################################
|
|
# [Imports]
|
|
|
|
import ui.main_window as ui
|
|
import dialogs as dial
|
|
import lib.console as cs
|
|
import lib.database as db
|
|
import lib.environment as env
|
|
import lib.gui as gui
|
|
import parser
|
|
import processes as proc
|
|
|
|
import PyQt5.QtCore as Core
|
|
import PyQt5.QtWidgets as Widgets
|
|
import functools
|
|
import logging
|
|
import os
|
|
import sys
|
|
import re
|
|
import shutil
|
|
|
|
###############################################################################
|
|
# [Constants]
|
|
|
|
LOGGER = logging.getLogger("[HMI]")
|
|
|
|
CONF_PATH = env.PAPARAZZI_CONF
|
|
SET_SYMBOLIC_LINK = CONF_PATH + "/conf.xml"
|
|
CONF_XML_COPY = CONF_PATH + "/conf_copy.xml"
|
|
|
|
OK_QPIXMAP = "icons/dialog-apply.svg"
|
|
ERROR_QPIXMAP = "icons/dialog-error.svg"
|
|
START_ICON = "icons/media-playback-start.svg"
|
|
STOP_ICON = "icons/process-stop.svg"
|
|
CHANGED_ICON = "icons/dialog-warning-symbolic.svg"
|
|
|
|
UNKNOWN_VALUE = "---"
|
|
|
|
PROGRESS_BAR_LINE = " "
|
|
PROGRESS_BAR_CURSOR = "***"
|
|
PROGRESS_BAR_SPEED = 10
|
|
|
|
|
|
###############################################################################
|
|
# [Functions] System functions
|
|
|
|
def control_cwd():
|
|
"""
|
|
-> Get the current directory path and log it.
|
|
"""
|
|
cwd = os.getcwd()
|
|
LOGGER.info("Current directory : '%s'.", cwd)
|
|
return cwd
|
|
|
|
|
|
def point_symbol_link_to(set_file):
|
|
"""
|
|
:param set_file:
|
|
-> Make the 'conf.xml' symbolic file point to the given 'set_file'.
|
|
"""
|
|
if os.path.exists(SET_SYMBOLIC_LINK):
|
|
os.remove(SET_SYMBOLIC_LINK)
|
|
os.symlink(set_file, SET_SYMBOLIC_LINK)
|
|
LOGGER.debug("'%s' -> '%s'.", SET_SYMBOLIC_LINK, set_file)
|
|
|
|
|
|
def init_conf_xml_path():
|
|
"""
|
|
-> Create a copy of the conf.xml file if it's an existing and real file.
|
|
-> Make the conf.xml file point to the copy created.
|
|
"""
|
|
if os.path.exists(SET_SYMBOLIC_LINK) \
|
|
and not os.path.islink(SET_SYMBOLIC_LINK):
|
|
shutil.copy(SET_SYMBOLIC_LINK, CONF_XML_COPY)
|
|
point_symbol_link_to(CONF_XML_COPY)
|
|
|
|
|
|
###############################################################################
|
|
# [Functions] Widgets management functions
|
|
|
|
def set_widgets_visibility(widgets, set_visible_bool):
|
|
"""
|
|
:param widgets:
|
|
:param set_visible_bool:
|
|
-> Set a list of widgets visible or not together.
|
|
"""
|
|
for widget in widgets:
|
|
widget.setVisible(set_visible_bool)
|
|
|
|
|
|
def set_widgets_availability(widgets, set_available_bool):
|
|
"""
|
|
:param widgets:
|
|
:param set_available_bool:
|
|
-> Set a list of widgets available or not together.
|
|
"""
|
|
for widget in widgets:
|
|
widget.setEnabled(set_available_bool)
|
|
|
|
|
|
def set_items_icons(list_widget_items, qicon):
|
|
"""
|
|
:param list_widget_items:
|
|
:param qicon:
|
|
-> Put the same QIcon to items of a QListWidget.
|
|
"""
|
|
for item in list_widget_items:
|
|
item.setIcon(qicon)
|
|
|
|
|
|
def clear_widgets(widgets):
|
|
"""
|
|
:param widgets:
|
|
-> Clear each widget in a list of widgets.
|
|
"""
|
|
for widget in widgets:
|
|
widget.clear()
|
|
|
|
|
|
def clear_list_widgets_selection(widgets):
|
|
"""
|
|
:param widgets:
|
|
-> Clear the selection of each widget in a list of QListWidget widgets.
|
|
"""
|
|
for widget in widgets:
|
|
widget.clearSelection()
|
|
widget.clearFocus()
|
|
|
|
|
|
###############################################################################
|
|
# [Hmi class]
|
|
|
|
class Hmi(Widgets.QMainWindow):
|
|
"""Class to manage the main HMI behavior."""
|
|
def __init__(self):
|
|
"""
|
|
-> Creation of a MainWindow object generated by the command :
|
|
'pyuic5 *.ui -o *.py' from the PyQt5 HMI designed under QtDesigner.
|
|
-> Creation of Dialog objects used from the main HMI.
|
|
-> Declaration of widgets groups for code factorization.
|
|
-> Declaration of HMI private attributes for its own methods.
|
|
"""
|
|
super(Hmi, self).__init__()
|
|
|
|
self.ui = ui.Ui_MainWindow()
|
|
self.ui.setupUi(self)
|
|
|
|
# Existing popups :
|
|
self.settings = None
|
|
self.data_changed_popup = None
|
|
self.credits_popup = None
|
|
self.tutorial_popup = None
|
|
|
|
# Shortcuts for lists of widgets :
|
|
self.change_config_widgets = [self.ui.current_airframes,
|
|
self.ui.current_settings,
|
|
self.ui.current_flight_plan,
|
|
self.ui.current_radio,
|
|
self.ui.current_telemetry]
|
|
self.display_config_widgets = [self.ui.airframes_overview,
|
|
self.ui.settings_overview,
|
|
self.ui.flight_plan_overview,
|
|
self.ui.radio_overview,
|
|
self.ui.telemetry_overview,
|
|
self.ui.airframes_overview_2,
|
|
self.ui.settings_overview_2,
|
|
self.ui.flight_plan_overview_2,
|
|
self.ui.radio_overview_2,
|
|
self.ui.telemetry_overview_2]
|
|
self.flight_plan_buttons = [self.ui.open_gui,
|
|
self.ui.select_kml]
|
|
self.session_widgets = [self.ui.session,
|
|
self.ui.session_overview_1,
|
|
self.ui.session_overview_2]
|
|
self.display_programs_widgets = [self.ui.programs_overview_1,
|
|
self.ui.programs_overview_2]
|
|
self.target_widgets = [self.ui.quick_target, self.ui.target]
|
|
self.messages_level_widgets = [self.ui.important,
|
|
self.ui.custom, self.ui.all]
|
|
self.log_filters_widgets = [self.ui.display_default,
|
|
self.ui.display_info,
|
|
self.ui.display_warnings,
|
|
self.ui.display_errors]
|
|
self.messages_nb_widgets = [self.ui.info_nb, self.ui.warnings_nb,
|
|
self.ui.errors_nb]
|
|
self.start_all_buttons = [self.ui.start_all_button,
|
|
self.ui.quick_restart,
|
|
self.ui.quick_restart_3]
|
|
self.kill_all_buttons = [self.ui.kill_all_button, self.ui.quick_kill,
|
|
self.ui.quick_kill_2]
|
|
|
|
# Icons used in widgets (can't be constants because qpixmap objects
|
|
# must be declared into a QApplication) :
|
|
self.ok_qpixmap = gui.generate_qpixmap(OK_QPIXMAP)
|
|
self.error_qpixmap = gui.generate_qpixmap(ERROR_QPIXMAP)
|
|
self.start_qicon = gui.generate_qicon(START_ICON)
|
|
self.stop_qicon = gui.generate_qicon(STOP_ICON)
|
|
self.changed_qicon = gui.generate_qicon(CHANGED_ICON)
|
|
|
|
# Progress bar initialization :
|
|
self.max_result_field_size = self.ui.build_result.width() // 10
|
|
self.progress_bar_to_left = False
|
|
|
|
# HMI parameters initialization (could be set in settings menu) :
|
|
self.dev_mode = False
|
|
|
|
self.current_log_filter = None
|
|
|
|
self.data = None
|
|
self.current_set = None
|
|
self.current_config = None
|
|
self.current_item = None
|
|
|
|
self.current_target = None
|
|
self.simulation_targets_names = ['nps', 'sim']
|
|
self.change_target_bool = True
|
|
|
|
self.current_device = None
|
|
self.current_session = None
|
|
self.current_program = None
|
|
self.current_option = None
|
|
|
|
self.run_version = None
|
|
self.build_version = None
|
|
|
|
self.configurations_changed = {}
|
|
self.sessions_changed = {}
|
|
|
|
self.build = None
|
|
self.build_running = False
|
|
self.clean = None
|
|
self.clean_running = False
|
|
self.upload = None
|
|
self.upload_running = False
|
|
|
|
self.running_programs = {}
|
|
|
|
self.console = cs.Console(self.ui.console)
|
|
|
|
# Logger output redirected to a special stream to allow an integrated
|
|
# console display (and logger level could be set in settings menu) :
|
|
self.logger_stream = proc.LoggerStream()
|
|
self.logger_stream.logger_log_sent.connect(self.write_log_in_console)
|
|
logging.basicConfig(level=logging.INFO, stream=self.logger_stream)
|
|
|
|
###############################################################################
|
|
# [Hmi methods] Init HMI methods
|
|
|
|
def init_hmi_data(self):
|
|
"""
|
|
-> Initialization of a 'Data' object with all necessary data from XML
|
|
files found in the 'CONF_PATH' Paparazzi UAV directory.
|
|
-> Loading of cache data (last settings used) to restore them.
|
|
-> Initialization of main HMI widgets with necessary data found.
|
|
"""
|
|
init_conf_xml_path()
|
|
|
|
# Data object creation implies XML parsing in the parser module :
|
|
try:
|
|
self.data = parser.Data(CONF_PATH)
|
|
|
|
# Cache parameters extracted from the cache dictionary :
|
|
last_geometry = self.data.cache[parser.LAST_GEOMETRY].split(" ")
|
|
last_x, last_y, last_width, last_height = map(int, last_geometry)
|
|
self.setGeometry(last_x, last_y, last_width, last_height)
|
|
|
|
last_set_name = self.data.cache[parser.LAST_SET]
|
|
self.current_set = self.data.sets[last_set_name]
|
|
|
|
point_symbol_link_to(self.current_set.name)
|
|
|
|
last_config_name = self.data.cache[parser.LAST_CONFIG]
|
|
self.current_config = self.data.configurations[last_config_name]
|
|
|
|
last_target_name = self.data.cache[parser.LAST_TARGET]
|
|
self.current_target = self.current_config.targets[last_target_name]
|
|
|
|
self.ui.upload.setEnabled(self.current_target.name
|
|
not in self.simulation_targets_names)
|
|
|
|
last_device_name = self.data.cache[parser.LAST_DEVICE]
|
|
self.current_device = self.data.devices[last_device_name]
|
|
|
|
last_session_name = self.data.cache[parser.LAST_SESSION]
|
|
self.current_session = self.data.sessions[last_session_name]
|
|
|
|
last_log_filters = self.data.cache[parser.LAST_LOG_FILTERS].split(" ")
|
|
last_level = last_log_filters[0]
|
|
last_default, last_info, last_warning, last_error =\
|
|
map(int, last_log_filters[1:])
|
|
self.current_log_filter = cs.LogFilter(last_level,
|
|
last_default, last_info,
|
|
last_warning, last_error)
|
|
except:
|
|
" if something goes wrong, delete cache and load again (to be improved)"
|
|
LOGGER.error("ERROR while load HMI cache"
|
|
"Original message : '%s'.", sys.exc_info()[0])
|
|
print("HMI error in cache, load default instead")
|
|
parser.delete_cache()
|
|
self.init_hmi_data()
|
|
|
|
# Run and build Paparazzi versions found by existing program
|
|
# './paparazzi_version' and file './var/build_version.txt' :
|
|
run_version_cmd = env.RUN_VERSION_EXE
|
|
self.run_version = os.popen(run_version_cmd).readline().strip()
|
|
build_version_cmd = " ".join(["cat", env.BUILD_VERSION_FILE])
|
|
self.build_version = os.popen(build_version_cmd).readline().strip()
|
|
|
|
def init_hmi_widgets(self):
|
|
"""
|
|
-> Initialization of all the HMI widgets content.
|
|
-> Initialization of existing popups.
|
|
"""
|
|
# Update functions used for initialization :
|
|
self.update_home_button()
|
|
self.update_mode_button()
|
|
self.update_versions()
|
|
self.update_set_combo()
|
|
self.update_config_combo()
|
|
self.update_config_items_lists()
|
|
self.update_target_combos(self.target_widgets)
|
|
self.update_log_filters()
|
|
self.change_messages_level()
|
|
self.update_device_combo()
|
|
self.update_session_combos()
|
|
self.update_programs_list()
|
|
self.update_program_options_list()
|
|
self.init_tools_menu()
|
|
|
|
# Popup initialization :
|
|
self.settings = dial.SettingsManager()
|
|
self.data_changed_popup = dial.Popup(dial.DATA_CHANGED_POPUP_TYPE)
|
|
self.credits_popup = dial.Popup(dial.CREDITS_POPUP_TYPE)
|
|
self.tutorial_popup = dial.Popup(dial.TUTORIAL_POPUP_TYPE)
|
|
|
|
def init_all_hmi(self):
|
|
"""
|
|
-> Centralize all initializations before the mainwindow is shown.
|
|
"""
|
|
control_cwd()
|
|
LOGGER.info("HMI init in progress...\n")
|
|
print("\nHMI init in progress...")
|
|
self.init_hmi_data()
|
|
self.init_hmi_widgets()
|
|
self.connect_signals()
|
|
LOGGER.info("HMI init finished.\n")
|
|
print("HMI init finished.\n")
|
|
|
|
###############################################################################
|
|
# [Hmi methods] Connect signals methods
|
|
|
|
def connect_signals(self):
|
|
"""
|
|
-> Connection of 'triggered' signals from 'QMenu' or 'QAction'.
|
|
-> Connection of 'currentIndexChanged' signals from 'QComboBox'.
|
|
-> Connection of 'clicked' signals from 'QPushButton'.
|
|
"""
|
|
#######################################################################
|
|
# Actions in menus
|
|
|
|
self.ui.actionQuit_Paparazzi_UAV.triggered.connect(self.close)
|
|
|
|
self.ui.actionSetManager.triggered.connect(self.settings.show)
|
|
self.ui.actionSave_2.triggered.connect(self.save_current_config)
|
|
|
|
self.ui.actionSave_3.triggered.connect(self.save_current_session)
|
|
|
|
self.ui.actionFull_screen.triggered.connect(self.fullscreen_view)
|
|
|
|
self.ui.actionAbout_Paparazzi_UAV.triggered.connect(
|
|
self.credits_popup.show)
|
|
self.ui.actionTutorial.triggered.connect(self.tutorial_popup.show)
|
|
|
|
#######################################################################
|
|
# Equipment tab
|
|
|
|
for list_widget in self.change_config_widgets:
|
|
list_widget.itemSelectionChanged.connect(functools.partial(
|
|
self.change_current_item, list_widget))
|
|
self.ui.current_settings.itemClicked.connect(
|
|
self.change_setting_check_state)
|
|
|
|
self.ui.current_set.currentIndexChanged.connect(self.change_current_set)
|
|
self.ui.current_configuration.currentIndexChanged.connect(
|
|
self.change_current_config)
|
|
|
|
self.ui.remove_item.clicked.connect(self.remove_item_from_config)
|
|
self.ui.add_item.clicked.connect(self.open_item_file_chooser)
|
|
self.ui.edit.clicked.connect(self.open_gedit)
|
|
|
|
self.ui.quick_build.clicked.connect(self.quick_build)
|
|
|
|
#######################################################################
|
|
# Build / flash tab
|
|
|
|
for combo_widget in self.target_widgets:
|
|
combo_widget.currentIndexChanged.connect(functools.partial(
|
|
self.change_current_target, combo_widget))
|
|
|
|
for widget in self.log_filters_widgets:
|
|
widget.clicked.connect(self.change_sensitivity_filters)
|
|
|
|
for widget in self.messages_level_widgets:
|
|
widget.clicked.connect(self.change_messages_level)
|
|
|
|
self.ui.clean.clicked.connect(self.clean_config)
|
|
self.ui.build.clicked.connect(self.build_config)
|
|
self.ui.upload.clicked.connect(self.flash_device)
|
|
self.ui.show_console.clicked.connect(functools.partial(
|
|
self.switch_to_tab, 3))
|
|
|
|
self.ui.device.currentIndexChanged.connect(self.change_current_device)
|
|
|
|
#######################################################################
|
|
# Session tab
|
|
|
|
for combo_widget in self.session_widgets:
|
|
combo_widget.currentIndexChanged.connect(functools.partial(
|
|
self.change_current_session, combo_widget))
|
|
|
|
self.ui.programs.itemSelectionChanged.connect(
|
|
self.change_current_program)
|
|
self.ui.options.itemSelectionChanged.connect(
|
|
self.change_current_option)
|
|
|
|
self.ui.play_stop_program.clicked.connect(functools.partial(
|
|
self.run_program, None))
|
|
for button in self.start_all_buttons:
|
|
button.clicked.connect(self.start_all_programs)
|
|
for button in self.kill_all_buttons:
|
|
button.clicked.connect(self.kill_all_programs)
|
|
|
|
self.ui.remove_program.clicked.connect(
|
|
self.remove_program_from_session)
|
|
self.ui.remove_option.clicked.connect(self.remove_option_from_program)
|
|
self.ui.add_option.clicked.connect(self.add_option_to_program)
|
|
self.ui.options.itemChanged.connect(self.edit_current_option)
|
|
|
|
#######################################################################
|
|
# Console tab
|
|
|
|
self.ui.clean_console.clicked.connect(self.clean_console)
|
|
|
|
#######################################################################
|
|
# Other widgets connected
|
|
|
|
self.ui.open_home_terminal.clicked.connect(self.open_terminal)
|
|
self.ui.switch_mode.clicked.connect(self.switch_current_mode)
|
|
|
|
###############################################################################
|
|
# [Hmi methods] Reimplemented methods
|
|
|
|
def closeEvent(self, event):
|
|
"""
|
|
:param event: QEvent object automatically filled when 'closed' signal
|
|
caught.
|
|
-> Called automatically when when 'closed' signal caught.
|
|
-> Kill all processes still running.
|
|
-> Update the cache file.
|
|
-> Call the real mainWindow closeEvent to finish the 'app.exec_'
|
|
events loop.
|
|
"""
|
|
for process in [self.clean, self.build, self.upload]:
|
|
# + list(self.running_programs.values()):
|
|
# /!\ REQUIREMENT : DON'T KILL SESSION PROCESSES WHEN THE
|
|
# APPLICATION EXITS...
|
|
if process is not None:
|
|
try:
|
|
process.subprocess.kill()
|
|
except ProcessLookupError:
|
|
print("Process : '%s' already stopped. "
|
|
"Can't be killed again !\n" % process.name)
|
|
else:
|
|
print("Process : '%s' killed !\n" % process.name)
|
|
|
|
# Ask for a confirmation if some data are unsaved and quit saving
|
|
# these data :
|
|
if self.configurations_changed != {} or self.sessions_changed != {}:
|
|
if self.sessions_changed != {parser.SIMULATION_NAME:
|
|
parser.SIMULATION_SESSION} \
|
|
and self.sessions_changed != {parser.REPLAY_NAME:
|
|
parser.REPLAY_SESSION}:
|
|
self.data_changed_popup.show()
|
|
self.data_changed_popup.accepted.connect(
|
|
self.save_all_changed_data)
|
|
self.data_changed_popup.rejected.connect(
|
|
self.save_cache_data)
|
|
else:
|
|
print("A special in-code defined session (Session or Replay) "
|
|
"has been changed and could not have been saved...\n")
|
|
self.save_cache_data()
|
|
else:
|
|
self.save_cache_data()
|
|
|
|
###############################################################################
|
|
# [Hmi methods] Update widgets methods
|
|
|
|
def update_home_button(self):
|
|
self.ui.open_home_terminal.setText(env.PAPARAZZI_HOME)
|
|
|
|
def update_mode_button(self):
|
|
if self.dev_mode:
|
|
self.ui.switch_mode.setText("Developer")
|
|
else:
|
|
self.ui.switch_mode.setText("Standard")
|
|
|
|
def update_versions(self):
|
|
for field, value in zip([self.ui.run_version, self.ui.build_version],
|
|
[self.run_version, self.build_version]):
|
|
if value is not None:
|
|
field.setText(value)
|
|
else:
|
|
field.setText(UNKNOWN_VALUE)
|
|
|
|
def switch_to_tab(self, tab_index):
|
|
self.ui.main_tab.setCurrentIndex(tab_index)
|
|
|
|
def init_tools_menu(self):
|
|
"""
|
|
-> Clear the 'Tools' menu actions (menubar).
|
|
-> Fill the 'Tools' menu with the names found in 'control_panel.xml'
|
|
-> Connect each action to the corresponding tool process.
|
|
"""
|
|
self.ui.menuTools.clear()
|
|
sorted_names = parser.sorted_tools_names(self.data.tools)
|
|
for name in sorted_names:
|
|
action = Widgets.QAction(name, self)
|
|
self.ui.menuTools.addAction(action)
|
|
for action in self.ui.menuTools.actions():
|
|
action.triggered.connect(functools.partial(
|
|
self.add_program_to_session, self.data.tools[action.text()]))
|
|
|
|
def fullscreen_view(self):
|
|
if self.isMaximized():
|
|
self.resize(800, 600)
|
|
else:
|
|
self.showMaximized()
|
|
|
|
def update_set_combo(self):
|
|
"""
|
|
-> Clear the 'Set' combobox.
|
|
-> Fill the combobox with the names found in 'conf.xml'.
|
|
"""
|
|
self.ui.current_set.clear()
|
|
sorted_names = parser.sorted_sets_names(self.data.sets)
|
|
self.ui.current_set.addItems(sorted_names)
|
|
self.ui.current_set.setCurrentText(self.current_set.name)
|
|
|
|
def update_config_combo(self):
|
|
"""
|
|
-> Clear the 'Configuration' combobox.
|
|
-> Fill the combobox with the configurations names corresponding to the
|
|
current selected set.
|
|
-> Save the current settings check state when configuration changed.
|
|
(allows to the configuration to be recovered when selected again)
|
|
"""
|
|
self.ui.current_configuration.clear()
|
|
sorted_names = parser.sorted_current_configs_names(
|
|
self.data.configurations,
|
|
self.current_set)
|
|
self.ui.current_configuration.addItems(sorted_names)
|
|
self.ui.current_configuration.setCurrentText(self.current_config.name)
|
|
|
|
def update_config_items_lists(self):
|
|
"""
|
|
-> Disable or hide widgets and buttons to init config.
|
|
-> Set config ID and color.
|
|
-> Set config items by category.
|
|
-> Make 'settings' items checkable and set check-boxes.
|
|
"""
|
|
set_widgets_visibility(self.display_config_widgets +
|
|
self.change_config_widgets, False)
|
|
set_widgets_availability([self.ui.remove_item,
|
|
self.ui.edit], False)
|
|
set_widgets_visibility(self.flight_plan_buttons, False)
|
|
|
|
self.ui.current_id.setText(self.current_config.id)
|
|
color = gui.generate_qcolor(self.current_config.color[0])
|
|
palette = gui.generate_widget_palette(self.ui.current_color, color)
|
|
self.ui.current_color.setPalette(palette)
|
|
|
|
clear_widgets(self.change_config_widgets)
|
|
for widget, values in zip(self.change_config_widgets,
|
|
[self.current_config.airframes,
|
|
self.current_config.settings +
|
|
self.current_config.modules,
|
|
self.current_config.flight_plan,
|
|
self.current_config.radio,
|
|
self.current_config.telemetry]):
|
|
for value in values:
|
|
value = value.strip()
|
|
if value:
|
|
widget.addItem(value)
|
|
if values:
|
|
set_widgets_visibility([widget], True)
|
|
|
|
for i in range(self.ui.current_settings.count()):
|
|
item = self.ui.current_settings.item(i)
|
|
item.setFlags(Core.Qt.ItemIsUserCheckable | Core.Qt.ItemIsEnabled |
|
|
Core.Qt.ItemIsSelectable)
|
|
if item.text().startswith("["):
|
|
item.setText(item.text().strip("[]"))
|
|
item.setCheckState(Core.Qt.Unchecked)
|
|
else:
|
|
item.setCheckState(Core.Qt.Checked)
|
|
|
|
self.update_config_overview_lists()
|
|
|
|
def update_config_overview_lists(self):
|
|
"""
|
|
-> Hide all the configuration overview widgets.
|
|
-> Clear these widgets.
|
|
-> Copy the content of the real configuration QListWidget widgets.
|
|
-> Keep only the basename of long items (put full name in tooltip).
|
|
-> Display the widgets only if not empty (save some vertical space).
|
|
"""
|
|
set_widgets_visibility(self.display_config_widgets, False)
|
|
clear_widgets(self.display_config_widgets)
|
|
for input_widget, display_widget in zip(self.change_config_widgets*2,
|
|
self.display_config_widgets):
|
|
items_full_names = []
|
|
for i in range(input_widget.count()):
|
|
item_full_name = input_widget.item(i).text()
|
|
item_name = os.path.splitext(
|
|
os.path.basename(item_full_name))[0]
|
|
display_widget.addItem(item_name)
|
|
items_full_names.append(item_full_name)
|
|
display_widget.setToolTip("\n".join(items_full_names))
|
|
if display_widget.count() != 0:
|
|
display_widget.setVisible(True)
|
|
|
|
def update_target_combos(self, combo_widgets):
|
|
"""
|
|
:param combo_widgets:
|
|
-> Clear the 2 'Target' combo widgets.
|
|
-> Fill them with the targets available for the current configuration.
|
|
"""
|
|
clear_widgets(combo_widgets)
|
|
sorted_names = parser.sorted_current_targets_names(
|
|
self.current_config.targets)
|
|
for widget in combo_widgets:
|
|
widget.addItems(sorted_names)
|
|
widget.setCurrentText(self.current_target.name)
|
|
|
|
def update_log_filters(self):
|
|
"""
|
|
-> Load the last filters from the cache.
|
|
-> Disable the filters if the level is minimum or maximum.
|
|
-> Set the filters check state by the current LogFilter object
|
|
properties.
|
|
"""
|
|
if self.current_log_filter.level == cs.MINIMAL_MESSAGES_LEVEL:
|
|
self.ui.important.setChecked(True)
|
|
set_widgets_availability(self.log_filters_widgets, False)
|
|
elif self.current_log_filter.level == cs.ALL_MESSAGES_LEVEL:
|
|
self.ui.all.setChecked(True)
|
|
set_widgets_availability(self.log_filters_widgets, False)
|
|
else:
|
|
self.ui.custom.setChecked(True)
|
|
set_widgets_availability(self.log_filters_widgets, True)
|
|
self.ui.display_default.setChecked(self.current_log_filter.default)
|
|
self.ui.display_info.setChecked(self.current_log_filter.info)
|
|
self.ui.display_warnings.setChecked(self.current_log_filter.warning)
|
|
self.ui.display_errors.setChecked(self.current_log_filter.error)
|
|
|
|
def update_device_combo(self):
|
|
"""
|
|
-> Clear the 'Device' combobox.
|
|
-> Search compatible boards for current target.
|
|
-> Update the combo-box with devices corresponding to each board.
|
|
-> Deal with empty devices list because of default Wi-Fi flash.
|
|
"""
|
|
self.ui.device.clear()
|
|
current_devices = []
|
|
for device in self.data.devices.values():
|
|
if device.name != parser.DEFAULT_DEVICE_NAME:
|
|
for board_regex in device.boards_regex:
|
|
found_match = re.search(board_regex, self.current_target.board)
|
|
if found_match:
|
|
current_devices.append(device)
|
|
break
|
|
else:
|
|
current_devices.append(device)
|
|
sorted_names = parser.sorted_current_devices_names(current_devices)
|
|
self.ui.device.addItems(sorted_names)
|
|
self.ui.device.setCurrentText(self.current_device.name)
|
|
self.ui.device.setEnabled(True)
|
|
|
|
def update_session_combos(self):
|
|
"""
|
|
-> Clear the 'Session' combobox.
|
|
-> Fill it with the sessions found in 'control_panel.xml'
|
|
"""
|
|
clear_widgets(self.session_widgets)
|
|
sorted_names = parser.sorted_sessions_names(self.data.sessions)
|
|
for widget in self.session_widgets:
|
|
widget.addItems(sorted_names)
|
|
sep_index = sorted_names.index(parser.SESSIONS_COMBO_SEP)
|
|
variant = Core.QVariant(not Core.Qt.ItemIsEnabled)
|
|
widget.setItemData(sep_index, variant, Core.Qt.UserRole - 1)
|
|
widget.setCurrentText(self.current_session.name)
|
|
|
|
def update_programs_list(self):
|
|
"""
|
|
-> Clear the programs QListWidget.
|
|
-> Fill it with the programs available for the current session.
|
|
-> Update the programs overview widgets with the same programs.
|
|
-> Init the play/stop program button HMI.
|
|
"""
|
|
self.ui.programs.clear()
|
|
self.current_program = None
|
|
sorted_names = parser.sorted_current_programs_names(
|
|
self.current_session)
|
|
self.ui.programs.addItems(sorted_names)
|
|
for i in range(self.ui.programs.count()):
|
|
self.ui.programs.item(i).setIcon(self.stop_qicon)
|
|
self.update_programs_overviews()
|
|
self.update_program_button()
|
|
self.update_program_options_list()
|
|
|
|
set_widgets_availability([self.ui.play_stop_program,
|
|
self.ui.remove_program,
|
|
self.ui.add_option] +
|
|
self.kill_all_buttons, False)
|
|
|
|
def update_programs_overviews(self):
|
|
"""
|
|
-> Clear the programs overview QListWidget widgets.
|
|
-> Fill them with the current programs widget items.
|
|
-> Set their icon to stopped state.
|
|
"""
|
|
clear_widgets(self.display_programs_widgets)
|
|
for input_widget, display_widget in zip([self.ui.programs]*2,
|
|
self.display_programs_widgets):
|
|
for i in range(input_widget.count()):
|
|
display_widget.addItem(input_widget.item(i).text())
|
|
item = display_widget.item(i)
|
|
item.setIcon(self.stop_qicon)
|
|
item.setFlags(Core.Qt.ItemIsEnabled)
|
|
|
|
def update_program_options_list(self):
|
|
"""
|
|
-> Clear the options QListWidget.
|
|
-> Fill them with the current options of the selected program if
|
|
possible (and detect the option shape : tuple or str).
|
|
"""
|
|
self.ui.options.clear()
|
|
self.current_option = None
|
|
if self.current_program is not None:
|
|
options = self.current_program[1].options
|
|
for option in options:
|
|
option_str = option
|
|
if type(option) is tuple:
|
|
option_str = " ".join(option)
|
|
self.ui.options.addItem(option_str)
|
|
for i in range(self.ui.options.count()):
|
|
item = self.ui.options.item(i)
|
|
item.setFlags(Core.Qt.ItemIsEditable | Core.Qt.ItemIsEnabled |
|
|
Core.Qt.ItemIsSelectable)
|
|
self.ui.remove_option.setEnabled(False)
|
|
|
|
def init_build_flash_hmi(self):
|
|
"""
|
|
-> Kill the build processes if already running.
|
|
-> Clear the build widgets.
|
|
"""
|
|
clear_widgets([self.ui.build_result_icon,
|
|
self.ui.build_result,
|
|
self.ui.flash_resut_icon,
|
|
self.ui.flash_result] + self.messages_nb_widgets)
|
|
|
|
def update_flags_nb_hmi(self, compilation_object):
|
|
"""
|
|
:param compilation_object:
|
|
-> Update the flags number QLabel widgets at each log sent to the
|
|
console.
|
|
"""
|
|
self.ui.errors_nb.setText(str(
|
|
compilation_object.flags[cs.ERROR_FLAG]))
|
|
self.ui.warnings_nb.setText(str(
|
|
compilation_object.flags[cs.WARNING_FLAG]))
|
|
self.ui.info_nb.setText(str(
|
|
compilation_object.flags[cs.INFO_FLAG]))
|
|
|
|
###############################################################################
|
|
# [Hmi methods] Clean methods
|
|
|
|
def set_clean_started_hmi(self):
|
|
"""
|
|
-> Turn HMI to clean running state.
|
|
"""
|
|
self.init_build_flash_hmi()
|
|
self.clean_running = True
|
|
set_widgets_availability([self.ui.build, self.ui.upload,
|
|
self.ui.quick_build] +
|
|
self.target_widgets, False)
|
|
self.ui.clean.setText("Stop !")
|
|
self.setCursor(gui.generate_qcursor(gui.WAIT_CURSOR_SHAPE))
|
|
|
|
def set_clean_stopped_hmi(self):
|
|
"""
|
|
-> Turn HMI to clean stopped state.
|
|
-> Display the result of the clean process.
|
|
-> Switch to the build / flash tab.
|
|
"""
|
|
self.clean_running = False
|
|
set_widgets_availability([self.ui.build, self.ui.quick_build,
|
|
self.ui.clean] +
|
|
self.target_widgets, True)
|
|
if self.current_target.name not in self.simulation_targets_names:
|
|
self.ui.upload.setEnabled(True)
|
|
self.ui.clean.setText("Clean")
|
|
self.setCursor(gui.generate_qcursor(gui.NORMAL_CURSOR_SHAPE))
|
|
self.update_flags_nb_hmi(self.clean)
|
|
errors = self.clean.flags[cs.ERROR_FLAG]
|
|
if self.clean.exit_code == proc.INTERRUPTED_EXIT_CODE:
|
|
self.ui.build_result.setText("Clean aborted by user !")
|
|
self.ui.build_result_icon.setPixmap(self.error_qpixmap)
|
|
elif errors != 0 or self.clean.exit_code == proc.DEFAULT_EXIT_CODE:
|
|
self.ui.build_result.setText("Error(s) found " +
|
|
"during clean. " +
|
|
"Please try again !")
|
|
self.ui.build_result_icon.setPixmap(self.error_qpixmap)
|
|
elif self.clean.exit_code == proc.SUCCESS_EXIT_CODE:
|
|
self.ui.build_result.setText("Clean done with success !")
|
|
self.ui.build_result_icon.setPixmap(self.ok_qpixmap)
|
|
else:
|
|
self.ui.build_result.setText("Unknown error !")
|
|
self.ui.build_result_icon.setPixmap(self.error_qpixmap)
|
|
self.clean = None
|
|
self.switch_to_tab(1)
|
|
|
|
###############################################################################
|
|
# [Hmi methods] Build methods
|
|
|
|
def set_build_started_hmi(self):
|
|
"""
|
|
-> Turn HMI to build running state.
|
|
"""
|
|
self.init_build_flash_hmi()
|
|
self.build_running = True
|
|
set_widgets_availability([self.ui.clean, self.ui.upload,
|
|
self.ui.quick_build] +
|
|
self.target_widgets, False)
|
|
self.ui.build.setText("Stop !")
|
|
self.setCursor(gui.generate_qcursor(gui.WAIT_CURSOR_SHAPE))
|
|
|
|
def set_build_stopped_hmi(self):
|
|
"""
|
|
-> Turn HMI to build stopped state.
|
|
-> Display the result of the build process.
|
|
-> Switch to the build / flash tab.
|
|
"""
|
|
self.build_running = False
|
|
set_widgets_availability([self.ui.clean, self.ui.quick_build,
|
|
self.ui.build] +
|
|
self.target_widgets, True)
|
|
if self.current_target.name not in self.simulation_targets_names:
|
|
self.ui.upload.setEnabled(True)
|
|
self.ui.build.setText("Build")
|
|
self.setCursor(gui.generate_qcursor(gui.NORMAL_CURSOR_SHAPE))
|
|
self.update_flags_nb_hmi(self.build)
|
|
errors = self.build.flags[cs.ERROR_FLAG]
|
|
if self.build.exit_code == proc.INTERRUPTED_EXIT_CODE:
|
|
self.ui.build_result.setText("Build aborted by user !")
|
|
self.ui.build_result_icon.setPixmap(self.error_qpixmap)
|
|
elif errors != 0 or self.build.exit_code == proc.DEFAULT_EXIT_CODE:
|
|
self.ui.build_result.setText("Error(s) found " +
|
|
"during build. " +
|
|
"Please try again !")
|
|
self.ui.build_result_icon.setPixmap(self.error_qpixmap)
|
|
elif self.build.exit_code == proc.SUCCESS_EXIT_CODE:
|
|
self.ui.build_result.setText("Build done with success !")
|
|
self.ui.build_result_icon.setPixmap(self.ok_qpixmap)
|
|
else:
|
|
self.ui.build_result.setText("Unknown error !")
|
|
self.ui.build_result_icon.setPixmap(self.error_qpixmap)
|
|
self.build = None
|
|
self.switch_to_tab(1)
|
|
|
|
###############################################################################
|
|
# [Hmi methods] Flash methods
|
|
|
|
def set_flash_started_hmi(self):
|
|
"""
|
|
-> Turn HMI to flash running state.
|
|
"""
|
|
self.init_build_flash_hmi()
|
|
self.upload_running = True
|
|
set_widgets_availability([self.ui.clean, self.ui.build,
|
|
self.ui.quick_build, self.ui.device] +
|
|
self.target_widgets, False)
|
|
self.ui.upload.setText("Stop !")
|
|
self.setCursor(gui.generate_qcursor(gui.WAIT_CURSOR_SHAPE))
|
|
clear_widgets([self.ui.flash_resut_icon,
|
|
self.ui.flash_result])
|
|
|
|
def set_flash_stopped_hmi(self):
|
|
"""
|
|
-> Turn HMI to flash stopped state.
|
|
-> Display the result of the upload process.
|
|
-> Switch to the build / flash tab.
|
|
"""
|
|
self.upload_running = False
|
|
set_widgets_availability([self.ui.clean, self.ui.build,
|
|
self.ui.upload,
|
|
self.ui.quick_build, self.ui.device] +
|
|
self.target_widgets, True)
|
|
self.ui.upload.setText("Upload")
|
|
self.setCursor(gui.generate_qcursor(gui.NORMAL_CURSOR_SHAPE))
|
|
self.update_flags_nb_hmi(self.upload)
|
|
errors = self.upload.flags[cs.ERROR_FLAG]
|
|
if self.upload.exit_code == proc.INTERRUPTED_EXIT_CODE:
|
|
self.ui.flash_result.setText("Upload aborted by user !")
|
|
self.ui.flash_resut_icon.setPixmap(self.error_qpixmap)
|
|
elif errors != 0 or self.upload.exit_code == proc.DEFAULT_EXIT_CODE:
|
|
self.ui.flash_result.setText("Error(s) found " +
|
|
"during upload. " +
|
|
"Please try again !")
|
|
self.ui.flash_resut_icon.setPixmap(self.error_qpixmap)
|
|
elif self.upload.exit_code == proc.SUCCESS_EXIT_CODE:
|
|
self.ui.flash_result.setText("Upload done with success !")
|
|
self.ui.flash_resut_icon.setPixmap(self.ok_qpixmap)
|
|
else:
|
|
self.ui.flash_result.setText("Unknown error !")
|
|
self.ui.flash_resut_icon.setPixmap(self.error_qpixmap)
|
|
self.upload = None
|
|
self.switch_to_tab(1)
|
|
|
|
###############################################################################
|
|
# [Hmi methods] Program methods
|
|
|
|
def update_program_button(self):
|
|
"""
|
|
-> Update the play/stop program button HMI to correct state.
|
|
"""
|
|
if self.current_program is not None:
|
|
self.ui.play_stop_program.setEnabled(True)
|
|
if self.current_program[1].name in self.running_programs.keys():
|
|
self.ui.play_stop_program.setIcon(self.stop_qicon)
|
|
self.ui.play_stop_program.setText("Stop")
|
|
else:
|
|
self.ui.play_stop_program.setIcon(self.start_qicon)
|
|
self.ui.play_stop_program.setText("Start")
|
|
else:
|
|
self.ui.play_stop_program.setEnabled(False)
|
|
self.ui.play_stop_program.setIcon(self.start_qicon)
|
|
|
|
def set_program_started_hmi(self, program_process):
|
|
"""
|
|
:param program_process:
|
|
-> Turn HMI to program running state.
|
|
"""
|
|
self.running_programs[program_process.program.name] = program_process
|
|
|
|
items = [widget.findItems(program_process.program.name,
|
|
Core.Qt.MatchExactly)[0]
|
|
for widget in [self.ui.programs,
|
|
self.ui.programs_overview_1,
|
|
self.ui.programs_overview_2]]
|
|
set_items_icons(items, self.start_qicon)
|
|
self.update_program_button()
|
|
|
|
if self.running_programs != {}:
|
|
set_widgets_availability([self.ui.session,
|
|
self.ui.session_overview_1,
|
|
self.ui.session_overview_2,
|
|
self.ui.current_set],
|
|
False)
|
|
set_widgets_availability(self.kill_all_buttons, True)
|
|
set_widgets_availability(self.start_all_buttons, False)
|
|
|
|
def set_program_stopped_hmi(self, program_process):
|
|
"""
|
|
:param program_process:
|
|
-> Turn HMI to program stopped state.
|
|
-> Display the result of the program process.
|
|
-> Switch to the sessions management tab.
|
|
"""
|
|
self.running_programs.pop(program_process.program.name)
|
|
|
|
items = [widget.findItems(program_process.program.name,
|
|
Core.Qt.MatchExactly)[0]
|
|
for widget in [self.ui.programs,
|
|
self.ui.programs_overview_1,
|
|
self.ui.programs_overview_2]]
|
|
set_items_icons(items, self.stop_qicon)
|
|
self.update_program_button()
|
|
|
|
if self.running_programs == {}:
|
|
set_widgets_availability([self.ui.session,
|
|
self.ui.session_overview_1,
|
|
self.ui.session_overview_2,
|
|
self.ui.current_set],
|
|
True)
|
|
set_widgets_availability(self.kill_all_buttons, False)
|
|
set_widgets_availability(self.start_all_buttons, True)
|
|
self.switch_to_tab(2)
|
|
|
|
###############################################################################
|
|
# [Hmi methods] Write log methods
|
|
|
|
def write_log_in_console(self, log, flag, log_type):
|
|
"""
|
|
:param log:
|
|
:param flag:
|
|
:param log_type:
|
|
-> Get the logs sent by the processes when a 'log_sent' signal is
|
|
caught.
|
|
-> Treat the log depending if it's an application or a process one.
|
|
-> Put color to the log line and write it into the console.
|
|
"""
|
|
if log_type == cs.PROCESS_MESSAGE_TYPE:
|
|
if self.current_log_filter.write_line_decision(flag):
|
|
str_color = cs.BACKGROUNDS_COLORS[flag]
|
|
background_qcolor = gui.generate_qcolor(str_color)
|
|
self.console.write(log,
|
|
backgroud_color=background_qcolor)
|
|
elif log_type == cs.APPLICATION_MESSAGE_TYPE:
|
|
if log != "\n":
|
|
str_color = cs.FONTS_COLORS[flag]
|
|
font_qcolor = gui.generate_qcolor(str_color)
|
|
self.console.write(log, font_color=font_qcolor)
|
|
|
|
def animate_progress_bar(self):
|
|
"""
|
|
-> For all running build / flash processes, animate the progress bar.
|
|
-> Change the text displayed in the result field according to its size
|
|
and the current direction.
|
|
"""
|
|
for program, label in zip([self.clean, self.build, self.upload],
|
|
[self.ui.build_result, self.ui.build_result,
|
|
self.ui.flash_result]):
|
|
if program is not None:
|
|
last_result_text = label.text()
|
|
last_result_text_size = label.fontMetrics().\
|
|
boundingRect(last_result_text).width()
|
|
field_width = label.width()
|
|
|
|
if len(last_result_text) < PROGRESS_BAR_SPEED+1:
|
|
new_result_text = PROGRESS_BAR_LINE * PROGRESS_BAR_SPEED \
|
|
+ PROGRESS_BAR_CURSOR
|
|
self.progress_bar_to_left = False
|
|
elif last_result_text_size > field_width - 100\
|
|
or self.progress_bar_to_left:
|
|
new_result_text = last_result_text[PROGRESS_BAR_SPEED:]
|
|
self.progress_bar_to_left = True
|
|
else:
|
|
new_result_text = PROGRESS_BAR_LINE * PROGRESS_BAR_SPEED \
|
|
+ last_result_text
|
|
self.progress_bar_to_left = False
|
|
label.setText(new_result_text)
|
|
|
|
###############################################################################
|
|
# [Hmi methods] Widgets connected with signals methods
|
|
|
|
def switch_current_mode(self):
|
|
"""
|
|
-> Real switch but no real effect (no dev mode yet...)
|
|
-> RELOAD MAIN WINDOW WITH NEW WIDGETS ?
|
|
"""
|
|
self.dev_mode = not self.dev_mode
|
|
self.update_mode_button()
|
|
|
|
def change_current_set(self):
|
|
"""
|
|
-> Get current set when combobox value changed.
|
|
-> Symbolic link to 'config.xml' changed to the new set.
|
|
-> Update configuration and its items.
|
|
"""
|
|
self.save_all_changed_configurations()
|
|
current_set_name = self.ui.current_set.currentText()
|
|
if current_set_name:
|
|
self.current_set = self.data.sets[current_set_name]
|
|
point_symbol_link_to(self.current_set.name)
|
|
self.update_config_combo()
|
|
self.update_config_items_lists()
|
|
|
|
def change_current_config(self):
|
|
"""
|
|
-> Get current configuration when combobox value changed.
|
|
-> Update target and device values.
|
|
-> Stop the build processes if some running.
|
|
-> Init the build / flash HMI to be ready to a new process.
|
|
"""
|
|
current_config_name = self.ui.current_configuration.currentText()
|
|
if current_config_name:
|
|
self.current_config = self.data.configurations[current_config_name]
|
|
self.update_config_items_lists()
|
|
self.update_target_combos([self.ui.target])
|
|
self.update_device_combo()
|
|
self.init_build_flash_hmi()
|
|
|
|
def change_current_item(self, list_widget):
|
|
"""
|
|
:param list_widget:
|
|
-> Update the current selected item : (widget, item, object).
|
|
-> Clear the others selected items in other categories.
|
|
-> Update the configuration management HMI (buttons).
|
|
-> Make the flight plan buttons appear or disappear.
|
|
-> Update all the check states of the settings list if the current
|
|
selected item is a setting.
|
|
"""
|
|
current_selected_item = list_widget.currentItem()
|
|
if current_selected_item is not None:
|
|
current_item_text = current_selected_item.text()
|
|
self.current_item = (list_widget,
|
|
current_selected_item,
|
|
current_item_text)
|
|
widgets_to_clear = [_ for _ in self.change_config_widgets]
|
|
widgets_to_clear.remove(list_widget)
|
|
clear_list_widgets_selection(widgets_to_clear)
|
|
set_widgets_availability([self.ui.remove_item,
|
|
self.ui.edit], True)
|
|
set_widgets_visibility(self.flight_plan_buttons,
|
|
list_widget.objectName() ==
|
|
"current_flight_plan")
|
|
|
|
def change_current_target(self, combo_widget):
|
|
"""
|
|
:param combo_widget:
|
|
-> Update the current target and the devices combobox (related).
|
|
-> Possible to change the target from 2 widgets. That's why the
|
|
solution found is strange...
|
|
"""
|
|
current_target_name = combo_widget.currentText()
|
|
if current_target_name and self.change_target_bool:
|
|
self.current_target = self.current_config.targets[
|
|
current_target_name]
|
|
self.ui.upload.setEnabled(self.current_target.name
|
|
not in self.simulation_targets_names)
|
|
other_widgets = [_ for _ in self.target_widgets]
|
|
other_widgets.remove(combo_widget)
|
|
# TODO BETTER : AVOID THE SIGNALS PING-PONG BETWEEN TARGETS WIDGETS
|
|
self.change_target_bool = False
|
|
self.update_target_combos(other_widgets)
|
|
self.change_target_bool = True
|
|
self.update_device_combo()
|
|
|
|
def change_sensitivity_filters(self):
|
|
"""
|
|
-> Get checkbox values to set the current filters.
|
|
-> Update the filters values for the console.
|
|
"""
|
|
self.current_log_filter.set_sensitivity_filter(
|
|
self.ui.display_default.checkState(),
|
|
self.ui.display_info.checkState(),
|
|
self.ui.display_warnings.checkState(),
|
|
self.ui.display_errors.checkState())
|
|
self.update_log_filters()
|
|
|
|
def change_messages_level(self):
|
|
"""
|
|
-> Get radiobutton value to set the current message level.
|
|
-> Update the level value for the console.
|
|
"""
|
|
if self.ui.all.isChecked():
|
|
self.current_log_filter.set_level(cs.ALL_MESSAGES_LEVEL)
|
|
self.current_log_filter.set_sensitivity_filter(2, 2, 2, 2)
|
|
elif self.ui.custom.isChecked():
|
|
self.current_log_filter.set_level(cs.CUSTOM_MESSAGES_LEVEL)
|
|
elif self.ui.important.isChecked():
|
|
self.current_log_filter.set_level(cs.MINIMAL_MESSAGES_LEVEL)
|
|
self.current_log_filter.set_sensitivity_filter(0, 0, 2, 2)
|
|
self.update_log_filters()
|
|
|
|
def change_current_device(self):
|
|
"""
|
|
-> Get current device when combobox value changed.
|
|
-> If no device available, the default flash mode is WiFi.
|
|
"""
|
|
current_device_name = self.ui.device.currentText()
|
|
if current_device_name:
|
|
self.current_device = self.data.devices[current_device_name]
|
|
|
|
def change_current_session(self, combo_widget):
|
|
"""
|
|
:param combo_widget:
|
|
-> Get current session when combobox value changed.
|
|
-> Update programs HMI for the new selected session.
|
|
-> Deselect the last selected program and clear the options.
|
|
"""
|
|
current_session_name = combo_widget.currentText()
|
|
if current_session_name:
|
|
clear_list_widgets_selection([self.ui.programs] +
|
|
self.display_programs_widgets)
|
|
self.current_session = self.data.sessions[current_session_name]
|
|
widgets = [_ for _ in self.session_widgets]
|
|
widgets.remove(combo_widget)
|
|
for widget in widgets:
|
|
widget.setCurrentText(self.current_session.name)
|
|
self.update_programs_list()
|
|
self.update_program_options_list()
|
|
self.ui.options.clear()
|
|
|
|
def change_current_program(self):
|
|
"""
|
|
-> Get current selected program when QListWidget selection changed.
|
|
-> Update options HMI for the new selected program.
|
|
"""
|
|
current_selected_item = self.ui.programs.currentItem()
|
|
if current_selected_item is not None:
|
|
item_name = current_selected_item.text()
|
|
if item_name in self.current_session.programs.keys():
|
|
self.current_program = (current_selected_item,
|
|
self.current_session.programs[
|
|
item_name])
|
|
self.ui.remove_program.setEnabled(item_name not in
|
|
self.running_programs.keys())
|
|
self.ui.add_option.setEnabled(True)
|
|
self.update_program_options_list()
|
|
self.update_program_button()
|
|
|
|
def change_current_option(self):
|
|
"""
|
|
-> Get the current selected option when QListWidget selection changed.
|
|
-> Enable options management HMI.
|
|
"""
|
|
current_selected_item = self.ui.options.currentItem()
|
|
if current_selected_item is not None:
|
|
item_name = current_selected_item.text()
|
|
self.current_option = (current_selected_item, item_name)
|
|
self.ui.remove_option.setEnabled(True)
|
|
|
|
def open_item_file_chooser(self):
|
|
"""
|
|
-> While left part of equipment tab is not finished, a simple file
|
|
chooser is used to select new equipment items.
|
|
-> Add the selected new item to the correct category if possible.
|
|
"""
|
|
file_chooser = Widgets.QFileDialog()
|
|
fc_title = "Select a file to add to '" +\
|
|
self.current_config.name + "'..."
|
|
fc_dir = env.PAPARAZZI_CONF
|
|
fc_filter = "XML file(*.xml)"
|
|
path, other = file_chooser.getOpenFileName(file_chooser,
|
|
fc_title,
|
|
fc_dir,
|
|
fc_filter)
|
|
if path:
|
|
filename = parser.full_to_conf_path(path)
|
|
self.add_item_to_config(filename)
|
|
|
|
def open_gedit(self):
|
|
"""
|
|
-> Open a text editor depending on the OS found.
|
|
"""
|
|
if self.current_item is not None:
|
|
|
|
if env.OS in ["linux", "linux2"]:
|
|
file_path = os.path.join(CONF_PATH, self.current_item[2])
|
|
command = " ".join(["gedit", file_path])
|
|
try:
|
|
os.popen(command)
|
|
except ChildProcessError:
|
|
LOGGER.error("Gedit text editor is not available !\n")
|
|
self.switch_to_tab(3)
|
|
elif env.OS in []:
|
|
pass
|
|
else:
|
|
LOGGER.error("No text file editor found for your OS !\n")
|
|
self.switch_to_tab(3)
|
|
|
|
def clean_console(self):
|
|
"""
|
|
-> Just clear the console QTextEdit widget.
|
|
"""
|
|
self.ui.console.clear()
|
|
|
|
def open_terminal(self):
|
|
"""
|
|
-> Open a terminal in the Paparazzi home directory depending on the OS
|
|
found.
|
|
"""
|
|
if env.OS in ["linux", "linux2"]:
|
|
command_terms = ["gnome-terminal", "--working-directory ",
|
|
env.PAPARAZZI_HOME]
|
|
command = " ".join(command_terms)
|
|
os.popen(command)
|
|
else:
|
|
LOGGER.error("No terminal shortcut available for your OS !\n")
|
|
self.switch_to_tab(3)
|
|
|
|
###############################################################################
|
|
# [Hmi methods] Processes management (connected with signals) methods
|
|
|
|
def quick_build(self):
|
|
"""
|
|
-> Clean the current configuration when the quick build button is
|
|
clicked.
|
|
-> Give the information to the clean function that a build process
|
|
is planned after the clean process.
|
|
"""
|
|
self.switch_to_tab(1)
|
|
self.clean_config(is_quick_build=True)
|
|
|
|
def clean_config(self, is_quick_build=False):
|
|
"""
|
|
:param is_quick_build:
|
|
-> Clean the current configuration if not already started.
|
|
-> Else, abort the current clean process and set the HMI to the
|
|
initial state.
|
|
"""
|
|
if not self.clean_running:
|
|
self.clean = proc.Process(proc.CLEAN,
|
|
configuration=self.current_config)
|
|
if self.clean.check_before_start():
|
|
self.clean.start()
|
|
self.clean.subprocess.finished.connect(self.set_clean_stopped_hmi)
|
|
self.clean.process_log_sent.connect(functools.partial(
|
|
self.update_flags_nb_hmi, self.clean))
|
|
self.clean.process_log_sent.connect(self.write_log_in_console)
|
|
self.clean.process_log_sent.connect(self.animate_progress_bar)
|
|
self.set_clean_started_hmi()
|
|
|
|
if is_quick_build:
|
|
self.clean.subprocess.finished.connect(functools.partial(
|
|
self.build_config))
|
|
else:
|
|
LOGGER.error("Incorrect config, can't clean !\n")
|
|
self.switch_to_tab(3)
|
|
else:
|
|
self.clean.process_killed.emit()
|
|
self.ui.clean.setEnabled(False)
|
|
|
|
def build_config(self):
|
|
"""
|
|
-> Build the current configuration if not already started.
|
|
-> Else, abort the current build process and set the HMI to the
|
|
initial state.
|
|
"""
|
|
if not self.build_running:
|
|
self.build = proc.Process(proc.BUILD,
|
|
configuration=self.current_config,
|
|
target=self.current_target)
|
|
self.save_current_config()
|
|
|
|
if self.build.check_before_start():
|
|
self.build.start()
|
|
self.build.subprocess.finished.connect(self.set_build_stopped_hmi)
|
|
self.build.process_log_sent.connect(functools.partial(
|
|
self.update_flags_nb_hmi, self.build))
|
|
self.build.process_log_sent.connect(self.write_log_in_console)
|
|
self.build.process_log_sent.connect(self.animate_progress_bar)
|
|
self.set_build_started_hmi()
|
|
else:
|
|
LOGGER.error("Incorrect config, can't build !\n")
|
|
self.switch_to_tab(3)
|
|
else:
|
|
self.build.process_killed.emit()
|
|
self.ui.build.setEnabled(False)
|
|
|
|
def flash_device(self):
|
|
"""
|
|
-> Flash the current configuration if not already started.
|
|
-> Else, abort the current upload process and set the HMI to the
|
|
initial state.
|
|
"""
|
|
if not self.upload_running:
|
|
self.upload = proc.Process(proc.UPLOAD,
|
|
configuration=self.current_config,
|
|
target=self.current_target,
|
|
device=self.current_device)
|
|
if self.upload.check_before_start():
|
|
self.upload.start()
|
|
self.upload.subprocess.finished.connect(self.set_flash_stopped_hmi)
|
|
self.upload.process_log_sent.connect(functools.partial(
|
|
self.update_flags_nb_hmi, self.upload))
|
|
self.upload.process_log_sent.connect(self.write_log_in_console)
|
|
self.upload.process_log_sent.connect(self.animate_progress_bar)
|
|
self.set_flash_started_hmi()
|
|
else:
|
|
LOGGER.error("Incorrect device, can't upload !\n")
|
|
self.switch_to_tab(3)
|
|
else:
|
|
self.upload.process_killed.emit()
|
|
self.ui.upload.setEnabled(False)
|
|
|
|
def run_program(self, program):
|
|
"""
|
|
:param program:
|
|
-> Run the current selected program if not already started.
|
|
Else, abort it and set the HMI to the initial state.
|
|
"""
|
|
if program is None and self.current_program is not None:
|
|
program_item, program = self.current_program
|
|
if program.name not in self.running_programs.keys():
|
|
new_program_proc = proc.Process(proc.PROGRAM,
|
|
program=program,
|
|
configuration=self.current_config,
|
|
target=self.current_target)
|
|
if new_program_proc.check_before_start():
|
|
new_program_proc.start()
|
|
new_program_proc.subprocess.finished.connect(functools.partial(
|
|
self.set_program_stopped_hmi, new_program_proc))
|
|
new_program_proc.process_log_sent.connect(
|
|
self.write_log_in_console)
|
|
self.set_program_started_hmi(new_program_proc)
|
|
else:
|
|
self.running_programs[program.name].process_killed.emit()
|
|
self.ui.play_stop_program.setEnabled(False)
|
|
|
|
def start_all_programs(self):
|
|
"""
|
|
-> Kill all running programs of the current session.
|
|
-> Start all programs not running (so it's a restart).
|
|
-> Keep the current selected program if there's one.
|
|
"""
|
|
for program in self.current_session.programs.values():
|
|
if program.name not in self.running_programs.keys():
|
|
self.run_program(program)
|
|
|
|
def kill_all_programs(self):
|
|
"""
|
|
-> Kill all running programs of the current session.
|
|
-> Keep the current selected program if there's one.
|
|
"""
|
|
for program in self.current_session.programs.values():
|
|
if program.name in self.running_programs.keys():
|
|
self.run_program(program)
|
|
|
|
###############################################################################
|
|
# [Hmi methods] Data management (connected with signals) methods
|
|
|
|
def change_setting_check_state(self, clicked_item):
|
|
"""
|
|
:param clicked_item:
|
|
-> Save the new check state of settings items.
|
|
-> Add the current config to changed configs => ask to save
|
|
before quitting the application.
|
|
"""
|
|
if self.current_config is not None\
|
|
and self.current_item is not None:
|
|
list_widget, list_item, item_name = self.current_item
|
|
setting_name = item_name
|
|
setting_state = list_item.checkState()
|
|
# TODO BETTER : NO SOLUTION (OR SIGNAL) FOUND TO DETECT A CLICK ON
|
|
# : TODO : 'QListWidgetItem' SO ONLY CURRENT ITEM CHECKSTATE
|
|
# : TODO : SHOULD BE CHANGED... (NOT WORKING WELL)
|
|
if clicked_item == list_item:
|
|
if parser.SETTINGS_REF in setting_name:
|
|
settings_modules = self.current_config.settings
|
|
else:
|
|
settings_modules = self.current_config.modules
|
|
settings_modules_names = [item.strip("[]") for item in settings_modules]
|
|
setting_index = settings_modules_names.index(setting_name)
|
|
settings_modules.pop(setting_index)
|
|
if not setting_state:
|
|
setting_name = "[" + setting_name + "]"
|
|
settings_modules.insert(setting_index, setting_name)
|
|
|
|
self.configurations_changed[self.current_config.name] = \
|
|
self.current_config
|
|
index = self.ui.current_configuration.currentIndex()
|
|
self.ui.current_configuration.setItemIcon(index, self.changed_qicon)
|
|
else:
|
|
clicked_item.setCheckState(setting_state)
|
|
|
|
def remove_item_from_config(self):
|
|
"""
|
|
-> Remove an item from the current selected configuration.
|
|
-> Impossible to remove all airframes because it doesn't make sense.
|
|
"""
|
|
if self.current_config is not None \
|
|
and self.current_item is not None:
|
|
list_widget, list_item, item_name = self.current_item
|
|
if not (list_widget is self.ui.current_airframes and
|
|
len(list_widget) == 1):
|
|
if list_widget == self.ui.current_settings and \
|
|
not list_item.checkState():
|
|
item_name = "[" + item_name + "]"
|
|
|
|
config_attributes_lists = [self.current_config.airframes,
|
|
self.current_config.settings,
|
|
self.current_config.modules,
|
|
self.current_config.flight_plan,
|
|
self.current_config.radio,
|
|
self.current_config.telemetry]
|
|
for attributes_list in config_attributes_lists:
|
|
if item_name in attributes_list:
|
|
attributes_list.remove(item_name)
|
|
break
|
|
|
|
list_widget.takeItem(list_widget.row(list_item))
|
|
list_widget.clearSelection()
|
|
self.current_item = None
|
|
self.ui.remove_item.setEnabled(False)
|
|
if not list_widget:
|
|
list_widget.setVisible(False)
|
|
if list_widget is self.ui.current_flight_plan:
|
|
set_widgets_visibility(self.flight_plan_buttons, False)
|
|
self.update_config_overview_lists()
|
|
|
|
self.configurations_changed[self.current_config.name] = \
|
|
self.current_config
|
|
index = self.ui.current_configuration.currentIndex()
|
|
self.ui.current_configuration.setItemIcon(index,
|
|
self.changed_qicon)
|
|
else:
|
|
LOGGER.error("You can't remove all airframes from a "
|
|
"configuration !\n")
|
|
self.switch_to_tab(3)
|
|
|
|
def add_item_to_config(self, filename):
|
|
"""
|
|
:param filename:
|
|
-> Add the selected filename to the correct category if it's possible.
|
|
-> Else, an error is raised in the console.
|
|
-> If a setting is added, it's checked by default.
|
|
"""
|
|
if self.current_config is not None:
|
|
keywords = [parser.AIRFRAME_REF, parser.SETTINGS_REF,
|
|
parser.MODULES_REF, parser.FP_REF,
|
|
parser.RADIO_REF, parser.TELEMETRY_REF]
|
|
fields = [self.current_config.airframes,
|
|
self.current_config.settings,
|
|
self.current_config.modules,
|
|
self.current_config.flight_plan,
|
|
self.current_config.radio,
|
|
self.current_config.telemetry]
|
|
widgets = self.change_config_widgets.copy()
|
|
widgets.insert(2, self.ui.current_settings)
|
|
|
|
match_found = False
|
|
for keyword, field, widget in zip(keywords, fields, widgets):
|
|
if keyword in filename:
|
|
field.append(filename)
|
|
widget.addItem(filename)
|
|
last_item = widget.item(widget.count()-1)
|
|
if widget == self.ui.current_settings:
|
|
last_item.setFlags(Core.Qt.ItemIsUserCheckable |
|
|
Core.Qt.ItemIsEnabled |
|
|
Core.Qt.ItemIsSelectable)
|
|
last_item.setCheckState(Core.Qt.Checked)
|
|
widget.setCurrentItem(last_item)
|
|
match_found = True
|
|
break
|
|
|
|
if match_found:
|
|
self.configurations_changed[self.current_config.name] = \
|
|
self.current_config
|
|
index = self.ui.current_configuration.currentIndex()
|
|
self.ui.current_configuration.setItemIcon(index,
|
|
self.changed_qicon)
|
|
else:
|
|
LOGGER.error("The selected file '%s' is not an acceptable file in"
|
|
" a configuration !\n", filename)
|
|
self.switch_to_tab(3)
|
|
|
|
def remove_program_from_session(self):
|
|
"""
|
|
-> Remove the selected program from the current selected session.
|
|
-> Add the current session to changed sessions => ask to save
|
|
before quitting the application.
|
|
"""
|
|
if self.current_session is not None \
|
|
and self.current_program is not None:
|
|
program_item, program = self.current_program
|
|
|
|
self.current_session.programs.pop(program.name)
|
|
self.update_programs_list()
|
|
|
|
self.sessions_changed[self.current_session.name] = \
|
|
self.current_session
|
|
index = self.ui.session.currentIndex()
|
|
self.ui.session.setItemIcon(index, self.changed_qicon)
|
|
self.switch_to_tab(2)
|
|
|
|
def add_program_to_session(self, program):
|
|
"""
|
|
:param program:
|
|
-> Add a program object (only a copy of a tool from the Tools menu
|
|
for now) to the current selected session.
|
|
"""
|
|
if self.current_session is not None:
|
|
|
|
new_program_name = parser.find_unused_item_name(
|
|
self.current_session.programs, program.name)
|
|
new_program = db.Program(new_program_name, program.command,
|
|
program.options)
|
|
self.current_session.programs[new_program.name] = new_program
|
|
self.update_programs_list()
|
|
new_item = self.ui.programs.findItems(new_program.name,
|
|
Core.Qt.MatchExactly)[0]
|
|
self.ui.programs.setCurrentItem(new_item)
|
|
|
|
self.sessions_changed[self.current_session.name] = \
|
|
self.current_session
|
|
index = self.ui.session.currentIndex()
|
|
self.ui.session.setItemIcon(index, self.changed_qicon)
|
|
self.switch_to_tab(2)
|
|
|
|
def remove_option_from_program(self):
|
|
"""
|
|
-> Remove the selected option from the current selected program.
|
|
"""
|
|
if self.current_program is not None \
|
|
and self.current_option is not None:
|
|
program_item, program = self.current_program
|
|
option_item, option = self.current_option
|
|
|
|
if type(option) is str:
|
|
program.options.remove(option)
|
|
else:
|
|
program.options.remove(tuple(option.split()))
|
|
self.update_program_options_list()
|
|
|
|
self.sessions_changed[self.current_session.name] = \
|
|
self.current_session
|
|
index = self.ui.session.currentIndex()
|
|
self.ui.session.setItemIcon(index, self.changed_qicon)
|
|
|
|
def add_option_to_program(self):
|
|
"""
|
|
-> Add an option to the current selected program.
|
|
"""
|
|
if self.current_session is not None \
|
|
and self.current_program is not None:
|
|
program_item, program = self.current_program
|
|
new_option = ""
|
|
program.options.append(new_option)
|
|
self.update_program_options_list()
|
|
last_item = self.ui.options.item(self.ui.options.count()-1)
|
|
self.ui.options.setCurrentItem(last_item)
|
|
|
|
self.sessions_changed[self.current_session.name] = \
|
|
self.current_session
|
|
index = self.ui.session.currentIndex()
|
|
self.ui.session.setItemIcon(index, self.changed_qicon)
|
|
|
|
def edit_current_option(self):
|
|
"""
|
|
-> Edit the selected option when enter pressed after changes.
|
|
"""
|
|
if self.current_program is not None \
|
|
and self.current_option is not None:
|
|
program_item, program = self.current_program
|
|
|
|
old_option_item, old_option = self.current_option
|
|
old_option_index = program.options.index(old_option)
|
|
program.options.remove(old_option)
|
|
self.change_current_option()
|
|
new_option_item, new_option = self.current_option
|
|
program.options.insert(old_option_index, new_option)
|
|
|
|
self.sessions_changed[self.current_session.name] = \
|
|
self.current_session
|
|
index = self.ui.session.currentIndex()
|
|
self.ui.session.setItemIcon(index, self.changed_qicon)
|
|
|
|
###############################################################################
|
|
# [Hmi methods] Data management : XML writing methods
|
|
|
|
def save_current_config(self):
|
|
"""
|
|
-> Update the new values of the current configuration in the
|
|
corresponding XML file.
|
|
"""
|
|
if self.current_config.name in self.configurations_changed.keys():
|
|
self.configurations_changed.pop(self.current_config.name)
|
|
index = self.ui.current_configuration.currentIndex()
|
|
self.ui.current_configuration.setItemIcon(index,
|
|
gui.generate_qicon())
|
|
|
|
LOGGER.info("Saving '%s' configuration...",
|
|
self.current_config.name)
|
|
parser.save_configuration(self.current_set.name,
|
|
self.current_config)
|
|
LOGGER.info("'%s' configuration saved.\n",
|
|
self.current_config.name)
|
|
else:
|
|
LOGGER.info("Nothing to save, configuration already up-to-date.\n")
|
|
|
|
def save_current_session(self):
|
|
"""
|
|
-> Update the new values of the current session in the
|
|
corresponding 'control_panel.xml' file.
|
|
-> Deal with the in-code defined sessions (Simulation, Replay).
|
|
"""
|
|
if self.current_session.name in self.sessions_changed.keys():
|
|
self.sessions_changed.pop(self.current_session.name)
|
|
index = self.ui.session.currentIndex()
|
|
self.ui.session.setItemIcon(index, gui.generate_qicon())
|
|
if self.current_session.name not in [parser.SIMULATION_NAME,
|
|
parser.REPLAY_NAME]:
|
|
LOGGER.info("Saving '%s' session...", self.current_session.name)
|
|
parser.save_session(parser.CONTROL_PANEL_FILE,
|
|
self.current_session)
|
|
LOGGER.info("'%s' session saved.\n", self.current_session.name)
|
|
else:
|
|
LOGGER.error("'%s' is an in-code defined session. "
|
|
"Impossible to save it !\n",
|
|
self.current_session.name)
|
|
print("'%s' is an in-code defined session. "
|
|
"Impossible to save it !\n"
|
|
% self.current_session.name)
|
|
self.switch_to_tab(3)
|
|
else:
|
|
LOGGER.info("Nothing to save, session already up-to-date.\n")
|
|
|
|
def save_all_changed_configurations(self):
|
|
changed_configs = [_ for _ in self.configurations_changed.values()]
|
|
for config in changed_configs:
|
|
self.current_config = config
|
|
self.save_current_config()
|
|
|
|
def save_all_changed_sessions(self):
|
|
changed_sessions = [_ for _ in self.sessions_changed.values()]
|
|
for session in changed_sessions:
|
|
self.current_session = session
|
|
self.save_current_session()
|
|
|
|
def save_all_changed_data(self):
|
|
self.save_all_changed_configurations()
|
|
self.save_all_changed_sessions()
|
|
print("All changes in data saved.\n")
|
|
self.save_cache_data()
|
|
|
|
def save_cache_data(self):
|
|
"""
|
|
-> Change values in cache with the 'write' mode of the common parsing
|
|
function.
|
|
"""
|
|
LOGGER.info("Cache updating...")
|
|
print("Cache updating...")
|
|
current_geometry_str = " ".join(map(str,
|
|
[self.geometry().x(),
|
|
self.geometry().y(),
|
|
self.geometry().width(),
|
|
self.geometry().height()]))
|
|
current_log_filters_str = " ".join(map(str,
|
|
[self.current_log_filter.level,
|
|
self.current_log_filter.default,
|
|
self.current_log_filter.info,
|
|
self.current_log_filter.warning,
|
|
self.current_log_filter.error]))
|
|
|
|
parser.parse_cache_file("w", self.data.cache_file,
|
|
geometry_str=current_geometry_str,
|
|
set_str=self.current_set.name,
|
|
config_str=self.current_config.name,
|
|
target_str=self.current_target.name,
|
|
log_filters_str=current_log_filters_str,
|
|
device_str=self.current_device.name,
|
|
session_str=self.current_session.name)
|
|
LOGGER.info("Cache now up-to-date.\n")
|
|
print("Cache now up-to-date.\n")
|