# SPDX-License-Identifier: MIT
# Copyright (c) 2018-2023 The Pybricks Authors

"""Generic cross-platform module for typical devices like lights, displays,
speakers, and batteries."""

from __future__ import annotations

from typing import (
    Union,
    Iterable,
    overload,
    Optional,
    Tuple,
    Collection,
    Set,
    TYPE_CHECKING,
)

from .tools import Matrix
from .parameters import Axis, Direction, Stop, Button, Port, Color, Side

if TYPE_CHECKING:
    from typing import Any, Awaitable, TypeVar

    from .parameters import Number

    _T_co = TypeVar("_T_co", covariant=True)

    class MaybeAwaitable(None, Awaitable[None]): ...

    # HACK: Cannot subclass bool, so using Any instead.
    class MaybeAwaitableBool(Any, Awaitable[bool]): ...

    class MaybeAwaitableFloat(float, Awaitable[float]): ...

    class MaybeAwaitableInt(int, Awaitable[int]): ...

    class MaybeAwaitableTuple(Tuple[_T_co], Awaitable[Tuple[_T_co]]): ...

    class MaybeAwaitableColor(Color, Awaitable[Color]): ...

    class MaybeAwaitableBytes(bytes, Awaitable[bytes]): ...


class System:
    """System control actions for a hub."""

    def set_stop_button(
        self, button: Optional[Union[Button, Iterable[Button]]]
    ) -> None:
        """
        set_stop_button(button)

        Sets the button or button combination that stops a running script.

        Normally, the center button is used to stop a running script. You can
        change or disable this behavior in order to use the button for other
        purposes.

        Arguments:
            button (Button): A button such
                as :attr:`Button.CENTER <pybricks.parameters.Button.CENTER>`,
                or a tuple of multiple buttons. Choose ``None`` to disable the
                stop button altogether. If you do, you can still turn the hub
                off by holding the center button for three seconds.
        """

    def shutdown(self) -> None:
        """shutdown()

        Stops your program and shuts the hub down."""

    @overload
    def storage(self, offset: int, *, read: int) -> bytes: ...

    @overload
    def storage(self, offset: int, *, write: bytes) -> None: ...

    def storage(self, offset, read=None, write=None):
        """
        storage(offset, write=)
        storage(offset, read=) -> bytes

        Reads or writes binary data to persistent storage.

        This lets you store data that can be used the next time you run the
        program.

        The data will be saved to flash memory when you turn the hub off
        normally. It will not be saved if the batteries are removed *while* the
        hub is still running.

        Once saved, the data will remain available even after you remove the
        batteries.

        Args:
            offset (int): The offset from the start of the user storage memory, in bytes.
            read (int): The number of bytes to read. Omit this argument when writing.
            write (bytes): The bytes to write. Omit this argument when reading.

        Returns:
            The bytes read if reading, otherwise ``None``.

        Raises:
            ValueError:
                If you try to read or write data outside of the allowed range.
        """

    def reset_storage(self) -> None:
        """reset_storage()

        Resets all user settings to default values and erases user programs.
        """

    def info(self) -> dict:
        """info() -> dict

        Gets information about the hub as a dictionary with the following keys:

         - ``"name"``: The hub name. This is the name you see when connecting
           via Bluetooth.
         - ``"reset_reason"``: Why the hub (re)booted. It is ``0`` if the hub
           was previously powered off normally. It is ``1`` if the hub rebooted
           automatically, like after a firmware update. It is ``2`` if the hub
           previously crashed due to a watchdog timeout, which indicates a
           firmware issue.
         - ``"host_connected_ble"``: ``True`` if the hub is connected to a
           computer, tablet, or phone via Bluetooth, and ``False`` otherwise.
         - ``"host_connected_usb"``: ``True`` if the hub is connected to a computer
           via USB and activated in the app. ``False`` otherwise.
         - ``"program_start_type"``: It is ``1`` if the program started
           automatically when the hub was powered on. It is ``2`` if the program
           was started with the hub buttons. It is ``3`` if the program was
           started from your connected computer.
         - `"program_id"`: Program (slot) number of the currently running program.

        Returns:
            A dictionary with system info.

        .. versionchanged:: 3.6
            The name and reset reason where previously available as separate
            methods. Now they are included in the info dictionary. The methods
            are still available for backwards compatibility.
        """


class DCMotor:
    """Generic class to control simple motors without rotation sensors, such
    as train motors."""

    def __init__(self, port: Port, positive_direction: Direction = Direction.CLOCKWISE):
        """__init__(port, positive_direction=Direction.CLOCKWISE)

        Arguments:
            port (Port): Port to which the motor is connected.
            positive_direction (Direction): Which direction the motor should
                turn when you give a positive duty cycle value.
        """

    def dc(self, duty: Number) -> None:
        """dc(duty)

        Rotates the motor at a given duty cycle (also known as "power").

        Arguments:
            duty (Number, %): The duty cycle (-100.0 to 100).
        """

    def stop(self) -> None:
        """stop()

        Stops the motor and lets it spin freely.

        The motor gradually stops due to friction."""

    def brake(self) -> None:
        """brake()

        Passively brakes the motor.

        The motor stops due to friction, plus the voltage that
        is generated while the motor is still moving."""

    @overload
    def settings(self, max_voltage: Number) -> None: ...

    @overload
    def settings(self) -> Tuple[int]: ...

    def settings(self, *args):
        """
        settings(max_voltage)
        settings() -> Tuple[int]

        Configures motor settings. If no arguments are given,
        this returns the current values.

        Arguments:
            max_voltage (Number, mV):
                Maximum voltage applied to the motor during all motor commands.
        """


