📡 Level 3 – Advanced Communication

Project 3.8: "Remote-Controlled Robot"

 

🚀 What you’ll learn

  • ✅ Goal 1: Build a direct remote controller (R32 + Joystick) that commands a robot (R32 + motors) via IR.
  • ✅ Goal 2: Implement robot status feedback (ACK and mode reports) for confident control.
  • ✅ Goal 3: Add compensated delay to keep controls responsive without flooding signals.
  • ✅ Goal 4: Create assisted automatic modes (Turbo, Quiet, Normal, Stop) with friendly toggles.
  • ✅ Goal 5: Achieve precise position control using timed motor pulses and step counts.

Key ideas

  • Direct control: Joystick buttons map to directions and modes; IR carries commands to the robot.
  • Feedback: The robot replies with short acknowledgments over Bluetooth or prints clear serial logs.
  • Compensation: Small adaptive delays and command spacing keep control snappy.
  • Assistance: Modes reduce cognitive load — fast/quiet/stop at a tap.
  • Precision: Time-based pulses emulate step control without encoders.

🧱 Blocks glossary (used in this project)

  • Digital input (pull‑up): Reads joystick buttons A–F (pressed = 0).
  • Analog input (ADC): Reads joystick X/Y values (0–4095), optionally for speed.
  • IR send/receive: Transmit commands from controller to robot; decode on robot side.
  • Digital output: Set motor direction pins (IN1–IN4).
  • PWM output: Set motor speed (ENA/ENB duty).
  • Bluetooth peripheral: Optional robot feedback “ACK: …” to a PC (useful in class demos).
  • def function: Encapsulate reusable actions (send_cmd, apply_mode, pulse_move).
  • Loop: Continuous control cycle with friendly timing and logs.

🧰 What you need

PartHow many?Pin connection (R32)
D1 R32 (Controller)1Joystick Shield buttons: A(26), B(25), C(17), D(16), E(27), F(14); IR TX → Pin 26
D1 R32 (Robot)1IR RX → Pin 26; L298N: ENA → Pin 5 (PWM), ENB → Pin 18 (PWM), IN1 → 23, IN2 → 19, IN3 → 13, IN4 → 21
TT Motors + L298N driver2 + 1Motors to L298N OUT1/OUT2 (left), OUT3/OUT4 (right)
Optional Bluetooth (robot)1Built-in (no pins), for “ACK” feedback to a PC
  • Share GND between controller R32, robot R32, and L298N.
  • Aim the controller’s IR transmitter towards the robot’s IR receiver (20–50 cm clear line of sight).

✅ Before you start

  • Connect both R32 boards via USB and open two serial monitors: “Controller” and “Robot”.
  • Quick serial test:
print("Ready!")  # Confirm serial is working

🎮 Microprojects (5 mini missions)

🎮 Microproject 3.8.1 – Direct remote control

Goal: Map joystick buttons A–D to robot directions: Forward, Backward, Left, Right (IR).

Controller code (A–D → IR):

# Microproject 3.8.1 – Controller: A–D buttons send IR direction commands

import machine                                    # Load hardware pin library
import irremote                                   # Load IR communication library
import time                                       # Load time library for delays

# Prepare joystick button inputs (active LOW with pull-up)
pinA = machine.Pin(26, machine.Pin.IN, machine.Pin.PULL_UP)  # Button A
pinB = machine.Pin(25, machine.Pin.IN, machine.Pin.PULL_UP)  # Button B
pinC = machine.Pin(17, machine.Pin.IN, machine.Pin.PULL_UP)  # Button C
pinD = machine.Pin(16, machine.Pin.IN, machine.Pin.PULL_UP)  # Button D

# Prepare IR transmitter on Pin 26 (power 100%, not inverted)
ir_tx = irremote.NEC_TX(26, False, 100)                       # IR TX setup
print("[Controller] IR TX ready on 26")                       # Serial: IR init

# Canonical direction codes
CODE_FORWARD  = 0x18                                          # Forward
CODE_BACKWARD = 0x52                                          # Backward
CODE_LEFT     = 0x08                                          # Left
CODE_RIGHT    = 0x5A                                          # Right

def send_cmd(code):                                           # Helper: send IR command
    ir_tx.transmit(0x00, code, 0x00)                          # Send code (addr=0x00, ctrl=0x00)
    print("[Controller] CMD:", hex(code))                     # Serial: log send

