<commit_before># -*- coding: utf-8 -*-

# Copyright (c) 2014-2015 <NAME>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

"""Schunk Motion Protocol for Python.

See http://schunk.rtfd.org/

Example
-------

::

    import schunk
    import serial

    mod = schunk.Module(schunk.SerialConnection(
        0x0B, serial.Serial, port=0, baudrate=9600, timeout=1))

    mod.move_pos(42)

"""

__version__ = "0.2.1"

import struct
import contextlib
import functools


class Module:

    """A Schunk module.

    For further documentation see the __init__() docstring (which is
    also used in the Sphinx documentation (http://schunk.rtfd.org/).

    """

    def __init__(self, connection):
        """Create an object for controlling a Schunk module.

        Parameters
        ----------
        connection
            Something that has an ``open()`` method which returns a
            coroutine.  This coroutine must accept a bytes object and
            send it to a Schunk module, read the response (taking D-Len
            into account) and yield the response (and further messages)
            as a bytearray.

            :class:`SerialConnection` happens to do exactly that.

        """
        self._connection = connection
        self._config = _Config(self)

    def reference(self):
        """2.1.1 CMD REFERENCE (0x92).

        A reference movement is completed.

        """
        self._send(0x92, expected=b'OK')

    def move_pos(self, position, velocity=None, acceleration=None,
                 current=None, jerk=None):
        """2.1.3 MOVE POS (0xB0).

        Parameters
        ----------
        position : float
            Absolute position.
        velocity, acceleration, current, jerk : float, optional
            If one of them is not specified, all following arguments
            must not be specified either.

        Returns
        -------
        float
            Estimated time to reach `position`.
            If the time cannot be estimated, 0.0 is returned.

        See Also
        --------
        move_pos_blocking, move_pos_rel
        set_target_vel, set_target_acc, set_target_cur, set_target_jerk

        """
        return self._move_pos_helper(0xB0, position, velocity,
                                     acceleration, current, jerk)

    def move_pos_blocking(self, position, velocity=None, acceleration=None,
                          current=None, jerk=None):
        """Move to position and wait until position is reached.

        .. note:: *Impulse messages* must be activated for this to work,
                  see :meth:`toggle_impulse_message` and
                  :attr:`communication_mode`.

                  This applies to all ``*_blocking()`` methods.

        Returns
        -------
        float
            The final position.

        See Also
        --------
        move_pos

        """
        return self._move_pos_helper(0xB0, position, velocity,
                                     acceleration, current, jerk,
                                     blocking=True)

    def move_pos_rel(self, position, velocity=None, acceleration=None,
                     current=None, jerk=None):
        """2.1.4 MOVE POS REL (0xB8).

        Parameters
        ----------
        position : float
            Relative position.
        velocity, acceleration, current, jerk : float, optional
            If one of them is not specified, the following must not be
            specified either.

        Returns
        -------
        float
            Estimated time to reach `position`.
            If the time cannot be estimated, 0.0 is returned.

        See Also
        --------
        move_pos_rel_blocking, move_pos
        set_target_vel, set_target_acc, set_target_cur, set_target_jerk

        """
        return self._move_pos_helper(0xB8, position, velocity,
                                     acceleration, current, jerk)

    def move_pos_rel_blocking(self, position, velocity=None, acceleration=None,
                              current=None, jerk=None):
        """Move to relative position and wait until position is reached.

        Returns
        -------
        float
            The actual relative motion.

        See Also
        --------
        move_pos_rel

        """
        return self._move_pos_helper(0xB8, position, velocity,
                                     acceleration, current, jerk,
                                     blocking=True)

    def move_pos_time(self, position, velocity=None, acceleration=None,
                      current=None, time=None):
        """2.1.5 MOVE POS TIME (0xB1).

        See Also
        --------
        move_pos_time_blocking, move_pos, set_target_time

        """
        return self._move_pos_helper(0xB1, position, velocity,
                                     acceleration, current, time)

    def move_pos_time_blocking(self, position, velocity=None,
                               acceleration=None, current=None, time=None):
        """Move to position and wait until position is reached.

        Returns
        -------
        float
            The final position.

        See Also
        --------
        move_pos_time

        """
        return self._move_pos_helper(0xB1, position, velocity,
                                     acceleration, current, time,
                                     blocking=True)

    def move_pos_time_rel(self, position, velocity=None, acceleration=None,
                          current=None, time=None):
        """2.1.6 MOVE POS TIME REL (0xB9).

        See Also
        --------
        move_pos_time_rel_blocking, move_pos_rel, set_target_time

        """
        return self._move_pos_helper(0xB9, position, velocity,
                                     acceleration, current, time)

    def move_pos_time_rel_blocking(self, position, velocity=None,
                                   acceleration=None, current=None, time=None):
        """Move to position and wait until position is reached.

        Returns
        -------
        float
            The actual relative motion.

        See Also
        --------
        move_pos_time_rel

        """
        return self._move_pos_helper(0xB9, position, velocity,
                                     acceleration, current, time,
                                     blocking=True)

    def set_target_vel(self, velocity):
        """2.1.14 SET TARGET VEL (0xA0).

        Initially, the target velocity is set to 10% of the maximum.

        """
        self._send(0xA0, struct.pack('<f', velocity), expected=b'OK')

    def set_target_acc(self, acceleration):
        """2.1.15 SET TARGET ACC (0xA1).

        Initially, the target acceleration is set to 10% of the maximum.

        """
        self._send(0xA1, struct.pack('<f', acceleration), expected=b'OK')

    def set_target_jerk(self, jerk):
        """2.1.16 SET TARGET JERK (0xA2).

        Initially, the target jerk is set to 50% of the maximum.

        """
        self._send(0xA2, struct.pack('<f', jerk), expected=b'OK')

    def set_target_cur(self, current):
        """2.1.17 SET TARGET CUR (0xA3).

        Initially, the target current is set to the nominal current.

        """
        self._send(0xA3, struct.pack('<f', current), expected=b'OK')

    def set_target_time(self, time):
        """2.1.18 SET TARGET TIME (0xA4)."""
        self._send(0xA4, struct.pack('<f', time), expected=b'OK')

    def stop(self):
        """2.1.19 CMD STOP (0x91)."""
        self._send(0x91, expected=b'OK')

    # Not implemented (see warnings in Schunk manual):
    # 2.1.20 CMD EMERGENCY STOP (0x90)

    def toggle_impulse_message(self):
        """2.2.6 CMD TOGGLE IMPULSE MESSAGE (0xE7).

        .. note:: *Impulse messages* must be switched on for
                  ``*_blocking()``, e.g. :meth:`move_pos_blocking`.

        Returns
        -------
        bool
            ``True`` if impulse messages were switched on, ``False`` if
            they were switched off.

        """
        response = self._send(0xE7)
        if response == b'ON':
            return True
        elif response == b'OFF':
            return False
        else:
            raise SchunkError("Unexpected response: {}".format(response))

    @property
    def config(self):
        """2.3.1 SET CONFIG (0x81) / 2.3.2 GET CONFIG (0x80).

        The `config` object has several attributes which can be queried
        and changed.
        Except where otherwise noted, the new settings are immediately
        stored in the EEPROM but are only applied after the module has
        been restarted.

        Some options are read-only, some can only be set as "Profi"
        user. See :meth:`change_user`.

        Attributes
        ----------

        module_type : bytes
        firmware_version : int
        protocol_version : int
        hardware_version : int
        firmware_date : bytes

        eeprom : bytes
            All configuration data is read/written in one process.
            Depending on the type of user certain data might not be
            written. After successful writing of the data, the module is
            rebooted.

            .. note:: This command should not be used with one's own
                      applications, as the structure of the data to be
                      received/sent is not known.

        module_id : int (1..255)
        group_id : int (1..255)
        serial_baudrate : int (1200, 2400, 4800, 9600, 19200, 38400)
        can_baudrate : int (50, 100, 125, 250, 500, 800, 1000)

        communication_mode : int
            See :const:`communication_modes`.

        unit_system : int
            See :const:`unit_systems`.

        soft_high : float
            The transferred value is not written to the EEPROM. The
            settings are applied immediately.

        soft_low : float
            The transferred value is not written to the EEPROM. The
            settings are applied immediately.

        gear_ratio : float
            The Gear Ratio 1 is changed (the command has no use with an
            integer unit system). The transferred value is written to
            the EEPROM and applied immediately.

        max_velocity : float
        max_acceleration : float
        max_current : float
        nom_current : float
        max_jerk : float
        offset_phase_a : int
        offset_phase_b : int

        data_crc : int
            A CRC16 over all variable and not module specified
            paramenters (like serial number, current offset).

        reference_offset : float
        serial_number : int
        order_number : int

        """
        return self._config

    def get_state(self):
        """2.5.1 GET STATE (0x95).

        Return the module status and other information.

        The time parameter (to get state repeatedly) is disabled
        (because impulse messages are not supported).
        The mode parameter is always set to request everything
        (position, velocity and current).

        Returns
        -------
        position, velocity, current : float
            Dito.
        status : dict
            See :func:`decode_status`.
        error_code : int
            See :const:`error_codes` for a mapping to strings.

        """
        data = struct.pack('<fB', 0.0, 0x01 | 0x02 | 0x04)
        pos, vel, cur, status, error = self._send(0x95, data, '<3fBB')
        return pos, vel, cur, decode_status(status), error

    def reboot(self):
        """2.5.2 CMD REBOOT (0xE0)."""
        self._send(0xE0, expected=b'OK')

    def change_user(self, password=None):
        """2.5.6 CHANGE USER (0xE3).

        If no password is specified - or if the password is wrong - the
        user is changed to "User".
        The default password for "Profi" is "<PASSWORD>", but don't tell
        anyone!

        After a reboot, the default user is "User".

        """
        if password is None:
            data = b''
        elif isinstance(password, str):
            data = password.encode()
        else:
            data = password
        ok, user = self._send(0xE3, data, '2sB')
        if ok != b'OK':
            raise SchunkError("Error changing user")
        return {0x00: "User",
                0x01: "Diag",
                0x02: "Profi",
                0x03: "Advanced"}[user]

    def check_mc_pc_communication(self):
        """2.5.7 CHECK MC PC COMMUNICATION (0xE4).

        Returns
        -------
        bool
            ``True`` on success.

        """
        response = self._send(0xE4, fmt=_test_format_string)
        if response != _test_values:
            raise SchunkError("Wrong response: {}".format(response))
        return True

    def check_pc_mc_communication(self):
        """2.5.8 CHECK PC MC COMMUNICATION (0xE5).

        Returns
        -------
        bool
            ``True`` on success.

        """
        data = struct.pack(_test_format_string, *_test_values)
        self._send(0xE5, data, expected=b'OK\x00')
        return True

    def ack(self):
        """2.8.1.4 CMD ACK (0x8B).

        Acknowledgement of a pending error message.

        """
        self._send(0x8B, expected=b'OK')

    def get_detailed_error_info(self):
        """2.8.1.5 GET DETAILED ERROR INFO (0x96).

        Returns
        -------
        command : {"ERROR", "WARNING", "INFO"}
        error_code : int
            See :const:`error_codes` for a mapping to strings.
        data : float
            The value can be interpreted by the Schunk Service.

        Raises
        ------
        SchunkError
            If no error is active, or no detailed information is
            available, the command is raising an exception saying:
            ``INFO FAILED (0x05)``.

        """
        command, error_code, data = self._send(0x96, fmt='<BBf')
        command = {0x88: "ERROR", 0x89: "WARNING", 0x8A: "INFO"}[command]
        return command, error_code, data

    def wait_until_position_reached(self):
        """Repeatedly check the state until the position is reached.

        This should only be used if impulse messages are disabled (see
        :meth:`toggle_impulse_message`).

        """
        gen = self._connection.open()
        try:
            while True:
                # 2.5.1 GET STATE (0x95)
                response = gen.send(_data_frame(0x95, b'\x00\x00\x00\x00\x01'))
                if response[1] == 0x94:
                    # 2.2.3 CMD POS REACHED (0x94) is ignored
                    response = gen.send(None)
                position, status, error = _check_response(
                    response, 0x95, '<fBB')
                if status & 0x80:  # position reached
                    return position
        except (KeyboardInterrupt, SystemExit):
            gen.close()
            gen = self._connection.open()
            # 2.1.19 CMD STOP (0x91)
            gen.send(b'\x01\x91')
            # response message is ignored
            raise
        finally:
            gen.close()

    def _send(self, command, data=b'', fmt=None, expected=None):
        """Send message, receive response.

        If the expected number of bytes doesn't match, an error is
        raised.
        If expected is a string, it is used as format strings to decode
        the received bytes.
        If expected is a bytes object, it is compared to the received
        data. If they are equal, the function returns, if not, an error
        is raised.

        """
        with contextlib.closing(self._connection.open()) as gen:
            response = gen.send(_data_frame(command, data))
            return _check_response(response, command, fmt, expected)

    def _move_pos_helper(self, command, *args, **kwargs):
        """Move to the given position.

        If blocking=False, the movement is started and the estimated
        time is immediately returned.
        If the time cannot be estimated, 0.0 is returned.
        If blocking=True, the final position (or the actual relative
        movement) is returned when the movement is finished.

        Use command=0xB0 for absolute and command=0xB8 for relative
        positions.  For the "time" variants, use command=0xB1 and
        command=0xB9, respectively.

        Trailing None arguments are removed, None arguments between
        other arguments are not allowed.
        At least one argument (position) has to be specified.

        """
        n = len(args)
        while n > 1 and args[n - 1] is None:
            n -= 1

        data = struct.pack('<{}f'.format(n), *args[:n])

        # Work-around since Python 2 doesn't support keyword-only args:
        blocking = kwargs.pop('blocking', False)
        assert not kwargs

        gen = self._connection.open()
        try:
            response = gen.send(_data_frame(command, data))
            response = _check_response(response, command)
            if response == b'OK':
                est_time = 0.0<commit_msg>Ignore CMD POS REACHED impulse messages<commit_after># -*- coding: utf-8 -*-