class Control:
    """Class to interact with PID controller and settings."""

    scale: int

    """
    Scaling factor between the controlled integer variable
    and the physical output. For example, for a single
    motor this is the number of encoder pulses per degree of rotation.
    """

    @overload
    def limits(
        self,
        speed: Optional[Number] = None,
        acceleration: Optional[Number] = None,
        torque: Optional[Number] = None,
    ) -> None: ...

    @overload
    def limits(self) -> Tuple[int, int, int]: ...

    def limits(self, *args):
        """
        limits(speed, acceleration, torque)
        limits() -> Tuple[int, int, int]

        Configures the maximum speed, acceleration, and torque.

        If no arguments are given, this will return the current values.

        The new ``acceleration`` and ``speed`` limit will become effective
        when you give a new motor command. Ongoing maneuvers are not affected.

        Arguments:
            speed (Number, deg/s or Number, mm/s):
                Maximum speed. All speed commands will be capped to this value.
            acceleration (Number, deg/s² or Number, mm/s²):
                Slope of the speed curve when accelerating or decelerating.
                Use a tuple to set acceleration and deceleration separately.
                If one value is given, it is used for both.
            torque (:ref:`torque`):
                Maximum feedback torque during control.
        """

    @overload
    def pid(
        self,
        kp: Optional[Number] = None,
        ki: Optional[Number] = None,
        kd: Optional[Number] = None,
        integral_deadzone: Optional[Number] = None,
        integral_rate: Optional[Number] = None,
    ) -> None: ...

    @overload
    def pid(self) -> Tuple[int, int, int, int, int]: ...

    def pid(self, *args):
        """pid(kp, ki, kd, integral_deadzone, integral_rate)
        pid() -> Tuple[int, int, int, int, int]

        Gets or sets the PID values for position and speed control.

        If no arguments are given, this will return the current values.

        Arguments:
            kp (int): Proportional position control
                constant. It is the feedback torque per degree of
                error: µNm/deg.
            ki (int): Integral position control constant. It is the feedback
                torque per accumulated degree of error: µNm/(deg s).
            kd (int): Derivative position (or proportional speed) control
                constant. It is the feedback torque per
                unit of speed: µNm/(deg/s).
            integral_deadzone (Number, deg or Number, mm): Zone around the
                target where the error integral does not accumulate errors.
            integral_rate (Number, deg/s or Number, mm/s): Maximum rate at
                which the error integral is allowed to grow.
        """

    @overload
    def target_tolerances(
        self, speed: Optional[Number] = None, position: Optional[Number] = None
    ) -> None: ...

    @overload
    def target_tolerances(self) -> Tuple[int, int]: ...

    def target_tolerances(self, *args):
        """target_tolerances(speed, position)
        target_tolerances() -> Tuple[int, int]

        Gets or sets the tolerances that say when a maneuver is done.

        If no arguments are given, this will return the current values.

        Arguments:
            speed (Number, deg/s or Number, mm/s): Allowed deviation
                from zero speed before motion is considered complete.
            position (Number, deg or :ref:`distance`): Allowed
                deviation from the target before motion is considered
                complete.
        """

    @overload
    def stall_tolerances(
        self, speed: Optional[Number] = None, time: Optional[Number] = None
    ) -> None: ...

    @overload
    def stall_tolerances(self) -> Tuple[int, int]: ...

    def stall_tolerances(self, speed, time):
        """stall_tolerances(speed, time)
        stall_tolerances() -> Tuple[int, int]

        Gets or sets stalling tolerances.

        If no arguments are given, this will return the current values.

        Arguments:
            speed (Number, deg/s or Number, mm/s): If the controller
                cannot reach this speed for some ``time`` even with maximum
                actuation, it is stalled.
            time (Number, ms): How long the controller has to be below this
                minimum ``speed`` before we say it is stalled.
        """


class Model:
    """Class to interact with motor state observer and settings."""

    def state(self) -> Tuple[float, float, float, bool]:
        """state() -> Tuple[float, float, float, bool]

        Gets the estimated angle, speed, current, and stall state of the motor,
        using a simulation model that mimics the real motor.
        These estimates are updated faster than the real measurements,
        which can be useful when building your own PID controllers.

        For most applications it is better to used the *measured*
        :meth:`angle <pybricks.pupdevices.Motor.angle>`,
        :meth:`speed <pybricks.pupdevices.Motor.speed>`,
        :meth:`load <pybricks.pupdevices.Motor.load>`, and
        :meth:`stall <pybricks.pupdevices.Motor.stalled>` state instead.

        Returns:
            Tuple with the estimated angle (deg), speed (deg/s), current (mA),
            and stall state (``True`` or ``False``).
        """

    @overload
    def settings(self, values: tuple) -> None: ...

    @overload
    def settings(self) -> tuple: ...

    def settings(self, speed, time):
        """settings(values)
        settings() -> Tuple

        Gets or sets model settings as a tuple of integers. If no arguments are
        given, this will return the current values. This method is mainly used
        to debug the motor model class. Changing these settings should not be
        needed in user programs.

        .. _model settings: https://docs.pybricks.com/projects/pbio/en/latest/struct__pbio__observer__settings__t.html

        Arguments:
            values (Tuple): Tuple with `model settings`_.
        """


