🌐 Level 6 – Wi-Fi Robotics

Project 6.11: "WiFi Surveillance System"

 

What you’ll learn

  • ✅ Bluetooth Low Energy (BLE): Advertise your robot, connect from a phone/PC, and exchange small commands.
  • ✅ Remote control + tuning: Change speed, mode, and send actions like FWD/LEFT via BLE characteristics.
  • ✅ Telemetry streaming: Send heartbeat and sensor values back to the controller at a steady pace.
  • ✅ Safety over BLE: Implement an emergency stop and a permissions token to avoid unintended control.
  • ✅ OLED dashboard: Show current mode, speed, and last BLE command for quick classroom debugging.

Blocks glossary (used in this project)

  • BLE peripheral: Create a GATT server with services and characteristics.
  • GATT characteristics: RX (write) for incoming commands, TX (notify) for outgoing status.
  • Tokens and modes: Simple strings like TOKEN:123, MODE:SAFE/ACTIVE, SPEED:nn.
  • Motor helpers: Forward/left/right/stop pulses guarded by safety flags.
  • OLED dashboard: Display “MODE/SPEED/CMD/HB”.
  • Serial println: Short labels “BLE:ADV/CONN/RX/TX”, “MOVE:…”, “SAFE:…”, “DASH:…”.

What you need

PartHow many?Pin connection / Notes
D1 R32 (ESP32)1USB cable (30 cm)
L298N + TT motors1 setLeft IN1→18, IN2→19; Right IN3→5, IN4→23
OLED SSD1306 128×641I2C: SCL→22, SDA→21, VCC, GND
LED + buzzer (optional)1 eachLED → Pin 13, Buzzer → Pin 23
Phone/PC with BLE1Use a BLE app (e.g., generic GATT client)

Notes

  • BLE runs locally without WiFi; keep other radios off for simpler testing.
  • On some apps, you must enable “notify” for TX to see live telemetry.
  • Keep the command set small and consistent: “TOKEN:123|MODE:SAFE|CMD:FWD|SPEED:22”.

Before you start

  • Motors and OLED wired and grounded
  • BLE client app installed on a phone or PC
  • Serial monitor open and shows:
print("Ready!")  # Confirm serial is working so you can see messages

Microprojects 1–5

Microproject 6.11.1 – BLE advertising and connection

Goal: Start BLE, advertise a name, and print when a client connects/disconnects.
Blocks used:

  • BLE peripheral: Name and service UUIDs.
  • Callbacks: On connect and on disconnect.

MicroPython code:

import bluetooth  # Import bluetooth to use BLE functions
import time  # Import time for short delays

ble = bluetooth.BLE()  # Create BLE interface
ble.active(True)  # Activate BLE radio
print("BLE:ACTIVE")  # Print BLE activated

NAME = "CluBot-ESP32"  # Define advertised device name
SERVICE_UUID = bluetooth.UUID("12345678-1234-5678-1234-56789abcdef0")  # Define a custom service UUID

conn_handle = None  # Initialize connection handle as None

def on_connect(event, data):  # Define connect callback
    global conn_handle  # Use global connection handle
    if event == 1:  # If event indicates connection
        conn_handle = data[0]  # Store connection handle
        print("BLE:CONN", conn_handle)  # Print connected handle

def on_disconnect(event, data):  # Define disconnect callback
    global conn_handle  # Use global connection handle
    if event == 2:  # If event indicates disconnection
        print("BLE:DISC", conn_handle)  # Print disconnect with last handle
        conn_handle = None  # Clear handle

ble.irq(lambda e, d: (on_connect(e, d), on_disconnect(e, d)))  # Register IRQ handler to call our callbacks
adv_data = bytes('\x02\x01\x06', 'latin1') + bytes([len(NAME)+1, 0x09]) + NAME.encode()  # Build minimal advertisement payload
ble.gap_advertise(100_000, adv_data)  # Start advertising every 100 ms (interval multiplied by 0.625 ms units)
print("BLE:ADV", NAME)  # Print advertising name

time.sleep(5)  # Wait to allow connection attempts

Reflection: Advertising is your “hello”—the phone sees the name and reaches out.
Challenge:

  • Easy: Change NAME to include a group number (e.g., “CluBot-G3”).
  • Harder: Print a retry message every 10 seconds if no connection.

Microproject 6.11.2 – GATT service and characteristics (RX write, TX notify)

