📱 Level 5 – App Communication

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:

  1. Init BLE central+peripheral “Clu-Bots”; attach receive callback.
  2. Create motor pins (18,19 for left; 5,23 for right).
  3. 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:

  1. mode = MANUAL, speed = 60%.
  2. Print MODE:MANUAL and SPEED:60.
  3. 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:

  1. Start in MANUAL.
  2. If “MODE:AUTO”, print and prepare auto behavior.
  3. 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:

  1. Create path = [].
  2. On FWD/BACK/LEFT/RIGHT, append (cmd, time).
  3. 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:

  1. Set robot_id = 1.
  2. Parse incoming text by “:”.
  3. 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:

  1. Init BLE (central+peripheral) and attach callback.
  2. Setup motor pins and helper functions (FWD/BACK/LEFT/RIGHT/STOP).
  3. Parse incoming words (including MODE and ID formats).
  4. Print status strings (MODE, SPEED, ACTION).
  5. 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.
On this page