📡 Level 3 – Advanced Communication

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.
On this page