Goal: Create a service with RX (write) and TX (notify) characteristics; print writes.
Blocks used:

  • GATT: Define UUIDs, flags (WRITE/READ/NOTIFY).
  • Notify: Send a short status line.

MicroPython code:

import bluetooth  # Import bluetooth for BLE GATT
import time  # Import time for pacing

ble = bluetooth.BLE()  # Create BLE interface
ble.active(True)  # Activate BLE
print("BLE:ACTIVE")  # Print activation

UUID_SVC = bluetooth.UUID("11111111-1111-1111-1111-111111111111")  # Define service UUID
UUID_RX  = bluetooth.UUID("22222222-2222-2222-2222-222222222222")  # Define RX characteristic UUID
UUID_TX  = bluetooth.UUID("33333333-3333-3333-3333-333333333333")  # Define TX characteristic UUID

FLAG_READ = bluetooth.FLAG_READ  # Set read flag
FLAG_WRITE = bluetooth.FLAG_WRITE  # Set write flag
FLAG_NOTIFY = bluetooth.FLAG_NOTIFY  # Set notify flag

# GATT table: service with characteristics (TX, RX)
TX_CHAR = (UUID_TX, FLAG_READ | FLAG_NOTIFY)  # Define TX characteristic properties
RX_CHAR = (UUID_RX, FLAG_WRITE)  # Define RX characteristic properties
SVC = (UUID_SVC, (TX_CHAR, RX_CHAR))  # Create service tuple with characteristics
handles = ble.gatts_register_services((SVC,))  # Register service with BLE stack
svc_handle = handles[0]  # Get service handles array
tx_handle = svc_handle[0]  # Get TX characteristic handle
rx_handle = svc_handle[1]  # Get RX characteristic handle
print("GATT:READY TX", tx_handle, "RX", rx_handle)  # Print characteristic handles

def on_write(event, data):  # Define write callback
    if event == 3:  # If event indicates a write request
        conn, attr_handle = data  # Unpack connection and attribute handle
        if attr_handle == rx_handle:  # If write is to RX characteristic
            value = ble.gatts_read(rx_handle)  # Read value from RX
            print("BLE:RX", value.decode(errors="ignore"))  # Print received text safely

ble.irq(on_write)  # Register IRQ to call on_write when characteristics change
ble.gatts_write(tx_handle, b"READY")  # Set TX initial value
print("BLE:TX_INIT READY")  # Print TX initialized

# Note: notifying requires connection; demonstration prints only for now
time.sleep(2)  # Short wait

Reflection: RX is your inbox; TX is your loudspeaker—together, they form a dialogue.
Challenge:

  • Easy: Notify “HELLO” once when a client connects.
  • Harder: Buffer last 3 RX lines and re‑notify them on reconnect.

Microproject 6.11.3 – Command parser for remote control

Goal: Parse small “key:value” pairs from RX (TOKEN, MODE, SPEED, CMD).
Blocks used:

  • Parser: Split by “|” and “:”.
  • State: mode, speed, last_cmd.

MicroPython code:

def parse_fields(text):  # Define parser to extract fields
    fields = {"TOKEN": "", "MODE": "", "SPEED": "", "CMD": ""}  # Initialize known fields
    for part in text.split("|"):  # Split string by '|'
        if ":" in part:  # If key:value pattern detected
            k, v = part.split(":", 1)  # Split once on ':'
            if k in fields:  # If key is one of our fields
                fields[k] = v  # Store value into fields dict
    print("PARSE:", fields)  # Print parsed fields dict
    return fields  # Return fields

# Demo parsing
demo = "TOKEN:123|MODE:SAFE|SPEED:20|CMD:FWD"  # Example command string
fields = parse_fields(demo)  # Parse demo text

Reflection: Tiny field parsing keeps commands readable and robust—students see exactly what’s happening.
Challenge:

  • Easy: Add “QUIET:ON/OFF” field.
  • Harder: Add a checksum field “CS:n” and print a mismatch warning.

Microproject 6.11.4 – Safe motor actions from commands

Goal: Map CMD to motor helpers; guard by MODE and TOKEN.
Blocks used:

  • Guard checks: MODE must be ACTIVE and TOKEN must match.
  • Actions: FWD/LEFT/RIGHT/STOP with speed scaling.

MicroPython code:

import machine  # Import machine to control motors
import time  # Import time for pulse timing