while True:                                                   # Main control loop
    if pinA.value() == 0:                                     # If A pressed (LOW)
        send_cmd(CODE_FORWARD)                                # Send forward
        time.sleep_ms(250)                                    # Debounce spacing
    if pinB.value() == 0:                                     # If B pressed (LOW)
        send_cmd(CODE_BACKWARD)                               # Send backward
        time.sleep_ms(250)                                    # Debounce spacing
    if pinC.value() == 0:                                     # If C pressed (LOW)
        send_cmd(CODE_LEFT)                                   # Send left
        time.sleep_ms(250)                                    # Debounce spacing
    if pinD.value() == 0:                                     # If D pressed (LOW)
        send_cmd(CODE_RIGHT)                                  # Send right
        time.sleep_ms(250)                                    # Debounce spacing

Robot code (decode A–D):

# Microproject 3.8.1 – Robot: Decode IR directions and drive motors

import irremote                                   # Load IR communication library
import machine                                    # Load hardware pin/PWM library
import time                                       # Load time library

# Prepare IR receiver on Pin 26 with buffer size 8
ir_rx = irremote.NEC_RX(26, 8)                    # IR RX setup
print("[Robot] IR RX ready on 26")                # Serial: IR init

# Prepare motor direction pins (L298N)
in1 = machine.Pin(23, machine.Pin.OUT)            # IN1 left
in2 = machine.Pin(19, machine.Pin.OUT)            # IN2 left
in3 = machine.Pin(13, machine.Pin.OUT)            # IN3 right
in4 = machine.Pin(21, machine.Pin.OUT)            # IN4 right
print("[Robot] Direction pins set 23,19,13,21")   # Serial: pins ready

# Prepare PWM speed pins
pwmA = machine.PWM(machine.Pin(5))                # ENA left PWM
pwmB = machine.PWM(machine.Pin(18))               # ENB right PWM
pwmA.freq(2000)                                   # Set PWM frequency 2kHz
pwmB.freq(2000)                                   # Set PWM frequency 2kHz
speed = 650                                       # Default medium speed
pwmA.duty(speed)                                  # Apply left duty
pwmB.duty(speed)                                  # Apply right duty
print("[Robot] Speed duty =", speed)              # Serial: speed init

# Canonical direction codes (must match controller)
CODE_FORWARD  = 0x18                              # Forward
CODE_BACKWARD = 0x52                              # Backward
CODE_LEFT     = 0x08                              # Left
CODE_RIGHT    = 0x5A                              # Right

def stop_all():                                   # Helper: stop motors (coast)
    in1.value(0)                                  # Left IN1 OFF
    in2.value(0)                                  # Left IN2 OFF
    in3.value(0)                                  # Right IN3 OFF
    in4.value(0)                                  # Right IN4 OFF
    print("[Robot] STOP")                         # Serial: stopped

def drive_forward():                              # Helper: forward direction
    in1.value(1)                                  # Left forward ON
    in2.value(0)                                  # Left backward OFF
    in3.value(1)                                  # Right forward ON
    in4.value(0)                                  # Right backward OFF
    print("[Robot] FORWARD")                      # Serial: forward

def drive_backward():                             # Helper: backward direction
    in1.value(0)                                  # Left forward OFF
    in2.value(1)                                  # Left backward ON
    in3.value(0)                                  # Right forward OFF
    in4.value(1)                                  # Right backward ON
    print("[Robot] BACKWARD")                     # Serial: backward

def turn_left():                                  # Helper: spin left
    in1.value(0)                                  # Left forward OFF
    in2.value(1)                                  # Left backward ON
    in3.value(1)                                  # Right forward ON
    in4.value(0)                                  # Right backward OFF
    print("[Robot] LEFT")                         # Serial: left turn

def turn_right():                                 # Helper: spin right
    in1.value(1)                                  # Left forward ON
    in2.value(0)                                  # Left backward OFF
    in3.value(0)                                  # Right forward OFF
    in4.value(1)                                  # Right backward ON
    print("[Robot] RIGHT")                        # Serial: right turn

while True:                                       # Main decode loop
    if ir_rx.any():                               # If any IR code arrived
        code = ir_rx.code[0]                      # Read first buffered code
        print("[Robot] IR:", hex(code))           # Serial: show code
        if code == CODE_FORWARD:                  # If forward
            drive_forward()                       # Drive forward
        elif code == CODE_BACKWARD:               # If backward
            drive_backward()                      # Drive backward
        elif code == CODE_LEFT:                   # If left
            turn_left()                           # Spin left
        elif code == CODE_RIGHT:                  # If right
            turn_right()                          # Spin right
        else:                                     # Otherwise unknown
            stop_all()                            # Stop safely
    else:                                         # If no IR this cycle
        # Optional idle behavior: keep last state or stop
        pass                                      # Do nothing to maintain current motion
    time.sleep_ms(150)                            # Short loop delay for responsiveness

