step.motion.line_follow ======================= .. py:module:: step.motion.line_follow .. autoapi-nested-parse:: Line following using IR sensors. This module provides steps for following lines using one or two IR sensors with PID-based steering control. Two families of steps are available: 1. **Profiled line follow** (``FollowLine``, ``FollowLineSingle``, etc.) — built on ``LinearMotion`` for trapezoidal-profiled distance control along a single axis. 2. **Directional line follow** (``DirectionalFollowLine``, ``StrafeFollowLine``, etc.) — uses direct ``ChassisVelocity`` control with independent heading and strafe speed inputs, allowing line following while strafing, driving diagonally, or any combination. All steps support composable ``StopCondition`` via the ``.until()`` builder method, enabling patterns like:: follow_line(left, right, speed=0.5).until(on_black(left) & on_black(right)) follow_line_single(sensor, speed=0.4).until(on_black(stop_sensor)) Classes ------- .. autoapisummary:: step.motion.line_follow.LineFollowConfig step.motion.line_follow.LineSide step.motion.line_follow.SingleLineFollowConfig step.motion.line_follow.LineFollow step.motion.line_follow.SingleSensorLineFollow step.motion.line_follow.FollowLine step.motion.line_follow.FollowLineSingle step.motion.line_follow.DirectionalLineFollowConfig step.motion.line_follow.DirectionalLineFollow step.motion.line_follow.DirectionalSingleLineFollowConfig step.motion.line_follow.DirectionalSingleLineFollow step.motion.line_follow.DirectionalFollowLine step.motion.line_follow.StrafeFollowLine step.motion.line_follow.StrafeFollowLineSingle step.motion.line_follow.DirectionalFollowLineSingle Module Contents --------------- .. py:class:: LineFollowConfig Configuration for LineFollow step with two sensors. .. py:attribute:: left_sensor :type: raccoon.sensor_ir.IRSensor .. py:attribute:: right_sensor :type: raccoon.sensor_ir.IRSensor .. py:attribute:: speed_scale :type: float .. py:attribute:: distance_cm :type: float | None :value: None .. py:attribute:: kp :type: float :value: 0.4 .. py:attribute:: ki :type: float :value: 0.0 .. py:attribute:: kd :type: float :value: 0.1 .. py:class:: LineSide(*args, **kwds) Bases: :py:obj:`enum.Enum` Which edge of the line to track with a single sensor. .. py:attribute:: LEFT :value: 'left' .. py:attribute:: RIGHT :value: 'right' .. py:class:: SingleLineFollowConfig Configuration for single-sensor line following. The sensor tracks the edge of a line using PID control. ``side`` selects which edge: LEFT means the sensor approaches from the left (steers right when it sees black), RIGHT is the opposite. .. py:attribute:: sensor :type: raccoon.sensor_ir.IRSensor .. py:attribute:: speed_scale :type: float .. py:attribute:: distance_cm :type: float | None :value: None .. py:attribute:: side :type: LineSide .. py:attribute:: kp :type: float :value: 0.4 .. py:attribute:: ki :type: float :value: 0.0 .. py:attribute:: kd :type: float :value: 0.1 .. py:class:: LineFollow(config: LineFollowConfig, until: step.condition.StopCondition | None = None) Bases: :py:obj:`step.motion.motion_step.MotionStep` Follow a line using two IR sensors with PID steering. Computes a steering error as the difference between the left and right sensors' ``probabilityOfBlack()`` readings and feeds it through a PID controller. The PID output is applied as an angular velocity (omega) override on the underlying ``LinearMotion``, which handles profiled distance control and odometry integration. Supports distance-based termination, composable ``StopCondition`` via ``.until()``, or both (whichever triggers first). .. py:attribute:: config .. py:method:: to_simulation_step() -> step.SimulationStep .. py:method:: on_start(robot: raccoon.robot.api.GenericRobot) -> None .. py:method:: on_update(robot: raccoon.robot.api.GenericRobot, dt: float) -> bool .. py:class:: SingleSensorLineFollow(config: SingleLineFollowConfig, until: step.condition.StopCondition | None = None) Bases: :py:obj:`step.motion.motion_step.MotionStep` Follow a line edge using a single IR sensor with PID edge-tracking. Targets ``probabilityOfBlack() = 0.5`` (the line edge) as the setpoint. The ``side`` configuration flips the error sign to select left vs. right edge tracking. The PID output overrides the angular velocity on the underlying ``LinearMotion``, which handles profiled distance control and odometry integration. Supports distance-based termination, composable ``StopCondition`` via ``.until()``, or both (whichever triggers first). .. py:attribute:: config .. py:method:: to_simulation_step() -> step.SimulationStep .. py:method:: on_start(robot: raccoon.robot.api.GenericRobot) -> None .. py:method:: on_update(robot: raccoon.robot.api.GenericRobot, dt: float) -> bool .. py:class:: FollowLine(left_sensor: raccoon.sensor_ir.IRSensor, right_sensor: raccoon.sensor_ir.IRSensor, distance_cm: float | None = None, speed: float = 0.5, kp: float = 0.4, ki: float = 0.0, kd: float = 0.1, until: step.condition.StopCondition | None = None) Bases: :py:obj:`LineFollow` Follow a line using two IR sensors for steering. Drives forward while a PID controller steers the robot to keep it centered on a line. The error signal is the difference between the left and right sensors' ``probabilityOfBlack()`` readings. A positive error (left sees more black) steers the robot back toward center. The underlying ``LinearMotion`` handles profiled velocity control and odometry-based distance tracking, while the PID output overrides the heading command as an angular velocity (omega). Supports distance-based termination, composable ``StopCondition`` via ``.until()``, or both (whichever triggers first). At least one of ``distance_cm`` or ``until`` must be provided. Both sensors must be calibrated (white/black thresholds set) before use. :param left_sensor: Left IR sensor instance, positioned to the left of the line. :param right_sensor: Right IR sensor instance, positioned to the right of the line. :param distance_cm: Distance to follow in centimeters. The step finishes when this distance has been traveled according to odometry. Optional if ``until`` is provided. :param speed: Fraction of max velocity (0.0--1.0). Lower speeds give the PID more time to correct but are slower overall. Default 0.5. :param kp: Proportional gain for steering PID. Higher values produce sharper corrections. Default 0.75. :param ki: Integral gain for steering PID. Typically left at 0.0 unless there is a persistent drift. Default 0.0. :param kd: Derivative gain for steering PID. Damps oscillation around the line. Default 0.5. :param until: Composable stop condition (e.g., ``on_black(left) & on_black(right)``). Can also be chained via the ``.until()`` builder method. :returns: A ``FollowLine`` step configured for line following. Example:: from raccoon.step.motion import FollowLine from raccoon.step.condition import on_black # Follow a line for 80 cm at half speed follow_line(left_sensor=left, right_sensor=right, distance_cm=80, speed=0.5) # Follow until both sensors see black (intersection) follow_line(left, right, speed=0.5).until(on_black(left) & on_black(right)) # Distance + early termination follow_line(left, right, distance_cm=100, speed=0.5).until(on_black(third)) .. py:class:: FollowLineSingle(sensor: raccoon.sensor_ir.IRSensor, distance_cm: float | None = None, speed: float = 0.5, side: LineSide = LineSide.LEFT, kp: float = 0.4, ki: float = 0.0, kd: float = 0.1, until: step.condition.StopCondition | None = None) Bases: :py:obj:`SingleSensorLineFollow` Follow a line edge using a single IR sensor. The sensor tracks the boundary between the line and the background, where ``probabilityOfBlack()`` is approximately 0.5. The PID controller drives the error ``(reading - 0.5)`` toward zero, keeping the sensor positioned right on the edge. The ``side`` parameter controls which edge: ``LEFT`` means the sensor is to the left of the line (steers right when it sees black), and ``RIGHT`` is the opposite. This variant is useful when only one sensor is available, or when the line is too narrow for two sensors. The underlying ``LinearMotion`` handles profiled velocity and odometry-based distance tracking. Supports distance-based termination, composable ``StopCondition`` via ``.until()``, or both (whichever triggers first). At least one of ``distance_cm`` or ``until`` must be provided. The sensor must be calibrated (white/black thresholds set) before use. :param sensor: The IR sensor instance used for edge tracking. :param distance_cm: Distance to follow in centimeters. The step finishes when this distance has been traveled. Optional if ``until`` is provided. :param speed: Fraction of max velocity (0.0--1.0). Default 0.5. :param side: Which edge of the line to track. ``LineSide.LEFT`` (default) or ``LineSide.RIGHT``. :param kp: Proportional gain for steering PID. Default 1.0. :param ki: Integral gain for steering PID. Default 0.0. :param kd: Derivative gain for steering PID. Default 0.3. :param until: Composable stop condition (e.g., ``on_black(stop_sensor)``). Can also be chained via the ``.until()`` builder method. :returns: A ``FollowLineSingle`` step. Example:: from raccoon.step.motion import FollowLineSingle, LineSide from raccoon.step.condition import on_black # Follow left edge for 60 cm follow_line_single(sensor=front_ir, distance_cm=60, speed=0.4) # Follow until stop sensor sees black follow_line_single(front_ir, speed=0.4, side=LineSide.LEFT).until(on_black(stop)) # Distance + early termination with timeout follow_line_single(front_ir, distance_cm=100).until(on_black(stop) | after_seconds(10)) .. py:class:: DirectionalLineFollowConfig Configuration for directional line following with two sensors. Allows independent heading (forward/backward) and strafe (left/right) speed components. The PID controller steers via angular velocity based on the difference between left and right sensor readings. When ``lateral_correction`` is True, the PID output controls lateral velocity (vy) instead of angular velocity (wz), keeping the robot's heading constant while correcting position by strafing. .. py:attribute:: left_sensor :type: raccoon.sensor_ir.IRSensor .. py:attribute:: right_sensor :type: raccoon.sensor_ir.IRSensor .. py:attribute:: heading_speed :type: float .. py:attribute:: strafe_speed :type: float .. py:attribute:: distance_cm :type: float | None :value: None .. py:attribute:: kp :type: float :value: 0.4 .. py:attribute:: ki :type: float :value: 0.0 .. py:attribute:: kd :type: float :value: 0.1 .. py:attribute:: lateral_correction :type: bool :value: False .. py:class:: DirectionalLineFollow(config: DirectionalLineFollowConfig, until: step.condition.StopCondition | None = None) Bases: :py:obj:`step.motion.motion_step.MotionStep` Follow a line with independent heading and strafe velocity components. Uses direct ``ChassisVelocity`` control instead of ``LinearMotion``, enabling line following while strafing, driving diagonally, or any combination. A PID controller computes angular velocity from the difference between the left and right sensors' ``probabilityOfBlack()`` readings. Distance is tracked via odometry as euclidean distance from the start position. Supports distance-based termination, composable ``StopCondition``, or both. .. py:attribute:: config .. py:method:: to_simulation_step() -> step.SimulationStep .. py:method:: on_start(robot: raccoon.robot.api.GenericRobot) -> None .. py:method:: on_update(robot: raccoon.robot.api.GenericRobot, dt: float) -> bool .. py:class:: DirectionalSingleLineFollowConfig Configuration for directional single-sensor line following. The sensor tracks the edge of a line using PID control while the robot moves with the given heading and strafe velocity components. When ``lateral_correction`` is True, the PID output controls lateral velocity (vy) instead of angular velocity (wz), keeping the robot's heading constant while correcting position by strafing. .. py:attribute:: sensor :type: raccoon.sensor_ir.IRSensor .. py:attribute:: heading_speed :type: float .. py:attribute:: strafe_speed :type: float .. py:attribute:: distance_cm :type: float | None :value: None .. py:attribute:: side :type: LineSide .. py:attribute:: kp :type: float :value: 0.4 .. py:attribute:: ki :type: float :value: 0.0 .. py:attribute:: kd :type: float :value: 0.1 .. py:attribute:: lateral_correction :type: bool :value: False .. py:class:: DirectionalSingleLineFollow(config: DirectionalSingleLineFollowConfig, until: step.condition.StopCondition | None = None) Bases: :py:obj:`step.motion.motion_step.MotionStep` Follow a line edge with independent heading and strafe velocity. Targets ``probabilityOfBlack() = 0.5`` (the line edge) as the setpoint. The ``side`` configuration flips the error sign to select left vs. right edge tracking. The PID output controls angular velocity while heading and strafe velocities are set directly via ``ChassisVelocity``. Supports distance-based and composable ``StopCondition``-based termination. .. py:attribute:: config .. py:method:: to_simulation_step() -> step.SimulationStep .. py:method:: on_start(robot: raccoon.robot.api.GenericRobot) -> None .. py:method:: on_update(robot: raccoon.robot.api.GenericRobot, dt: float) -> bool .. py:class:: DirectionalFollowLine(left_sensor: raccoon.sensor_ir.IRSensor, right_sensor: raccoon.sensor_ir.IRSensor, distance_cm: float | None = None, heading_speed: float = 0.0, strafe_speed: float = 0.0, kp: float = 0.4, ki: float = 0.0, kd: float = 0.1, until: step.condition.StopCondition | None = None) Bases: :py:obj:`DirectionalLineFollow` Follow a line with independent heading and strafe speeds. Drive along a line using any combination of forward and lateral velocity while a PID controller steers the robot via angular velocity. The error signal is the difference between the left and right sensors' ``probabilityOfBlack()`` readings. Distance is tracked via odometry as the euclidean distance from the start position. Unlike ``FollowLine`` which only drives forward, this step accepts both ``heading_speed`` (forward/backward) and ``strafe_speed`` (left/right) as independent fractions of max velocity, enabling line following while strafing or driving diagonally. Supports distance-based termination, composable ``StopCondition`` via ``.until()``, or both (whichever triggers first). At least one of ``distance_cm`` or ``until`` must be provided. Both sensors must be calibrated (white/black thresholds set) before use. Requires a mecanum or omni-wheel drivetrain if ``strafe_speed`` is nonzero. :param left_sensor: Left IR sensor instance, positioned to the left of the line. :param right_sensor: Right IR sensor instance, positioned to the right of the line. :param distance_cm: Distance to follow in centimeters. The step finishes when this euclidean distance has been traveled. Optional if ``until`` is provided. :param heading_speed: Forward/backward speed as a fraction of max velocity (-1.0 to 1.0). Positive = forward, negative = backward. Default 0.0. :param strafe_speed: Lateral speed as a fraction of max velocity (-1.0 to 1.0). Positive = right, negative = left. Default 0.0. :param kp: Proportional gain for steering PID. Default 0.75. :param ki: Integral gain for steering PID. Default 0.0. :param kd: Derivative gain for steering PID. Default 0.5. :param until: Composable stop condition. Can also be chained via the ``.until()`` builder method. :returns: A ``DirectionalFollowLine`` step. Example:: from raccoon.step.motion import DirectionalFollowLine from raccoon.step.condition import on_black # Strafe right while following a line for 50 cm directional_follow_line(left, right, distance_cm=50, strafe_speed=0.5) # Follow until both sensors see black directional_follow_line(left, right, strafe_speed=0.4).until( on_black(left) & on_black(right) ) .. py:class:: StrafeFollowLine(left_sensor: raccoon.sensor_ir.IRSensor, right_sensor: raccoon.sensor_ir.IRSensor, distance_cm: float | None = None, speed: float = 0.5, kp: float = 0.4, ki: float = 0.0, kd: float = 0.1, until: step.condition.StopCondition | None = None) Bases: :py:obj:`DirectionalLineFollow` Follow a line forward, correcting position by strafing left/right. The robot drives forward at the given speed while a PID controller corrects lateral position using two sensors. Unlike ``FollowLine`` which steers by rotating, this step keeps the robot's heading constant and corrects by strafing, which is useful when the robot must maintain a fixed orientation (e.g. to keep a side-mounted mechanism aligned). Supports distance-based termination, composable ``StopCondition`` via ``.until()``, or both (whichever triggers first). At least one of ``distance_cm`` or ``until`` must be provided. Both sensors must be calibrated. Requires a mecanum or omni-wheel drivetrain. :param left_sensor: Left IR sensor instance. :param right_sensor: Right IR sensor instance. :param distance_cm: Distance to follow in centimeters. Optional if ``until`` is provided. :param speed: Forward speed as fraction of max velocity (0.0 to 1.0). Default 0.5. Use negative values to drive backward. :param kp: Proportional gain for lateral PID. Default 0.75. :param ki: Integral gain for lateral PID. Default 0.0. :param kd: Derivative gain for lateral PID. Default 0.5. :param until: Composable stop condition. Can also be chained via the ``.until()`` builder method. :returns: A ``StrafeFollowLine`` step configured for lateral correction. Example:: from raccoon.step.motion import StrafeFollowLine from raccoon.step.condition import on_black # Follow a line for 40 cm, correcting via strafe strafe_follow_line(left, right, distance_cm=40, speed=0.4) # Follow until both sensors see black strafe_follow_line(left, right, speed=0.4).until( on_black(left) & on_black(right) ) .. py:class:: StrafeFollowLineSingle(sensor: raccoon.sensor_ir.IRSensor, distance_cm: float | None = None, speed: float = 0.5, side: LineSide = LineSide.LEFT, kp: float = 0.4, ki: float = 0.0, kd: float = 0.1, until: step.condition.StopCondition | None = None) Bases: :py:obj:`DirectionalSingleLineFollow` Follow a line edge forward, correcting position by strafing. The robot drives forward at the given speed while a PID controller corrects lateral position using a single sensor tracking the line edge. Unlike ``FollowLineSingle`` which steers by rotating, this step keeps the robot's heading constant and corrects by strafing. Supports distance-based termination, composable ``StopCondition`` via ``.until()``, or both (whichever triggers first). At least one of ``distance_cm`` or ``until`` must be provided. The sensor must be calibrated. Requires a mecanum or omni-wheel drivetrain. :param sensor: IR sensor for edge tracking. :param distance_cm: Distance to follow in centimeters. Optional if ``until`` is provided. :param speed: Forward speed as fraction of max velocity (0.0 to 1.0). Default 0.5. Use negative values to drive backward. :param side: Which edge of the line to track. Default ``LineSide.LEFT``. :param kp: Proportional gain for lateral PID. Default 1.0. :param ki: Integral gain for lateral PID. Default 0.0. :param kd: Derivative gain for lateral PID. Default 0.3. :param until: Composable stop condition. Can also be chained via the ``.until()`` builder method. :returns: A ``StrafeFollowLineSingle`` step configured for lateral correction. Example:: from raccoon.step.motion import StrafeFollowLineSingle, LineSide from raccoon.step.condition import on_black # Follow a line edge for 40 cm, correcting via strafe strafe_follow_line_single(front_ir, distance_cm=40, speed=0.4) # Follow until stop sensor sees black strafe_follow_line_single(front_ir, speed=0.4).until(on_black(stop)) .. py:class:: DirectionalFollowLineSingle(sensor: raccoon.sensor_ir.IRSensor, distance_cm: float | None = None, heading_speed: float = 0.0, strafe_speed: float = 0.0, side: LineSide = LineSide.LEFT, kp: float = 0.4, ki: float = 0.0, kd: float = 0.1, until: step.condition.StopCondition | None = None) Bases: :py:obj:`DirectionalSingleLineFollow` Follow a line edge with a single sensor and independent heading/strafe speeds. The sensor tracks the boundary between the line and the background, where ``probabilityOfBlack()`` is approximately 0.5. The ``side`` parameter selects which edge to track. The PID output controls angular velocity while heading and strafe velocities are set independently. Supports distance-based termination, composable ``StopCondition`` via ``.until()``, or both (whichever triggers first). At least one of ``distance_cm`` or ``until`` must be provided. The sensor must be calibrated (white/black thresholds set) before use. Requires a mecanum or omni-wheel drivetrain if ``strafe_speed`` is nonzero. :param sensor: IR sensor for edge tracking. :param distance_cm: Distance to follow in centimeters. Optional if ``until`` is provided. :param heading_speed: Forward/backward speed fraction (-1.0 to 1.0). Default 0.0. :param strafe_speed: Lateral speed fraction (-1.0 to 1.0). Default 0.0. :param side: Which edge of the line to track. Default ``LineSide.LEFT``. :param kp: Proportional gain for steering PID. Default 1.0. :param ki: Integral gain for steering PID. Default 0.0. :param kd: Derivative gain for steering PID. Default 0.3. :param until: Composable stop condition. Can also be chained via the ``.until()`` builder method. :returns: A ``DirectionalFollowLineSingle`` step. Example:: from raccoon.step.motion import DirectionalFollowLineSingle, LineSide from raccoon.step.condition import on_black # Strafe right while tracking the left edge for 50 cm directional_follow_line_single(front_ir, distance_cm=50, strafe_speed=0.4) # Follow until stop sensor sees black directional_follow_line_single(front_ir, strafe_speed=0.4).until(on_black(stop))