-
Notifications
You must be signed in to change notification settings - Fork 8
feat: add support of comma-separated param files with comments and empty lines #402
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -198,6 +198,8 @@ class Formats(Enum): | |
| """Ardupilot-compatible file.""" | ||
| apj = "apj" | ||
| """File compatible with Ardupilot's apj tool.""" | ||
| mp = "mp" | ||
| """Ardupilot Mission Planner comma separated file with comments.""" | ||
|
|
||
|
|
||
| ReservedOptions = Literal[ | ||
|
|
@@ -821,20 +823,6 @@ def build_param_from_qgc(row: list[str]) -> Parameter: | |
| return param | ||
|
|
||
|
|
||
| def build_param_from_ulog_params(row: list[str]) -> Parameter: | ||
| """Build a Parameter from an ulog_params printout entry.""" | ||
| param_name = row[0] | ||
| param_value: int | float | ||
| try: | ||
| param_value = int(row[1]) | ||
| except ValueError: | ||
| param_value = float(row[1]) | ||
|
|
||
| param = Parameter(param_name, param_value) | ||
|
|
||
| return param | ||
|
|
||
|
|
||
| def build_param_from_mavproxy(item: Sequence) -> Parameter: | ||
| """Convert a mavproxy parameter line to a parameter. | ||
|
|
||
|
|
@@ -951,45 +939,12 @@ def read_params_qgc(filepath: Path) -> ParameterList: | |
| raise (SyntaxError("Could not extract any parameter from file.")) | ||
|
|
||
| return param_list | ||
| except UnicodeDecodeError as e: | ||
| raise SyntaxError(f"File encoding error - not a valid QGC parameter file: {e}") from e | ||
| except SyntaxError as e: | ||
| raise SyntaxError(f"File is not of QGC format:\n{e}") from e | ||
|
|
||
|
|
||
| @parser | ||
| def read_params_ulog_param(filepath: Path) -> ParameterList: | ||
| """Read and parse the outputs of the ulog_params program.""" | ||
| param_list = ParameterList() | ||
|
|
||
| try: | ||
| with open(filepath) as csvfile: | ||
| param_reader = csv.reader(csvfile, delimiter=",") | ||
| for param_row in param_reader: # pragma: no branch | ||
| if param_row[0][0] == "#": # Skip comment lines | ||
| continue | ||
| # Check if line has exactly two elements | ||
| if len(param_row) != 2: | ||
| raise SyntaxError( | ||
| f"Invalid number of elements for ulog param decoder: {len(param_row)}" | ||
| ) | ||
| # Check if first element is a string | ||
| try: | ||
| float(param_row[0]) | ||
| raise SyntaxError( | ||
| "First row element must be a parameter name string" | ||
| ) | ||
| except ValueError: | ||
| pass | ||
| param = build_param_from_ulog_params(param_row) | ||
| param_list.add_param(param) | ||
|
|
||
| if len(param_list.params) == 0: | ||
| raise (SyntaxError("Could not extract any parameter from file.")) | ||
|
|
||
| return param_list | ||
| except SyntaxError as e: | ||
| raise SyntaxError(f"File is not of ulog format:\n{e}") from e | ||
|
|
||
|
|
||
| def split_mavproxy_row(row: str) -> Sequence: | ||
| """Split a line, assuming it is mavproxy syntax.""" | ||
| params = row.split() | ||
|
|
@@ -1015,13 +970,41 @@ def split_mavproxy_row(row: str) -> Sequence: | |
| return params | ||
|
|
||
|
|
||
| def split_missionplanner_row(row: str) -> Sequence: | ||
| """Split a line, assuming it is MissionPlanner or ULOG (as its subset) syntax.""" | ||
| params = row.split(",", 1) | ||
| # Check if there's at least one comma (should have 2 parts) | ||
| if len(params) < 2: | ||
| raise SyntaxError("MP: Line must contain at least one comma separator.") | ||
|
|
||
| # Check if first element is a string | ||
| try: | ||
| float(params[0]) | ||
| raise SyntaxError("MP: First row element must be a parameter name string.") | ||
| except ValueError: | ||
| pass | ||
|
|
||
| value_reasoning = params[1].split("#", 1) | ||
| params[1] = value_reasoning[0].strip() | ||
| try: | ||
| float(params[1]) | ||
| except ValueError as e: | ||
| raise SyntaxError("MP: First row element must be a parameter name string.") from e | ||
| if len(value_reasoning) <= 1: | ||
| params.append("") | ||
| else: | ||
| params.append(value_reasoning[1].strip()) | ||
|
|
||
| return params | ||
|
|
||
|
|
||
| @parser | ||
| def read_params_mavproxy(filepath: Path) -> ParameterList: | ||
| """Read and parse the outputs of mavproxy.""" | ||
| param_list = ParameterList() | ||
|
|
||
| try: | ||
| with open(filepath) as f: | ||
| with open(filepath, encoding='utf-8') as f: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm curious why this was necessary.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for supporting Cyrillic characters in the comments -) |
||
| for line in f: # pragma: no branch | ||
| if line[0] == "#": # Skip comment lines | ||
| continue | ||
|
|
@@ -1037,6 +1020,31 @@ def read_params_mavproxy(filepath: Path) -> ParameterList: | |
| raise SyntaxError(f"File is not of mavproxy format:\n{e}") from e | ||
|
|
||
|
|
||
| @parser | ||
| def read_params_missionplanner(filepath: Path) -> ParameterList: | ||
| """Read and parse the outputs of MissionPlanner or ULOG (as its subset).""" | ||
| param_list = ParameterList() | ||
|
|
||
| try: | ||
| with open(filepath, encoding='utf-8') as f: | ||
| for line in f: # pragma: no branch | ||
| if line[0] == "#": # Skip comment lines | ||
| continue | ||
| # skip empty lines | ||
| if line.strip() == "": | ||
| continue | ||
| params = split_missionplanner_row(line) | ||
| param = build_param_from_mavproxy(params) | ||
| param_list.add_param(param) | ||
|
|
||
| if len(param_list.params) == 0: | ||
| raise (SyntaxError("Could not extract any parameter from file.")) | ||
|
|
||
| return param_list | ||
| except SyntaxError as e: | ||
| raise SyntaxError(f"File is not of MissionPlanner format:\n{e}") from e | ||
|
|
||
|
|
||
| def read_params(filepath: Path) -> ParameterList: | ||
| """Universal parameter reader.""" | ||
| get_logger().debug(f"Attempting to read file {filepath}") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -642,30 +642,42 @@ def export_to_csv(self) -> Generator[str, None, None]: | |
| yield from self.retrieve_header_footer("footer", Formats.csv) | ||
|
|
||
| def export_to_apm( | ||
| self, include_readonly: bool = False | ||
| self, include_readonly: bool = False, csv: bool = False, comments: bool = False | ||
| ) -> Generator[str, None, None]: | ||
| """Export as apm parameter file. | ||
|
|
||
| INPUTS: | ||
| include_readonly: flag to enable including @READONLY on a parameter. | ||
| Necessary for apj tools, unsuitable for loading via a GCS. | ||
| csv: flag to use comma-separated format instead of tab-separated. | ||
| comments: flag to add parameter descriptions as comments after #. | ||
| """ | ||
| # Read header | ||
| yield from self.retrieve_header_footer("header", Formats.apm) | ||
|
|
||
| indentation = "" | ||
| separator = "," if csv else "\t" | ||
|
|
||
| param_hashes = sorted(self.param_list.keys()) | ||
| for param_name in param_hashes: | ||
| param_name = self.param_list[param_name].name | ||
| param_value = self.param_list[param_name].get_pretty_value() | ||
| is_readonly = self.param_list[param_name].readonly | ||
| for param_hash in param_hashes: | ||
| param = self.param_list[param_hash] | ||
| param_name = param.name | ||
| param_value = param.get_pretty_value() | ||
| is_readonly = param.readonly | ||
|
|
||
| # Build the parameter line | ||
| line = f"{indentation}{param_name}{separator}{param_value}" | ||
|
|
||
| # Add readonly marker if needed | ||
| if include_readonly and is_readonly: | ||
| readonly_string = "\t@READONLY" | ||
| else: | ||
| readonly_string = "" | ||
|
|
||
| yield f"{indentation}{param_name}\t{param_value}{readonly_string}\n" | ||
| readonly_string = f"{separator}@READONLY" if csv else "\t@READONLY" | ||
| line += readonly_string | ||
|
|
||
| # Add comment if requested and description is available | ||
| if comments and getattr(param, 'reasoning', None): | ||
| line += f" # {param.reasoning}" | ||
|
|
||
| yield line + "\n" | ||
|
|
||
| # Read footer | ||
| yield from self.retrieve_header_footer("footer", Formats.apm) | ||
|
|
@@ -694,6 +706,8 @@ def export(self, format: Formats) -> Iterable[str]: | |
| return self.export_to_apm(include_readonly=False) | ||
| elif format == Formats.apj: | ||
| return self.export_to_apm(include_readonly=True) | ||
| elif format == Formats.mp: | ||
| return self.export_to_apm(include_readonly=False, csv=True, comments=True) | ||
| else: | ||
| raise ValueError(f"Output format {format} not supported.") | ||
|
|
||
|
|
@@ -734,6 +748,8 @@ def build_filename(format: Formats, meal: Meal) -> str: | |
| filename += ".hil" | ||
| return filename | ||
| elif format in (Formats.apm, Formats.apj): | ||
| return f"{meal.name}.parm" | ||
| elif format == Formats.mp: | ||
|
Comment on lines
+751
to
+752
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're re-routing the .param format to the Mission Planner one, and throwing .parm to .apm and .apj.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Both apj and mavproxy(apm) use the format Mission planner (MP) exports parameters in the format Basically I replaced/extended
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://ardupilot.org/dev/docs/apjtools-intro.html
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll try to disentangle the definitions of the different extensions, but keep in mind that, AFAIK, there isn't a hard rule for a 1:1 match between extension name and file format. For example, the ArduPilot apj tool can probably handle .parm and .param files. I haven't tried it and I probably should. |
||
| return f"{meal.name}.param" | ||
| else: | ||
| raise ValueError(f"Unsupported format {format}") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| #ACRO_YAW_RATE,90 | ||
|
|
||
| AHRS_ORIENTATION,25 # realflight | ||
|
|
||
|
|
||
| ### Variant 1 - GPS module + Spoofing | ||
| AHRS_GPS_USE,1 | ||
| GPS1_TYPE,1 | ||
|
|
||
|
|
||
| ### Variant 2 - LUA Script | ||
| #SCR_ENABLE,1 | ||
| AHRS_ORIG_LAT,37.090662 | ||
| #AHRS_ORIG_LON,-3.0745569 | ||
| #AHRS_ORIG_ALT,2736 | ||
|
|
||
|
|
||
| AHRS_OPTIONS,3 # вимкнути DCM fallback in VTOL and FW | ||
| #AHRS_OPTIONS,7 # don't disable AIRSPEED using EKF біт3 (в симуляторі результати гірше) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This text is wrong. It should refer to the second element, the parameter value.