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
| Part | How many? | Pin connection / Notes |
|---|---|---|
| D1 R32 (ESP32) | 1 | USB cable (30 cm) |
| L298N + TT motors | 1 set | Left IN1→18, IN2→19; Right IN3→5, IN4→23 |
| OLED SSD1306 128×64 | 1 | I2C: SCL→22, SDA→21, VCC, GND |
| LED + buzzer (optional) | 1 each | LED → Pin 13, Buzzer → Pin 23 |
| Phone/PC with BLE | 1 | Use 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.