Reflection: You’ve built the basic “remote → robot” pipeline.
Challenge: Add STOP on button F (controller) and decode it on robot.


🎮 Microproject 3.8.2 – Robot status feedback

Goal: Robot sends “ACK” status back (Bluetooth to PC) and prints concise serial logs on actions.

Robot feedback code (optional BLE):

# Microproject 3.8.2 – Robot: Bluetooth ACK feedback for actions

import ble_peripheral                              # Load Bluetooth peripheral helper
import ble_handle                                  # Load Bluetooth callback handle

# Create Bluetooth peripheral named 'Robot-R32'
ble_p = ble_peripheral.BLESimplePeripheral('Robot-R32')  # BLE peripheral
handle = ble_handle.Handle()                              # BLE handle
print("[Robot] BLE 'Robot-R32' ready")                   # Serial: BLE init

def ack(label):                                          # Helper: send ACK feedback
    ble_p.send("ACK:" + str(label))                      # Send acknowledgment string
    print("[Robot] ACK:", label)                         # Serial: log ACK

How to use: call ack("FORWARD"), ack("LEFT"), or ack("MODE:TURBO") after executing actions in the robot code.
Reflection: ACK builds trust — students see actions confirmed.
Challenge: Add error feedback “ERR:UNKNOWN” for unknown IR codes.


🎮 Microproject 3.8.3 – Compensated delay control

Goal: Controller spaces commands adaptively to avoid flooding and keep responsive control.

Controller code (adaptive spacing):

# Microproject 3.8.3 – Controller: adaptive spacing for IR commands

import machine                                    # Load hardware pin library
import irremote                                   # Load IR communication library
import time                                       # Load time library

# Inputs (A–D directions, F stop)
pinA = machine.Pin(26, machine.Pin.IN, machine.Pin.PULL_UP)  # A
pinB = machine.Pin(25, machine.Pin.IN, machine.Pin.PULL_UP)  # B
pinC = machine.Pin(17, machine.Pin.IN, machine.Pin.PULL_UP)  # C
pinD = machine.Pin(16, machine.Pin.IN, machine.Pin.PULL_UP)  # D
pinF = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_UP)  # F

# IR TX
ir_tx = irremote.NEC_TX(26, False, 100)                       # IR transmitter
print("[Controller] IR TX adaptive")                          # Serial: init

# Codes
CODE_FORWARD  = 0x18                                          # Forward
CODE_BACKWARD = 0x52                                          # Backward
CODE_LEFT     = 0x08                                          # Left
CODE_RIGHT    = 0x5A                                          # Right
CODE_STOP     = 0x19                                          # Stop

# Adaptive timing state
last_send_ms = 0                                              # Last send timestamp (ms)
base_gap_ms  = 200                                            # Minimum gap between sends
extra_gap_ms = 0                                              # Additional gap (adaptive)

def now_ms():                                                 # Helper: current ms
    return time.ticks_ms()                                    # Return ticks in ms

def can_send():                                               # Helper: check spacing
    elapsed = time.ticks_diff(now_ms(), last_send_ms)         # Compute ms since last send
    return elapsed >= (base_gap_ms + extra_gap_ms)            # Compare with adaptive gap

def sent():                                                   # Helper: mark a send
    global last_send_ms                                       # Use global timestamp
    last_send_ms = now_ms()                                   # Update last send time

def bump_gap():                                               # Helper: increase adaptive gap
    global extra_gap_ms                                       # Use global gap
    extra_gap_ms = min(extra_gap_ms + 50, 300)                # Increase up to 300 ms

def reduce_gap():                                             # Helper: reduce adaptive gap
    global extra_gap_ms                                       # Use global gap
    extra_gap_ms = max(extra_gap_ms - 20, 0)                  # Reduce down to 0 ms

def send_cmd(code):                                           # Helper: transmit IR command
    ir_tx.transmit(0x00, code, 0x00)                          # Send code
    print("[Controller] CMD:", hex(code),                     # Serial: log cmd and gap
          "| gap", base_gap_ms + extra_gap_ms, "ms")
    sent()                                                    # Mark send
    reduce_gap()                                              # Slightly reduce gap on successful send

