Wednesday, June 17, 2026

WiFi Robot Control Panel with Flask and Raspberry Pi — Part 18: The Robot Gets a Dashboard

New here? This part builds a web-based control panel for the C101 robot — open a browser on any device connected to the same WiFi, and you get buttons to drive the robot, toggle autonomous mode, and enable voice control. The setup builds on Parts 12–17. If the Raspberry Pi environment or remote connection feels unfamiliar, Part 12 has the full walkthrough. Otherwise, let’s go.


Taking Stock

Let’s be honest about how far we’ve come.

Learn to Move → Perception → Localization → Planning → Control → [repeat from Perception]

✅ Drives autonomously — no remote control, no keyboard
✅ Detects obstacles with HC-SR04
✅ Analyzes surroundings with Pi Camera and steers intelligently
✅ Responds to voice commands — “go” and “stop”

By any reasonable measure, the C101 has earned a solid grade. It moves, it senses, it decides, it listens.

But there’s still one thing that quietly bothers us. Every time we want it to run, we open a laptop, connect via VNC, find Thonny, press F5. The robot is autonomous once it’s going — getting it going still requires a human at a keyboard. Not exactly the sleek, impressive setup we imagined when we started this whole thing.

What if instead, you picked up your phone, opened a browser, tapped GO, and walked away?

That’s Part 18.


The Plan

Two files. One robot dashboard.

app.py — a Flask web server running on the Pi. It receives button presses from the browser and translates them into motor commands.

templates/index.html — the web page with the actual buttons. Loads in any browser, on any device, on the same WiFi network.

No app to install. No Bluetooth pairing. Just a URL.


Setup

Install Flask:

pip install flask

File structure — everything must be in the right place:

my_project_env/
├── VoiceRobot.py
├── app.py
└── templates/
    └── index.html

The templates folder must be at the same level as app.py. Flask looks for HTML files there by convention — put it anywhere else and you’ll get a template not found error.


Important: Two Changes to VoiceRobot.py

Before app.py can import from VoiceRobot.py, two things need to be added.

Change 1 — Add a cleanup() function:

Any file that controls hardware (GPIO, camera) and gets imported by another file needs a cleanup() function so the importing file can shut everything down properly on exit.

Add this to VoiceRobot.py, just before the if __name__ == '__main__': block:

def cleanup():
    stop_robot()
    pwm_A.stop()
    pwm_B.stop()
    GPIO.cleanup()
    picam.stop()

Change 2 — Wrap the main loop in if __name__ == '__main__'::

This is a fundamental Python pattern that trips up almost everyone the first time. When app.py imports from VoiceRobot.py, Python executes the entire file — including the while True: main loop. That loop runs forever, and Flask never gets a chance to start.

The fix: wrap the main loop so it only runs when VoiceRobot.py is run directly, not when it’s imported.

# --- Main loop ---
if __name__ == '__main__':
    try:
        print("Voice control active. Say 'go' to start, 'stop' to halt. Ctrl+C to exit.")
        voice_thread = threading.Thread(target=listen_to_commands, daemon=True)
        voice_thread.start()

        while True:
            if is_running:
                dist = get_distance()
                print(f"Distance: {dist:.1f} cm")
                if dist > 20:
                    forward(speed)
                else:
                    stop_robot()
                    backward(50)
                    time.sleep(0.8)
                    stop_robot()
                    choice = decide_direction()
                    if choice == "left":
                        turn_left(speed)
                    else:
                        turn_right(speed)
                    time.sleep(0.8)
                    stop_robot()
                    time.sleep(0.2)
            else:
                stop_robot()
                time.sleep(0.1)

    except KeyboardInterrupt:
        print("Stopped.")
    finally:
        stop_robot()
        pwm_A.stop()
        pwm_B.stop()
        GPIO.cleanup()
        picam.stop()

Without this change: app.py imports VoiceRobot → Python runs the main loop → Flask never starts → browser shows “This site can’t be reached.” With this change: import works cleanly, Flask starts, everything works.


app.py

