Wi-Fi Remote Control¶
![]()
Time to take the controls! In this final robot lesson, the ESP32 creates its own Wi-Fi network and hosts a tiny web page with driving buttons. Connect your phone to the robot, open the page, and drive it around like a remote-control car. No app to install, and no home Wi-Fi needed, so it works anywhere.
You'll need:
- Your robot (the sensors can stay on; this program doesn't use them)
- A phone, tablet or laptop with Wi-Fi and a web browser
-
robot.pysaved on the ESP32
How it works¶
The robot is the Wi-Fi hotspot¶
Normally, your ESP32 joins a Wi-Fi network, like your phone joins your home router. That's called station mode. But the ESP32 can also be the network, like a phone's personal hotspot. That's access point mode:
ap = network.WLAN(network.WLAN.IF_AP)
ap.config(ssid="CircuitCoder-Robot", key="drive-me-123",
security=network.WLAN.SEC_WPA2)
ap.active(True)
Your phone connects to the network called CircuitCoder-Robot, and the robot is always at the address 192.168.4.1 on its own network.
No internet on this network
The robot's hotspot isn't connected to the internet. Your phone may warn you about that, or even try to switch back to your home Wi-Fi or mobile data. Tell it to stay connected.
The robot is also a web server¶
When you type an address into a browser, it sends a request to a server, a bit like a letter asking for a page. The server sends back a response with the page inside. The robot runs a tiny web server that understands two requests:
| The browser asks for | The robot sends back |
|---|---|
GET / |
The remote control page (HTML and a little JavaScript) |
GET /go?cmd=forward&speed=70 |
Drives forwards at 70 %, and replies "OK" |
The bit after the ? is called a query string. It's a simple way to pass
information in a web address: cmd says which way to go, and speed says how
fast.
The buttons¶
The web page has five big buttons and a speed slider. While you hold a
button, the page sends that command every 200 ms. When you let go, it sends
stop.
Safety first: the "dead man's switch"¶
What if your phone goes out of range, or the browser freezes, just as the
robot is driving towards the stairs? The stop message would never arrive.
To handle this, the robot uses a dead man's switch (a name that comes from trains, where the driver has to keep holding a handle). The robot only keeps moving while commands keep arriving. If it hears nothing for 0.7 seconds while moving, it stops by itself. That's why the page repeats the command while you hold a button, instead of sending it just once.
Code¶
Save this to the ESP32 as main.py. Change HOTSPOT_PASSWORD to your
own password (at least 8 characters) first!
# Wi-Fi Remote Control: the robot makes its own Wi-Fi hotspot and serves
# a web page with driving buttons. Join the hotspot on your phone, then
# open http://192.168.4.1 in the browser.
import socket
import time
import network
from robot import Robot
HOTSPOT_NAME = "CircuitCoder-Robot"
HOTSPOT_PASSWORD = "drive-me-123" # at least 8 characters: change it!
DEADMAN_MS = 700 # stop if no command arrives for this long
PAGE = """<!DOCTYPE html>
<html><head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Robot Remote</title>
<style>
body { font-family: sans-serif; background: #0e1116; color: #fff;
text-align: center; margin: 0; padding: 20px;
user-select: none; -webkit-user-select: none; }
.pad { display: grid; grid-template-columns: repeat(3, 90px);
gap: 12px; justify-content: center; margin: 24px 0; }
button { height: 90px; font-size: 40px; border: 0; border-radius: 16px;
background: #1a8fe6; color: #fff; touch-action: none; }
button:active { background: #ffd23f; color: #000; }
#stop { background: #e63946; font-size: 22px; }
input { width: 280px; }
</style></head><body>
<h2>Robot Remote</h2>
<div class="pad">
<span></span><button data-cmd="forward">▲</button><span></span>
<button data-cmd="left">◀</button>
<button id="stop" data-cmd="stop">STOP</button>
<button data-cmd="right">▶</button>
<span></span><button data-cmd="backward">▼</button><span></span>
</div>
<p>Speed: <span id="val">70</span>%</p>
<input id="speed" type="range" min="30" max="100" value="70">
<script>
const speed = document.getElementById("speed");
speed.oninput = () => document.getElementById("val").textContent = speed.value;
let current = "stop", repeat = null;
function send(cmd) {
fetch("/go?cmd=" + cmd + "&speed=" + speed.value).catch(() => {});
}
function press(cmd) {
current = cmd; send(cmd);
clearInterval(repeat);
// keep repeating while held, so the robot knows we're still here
repeat = setInterval(() => send(current), 200);
}
function release() {
clearInterval(repeat); current = "stop"; send("stop");
}
document.querySelectorAll("button").forEach(b => {
b.addEventListener("pointerdown", e => { e.preventDefault(); press(b.dataset.cmd); });
["pointerup", "pointercancel", "pointerleave"].forEach(ev =>
b.addEventListener(ev, release));
});
</script>
</body></html>
"""
robot = Robot()
def start_hotspot():
ap = network.WLAN(network.WLAN.IF_AP)
ap.config(ssid=HOTSPOT_NAME, key=HOTSPOT_PASSWORD,
security=network.WLAN.SEC_WPA2)
ap.active(True)
while not ap.active():
time.sleep_ms(100)
address = ap.ipconfig("addr4")[0]
print("Join the Wi-Fi network", HOTSPOT_NAME)
print("Then open http://" + address)
def parse_request(request):
# "GET /go?cmd=forward&speed=70 HTTP/1.1" -> ("/go", {"cmd": "forward", "speed": "70"})
first_line = request.split("\r\n", 1)[0]
parts = first_line.split(" ")
if len(parts) < 2:
return None, {}
path = parts[1]
query = {}
if "?" in path:
path, query_string = path.split("?", 1)
for pair in query_string.split("&"):
if "=" in pair:
key, value = pair.split("=", 1)
query[key] = value
return path, query
def run_command(cmd, speed):
# returns True if the robot is now moving
if cmd == "forward":
robot.forward(speed)
elif cmd == "backward":
robot.backward(speed)
elif cmd == "left":
robot.spin_left(speed * 2 // 3) # spins are gentler, easier to aim
elif cmd == "right":
robot.spin_right(speed * 2 // 3)
else:
robot.stop()
return False
return True
def send_response(client, status, content_type, body):
client.sendall(("HTTP/1.1 " + status + "\r\n"
"Content-Type: " + content_type + "\r\n"
"Connection: close\r\n\r\n").encode())
client.sendall(body.encode())
start_hotspot()
server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("0.0.0.0", 80))
server.listen(2)
server.settimeout(0.05) # don't wait forever for a visitor
moving = False
last_command = time.ticks_ms()
while True:
# 1. Answer a web request, if one is waiting
try:
client, address = server.accept()
except OSError:
client = None # nobody knocked: carry on
if client:
try:
client.settimeout(1)
request = client.recv(1024).decode()
path, query = parse_request(request)
if path == "/":
send_response(client, "200 OK", "text/html", PAGE)
elif path == "/go":
try:
speed = max(30, min(100, int(query.get("speed", "70"))))
except ValueError:
speed = 70
moving = run_command(query.get("cmd", "stop"), speed)
last_command = time.ticks_ms()
send_response(client, "200 OK", "text/plain", "OK")
else:
send_response(client, "404 Not Found", "text/plain", "Not found")
except (OSError, ValueError):
pass # the phone hung up early or sent garbage: no problem
client.close()
# 2. Safety: if the phone goes quiet while we're moving, stop
if moving and time.ticks_diff(time.ticks_ms(), last_command) > DEADMAN_MS:
robot.stop()
moving = False
print("No signal: stopped")
Drive it!¶
- Switch on the robot's battery pack and power the ESP32 from the power bank.
- On your phone, open the Wi-Fi settings and join CircuitCoder-Robot, using the password you set.
- Open the web browser and go to http://192.168.4.1
- Hold the arrows to drive. Use the slider to change speed.
Can't connect?
- Plug the ESP32 into your computer and check the Shell. It prints the network name and the address to open.
- Make sure you typed http://, not https://. The robot doesn't do encrypted connections.
- If the page loads but the buttons do nothing, check the battery pack's switch.
How the code works¶
The main loop does two jobs¶
server.accept() waits for a browser to connect. Normally it would wait
forever, and the robot couldn't do anything else in the meantime. With a
50 ms timeout, it gives up quickly if nobody's there, which lets the loop
check the dead man's switch too. So every time round the loop:
- Answer a request, if one is waiting.
- Check the dead man's switch: if the robot is moving and the last command was too long ago, stop.
Reading the request¶
The first line of every request looks like this:
parse_request() splits it into the path (/go) and a dictionary of
the query values ({"cmd": "forward", "speed": "70"}), using nothing more
than the string split() method. The speed arrives as text, so it's turned
into a number with int(), and clamped between 30 and 100 in case
anyone sends something silly.
The web page lives in the program¶
The whole page, its HTML (layout), CSS (colours and sizes) and JavaScript
(the button behaviour), is stored in one big string called PAGE. You don't
need to know those languages to use it, but it's worth a look. Try changing
the button colours in the <style> section!
Make it your own¶
- Horn: add a button to the page that sends
/go?cmd=horn, and make the robot beep the buzzer when it gets it. - Distance readout: add a
/distancerequest that replies with the ultrasonic sensor's reading, and have the page show it, updating every half-second. - Crash protection: combine this with the Obstacle Avoider so that the robot refuses to drive forwards when something is too close, even if you tell it to.
- Your home network: instead of a hotspot, connect the robot to your home
Wi-Fi with
wifi.connect()(from the Wi-Fi & APIs lesson), and drive it from any device in the house. - Tank steering: replace the arrows with two sliders, one for each
wheel, and send them straight to
robot.drive(left, right).
You built a robot!¶
Take a moment to look at what you've made. Your robot has:
- Actuators (motors) that move it,
- Sensors (ultrasonic and infrared) that let it perceive the world,
- a controller (the ESP32) running code that makes decisions, and
- communication (Wi-Fi) so it can talk to other devices.
That's every major piece of a real robot. From here, you can add more sensors, an arm with servos, a camera, or a completely different body. Whatever you build next, you know how the parts fit together.