class Motor(DCMotor):
    """Generic class to control motors with built-in rotation sensors."""

    control = Control()
    """The motors use PID control to accurately track the speed and
    angle targets that you specify. You can change its behavior through the
    ``control`` attribute of the motor. See :ref:`control` for an overview
    of available methods."""

    model = Model()
    """Model representing the observer that estimates the motor state."""

    def __init__(
        self,
        port: Port,
        positive_direction: Direction = Direction.CLOCKWISE,
        gears: Optional[Union[Collection[int], Collection[Collection[int]]]] = None,
        reset_angle: bool = True,
        profile: Number = None,
    ):
        """__init__(port, positive_direction=Direction.CLOCKWISE, gears=None, reset_angle=True, profile=None)

        Arguments:
            port (Port): Port to which the motor is connected.
            positive_direction (Direction): Which direction the motor should
                turn when you give a positive speed value or
                angle.
            gears (list):
                List of gears linked to the motor. The gear connected
                to the motor comes first and the gear connected to the output
                comes last.

                For example: ``[12, 36]`` represents a gear train with a
                12-tooth gear connected to the motor and a 36-tooth gear
                connected to the output. Use a list of lists for multiple
                gear trains, such as ``[[12, 36], [20, 16, 40]]``.

                When you specify a gear train, all motor commands and settings
                are automatically adjusted to account for the resulting gear
                ratio. The motor direction remains unchanged by this.
            reset_angle (bool):
                Choose ``True`` to reset the rotation sensor value to the
                absolute marker angle (between -180 and 179).
                Choose ``False`` to keep the
                current value, so your program knows where it left off last
                time.
            profile (Number, deg): Precision profile. This is the approximate
                position tolerance in degrees that is acceptable in your
                application. A lower value gives more precise but more erratic
                movement; a higher value gives less precise but smoother
                movement. If no value is given, a suitable profile for this
                motor type will be selected automatically (about 11 degrees).
        """

    def angle(self) -> int:
        """angle() -> int: deg

        Gets the rotation angle of the motor.

        Returns:
            Motor angle.
        """

    def speed(self, window: Number = 100) -> int:
        """speed(window=100) -> int: deg/s

        Gets the speed of the motor.

        The speed is measured as the change in the motor angle during the
        given time window. A short window makes the speed value more
        responsive to motor movement, but less steady. A long window makes the
        speed value less responsive, but more steady.

        Arguments:
            window (Number, ms): The time window used to determine the speed.

        Returns:
            Motor speed.

        """

    def stalled(self) -> bool:
        """stalled() -> bool

        Checks if the motor is currently stalled.

        It is stalled when it cannot reach the target speed or position, even
        with the maximum actuation signal.

        Returns:
            ``True`` if the motor is stalled, ``False`` if not.
        """

    def load(self) -> int:
        """load() -> int: mNm

        Estimates the load that holds back the motor when it tries to move.

        Returns:
            The load torque.
        """

    def reset_angle(self, angle: Optional[Number]) -> None:
        """
        reset_angle(angle)

        Sets the accumulated rotation angle of the motor to a desired value.

        If this motor is also being used by a drive base, its distance and
        angle values will also be affected. You might want to
        use its :meth:`reset <pybricks.robotics.DriveBase.reset>`
        method instead.

        Arguments:
            angle (Number, deg): Value to which the angle should be reset.
        """

    def hold(self) -> None:
        """hold()

        Stops the motor and actively holds it at its current angle."""

    def run(self, speed: Number) -> None:
        """run(speed)

        Runs the motor at a constant speed.

        The motor accelerates to the given speed and keeps running at this
        speed until you give a new command.

        Arguments:
            speed (Number, deg/s): Speed of the motor.
        """

    def run_time(
        self, speed: Number, time: Number, then: Stop = Stop.HOLD, wait: bool = True
    ) -> MaybeAwaitable:
        """run_time(speed, time, then=Stop.HOLD, wait=True)

        Runs the motor at a constant speed for a given amount of time.

        The motor accelerates to the given speed, keeps running at this speed,
        and then decelerates. The total maneuver lasts for exactly the given
        amount of ``time``.

        Arguments:
            speed (Number, deg/s): Speed of the motor.
            time (Number, ms): Duration of the maneuver.
            then (Stop): What to do after coming to a standstill.
            wait (bool): Wait for the maneuver to complete before continuing
                with the rest of the program.
        """

    def run_angle(
        self,
        speed: Number,
        rotation_angle: Number,
        then: Stop = Stop.HOLD,
        wait: bool = True,
    ) -> MaybeAwaitable:
        """run_angle(speed, rotation_angle, then=Stop.HOLD, wait=True)

        Runs the motor at a constant speed by a given angle.

        Arguments:
            speed (Number, deg/s): Speed of the motor.
            rotation_angle (Number, deg): Angle by which the motor should
                rotate.
            then (Stop): What to do after coming to a standstill.
            wait (bool): Wait for the maneuver to complete before continuing
                with the rest of the program.
        """

    def run_target(
        self,
        speed: Number,
        target_angle: Number,
        then: Stop = Stop.HOLD,
        wait: bool = True,
    ) -> MaybeAwaitable:
        """run_target(speed, target_angle, then=Stop.HOLD, wait=True)

        Runs the motor at a constant speed towards a given target angle.

        The direction of rotation is automatically selected based on the target
        angle. It does not matter if ``speed`` is positive or negative.

        Arguments:
            speed (Number, deg/s): Speed of the motor.
            target_angle (Number, deg): Angle that the motor should rotate to.
            then (Stop): What to do after coming to a standstill.
            wait (bool): Wait for the motor to reach the target
                before continuing with the rest of the program.
        """

    def run_until_stalled(
        self,
        speed: Number,
        then: Stop = Stop.COAST,
        duty_limit: Optional[Number] = None,
    ) -> MaybeAwaitableInt:
        """
        run_until_stalled(speed, then=Stop.COAST, duty_limit=None) -> int: deg

        Runs the motor at a constant speed until it stalls.

        Arguments:
            speed (Number, deg/s): Speed of the motor.
            then (Stop): What to do after coming to a standstill.
            duty_limit (Number, %): Duty cycle limit during this
                command. This is useful to avoid applying the full motor
                torque to a geared or lever mechanism. If it is ``None``, the
                duty limit won't be changed during this command.

        Returns:
            Angle at which the motor becomes stalled.
        """

    def done(self) -> bool:
        """done() -> bool

        Checks if an ongoing command or maneuver is done.

        Returns:
            ``True`` if the command is done, ``False`` if not.
        """

    def track_target(self, target_angle: Number) -> None:
        """track_target(target_angle)

        Tracks a target angle. This is similar to :meth:`.run_target`, but
        the usual smooth acceleration is skipped: it will move to the target
        angle as fast as possible. This method is useful if you want to
        continuously change the target angle.

        Arguments:
            target_angle (Number, deg): Target angle that the motor should
                rotate to.
        """

    def close(self) -> None:
        """close()

        Closes the motor object so you can call ``Motor`` again to initialize
        a new object.

        This allows advanced users to change properties such as gearing in the
        middle of the program, which can be useful for removeable attachments.
        """


