Skip to content

Commit

Permalink
FEATURE: Template overview window for selecting the vehicle template …
Browse files Browse the repository at this point in the history
…configuration directory
  • Loading branch information
amilcarlucas committed Jun 10, 2024
1 parent 7cf8c44 commit 9ed7921
Show file tree
Hide file tree
Showing 4 changed files with 262 additions and 4 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 MethodicConfigurator.backend_filesystem_vehicle_components import VehicleComponents
from MethodicConfigurator.backend_filesystem_configuration_steps import ConfigurationSteps

from MethodicConfigurator.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
19 changes: 15 additions & 4 deletions MethodicConfigurator/frontend_tkinter_directory_selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from logging import basicConfig as logging_basicConfig
from logging import getLevelName as logging_getLevelName
from logging import warning as logging_warning
from logging import info as logging_info
from logging import debug as logging_error

import tkinter as tk
Expand All @@ -35,6 +36,8 @@
from MethodicConfigurator.frontend_tkinter_base import show_tooltip
from MethodicConfigurator.frontend_tkinter_base import BaseWindow

from MethodicConfigurator.frontend_tkinter_template_overview import TemplateOverviewWindow


class DirectorySelectionWidgets():
"""
Expand All @@ -45,11 +48,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 @@ -81,8 +86,13 @@ def __init__(self, parent, parent_frame, initialdir: str, label_text: str, # py
self.directory_entry.xview_moveto(1.0)

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:
TemplateOverviewWindow(self.parent.root, self.local_filesystem)
selected_directory = self.local_filesystem.get_recently_used_dirs()[0]
logging_info("Selected template directory: %s", selected_directory)
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 +245,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
160 changes: 160 additions & 0 deletions MethodicConfigurator/frontend_tkinter_template_overview.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#!/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 MethodicConfigurator.middleware_template_overview import TemplateOverview

from MethodicConfigurator.backend_filesystem import LocalFilesystem

from MethodicConfigurator.common_arguments import add_common_arguments_and_parse

from MethodicConfigurator.frontend_tkinter_base import BaseWindow
from MethodicConfigurator.frontend_tkinter_base import show_error_message

from MethodicConfigurator.version import VERSION


class TemplateOverviewWindow(BaseWindow):
"""
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, parent: tk.Tk, local_filesystem: LocalFilesystem):
super().__init__(parent)
self.root.title(f"Amilcar Lucas's - ArduPilot methodic configurator {VERSION} - Template Overview and selection")
self.root.geometry("1200x300")
self.local_filesystem = local_filesystem

instruction_text = "Please double-click the template below that most resembles your own vehicle components"
instruction_label = ttk.Label(self.main_frame, text=instruction_text, font=('Arial', 12))
instruction_label.pack(pady=(10, 20))

style = ttk.Style(self.root)
# Add padding to Treeview heading style
style.layout("Treeview.Heading", [
("Treeview.Heading.cell", {'sticky': 'nswe'}),
("Treeview.Heading.border", {'sticky':'nswe', 'children': [
("Treeview.Heading.padding", {'sticky':'nswe', 'children': [
("Treeview.Heading.image", {'side':'right', 'sticky':''}),
("Treeview.Heading.text", {'sticky':'we'})
]})
]}),
])
style.configure("Treeview.Heading", padding=[2, 2, 2, 18], justify='center')

# Define the columns for the Treeview
columns = TemplateOverview.columns()
self.tree = ttk.Treeview(self.main_frame, columns=columns, show='headings')
for col in columns:
self.tree.heading(col, text=col)

# Populate the Treeview with data from the template overview
for key, template_overview in self.local_filesystem.get_vehicle_components_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.adjust_treeview_column_widths()

self.tree.bind('<Double-1>', self.on_row_double_click)
self.tree.pack(fill=tk.BOTH, expand=True)

if isinstance(self.root, tk.Toplevel):
try:
while self.root.children:
self.root.update_idletasks()
self.root.update()
except tk.TclError as _exp:
pass
else:
self.root.mainloop()

def adjust_treeview_column_widths(self):
"""
Adjusts the column widths of the Treeview to fit the contents of each column.
"""
for col in self.tree["columns"]:
max_width = 0
for subtitle in col.title().split('\n'):
max_width = max(max_width, tk.font.Font().measure(subtitle))

# Iterate over all rows and update the max_width if a wider entry is found
for item in self.tree.get_children():
item_text = self.tree.item(item, 'values')[self.tree["columns"].index(col)]
text_width = tk.font.Font().measure(item_text)
max_width = max(max_width, text_width)

# Update the column's width property to accommodate the largest text width
self.tree.column(col, width=int(max_width*0.6 + 10))

def on_row_double_click(self, event):
"""Handle row double-click event."""
item_id = self.tree.identify_row(event.y)
if item_id:
selected_template_relative_path = self.tree.item(item_id)['text']
self.local_filesystem.store_template_dir(selected_template_relative_path)
self.root.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

TemplateOverviewWindow(None, 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\nManufacturer",
"FC\nModel",
"TOW Min\n[KG]",
"TOW Max\n[KG]",
"RC\nProtocol",
"Telemetry\nModel",
"ESC\nProtocol",
"Prop Diameter\n[inches]",
"GNSS\nModel")

def attributes(self):
return self.__dict__.keys()

0 comments on commit 9ed7921

Please sign in to comment.