while True:                                                   # Control loop
    if pinA.value() == 0 and can_send():                      # If A pressed and spacing ok
        send_cmd(CODE_FORWARD)                                # Send forward
    if pinB.value() == 0 and can_send():                      # If B pressed and spacing ok
        send_cmd(CODE_BACKWARD)                               # Send backward
    if pinC.value() == 0 and can_send():                      # If C pressed and spacing ok
        send_cmd(CODE_LEFT)                                   # Send left
    if pinD.value() == 0 and can_send():                      # If D pressed and spacing ok
        send_cmd(CODE_RIGHT)                                  # Send right
    if pinF.value() == 0 and can_send():                      # If F pressed and spacing ok
        send_cmd(CODE_STOP)                                   # Send stop
        bump_gap()                                            # Increase adaptive gap after STOP
    time.sleep_ms(40)                                         # Short polling delay

Reflection: Adaptive spacing balances responsiveness with reliability.
Challenge: Show current gap on an LCD (Project 3.3) or on serial once per second.


🎮 Microproject 3.8.4 – Assisted automatic modes

Goal: E toggles speed modes (Turbo, Quiet, Normal, Stop), robot applies PWM.

Controller code (mode sends):

# Microproject 3.8.4 – Controller: E toggles assisted modes via IR

import machine                                    # Load hardware pin library
import irremote                                   # Load IR library
import time                                       # Load time library

pinE = machine.Pin(27, machine.Pin.IN, machine.Pin.PULL_UP)  # Button E (mode toggle)
ir_tx = irremote.NEC_TX(26, False, 100)                       # IR transmitter
print("[Controller] Mode toggle ready")                       # Serial: init

MODE_TURBO  = 0x55                                           # Turbo mode code
MODE_QUIET  = 0x16                                           # Quiet mode code
MODE_NORMAL = 0x46                                           # Normal mode code
MODE_STOP   = 0x19                                           # Stop mode code

modes = [MODE_NORMAL, MODE_TURBO, MODE_QUIET, MODE_STOP]     # Mode cycle
idx   = 0                                                    # Current mode index

def send_mode(code):                                         # Helper: send IR mode
    ir_tx.transmit(0x00, code, 0x00)                         # Send mode code
    print("[Controller] MODE:", hex(code))                   # Serial: log mode

while True:                                                  # Toggle loop
    if pinE.value() == 0:                                    # If E pressed
        idx = (idx + 1) % len(modes)                         # Advance mode index
        send_mode(modes[idx])                                # Send new mode
        time.sleep_ms(300)                                   # Debounce delay
    time.sleep_ms(40)                                        # Short loop sleep

Robot code (apply modes):

# Microproject 3.8.4 – Robot: apply PWM speed modes

import irremote                                   # Load IR library
import machine                                    # Load hardware PWM/pin library
import time                                       # Load time library

ir_rx = irremote.NEC_RX(26, 8)                    # IR receiver
pwmA = machine.PWM(machine.Pin(5))                # Left PWM
pwmB = machine.PWM(machine.Pin(18))               # Right PWM
pwmA.freq(2000)                                   # PWM frequency 2kHz
pwmB.freq(2000)                                   # PWM frequency 2kHz

MODE_TURBO  = 0x55                                 # Turbo code
MODE_QUIET  = 0x16                                 # Quiet code
MODE_NORMAL = 0x46                                 # Normal code
MODE_STOP   = 0x19                                 # Stop code

speed = 650                                        # Default duty
pwmA.duty(speed)                                   # Apply left duty
pwmB.duty(speed)                                   # Apply right duty
print("[Robot] Start duty =", speed)               # Serial: speed init

def apply_speed(duty):                             # Helper: set both PWMs
    global speed                                   # Use global speed var
    speed = duty                                   # Update stored speed
    pwmA.duty(speed)                               # Apply left duty
    pwmB.duty(speed)                               # Apply right duty
    print("[Robot] Duty =", speed)                 # Serial: log applied duty

def emergency_stop():                              # Helper: set duty to 0
    pwmA.duty(0)                                   # Left duty 0
    pwmB.duty(0)                                   # Right duty 0
    print("[Robot] STOP duty=0")                   # Serial: stop

while True:                                        # Mode loop
    if ir_rx.any():                                # If any IR code available
        code = ir_rx.code[0]                       # Read first buffered code
        print("[Robot] IR MODE:", hex(code))       # Serial: show mode code
        if code == MODE_TURBO:                     # If turbo
            apply_speed(1023)                      # Max duty
        elif code == MODE_QUIET:                   # If quiet
            apply_speed(400)                       # Low duty
        elif code == MODE_NORMAL:                  # If normal
            apply_speed(650)                       # Medium duty
        elif code == MODE_STOP:                    # If stop
            emergency_stop()                       # Stop motors
    time.sleep_ms(150)                             # Short loop delay