class Speaker:
    """Plays beeps and sounds using a speaker."""

    @overload
    def volume(self, volume: Number) -> None: ...

    @overload
    def volume(self) -> int: ...

    def volume(self, *args):
        """volume(volume)
        volume() -> int: %

        Gets or sets the speaker volume.

        If no volume is given, this method returns the current volume.

        Arguments:
            volume (Number, %): Volume of the speaker in the 0-100 range.
        """

    def beep(self, frequency: Number = 500, duration: Number = 100) -> MaybeAwaitable:
        """beep(frequency=500, duration=100)

        Play a beep/tone.

        Arguments:
            frequency (Number, Hz):
                Frequency of the beep in the 64-24000 Hz range.
            duration (Number, ms):
                Duration of the beep. If the duration is less
                than 0, then the method returns immediately and the frequency
                play continues to play indefinitely.
        """

    def play_notes(self, notes: Iterable[str], tempo: Number = 120) -> MaybeAwaitable:
        """play_notes(notes, tempo=120)

        Plays a sequence of musical notes. For example:
        ``["C4/4", "C4/4", "G4/4", "G4/4"]``.

        Each note is a string with the following format:

            - The first character is the name of the note, ``A`` to ``G``
              or ``R`` for a rest.
            - Note names can also include an accidental ``#`` (sharp) or
              ``b`` (flat). ``B#``/``Cb`` and ``E#``/``Fb`` are not
              allowed.
            - The note name is followed by the octave number ``2``
              to ``8``. For example ``C4`` is middle C. The octave changes
              to the next number at the note C, for example, ``B3`` is the
              note below middle C (``C4``).
            - The octave is followed by ``/`` and a number that indicates
              the size of the note. For example ``/4`` is a quarter note,
              ``/8`` is an eighth note and so on.
            - This can optionally followed by a ``.`` to make a dotted
              note. Dotted notes are 1-1/2 times as long as notes without a
              dot.
            - The note can optionally end with a ``_`` which is a tie or a
              slur. This causes there to be no pause between this note and
              the next note.

        Arguments:
            notes (iter):
                A sequence of notes to be played.
            tempo (int):
                Beats per minute. A quarter note is one beat.
        """