# Motor pins for L298N driver
L_IN1 = machine.Pin(18, machine.Pin.OUT)  # Left IN1
L_IN2 = machine.Pin(19, machine.Pin.OUT)  # Left IN2
R_IN3 = machine.Pin(5, machine.Pin.OUT)   # Right IN3
R_IN4 = machine.Pin(23, machine.Pin.OUT)  # Right IN4
print("MOTORS:READY 18/19 5/23")  # Print motors ready

SAFE_TOKEN = "123"  # Define required token for control
mode = "SAFE"  # Initialize mode to SAFE
speed = 20  # Initialize speed as percentage (0–100)
last_cmd = ""  # Initialize last command string

def motors_stop():  # Define helper to stop motors
    L_IN1.value(0)  # Left IN1 LOW
    L_IN2.value(0)  # Left IN2 LOW
    R_IN3.value(0)  # Right IN3 LOW
    R_IN4.value(0)  # Right IN4 LOW
    print("MOVE:STOP")  # Print stop action

def scale_pulse(base_ms):  # Define helper to scale pulse duration by speed
    pulse = max(50, int(base_ms * speed / 50))  # Compute pulse ensuring minimum duration
    print("SCALE:PULSE", pulse)  # Print pulse duration
    return pulse  # Return pulse in ms

def do_action(fields):  # Define function to execute action from parsed fields
    global mode, speed, last_cmd  # Use global state vars
    if fields["TOKEN"] and fields["TOKEN"] != SAFE_TOKEN:  # If token provided and mismatched
        print("SAFE:TOKEN_FAIL")  # Print token failure
        motors_stop()  # Ensure stop
        return  # Exit without moving
    if fields["MODE"]:  # If mode field is present
        mode = fields["MODE"]  # Update mode value
        print("MODE:", mode)  # Print mode
    if fields["SPEED"]:  # If speed field is present
        try:  # Try to parse speed integer
            speed = max(0, min(100, int(fields["SPEED"])))  # Clamp speed 0–100
        except ValueError:  # If parsing fails
            print("SPEED:ERR")  # Print speed error
    cmd = fields["CMD"]  # Extract command field
    last_cmd = cmd  # Update last command
    print("CMD:", cmd)  # Print command

    if mode != "ACTIVE":  # If not ACTIVE mode
        print("SAFE:MODE_BLOCK")  # Print block reason
        motors_stop()  # Stop motors
        return  # Exit

    pulse_ms = 200  # Set base pulse duration in milliseconds
    p = scale_pulse(pulse_ms)  # Scale pulse by speed

    if cmd == "FWD":  # If forward command
        L_IN1.value(1)  # Left forward HIGH
        L_IN2.value(0)  # Left forward LOW
        R_IN3.value(1)  # Right forward HIGH
        R_IN4.value(0)  # Right forward LOW
        print("MOVE:FWD", p)  # Print forward action
        time.sleep(p / 1000.0)  # Run motors for scaled duration
        motors_stop()  # Stop motors
    elif cmd == "LEFT":  # If left command
        L_IN1.value(0)  # Left backward LOW
        L_IN2.value(1)  # Left backward HIGH
        R_IN3.value(1)  # Right forward HIGH
        R_IN4.value(0)  # Right forward LOW
        print("MOVE:LEFT", p)  # Print left action
        time.sleep(p / 1000.0)  # Run motors for scaled duration
        motors_stop()  # Stop motors
    elif cmd == "RIGHT":  # If right command
        L_IN1.value(1)  # Left forward HIGH
        L_IN2.value(0)  # Left forward LOW
        R_IN3.value(0)  # Right backward LOW
        R_IN4.value(1)  # Right backward HIGH
        print("MOVE:RIGHT", p)  # Print right action
        time.sleep(p / 1000.0)  # Run motors for scaled duration
        motors_stop()  # Stop motors
    elif cmd == "STOP":  # If stop command
        motors_stop()  # Stop motors immediately
    else:  # If unknown command
        print("CMD:UNKNOWN")  # Print unknown command
        motors_stop()  # Stop motors for safety

# Demo run with ACTIVE and FWD at speed 40
fields = {"TOKEN": "123", "MODE": "ACTIVE", "SPEED": "40", "CMD": "FWD"}  # Create demo fields dict
do_action(fields)  # Execute demo action