Reflection: Assisted modes simplify speed control to a single button.
Challenge: Display current mode via Bluetooth ACK from the robot (Microproject 3.8.2).


🎮 Microproject 3.8.5 – Precise position control

Goal: Use timed pulses to move a fixed “step” distance; accumulate steps for precision.

Robot code (timed pulses):

# Microproject 3.8.5 – Robot: precise position via timed pulses (no encoders)

import irremote                                   # Load IR library
import machine                                    # Load hardware pin/PWM library
import time                                       # Load time library

# IR RX
ir_rx = irremote.NEC_RX(26, 8)                    # IR receiver
print("[Robot] Precision pulse ready")            # Serial: init

# Motor pins
in1 = machine.Pin(23, machine.Pin.OUT)            # IN1 left
in2 = machine.Pin(19, machine.Pin.OUT)            # IN2 left
in3 = machine.Pin(13, machine.Pin.OUT)            # IN3 right
in4 = machine.Pin(21, machine.Pin.OUT)            # IN4 right

# PWM pins
pwmA = machine.PWM(machine.Pin(5))                # ENA left
pwmB = machine.PWM(machine.Pin(18))               # ENB right
pwmA.freq(2000)                                   # PWM frequency
pwmB.freq(2000)                                   # PWM frequency
pwmA.duty(700)                                    # Duty for pulses
pwmB.duty(700)                                    # Duty for pulses

# Codes for pulse control (reuse direction codes for stepping)
CODE_STEP_FWD  = 0x18                              # Step forward
CODE_STEP_BACK = 0x52                              # Step backward
STEP_MS        = 200                               # Pulse duration per step (ms)
steps_count    = 0                                 # Accumulated step counter

def pulse_forward(ms):                             # Helper: one forward pulse
    in1.value(1)                                   # Left forward ON
    in2.value(0)                                   # Left backward OFF
    in3.value(1)                                   # Right forward ON
    in4.value(0)                                   # Right backward OFF
    time.sleep_ms(ms)                              # Hold forward for ms
    in1.value(0)                                   # Left forward OFF
    in3.value(0)                                   # Right forward OFF
    print("[Robot] Pulse FWD", ms, "ms")           # Serial: log pulse

def pulse_backward(ms):                            # Helper: one backward pulse
    in1.value(0)                                   # Left forward OFF
    in2.value(1)                                   # Left backward ON
    in3.value(0)                                   # Right forward OFF
    in4.value(1)                                   # Right backward ON
    time.sleep_ms(ms)                              # Hold backward for ms
    in2.value(0)                                   # Left backward OFF
    in4.value(0)                                   # Right backward OFF
    print("[Robot] Pulse BACK", ms, "ms")          # Serial: log pulse

while True:                                        # Precision control loop
    if ir_rx.any():                                # If any IR code available
        code = ir_rx.code[0]                       # Read first code
        print("[Robot] IR STEP:", hex(code))       # Serial: show step code
        if code == CODE_STEP_FWD:                  # If forward step
            pulse_forward(STEP_MS)                 # Execute pulse
            steps_count = steps_count + 1          # Increment step counter
            print("[Robot] Steps =", steps_count)  # Serial: log steps
        elif code == CODE_STEP_BACK:               # If backward step
            pulse_backward(STEP_MS)                # Execute pulse
            steps_count = steps_count - 1          # Decrement step counter
            print("[Robot] Steps =", steps_count)  # Serial: log steps
    time.sleep_ms(120)                             # Short loop delay

Reflection: Timed pulses let you “step” the robot forward/backward predictably.
Challenge: Add left/right stepping with smaller pulse time (STEP_MS = 150).


✨ Main project – Remote-controlled robot with feedback, compensation, modes, and precision

🔧 Blocks steps (with glossary)

  • Digital input + IR send (controller): Map joystick buttons A–F to direction, stop, and mode codes.
  • IR receive + motor control (robot): Decode and drive motors (IN1–IN4 + ENA/ENB).
  • Bluetooth ACK (robot): Optionally send “ACK:CMD/MODE” to a PC for status feedback.
  • Compensated delay (controller): Adaptive spacing between IR sends to avoid flooding.
  • Precise pulses (robot): Use timed movement pulses to emulate position steps.

🐍 MicroPython code (complete system)

# Project 3.8 – Remote-Controlled Robot (Controller + Robot)
# Controller: Joystick buttons → IR commands/modes with adaptive spacing.
# Robot: IR decode → Motor control + optional Bluetooth ACK + pulse precision.

