> ## Documentation Index
> Fetch the complete documentation index at: https://stem-docs.intellectualpoint.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Line following

> Build a line-following robot from scratch using the grayscale sensor, if/else steering, and backup recovery for sharp turns.

## Line following

<Info>
  **Time**: 11:05 AM to 11:35 AM
</Info>

In this section you will build a program that makes your robot follow a dark line on a light surface without any human input. This is your first taste of autonomous behavior, but it is not AI — it uses simple if/else rules.

***

## Set up the track

Place a strip of dark electrical tape on a light-colored surface. Include both straight sections and curves. The tape should be about 2 cm wide with strong contrast against the background.

<Tip>
  Use the black electrical tape from your kit on a white table or light floor. The stronger the contrast, the better the robot tracks.
</Tip>

***

## How the grayscale sensor works

The PiCar-X has three grayscale sensors underneath, arranged left-center-right. Each one returns an analog brightness value:

* **Low value** (100–400) = dark surface (the tape)
* **High value** (800–1400) = light surface (the background)

The `px.get_line_status()` method compares each sensor's value against the `line_reference` you calibrated on Day 1 and returns a list of three values:

| Status | Meaning                                         |
| ------ | ----------------------------------------------- |
| `0`    | Sensor sees the line (dark, below reference)    |
| `1`    | Sensor sees background (light, above reference) |

So `[0, 1, 1]` means only the left sensor sees the line — the line is to the left.

***

## Build the line follower

```bash theme={null}
nano ~/camp/day2/line_follower.py
```

### Step 1 — Setup

```python theme={null}
#!/usr/bin/env python3
from picarx import Picarx
import time

px = Picarx()

px_power = 10
offset = 20
last_state = "stop"
```

`px_power` is the motor speed (10% — slow and steady). `offset` is the steering angle when the line drifts to one side. `last_state` remembers which direction the robot was going before it lost the line.

### Step 2 — Interpret the sensor

Write a function that converts the raw sensor status into a direction:

```python theme={null}
def get_status(val_list):
    state = px.get_line_status(val_list)

    if state == [0, 0, 0]:
        return 'stop'
    elif state[1] == 1:
        return 'forward'
    elif state[0] == 1:
        return 'right'
    elif state[2] == 1:
        return 'left'
```

How to read this:

* `[0, 0, 0]` — all sensors see dark. The line is lost (or the robot drove onto a fully dark area). Return `'stop'`.
* `state[1] == 1` — center sensor sees background. The line is under the outer sensors, which means the robot is roughly centered. Return `'forward'`.
* `state[0] == 1` — left sensor sees background (line is NOT under the left sensor). The line must be to the right, so return `'right'` (steer right to follow it).
* `state[2] == 1` — right sensor sees background. The line is to the left, so return `'left'`.

### Step 3 — Backup recovery

This is the critical function that makes sharp turns work. When the robot loses the line, instead of driving forward off the track, it **backs up** while steering toward the last known direction:

```python theme={null}
def outHandle():
    global last_state
    if last_state == 'left':
        px.set_dir_servo_angle(-30)
        px.backward(10)
    elif last_state == 'right':
        px.set_dir_servo_angle(30)
        px.backward(10)

    while True:
        gm_val_list = px.get_grayscale_data()
        gm_state = get_status(gm_val_list)
        print(f"  RECOVER  L={gm_val_list[0]:>5.0f} "
              f"C={gm_val_list[1]:>5.0f} "
              f"R={gm_val_list[2]:>5.0f}  status={gm_state}")
        if gm_state != last_state:
            break
    time.sleep(0.001)
```

The `while True` loop keeps backing up until a sensor picks up the line again (the state changes). This is what allows the robot to handle sharp turns — it reverses back onto the line instead of overshooting.

### Step 4 — The main loop

Tie everything together. Read the sensors, decide which direction to go, and drive:

```python theme={null}
def follow_line():
    global last_state
    print(f"\n  Following line — power={px_power}, steer={offset}°")
    print("  Press Ctrl-C to stop.\n")

    last_state = "stop"

    try:
        while True:
            gm_val_list = px.get_grayscale_data()
            gm_state = get_status(gm_val_list)

            print(f"  L={gm_val_list[0]:>5.0f} "
                  f"C={gm_val_list[1]:>5.0f} "
                  f"R={gm_val_list[2]:>5.0f}  status={gm_state}")

            if gm_state != "stop":
                last_state = gm_state

            if gm_state == 'forward':
                px.set_dir_servo_angle(0)
                px.forward(px_power)
            elif gm_state == 'left':
                px.set_dir_servo_angle(offset)
                px.forward(px_power)
            elif gm_state == 'right':
                px.set_dir_servo_angle(-offset)
                px.forward(px_power)
            else:
                outHandle()

    except KeyboardInterrupt:
        pass
    finally:
        px.stop()
        px.set_dir_servo_angle(0)
        print("\n  Stopped.\n")
```

The key line: `if gm_state != "stop": last_state = gm_state`. This saves the last known direction so that if the line is lost, `outHandle()` knows which way to back up.

### Step 5 — Live sensor view

Add a helper mode that shows sensor values without moving. This is useful for checking your calibration:

```python theme={null}
def live_sensor_view():
    print("\n  Live Sensor View (Ctrl-C to stop)\n")
    try:
        while True:
            vals = px.get_grayscale_data()
            state = px.get_line_status(vals)
            status = get_status(vals)
            print(f"  L={vals[0]:>6.0f}  C={vals[1]:>6.0f}  "
                  f"R={vals[2]:>6.0f}  {state}  {status}")
            time.sleep(0.3)
    except KeyboardInterrupt:
        print("\n  Stopped.\n")
```

