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
| Part | How many? | Pin connection (R32) |
|---|---|---|
| D1 R32 (Controller) | 1 | Joystick Shield buttons: A(26), B(25), C(17), D(16), E(27), F(14); IR TX → Pin 26 |
| D1 R32 (Robot) | 1 | IR RX → Pin 26; L298N: ENA → Pin 5 (PWM), ENB → Pin 18 (PWM), IN1 → 23, IN2 → 19, IN3 → 13, IN4 → 21 |
| TT Motors + L298N driver | 2 + 1 | Motors to L298N OUT1/OUT2 (left), OUT3/OUT4 (right) |
| Optional Bluetooth (robot) | 1 | Built-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.