# ---------- CONTROLLER (R32 #1) ----------
import machine                                    # Load hardware pin library
import irremote                                   # Load IR communication library
import time                                       # Load time library

# Buttons A–F (active LOW)
pinA = machine.Pin(26, machine.Pin.IN, machine.Pin.PULL_UP)  # A forward
pinB = machine.Pin(25, machine.Pin.IN, machine.Pin.PULL_UP)  # B backward
pinC = machine.Pin(17, machine.Pin.IN, machine.Pin.PULL_UP)  # C left
pinD = machine.Pin(16, machine.Pin.IN, machine.Pin.PULL_UP)  # D right
pinE = machine.Pin(27, machine.Pin.IN, machine.Pin.PULL_UP)  # E mode cycle
pinF = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_UP)  # F stop / step

# IR TX setup
ir_tx = irremote.NEC_TX(26, False, 100)                       # IR transmitter
print("[Controller] IR TX ready")                             # Serial: IR init

# Direction/step codes
CODE_FORWARD   = 0x18                                         # Forward
CODE_BACKWARD  = 0x52                                         # Backward
CODE_LEFT      = 0x08                                         # Left
CODE_RIGHT     = 0x5A                                         # Right
CODE_STOP      = 0x19                                         # Stop
# Mode codes
MODE_TURBO     = 0x55                                         # Turbo
MODE_QUIET     = 0x16                                         # Quiet
MODE_NORMAL    = 0x46                                         # Normal
# Adaptive timing state
last_send_ms   = 0                                            # Last send time
base_gap_ms    = 200                                          # Base min gap
extra_gap_ms   = 0                                            # Adaptive extra gap
# Mode cycle state
modes          = [MODE_NORMAL, MODE_TURBO, MODE_QUIET, CODE_STOP]  # Cycle list
idx            = 0                                            # Current index

def now_ms():                                                 # Helper: current ms
    return time.ticks_ms()                                    # Return ms ticks

def can_send():                                               # Helper: spacing check
    elapsed = time.ticks_diff(now_ms(), last_send_ms)         # Elapsed since last send
    return elapsed >= (base_gap_ms + extra_gap_ms)            # Compare against gap

def mark_sent():                                              # Helper: mark send time
    global last_send_ms                                       # Use global timestamp
    last_send_ms = now_ms()                                   # Update last send

def bump_gap():                                               # Helper: increase adaptive gap
    global extra_gap_ms                                       # Use global gap
    extra_gap_ms = min(extra_gap_ms + 50, 300)                # Increase up to 300 ms

def reduce_gap():                                             # Helper: reduce adaptive gap
    global extra_gap_ms                                       # Use global gap
    extra_gap_ms = max(extra_gap_ms - 20, 0)                  # Reduce down to 0 ms

def send_ir(code):                                            # Helper: send IR packet
    ir_tx.transmit(0x00, code, 0x00)                          # Transmit code
    print("[Controller] TX:", hex(code),                      # Serial: log send + gap
          "| gap", base_gap_ms + extra_gap_ms, "ms")
    mark_sent()                                               # Mark send
    reduce_gap()                                              # Lightly reduce gap after send

print("[Controller] Remote loop start")                       # Serial: start controller

# Controller loop (run on the CONTROLLER)
while True:                                                   # Main controller loop
    if pinA.value() == 0 and can_send():                      # If A pressed and spaced
        send_ir(CODE_FORWARD)                                 # Send forward
    if pinB.value() == 0 and can_send():                      # If B pressed and spaced
        send_ir(CODE_BACKWARD)                                # Send backward
    if pinC.value() == 0 and can_send():                      # If C pressed and spaced
        send_ir(CODE_LEFT)                                    # Send left
    if pinD.value() == 0 and can_send():                      # If D pressed and spaced
        send_ir(CODE_RIGHT)                                   # Send right
    if pinF.value() == 0 and can_send():                      # If F pressed and spaced
        send_ir(CODE_STOP)                                    # Send stop
        bump_gap()                                            # Increase gap to avoid flood
        time.sleep_ms(200)                                    # Debounce stop press
    if pinE.value() == 0 and can_send():                      # If E pressed (mode cycle)
        idx = (idx + 1) % len(modes)                          # Advance index
        send_ir(modes[idx])                                   # Send mode code
        time.sleep_ms(250)                                    # Debounce for mode switch
    time.sleep_ms(40)                                         # Short poll delay

# ---------- ROBOT (R32 #2) ----------
# NOTE: Run the following robot code on the second R32 board.

