Set param updates - #1994
Conversation
There was a problem hiding this comment.
Pull request overview
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Updates flight-controller parameter write behavior to detect and surface MAVLink-2 PARAM_ERROR rejections (newer ArduPilot firmware), while keeping legacy “send-only” semantics when no acknowledgement is emitted.
Changes:
- Add a pymavlink compatibility layer to decode
PARAM_ERRORand map error codes to user-facing messages. - Update
FlightControllerParams.set_param()to poll briefly forPARAM_ERRORand return a meaningful failure when received. - Improve fakes/tests for MAVLink connection caching and message-type filtering; add tests for firmware-rejected vs legacy behavior.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_frontend_tkinter_log_analysis.py | Minor formatting-only change in tests. |
| tests/test_backend_flightcontroller_params.py | Adds tests for PARAM_ERROR handling and legacy timeout behavior. |
| tests/test_backend_flightcontroller_factory_mavlink.py | Updates fake factory expectations and adds message-type filtering tests. |
| ardupilot_methodic_configurator/backend_mavlink_param_error.py | Introduces runtime registration/decoding helpers for MAVLink PARAM_ERROR. |
| ardupilot_methodic_configurator/backend_flightcontroller_params.py | Implements PARAM_ERROR polling and error mapping in set_param(). |
| ardupilot_methodic_configurator/backend_flightcontroller_factory_mavlink.py | Stores created fake connections and adds recv_match(type=...) filtering. |
| ARCHITECTURE_2_flight_controller_communication.md | Updates architecture/docs to reflect current synchronous behavior and PARAM_ERROR support. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| ordered_fieldnames: ClassVar[list[str]] = ["param_index", "target_system", "target_component", "param_id", "error"] | ||
| fieldtypes: ClassVar[list[str]] = ["uint8_t", "uint8_t", "char", "int16_t", "uint8_t"] | ||
| orders: ClassVar[list[int]] = [1, 2, 3, 0, 4] | ||
| lengths: ClassVar[list[int]] = [1, 1, 1, 1, 1] |
| The definition matches MAVLink common.xml message 345. Re-registration is harmless | ||
| and also handles applications that change pymavlink's dialect at runtime. | ||
|
|
||
| """ | ||
| mavlink = mavutil.mavlink | ||
| if PARAM_ERROR_MESSAGE_ID in mavlink.mavlink_map: | ||
| return |
| while time_time() - start_time < self.PARAM_SET_PROPAGATION_DELAY: | ||
| message = self.master.recv_match(type="PARAM_ERROR", blocking=False) | ||
| if message is not None: | ||
| error_code = get_param_error_message(message, param_name) | ||
| if error_code is not None: | ||
| return error_code | ||
| # Test doubles or malformed data should not turn into a half-second | ||
| # busy wait. Real pymavlink filtering only returns PARAM_ERROR here. | ||
| if not isinstance(getattr(message, "param_id", None), (bytes, str)): | ||
| return None | ||
| time_sleep(self.PARAM_FETCH_POLL_DELAY) |
☂️ Code Coverage
Overall Coverage
New FilesNo new covered files... Modified FilesNo covered modified files...
|
Test Results 2 files 2 suites 13m 47s ⏱️ Results for commit 65fdfd0. ♻️ This comment has been updated with latest results. |
7dd746a to
e11e273
Compare
| for message in self._pending_param_errors: | ||
| error_code = get_param_error_message(message, param_name) | ||
| if error_code is not None: | ||
| self._pending_param_errors.remove(message) | ||
| return error_code |
| mock_master = MagicMock() | ||
| mock_master.recv_match.side_effect = [SimpleNamespace(param_id="SECOND_PARAM", error=5)] | ||
| mock_conn_mgr = Mock() | ||
| mock_conn_mgr.master = mock_master | ||
| mock_conn_mgr.info = FlightControllerInfo() | ||
| params_mgr = FlightControllerParams(connection_manager=mock_conn_mgr) | ||
|
|
||
| with patch("ardupilot_methodic_configurator.backend_flightcontroller_params.time_time", side_effect=[0.0, 1.0]): | ||
| success, error = params_mgr.set_param("FIRST_PARAM", 1.0) |
| error_code = self._wait_for_param_error(param_name) | ||
| if error_code is not None: | ||
| error_msg = self.PARAM_ERROR_MESSAGES.get(error_code, _("Flight controller rejected parameter write")) | ||
| error_msg = _("Failed to set %(name)s: %(error)s") % {"name": param_name, "error": error_msg} | ||
| logging_error(error_msg) | ||
| return False, error_msg |
| ordered_fieldnames: ClassVar[list[str]] = ["param_index", "target_system", "target_component", "param_id", "error"] | ||
| fieldtypes: ClassVar[list[str]] = ["uint8_t", "uint8_t", "char", "int16_t", "uint8_t"] | ||
| orders: ClassVar[list[int]] = [1, 2, 3, 0, 4] | ||
| lengths: ClassVar[list[int]] = [1, 1, 1, 1, 1] |
Coverage Report for CI Build 33611905508Warning Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes. Coverage increased (+0.006%) to 89.404%Details
Uncovered ChangesNo uncovered changes found. Coverage Regressions14 previously-covered lines in 2 files lost coverage.
Coverage Stats
💛 - Coveralls |
e11e273 to
24ef64c
Compare
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.
| if condition is not None: | ||
| error_message = "Buffered receive does not support conditions" | ||
| raise NotImplementedError(error_message) | ||
|
|
||
| message_types = None if type is None else {type} if isinstance(type, str) else set(type) | ||
| start_time = monotonic() | ||
| while True: | ||
| # A busy telemetry link can always have another unrelated message | ||
| # ready. Check the deadline before every read so a filtered wait | ||
| # cannot indefinitely drain and buffer that stream. | ||
| if timeout is not None and monotonic() - start_time >= timeout: | ||
| return None | ||
| message = self._pop_pending_matching(message_types) | ||
| if message is None: | ||
| recv_msg = self._connection.recv_msg # type: ignore[attr-defined] | ||
| message = recv_msg() |
| fieldnames: ClassVar[list[str]] = ["target_system", "target_component", "param_id", "param_index", "error"] | ||
| ordered_fieldnames: ClassVar[list[str]] = ["param_index", "target_system", "target_component", "param_id", "error"] | ||
| fieldtypes: ClassVar[list[str]] = ["uint8_t", "uint8_t", "char", "int16_t", "uint8_t"] | ||
| orders: ClassVar[list[int]] = [1, 2, 3, 0, 4] | ||
| lengths: ClassVar[list[int]] = [1, 1, 1, 1, 1] | ||
| array_lengths: ClassVar[list[int]] = [0, 0, 16, 0, 0] | ||
| crc_extra: ClassVar[int] = 209 | ||
| unpacker: ClassVar[struct.Struct] = struct.Struct("<hBB16sB") | ||
| native_format: ClassVar[bytearray] = bytearray(b"<hBB16sB") |
| def pack(self, mav: object, force_mavlink1: bool = False) -> bytes: | ||
| """Serialize using the same implementation generated by pymavlink.""" | ||
| return self._pack( # type: ignore[attr-defined] | ||
| mav, | ||
| self.crc_extra, | ||
| self.unpacker.pack( | ||
| self.param_index, | ||
| self.target_system, | ||
| self.target_component, | ||
| self.param_id.encode("ascii"), | ||
| self.error, | ||
| ), | ||
| force_mavlink1=force_mavlink1, | ||
| ) |
| while time_monotonic() - start_time < self.PARAM_SET_PROPAGATION_DELAY: | ||
| remaining = self.PARAM_SET_PROPAGATION_DELAY - (time_monotonic() - start_time) |
| @@ -0,0 +1,48 @@ | |||
| #!/usr/bin/env python3 | |||
Detect the connected ArduPilot firmware version from AUTOPILOT_VERSION and enable PARAM_ERROR acknowledgement handling only for firmware 4.7.0 and newer. Older firmware keeps the original send-only parameter-write behavior without an unnecessary response timeout. Add a local MAVLink-2 PARAM_ERROR compatibility decoder for pymavlink versions that do not define message 345. Preserve unrelated PARAM_VALUE messages received while waiting for an acknowledgement so later parameter operations can consume them safely. Extend the MAVLink connection test double with message filtering, unfiltered message reception, connection lookup, and parameter-send support. Add regression tests covering firmware-version boundaries, PARAM_ERROR decoding, stale replies, message preservation, and legacy write performance.
24ef64c to
65fdfd0
Compare
Description
Set param updates
Checklist
git commit --signoff)Testing
Describe how you tested these changes: