Skip to content

Commit 48269e5

Browse files
committed
feat(project-opener): create vehicle project from ArduPilot .bin log file
Add a "Create a vehicle project from a .bin log file" button to the New Vehicle panel in the project-opener window. Business logic additions in data_model_vehicle_project_creator.py: - extract_param_files_from_bin_log(): calls extract_parameter_values() for both "defaults" and "values" snapshots; wraps results as ParDict - template_dir_for_bin_import(): returns the empty_4.6.x template path - vehicle_name_from_bin_log(): derives the new vehicle name from the .bin file stem - next_import_filename(): finds the next available numbered slot for the imported parameter file Orchestration in data_model_vehicle_project.py (create_new_vehicle_from_bin_log): - Scaffolds the project from empty_4.6.x with current FC params used for template substitution - Replaces the template's 00_default.param with the .bin-extracted defaults snapshot (key deviation from normal template-based creation) - Exports remaining current parameters into a numbered *_imported_bin_log_parameters.param file GUI additions: - BinLogSelectionWidgets reusable widget added to frontend_tkinter_directory_selection.py - Widget wired into VehicleProjectOpenerWindow.create_option1_widgets()
1 parent 0f84f27 commit 48269e5

4 files changed

Lines changed: 163 additions & 2 deletions

File tree

ardupilot_methodic_configurator/data_model_vehicle_project.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
from ardupilot_methodic_configurator import _
1818
from ardupilot_methodic_configurator.backend_filesystem import LocalFilesystem
19+
from ardupilot_methodic_configurator.data_model_par_dict import is_within_tolerance
1920
from ardupilot_methodic_configurator.data_model_vehicle_project_creator import NewVehicleProjectSettings, VehicleProjectCreator
2021
from ardupilot_methodic_configurator.data_model_vehicle_project_opener import VehicleProjectOpener
2122