import irremote                                   # Load IR library
import machine                                    # Load hardware pin/PWM library
import time                                       # Load time library
# Optional Bluetooth feedback:
import ble_peripheral                              # Load Bluetooth peripheral helper
import ble_handle                                  # Load Bluetooth handle helper

# IR RX setup
ir_rx = irremote.NEC_RX(26, 8)                    # IR receiver on Pin 26
print("[Robot] IR RX ready")                      # Serial: IR initialized

# Motor pins (L298N)
in1 = machine.Pin(23, machine.Pin.OUT)            # IN1 left
in2 = machine.Pin(19, machine.Pin.OUT)            # IN2 left
in3 = machine.Pin(13, machine.Pin.OUT)            # IN3 right
in4 = machine.Pin(21, machine.Pin.OUT)            # IN4 right
print("[Robot] Direction pins set")               # Serial: direction pins ready

# PWM pins (L298N enables)
pwmA = machine.PWM(machine.Pin(5))                # ENA PWM
pwmB = machine.PWM(machine.Pin(18))               # ENB PWM
pwmA.freq(2000)                                   # PWM freq
pwmB.freq(2000)                                   # PWM freq
speed = 650                                       # Start medium duty
pwmA.duty(speed)                                  # Apply left duty
pwmB.duty(speed)                                  # Apply right duty
print("[Robot] Start duty =", speed)              # Serial: duty init

# Optional BLE ACK setup
ble_p = ble_peripheral.BLESimplePeripheral('Robot-R32')  # Bluetooth peripheral
handle = ble_handle.Handle()                              # Callback handle
print("[Robot] BLE 'Robot-R32' ready")                   # Serial: BLE initialized

# Codes (must match controller)
CODE_FORWARD   = 0x18                                    # Forward
CODE_BACKWARD  = 0x52                                    # Backward
CODE_LEFT      = 0x08                                    # Left
CODE_RIGHT     = 0x5A                                    # Right
CODE_STOP      = 0x19                                    # Stop
MODE_TURBO     = 0x55                                    # Turbo
MODE_QUIET     = 0x16                                    # Quiet
MODE_NORMAL    = 0x46                                    # Normal

def ack(label):                                          # Helper: send ACK via BLE
    ble_p.send("ACK:" + str(label))                      # Send ACK string
    print("[Robot] ACK:", label)                         # Serial: log ACK

def stop_all():                                          # Helper: stop motors
    in1.value(0)                                         # Left IN1 OFF
    in2.value(0)                                         # Left IN2 OFF
    in3.value(0)                                         # Right IN3 OFF
    in4.value(0)                                         # Right IN4 OFF
    print("[Robot] STOP")                                # Serial: stopped

def drive_forward():                                     # Helper: forward motion
    in1.value(1)                                         # Left forward ON
    in2.value(0)                                         # Left backward OFF
    in3.value(1)                                         # Right forward ON
    in4.value(0)                                         # Right backward OFF
    print("[Robot] FORWARD")                             # Serial: forward

def drive_backward():                                    # Helper: backward motion
    in1.value(0)                                         # Left forward OFF
    in2.value(1)                                         # Left backward ON
    in3.value(0)                                         # Right forward OFF
    in4.value(1)                                         # Right backward ON
    print("[Robot] BACKWARD")                            # Serial: backward

def turn_left():                                         # Helper: spin left
    in1.value(0)                                         # Left forward OFF
    in2.value(1)                                         # Left backward ON
    in3.value(1)                                         # Right forward ON
    in4.value(0)                                         # Right backward OFF
    print("[Robot] LEFT")                                # Serial: left turn

def turn_right():                                        # Helper: spin right
    in1.value(1)                                         # Left forward ON
    in2.value(0)                                         # Left backward OFF
    in3.value(0)                                         # Right forward OFF
    in4.value(1)                                         # Right backward ON
    print("[Robot] RIGHT")                               # Serial: right turn

def apply_mode(code):                                    # Helper: set PWM speed by mode
    global speed                                         # Use global speed variable
    if code == MODE_TURBO:                               # If turbo
        speed = 1023                                     # Max duty
        print("[Robot] MODE TURBO duty", speed)          # Serial: log turbo
        ack("MODE:TURBO")                                # ACK turbo
    elif code == MODE_QUIET:                             # If quiet
        speed = 400                                      # Low duty
        print("[Robot] MODE QUIET duty", speed)          # Serial: log quiet
        ack("MODE:QUIET")                                # ACK quiet
    elif code == MODE_NORMAL:                            # If normal
        speed = 650                                      # Medium duty
        print("[Robot] MODE NORMAL duty", speed)         # Serial: log normal
        ack("MODE:NORMAL")                               # ACK normal
    elif code == CODE_STOP:                              # If stop mode code used
        speed = 0                                        # Duty zero
        print("[Robot] MODE STOP duty", speed)           # Serial: log stop
        ack("MODE:STOP")                                 # ACK stop
    pwmA.duty(speed)                                     # Apply left duty
    pwmB.duty(speed)                                     # Apply right duty