class ColorLight:
    """Control a multi-color light."""

    def on(self, color: Color) -> None:
        """on(color)

        Turns on the light at the specified color.

        Arguments:
            color (Color): Color of the light.
        """

    def off(self) -> None:
        """off()

        Turns off the light."""

    def blink(self, color: Color, durations: Collection[Number]) -> None:
        """blink(color, durations)

        Blinks the light at a given color by turning it on and off for given
        durations.

        The light keeps blinking indefinitely while the rest of your
        program keeps running.

        This method provides a simple way to make basic but useful patterns.
        For more generic and multi-color patterns, use ``animate()``
        instead.

        Arguments:
            color (Color): Color of the light.
            durations (list): Sequence of time values of the
                form ``[on_1, off_1, on_2, off_2, ...]``.
        """

    def animate(self, colors: Collection[Color], interval: Number) -> None:
        """animate(colors, interval)

        Animates the light with a sequence of colors, shown one by
        one for the given interval.

        The animation runs in the background while the rest of your program
        keeps running. When the animation completes, it repeats.

        Arguments:
            colors (list): Sequence of :class:`Color <.parameters.Color>`
                values.
            interval (Number, ms): Time between color updates.
        """


class ExternalColorLight:
    """Control a multi-color light."""

    def on(self, color: Color) -> MaybeAwaitable:
        """on(color)

        Turns on the light at the specified color.

        Arguments:
            color (Color): Color of the light.
        """

    def off(self) -> MaybeAwaitable:
        """off()

        Turns off the light.
        """


class LightArray3:
    """Control an array of three single-color lights."""

    def on(
        self, brightness: Union[Number, Tuple[Number, Number, Number]]
    ) -> MaybeAwaitable:
        """on(brightness)

        Turns on the lights at the specified brightness.

        Arguments:
            brightness (Number or tuple, %):
                Use a single value to set the brightness of all lights at the
                same time. Use a tuple of three values to set the brightness
                of each light individually.
        """

    def off(self) -> MaybeAwaitable:
        """off()

        Turns off all the lights.
        """


class LightArray4(LightArray3):
    """Control an array of four single-color lights."""

    def on(
        self, brightness: Union[Number, Tuple[Number, Number, Number, Number]]
    ) -> MaybeAwaitable:
        """on(brightness)

        Turns on the lights at the specified brightness.

        Arguments:
            brightness (Number or tuple, %):
                Use a single value to set the brightness of all lights at the
                same time. Use a tuple of four values to set the brightness
                of each light individually. The order of the lights is shown
                in the image above.
        """


class LightMatrix:
    """Control a rectangular grid of single-color lights."""

    def __init__(self, rows: int, columns: int):
        """LightMatrix(rows, columns)

        Initializes the light matrix display.

        Arguments:
            rows (int): Number of rows in the grid
            columns (int): Number of columns in the grid
        """

    def orientation(self, up: Side) -> None:
        """orientation(up)

        Sets the orientation of the light matrix display.

        Only new displayed images and pixels are affected. The existing display
        contents remain unchanged.

        Arguments:
            up (Side): Which side of the light matrix display is "up" in your
                design. Choose ``Side.TOP``, ``Side.LEFT``, ``Side.RIGHT``,
                or ``Side.BOTTOM``.
        """

    def icon(self, icon: Matrix) -> None:
        """icon(icon)

        Displays an icon, represented by a matrix of :ref:`brightness`
        values.

        Arguments:
            icon (Matrix): Matrix of intensities (:ref:`brightness`). A 2D
                list is also accepted.
        """

    def animate(self, matrices: Collection[Matrix], interval: Number) -> None:
        """animate(matrices, interval)

        Displays an animation made using a list of images.

        Each image has the same format as above. Each image is
        shown for the given interval. The animation repeats
        forever while the rest of your program keeps running.

        Arguments:
            matrices (iter): Sequence of
                :class:`Matrix <pybricks.tools.Matrix>` of intensities.
            interval (Number, ms): Time to display each image in the list.
        """

    def pixel(self, row: Number, column: Number, brightness: Number = 100) -> None:
        """pixel(row, column, brightness=100)

        Turns on one pixel at the specified brightness.

        Arguments:
            row (Number): Vertical grid index, starting at 0 from the top.
            column (Number): Horizontal grid index, starting at 0 from the left.
            brightness (Number :ref:`brightness`): Brightness of the pixel.
        """

    def off(self) -> None:
        """off()

        Turns off all the pixels."""

    def number(self, number: Number) -> None:
        """number(number)

        Displays a number in the range -99 to 99.

        A minus sign (``-``) is shown as a faint dot
        in the center of the display. Numbers greater than 99 are
        shown as ``>``. Numbers less than -99 are shown as ``<``.

        Arguments:
            number (int): The number to be displayed.
        """

    def char(self, char: str) -> None:
        """char(char)

        Displays a character or symbol on the light grid. This may
        be any letter (``a``--``z``), capital letter (``A``--``Z``) or one of
        the following symbols: ``!"#$%&'()*+,-./:;<=>?@[\\]^_`{|}``.

        Arguments:
            character (str): The character or symbol to be displayed.
        """

    def text(self, text: str, on: Number = 500, off: Number = 50) -> None:
        """text(text, on=500, off=50)

        Displays a text string, one character at a time, with a pause
        between each character. After the last character is shown, all lights
        turn off.

        Arguments:
            text (str): The text to be displayed.
            on (Number, ms): For how long a character is shown.
            off (Number, ms): For how long the display is off between
                characters.
        """


