
    async def motor_move_execute(self,
                                 node_id: int,
                                 position: int,
                                 velocity: int,
                                 acceleration: int,
                                 index: int, # Corresponds to MoveBuffer index
                                 relative: bool = False,
                                 timeout_msec: int = 10000,
                                 joint_move_direction: eJointMoveDirection = eJointMoveDirection.ShortestWay,
                                 wait_until_done: bool = True) -> int:
        """
        Executes a single motor move operation.
        Equivalent to C# MotorMoveExecute.
        """
        event_data = sEventData()
        flags_control_word = 0 # Corresponds to C# num1 (ushort)

        if relative:
            flags_control_word |= 0x40 # Bit 6 (0x40) for Relative mode

        node_idx = self.get_node_id_index(node_id)
        if node_idx == -1:
            return 1 # Node not found error

        # C# Check: `if (!self.MoveDoneResponse[self.GetNodeIDIndex(NodeID)])`
        if not self.move_done_response[node_idx]:
            # Wait for previous move to complete if it hasn't, or timeout
            # C# `long TimeoutMSec = checked (self.MoveDoneTime[self.GetNodeIDIndex(NodeID)] - (long) Environment.TickCount);`
            # This requires converting C# Environment.TickCount to Python time.monotonic()
            
            remaining_timeout_sec = (self.move_done_time[node_idx] - time.monotonic() * 1000) / 1000.0
            if remaining_timeout_sec <= 0:
                remaining_timeout_sec = 1.0 # Default 1 sec

            err_code = await self.motor_wait_for_move_done(node_id, int(remaining_timeout_sec * 1000))
            if err_code != 0:
                self.move_error_index = index
                self.move_error_code = err_code
                return err_code

        self.sending_move_command = True
        err_code = await self.pvt_select_mode(False) # Ensure PVT mode is off
        if err_code != 0: return err_code

        err_code = await self.motor_set_move_direction(node_id, joint_move_direction)
        if err_code != 0:
            self.move_error_index = index
            self.move_error_code = err_code
            return err_code

        # Set target position (0x607A)
        err_code = await self.can_sdo_send_elmo_object(node_id, eElmoObject.TargetPosition, 0, str(position), eElmoObjectDataType.INTEGER32)
        if err_code != 0:
            self.move_error_index = index
            self.move_error_code = err_code
            return err_code

        # Set Profile Velocity (0x6081)
        err_code = await self.can_sdo_send_elmo_object(node_id, eElmoObject.ElmoParamSV, 0, str(velocity), eElmoObjectDataType.UNSIGNED32) # In C# context, it's Object 0x6081, ElmoParamSV is also used.
        if err_code != 0: # C# uses different object keys like 0x60FF for TargetVelocity. Cross-check.
            self.move_error_index = index
            self.move_error_code = err_code
            return err_code

        # Set Profile Acceleration (0x6083)
        err_code = await self.can_sdo_send_elmo_object(node_id, eElmoObject.ElmoParamAC, 0, str(acceleration), eElmoObjectDataType.UNSIGNED32) # 0x6083 is Profile Acceleration
        if err_code != 0:
            self.move_error_index = index
            self.move_error_code = err_code
            return err_code

        # Set Profile Deceleration (0x6084)
        err_code = await self.can_sdo_send_elmo_object(node_id, eElmoObject.ElmoParamDC, 0, str(acceleration), eElmoObjectDataType.UNSIGNED32) # 0x6084 is Profile Deceleration
        if err_code != 0:
            self.move_error_index = index
            self.move_error_code = err_code
            return err_code

        self.move_done_response[node_idx] = False
        self.move_done_time[node_idx] = int(time.monotonic() * 1000) + timeout_msec # Store end time in ms

        # Control word set to start motion (0x004F for absolute, 0x0040 + 0x000F for profile position mode)
        # C# sends 0x0047 (decimal) -> 0x2F (hex). then 0x005F (0x47 + 0x10)
        
        # First CW set
        cw1 = 0x2F | flags_control_word
        err_code = await self.control_word_set(node_id, cw1)
        if err_code != 0:
            self.move_error_index = index
            self.move_error_code = err_code
            return err_code

        # Second CW set: (previous CW value) | (new set point bit 0x10)
        cw2 = cw1 | 0x10
        err_code = await self.control_word_set(node_id, cw2)
        if err_code != 0:
            self.move_error_index = index
            self.move_error_code = err_code
            return err_code

        self.move_buffer[index].motion_started = True
        self.sending_move_command = False

        if wait_until_done:
            err_code = await self.motor_wait_for_move_done(node_id, timeout_msec)
            if err_code != 0:
                self.move_error_index = index
                self.move_error_code = err_code
                return err_code
            
            event_data.event_type = eEventType.MotorMoveDone
            event_data.node_id = node_id
            event_data.me_index = index
            event_data.mmd_all_moves_done = (self.move_buffer_first_index == self.move_buffer_last_index)
            await self._raise_an_event(event_data)

        return 0


    async def motor_set_move_direction(self, node_id: int, direction: eJointMoveDirection) -> int:
        """
        Sets the joint move direction for a motor.
        Equivalent to C# MotorSetMoveDirection.
        """
        node_idx = self.get_node_id_index(node_id)
        if node_idx == -1:
            return 1 # Error: Node not found

        if self.gc_joint_move_direction and self.joint_move_direction_initialized and \
           self.gc_joint_move_direction[node_idx] == direction and self.joint_move_direction_initialized[node_idx]:
            return 0 # Already set

        value_to_send: str = ""
        if direction == eJointMoveDirection.Normal:
            value_to_send = "1"
        elif direction == eJointMoveDirection.Clockwise:
            value_to_send = "65"
        elif direction == eJointMoveDirection.Counterclockwise:
            value_to_send = "129"
        elif direction == eJointMoveDirection.ShortestWay:
            value_to_send = "193"
        
        # Search for `24818` in C# (0x60F2).
        # This is `eElmoObject.ElmoParamHV` in C#. Let's use `eElmoObject(0x60F2)`.
        
        class ElmoObject_Custom(IntEnum):
            JointMoveDirection = 0x60F2 # Custom value for this specific field

        err_code = await self.can_sdo_send_elmo_object(node_id, ElmoObject_Custom.JointMoveDirection, 0, value_to_send, eElmoObjectDataType.UNSIGNED16)
        if err_code != 0: return err_code

        if self.gc_joint_move_direction and self.joint_move_direction_initialized:
            self.gc_joint_move_direction[node_idx] = direction
            self.joint_move_direction_initialized[node_idx] = True

        return 0


    async def motor_emergency_stop(self, node_id: int) -> None:
        """
        Immediately stops a motor.
        Equivalent to C# MotorEmergencyStop.
        C# `BinaryInterpreter((byte) NodeID1, "MO", 0, clsCANMotor.eCmdType.ValSet, ref local1, ResponseTime: ref local2)`
        The ValueSet to "MO" (Motor Operation Mode) to 0. (Disable motor operation)
        """
        # "MO" command (Motor Operation) with value "0" (off or disabled)
        # NodeID is passed as the first parameter. C# calls BinaryInterpreter as an awaitable.
        value_ref = "0"
        await self.binary_interpreter(node_id, "MO", 0, eCmdType.ValSet, value_ref)
        # Note: C# doesn't check the return code of this one.


    async def motor_wait_for_move_done(self, node_id: int, timeout_msec: int) -> int:
        """
        Waits for a single motor to complete its move.
        Equivalent to C# MotorWaitForMoveDone.
        """
        done = False
        start_time = time.monotonic()
        timeout_sec = float(timeout_msec) / 1000.0

        node_idx = self.get_node_id_index(node_id)
        if node_idx == -1:
            return 1 # Node not found error


        while (time.monotonic() - start_time) < (timeout_sec + 2.0): # C# 2000ms buffer
            if self.move_done_response[node_idx]:
                done = True
                break
            
            if self.emcy_move_error_received:
                self.error_code[1] = self.emcy_move_error
                return 1 # Emergency error

            await asyncio.sleep(0.01) # C# Thread.Sleep(10)

        if not done:
            done_ref_val = False # Create a mutable container for the ref bool
            err_code = await self.motor_check_if_move_done(node_id, done_ref_val) # C# passes ref bool
            if err_code != 0: return err_code
            if not done_ref_val:
                return 30 # Motion timed out

        return 0
    
    async def motor_check_if_move_done(self, node_id: int, done_ref: bool) -> int:
        """
        Checks if a motor's move is complete.
        Equivalent to C# MotorCheckIfMoveDone.
        The `done_ref` simulates `ref bool Done` and modifies it in place for returning value.
        """
        response_str = ""
        err_code = await self.binary_interpreter(node_id, "MS", 0, eCmdType.ValQuery, response_str)
        if err_code != 0:
            return err_code
        
        # C# Left2 = str; Operators.CompareString(Left2, "0", false) == 0;
        if response_str.upper() == "0":
            done_ref = True # Motion status '0' means moving
        elif response_str.upper() == "1":
            # Motion Status '1' usually means 'target reached' but can mean 'error'.
            # C# then checks MO (Motor Operation) mode
            mo_response = ""
            err_code = await self.binary_interpreter(node_id, "MO", 0, eCmdType.ValQuery, mo_response)
            if err_code != 0: return err_code

            if mo_response.upper() == "1": # If motor is enabled
                done_ref = True
            else:
                done_ref = False
                fault_str = "" # Passed by ref
                fault_err_code = await self.motor_get_fault(node_id, fault_str)
                if fault_err_code != 0: return fault_err_code
                
                self.error_code[10] = f"Motor fault. {fault_str}"
                return 10
        else: # Motion Status '2' or other indicates error or not done.
            done_ref = False # Must be explicitly set to False if not 0 or 1.

        return 0


    async def motor_get_fault(self, node_id: int, fault_ref: str) -> int:
        """
        Retrieves the fault status of a motor.
        Equivalent to C# MotorGetFault.
        """
        expression = ""
        err_code = await self.binary_interpreter(node_id, "MF", 0, eCmdType.ValQuery, expression)
        if err_code != 0:
            fault_ref = ""
            return err_code

        if not vb_isnumeric(expression):
            fault_ref = ""
            return 0 # No numeric fault code

        fault_code = to_long(expression)

        if fault_code == 0:
            fault_ref = ""
            return 0 # No active fault

        # Build fault string based on bit flags
        fault_messages: List[str] = []
        if (fault_code & 0x1) == 0x1: fault_messages.append("Motor Hall sensor feedback angle not found yet.")
        if (fault_code & 0x4) == 0x4: fault_messages.append("Feedback loss: no match between encoder and Hall location.")
        if (fault_code & 0x8) == 0x8: fault_messages.append("The peak current has been exceeded.")
        if (fault_code & 0x10) == 0x10: fault_messages.append("Inhibit.")
        if (fault_code & 0x40) == 0x40: fault_messages.append("Two digital Hall sensors were changed at the same time.")
        if (fault_code & 0x80) == 0x80: fault_messages.append("Speed tracking error.")
        if (fault_code & 0x100) == 0x100: fault_messages.append("Position tracking error.")
        if (fault_code & 0x200) == 0x200: fault_messages.append("Inconsistent drive database.")
        if (fault_code & 0x400) == 0x400: fault_messages.append("Too large a difference in ECAM table.")
        if (fault_code & 0x800) == 0x800: fault_messages.append("CAN heartbeat failure.")
        if (fault_code & 0x1000) == 0x1000: fault_messages.append("Servo drive fault.")
        
        # Voltage faults, C# had complex conditions for these.
        # (num2 & 32768L) == 0L & (num2 & 16384L) == 0L & (num2 & 8192L) == 8192L => Under-voltage (0x2000)
        if (fault_code & 0x8000) == 0 and (fault_code & 0x4000) == 0 and (fault_code & 0x2000) == 0x2000:
            fault_messages.append("Power supply under voltage.")
        # (num2 & 32768L) == 0L & (num2 & 16384L) == 16384L & (num2 & 8192L) == 0L => Over-voltage (0x4000)
        if (fault_code & 0x8000) == 0 and (fault_code & 0x4000) == 0x4000 and (fault_code & 0x2000) == 0:
            fault_messages.append("Power supply over voltage.")

        # C# conditions for Motor Lead Short Circuit and Drive Overheated (0x8000 and 0x4000 bits)
        # (num2 & 32768L) == 32768L & (num2 & 16384L) == 0L & (num2 & 8192L) == 8192L => 0x8000, 0x2000 for Short Circuit
        if (fault_code & 0x8000) == 0x8000 and (fault_code & 0x4000) == 0 and (fault_code & 0x2000) == 0x2000:
            fault_messages.append("Motor lead short circuit or faulty drive.")
        # (num2 & 32768L) == 32768L & (num2 & 16384L) == 16384L & (num2 & 8192L) == 0L => 0x8000, 0x4000 for Overheated
        if (fault_code & 0x8000) == 0x8000 and (fault_code & 0x4000) == 0x4000 and (fault_code & 0x2000) == 0:
            fault_messages.append("Drive overheated.")


        if (fault_code & 0x10000) == 0x10000: fault_messages.append("Failed to find the electrical zero of the motor during startup.")
        if (fault_code & 0x20000) == 0x20000: fault_messages.append("Speed limit exceeded.")
        if (fault_code & 0x40000) == 0x40000: fault_messages.append("Drive CPU stack overflow.")
        if (fault_code & 0x80000) == 0x80000: fault_messages.append("Drive CPU exception.")
        if (fault_code & 0x200000) == 0x200000: fault_messages.append("Motor stuck.")
        if (fault_code & 0x400000) == 0x400000: fault_messages.append("Position limit exceeded.")
        if (fault_code & 0x20000000) == 0x20000000: fault_messages.append("Cannot start motor.")
        
        fault_ref = " ".join(fault_messages)
        return 0


    async def pvt_select_mode(self, enable: bool) -> int:
        """
        Enables or disables PVT (Position, Velocity, Time) mode.
        Equivalent to C# PVTSelectMode.
        """
        data_byte = [0] * 1 # C# had `byte[] DataByte = new byte[1];`
        
        if enable:
            if not self.pvt_mode:
                if self.grp_node_id:
                    for node_id in self.grp_node_id:
                        # C# sends `0` to object 0x60C4 (PVT mode select), subindex 6. Means turn off.
                        # Then it sends `7` to object 0x6060 (Modes of Operation), subindex 0. Means interpolate mode.
                        
                        # Set Modes of Operation (0x6060) to Interpolated Position Mode (7)
                        err_code = await self.can_sdo_send_elmo_object(node_id, eElmoObject(0x6060), 0, "7", eElmoObjectDataType.INTEGER8)
                        if err_code != 0: return err_code
                        
                self.pvt_mode = True # Now in PVT mode

        else: # Disable PVT mode
            if self.pvt_mode:
                if self.grp_node_id:
                    for node_id in self.grp_node_id:
                        # Set Modes of Operation (0x6060) back to Profile Position Mode (1), or other safe mode.
                        # C# code sends `1` to 0x6060, subindex 0
                        err_code = await self.can_sdo_send_elmo_object(node_id, eElmoObject(0x6060), 0, "1", eElmoObjectDataType.INTEGER8)
                        if err_code != 0: return err_code

                self.pvt_mode = False # Not in PVT mode

        return 0

    async def control_word_set(self, node_id: int, value: int, SYNC: bool = True) -> int:
        """
        Sets the control word for a motor.
        Equivalent to C# ControlWordSet. This sends an RPDO1 message.
        """
        data_bytes = list(value.to_bytes(2, 'little')) # Control word is 2 bytes (ushort)
        err_code = await self.can_write(eCOBType.RPDO1, node_id,
                                        byte0=data_bytes[0], byte1=data_bytes[1],
                                        data_length=2)
        if err_code != 0: return err_code
        
        if SYNC:
            err_code = await self.can_sync()
            if err_code != 0: return err_code
        
        return 0

    async def can_sync(self) -> int:
        """
        Sends a SYNC message on the CAN bus.
        Equivalent to C# CAN_SYNC.
        """
        err_code = await self.can_write(eCOBType.SYNC, 0) # SYNC message has NodeID 0
        if err_code != 0:
            self.error_code[5] = self.can_get_error(err_code)
            return 5
        return 0

    async def motors_move_absolute_execute(self,
                                           node_ids: List[int],
                                           positions: List[int],
                                           velocities: List[int],
                                           accelerations: List[int],
                                           index: int,
                                           joint_move_directions: List[eJointMoveDirection],
                                           timeout_msec: int = 10000) -> int:
        """
        Executes a synchronized absolute move for multiple motors.
        Equivalent to C# MotorsMoveAbsoluteExecute.
        """
        event_data = sEventData()
        
        # Wait for any previous moves on all nodes to finish
        # This involves polling `self.move_done_response` for all `grp_node_id` or `node_ids`
        # and calling `motors_wait_for_move_done`.
        # C# logic: `flag=true`, `num1=-2147483647` is `MoveDoneTime` for longest timeout.
        
        all_moves_done = True
        max_prev_move_end_time = -1 # A sentinel value for Python instead of large negative
        if self.grp_node_id:
            for grp_node_id in self.grp_node_id:
                node_idx = self.get_node_id_index(grp_node_id)
                if node_idx != -1 and not self.move_done_response[node_idx]:
                    all_moves_done = False
                    if self.move_done_time[node_idx] > max_prev_move_end_time:
                        max_prev_move_end_time = self.move_done_time[node_idx]

        if not all_moves_done:
            # Timeout for previous moves: (max_prev_move_end_time - current_time_ms)
            remaining_timeout = max(0, int(max_prev_move_end_time - time.monotonic() * 1000))
            err_code = await self.motors_wait_for_move_done(remaining_timeout)
            if err_code != 0:
                self.move_error_index = index
                self.move_error_code = err_code
                return err_code

        self.sending_move_command = True
        err_code = await self.pvt_select_mode(False) # Ensure PVT mode is off
        if err_code != 0: return err_code

        # For each motor: set direction, position, velocity, acceleration
        for i, node_id in enumerate(node_ids):
            err_code = await self.motor_set_move_direction(node_id, joint_move_directions[i])
            if err_code != 0:
                self.move_error_index = index
                self.move_error_code = err_code
                return err_code
            
            # Set target position
            err_code = await self.can_sdo_send_elmo_object(node_id, eElmoObject.TargetPosition, 0, str(positions[i]), eElmoObjectDataType.INTEGER32)
            if err_code != 0:
                self.move_error_index = index
                self.move_error_code = err_code
                return err_code

            # Set Profile Velocity
            err_code = await self.can_sdo_send_elmo_object(node_id, eElmoObject.ElmoParamSV, 0, str(velocities[i]), eElmoObjectDataType.UNSIGNED32)
            if err_code != 0:
                self.move_error_index = index
                self.move_error_code = err_code
                return err_code

            # Set Profile Acceleration and Deceleration
            accel_val = max(100, accelerations[i]) # C# ensures accel >= 100
            err_code = await self.can_sdo_send_elmo_object(node_id, eElmoObject.ElmoParamAC, 0, str(accel_val), eElmoObjectDataType.UNSIGNED32)
            if err_code != 0:
                self.move_error_index = index
                self.move_error_code = err_code
                return err_code
            err_code = await self.can_sdo_send_elmo_object(node_id, eElmoObject.ElmoParamDC, 0, str(accel_val), eElmoObjectDataType.UNSIGNED32)
            if err_code != 0:
                self.move_error_index = index
                self.move_error_code = err_code
                return err_code

        err_code = await self.motors_move_start(node_ids, True, timeout_msec, index)
        if err_code != 0:
                self.move_error_index = index
                self.move_error_code = err_code
                return err_code

        event_data.event_type = eEventType.MotorsMoveDone
        event_data.me_index = index
        event_data.mmd_all_moves_done = (self.move_buffer_first_index == self.move_buffer_last_index)
        await self._raise_an_event(event_data)

        return 0
    
    async def motors_move_start(self, node_ids: List[int], wait_until_done: bool, timeout_msec: int, index: int) -> int:
        """
        Starts a synchronized move for multiple motors and waits for completion.
        Equivalent to C# MotorsMoveStart.
        """
        # Mark all involved nodes as 'move in progress'
        for node_id in node_ids:
            node_idx = self.get_node_id_index(node_id)
            if node_idx != -1:
                self.move_done_response[node_idx] = False

        # Send Control Word 0x0047 to all nodes, then 0x004F
        # C# sends 0x0047 to all nodes, then 0x005F to all nodes. Group sync is important.

        # Control Word 1: 0x47 (0b01000111)
        # Assuming `47` is `0x2F` as derived before (Operation Enabled, Change Set Immediately).
        # C# uses 47, then 47 + num1 (+ relative/absolute flag), then 47 + num1 + 16 (new setpoint)
        # So here it should be 0x2F (base for profile position mode)
        # The `num1` (relative flag) isn't used here, it's for `motor_move_execute`.
        # `47` directly, then `63` (0x3F).
        # First: CW = 47. 
        # Then CW = 63 (0x3F). This is 0x2F (base) | 0x10 (new setpoint).
        # So the sequence is 0x2F, then 0x3F.
        
        # This will send RPDO1 for ControlWord
        sync_flag = False
        if self.grp_node_id:
            for i, node_id in enumerate(self.grp_node_id):
                if i == len(self.grp_node_id) - 1: # Last node in group gets SYNC
                    sync_flag = True
                
                err_code = await self.control_word_set(node_id, 0x2F, SYNC=sync_flag)
                if err_code != 0: return err_code
        
        sync_flag = False
        if self.grp_node_id:
            for i, node_id in enumerate(self.grp_node_id):
                if i == len(self.grp_node_id) - 1: # Last node in group gets SYNC
                    sync_flag = True
                
                err_code = await self.control_word_set(node_id, 0x3F, SYNC=sync_flag)
                if err_code != 0: return err_code

        self.move_buffer[index].motion_started = True
        self.sending_move_command = False

        if wait_until_done:
            start_time = time.monotonic()
            for node_id in node_ids:
                node_elapsed_time = int(time.monotonic() * 1000 - start_time * 1000)
                remaining_timeout = max(0, timeout_msec - node_elapsed_time + 1000) # C# adds 1000ms buffer
                err_code = await self.motor_wait_for_move_done(node_id, remaining_timeout)
                if err_code != 0: return err_code
        
        return 0

    async def motors_wait_for_move_done(self, timeout_msec: int) -> int:
        """
        Waits for multiple motors (in the group) to complete their moves.
        Equivalent to C# MotorsWaitForMoveDone.
        """
        done = False
        start_time = time.monotonic()
        timeout_sec = float(timeout_msec) / 1000.0
        
        # C# `WriteToDebugTraceFile` logic could be implemented if `self.write_to_debug_trace_file` is set
        # with `self.write_string_to_file`.

        if self.grp_node_id is None:
            return 0 # No group defined, nothing to wait for.

        while (time.monotonic() - start_time) < (timeout_sec + 2.0): # C# 2000ms buffer
            temp_done = True
            for grp_node_id in self.grp_node_id:
                node_idx = self.get_node_id_index(grp_node_id)
                if node_idx == -1 or not self.move_done_response[node_idx]:
                    temp_done = False
                    break
            
            if temp_done:
                done = True
                break

            if self.emcy_move_error_received:
                # C# complex error aggregation for multiple faults.
                # Simplified: if any emergency error came, return it.
                self.error_code[1] = self.emcy_move_error
                return 1

            await asyncio.sleep(0.01)

        if not done:
            done_ref_val = False # Ref variable to pass
            err_code = await self.motors_check_if_move_done(done_ref_val)
            if err_code != 0: return err_code
            if not done_ref_val:
                # C# builds a detailed timeout message with Motion Status of each axis.
                # Requires `motor_get_motion_status`
                
                motion_status_details: List[str] = []
                if self.grp_node_id:
                    for i, node_id in enumerate(self.grp_node_id):
                        motion_status_val = 0 # Passed by ref int
                        status_err = await self.motor_get_motion_status(node_id, motion_status_val)
                        if status_err != 0: 
                            motion_status_details.append(f"Axis{i}={status_err} (Status Error)")
                        else:
                            motion_status_details.append(f"Axis{i}={motion_status_val}")
                
                self.error_code[1] = "Motion timed out. " + ", ".join(motion_status_details)
                return 1

        return 0

    async def motors_check_if_move_done(self, done_ref: bool) -> int:
        """
        Checks if moves for all motors in the group are complete.
        Equivalent to C# MotorsCheckIfMoveDone.
        """
        response_str = "" # For binary interpreter response (comma sep)
        if self.grp_id > 0:
            err_code = await self.binary_interpreter(self.grp_id, "MS", 0, eCmdType.ValQuery, response_str)
            if err_code != 0: return err_code
        else:
            # If no group ID, or group query failed, need to query each node individually
            # This case is less explicit in C#, but implies similar behavior for single motor.
            done_ref = False # Must explicitly set it or retrieve from each motor.
            return 0

        motion_status_strings = response_str.split(",")
        if self.grp_node_id and len(motion_status_strings) != len(self.grp_node_id):
            done_ref = False
            return 0 # Mismatch in response length

        done_ref = True
        if self.grp_node_id:
            for i, grp_node_id in enumerate(self.grp_node_id):
                if not vb_isnumeric(motion_status_strings[i]):
                    done_ref = False
                    return 0
                
                status = to_integer(motion_status_strings[i])
                
                if status == 0: # Moving
                    pass # Keep done_ref as True if others are done, or set to False if any is moving
                elif status == 1: # Target Reached/Ready
                    mo_response = ""
                    mo_err = await self.binary_interpreter(grp_node_id, "MO", 0, eCmdType.ValQuery, mo_response)
                    if mo_err != 0: return mo_err
                    if mo_response.upper() != "1": # If enabled
                        fault_str = ""
                        fault_err = await self.motor_get_fault(grp_node_id, fault_str)
                        if fault_err != 0: return fault_err
                        self.error_code[10] = f"Motor {grp_node_id} fault. {fault_str}"
                        done_ref = False # Not truly done if there's a fault
                        return 10
                else: # Any other status usually means not done or error.
                    done_ref = False
        return 0 # All clear, or error code returned earlier

    async def motor_get_motion_status(self, node_id: int, motion_status_ref: int) -> int:
        """
        Retrieves the current motion status of a motor.
        Equivalent to C# MotorGetMotionStatus.
        """
        response_str = ""
        err_code = await self.binary_interpreter(node_id, "MS", 0, eCmdType.ValQuery, response_str)
        if err_code != 0: return err_code

        if vb_isnumeric(response_str):
            motion_status_ref = to_integer(response_str) # Assign to ref
        else:
            motion_status_ref = 0 # Default if not numeric (error in response)
            self.error_code[1] = f"Invalid motion status response for node {node_id}: {response_str}"
            return 1

        return 0

    async def motors_move_path_execute(self,
                                       pva_point: List[List[sPVAPoint]],
                                       time_interval_msec: int,
                                       stop_deceleration: List[int],
                                       index: int,
                                       skip_ax: List[bool],
                                       timeout_msec: int = 10000) -> int:
        """
        Executes a synchronized path movement for multiple motors using PVT (Position, Velocity, Time).
        Equivalent to C# MotorsMovePathExecute.
        """
        event_data = sEventData()

        # Check for previous moves and wait if outstanding. Similar to motors_move_absolute_execute.
        all_moves_done = True
        max_prev_move_end_time = -1
        if self.grp_node_id:
            for grp_node_id in self.grp_node_id:
                node_idx = self.get_node_id_index(grp_node_id)
                if node_idx != -1 and not self.move_done_response[node_idx]:
                    all_moves_done = False
                    if self.move_done_time[node_idx] > max_prev_move_end_time:
                        max_prev_move_end_time = self.move_done_time[node_idx]

        if not all_moves_done:
            remaining_timeout = max(0, int(max_prev_move_end_time - time.monotonic() * 1000))
            err_code = await self.motors_wait_for_move_done(remaining_timeout)
            if err_code != 0:
                self.move_error_index = index
                self.move_error_code = err_code
                return err_code

        self.sending_move_command = True

        # Set PVT Time Interval if changed
        if self.pvt_time_interval_msec != time_interval_msec:
            err_code = await self.pvt_set_time_interval(time_interval_msec)
            if err_code != 0: return err_code
            self.pvt_time_interval_msec = time_interval_msec
        
        err_code = await self.pvt_select_mode(True) # Put into PVT mode
        if err_code != 0: return err_code

        # Mark nodes as 'move in progress' that are not skipped
        last_active_node_in_group = -1
        if self.grp_node_id:
            for i, node_id in enumerate(self.grp_node_id):
                if not skip_ax[i]:
                    node_idx = self.get_node_id_index(node_id)
                    if node_idx != -1:
                        self.move_done_response[node_idx] = False
                    last_active_node_in_group = i # Keep track for final SYNC

        # Send initial PVA data points (first 8 points as per C# logic)
        num_pva_points_to_send_initially = min(8, len(pva_point[0])) # Assuming pva_point[0] for len
        
        if self.grp_node_id:
            for point_idx in range(num_pva_points_to_send_initially):
                for node_idx_in_group, grp_node_id in enumerate(self.grp_node_id):
                    if not skip_ax[node_idx_in_group]:
                        current_pva = pva_point[grp_node_id][point_idx] # C# uses NodeID as first index for PVA.
                        err_code = await self.pvt_send_data_point(grp_node_id, current_pva.p, current_pva.v)
                        if err_code != 0: return err_code
        
        # Reset PVT_EMCY flags
        if self.grp_node_id:
            for node_id in self.grp_node_id:
                self.pvt_emcy[node_id].queue_full.state = False
                self.pvt_emcy[node_id].queue_low.state = False

        # Start motion
        err_code = await self.pvt_begin_motion(True, skip_ax)
        if err_code != 0: return err_code

        path_motion_start_time = time.monotonic()
        self.move_buffer[index].motion_started = True
        self.sending_move_command = False

        # Loop to send remaining PVA data points and handle PVT stops / errors
        total_pva_points = len(pva_point[0]) # Get total points from first axis
        if self.grp_node_id:
            for point_idx in range(8, total_pva_points): # Start from 8 as first 8 were sent
                for node_idx_in_group, grp_node_id in enumerate(self.grp_node_id):
                    if not skip_ax[node_idx_in_group]:
                        current_pva = pva_point[grp_node_id][point_idx]
                        err_code = await self.pvt_send_data_point(grp_node_id, current_pva.p, current_pva.v)
                        if err_code != 0: return err_code
                
                # Check for PVTStop and PVT_EMCY errors
                waiting_for_segment_completion = True
                curr_loop_time = time.monotonic()
                # C# `while (checked (tickCount2 - tickCount1) < checked ((long) ((int) index6 - 7) * (long) TimeIntervalMsec))`
                # This is a timing loop to ensure points are sent at correct intervals.
                # `(point_idx - 7) * time_interval_msec` is the current expected elapsed time for the motion
                
                expected_elapsed_time_sec = float(point_idx - 7) * float(time_interval_msec) / 1000.0
                while (time.monotonic() - path_motion_start_time) < expected_elapsed_time_sec:
                    if self.pvt_stop:
                        # Handle PVT stop logic (calculate deceleration, stop motors)
                        # This part is highly complex in C# involving Math.Pow etc.
                        # It iterates to find the "closest" PVA point to stop at smoothly.
                        # For now, simplify action on PVTStop.
                        print("PVT Stop requested, stopping motors.")
                        # Need to call MotorsStop or similar.
                        await self.motors_stop(True) # Wait until done
                        # Then needs to break out of all loops and return.
                        # For now, `return X` on error, continue normal path if no error.
                        break # Break from timing loop, then will break from point_idx loop
                    else:
                        # Check PVT EMCY status (QueueFull/QueueLow)
                        for node_id_check in self.grp_node_id:
                            emcy = self.pvt_emcy[node_id_check]
                            if emcy.queue_full.state or (emcy.queue_low.state and not skip_ax[self.get_node_id_index(node_id_check)]):
                                self.error_code[15] = f"PVT fault. Axis {node_id_check} queue {'full' if emcy.queue_full.state else 'empty'}."
                                return 15 # PVT fault

                    await asyncio.sleep(0.001) # Yield

                if self.pvt_stop: # If PVTStop broke the inner loop
                    break # Break from outer point_idx loop

        # Wait for all remaining moves to complete after path commands are sent.
        if self.grp_node_id:
            for grp_node_id in self.grp_node_id:
                node_idx = self.get_node_id_index(grp_node_id)
                if node_idx != -1 and not skip_ax[node_idx]:
                    remaining_timeout = max(0, timeout_msec - int((time.monotonic() - path_motion_start_time)*1000) + 1000)
                    err_code = await self.motor_wait_for_move_done(grp_node_id, remaining_timeout)
                    if err_code != 0: return err_code
        
        # End PVT motion (`pvt_begin_motion(False, ...)` equivalent)
        err_code = await self.pvt_begin_motion(False, skip_ax)
        if err_code != 0: return err_code

        event_data.event_type = eEventType.MotorsMovePathDone
        event_data.me_index = index
        event_data.mmd_all_moves_done = (self.move_buffer_first_index == self.move_buffer_last_index)
        await self._raise_an_event(event_data)

        return 0


    async def pvt_send_data_point(self, node_id: int, position: int, velocity: int) -> int:
        """
        Sends a single PVA data point to a motor via RPDO3 for PVT.
        Equivalent to C# PVTSendDataPoint.
        """
        # PVT data is typically sent in RPDO3, mapping to Target Position IP and Target Velocity IP.
        # This means the CAN message data bytes should contain position and velocity.
        # C# uses `BitConverter.GetBytes` then `GetByte` from long.
        # Position is INT32 (4 bytes), Velocity is INT32 (4 bytes).
        
        pos_bytes = list(position.to_bytes(4, 'little', signed=True))
        vel_bytes = list(velocity.to_bytes(4, 'little', signed=True))
        all_data_bytes = pos_bytes + vel_bytes
        
        timestamp = int(time.monotonic() * 1000) # milliseconds
        err_code = await self.can_write(eCOBType.RPDO3, node_id,
                                        byte0=all_data_bytes[0], byte1=all_data_bytes[1],
                                        byte2=all_data_bytes[2], byte3=all_data_bytes[3],
                                        byte4=all_data_bytes[4], byte5=all_data_bytes[5],
                                        byte6=all_data_bytes[6], byte7=all_data_bytes[7],
                                        data_length=8, time_stamp=timestamp)
        return err_code

    async def pvt_set_time_interval(self, time_interval_msec: int) -> int:
        """
        Sets the PVT time interval for all motors in the group.
        Equivalent to C# PVTSetTimeInterval.
        """
        if self.grp_node_id:
            for node_id in self.grp_node_id:
                # Elmo Object 0x60EC, subindex 1.
                
                class ElmoObject_Custom(IntEnum):
                    PVTTimeInterval = 0x60EC # Custom value for this specific field

                err_code = await self.can_sdo_send_elmo_object(node_id, ElmoObject_Custom.PVTTimeInterval, 1, str(time_interval_msec), eElmoObjectDataType.UNSIGNED8)
                if err_code != 0: return err_code
        return 0

    async def pvt_begin_motion(self, begin: bool, skip_ax: List[bool]) -> int:
        """
        Starts or stops PVT motion across multiple axes.
        Equivalent to C# PVTBeginMotion.
        """
        data_byte = [0] * 1
        sync_flag = False
        
        # Find the last active node for SYNC command if any
        last_active_node_idx = -1
        if self.grp_node_id and skip_ax:
            for i in range(len(self.grp_node_id) - 1, -1, -1): # Loop backwards
                if not skip_ax[i]:
                    last_active_node_idx = i
                    break

        if begin:
            if self.grp_node_id:
                for i, node_id in enumerate(self.grp_node_id):
                    if i == last_active_node_idx:
                        sync_flag = True
                    
                    if not skip_ax[i]:
                        # C# sends control word 0x1F (31 decimal)
                        # 0x1F = 0b00011111 = Switch On + Enable Voltage + Quick Stop + Enable Operation + New Set Point
                        err_code = await self.control_word_set(node_id, 0x1F, SYNC=sync_flag)
                        if err_code != 0: return err_code
        else: # Stop motion
            if self.grp_node_id:
                for i, node_id in enumerate(self.grp_node_id):
                    if i == len(self.grp_node_id) - 1: # Last node in group gets SYNC
                        sync_flag = True
                    # C# sends control word 0x0F (15 decimal)
                    # 0x0F = 0b00001111 = Switch On + Enable Voltage + Quick Stop + Enable Operation
                    err_code = await self.control_word_set(node_id, 0x0F, SYNC=sync_flag)
                    if err_code != 0: return err_code
        return 0

    async def motors_stop(self, wait_until_done: bool = False) -> int:
        """
        Stops all motors in the group.
        Equivalent to C# MotorsStop.
        """
        data_byte = [0] * 1
        
        # C# differentiation here: first try to read object 0x6060 sub 0
        # If it returns 7 (Interpolated Position Mode), then a PVT stop is done.
        # Otherwise, a ControlWord 0x10F is sent (Disable Interpolation).
        
        # Check current mode of operation (0x6060, subindex 0)
        if self.grp_node_id:
            # Check for PVT mode first using the first node in the group
            node_id_to_check = self.grp_node_id[0]
            mode_of_op_str = ""
            # This requires CAN_SDO_Send_ElmoObject (read mode)
            err_code = await self.can_sdo_send_elmo_object(node_id_to_check, eElmoObject(0x6060), 0, mode_of_op_str, eElmoObjectDataType.INTEGER8)
            current_mode_of_op = to_integer(mode_of_op_str)
            
            if err_code == 0 and current_mode_of_op == 7: # If in Interpolated Position Mode (PVT)
                self.pvt_stop = True # Signal PVT threads to stop
            else:
                sync_flag = False
                for i, node_id in enumerate(self.grp_node_id):
                    if i == len(self.grp_node_id) - 1: # Last node in group gets SYNC
                        sync_flag = True
                    
                    # C# sends CW 0x10F (0b000100001111) which means `0xF | 0x100 (`IPHalt` in eCW)
                    # This disables interpolation/halts motion.
                    err_code = await self.control_word_set(node_id, 0x10F, SYNC=sync_flag)
                    if err_code != 0: return err_code

        if wait_until_done:
            err_code = await self.motors_wait_for_move_done(2500) # C# timeout 2500ms
            if err_code != 0: return err_code
        
        return 0