print("[Robot] Control loop start")                       # Serial: start robot loop

# Robot loop (run on the ROBOT)
while True:                                               # Main robot loop
    if ir_rx.any():                                       # If code available
        code = ir_rx.code[0]                              # Read IR code
        print("[Robot] IR:", hex(code))                   # Serial: show code
        if code == CODE_FORWARD:                          # If forward
            drive_forward()                               # Drive forward
            ack("CMD:FORWARD")                            # ACK forward
        elif code == CODE_BACKWARD:                       # If backward
            drive_backward()                              # Drive backward
            ack("CMD:BACKWARD")                           # ACK backward
        elif code == CODE_LEFT:                           # If left
            turn_left()                                   # Spin left
            ack("CMD:LEFT")                               # ACK left
        elif code == CODE_RIGHT:                          # If right
            turn_right()                                  # Spin right
            ack("CMD:RIGHT")                              # ACK right
        elif code in (MODE_TURBO, MODE_QUIET, MODE_NORMAL, CODE_STOP):  # If mode
            apply_mode(code)                              # Apply mode
        elif code == CODE_STOP:                           # If explicit stop command
            stop_all()                                    # Stop motors
            ack("CMD:STOP")                               # ACK stop
        else:                                             # Unknown code
            print("[Robot] UNKNOWN:", hex(code))          # Serial: warn
            ble_p.send("ERR:UNKNOWN:" + hex(code))        # Send error feedback
    time.sleep_ms(150)                                    # Loop delay for responsiveness

📖 External explanation

  • What it teaches: A clean remote → robot architecture with reliable commands, modes, and feedback.
  • Why it works: IR is simple and robust for one-way control; PWM sets speeds; clear helpers keep behavior understandable; BLE ACKs give confidence.
  • Key concept: Separate concerns — controller focuses on inputs and command cadence; robot focuses on actuation and status.

✨ Story time

You’ve built a rover squad: the hand-held commander (controller R32) and the responsive rover (robot R32). A quick tap, a clean beam, a solid move — with a radio “ACK” to say “got it!”


🕵️ Debugging (2)

🐞 Debugging 3.8.A – Excessive delay

  • Symptom: Commands feel laggy; robot moves late.
  • Check: Controller’s adaptive gap too high or long sleeps; robot loop sleeps too long.
  • Fix:
# Controller: keep base_gap_ms ~200 and extra_gap_ms <= 300
time.sleep_ms(40)   # Poll fast; avoid long sleeps

# Robot: keep loop delay short
time.sleep_ms(150)  # Responsive read frequency

🐞 Debugging 3.8.B – Incorrect feedback

  • Symptom: ACK says TURBO but speed feels quiet.
  • Check: Mode codes mismatch or BLE ACK sent before PWM applied.
  • Fix:
# Ensure constants match on both boards
MODE_TURBO  = 0x55
MODE_QUIET  = 0x16
MODE_NORMAL = 0x46
CODE_STOP   = 0x19

# Apply PWM before ACK, or confirm after set:
apply_mode(code)    # Sets PWM
ack("MODE:TURBO")   # Then send ACK (order matters)

✅ Final checklist

  • Controller buttons A–D control directions; F stops; E cycles modes.
  • Robot decodes IR and moves correctly with IN1–IN4.
  • PWM speed changes with Turbo/Quiet/Normal/Stop.
  • Feedback logs and optional BLE ACKs match actions.
  • Adaptive spacing keeps control responsive without flooding.

📚 Extras

  • 🧠 Student tip: Start at “Normal” mode, then test Turbo/Quiet to feel the difference.
  • 🧑‍🏫 Instructor tip: Have pairs swap roles — one controls, one watches logs for ACKs/errors.
  • 📖 Glossary:
    • IR: Infrared light used for simple remote commands.
    • PWM duty: Percentage of power; controls speed.
    • ACK: Short message confirming an action or mode.
  • 💡 Mini tips:
    • Keep IR modules aligned and at 20–50 cm distance in bright rooms.
    • Print short logs only; verbose prints slow loops.
    • Share GND across controller, robot, and motor driver.
On this page