class Keypad:
    """Get status of buttons on a keypad layout."""

    def __init__(self, active_buttons): ...

    def pressed(self) -> Set[Button]:
        """pressed() -> Set[Button]

        Checks which buttons are currently pressed.

        Returns:
            Set of pressed buttons.
        """


class Battery:
    """Get the status of a battery."""

    def voltage(self) -> int:
        """voltage() -> int: mV

        Gets the voltage of the battery.

        Returns:
            Battery voltage.
        """

    def current(self) -> int:
        """current() -> int: mA

        Gets the current supplied by the battery.

        Returns:
            Battery current.
        """


class Charger:
    """Get the status of a battery charger."""

    def connected(self) -> bool:
        """connected() -> bool

        Checks whether a charger is connected via USB.

        Returns:
            ``True`` if a charger is connected, ``False`` if not.
        """

    def status(self) -> int:
        """status() -> int

        Gets the status of the battery charger, represented by one of the
        following values. This corresponds to the battery light indicator
        right next to the USB port.

            0. Not charging (light is off).
            1. Charging (light is red).
            2. Charging is complete (light is green).
            3. There is a problem with the charger (light is yellow).

        Returns:
            Status value.
        """

    def current(self) -> int:
        """current() -> int: mA

        Gets the charging current.

        Returns:
            Charging current.
        """


class SimpleAccelerometer:
    """Get measurements from an accelerometer."""

    def acceleration(self) -> Tuple[int, int, int]:
        """acceleration() -> Tuple[int, int, int]: mm/s²

        Gets the acceleration of the device.

        Returns:
            Acceleration along all three axes.
        """

    def up(self) -> Side:
        """up() -> Side

        Checks which side of the hub currently faces upward.

        Returns:
            ``Side.TOP``, ``Side.BOTTOM``, ``Side.LEFT``, ``Side.RIGHT``,
            ``Side.FRONT`` or ``Side.BACK``.
        """

    def tilt(self) -> Tuple[int, int]:
        """tilt() -> Tuple[int, int]

        Gets the pitch and roll angles. This is relative to the
        :ref:`user-specified neutral orientation <robotframe>`.

        The order of rotation is pitch-then-roll. This is equivalent to a
        positive rotation along the robot y-axis and then a positive rotation
        along the x-axis.

        Returns:
            Tuple of pitch and roll angles in degrees.
        """