from flask import Flask, render_template, request, jsonify
from VoiceRobot import forward, backward, turn_left, turn_right, stop_robot, get_distance, decide_direction, cleanup
import threading
import time
import speech_recognition as sr

app = Flask(__name__)

# Global state
speed = 50
auto_mode = False
voice_thread_active = False

# --- Voice recognition thread ---
def voice_recognition_thread():
    recognizer = sr.Recognizer()
    microphone = sr.Microphone()
    global auto_mode, speed, voice_thread_active

    with microphone as source:
        recognizer.adjust_for_ambient_noise(source)

    print("Voice recognition started...")
    while voice_thread_active:
        try:
            with microphone as source:
                audio = recognizer.listen(source, timeout=2, phrase_time_limit=3)
            command = recognizer.recognize_google(audio, language="en-US").lower()
            print(f"Recognized: {command}")
            if "forward" in command: forward(speed)
            elif "backward" in command: backward(speed)
            elif "left" in command: turn_left(speed)
            elif "right" in command: turn_right(speed)
            elif "stop" in command: stop_robot()
            elif "auto" in command: auto_mode = True
            elif "manual" in command: auto_mode = False
        except sr.WaitTimeoutError:
            pass
        except Exception:
            pass

# --- Autonomous driving thread ---
def auto_pilot_thread():
    global auto_mode
    while True:
        if auto_mode:
            distance = get_distance()
            print(f"Distance: {distance} cm")
            if distance < 30:
                stop_robot()
                time.sleep(0.5)
                choice = decide_direction()
                if choice == "left":
                    turn_left(speed)
                else:
                    turn_right(speed)
                time.sleep(0.8)
                stop_robot()
            else:
                forward(speed)
        time.sleep(0.2)

# --- Flask routes ---
@app.route('/')
def index():
    return render_template('index.html')

@app.route('/api/control', methods=['POST'])
def control():
    data = request.json
    action = data.get('action')
    global auto_mode, speed
    if action == 'forward': forward(speed)
    elif action == 'backward': backward(speed)
    elif action == 'left': turn_left(speed)
    elif action == 'right': turn_right(speed)
    elif action == 'stop': stop_robot()
    elif action == 'speed_up': speed = min(100, speed + 10)
    elif action == 'speed_down': speed = max(10, speed - 10)
    return jsonify({"status": "success", "speed": speed})

@app.route('/api/mode', methods=['POST'])
def mode():
    data = request.json
    global auto_mode
    auto_mode = data.get('auto_mode', False)
    return jsonify({"status": "success", "auto_mode": auto_mode})

@app.route('/api/voice', methods=['POST'])
def voice_control():
    data = request.json
    global voice_thread_active
    if data.get('start_voice'):
        if not voice_thread_active:
            voice_thread_active = True
            threading.Thread(target=voice_recognition_thread, daemon=True).start()
    else:
        voice_thread_active = False
    return jsonify({"status": "success", "voice_active": voice_thread_active})

if __name__ == '__main__':
    threading.Thread(target=auto_pilot_thread, daemon=True).start()
    try:
        app.run(host='0.0.0.0', port=5000, debug=False)
    finally:
        cleanup()

templates/index.html

Create a folder called templates in the same directory as app.py. Inside it, create index.html.

