Skip to content

Commit

Permalink
FEATURE: Template overview window for selecting the template
Browse files Browse the repository at this point in the history
  • Loading branch information
amilcarlucas committed Jun 9, 2024
1 parent 3f4f052 commit 09d0946
Show file tree
Hide file tree
Showing 4 changed files with 243 additions and 3 deletions.
40 changes: 40 additions & 0 deletions MethodicConfigurator/backend_filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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',
Expand Down
16 changes: 13 additions & 3 deletions MethodicConfigurator/frontend_tkinter_directory_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
"""
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down
143 changes: 143 additions & 0 deletions MethodicConfigurator/frontend_tkinter_template_overview.py
Original file line number Diff line number Diff line change
@@ -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('<Double-1>', 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()
47 changes: 47 additions & 0 deletions MethodicConfigurator/middleware_template_overview.py
Original file line number Diff line number Diff line change
@@ -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()

0 comments on commit 09d0946

Please sign in to comment.