### Step 6 — Menu and entry point

```python theme={null}
if __name__ == "__main__":
    print("=" * 50)
    print("  PiCar-X — Line Follower")
    print("=" * 50)
    print(f"\n  Line reference: {px.line_reference}")
    print("  (Set during Day 1 grayscale calibration)\n")

    while True:
        print("  [1] Live sensor view (no movement)")
        print("  [2] Follow the line")
        print("  [q] Quit\n")

        choice = input("  Pick 1-2 or q: ").strip().lower()
        if choice == '1': live_sensor_view()
        elif choice == '2': follow_line()
        elif choice in ('q', 'quit'): break
        else: print("  Enter 1, 2, or q.\n")

    px.stop()
    print("  Bye!\n")
```

### Run it

```bash theme={null}
sudo python3 ~/camp/day2/line_follower.py
```

Start with option **1** to verify your sensors respond to the tape. Then place the robot on the track and select option **2**.

<Warning>
  If the line reference shows default values and the robot does not track well, re-run the grayscale calibration from Day 1:

  ```bash theme={null}
  sudo python3 ~/camp/day1/calibrate_picar.py
  ```

  Select option **3** and follow the prompts.
</Warning>

***

### Final code: line\_follower.py

<Accordion title="Click to see the complete program">
  ```python theme={null}
  #!/usr/bin/env python3
  from picarx import Picarx
  import sys
  import time

  px = Picarx()

  px_power = 10
  offset = 20
  last_state = "stop"


  def get_status(val_list):
      _state = px.get_line_status(val_list)
      if _state == [0, 0, 0]:
          return 'stop'
      elif _state[1] == 1:
          return 'forward'
      elif _state[0] == 1:
          return 'right'
      elif _state[2] == 1:
          return 'left'


  def outHandle():
      global last_state
      if last_state == 'left':
          px.set_dir_servo_angle(-30)
          px.backward(10)
      elif last_state == 'right':
          px.set_dir_servo_angle(30)
          px.backward(10)
      while True:
          gm_val_list = px.get_grayscale_data()
          gm_state = get_status(gm_val_list)
          print(f"  RECOVER  L={gm_val_list[0]:>5.0f} "
                f"C={gm_val_list[1]:>5.0f} "
                f"R={gm_val_list[2]:>5.0f}  status={gm_state}")
          if gm_state != last_state:
              break
      time.sleep(0.001)


  def live_sensor_view():
      print("\n  Live Sensor View (Ctrl-C to stop)\n")
      try:
          while True:
              vals = px.get_grayscale_data()
              state = px.get_line_status(vals)
              status = get_status(vals)
              print(f"  L={vals[0]:>6.0f}  C={vals[1]:>6.0f}  "
                    f"R={vals[2]:>6.0f}  {state}  {status}")
              time.sleep(0.3)
      except KeyboardInterrupt:
          print("\n  Stopped.\n")


  def follow_line():
      global last_state
      print(f"\n  Following line — power={px_power}, steer={offset}°")
      print("  Press Ctrl-C to stop.\n")
      last_state = "stop"

      try:
          while True:
              gm_val_list = px.get_grayscale_data()
              gm_state = get_status(gm_val_list)

              print(f"  L={gm_val_list[0]:>5.0f} "
                    f"C={gm_val_list[1]:>5.0f} "
                    f"R={gm_val_list[2]:>5.0f}  status={gm_state}")

              if gm_state != "stop":
                  last_state = gm_state

              if gm_state == 'forward':
                  px.set_dir_servo_angle(0)
                  px.forward(px_power)
              elif gm_state == 'left':
                  px.set_dir_servo_angle(offset)
                  px.forward(px_power)
              elif gm_state == 'right':
                  px.set_dir_servo_angle(-offset)
                  px.forward(px_power)
              else:
                  outHandle()

      except KeyboardInterrupt:
          pass
      finally:
          px.stop()
          px.set_dir_servo_angle(0)
          print("\n  Stopped.\n")


  if __name__ == "__main__":
      print("=" * 50)
      print("  PiCar-X — Line Follower")
      print("=" * 50)
      print(f"\n  Line reference: {px.line_reference}")
      print("  (Set during Day 1 grayscale calibration)\n")

      while True:
          print("  [1] Live sensor view (no movement)")
          print("  [2] Follow the line")
          print("  [q] Quit\n")

          choice = input("  Pick 1-2 or q: ").strip().lower()
          if choice == '1': live_sensor_view()
          elif choice == '2': follow_line()
          elif choice in ('q', 'quit'): break
          else: print("  Enter 1, 2, or q.\n")

      px.stop()
      print("  Bye!\n")
  ```
</Accordion>

***

## Why this is not AI

<Note>
  The line follower uses **explicit rules** you wrote in `get_status()`. The robot cannot learn, adapt, or handle a track it was not programmed for. If you change the tape color or surface, you need to re-calibrate manually.

  AI replaces these hand-written rules with **learned patterns**. That is exactly what you will explore on Days 3 and 4.
</Note>

***

## Experiment

1. **Change the speed**: edit `px_power = 10` to `20` or `30`. At what speed does the robot start losing the line?
2. **Widen the steering**: change `offset = 20` to `30`. Does it handle sharper curves?
3. **90° corners**: make a right-angle turn in your tape. Can the robot handle it? Watch the backup recovery kick in.

<Tip>
  Speed vs. tracking accuracy is a real engineering trade-off. Professional line-following robots use the same balance — faster means less time to react to sensor changes.
</Tip>