One important note: do not write this file in Microsoft Word. Word inserts invisible formatting — smart quotes, auto-capitalization, hidden characters — that break HTML and JavaScript silently. Use Notepad, Notepad++, or any plain text editor. Save with the .html extension.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Robot Control Panel</title>
    <style>
        body { font-family: Arial, sans-serif; text-align: center; background-color: #f4f4f9; padding: 20px; }
        .control-panel { max-width: 400px; margin: auto; background: white; padding: 20px; border-radius: 10px; box-shadow: 0 4px 8px rgba(0,0,0,0.1); }
        button { padding: 15px 25px; font-size: 16px; margin: 5px; border: none; border-radius: 5px; cursor: pointer; background-color: #007bff; color: white; transition: 0.2s; }
        button:hover { background-color: #0056b3; }
        button:active { transform: scale(0.95); }
        .danger-btn { background-color: #dc3545; }
        .danger-btn:hover { background-color: #bd2130; }
        .success-btn { background-color: #28a745; }
        .success-btn:hover { background-color: #218838; }
    </style>
</head>
<body>
    <div class="control-panel">
        <h2>🤖 Robot Control Panel</h2>

        <div>
            <button onclick="sendAction('forward')">Forward</button><br>
            <button onclick="sendAction('left')">Left</button>
            <button onclick="sendAction('stop')" class="danger-btn">STOP</button>
            <button onclick="sendAction('right')">Right</button><br>
            <button onclick="sendAction('backward')">Backward</button>
        </div>

        <hr>

        <div>
            <button onclick="sendAction('speed_down')">Speed −</button>
            <span>Speed: <span id="speed-val">50</span>%</span>
            <button onclick="sendAction('speed_up')">Speed +</button>
        </div>

        <hr>

        <div>
            <button id="mode-btn" class="success-btn" onclick="toggleAutoMode()">Mode: Manual</button>
            <button id="voice-btn" onclick="toggleVoice()">Enable Voice</button>
        </div>
    </div>

    <script>
        function sendAction(action) {
            fetch('/api/control', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ action: action })
            })
            .then(response => response.json())
            .then(data => {
                if(data.speed) document.getElementById('speed-val').innerText = data.speed;
            });
        }

        function toggleAutoMode() {
            let btn = document.getElementById('mode-btn');
            let isAuto = btn.innerText.includes("Manual");
            fetch('/api/mode', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ auto_mode: isAuto })
            })
            .then(response => response.json())
            .then(data => {
                btn.innerText = data.auto_mode ? "Mode: Auto" : "Mode: Manual";
                btn.style.backgroundColor = data.auto_mode ? "#ffc107" : "#28a745";
            });
        }

        function toggleVoice() {
            let btn = document.getElementById('voice-btn');
            let isStarting = btn.innerText.includes("Enable");
            fetch('/api/voice', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ start_voice: isStarting })
            })
            .then(response => response.json())
            .then(data => {
                btn.innerText = data.voice_active ? "Disable Voice" : "Enable Voice";
                btn.style.backgroundColor = data.voice_active ? "#dc3545" : "#007bff";
            });
        }
    </script>
</body>
</html>

Running It

Activate the virtual environment and start the server:

cd /home/pi3/my_project_env
source bin/activate
python3 app.py

You should see:

Running on http://0.0.0.0:5000

Find the Pi’s IP address:

hostname -I

Open a browser on any device on the same WiFi — phone, tablet, laptop — and go to:

http://192.168.x.x:5000

The control panel loads. Tap Forward, Left, Right, Backward for manual control. Tap Mode: Manual to switch to autonomous mode — the robot drives and avoids obstacles on its own. Tap Enable Voice to activate voice recognition from the Pi’s USB microphone.

No app. No pairing. No F5 in Thonny. Just a URL and a robot that responds.




What Just Happened

The robot now has three control modes, all accessible from a single web page:

Manual — tap buttons to drive directly. Good for navigating tight spaces or testing.

Autonomous — tap once to activate. The robot drives, avoids obstacles, and steers based on camera analysis. No further input needed.

Voice — say “forward”, “backward”, “left”, “right”, “stop”, “auto”, or “manual” into the USB mic. The Pi listens and responds.

Switch between them freely. The web interface handles all three.


The Two Lessons That Made This Work

if __name__ == '__main__': — when one Python file imports another, the imported file runs top to bottom, including any while True: loops. Wrapping the main loop in this condition prevents it from running on import. Without it, Flask never starts. This pattern applies to any Python project that splits code across multiple files.

cleanup() — any file that controls hardware and gets imported needs a cleanup function. The importing file calls it on exit to release GPIO pins, stop PWM, and close the camera properly. Skip it and you’ll get “GPIO already in use” errors on the next run.

Both lessons cost a full debug session to learn. Now they’re in the toolbox permanently.


Next up: Part 19 — The robot starts remembering where it’s been.