@@ -156,6 +157,64 @@ def create_new_vehicle_from_template(
156157
self.open_vehicle_directory(new_path)
157158
return new_path
158159

160+
def create_new_vehicle_from_bin_log(self, bin_file: str, vehicle_type: str = "ArduCopter") -> str:
161+
"""
162+
Create a new vehicle configuration directory from an ArduPilot .bin log file.
163+
164+
The project is based on the empty_4.6.x template, uses the extracted
165+
current parameter values for template substitution, replaces the template's
166+
00_default.param with the extracted defaults snapshot, and exports any
167+
remaining parameters missing from the AMC files into a final import file.
168+
169+
Args:
170+
bin_file: Path to the ArduPilot .bin log file
171+
vehicle_type: Vehicle type whose empty template should be used
172+
173+
Returns:
174+
The created vehicle directory path
175+
176+
Raises:
177+
VehicleProjectCreationError: If creation or extraction fails
178+
179+
"""
180+
template_dir = self._creator.template_dir_for_bin_import(vehicle_type)
181+
new_base_dir = str(LocalFilesystem.get_vehicles_default_dir())
182+
new_vehicle_name = self._creator.vehicle_name_from_bin_log(bin_file)
183+
default_params, current_params = self._creator.extract_param_files_from_bin_log(bin_file)
184+
current_param_values = {name: param.value for name, param in current_params.items()}
185+
settings = NewVehicleProjectSettings(
186+
blank_change_reason=True,
187+
infer_comp_specs_and_conn_from_fc_params=True,
188+
use_fc_params=True,
189+
)
190+
191+
new_path = self._creator.create_new_vehicle_from_template(
192+
template_dir,
193+
new_base_dir,
194+
new_vehicle_name,
195+
settings,
196+
fc_connected=False,
197+
fc_parameters=current_param_values,
198+
)
199+
self._settings = settings
200+
self.configuration_template = self.get_directory_name_from_path(template_dir)
201+
self.store_recently_used_template_dirs(template_dir, new_base_dir)
202+
self.open_vehicle_directory(new_path)
203+
204+
self._local_filesystem.write_param_default_values_to_file(default_params)
205+
206+
compound_params, _first_filename = self._local_filesystem.compound_params(skip_default=True)
207+
imported_params = current_params.get_missing_or_different(compound_params, is_within_tolerance)
208+
if imported_params:
209+
import_filename = self._creator.next_import_filename(new_path)
210+
self._local_filesystem.export_to_param(imported_params, import_filename, annotate_doc=False)
211+
self._local_filesystem.re_init(new_path, vehicle_type)
212+
213+
if self._flight_controller is not None:
214+
self._flight_controller.fc_parameters = current_param_values
215+
216+
return new_path
217+
159218
# Vehicle project opening operations
160219
def open_vehicle_directory(self, vehicle_dir: str) -> str:
161220
"""

ardupilot_methodic_configurator/data_model_vehicle_project_creator.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,13 @@
1212
"""
1313

1414
from dataclasses import MISSING, dataclass, fields
15+
from pathlib import Path
1516
from typing import ClassVar, NamedTuple, Optional
1617

1718
from ardupilot_methodic_configurator import _
1819
from ardupilot_methodic_configurator.backend_filesystem import LocalFilesystem
20+
from ardupilot_methodic_configurator.data_model_par_dict import ParDict
21+
from ardupilot_methodic_configurator.extract_param_defaults import extract_parameter_values
1922

2023

2124
class VehicleProjectCreationError(Exception):
@@ -387,7 +390,7 @@ def adjust_for_fc_connection(
387390
return NewVehicleProjectSettings(**adjusted_settings)
388391

389392

390-
class VehicleProjectCreator: # pylint: disable=too-few-public-methods
393+
class VehicleProjectCreator:
391394
"""Manages vehicle project creation operations."""
392395

393396
def __init__(self, local_filesystem: LocalFilesystem) -> None:
@@ -400,6 +403,44 @@ def __init__(self, local_filesystem: LocalFilesystem) -> None:
400403
"""
401404
self.local_filesystem = local_filesystem
402405

406+
@staticmethod
407+
def template_dir_for_bin_import(vehicle_type: str = "ArduCopter") -> str:
408+
"""Return the empty template directory used for .bin log imports."""
409+
return str(Path(LocalFilesystem.get_templates_base_dir()) / vehicle_type / "empty_4.6.x")
410+
411+
@staticmethod
412+
def vehicle_name_from_bin_log(bin_file: str) -> str:
413+
"""Return the default vehicle directory name derived from the .bin filename."""
414+
return Path(bin_file).stem
415+
416+
@staticmethod
417+
def next_import_filename(vehicle_dir: str) -> str:
418+
"""Return the next available numbered parameter filename for imported log parameters."""
419+
highest_prefix = 0
420+
for file_path in Path(vehicle_dir).iterdir():
421+
if not file_path.is_file() or file_path.suffix != ".param":
422+
continue
423+
prefix = file_path.name[:2]
424+
if prefix.isdigit():
425+
highest_prefix = max(highest_prefix, int(prefix))
426+
427+
next_prefix = highest_prefix + 1
428+
if next_prefix > 99:
429+
msg = _("Could not create an import parameter file because no numbered slot is available in {vehicle_dir}")
430+
raise VehicleProjectCreationError(_("Parameter import"), msg.format(vehicle_dir=vehicle_dir))
431+
return f"{next_prefix:02d}_imported_bin_log_parameters.param"
432+
433+
@staticmethod
434+
def extract_param_files_from_bin_log(bin_file: str) -> tuple[ParDict, ParDict]:
435+
"""Extract default and current parameter snapshots from an ArduPilot .bin log file."""
436+
try:
437+
default_params = ParDict.from_float_dict(extract_parameter_values(bin_file, "defaults"))
438+
current_params = ParDict.from_float_dict(extract_parameter_values(bin_file, "values"))
439+
except SystemExit as exc:
440+
msg = str(exc) or _("Failed to extract parameters from the selected .bin log file")
441+
raise VehicleProjectCreationError(_(".bin log import"), msg) from exc
442+
return default_params, current_params
443+
403444
def create_new_vehicle_from_template( # pylint: disable=too-many-arguments, too-many-positional-arguments
404445
self,
405446
template_dir: str,

ardupilot_methodic_configurator/frontend_tkinter_directory_selection.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from typing import Callable, Optional
1515

1616
from ardupilot_methodic_configurator import _
17+
from ardupilot_methodic_configurator.data_model_vehicle_project_creator import VehicleProjectCreationError
1718
from ardupilot_methodic_configurator.data_model_vehicle_project_opener import VehicleProjectOpenError
1819
from ardupilot_methodic_configurator.frontend_tkinter_base_window import BaseWindow
1920
from ardupilot_methodic_configurator.frontend_tkinter_show import show_tooltip
@@ -135,6 +136,54 @@ def get_selected_directory(self) -> str:
135136
return self.directory
136137

137138

139+
class BinLogSelectionWidgets: # pylint: disable=too-few-public-methods
140+
"""A GUI widget for selecting a .bin log file and invoking a callback."""
141+
142+
def __init__(
143+
self,
144+
parent: BaseWindow,
145+
parent_frame: ttk.Widget,
146+
on_select_file_callback: Callable[[str], None],
147+
) -> None:
148+
self.parent = parent
149+
self.on_select_file_callback = on_select_file_callback
150+
151+
self.container_frame = ttk.Frame(parent_frame)
152+
self.select_file_button = ttk.Button(
153+
self.container_frame,
154+
text=_("Create a vehicle project from a .bin log file"),
155+
command=self.on_select_file,
156+
)
157+
self.select_file_button.pack(expand=False, fill=tk.X, padx=20, pady=5, anchor=tk.CENTER)
158+
show_tooltip(
159+
self.select_file_button,
160+
_(
161+
"Extract default and current parameters from an ArduPilot .bin log file, create a new vehicle project\n"
162+
"from the empty_4.6.x template, replace the template's 00_default.param with the extracted one and\n"
163+
"continue with the normal component editor flow"
164+
),
165+
)
166+
167+
def on_select_file(self) -> bool:
168+
selected_file = filedialog.askopenfilename(
169+
parent=self.parent.root,
170+
title=_("Select an ArduPilot .bin log file"),
171+
filetypes=[(_("ArduPilot binary log files"), "*.bin"), (_("All files"), "*.*")],
172+
)
173+
if not selected_file:
174+
return False
175+
176+
try:
177+
self.on_select_file_callback(selected_file)
178+
return True
179+
except VehicleProjectCreationError as exc:
180+
messagebox.showerror(exc.title, exc.message)
181+
return False
182+
except OSError as exc:
183+
messagebox.showerror(_(".bin log import"), str(exc))
184+
return False
185+
186+
138187
class VehicleDirectorySelectionWidgets(DirectorySelectionWidgets):
139188
"""
140189
A subclass of DirectorySelectionWidgets specifically tailored for selecting vehicle directories.

ardupilot_methodic_configurator/frontend_tkinter_project_opener.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from ardupilot_methodic_configurator.data_model_vehicle_project_opener import VehicleProjectOpenError
2929
from ardupilot_methodic_configurator.frontend_tkinter_base_window import BaseWindow
3030
from ardupilot_methodic_configurator.frontend_tkinter_directory_selection import (
31+
BinLogSelectionWidgets,
3132
VehicleDirectorySelectionWidgets,
3233
)
3334
from ardupilot_methodic_configurator.frontend_tkinter_project_creator import VehicleProjectCreatorWindow
@@ -64,7 +65,7 @@ def __init__(self, project_manager: VehicleProjectManager) -> None:
6465
self.main_frame,
6566
anchor=tk.CENTER,
6667
justify=tk.CENTER,
67-
text=introduction_text + _("\nChoose one of the following three options:"),
68+
text=introduction_text + _("\nChoose one of the following options:"),
6869
)
6970
introduction_label.pack(expand=False, fill=tk.X, padx=6, pady=6)
7071
_template_dir, _new_base_dir, vehicle_dir = self.project_manager.get_recently_used_dirs()
@@ -96,6 +97,17 @@ def create_option1_widgets(self) -> None:
9697
_("Create a new vehicle configuration directory, choose this option when using the software for the first time"),
9798
)
9899

100+
def on_bin_log_selected(bin_file: str) -> None:
101+
self.project_manager.create_new_vehicle_from_bin_log(bin_file)
102+
self.root.destroy()
103+
104+
self.bin_log_selection_widgets = BinLogSelectionWidgets(
105+
self,
106+
option1_label_frame,
107+
on_select_file_callback=on_bin_log_selected,
108+
)
109+
self.bin_log_selection_widgets.container_frame.pack(expand=False, fill=tk.X, padx=3, pady=(0, 5), anchor=tk.NW)
110+
99111
# pylint: enable=duplicate-code
100112

101113
def create_option2_widgets(self, initial_dir: str) -> None:

0 commit comments

Comments
 (0)