class IMU:

    def up(self, calibrated: bool = True) -> Side:
        """up(calibrated=True) -> Side

        Checks which side of the hub currently faces upward.

        Arguments:
            calibrated (bool): Choose ``True`` to use calibrated gyroscope and
                accelerometer data to determine which way is up. Choose
                ``False`` to use raw acceleration values.

        Returns:
            ``Side.TOP``, ``Side.BOTTOM``, ``Side.LEFT``, ``Side.RIGHT``,
            ``Side.FRONT`` or ``Side.BACK``.
        """

    def tilt(self, calibrated: bool = True) -> Tuple[int, int]:
        """tilt(calibrated=True) -> Tuple[int, int]

        Gets the pitch and roll angles. This is relative to the
        :ref:`user-specified neutral orientation <robotframe>`.

        The order of rotation is pitch-then-roll. This is equivalent to a
        positive rotation along the robot y-axis and then a positive rotation
        along the x-axis.

        Arguments:
            calibrated (bool): Choose ``True`` to use calibrated gyroscope and
                accelerometer data to determine the tilt. Choose ``False``
                to use raw acceleration values.

        Returns:
            Tuple of pitch and roll angles in degrees.
        """

    @overload
    def acceleration(self, axis: Axis = None, calibrated: bool = True) -> float: ...

    @overload
    def acceleration(self, calibrated: bool = True) -> Matrix: ...

    def acceleration(self, *args):
        """
        acceleration(axis, calibrated=True) -> float: mm/s²
        acceleration(calibrated=True) -> vector: mm/s²

        Gets the acceleration of the device along a given axis in the
        :ref:`robot reference frame <robotframe>`.

        Arguments:
            axis (Axis): Axis along which the acceleration should be
                measured, or ``None`` to get a vector along all axes.
            calibrated (bool): Choose ``True`` to use calibrated acceleration
                values. Choose ``False`` to use raw acceleration values.

        Returns:
            Acceleration along the specified axis. If you specify no axis,
            this returns a vector of accelerations along all axes.
        """

    def ready(self) -> bool:
        """ready() -> bool

        Checks if the device is calibrated and ready for use.

        This becomes ``True`` when the robot has been sitting stationary for a
        few seconds, which allows the device to re-calibrate. It is ``False``
        if the hub has just been started, or if it hasn't had a chance to
        calibrate for more than 10 minutes.

        Returns:
            ``True`` if it is ready for use, ``False`` if not.
        """

    def stationary(self) -> bool:
        """stationary() -> bool

        Checks if the device is currently stationary (not moving).

        Returns:
            ``True`` if stationary for at least a second, ``False`` if it is
            moving.
        """

    @overload
    def settings(
        self,
        *,
        angular_velocity_threshold: float = None,
        acceleration_threshold: float = None,
        heading_correction: float = None,
        angular_velocity_bias: Tuple[float, float, float] = None,
        angular_velocity_scale: Tuple[float, float, float] = None,
        acceleration_correction: Tuple[float, float, float, float, float, float] = None,
    ) -> None: ...

    @overload
    def settings(
        self,
    ) -> Tuple[
        float,
        float,
        float,
        Tuple[float, float, float],
        Tuple[float, float, float],
        Tuple[float, float, float, float, float, float],
    ]: ...

    def settings(self, *args):
        """
        settings(*, angular_velocity_threshold, acceleration_threshold, heading_correction, angular_velocity_bias, angular_velocity_scale, acceleration_correction)
        settings() -> Tuple

        Configures the IMU settings. If no arguments are given,
        this returns the current values. Use keyword arguments for each value
        to ensure correct behavior because settings may be added or changed in
        future releases.

        These IMU settings are saved on the hub. They will keep their values
        until you change them again. The values will be reset to default values
        if you update the hub to a different firmware version or call the
        ``hub.system.reset_storage`` method.

        The ``angular_velocity_threshold`` and ``acceleration_threshold``
        define when the hub is considered stationary. If all
        measurements stay below these thresholds for one second, the IMU
        will recalibrate itself. In a noisy room with high ambient vibrations (such as a
        competition hall), you can increase the thresholds
        slightly to give your robot the chance to calibrate.
        To verify that your settings are working as expected, test that
        the ``stationary()`` method gives ``False`` if your robot is moving,
        and ``True`` if it is sitting still.

        The gyroscope measures how fast the hub rotates to estimate the total
        angle. Due to variations in the production process, each
        hub consistently reports a different value for a full rotation. For
        example, your hub might consistently report `357` degrees for every
        `360` degree turn. You can measure this value
        with ``hub.imu.rotation(-Axis.Z, calibrated=False)`` and enter it as
        the ``heading_correction`` setting. Then, the ``hub.imu.heading()``
        method will take it into account going forward, correctly scaling it
        to 360 degrees for a full rotation.

        Arguments:
            angular_velocity_threshold (Number, deg/s): The threshold for
                variations in the angular velocity below which the hub is
                considered stationary enough to calibrate.
                After a reset the value is 2 deg/s.
            acceleration_threshold (Number, mm/s²): The threshold for
                variations in acceleration below which the hub is considered
                stationary enough to calibrate. After a reset the value
                is 2500 mm/s².
            heading_correction (Number, deg): Number of degrees
                reported by for one full rotation of your robot.
                After a reset the value is 360 degrees. This is applied on top
                of any scaling that is done by the ``angular_velocity_scale``
                setting.
            angular_velocity_bias (tuple, deg/s): Initial bias for angular
                velocity measurements along x, y, and z immediately after boot.
                After a reset the value is (0, 0, 0) deg/s.
            angular_velocity_scale (tuple, deg): Scale adjustment for x, y, and
                z rotation to account for manufacturing differences. After a
                reset the value is (360, 360, 360) deg/s. The correct values
                can be obtained using `hub.imu.rotation(Axis.X, calibrated=False)`
                and repeating it for each axis.
            acceleration_correction (tuple, mm/s²): Scale adjustment for x, y,
                and z gravity magnitude in both directions to account for
                manufacturing differences. After a reset the
                value is (9806.65, -9806.65, 9806.65, -9806.65, 9806.65, -9806.65) mm/s².
                The correct values can be
                obtained using `hub.imu.acceleration(Axis.X, calibrated=False)`
                and repeating it for all axes in both directions.
        """

    def heading(self) -> float:
        """heading() -> float: deg

        Gets the heading angle of your robot. A positive value means a
        clockwise turn.

        The heading is 0 when your program starts. The value continues to grow
        even as the robot turns more than 180 degrees. It does not wrap around
        to -180 like it does in some apps.

        Returns:
            Heading angle relative to starting orientation.

        """

    def reset_heading(self, angle: Number) -> None:
        """reset_heading(angle)

        Resets the accumulated heading angle of the robot.

        This cannot be called while a drive base is using the gyro to drive or
        hold position.
        Use :meth:`DriveBase.reset() <pybricks.robotics.DriveBase.reset>`
        instead, which will stop the robot and then set the new heading value.

        .. versionchanged:: 3.6 Resetting the angle while driving is not allowed. Stop first.

        Arguments:
            angle (Number, deg): Value to which the heading should be reset.

        Raises:
            OSError:
                There is a drive base that is currently using the gyro.
        """

    @overload
    def angular_velocity(self, axis: Axis = None, calibrated: bool = True) -> float: ...

    @overload
    def angular_velocity(self, calibrated: bool = True) -> Matrix: ...

    def angular_velocity(self, *args):
        """
        angular_velocity(axis, calibrated=True) -> float: deg/s
        angular_velocity(calibrated=True) -> vector: deg/s

        Gets the angular velocity of the device along a given axis in
        the :ref:`robot reference frame <robotframe>`.

        Arguments:
            axis (Axis): Axis along which the angular velocity should be
                measured, or ``None`` to get a vector along all axes.
            calibrated (bool): Choose ``True`` to compensate for the estimated
                bias and configured scale of the gyroscope. Choose ``False``
                to get raw angular velocity values.

        Returns:
            Angular velocity along the specified axis. If you specify no axis,
            this returns a vector of accelerations along all axes.
        """

    def rotation(self, axis: Axis, calibrated: bool = True) -> float:
        """
        rotation(axis, calibrated=True) -> float: deg

        Gets the rotation of the device along a given axis in
        the :ref:`robot reference frame <robotframe>`.

        This value is useful if your robot *only* rotates along the requested
        axis. For general three-dimensional motion, use the
        ``orientation()`` method instead.

        Arguments:
            axis (Axis): Axis along which the rotation should be measured.
            calibrated (bool): Choose ``True`` to compensate for configured
                scale of the gyroscope. Choose ``False`` to get unscaled values.

        Returns:
            The rotation angle.
        """

    def orientation(self) -> Matrix:
        """
        orientation() -> Matrix

        Gets the three-dimensional orientation of the robot in
        the :ref:`robot reference frame <robotframe>`.

        It returns a rotation matrix whose columns represent the ``X``, ``Y``,
        and ``Z`` axis of the robot.

        Returns:
            The 3x3 rotation matrix.
        """