Reflection: Simple guards prevent accidents—students earn control by switching to ACTIVE with the right token.
Challenge:

  • Easy: Add “BACK” command.
  • Harder: Add “SPEED:0” to function like a soft emergency stop.

Microproject 6.11.5 – Telemetry and OLED dashboard

Goal: Send a heartbeat via TX notify and show MODE/SPEED/CMD on OLED.
Blocks used:

  • Notify: “HB:hh:mm:ss SPEED=nn MODE=X CMD=Y”.
  • OLED: Show dashboard lines.

MicroPython code:

import bluetooth  # Import bluetooth for notify
import machine  # Import machine for I2C
import time  # Import time for timestamps
import oled128x64  # Import OLED driver for SSD1306

ble = bluetooth.BLE()  # Create BLE interface
ble.active(True)  # Activate BLE
print("BLE:ACTIVE")  # Print BLE status

UUID_SVC = bluetooth.UUID("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")  # Define service UUID
UUID_TX  = bluetooth.UUID("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb")  # Define TX UUID
UUID_RX  = bluetooth.UUID("cccccccc-cccc-cccc-cccc-cccccccccccc")  # Define RX UUID
TX_CHAR = (UUID_TX, bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY)  # Define TX properties
RX_CHAR = (UUID_RX, bluetooth.FLAG_WRITE)  # Define RX properties
SVC = (UUID_SVC, (TX_CHAR, RX_CHAR))  # Create service
tx_handle = ble.gatts_register_services((SVC,))[0][0]  # Register services and get TX handle
rx_handle = ble.gatts_register_services((SVC,))[0][1]  # Register services and get RX handle (re-register for demo simplicity)
print("GATT:TX", tx_handle, "RX", rx_handle)  # Print handles

# Dashboard state
mode = "SAFE"  # Initialize mode string
speed = 20  # Initialize speed percentage
cmd = "-"  # Initialize last command

# OLED setup
i2c = machine.SoftI2C(scl=machine.Pin(22), sda=machine.Pin(21), freq=100000)  # Create I2C bus
oled = oled128x64.OLED(i2c, address=0x3c, font_address=0x3A0000, types=0)  # Initialize OLED
print("OLED:READY 0x3C")  # Print OLED ready

