diff --git a/MethodicConfigurator/backend_filesystem.py b/MethodicConfigurator/backend_filesystem.py index 0ff0d3c..27832a1 100644 --- a/MethodicConfigurator/backend_filesystem.py +++ b/MethodicConfigurator/backend_filesystem.py @@ -13,6 +13,7 @@ from os import listdir as os_listdir from os import makedirs as os_makedirs from os import rename as os_rename +from os import walk as os_walk from os import sep as os_sep from shutil import copy2 as shutil_copy2 @@ -53,6 +54,7 @@ from backend_filesystem_vehicle_components import VehicleComponents from backend_filesystem_configuration_steps import ConfigurationSteps +from middleware_template_overview import TemplateOverview TOOLTIP_MAX_LENGTH = 105 @@ -537,6 +539,19 @@ def store_recently_used_template_dirs(template_dir: str, new_base_dir: str): LocalFilesystem.__set_settings_from_dict(settings) + @staticmethod + def store_template_dir(relative_template_dir: str): + settings, pattern, replacement = LocalFilesystem.__get_settings_config() + + template_dir = os_path.join(LocalFilesystem.get_templates_base_dir(), relative_template_dir) + + # Update the settings with the new values + settings["directory_selection"].update({ + "template_dir": re_sub(pattern, replacement, template_dir) + }) + + LocalFilesystem.__set_settings_from_dict(settings) + @staticmethod def store_recently_used_vehicle_dir(vehicle_dir: str): settings, pattern, replacement = LocalFilesystem.__get_settings_config() @@ -667,6 +682,31 @@ def supported_vehicles(): return ['AP_Periph', 'AntennaTracker', 'ArduCopter', 'ArduPlane', 'ArduSub', 'Blimp', 'Heli', 'Rover', 'SITL'] + @staticmethod + def get_vehicle_components_overviews(): + """ + Finds all subdirectories of base_dir containing a "vehicle_components.json" file, + creates a dictionary where the keys are the subdirectory names (relative to base_dir) + and the values are instances of VehicleComponents. + + :param base_dir: The base directory to start searching from. + :return: A dictionary mapping subdirectory paths to VehicleComponents instances. + """ + vehicle_components_dict = {} + file_to_find = VehicleComponents().vehicle_components_json_filename + template_default_dir = LocalFilesystem.get_templates_base_dir() + for root, _dirs, files in os_walk(template_default_dir): + if file_to_find in files: + relative_path = os_path.relpath(root, template_default_dir) + vehicle_components = VehicleComponents() + comp_data = vehicle_components.load_vehicle_components_json_data(root) + if comp_data: + comp_data = comp_data.get('Components', {}) + vehicle_components_overview = TemplateOverview(comp_data) + vehicle_components_dict[relative_path] = vehicle_components_overview + + return vehicle_components_dict + @staticmethod def add_argparse_arguments(parser): parser.add_argument('-t', '--vehicle-type', diff --git a/MethodicConfigurator/frontend_tkinter_directory_selection.py b/MethodicConfigurator/frontend_tkinter_directory_selection.py index f3ceab4..a1a346a 100644 --- a/MethodicConfigurator/frontend_tkinter_directory_selection.py +++ b/MethodicConfigurator/frontend_tkinter_directory_selection.py @@ -35,6 +35,8 @@ from frontend_tkinter_base import show_tooltip from frontend_tkinter_base import BaseWindow +from frontend_tkinter_template_overview import TemplateOverviewWindow + class DirectorySelectionWidgets(): """ @@ -45,11 +47,13 @@ class DirectorySelectionWidgets(): directory selection dialog. """ def __init__(self, parent, parent_frame, initialdir: str, label_text: str, # pylint: disable=too-many-arguments - autoresize_width: bool, dir_tooltip: str, button_tooltip: str): + autoresize_width: bool, dir_tooltip: str, button_tooltip: str, + local_filesystem: LocalFilesystem = None): self.parent = parent self.directory = deepcopy(initialdir) self.label_text = label_text self.autoresize_width = autoresize_width + self.local_filesystem = local_filesystem # Create a new frame for the directory selection label and button self.container_frame = ttk.Frame(parent_frame) @@ -82,7 +86,12 @@ def __init__(self, parent, parent_frame, initialdir: str, label_text: str, # py def on_select_directory(self): # Open the directory selection dialog - selected_directory = filedialog.askdirectory(initialdir=self.directory, title=f"Select {self.label_text}") + if self.local_filesystem: + vehicle_components_overviews = self.local_filesystem.get_vehicle_components_overviews() + TemplateOverviewWindow(vehicle_components_overviews, self.local_filesystem) + selected_directory = self.local_filesystem.get_recently_used_dirs()[0] + else: + selected_directory = filedialog.askdirectory(initialdir=self.directory, title=f"Select {self.label_text}") if selected_directory: if self.autoresize_width: # Set the width of the directory_entry to match the width of the selected_directory text @@ -235,7 +244,8 @@ def create_option1_widgets(self, initial_template_dir: str, initial_base_dir: st "(source) Template directory:", False, template_dir_edit_tooltip, - template_dir_btn_tooltip) + template_dir_btn_tooltip, + self.local_filesystem) self.template_dir.container_frame.pack(expand=False, fill=tk.X, padx=3, pady=5, anchor=tk.NW) use_fc_params_checkbox = ttk.Checkbutton(option1_label_frame, variable=self.use_fc_params, diff --git a/MethodicConfigurator/frontend_tkinter_template_overview.py b/MethodicConfigurator/frontend_tkinter_template_overview.py new file mode 100644 index 0000000..a304695 --- /dev/null +++ b/MethodicConfigurator/frontend_tkinter_template_overview.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 + +''' +This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator + +(C) 2024 Amilcar do Carmo Lucas + +SPDX-License-Identifier: GPL-3 +''' + +import argparse +from logging import basicConfig as logging_basicConfig +from logging import getLevelName as logging_getLevelName + +import tkinter as tk +from tkinter import ttk + +from typing import Dict + +from middleware_template_overview import TemplateOverview + +from backend_filesystem import LocalFilesystem + +from common_arguments import add_common_arguments_and_parse + +from frontend_tkinter_base import show_error_message + + +class TemplateOverviewWindow: + """ + Represents the window for viewing and managing ArduPilot vehicle templates. + + This class creates a graphical user interface (GUI) window that displays an overview of available vehicle templates. + Users can browse through different templates, view their attributes, and perform actions such as storing a template + directory for further configuration. The window utilizes a Treeview widget to present the templates in a structured + manner, making it easier for users to navigate and select the desired template for configuration. + + Attributes: + window (tk.Tk): The root Tkinter window object for the GUI. + local_filesystem (LocalFilesystem): An instance of LocalFilesystem used to interact with the filesystem, including + operations related to template directories. + + Methods: + on_row_double_click(event): Handles the event triggered when a row in the Treeview is double-clicked, allowing the user + to store the corresponding template directory. + """ + def __init__(self, vehicle_templates_overviews: Dict[str, TemplateOverview], local_filesystem: LocalFilesystem): + self.window = tk.Tk() + self.window.title("ArduPilot methodic configurator - Template Overview") + self.window.geometry("800x600") + self.local_filesystem = local_filesystem + self.font = tk.font.Font() # Default font for measuring text width + + # Instantiate the RichText widget to display instructions above the Treeview + instruction_text = "Please double-click the template below that most resembles your own vehicle components" + instruction_label = ttk.Label(self.window, text=instruction_text, font=('Arial', 12)) + instruction_label.pack(pady=(10, 20)) + + # Define the columns for the Treeview + columns = TemplateOverview.columns() + self.tree = ttk.Treeview(self.window, columns=columns, show='headings') + for col in columns: + self.tree.heading(col, text=col) + #self.tree.column(col, width=100) + + # Populate the Treeview with data from the template overview + for key, template_overview in vehicle_templates_overviews.items(): + attribute_names = template_overview.attributes() + values = (key,) + tuple(getattr(template_overview, attr, '') for attr in attribute_names) + self.tree.insert('', 'end', text=key, values=values) + + self.tree.bind('', self.on_row_double_click) + self.tree.pack(fill=tk.BOTH, expand=True) + + #self.adjust_treeview_column_widths() + + self.window.mainloop() + + def adjust_treeview_column_widths(self): + """ + Adjusts the column widths of the Treeview to fit the contents of each column. + """ + max_widths = [0] * len(self.tree["columns"]) # Initialize max_widths list with zeros + + # Iterate through all items to find the maximum width required for each column + for item in self.tree.get_children(): + for i, col in enumerate(self.tree["columns"][:-1]): # Exclude the last column ('text') as it's handled separately + max_widths[i] = max(max_widths[i], self.measure_text_width(self.tree.set(item, col))) + + # Set the column widths + for i, max_w in enumerate(max_widths): + self.tree.column(self.tree["columns"][i], width=max_w + 0) # Adding some padding + + def measure_text_width(self, text): + return self.font.measure(text) # The width of the text in pixels + + def on_row_double_click(self, event): + """Handle row double-click event.""" + item_id = self.tree.identify_row(event.y) + if item_id: + template_relative_path = self.tree.item(item_id)['text'] + self.local_filesystem.store_template_dir(template_relative_path) + self.window.destroy() + +def argument_parser(): + """ + Parses command-line arguments for the script. + + This function sets up an argument parser to handle the command-line arguments for the script. + + Returns: + argparse.Namespace: An object containing the parsed arguments. + """ + parser = argparse.ArgumentParser(description='ArduPilot methodic configurator is a GUI-based tool designed to simplify ' + 'the management and visualization of ArduPilot parameters. It enables users ' + 'to browse through various vehicle templates, edit parameter files, and ' + 'apply changes directly to the flight controller. The tool is built to ' + 'semi-automate the configuration process of ArduPilot for drones by ' + 'providing a clear and intuitive interface for parameter management.') + parser = LocalFilesystem.add_argparse_arguments(parser) + return add_common_arguments_and_parse(parser) + +def main(): + args = argument_parser() + + logging_basicConfig(level=logging_getLevelName(args.loglevel), format='%(asctime)s - %(levelname)s - %(message)s') + + vehicle_type = "ArduCopter" + + try: + local_filesystem = LocalFilesystem(args.vehicle_dir, vehicle_type, args.allow_editing_template_files) + except SystemExit as expt: + show_error_message("Fatal error reading parameter files", f"{expt}") + raise + + vehicle_components_overviews = local_filesystem.get_vehicle_components_overviews() + + TemplateOverviewWindow(vehicle_components_overviews, local_filesystem) + + print(local_filesystem.get_recently_used_dirs()[0]) + +if __name__ == "__main__": + main() diff --git a/MethodicConfigurator/middleware_template_overview.py b/MethodicConfigurator/middleware_template_overview.py new file mode 100644 index 0000000..dc90b54 --- /dev/null +++ b/MethodicConfigurator/middleware_template_overview.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 + +''' +This file is part of Ardupilot methodic configurator. https://github.com/ArduPilot/MethodicConfigurator + +(C) 2024 Amilcar do Carmo Lucas + +SPDX-License-Identifier: GPL-3 +''' + +class TemplateOverview: # pylint: disable=too-many-instance-attributes + """ + Represents a single vehicle template configuration within the ArduPilot Methodic Configurator. + + This class encapsulates the data and attributes associated with a specific vehicle template configuration. + It is designed to hold information about various components of a drone, such as the flight controller, telemetry system, + ESCs, propellers, and GNSS receiver, along with their specifications. The class facilitates easy access to these + attributes, enabling the GUI to display and select the templates in a structured format. + """ + def __init__(self, components_data: dict): + # The declaration order of these parameters determines the column order in the GUI + self.fc_manufacturer = components_data.get('Flight Controller', {}).get('Product', {}).get('Manufacturer', '') + self.fc_model = components_data.get('Flight Controller', {}).get('Product', {}).get('Model', '') + self.tow_min_kg = components_data.get('Frame', {}).get('Specifications', {}).get('tow_min_kg', '') + self.tow_max_kg = components_data.get('Frame', {}).get('Specifications', {}).get('tow_max_kg', '') + self.rc_protocol = components_data.get('RC Receiver', {}).get('FC Connection', {}).get('Protocol', '') + self.telemetry_model = components_data.get('Telemetry', {}).get('Product', {}).get('Model', '') + self.esc_protocol = components_data.get('ESC', {}).get('FC Connection', {}).get('Protocol', '') + self.prop_diameter_inches = components_data.get('Propellers', {}).get('Specifications', {}).get('Diameter_inches', '') + self.gnss_model = components_data.get('GNSS receiver', {}).get('Product', {}).get('Model', '') + + @staticmethod + def columns(): + # Must match the order in the __init__() function above + return ("Template path", + "FC Manufacturer", + "FC Model", + "TOW Min\n[KG]", + "TOW Max\n[KG]", + "RC Protocol", + "Telemetry Model", + "ESC Protocol", + "Prop Diameter\n[inches]", + "GNSS Model") + + def attributes(self): + return self.__dict__.keys()