Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CONTRIBUTING.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ Navigate into the location where you cloned *Parasect* and install the package w
.. code:: console

$ poetry install
$ poetry self add poetry-plugin-shell
$ poetry self add poetry-plugin-export

You can now run an interactive Poetry shell, giving you access to the virtual environment.

Expand Down
5 changes: 3 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "parasect"
version = "1.2.0"
version = "1.2.2-dev"
description = "Utility for manipulating parameter sets for autopilots."
authors = ["George Zogopoulos <geo.zogop.papal@gmail.com>"]
license = "MIT"
Expand Down
113 changes: 62 additions & 51 deletions src/parasect/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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[
Expand Down Expand Up @@ -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.

Expand All @@ -847,7 +835,10 @@ def build_param_from_mavproxy(item: Sequence) -> Parameter:
value = int(item[1])
except ValueError:
value = float(item[1])
reasoning = item[2]
if len(item) < 3:
reasoning = None
else:
reasoning = item[2]

param = Parameter(name, value)
param.reasoning = reasoning
Expand Down Expand Up @@ -951,45 +942,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()
Expand All @@ -1015,13 +973,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:
for line in f: # pragma: no branch
if line[0] == "#": # Skip comment lines
continue
Expand All @@ -1037,6 +1023,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}")
Expand Down
42 changes: 29 additions & 13 deletions src/parasect/build_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -642,33 +642,45 @@ 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"

# Read footer
yield from self.retrieve_header_footer("footer", Formats.apm)
readonly_string = f"{separator}@READONLY" if csv else "\t@READONLY"
line += readonly_string

# Add comment if requested and description is available
if comments and hasattr(param, 'reasoning') and param.reasoning:
line += f" # {param.reasoning}"

yield line + "\n"

# Read footer
yield from self.retrieve_header_footer("footer", Formats.apm)

def apply_additions_px4(self) -> None:
# Add the AUTOSTART value for each configuration
Expand All @@ -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.")

Expand Down Expand Up @@ -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:
return f"{meal.name}.param"
else:
raise ValueError(f"Unsupported format {format}")
Expand Down
19 changes: 19 additions & 0 deletions tests/assets/ardupilot/mission_planner.param
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 + Spooffing
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 (В СИМУЛЯТОРІ результати гірше - літає боком)
Loading