def hhmmss():  # Create simple hh:mm:ss from ticks
    t = time.ticks_ms() // 1000  # Seconds since boot
    h = (t // 3600) % 24  # Hours modulo 24
    m = (t % 3600) // 60  # Minutes
    s = t % 60  # Seconds
    return "{:02d}:{:02d}:{:02d}".format(h, m, s)  # Format timestamp

def update_dashboard():  # Update OLED dashboard
    oled.clear()  # Clear display
    oled.shows('BLE Dashboard', x=0, y=0, size=1, space=0, center=False)  # Title
    oled.shows('MODE:'+mode, x=0, y=12, size=1, space=0, center=False)  # Mode line
    oled.shows('SPEED:'+str(speed), x=0, y=24, size=1, space=0, center=False)  # Speed line
    oled.shows('CMD:'+cmd, x=0, y=36, size=1, space=0, center=False)  # Command line
    oled.shows('HB:'+hhmmss(), x=0, y=48, size=1, space=0, center=False)  # Heartbeat line
    oled.show()  # Refresh display
    print("DASH:UPDATE")  # Print dashboard updated

def tx_notify_line():  # Notify telemetry line if connected
    line = "HB:" + hhmmss() + " SPEED=" + str(speed) + " MODE=" + mode + " CMD=" + cmd  # Build telemetry text
    ble.gatts_write(tx_handle, line.encode())  # Write to TX characteristic
    # Note: client must have notifications enabled to receive updates
    print("BLE:TX", line)  # Print transmitted line

# Demo update without a real client
update_dashboard()  # Update OLED once
tx_notify_line()  # Notify line (no actual client in demo)

Reflection: A tiny dashboard makes students feel in control—what they send is what they see.
Challenge:

  • Easy: Add battery percent to the dashboard (estimate).
  • Harder: Add a “mini graph” by drawing a bar for speed.

Main project – BLE remote control and telemetry robot

Blocks steps (with glossary)

  • BLE peripheral: Advertise name; set up RX (write) and TX (notify).
  • Parser + guards: TOKEN + MODE + SPEED + CMD interpreted safely.
  • Motor actions: FWD/LEFT/RIGHT/STOP with speed scaling and minimum pulse.
  • Telemetry: Send heartbeat plus current fields via TX; pace at ~2 s.
  • Dashboard: OLED shows MODE, SPEED, CMD, and HB; classroom‑friendly prints.

MicroPython code (mirroring blocks)

# Project 6.11 – BLE Remote Control and Telemetry Robot

import bluetooth  # Import bluetooth for BLE GATT and advertising
import machine  # Import machine for motor pins and OLED I2C
import time  # Import time for timestamps and delays
import oled128x64  # Import OLED driver for SSD1306 displays

# ====== BLE setup ======
ble = bluetooth.BLE()  # Create BLE interface
ble.active(True)  # Activate BLE radio
print("BLE:ACTIVE")  # Print BLE status

NAME = "CluBot-ESP32"  # Define advertised BLE name
UUID_SVC = bluetooth.UUID("d0f0d0f0-d0f0-d0f0-d0f0-d0f0d0f0d0f0")  # Define service UUID
UUID_TX  = bluetooth.UUID("d0f0d0f1-d0f0-d0f0-d0f0-d0f0d0f0d0f1")  # Define TX characteristic UUID
UUID_RX  = bluetooth.UUID("d0f0d0f2-d0f0-d0f0-d0f0-d0f0d0f0d0f2")  # Define RX characteristic UUID

TX_CHAR = (UUID_TX, bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY)  # Define TX properties
RX_CHAR = (UUID_RX, bluetooth.FLAG_WRITE)  # Define RX properties
SVC = (UUID_SVC, (TX_CHAR, RX_CHAR))  # Create service with TX and RX
handles = ble.gatts_register_services((SVC,))  # Register service
tx_handle = handles[0][0]  # Extract TX handle
rx_handle = handles[0][1]  # Extract RX handle
print("GATT:READY TX", tx_handle, "RX", rx_handle)  # Print handles

conn_handle = None  # Initialize connection handle

def hhmmss():  # Build hh:mm:ss from ticks
    t = time.ticks_ms() // 1000  # Get seconds since boot
    h = (t // 3600) % 24  # Compute hours
    m = (t % 3600) // 60  # Compute minutes
    s = t % 60  # Compute seconds
    return "{:02d}:{:02d}:{:02d}".format(h, m, s)  # Format timestamp

def ble_irq(event, data):  # BLE IRQ handler for connect/disconnect/write
    global conn_handle  # Use global connection handle
    if event == 1:  # If a client connected
        conn_handle = data[0]  # Store connection handle
        print("BLE:CONN", conn_handle)  # Print connected handle
        ble.gatts_write(tx_handle, b"WELCOME")  # Write welcome text
    elif event == 2:  # If a client disconnected
        print("BLE:DISC", conn_handle)  # Print disconnect
        conn_handle = None  # Clear handle
    elif event == 3:  # If write occurred
        conn, attr = data  # Unpack conn and attribute handle
        if attr == rx_handle:  # If write to RX characteristic
            text = ble.gatts_read(rx_handle).decode(errors="ignore")  # Read and decode RX value
            print("BLE:RX", text)  # Print received text
            fields = parse_fields(text)  # Parse fields from text
            do_action(fields)  # Execute action safely
            tx_notify_line()  # Send telemetry after action

ble.irq(ble_irq)  # Register BLE IRQ handler

adv_data = bytes('\x02\x01\x06', 'latin1') + bytes([len(NAME)+1, 0x09]) + NAME.encode()  # Build advertisement payload
ble.gap_advertise(100_000, adv_data)  # Advertise continuously
print("BLE:ADV", NAME)  # Print advertised name

# ====== OLED dashboard ======
i2c = machine.SoftI2C(scl=machine.Pin(22), sda=machine.Pin(21), freq=100000)  # Create I2C bus
oled = oled128x64.OLED(i2c, address=0x3c, font_address=0x3A0000, types=0)  # Initialize OLED
print("OLED:READY 0x3C")  # Print OLED ready

mode = "SAFE"  # Initialize mode
speed = 20  # Initialize speed %
cmd = "-"  # Initialize last command
SAFE_TOKEN = "123"  # Define control token

def update_dashboard():  # Update OLED lines
    oled.clear()  # Clear screen
    oled.shows('BLE Control', x=0, y=0, size=1, space=0, center=False)  # Title line
    oled.shows('MODE:'+mode, x=0, y=12, size=1, space=0, center=False)  # Mode line
    oled.shows('SPEED:'+str(speed), x=0, y=24, size=1, space=0, center=False)  # Speed line
    oled.shows('CMD:'+cmd, x=0, y=36, size=1, space=0, center=False)  # Command line
    oled.shows('HB:'+hhmmss(), x=0, y=48, size=1, space=0, center=False)  # Heartbeat line
    oled.show()  # Refresh OLED
    print("DASH:UPDATE")  # Print dashboard update

def tx_notify_line():  # Notify telemetry via TX
    line = "HB:" + hhmmss() + " SPEED=" + str(speed) + " MODE=" + mode + " CMD=" + cmd  # Build telemetry string
    ble.gatts_write(tx_handle, line.encode())  # Write to TX characteristic
    print("BLE:TX", line)  # Print transmitted line

# ====== Parser and motor actions ======
def parse_fields(text):  # Parse TOKEN/MODE/SPEED/CMD
    fields = {"TOKEN": "", "MODE": "", "SPEED": "", "CMD": ""}  # Initialize fields
    for part in text.split("|"):  # Split by '|'
        if ":" in part:  # If key:value present
            k, v = part.split(":", 1)  # Split once on ':'
            if k in fields:  # If known key
                fields[k] = v  # Store value
    print("PARSE:", fields)  # Print fields
    return fields  # Return fields

# Motor pins
L_IN1 = machine.Pin(18, machine.Pin.OUT)  # Left IN1
L_IN2 = machine.Pin(19, machine.Pin.OUT)  # Left IN2
R_IN3 = machine.Pin(5, machine.Pin.OUT)   # Right IN3
R_IN4 = machine.Pin(23, machine.Pin.OUT)  # Right IN4
print("MOTORS:READY 18/19 5/23")  # Print motors ready

def motors_stop():  # Stop motors
    L_IN1.value(0)  # Left IN1 LOW
    L_IN2.value(0)  # Left IN2 LOW
    R_IN3.value(0)  # Right IN3 LOW
    R_IN4.value(0)  # Right IN4 LOW
    print("MOVE:STOP")  # Print stop

def scale_pulse(base_ms):  # Scale pulse by speed %
    pulse = max(50, int(base_ms * speed / 50))  # Compute scaled ms (min 50 ms)
    print("SCALE:PULSE", pulse)  # Print pulse
    return pulse  # Return ms

def do_action(fields):  # Execute command safely
    global mode, speed, cmd  # Use globals for dashboard and behavior
    if fields["TOKEN"] and fields["TOKEN"] != SAFE_TOKEN:  # If token mismatch
        print("SAFE:TOKEN_FAIL")  # Print failure
        motors_stop()  # Stop motors
        return  # Abort
    if fields["MODE"]:  # If mode provided
        mode = fields["MODE"]  # Update mode
        print("MODE:", mode)  # Print mode
    if fields["SPEED"]:  # If speed provided
        try:  # Try parse int
            speed = max(0, min(100, int(fields["SPEED"])))  # Clamp to 0–100
        except ValueError:  # If parsing fails
            print("SPEED:ERR")  # Print error
    cmd = fields["CMD"] or "-"  # Update last command
    print("CMD:", cmd)  # Print command

    if mode != "ACTIVE":  # If not ACTIVE
        print("SAFE:MODE_BLOCK")  # Block movement
        motors_stop()  # Stop motors
        update_dashboard()  # Update OLED
        return  # Exit

    p = scale_pulse(200)  # Compute pulse ms

    if cmd == "FWD":  # Forward
        L_IN1.value(1)  # Left forward HIGH
        L_IN2.value(0)  # Left forward LOW
        R_IN3.value(1)  # Right forward HIGH
        R_IN4.value(0)  # Right forward LOW
        print("MOVE:FWD", p)  # Print move
        time.sleep(p / 1000.0)  # Run motors
        motors_stop()  # Stop motors
    elif cmd == "LEFT":  # Left turn
        L_IN1.value(0)  # Left backward LOW
        L_IN2.value(1)  # Left backward HIGH
        R_IN3.value(1)  # Right forward HIGH
        R_IN4.value(0)  # Right forward LOW
        print("MOVE:LEFT", p)  # Print move
        time.sleep(p / 1000.0)  # Run motors
        motors_stop()  # Stop motors
    elif cmd == "RIGHT":  # Right turn
        L_IN1.value(1)  # Left forward HIGH
        L_IN2.value(0)  # Left forward LOW
        R_IN3.value(0)  # Right backward LOW
        R_IN4.value(1)  # Right backward HIGH
        print("MOVE:RIGHT", p)  # Print move
        time.sleep(p / 1000.0)  # Run motors
        motors_stop()  # Stop motors
    elif cmd == "STOP":  # Stop
        motors_stop()  # Stop motors
    else:  # Unknown
        print("CMD:UNKNOWN")  # Print unknown
        motors_stop()  # Stop motors

    update_dashboard()  # Refresh OLED after action

# ====== Main loop: paced telemetry ======
last_tx = time.ticks_ms()  # Initialize last telemetry time
print("RUN:BLE Robot")  # Announce run

while True:  # Continuous loop
    # Pace telemetry every ~2 s
    now = time.ticks_ms()  # Read current time ms
    if time.ticks_diff(now, last_tx) > 2000:  # If > 2 s since last TX
        tx_notify_line()  # Send telemetry
        update_dashboard()  # Refresh OLED
        last_tx = now  # Update last TX time
    time.sleep(0.05)  # Short delay for responsiveness

External explanation

  • What it teaches: You created a BLE‑controlled robot: advertise, connect, parse commands, guard with token and mode, move safely with speed scaling, and stream telemetry while showing an OLED dashboard.
  • Why it works: BLE GATT gives you a simple inbox/outbox; small text fields are easy to parse; the safety token and modes prevent mistakes; telemetry plus OLED makes the system transparent.
  • Key concept: “Advertise → connect → parse → guard → act → report.”

Story time

A student taps “ACTIVE, SPEED 40, FWD” on their phone. The robot nudges forward and the OLED flashes the command like a little cockpit. Two seconds later the heartbeat reports back: calm, honest, alive. It feels like remote control with training wheels.


Debugging (2)

Debugging 6.11.1 – Commands ignored

Problem: You send CMD:FWD but nothing moves.
Clues: Prints show SAFE:MODE_BLOCK; MODE remains SAFE.
Broken code:

# No MODE switch in the message
"TOKEN:123|SPEED:40|CMD:FWD"

Fixed code:

# Include explicit ACTIVE mode
"TOKEN:123|MODE:ACTIVE|SPEED:40|CMD:FWD"

Why it works: Movement is allowed only in ACTIVE mode; adding it unlocks the action.
Avoid next time: Always include MODE:ACTIVE in control messages.

Debugging 6.11.2 – Wrong speed scaling

Problem: Speed changes seem random or pulses too short.
Clues: SCALE:PULSE prints 50 even for SPEED:100.
Broken code:

pulse = int(base_ms * speed / 200)  # Too small; clamps to min often

Fixed code:

pulse = max(50, int(base_ms * speed / 50))  # Wider range with a safe minimum
print("DEBUG:PULSE", pulse)  # Verify scaled result

Why it works: A sensible mapping plus a minimum pulse guarantees visible movement without stalls.
Avoid next time: Print the scaled pulse whenever tuning SPEED so students can see the effect.


Final checklist

  • BLE advertises “CluBot-ESP32” and shows connection/disconnection in prints
  • RX writes are parsed into TOKEN/MODE/SPEED/CMD reliably
  • Movement only occurs in MODE:ACTIVE and with correct TOKEN
  • Speed scaling prints a clear pulse duration and matches the command
  • Telemetry TX sends HB + SPEED + MODE + CMD every ~2 s
  • OLED dashboard updates lines for MODE/SPEED/CMD/HB

Extras

  • 🧠 Student tip: Save your favorite control strings as app presets so you can send them quickly (ACTIVE/FWD/LEFT/RIGHT/STOP).
  • 🧑‍🏫 Instructor tip: Have pairs alternate roles: one student sends commands, the other watches OLED and logs telemetry prints for a short report.
  • 📖 Glossary:
    • GATT: BLE’s method for organizing data into services and characteristics.
    • Characteristic: A single data point in GATT; can be readable, writable, or notifiable.
    • Notify: A push from the robot to the client without the client polling.
  • 💡 Mini tips:
    • Keep tokens short and rotate them per session for safety.
    • Pace telemetry; flooding notifications can disconnect some clients.
    • Test actions with SPEED:30 first; then tune up to avoid surprises.
On this page