|
43 | 43 | CAN_CMD_SET_ZERO, |
44 | 44 | DEFAULT_BAUDRATE, |
45 | 45 | DEFAULT_TIMEOUT_MS, |
| 46 | + HANDSHAKE_TIMEOUT_S, |
46 | 47 | MODEL_RESOLUTION, |
47 | 48 | MOTOR_LIMIT_PARAMS, |
48 | 49 | NORMALIZED_DATA, |
@@ -215,14 +216,16 @@ def connect(self, handshake: bool = True) -> None: |
215 | 216 | self._is_connected = False |
216 | 217 | raise ConnectionError(f"Failed to connect to CAN bus: {e}") from e |
217 | 218 |
|
218 | | - def _query_status_via_clear_fault(self, motor: NameOrID) -> tuple[bool, can.Message | None]: |
| 219 | + def _query_status_via_clear_fault( |
| 220 | + self, motor: NameOrID, timeout: float = RUNNING_TIMEOUT |
| 221 | + ) -> tuple[bool, can.Message | None]: |
219 | 222 | motor_name = self._get_motor_name(motor) |
220 | 223 | motor_id = self._get_motor_id(motor_name) |
221 | 224 | recv_id = self._get_motor_recv_id(motor_name) |
222 | 225 | data = [0xFF] * 7 + [CAN_CMD_CLEAR_FAULT] |
223 | 226 | msg = can.Message(arbitration_id=motor_id, data=data, is_extended_id=False) |
224 | 227 | self._bus().send(msg) |
225 | | - return self._recv_status_via_clear_fault(expected_recv_id=recv_id) |
| 228 | + return self._recv_status_via_clear_fault(expected_recv_id=recv_id, timeout=timeout) |
226 | 229 |
|
227 | 230 | def _recv_status_via_clear_fault( |
228 | 231 | self, expected_recv_id: int | None = None, timeout: float = RUNNING_TIMEOUT |
@@ -280,7 +283,7 @@ def _handshake(self) -> None: |
280 | 283 | faulted_motors = [] |
281 | 284 |
|
282 | 285 | for motor_name in self.motors: |
283 | | - has_fault, msg = self._query_status_via_clear_fault(motor_name) |
| 286 | + has_fault, msg = self._query_status_via_clear_fault(motor_name, timeout=HANDSHAKE_TIMEOUT_S) |
284 | 287 | if msg is None: |
285 | 288 | missing_motors.append(motor_name) |
286 | 289 | elif has_fault: |
@@ -505,6 +508,87 @@ def _recv_all_responses( |
505 | 508 |
|
506 | 509 | return responses |
507 | 510 |
|
| 511 | + def _recv_all_messages_until_quiet( |
| 512 | + self, |
| 513 | + *, |
| 514 | + timeout: float = RUNNING_TIMEOUT, |
| 515 | + max_messages: int = 4096, |
| 516 | + ) -> list[can.Message]: |
| 517 | + """ |
| 518 | + Receive frames until the bus goes quiet. |
| 519 | +
|
| 520 | + Args: |
| 521 | + timeout: Poll timeout used for each recv() call. Collection stops |
| 522 | + when one recv() times out (quiet gap). |
| 523 | + max_messages: Safety cap to prevent unbounded loops. |
| 524 | + """ |
| 525 | + out: list[can.Message] = [] |
| 526 | + max_messages = max(1, max_messages) |
| 527 | + timeout = max(0.0, timeout) |
| 528 | + |
| 529 | + try: |
| 530 | + while len(out) < max_messages: |
| 531 | + msg = self._bus().recv(timeout=timeout) |
| 532 | + if msg is None: |
| 533 | + break |
| 534 | + out.append(msg) |
| 535 | + except (can.CanError, OSError) as e: |
| 536 | + logger.debug(f"Error draining CAN RX queue on {self.port}: {e}") |
| 537 | + |
| 538 | + return out |
| 539 | + |
| 540 | + def _process_feedback_messages(self, messages: list[can.Message]) -> set[int]: |
| 541 | + """ |
| 542 | + Decode all received feedback frames and update cached motor states. |
| 543 | +
|
| 544 | + Returns: |
| 545 | + Set of payload recv_ids that were successfully mapped to motors. |
| 546 | + """ |
| 547 | + processed_recv_ids: set[int] = set() |
| 548 | + for msg in messages: |
| 549 | + if len(msg.data) < 1: |
| 550 | + logger.debug( |
| 551 | + f"Dropping short CAN frame on {self.port} " |
| 552 | + f"(arb=0x{int(msg.arbitration_id):02X}, data={bytes(msg.data).hex()})" |
| 553 | + ) |
| 554 | + continue |
| 555 | + |
| 556 | + recv_id = int(msg.data[0]) |
| 557 | + motor_name = self._recv_id_to_motor.get(recv_id) |
| 558 | + if motor_name is None: |
| 559 | + logger.debug( |
| 560 | + f"Unmapped CAN frame on {self.port} " |
| 561 | + f"(arb=0x{int(msg.arbitration_id):02X}, recv_id=0x{recv_id:02X}, data={bytes(msg.data).hex()})" |
| 562 | + ) |
| 563 | + continue |
| 564 | + |
| 565 | + self._process_response(motor_name, msg) |
| 566 | + processed_recv_ids.add(recv_id) |
| 567 | + |
| 568 | + return processed_recv_ids |
| 569 | + |
| 570 | + def flush_rx_queue(self, poll_timeout_s: float = 0.0005, max_messages: int = 4096) -> int: |
| 571 | + """ |
| 572 | + Drain pending RX frames from the CAN interface. |
| 573 | +
|
| 574 | + This is used by higher-level controllers to drop stale feedback before issuing |
| 575 | + a fresh read cycle, so subsequent state reads are based on most recent replies. |
| 576 | + It should also be called once when a controller instance is created/connected, |
| 577 | + to clear residual frames left on the interface from previous sessions. |
| 578 | + """ |
| 579 | + drained = 0 |
| 580 | + poll_timeout_s = max(0.0, poll_timeout_s) |
| 581 | + max_messages = max(1, max_messages) |
| 582 | + try: |
| 583 | + while drained < max_messages: |
| 584 | + msg = self._bus().recv(timeout=poll_timeout_s) |
| 585 | + if msg is None: |
| 586 | + break |
| 587 | + drained += 1 |
| 588 | + except (can.CanError, OSError) as e: |
| 589 | + logger.debug(f"Failed to flush CAN RX queue on {self.port}: {e}") |
| 590 | + return drained |
| 591 | + |
508 | 592 | def _speed_control( |
509 | 593 | self, |
510 | 594 | motor: NameOrID, |
@@ -644,11 +728,14 @@ def _mit_control_batch( |
644 | 728 | msg = can.Message(arbitration_id=motor_id, data=data, is_extended_id=False) |
645 | 729 | self._bus().send(msg) |
646 | 730 | recv_id_to_motor[self._get_motor_recv_id(motor)] = motor_name |
| 731 | + # Read every feedback frame until RX goes quiet, then decode all of them. |
| 732 | + # This avoids dropping useful frames when responses from different motors interleave. |
| 733 | + messages = self._recv_all_messages_until_quiet() |
| 734 | + processed_recv_ids = self._process_feedback_messages(messages) |
647 | 735 |
|
648 | | - responses = self._recv_all_responses(list(recv_id_to_motor.keys()), timeout=RUNNING_TIMEOUT) |
649 | 736 | for recv_id, motor_name in recv_id_to_motor.items(): |
650 | | - if msg := responses.get(recv_id): |
651 | | - self._process_response(motor_name, msg) |
| 737 | + if recv_id not in processed_recv_ids: |
| 738 | + logger.warning(f"Packet drop: {motor_name} (ID: 0x{recv_id:02X}). Using last known state.") |
652 | 739 |
|
653 | 740 | def _float_to_uint(self, x: float, x_min: float, x_max: float, bits: int) -> int: |
654 | 741 | """Convert float to unsigned integer for CAN transmission.""" |
@@ -711,7 +798,10 @@ def _process_response(self, motor: str, msg: can.Message) -> None: |
711 | 798 | try: |
712 | 799 | self._decode_motor_state(msg.data) |
713 | 800 | except Exception as e: |
714 | | - logger.warning(f"Failed to decode response from {motor}: {e}") |
| 801 | + logger.warning( |
| 802 | + f"Failed to decode response from {motor} " |
| 803 | + f"(arb=0x{int(msg.arbitration_id):02X}, data={bytes(msg.data).hex()}): {e}" |
| 804 | + ) |
715 | 805 |
|
716 | 806 | def _get_cached_value(self, motor: str, data_name: str) -> Value: |
717 | 807 | """Retrieve a specific value from the state cache.""" |
@@ -848,20 +938,12 @@ def _batch_refresh(self, motors: list[str]) -> None: |
848 | 938 | self._bus().send(msg) |
849 | 939 | updated_motors.append(motor) |
850 | 940 |
|
851 | | - expected_recv_ids = [self._get_motor_recv_id(motor) for motor in updated_motors] |
852 | | - responses = self._recv_all_responses(expected_recv_ids, timeout=RUNNING_TIMEOUT) |
853 | | - |
854 | | - for response in responses.values(): |
855 | | - payload_motor_name = self._recv_id_to_motor.get(response.data[0]) |
856 | | - if payload_motor_name is not None: |
857 | | - self._process_response(payload_motor_name, response) |
858 | | - else: |
859 | | - # Fallback: still attempt to decode based on payload byte0 mapping. |
860 | | - self._decode_motor_state(response.data) |
| 941 | + messages = self._recv_all_messages_until_quiet() |
| 942 | + processed_recv_ids = self._process_feedback_messages(messages) |
861 | 943 |
|
862 | 944 | for motor in updated_motors: |
863 | 945 | recv_id = self._get_motor_recv_id(motor) |
864 | | - if recv_id not in responses: |
| 946 | + if recv_id not in processed_recv_ids: |
865 | 947 | logger.warning(f"Packet drop: {motor} (ID: 0x{recv_id:02X}). Using last known state.") |
866 | 948 |
|
867 | 949 | def read_calibration(self) -> dict[str, MotorCalibration]: |
|
0 commit comments