class CommonColorSensor:
    """Generic color sensor that supports Pybricks color calibration."""

    def __init__(self, port: Port):
        """__init__(port)

        Arguments:
            port (Port): Port to which the sensor is connected.
        """

    def color(self) -> MaybeAwaitableColor:
        """color() -> Color

        Scans the color of a surface.

        You choose which colors are detected using the
        ``detectable_colors()`` method. By default, it detects
        ``Color.RED``, ``Color.YELLOW``, ``Color.GREEN``, ``Color.BLUE``,
        ``Color.WHITE``, or ``Color.NONE``.

        Returns:
            Detected color.
        """

    def hsv(self) -> MaybeAwaitableColor:
        """hsv() -> Color

        Scans the color of a surface.

        This method is similar to ``color()``, but it gives the full range
        of hue, saturation and brightness values, instead of rounding it to the
        nearest detectable color.

        Returns:
            Measured color. The color is described by a hue (0--359), a
            saturation (0--100), and a brightness value (0--100).
        """

    def ambient(self) -> MaybeAwaitableInt:
        """ambient() -> int: %

        Measures the ambient light intensity.

        Returns:
            Ambient light intensity, ranging from 0% (dark)
            to 100% (bright).
        """

    def reflection(self) -> MaybeAwaitableInt:
        """reflection() -> int: %

        Measures how much a surface reflects the light emitted by the
        sensor.

        Returns:
            Measured reflection, ranging from 0% (no reflection) to
            100% (high reflection).
        """

    @overload
    def detectable_colors(self, colors: Collection[Color]) -> None: ...

    @overload
    def detectable_colors(self) -> Collection[Color]: ...

    def detectable_colors(self, *args):
        """
        detectable_colors(colors)
        detectable_colors() -> Collection[Color]

        Configures which colors the ``color()`` method should detect.

        Specify only colors that you wish to detect in your application.
        This way, the full-color measurements are rounded to the nearest
        desired color, and other colors are ignored. This improves reliability.

        If you give no arguments, the currently chosen colors will be returned.

        When coding with blocks, this is configured in the sensor setup block.

        Arguments:
            colors (list or tuple): List of :class:`Color <.parameters.Color>`
                objects: the colors that you want to detect. You can pick
                standard colors such as ``Color.MAGENTA``, or provide your
                own colors like ``Color(h=348, s=96, v=40)`` for even
                better results. You measure your own colors with the
                ``hsv()`` method.
        """


class AmbientColorSensor(CommonColorSensor):
    """Like CommonColorSensor, but also detects ambient colors when the sensor
    light is turned off"""

    def color(self, surface: bool = True) -> MaybeAwaitableColor:
        """color(surface=True) -> Color

        Scans the color of a surface or an external light source.

        You choose which colors are detected using the
        ``detectable_colors()`` method. By default, it detects
        ``Color.RED``, ``Color.YELLOW``, ``Color.GREEN``, ``Color.BLUE``,
        ``Color.WHITE``, or ``Color.NONE``.

        Arguments:
            surface (bool): Choose ``true`` to scan the color of objects
                and surfaces. Choose ``false`` to scan the color of
                screens and other external light sources.

        Returns:
            Detected color.`
        """

    def hsv(self, surface: bool = True) -> MaybeAwaitableColor:
        """hsv(surface=True) -> Color

        Scans the color of a surface or an external light source.

        This method is similar to ``color()``, but it gives the full range
        of hue, saturation and brightness values, instead of rounding it to the
        nearest detectable color.

        Arguments:
            surface (bool): Choose ``true`` to scan the color of objects
                and surfaces. Choose ``false`` to scan the color of
                screens and other external light sources.

        Returns:
            Measured color. The color is described by a hue (0--359), a
            saturation (0--100), and a brightness value (0--100).
        """