# Copyright (c) 2014-2015 <NAME>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

"""Schunk Motion Protocol for Python.

See http://schunk.rtfd.org/

Example
-------

::

    import schunk
    import serial

    mod = schunk.Module(schunk.SerialConnection(
        0x0B, serial.Serial, port=0, baudrate=9600, timeout=1))

    mod.move_pos(42)

"""

__version__ = "0.2.1"

import struct
import contextlib
import functools


class Module:

    """A Schunk module.

    For further documentation see the __init__() docstring (which is
    also used in the Sphinx documentation (http://schunk.rtfd.org/).

    """

    def __init__(self, connection):
        """Create an object for controlling a Schunk module.

        Parameters
        ----------
        connection
            Something that has an ``open()`` method which returns a
            coroutine.  This coroutine must accept a bytes object and
            send it to a Schunk module, read the response (taking D-Len
            into account) and yield the response (and further messages)
            as a bytearray.

            :class:`SerialConnection` happens to do exactly that.

        """
        self._connection = connection
        self._config = _Config(self)

    def reference(self):
        """2.1.1 CMD REFERENCE (0x92).

        A reference movement is completed.

        """
        self._send(0x92, expected=b'OK')

    def move_pos(self, position, velocity=None, acceleration=None,
                 current=None, jerk=None):
        """2.1.3 MOVE POS (0xB0).

        Parameters
        ----------
        position : float
            Absolute position.
        velocity, acceleration, current, jerk : float, optional
            If one of them is not specified, all following arguments
            must not be specified either.

        Returns
        -------
        float
            Estimated time to reach `position`.
            If the time cannot be estimated, 0.0 is returned.

        See Also
        --------
        move_pos_blocking, move_pos_rel
        set_target_vel, set_target_acc, set_target_cur, set_target_jerk

        """
        return self._move_pos_helper(0xB0, position, velocity,
                                     acceleration, current, jerk)

    def move_pos_blocking(self, position, velocity=None, acceleration=None,
                          current=None, jerk=None):
        """Move to position and wait until position is reached.

        .. note:: *Impulse messages* must be activated for this to work,
                  see :meth:`toggle_impulse_message` and
                  :attr:`communication_mode`.

                  This applies to all ``*_blocking()`` methods.

        Returns
        -------
        float
            The final position.

        See Also
        --------
        move_pos

        """
        return self._move_pos_helper(0xB0, position, velocity,
                                     acceleration, current, jerk,
                                     blocking=True)

    def move_pos_rel(self, position, velocity=None, acceleration=None,
                     current=None, jerk=None):
        """2.1.4 MOVE POS REL (0xB8).

        Parameters
        ----------
        position : float
            Relative position.
        velocity, acceleration, current, jerk : float, optional
            If one of them is not specified, the following must not be
            specified either.

        Returns
        -------
        float
            Estimated time to reach `position`.
            If the time cannot be estimated, 0.0 is returned.

        See Also
        --------
        move_pos_rel_blocking, move_pos
        set_target_vel, set_target_acc, set_target_cur, set_target_jerk

        """
        return self._move_pos_helper(0xB8, position, velocity,
                                     acceleration, current, jerk)

    def move_pos_rel_blocking(self, position, velocity=None, acceleration=None,
                              current=None, jerk=None):
        """Move to relative position and wait until position is reached.

        Returns
        -------
        float
            The actual relative motion.

        See Also
        --------
        move_pos_rel

        """
        return self._move_pos_helper(0xB8, position, velocity,
                                     acceleration, current, jerk,
                                     blocking=True)

    def move_pos_time(self, position, velocity=None, acceleration=None,
                      current=None, time=None):
        """2.1.5 MOVE POS TIME (0xB1).

        See Also
        --------
        move_pos_time_blocking, move_pos, set_target_time

        """
        return self._move_pos_helper(0xB1, position, velocity,
                                     acceleration, current, time)

    def move_pos_time_blocking(self, position, velocity=None,
                               acceleration=None, current=None, time=None):
        """Move to position and wait until position is reached.

        Returns
        -------
        float
            The final position.

        See Also
        --------
        move_pos_time

        """
        return self._move_pos_helper(0xB1, position, velocity,
                                     acceleration, current, time,
                                     blocking=True)

    def move_pos_time_rel(self, position, velocity=None, acceleration=None,
                          current=None, time=None):
        """2.1.6 MOVE POS TIME REL (0xB9).

        See Also
        --------
        move_pos_time_rel_blocking, move_pos_rel, set_target_time

        """
        return self._move_pos_helper(0xB9, position, velocity,
                                     acceleration, current, time)

    def move_pos_time_rel_blocking(self, position, velocity=None,
                                   acceleration=None, current=None, time=None):
        """Move to position and wait until position is reached.

        Returns
        -------
        float
            The actual relative motion.

        See Also
        --------
        move_pos_time_rel

        """
        return self._move_pos_helper(0xB9, position, velocity,
                                     acceleration, current, time,
                                     blocking=True)

    def set_target_vel(self, velocity):
        """2.1.14 SET TARGET VEL (0xA0).

        Initially, the target velocity is set to 10% of the maximum.

        """
        self._send(0xA0, struct.pack('<f', velocity), expected=b'OK')

    def set_target_acc(self, acceleration):
        """2.1.15 SET TARGET ACC (0xA1).

        Initially, the target acceleration is set to 10% of the maximum.

        """
        self._send(0xA1, struct.pack('<f', acceleration), expected=b'OK')

    def set_target_jerk(self, jerk):
        """2.1.16 SET TARGET JERK (0xA2).

        Initially, the target jerk is set to 50% of the maximum.

        """
        self._send(0xA2, struct.pack('<f', jerk), expected=b'OK')

    def set_target_cur(self, current):
        """2.1.17 SET TARGET CUR (0xA3).

        Initially, the target current is set to the nominal current.

        """
        self._send(0xA3, struct.pack('<f', current), expected=b'OK')

    def set_target_time(self, time):
        """2.1.18 SET TARGET TIME (0xA4)."""
        self._send(0xA4, struct.pack('<f', time), expected=b'OK')

    def stop(self):
        """2.1.19 CMD STOP (0x91)."""
        self._send(0x91, expected=b'OK')

    # Not implemented (see warnings in Schunk manual):
    # 2.1.20 CMD EMERGENCY STOP (0x90)

    def toggle_impulse_message(self):
        """2.2.6 CMD TOGGLE IMPULSE MESSAGE (0xE7).

        .. note:: *Impulse messages* must be switched on for
                  ``*_blocking()``, e.g. :meth:`move_pos_blocking`.

        Returns
        -------
        bool
            ``True`` if impulse messages were switched on, ``False`` if
            they were switched off.

        """
        response = self._send(0xE7)
        if response == b'ON':
            return True
        elif response == b'OFF':
            return False
        else:
            raise SchunkError("Unexpected response: {}".format(response))

    @property
    def config(self):
        """2.3.1 SET CONFIG (0x81) / 2.3.2 GET CONFIG (0x80).

        The `config` object has several attributes which can be queried
        and changed.
        Except where otherwise noted, the new settings are immediately
        stored in the EEPROM but are only applied after the module has
        been restarted.

        Some options are read-only, some can only be set as "Profi"
        user. See :meth:`change_user`.

        Attributes
        ----------

        module_type : bytes
        firmware_version : int
        protocol_version : int
        hardware_version : int
        firmware_date : bytes

        eeprom : bytes
            All configuration data is read/written in one process.
            Depending on the type of user certain data might not be
            written. After successful writing of the data, the module is
            rebooted.

            .. note:: This command should not be used with one's own
                      applications, as the structure of the data to be
                      received/sent is not known.

        module_id : int (1..255)
        group_id : int (1..255)
        serial_baudrate : int (1200, 2400, 4800, 9600, 19200, 38400)
        can_baudrate : int (50, 100, 125, 250, 500, 800, 1000)

        communication_mode : int
            See :const:`communication_modes`.

        unit_system : int
            See :const:`unit_systems`.

        soft_high : float
            The transferred value is not written to the EEPROM. The
            settings are applied immediately.

        soft_low : float
            The transferred value is not written to the EEPROM. The
            settings are applied immediately.

        gear_ratio : float
            The Gear Ratio 1 is changed (the command has no use with an
            integer unit system). The transferred value is written to
            the EEPROM and applied immediately.

        max_velocity : float
        max_acceleration : float
        max_current : float
        nom_current : float
        max_jerk : float
        offset_phase_a : int
        offset_phase_b : int

        data_crc : int
            A CRC16 over all variable and not module specified
            paramenters (like serial number, current offset).

        reference_offset : float
        serial_number : int
        order_number : int

        """
        return self._config

    def get_state(self):
        """2.5.1 GET STATE (0x95).

        Return the module status and other information.

        The time parameter (to get state repeatedly) is disabled
        (because impulse messages are not supported).
        The mode parameter is always set to request everything
        (position, velocity and current).

        Returns
        -------
        position, velocity, current : float
            Dito.
        status : dict
            See :func:`decode_status`.
        error_code : int
            See :const:`error_codes` for a mapping to strings.

        """
        data = struct.pack('<fB', 0.0, 0x01 | 0x02 | 0x04)
        pos, vel, cur, status, error = self._send(0x95, data, '<3fBB')
        return pos, vel, cur, decode_status(status), error

    def reboot(self):
        """2.5.2 CMD REBOOT (0xE0)."""
        self._send(0xE0, expected=b'OK')

    def change_user(self, password=None):
        """2.5.6 CHANGE USER (0xE3).

        If no password is specified - or if the password is wrong - the
        user is changed to "User".
        The default password for "Profi" is "<PASSWORD>", but don't tell
        anyone!

        After a reboot, the default user is "User".

        """
        if password is None:
            data = b''
        elif isinstance(password, str):
            data = password.encode()
        else:
            data = password
        ok, user = self._send(0xE3, data, '2sB')
        if ok != b'OK':
            raise SchunkError("Error changing user")
        return {0x00: "User",
                0x01: "Diag",
                0x02: "Profi",
                0x03: "Advanced"}[user]

    def check_mc_pc_communication(self):
        """2.5.7 CHECK MC PC COMMUNICATION (0xE4).

        Returns
        -------
        bool
            ``True`` on success.

        """
        response = self._send(0xE4, fmt=_test_format_string)
        if response != _test_values:
            raise SchunkError("Wrong response: {}".format(response))
        return True

    def check_pc_mc_communication(self):
        """2.5.8 CHECK PC MC COMMUNICATION (0xE5).

        Returns
        -------
        bool
            ``True`` on success.

        """
        data = struct.pack(_test_format_string, *_test_values)
        self._send(0xE5, data, expected=b'OK\x00')
        return True

    def ack(self):
        """2.8.1.4 CMD ACK (0x8B).

        Acknowledgement of a pending error message.

        """
        self._send(0x8B, expected=b'OK')

    def get_detailed_error_info(self):
        """2.8.1.5 GET DETAILED ERROR INFO (0x96).

        Returns
        -------
        command : {"ERROR", "WARNING", "INFO"}
        error_code : int
            See :const:`error_codes` for a mapping to strings.
        data : float
            The value can be interpreted by the Schunk Service.

        Raises
        ------
        SchunkError
            If no error is active, or no detailed information is
            available, the command is raising an exception saying:
            ``INFO FAILED (0x05)``.

        """
        command, error_code, data = self._send(0x96, fmt='<BBf')
        command = {0x88: "ERROR", 0x89: "WARNING", 0x8A: "INFO"}[command]
        return command, error_code, data

    def wait_until_position_reached(self):
        """Repeatedly check the state until the position is reached.

        This should only be used if impulse messages are disabled (see
        :meth:`toggle_impulse_message`).

        """
        gen = self._connection.open()
        try:
            while True:
                # 2.5.1 GET STATE (0x95)
                response = gen.send(_data_frame(0x95, b'\x00\x00\x00\x00\x01'))
                if response[1] == 0x94:
                    # 2.2.3 CMD POS REACHED (0x94) is ignored
                    response = gen.send(None)
                position, status, error = _check_response(
                    response, 0x95, '<fBB')
                if status & 0x80:  # position reached
                    return position
        except (KeyboardInterrupt, SystemExit):
            gen.close()
            gen = self._connection.open()
            # 2.1.19 CMD STOP (0x91)
            gen.send(b'\x01\x91')
            # response message is ignored
            raise
        finally:
            gen.close()

    def _send(self, command, data=b'', fmt=None, expected=None):
        """Send message, receive response.

        If the expected number of bytes doesn't match, an error is
        raised.
        If expected is a string, it is used as format strings to decode
        the received bytes.
        If expected is a bytes object, it is compared to the received
        data. If they are equal, the function returns, if not, an error
        is raised.

        """
        with contextlib.closing(self._connection.open()) as gen:
            response = gen.send(_data_frame(command, data))
            if response[1] == 0x94:
                # 2.2.3 CMD POS REACHED (0x94) is ignored
                response = gen.send(None)
            return _check_response(response, command, fmt, expected)

    def _move_pos_helper(self, command, *args, **kwargs):
        """Move to the given position.

        If blocking=False, the movement is started and the estimated
        time is immediately returned.
        If the time cannot be estimated, 0.0 is returned.
        If blocking=True, the final position (or the actual relative
        movement) is returned when the movement is finished.

        Use command=0xB0 for absolute and command=0xB8 for relative
        positions.  For the "time" variants, use command=0xB1 and
        command=0xB9, respectively.

        Trailing None arguments are removed, None arguments between
        other arguments are not allowed.
        At least one argument (position) has to be specified.

        """
        n = len(args)
        while n > 1 and args[n - 1] is None:
            n -= 1

        data = struct.pack('<{}f'.format(n), *args[:n])

        # Work-around since Python 2 doesn't support keyword-only args:
        blocking = kwargs.pop('blocking', False)
        assert not kwargs

        gen = self._connection.open()
        try:
            response = gen.send(_data_frame(command, data))
            if response[1] == 0x94:
                # 2.2.3 CMD POS REACHED (0x94) is ignored
                response = gen.send(None)
            response = _check_response(response, command)
            if response == b'OK':
                est_time = 0.0
{'fraction_non_alphanumeric': 0.09285399205999118, 'fraction_numerical': 0.020732245258050286, 'mean_word_length': 1.7778373410935824, 'pattern_counts': {'":': 0, '<': 27, '<?xml version=': 0, '>': 9, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 4, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 26, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1134123', 'n_tokens_mistral': 10205, 'n_tokens_neox': 9437, 'n_words': 3328}