Project 3.8: "Remote-Controlled Robot 2.0"
What you’ll learn
- Goal 1: Build a direct remote controller (R32 + joystick) that commands a robot (R32 + motors) via Bluetooth.
- Goal 2: Implement robot status feedback (ACK and mode reports) for confident control.
- Goal 3: Add compensated delay to keep controls responsive without flooding Bluetooth.
- 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: Buttons map to directions/modes; Bluetooth carries text commands reliably.
- Feedback: Robot replies with short ACKs; controller 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
- Bluetooth central: Scans/connects to a peripheral and sends text commands.
- Bluetooth peripheral: Advertises a name and receives/sends text.
- Callback (receive): Function triggered automatically on incoming data.
- Digital input (pull‑up): Reads joystick buttons A–F (pressed = 0).
- Digital output: Sets motor direction pins (IN1–IN4).
- PWM output: Sets motor speed (ENA/ENB duty).
- def function: Encapsulates reusable actions (send_cmd, apply_mode, pulse_move).
- Loop: Continuous control cycle with friendly timing and logs.
What you need
Controller R32:
- Joystick Shield buttons: A(26), B(25), C(17), D(16), E(27), F(14)
- Bluetooth: Built-in (central role)
Robot R32:
- Bluetooth: Built-in (peripheral role, advertised name “Robot-R32”)
- L298N connections: ENA → Pin 5 (PWM), ENB → Pin 18 (PWM), IN1 → 23, IN2 → 19, IN3 → 13, IN4 → 21
- TT Motors: Wired to L298N outputs (OUT1/OUT2 left, OUT3/OUT4 right)
Shared:
- Power: USB to each board, battery to L298N per kit
- Ground: Share GND between robot R32 and L298N
Wiring tips
- Bluetooth: No physical pins; keep boards within 1–3 meters for reliable connection.
- Motors: Double-check IN1–IN4 direction mapping to avoid reversed motion.
Before you start
- Open serial monitors: One for the controller, one for the robot.
- Quick serial test:
print("Ready!") # Confirm serial is working
Microprojects 1–5
Microproject 3.9.1 – Direct remote control (Bluetooth text commands)
Goal: Controller sends text commands; robot decodes and drives.
Controller (Central) code:
# Microproject 3.9.1 – Controller (Central): send Bluetooth text commands
import machine # Load hardware pin library
import ble_central # Load Bluetooth central helper
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 → FORWARD
pinB = machine.Pin(25, machine.Pin.IN, machine.Pin.PULL_UP) # Button B → BACKWARD
pinC = machine.Pin(17, machine.Pin.IN, machine.Pin.PULL_UP) # Button C → LEFT
pinD = machine.Pin(16, machine.Pin.IN, machine.Pin.PULL_UP) # Button D → RIGHT
print("[Controller] Buttons A–D ready") # Serial: confirm button setup
central = ble_central.BLESimpleCentral() # Create Bluetooth central object
print("[Controller] Central ready") # Serial: central created
def connect_robot(): # Helper: connect to 'Robot-R32'
central.scan() # Start scanning for peripherals
time.sleep_ms(500) # Short wait to gather devices
central.connect('Robot-R32') # Connect to robot peripheral by name
print("[Controller] Connected to Robot-R32") # Serial: connection successful
def send_cmd(text): # Helper: send a text command
central.send(text) # Send text to the robot
print("[Controller] TX:", text) # Serial: log the send
connect_robot() # Establish Bluetooth connection
while True: # Main control loop
if pinA.value() == 0: # If A pressed (LOW)
send_cmd("CMD:FORWARD") # Send forward command
time.sleep_ms(250) # Debounce spacing
if pinB.value() == 0: # If B pressed (LOW)
send_cmd("CMD:BACKWARD") # Send backward command
time.sleep_ms(250) # Debounce spacing
if pinC.value() == 0: # If C pressed (LOW)
send_cmd("CMD:LEFT") # Send left command
time.sleep_ms(250) # Debounce spacing
if pinD.value() == 0: # If D pressed (LOW)
send_cmd("CMD:RIGHT") # Send right command
time.sleep_ms(250) # Debounce spacing
Robot (Peripheral) code:
# Microproject 3.9.1 – Robot (Peripheral): decode commands and drive motors
import ble_peripheral # Load Bluetooth peripheral helper
import ble_handle # Load Bluetooth callback helper
import machine # Load hardware pin/PWM library
import time # Load time library
# Create Bluetooth peripheral named 'Robot-R32'
ble_p = ble_peripheral.BLESimplePeripheral('Robot-R32') # Peripheral with name
handle = ble_handle.Handle() # Create callback handle
print("[Robot] BLE 'Robot-R32' ready") # Serial: Bluetooth init
# Prepare motor direction pins (L298N)
in1 = machine.Pin(23, machine.Pin.OUT) # IN1 left motor
in2 = machine.Pin(19, machine.Pin.OUT) # IN2 left motor
in3 = machine.Pin(13, machine.Pin.OUT) # IN3 right motor
in4 = machine.Pin(21, machine.Pin.OUT) # IN4 right motor
print("[Robot] Direction pins set 23,19,13,21") # Serial: pins ready
# Prepare PWM speed pins
pwmA = machine.PWM(machine.Pin(5)) # ENA left motor PWM
pwmB = machine.PWM(machine.Pin(18)) # ENB right motor PWM
pwmA.freq(2000) # Set PWM frequency 2kHz
pwmB.freq(2000) # Set PWM frequency 2kHz
speed = 650 # Default medium duty
pwmA.duty(speed) # Apply left duty
pwmB.duty(speed) # Apply right duty
print("[Robot] Speed duty =", speed) # Serial: speed init
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 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
def handle_method(msg): # Callback: when a message arrives
s = str(msg) # Convert to string
print("[Robot] RX:", s) # Serial: show incoming text
if s == "CMD:FORWARD": # If forward command
drive_forward() # Drive forward
ble_p.send("ACK:FORWARD") # Send ACK
elif s == "CMD:BACKWARD": # If backward command
drive_backward() # Drive backward
ble_p.send("ACK:BACKWARD") # Send ACK
elif s == "CMD:LEFT": # If left command
turn_left() # Spin left
ble_p.send("ACK:LEFT") # Send ACK
elif s == "CMD:RIGHT": # If right command
turn_right() # Spin right
ble_p.send("ACK:RIGHT") # Send ACK
elif s == "CMD:STOP": # If stop command
stop_all() # Stop motors
ble_p.send("ACK:STOP") # Send ACK
else: # Unknown command
ble_p.send("ERR:UNKNOWN") # Send error feedback
print("[Robot] Unknown command") # Serial: warn
handle.recv(handle_method) # Register the receive callback
print("[Robot] Callback registered") # Serial: callback active
while True: # Idle loop (actions via callback)
time.sleep_ms(200) # Short sleep to keep CPU cool
Reflection: You’ve built the basic “controller → robot” pipeline over Bluetooth.
Challenge: Add STOP on button F (controller) and handle it on the robot.
Microproject 3.9.2 – Robot status feedback (ACKs)
Goal: Robot sends concise ACKs; controller prints them to confirm actions.
Controller (Central) — receive ACKs:
# Microproject 3.9.2 – Controller: print ACKs from the robot
import ble_central # Load Bluetooth central helper
import ble_handle # Load Bluetooth handle helper
central = ble_central.BLESimpleCentral() # Create central object
handle = ble_handle.Handle() # Create handle for callbacks
print("[Controller] Central + handle ready") # Serial: init
def log_rx(msg): # Helper: log received ACKs
print("[Controller] RX:", msg) # Serial: show incoming text
def handle_method(msg): # Callback: when robot sends data
log_rx(msg) # Log the received message
handle.recv(handle_method) # Register receive callback
print("[Controller] Callback registered") # Serial: active
Call this after connecting to “Robot-R32” in Microproject 3.9.1.
Reflection: ACK builds trust — students see actions confirmed.
Challenge: Print “ERROR” in red on LCD (if available) when “ERR:UNKNOWN” appears.
Microproject 3.9.3 – Compensated delay control (adaptive spacing)
Goal: Controller spaces commands adaptively to avoid flooding Bluetooth while staying responsive.
Controller (Central) — adaptive spacing:
# Microproject 3.9.3 – Controller: adaptive spacing for Bluetooth commands
import machine # Load hardware pin library
import ble_central # Load Bluetooth central helper
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
central = ble_central.BLESimpleCentral() # Create central object
print("[Controller] Central ready") # Serial: init
last_send_ms = 0 # Last send timestamp (ms)
base_gap_ms = 180 # Minimum gap between sends
extra_gap_ms = 0 # Additional gap (adaptive)
def now_ms(): # Helper: current ms
return time.ticks_ms() # Return ms ticks
def can_send(): # Helper: check spacing
elapsed = time.ticks_diff(now_ms(), last_send_ms) # Elapsed since last send
return elapsed >= (base_gap_ms + extra_gap_ms) # Compare to adaptive gap
def mark_sent(): # Helper: mark a send
global last_send_ms # Use global
last_send_ms = now_ms() # Update timestamp
def bump_gap(): # Helper: increase gap
global extra_gap_ms # Use global
extra_gap_ms = min(extra_gap_ms + 50, 300) # Increase up to 300 ms
def reduce_gap(): # Helper: reduce gap
global extra_gap_ms # Use global
extra_gap_ms = max(extra_gap_ms - 15, 0) # Reduce down to 0 ms
def send_cmd(text): # Helper: transmit command
central.send(text) # Send over Bluetooth
print("[Controller] TX:", text, # Serial: show TX and gap
"| gap", base_gap_ms + extra_gap_ms, "ms")
mark_sent() # Mark send time
reduce_gap() # Lightly reduce gap
while True: # Control loop
if pinA.value() == 0 and can_send(): # If A pressed and spaced
send_cmd("CMD:FORWARD") # Send forward
if pinB.value() == 0 and can_send(): # If B pressed and spaced
send_cmd("CMD:BACKWARD") # Send backward
if pinC.value() == 0 and can_send(): # If C pressed and spaced
send_cmd("CMD:LEFT") # Send left
if pinD.value() == 0 and can_send(): # If D pressed and spaced
send_cmd("CMD:RIGHT") # Send right
if pinF.value() == 0 and can_send(): # If F pressed and spaced
send_cmd("CMD:STOP") # Send stop
bump_gap() # Increase gap after STOP
time.sleep_ms(200) # Debounce
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 print it once per second.
Microproject 3.9.4 – Assisted automatic modes (Turbo, Quiet, Normal, Stop)
Goal: E toggles speed modes; robot adjusts PWM; ACK confirms mode.
Controller (Central) — send modes:
# Microproject 3.9.4 – Controller: toggle assisted modes via Bluetooth
import machine # Load hardware pin library
import ble_central # Load Bluetooth central helper
import time # Load time library
pinE = machine.Pin(27, machine.Pin.IN, machine.Pin.PULL_UP) # Button E (mode toggle)
central = ble_central.BLESimpleCentral() # Create central object
print("[Controller] Mode toggle ready") # Serial: init
modes = ["MODE:NORMAL", "MODE:TURBO", "MODE:QUIET", "MODE:STOP"] # Mode cycle
idx = 0 # Current mode index
def send_mode(text): # Helper: send a mode message
central.send(text) # Send mode over Bluetooth
print("[Controller] TX:", text) # Serial: log send
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(250) # Debounce delay
time.sleep_ms(40) # Short loop sleep
Robot (Peripheral) — apply modes:
# Microproject 3.9.4 – Robot: apply PWM speed modes and ACK
import ble_peripheral # Load Bluetooth peripheral helper
import ble_handle # Load Bluetooth callback helper
import machine # Load hardware PWM/pin library
import time # Load time library
ble_p = ble_peripheral.BLESimplePeripheral('Robot-R32') # Peripheral named Robot-R32
handle = ble_handle.Handle() # Callback handle
print("[Robot] BLE modes ready") # Serial: init
pwmA = machine.PWM(machine.Pin(5)) # Left PWM (ENA)
pwmB = machine.PWM(machine.Pin(18)) # Right PWM (ENB)
pwmA.freq(2000) # PWM frequency
pwmB.freq(2000) # PWM frequency
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
speed = duty # Update speed variable
pwmA.duty(speed) # Apply left duty
pwmB.duty(speed) # Apply right duty
print("[Robot] Duty =", speed) # Serial: log applied duty
def handle_method(msg): # Callback: mode messages
s = str(msg) # Convert to string
print("[Robot] RX:", s) # Serial: log RX
if s == "MODE:TURBO": # If turbo
apply_speed(1023) # Max duty
ble_p.send("ACK:MODE:TURBO") # ACK turbo
elif s == "MODE:QUIET": # If quiet
apply_speed(400) # Low duty
ble_p.send("ACK:MODE:QUIET") # ACK quiet
elif s == "MODE:NORMAL": # If normal
apply_speed(650) # Medium duty
ble_p.send("ACK:MODE:NORMAL") # ACK normal
elif s == "MODE:STOP": # If stop
apply_speed(0) # Duty 0
ble_p.send("ACK:MODE:STOP") # ACK stop
else: # Unknown
ble_p.send("ERR:UNKNOWN") # Send error feedback
handle.recv(handle_method) # Register callback
print("[Robot] Callback registered") # Serial: active
while True: # Idle loop
time.sleep_ms(200) # Short sleep
Reflection: Assisted modes simplify speed to a single toggle.
Challenge: Display current mode via LCD or log it once per second.
Microproject 3.9.5 – Precise position control (timed pulses, step counts)
Goal: Use pulse commands to move fixed “steps” forward/back; accumulate steps.
Robot (Peripheral) — pulse control:
# Microproject 3.9.5 – Robot: precise position via timed pulses over Bluetooth
import ble_peripheral # Load Bluetooth peripheral helper
import ble_handle # Load Bluetooth callback helper
import machine # Load hardware pin/PWM library
import time # Load time library
ble_p = ble_peripheral.BLESimplePeripheral('Robot-R32') # Peripheral named Robot-R32
handle = ble_handle.Handle() # Callback handle
print("[Robot] BLE pulse control 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 and duty for pulses
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
STEP_MS = 200 # Pulse duration per step (ms)
steps_count = 0 # Accumulated step counter
print("[Robot] STEP_MS =", STEP_MS) # Serial: pulse config
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
def handle_method(msg): # Callback: step commands
global steps_count # Use global step counter
s = str(msg) # Convert to string
print("[Robot] RX:", s) # Serial: show command
if s == "STEP:FWD": # If forward step
pulse_forward(STEP_MS) # Execute forward pulse
steps_count = steps_count + 1 # Increment counter
ble_p.send("ACK:STEPS=" + str(steps_count)) # ACK with step count
elif s == "STEP:BACK": # If backward step
pulse_backward(STEP_MS) # Execute backward pulse
steps_count = steps_count - 1 # Decrement counter
ble_p.send("ACK:STEPS=" + str(steps_count)) # ACK with step count
else: # Unknown
ble_p.send("ERR:UNKNOWN") # Error feedback
handle.recv(handle_method) # Register callback
print("[Robot] Callback registered") # Serial: active
while True: # Idle; actions via callback
time.sleep_ms(200) # Short sleep
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 over Bluetooth with feedback, compensation, modes, and precision
- Bluetooth central (controller): Map joystick buttons A–F to commands/modes/steps; connect to “Robot-R32” and send text.
- Bluetooth peripheral (robot): Decode text commands; drive motors (IN1–IN4 + ENA/ENB); send ACKs; support steps.
- Compensated delay (controller): Adaptive spacing between sends to avoid flooding.
- Precise pulses (robot): Use timed movement pulses to emulate position steps.
# Project 3.9 – Complete Bluetooth Remote-Controlled Robot
# Controller (Central): Joystick buttons → Bluetooth text commands with adaptive spacing and modes.
# Robot (Peripheral): Decode commands → Motor control + ACK feedback + pulse precision.
# ---------- CONTROLLER (R32 #1: Central) ----------
import machine # Load hardware pin library
import ble_central # Load Bluetooth central helper
import ble_handle # Load Bluetooth handle helper
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
print("[Controller] Buttons A–F ready") # Serial: confirm pins
central = ble_central.BLESimpleCentral() # Create central object
handle = ble_handle.Handle() # Create handle for receive callbacks
print("[Controller] Central + handle ready") # Serial: init
def connect_robot(): # Helper: connect to Robot-R32
central.scan() # Scan for peripherals
time.sleep_ms(600) # Short wait for scan
central.connect('Robot-R32') # Connect by name
print("[Controller] Connected to Robot-R32") # Serial: connection OK
def log_rx(msg): # Helper: log ACKs from robot
print("[Controller] RX:", msg) # Serial: show incoming text
def handle_method(msg): # Callback: when robot sends data
log_rx(msg) # Log ACKs and errors
handle.recv(handle_method) # Register receive callback
print("[Controller] Callback registered") # Serial: callback active
# Adaptive spacing state
last_send_ms = 0 # Last send timestamp (ms)
base_gap_ms = 180 # Base minimum gap
extra_gap_ms = 0 # Adaptive extra gap
print("[Controller] Adaptive gap base =", base_gap_ms) # Serial: gap info
def now_ms(): # Helper: current ms
return time.ticks_ms() # Return ms ticks
def can_send(): # Helper: check spacing
elapsed = time.ticks_diff(now_ms(), last_send_ms) # Elapsed since last send
return elapsed >= (base_gap_ms + extra_gap_ms) # Compare with adaptive gap
def mark_sent(): # Helper: mark a 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 - 15, 0) # Reduce down to 0 ms
def send_text(text): # Helper: transmit command/mode/step
central.send(text) # Send Bluetooth text
print("[Controller] TX:", text, # Serial: show TX and gap
"| gap", base_gap_ms + extra_gap_ms, "ms")
mark_sent() # Update last send time
reduce_gap() # Lightly reduce gap
# Mode cycle list
modes = ["MODE:NORMAL", "MODE:TURBO", "MODE:QUIET", "MODE:STOP"] # Mode strings
idx = 0 # Current mode index
connect_robot() # Connect to robot
print("[Controller] Control loop start") # Serial: start controller loop
while True: # Controller loop
if pinA.value() == 0 and can_send(): # If A pressed and spaced
send_text("CMD:FORWARD") # Forward command
if pinB.value() == 0 and can_send(): # If B pressed and spaced
send_text("CMD:BACKWARD") # Backward command
if pinC.value() == 0 and can_send(): # If C pressed and spaced
send_text("CMD:LEFT") # Left command
if pinD.value() == 0 and can_send(): # If D pressed and spaced
send_text("CMD:RIGHT") # Right command
if pinF.value() == 0 and can_send(): # If F pressed and spaced
send_text("CMD:STOP") # Stop command
bump_gap() # Increase gap to avoid flood
time.sleep_ms(200) # Debounce
if pinE.value() == 0 and can_send(): # If E pressed (mode cycle)
idx = (idx + 1) % len(modes) # Advance index
send_text(modes[idx]) # Send mode
time.sleep_ms(250) # Debounce
time.sleep_ms(40) # Short poll delay
# ---------- ROBOT (R32 #2: Peripheral) ----------
# Run this section on the robot board.
import ble_peripheral # Load Bluetooth peripheral helper
import ble_handle # Load Bluetooth callback helper
import machine # Load hardware pin/PWM library
import time # Load time library
ble_p = ble_peripheral.BLESimplePeripheral('Robot-R32') # Peripheral named Robot-R32
handle = ble_handle.Handle() # Callback handle
print("[Robot] BLE 'Robot-R32' ready") # Serial: Bluetooth 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
print("[Robot] Direction pins set") # Serial: pins ready
# PWM pins
pwmA = machine.PWM(machine.Pin(5)) # ENA PWM
pwmB = machine.PWM(machine.Pin(18)) # ENB PWM
pwmA.freq(2000) # PWM frequency 2kHz
pwmB.freq(2000) # PWM frequency 2kHz
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 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(text): # Helper: set PWM speed by mode
global speed # Use global speed variable
if text == "MODE:TURBO": # If turbo
speed = 1023 # Max duty
print("[Robot] MODE TURBO duty", speed) # Serial: turbo
ack("MODE:TURBO") # ACK turbo
elif text == "MODE:QUIET": # If quiet
speed = 400 # Low duty
print("[Robot] MODE QUIET duty", speed) # Serial: quiet
ack("MODE:QUIET") # ACK quiet
elif text == "MODE:NORMAL": # If normal
speed = 650 # Medium duty
print("[Robot] MODE NORMAL duty", speed) # Serial: normal
ack("MODE:NORMAL") # ACK normal
elif text == "MODE:STOP": # If stop
speed = 0 # Duty zero
print("[Robot] MODE STOP duty", speed) # Serial: stop
ack("MODE:STOP") # ACK stop
pwmA.duty(speed) # Apply left duty
pwmB.duty(speed) # Apply right duty
def handle_method(msg): # Callback: when the controller sends text
s = str(msg) # Ensure string
print("[Robot] RX:", s) # Serial: show command text
if s == "CMD:FORWARD": # Forward command
drive_forward() # Drive forward
ack("CMD:FORWARD") # ACK forward
elif s == "CMD:BACKWARD": # Backward command
drive_backward() # Drive backward
ack("CMD:BACKWARD") # ACK backward
elif s == "CMD:LEFT": # Left command
turn_left() # Spin left
ack("CMD:LEFT") # ACK left
elif s == "CMD:RIGHT": # Right command
turn_right() # Spin right
ack("CMD:RIGHT") # ACK right
elif s.startswith("MODE:"): # Mode command
apply_mode(s) # Apply mode
elif s == "CMD:STOP": # Stop command
stop_all() # Stop motors
ack("CMD:STOP") # ACK stop
elif s == "STEP:FWD": # Optional: step forward
# Could call pulse logic from Microproject 3.9.5
ack("STEP:FWD") # ACK step forward
elif s == "STEP:BACK": # Optional: step backward
# Could call pulse logic from Microproject 3.9.5
ack("STEP:BACK") # ACK step backward
else: # Unknown
ble_p.send("ERR:UNKNOWN") # Error feedback
print("[Robot] UNKNOWN") # Serial: warn
handle.recv(handle_method) # Register callback
print("[Robot] Callback registered") # Serial: active
while True: # Robot loop (actions via callback)
time.sleep_ms(150) # Short sleep for stability
External explanation
- What it teaches: A clean remote → robot architecture using Bluetooth text, with reliable commands, speed modes, adaptive spacing, and optional precise pulses.
- Why it works: Central sends short text messages; peripheral decodes and acts; PWM sets speeds; clear helpers keep behavior understandable; ACKs give confidence.
- Key concept: Separate concerns — controller focuses on inputs and command cadence; robot focuses on actuation and feedback.
Story time
Your rover now listens over radio. A quick tap sends “CMD:FORWARD,” the robot moves, and replies “ACK:FORWARD.” It’s like piloting a rover with a walkie‑talkie that also nods back.
Debugging (2)
Debugging 3.9.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 ~180 and extra_gap_ms <= 300
time.sleep_ms(40) # Fast polling; avoid long sleeps
# Robot: keep loop delay short
time.sleep_ms(150) # Responsive callback-driven design
Debugging 3.9.B – Incorrect feedback
- Symptom: ACK says TURBO but speed feels quiet.
- Check: Mode text mismatch or ACK sent before PWM applied.
- Fix:
# Ensure mode strings match exactly on both sides
# Apply PWM first, then send ACK so feedback matches reality
apply_mode("MODE:TURBO") # Set duty
ble_p.send("ACK:MODE:TURBO") # Confirm after changing
Final checklist
- Controller buttons A–D control directions; F stops; E cycles modes.
- Robot decodes Bluetooth text and moves correctly with IN1–IN4.
- PWM speed changes with Turbo/Quiet/Normal/Stop.
- Feedback logs and ACKs match actions.
- Adaptive spacing keeps control responsive without flooding.
Extras
- Student tip: Start at “Normal,” then test Turbo/Quiet to feel differences.
- Instructor tip: Have pairs swap roles — one controls, one watches ACKs/errors.
- Glossary:
- Central: Bluetooth role that connects and sends data.
- Peripheral: Bluetooth role that advertises and receives data.
- ACK: Short message confirming an action or mode.
- Mini tips:
- Keep messages short (CMD:…, MODE:…, STEP:…).
- Print concise logs; verbose prints slow loops.
- Maintain 1–3 m distance and clear line of sight for classroom reliability.