Project 5.7: "Robot with App Control"
What you’ll learn
- ✅ Motion by app: Drive the robot with app buttons (FWD, BACK, LEFT, RIGHT, STOP).
- ✅ Live data: Show robot status (MODE, SPEED, DIST) in the app using clean messages.
- ✅ Modes: Switch MANUAL/AUTO from the app.
- ✅ Record & replay: Save a path of commands and play it back.
- ✅ Group control: Target one robot or all robots using IDs in messages.
Key ideas
- Short definition: The app sends short words; the robot reads them and sets its motor pins.
- Real-world link: Delivery bots and RC cars follow commands and can repeat recorded routes.
Blocks glossary (used in this project)
- Bluetooth init + callback: Start BLE and run a function whenever a message arrives.
- Variables: Store last_command, mode, speed, robot_id, and a recorded path list.
- Digital outputs (motor pins): Control L298N inputs to spin motors forward/backward.
- PWM (optional): Change speed smoothly by setting duty on enable pins (if used).
- Serial println: Print status strings (e.g., MODE:MANUAL) the app can read.
What you need
| Part | How many? | Pin connection |
|---|---|---|
| D1 R32 | 1 | USB cable (30 cm) |
| L298N motor driver | 1 | Left: IN1→Pin 18, IN2→Pin 19; Right: IN3→Pin 5, IN4→Pin 23 |
| TT motors + wheels | 2 | Connect motor wires to L298N OUT1/OUT2 and OUT3/OUT4 |
| Smartphone (Bluetooth) | 1 | Connect to the robot named “Clu-Bots” |
🔌 Wiring tip: Keep motor wires tidy. Match left motor to L298N OUT1/OUT2 and right motor to OUT3/OUT4. Use Pin 18/19 for left motor control and Pin 5/23 for right motor control.
📍 Pin map snapshot: Using pins 18, 19, 5, 23 for motion. All other pins remain free for sensors or OLED.
Before you start
- USB cable is plugged in
- Serial monitor is open
- Test print shows:
print("Ready!") # Confirm serial is working
Microprojects (5 mini missions)
Microproject 5.7.1 – Motion control from app
Goal: Map app words to motor actions on L298N.
Blocks used:
- BLE init + callback: Receive “FWD/BACK/LEFT/RIGHT/STOP”.
- Digital outputs: Drive IN1–IN4 for motion.
Block sequence:
- Init BLE central+peripheral “Clu-Bots”; attach receive callback.
- Create motor pins (18,19 for left; 5,23 for right).
- On message, set pins for action and print status.
MicroPython code:
import ble_central # Import BLE central role to scan/connect
import ble_peripheral # Import BLE peripheral role to advertise a name
import ble_handle # Import handler to receive messages
import machine # Import machine to control pins
ble_c = ble_central.BLESimpleCentral() # Create central device
ble_p = ble_peripheral.BLESimplePeripheral('Clu-Bots') # Create peripheral named 'Clu-Bots'
print("Microproject 5.7.1: BLE ready (central+peripheral)") # Status message
ble_c.scan() # Start scanning for peripherals
print("Scanning for 'Clu-Bots'...") # Explain scanning
ble_c.connect(name='Clu-Bots') # Attempt to connect to our peripheral by name
print("Connecting to 'Clu-Bots'...") # Explain connection attempt
left_in1 = machine.Pin(18, machine.Pin.OUT) # Left motor IN1 on Pin 18
left_in2 = machine.Pin(19, machine.Pin.OUT) # Left motor IN2 on Pin 19
right_in3 = machine.Pin(5, machine.Pin.OUT) # Right motor IN3 on Pin 5
right_in4 = machine.Pin(23, machine.Pin.OUT) # Right motor IN4 on Pin 23
print("Motor pins ready (L:18/19, R:5/23)") # Confirm pin setup
last_command = "STOP" # Initialize last command to STOP
print("Initial last_command:", last_command) # Show initial state
def move_forward(): # Define function to move forward
left_in1.value(1) # Set left IN1 HIGH
left_in2.value(0) # Set left IN2 LOW
right_in3.value(1) # Set right IN3 HIGH
right_in4.value(0) # Set right IN4 LOW
print("MOTORS: FWD") # Print action
def move_backward(): # Define function to move backward
left_in1.value(0) # Set left IN1 LOW
left_in2.value(1) # Set left IN2 HIGH
right_in3.value(0) # Set right IN3 LOW
right_in4.value(1) # Set right IN4 HIGH
print("MOTORS: BACK") # Print action
def turn_left(): # Define function to turn left
left_in1.value(0) # Stop/Reverse left wheel for turning
left_in2.value(1) # Reverse left
right_in3.value(1) # Forward right
right_in4.value(0) # Keep right forward
print("MOTORS: LEFT") # Print action
def turn_right(): # Define function to turn right
left_in1.value(1) # Forward left
left_in2.value(0) # Keep left forward
right_in3.value(0) # Reverse/Stop right for turning
right_in4.value(1) # Reverse right
print("MOTORS: RIGHT") # Print action
def motors_stop(): # Define function to stop
left_in1.value(0) # Left IN1 LOW
left_in2.value(0) # Left IN2 LOW
right_in3.value(0) # Right IN3 LOW
right_in4.value(0) # Right IN4 LOW
print("MOTORS: STOP") # Print action
def handle_method(key1, key2, key3, keyx): # Define BLE receive callback
msg = str(key1) # Convert payload to text
print("APP->R32:", msg) # Show incoming command
global last_command # Declare global to update last_command
last_command = msg # Save the latest command
if msg == "FWD": # If forward
move_forward() # Call forward action
elif msg == "BACK": # If backward
move_backward() # Call backward action
elif msg == "LEFT": # If left
turn_left() # Call left action
elif msg == "RIGHT": # If right
turn_right() # Call right action
elif msg == "STOP": # If stop
motors_stop() # Call stop action
else: # Unrecognized command
print("UNKNOWN CMD:", msg) # Print unknown command
handle = ble_handle.Handle() # Create BLE handler object
handle.recv(handle_method) # Attach callback to receive messages
print("Callback attached (send FWD/BACK/LEFT/RIGHT/STOP)") # Guidance
Reflection: Clear words control clear actions—keep command names short and exact.
Challenge:
- Easy: Add “SPIN” that turns left for 1 second.
- Harder: Create “DIAG” that sets one wheel forward and the other stopped.
Microproject 5.7.2 – Visualization of sensor data in the app
Goal: Print clean status strings the app can display (MODE, SPEED, DIST).
Blocks used:
- Serial println: Output “KEY:VALUE” messages.
- Variables: Track mode and speed.
Block sequence:
- mode = MANUAL, speed = 60%.
- Print MODE:MANUAL and SPEED:60.
- Print DIST:— (simulated for now).
MicroPython code:
mode = "MANUAL" # Choose initial control mode
speed_pct = 60 # Choose initial speed percent (0-100)
print("Microproject 5.7.2: MODE:", mode) # App-friendly mode string
print("SPEED:", speed_pct) # App-friendly speed string
print("DIST:", 0) # Simulated distance value (replace with sensor later)
Reflection: Consistent formats help your app parse and display data without errors.
Challenge:
- Easy: Change SPEED to 80%.
- Harder: Print “ALERT:LOW_BAT” when speed_pct < 20.
Microproject 5.7.3 – Setting modes from the app
Goal: Allow the app to switch between MANUAL and AUTO.
Blocks used:
- Receive callback: Detect “MODE:MANUAL” or “MODE:AUTO”.
- Variables: Update mode and act accordingly.
Block sequence:
- Start in MANUAL.
- If “MODE:AUTO”, print and prepare auto behavior.
- If “MODE:MANUAL”, print and stop auto.
MicroPython code:
mode = "MANUAL" # Start in manual mode
print("Microproject 5.7.3: Start mode =", mode) # Status message
def handle_method(key1, key2, key3, keyx): # Define BLE callback for mode control
msg = str(key1) # Convert payload to text
print("APP->R32:", msg) # Show incoming message
global mode # Declare using global mode variable
if msg == "MODE:AUTO": # Check for AUTO mode command
mode = "AUTO" # Set mode to AUTO
print("MODE set to AUTO") # Confirm mode
elif msg == "MODE:MANUAL": # Check for MANUAL mode command
mode = "MANUAL" # Set mode to MANUAL
print("MODE set to MANUAL") # Confirm mode
else: # If message is not a mode command
print("No mode change") # Explain no change
Reflection: Modes let you switch strategies—your app is the remote brain.
Challenge:
- Easy: Add “MODE:SAFE” that forces STOP immediately.
- Harder: Add “SLOW/FAST” to change speed_pct.
Microproject 5.7.4 – Recording and playback of trajectories
Goal: Save incoming motion commands and replay them.
Blocks used:
- Variables + list: Store commands and timestamps.
- Loop: Step through recorded commands.
Block sequence:
- Create path = [].
- On FWD/BACK/LEFT/RIGHT, append (cmd, time).
- On “PLAY”, iterate and execute each one.
MicroPython code:
import time # Import time to store timestamps
path = [] # Create an empty list to record path commands
print("Microproject 5.7.4: Path recording ready") # Status message
def record_cmd(cmd): # Define function to add a command to the path
ts = time.ticks_ms() # Get current time in milliseconds
path.append((cmd, ts)) # Append tuple (command, timestamp)
print("RECORDED:", cmd, "@", ts) # Print what was recorded
def playback(): # Define function to play back recorded commands
print("PLAYBACK START") # Announce playback
for cmd, ts in path: # Iterate through recorded path
print("PLAY:", cmd, "from", ts) # Show which command is playing
# Here you would call motor functions matching the cmd
print("PLAYBACK END") # Announce playback end
Reflection: Recording turns your robot into a memory machine—teach it a route and replay.
Challenge:
- Easy: Limit path length to 20 steps.
- Harder: Add “CLEAR” to empty the path list.
Microproject 5.7.5 – Group control of multiple robots
Goal: Use IDs in messages to target one robot or all.
Blocks used:
- Variables: robot_id = 1.
- Parsing: Accept “ID:1:FWD” or “ID:ALL:STOP”.
Block sequence:
- Set robot_id = 1.
- Parse incoming text by “:”.
- If matches robot_id or ALL, execute.
MicroPython code:
robot_id = 1 # Choose this robot's ID
print("Microproject 5.7.5: robot_id =", robot_id) # Show robot ID
def route_msg(msg): # Define function to route messages with IDs
parts = msg.split(":") # Split incoming string by ':'
if len(parts) >= 3: # Ensure format ID:x:CMD
target = parts[1] # Extract target ID or 'ALL'
cmd = parts[2] # Extract command word
print("Parsed target =", target, "cmd =", cmd) # Show parsing
if (target == str(robot_id)) or (target == "ALL"): # Check if matches this robot
print("Target match -> execute", cmd) # Confirm execution
else: # Not for this robot
print("Ignored (not my ID)") # Explain ignoring
else: # Bad format
print("Invalid format (expected ID:x:CMD)") # Explain format error
Reflection: IDs keep teams organized—only the right robot obeys the right command.
Challenge:
- Easy: Change robot_id to 2.
- Harder: Add “GROUP:A” and obey if target equals your group.
Main project – Robot with app control
Blocks steps (with glossary)
- BLE init + callback: Receive commands and mode changes from the app.
- Digital outputs: Drive motor pins (L298N IN1–IN4) for directions.
- Status prints: Send MODE, SPEED, and action messages to the app.
- Record & replay: Save a sequence and play back on request.
- IDs: Execute commands only for matching robot_id or ALL.
Block sequence:
- Init BLE (central+peripheral) and attach callback.
- Setup motor pins and helper functions (FWD/BACK/LEFT/RIGHT/STOP).
- Parse incoming words (including MODE and ID formats).
- Print status strings (MODE, SPEED, ACTION).
- Record motion commands and play back on “PLAY”.
MicroPython code (mirroring blocks)
# Project 5.7 – Robot with App Control
import ble_central # Start BLE central role (scan/connect)
import ble_peripheral # Start BLE peripheral role (advertise name)
import ble_handle # Handle BLE message callbacks
import machine # Control motor pins via L298N
import time # Use time for timestamps and brief delays
ble_c = ble_central.BLESimpleCentral() # Create central device
ble_p = ble_peripheral.BLESimplePeripheral('Clu-Bots') # Create peripheral named 'Clu-Bots'
print("BLE ready: central+peripheral") # Confirm BLE setup
ble_c.scan() # Begin scanning for peripherals
print("Scanning for 'Clu-Bots'...") # Explain scanning
ble_c.connect(name='Clu-Bots') # Attempt to connect by name
print("Connection requested to 'Clu-Bots'") # Explain connection attempt
left_in1 = machine.Pin(18, machine.Pin.OUT) # Left motor IN1 pin
left_in2 = machine.Pin(19, machine.Pin.OUT) # Left motor IN2 pin
right_in3 = machine.Pin(5, machine.Pin.OUT) # Right motor IN3 pin
right_in4 = machine.Pin(23, machine.Pin.OUT) # Right motor IN4 pin
print("Motor pins set (L:18/19, R:5/23)") # Confirm pin setup
def move_forward(): # Helper: forward
left_in1.value(1) # Left IN1 HIGH
left_in2.value(0) # Left IN2 LOW
right_in3.value(1) # Right IN3 HIGH
right_in4.value(0) # Right IN4 LOW
print("ACTION:FWD") # App-friendly action string
def move_backward(): # Helper: backward
left_in1.value(0) # Left IN1 LOW
left_in2.value(1) # Left IN2 HIGH
right_in3.value(0) # Right IN3 LOW
right_in4.value(1) # Right IN4 HIGH
print("ACTION:BACK") # App-friendly action string
def turn_left(): # Helper: left turn
left_in1.value(0) # Reverse/stop left
left_in2.value(1) # Reverse left
right_in3.value(1) # Forward right
right_in4.value(0) # Keep right forward
print("ACTION:LEFT") # App-friendly action string
def turn_right(): # Helper: right turn
left_in1.value(1) # Forward left
left_in2.value(0) # Keep left forward
right_in3.value(0) # Reverse/stop right
right_in4.value(1) # Reverse right
print("ACTION:RIGHT") # App-friendly action string
def motors_stop(): # Helper: stop
left_in1.value(0) # Left IN1 LOW
left_in2.value(0) # Left IN2 LOW
right_in3.value(0) # Right IN3 LOW
right_in4.value(0) # Right IN4 LOW
print("ACTION:STOP") # App-friendly action string
mode = "MANUAL" # Start mode in MANUAL
speed_pct = 60 # Simulated speed percent for app display
robot_id = 1 # This robot's ID
path = [] # Recorded motions list
print("MODE:", mode) # Print mode for app
print("SPEED:", speed_pct) # Print speed for app
print("ID:", robot_id) # Print robot ID
def record_cmd(cmd): # Recorder helper
ts = time.ticks_ms() # Timestamp in milliseconds
path.append((cmd, ts)) # Append (command, timestamp)
print("REC:", cmd, "@", ts) # Show recording
def playback(): # Playback helper
print("PLAY:BEGIN") # Announce start
for cmd, ts in path: # Iterate recorded commands
print("PLAY:CMD", cmd, "FROM", ts) # Show which command runs
if cmd == "FWD": # Match forward
move_forward() # Execute action
elif cmd == "BACK": # Match backward
move_backward() # Execute action
elif cmd == "LEFT": # Match left
turn_left() # Execute action
elif cmd == "RIGHT": # Match right
turn_right() # Execute action
elif cmd == "STOP": # Match stop
motors_stop() # Execute action
time.sleep(0.5) # Brief delay between steps
print("PLAY:END") # Announce end
def route_msg(msg): # ID routing helper
parts = msg.split(":") # Split by ':'
if len(parts) >= 3: # Expect ID:<id>:CMD format
target = parts[1] # Extract target ID or 'ALL'
cmd = parts[2] # Extract command
print("ROUTE:TARGET", target, "CMD", cmd) # Show routing info
if (target == str(robot_id)) or (target == "ALL"): # Check match
execute_cmd(cmd) # Execute command if matched
else: # Not for this robot
print("ROUTE:IGNORED") # Show ignored
else: # No ID format
execute_cmd(msg) # Execute simple command without ID
def execute_cmd(cmd): # Command executor
global mode # Allow mode updates
global speed_pct # Allow speed updates
if cmd == "FWD": # Forward
move_forward() # Run forward
record_cmd("FWD") # Record action
elif cmd == "BACK": # Backward
move_backward() # Run backward
record_cmd("BACK") # Record action
elif cmd == "LEFT": # Left
turn_left() # Run left
record_cmd("LEFT") # Record action
elif cmd == "RIGHT": # Right
turn_right() # Run right
record_cmd("RIGHT") # Record action
elif cmd == "STOP": # Stop
motors_stop() # Stop motors
record_cmd("STOP") # Record action
elif cmd == "PLAY": # Playback path
playback() # Play recorded path
elif cmd == "MODE:AUTO": # Switch to AUTO
mode = "AUTO" # Set mode
print("MODE:", mode) # Report mode
elif cmd == "MODE:MANUAL": # Switch to MANUAL
mode = "MANUAL" # Set mode
print("MODE:", mode) # Report mode
elif cmd.startswith("SPEED:"): # Speed update
try: # Protect parse
speed_pct = int(cmd.split(":")[1]) # Parse number
print("SPEED:", speed_pct) # Report speed
except: # Parsing failed
print("SPEED:INVALID") # Report error
else: # Unknown command
print("CMD:UNKNOWN", cmd) # Report unknown
def handle_method(key1, key2, key3, keyx): # BLE receive callback
msg = str(key1) # Convert payload to text
print("APP->R32:", msg) # Show incoming app message
route_msg(msg) # Route or execute based on ID format
handle = ble_handle.Handle() # Create BLE handler
handle.recv(handle_method) # Attach receive callback
print("Ready: send FWD/BACK/LEFT/RIGHT/STOP, MODE:..., SPEED:..., PLAY, or ID:x:CMD") # Guidance
# Optional: simple loop to print heartbeat status
while True: # Main heartbeat loop
print("STATUS:MODE", mode) # Report mode
print("STATUS:SPEED", speed_pct) # Report speed
time.sleep(1) # One-second heartbeat interval
External explanation
- What it teaches: Your app drives the robot, shows live status, changes modes, and can record/replay routes.
- Why it works: Motor pins on L298N follow simple HIGH/LOW patterns for direction; BLE callback reads text commands instantly; IDs filter commands; recorded lists let you replay sequences.
- Key concept: “Read command → route → act.”
Story time
You are now the team lead of a small fleet. Tap a button and one rover moves—tap an ID and the whole squad responds. Record a patrol and watch it repeat like clockwork.
Debugging (2)
Debugging 5.7.A – Control latency
Problem: Robot reacts slowly to app commands.
Clues: Actions occur only after heartbeat prints.
Broken code:
while True: # Heartbeat loop runs heavy
print("STATUS") # Prints too often
time.sleep(2) # Long delay blocks responsiveness
Fixed code:
while True: # Heartbeat loop
print("STATUS") # Keep status
time.sleep(0.5) # Shorter delay reduces blocking
# Avoid long sleeps; callbacks run immediately on message receipt
Why it works: Shorter sleeps keep the loop responsive while callbacks still trigger instantly.
Avoid next time: Don’t put long delays in the main loop when expecting commands.
Debugging 5.7.B – Sensor data not updated in app
Problem: MODE/SPEED prints don’t change when app sends updates.
Clues: You see “APP->R32: SPEED:80” but heartbeat still shows old speed.
Broken code:
def execute_cmd(cmd):
speed_pct = int(cmd.split(":")[1]) # Missing global; updates local only
Fixed code:
def execute_cmd(cmd):
global speed_pct # Declare we are updating the global
speed_pct = int(cmd.split(":")[1]) # Parse and update
print("SPEED:", speed_pct) # Confirm change
Why it works: Using global ensures the heartbeat prints the updated shared value.
Avoid next time: Declare globals when changing shared state inside functions.
Final checklist
- App commands move the robot (FWD/BACK/LEFT/RIGHT/STOP)
- MODE and SPEED messages update in serial
- PLAY replays the recorded path
- ID routing executes only for matching robot_id or ALL
- Robot feels responsive to app control
Extras
- 🧠 Student tip: Add “SAFE” mode that forces STOP and ignores motion for 3 seconds.
- 🧑🏫 Instructor tip: Have students sketch pin logic tables (IN1/IN2, IN3/IN4) before coding—clarity prevents wiring mistakes.
- 📖 Glossary:
- L298N: Motor driver that controls DC motors with simple input pins.
- Route: Decide if a message is for this robot or to ignore it.
- Record: Save a list of commands to replay later.
- 💡 Mini tips:
- Keep command words short and consistent.
- If wheels spin opposite, swap motor wires or IN1/IN2 assignment.
- Use brief delays (≤500 ms) in playback for smooth motion.