Building MicroPython Wifi Robot Car - Featured Image

Posted on

by

in

,

Building a MicroPython Wifi Robot Car

Introduction

This post will show you how to create a MicroPython-powered Wifi robot car that you can control thru your mobile phone. I have searched the internet on how this can be done but I was not able to find one so I rolled on my custom solution for this. This might be a long post but I will try to explain to you the finer details of my project. Let’s start moving! 🙂

If you want to see this project in a video format then please see below. You can also watch this on my YouTube channel.

Design

MicroPython Wifi Robot Car - Design

The image above is the overall design of our MicroPython Wifi Robot Car project.

The following are the 3 important components that power our project:

  • Robot Car Chassis
  • MicroPython device
  • MicroDot web server

You can select any cheap robot car chassis available around you. The MicroPython device should be Wi-Fi-capable such as the ESP32. The MicroDot web server is deployed inside the file system of our ESP32.

The Web Server creates a web application that we need to access on our Mobile Phones browser. Note that we should access the web application only on our mobile phones as we have added javascript code that only supports touch gestures.

The communication between the MicroDot web application and the web server is thru WebSocket as this is a real-time project. If you are not familiar with what WebSocket is then please read my previous post about it.

Related Content:
Using WebSocket in the Internet of Things (IoT) projects

We will control the direction of our robot car by inspecting the WebSocket messages that we are receiving from our web application.

Prerequisites

The following are the components needed to follow along with this MicroPython robot car project.

Disclosure: These are affiliate links and I will earn small commissions to support my site when you buy through these links.

You should have installed the latest MicroPython firmware on your device.

Related Content:
How to install MicroPython on ESP32 and download firmware

Also, you need to understand the basics of working with the MicroDot library as our web server. I have created the following posts and you must have done this before proceeding further. I highly recommend that you go through the posts below as I will not be explaining some of the finer details about the MicroDot library. It is also important that you have a firm grasp of the basics before we go deep dive into the code for this MicroPython robot car project.

Must Read:
Develop MicroPython Application using MicroDot
How to create a MicroPython Web Server the easy way!

I will be using Thonny in developing this project but you could use another IDE such as the PyMakr 2 extension in Visual Studio Code.

Related Content:
MicroPython Development Using Thonny IDE
MicroPython using VSCode PyMakr on ESP32/ESP8266

Schematic/Wiring

MicroPython Wifi Robot Car - Schematic - Wiring

The image above is the wiring or schematic of our project. You can choose any other GPIO (General Purpose Input Output) pins of your liking. Make sure that the pins that you are gonna be using on the LM298N ENA and ENB pin are capable of generating PWM (Pulse Width Modulation) signal.

The ground between the LM298N, ESP32, and Battery Pack should be connected in common.

Note: Never connect the Vin pin of your ESP32 to the Vs of the LM 298N and positive of your battery pack while we are in development or the USB is connected to your ESP32. Connect this only when we are done loading the program to the ESP32 MicroPython device.

Code

The complete code for this project is available in my GitHub repository. You can either clone it or download it as a zip file.

git clone https://github.com/donskytech/micropython-wifi-robot-car.git

The image below is the project file layout of our program.

MicroPython Wifi Robot Car - File Structure

Let’s go through how each part of the program participates in our project.

  • static/css and static/js – contains the Cascading Style Sheets (CSS) or styling file of our index.html page. The Javascript file (custom.js) handles the WebSocket message exchange between the MicroDot WebSocket server and our browser.
  • templates/index.html – this contains the user interface of our project and has the HTML elements to create the D-Pad or O-Pad controller.
  • boot.py – default script run by MicroPython when it is restarted. This file connects to our Wifi network.
  • main.py – contains our MicroDot web server and WebSocket routes.
  • microdot* – these are MicroDot-specific project files.
  • robot-car.py – represents our Car object. The logic to control the motor driver LM298N is embedded in this file.

Let us go through and explain in detail the code for each file.

A little note about the LM298N driver

Before we continue let us define first how to control our LM2898N motor driver.

IN1IN2SPIN DIRECTION
LOW (0)LOW (0)Motor Off
HIGH (1)LOW (0)Forward
LOW(0)HIGH(1)Reverse
HIGH(1)HIGH(1)Motor Off

The Enable Pins can control the speed of the motor by adjusting the duty cycle of our period. In this project, I am passing the following values to represent the speed of the duty cycle. This is passed by the main.py file.

# Wifi Robot Car Configuration
MAX_POWER_LEVEL = 65535		# 100% Power
MEDIUM_POWER_LEVEL = 49151      # 75% Power
MIN_POWER_LEVEL = 32767		# 50% Power

Having known this setup then you could figure out how you can control your robot car.

robot_car.py

We have created a dedicated class to represent our MicroPython robot car and below is its code.

from machine import Pin, PWM

"""
Class to represent our robot car
""""
class RobotCar():
    def __init__(self, enable_pins, motor_pins, speed):
        self.right_motor_enable_pin = PWM(Pin(enable_pins[0]), freq=2000)
        self.left_motor_enable_pin = PWM(Pin(enable_pins[1]), freq=2000)
        
        self.right_motor_control_1 = Pin(motor_pins[0], Pin.OUT)
        self.right_motor_control_2 = Pin(motor_pins[1], Pin.OUT)
        
        self.left_motor_control_1 = Pin(motor_pins[2], Pin.OUT)
        self.left_motor_control_2 = Pin(motor_pins[3], Pin.OUT)
        
        self.speed = speed
        
    def stop(self):
        print('Car stopping')
        self.right_motor_control_1.value(0)
        self.right_motor_control_2.value(0)
        self.left_motor_control_1.value(0)
        self.left_motor_control_2.value(0)
        self.right_motor_enable_pin.duty_u16(0)
        self.left_motor_enable_pin.duty_u16(0)
        
    def forward(self):
        print('Move forward')
        self.right_motor_enable_pin.duty_u16(self.speed)
        self.left_motor_enable_pin.duty_u16(self.speed)
        
        self.right_motor_control_1.value(1)
        self.right_motor_control_2.value(0)
        self.left_motor_control_1.value(1)
        self.left_motor_control_2.value(0)

    
    def reverse(self):
        print('Move reverse')
        self.right_motor_enable_pin.duty_u16(self.speed)
        self.left_motor_enable_pin.duty_u16(self.speed)
        
        self.right_motor_control_1.value(0)
        self.right_motor_control_2.value(1)
        self.left_motor_control_1.value(0)
        self.left_motor_control_2.value(1)
    
    def turnLeft(self):
        print('Turning Left')
        self.right_motor_enable_pin.duty_u16(self.speed)
        self.left_motor_enable_pin.duty_u16(self.speed)
        
        self.right_motor_control_1.value(1)
        self.right_motor_control_2.value(0)
        self.left_motor_control_1.value(0)
        self.left_motor_control_2.value(0)
    
    def turnRight(self):
        print('Turning Right')
        self.right_motor_enable_pin.duty_u16(self.speed)
        self.left_motor_enable_pin.duty_u16(self.speed)
        
        self.right_motor_control_1.value(0)
        self.right_motor_control_2.value(0)
        self.left_motor_control_1.value(1)
        self.left_motor_control_2.value(0)
        
    def set_speed(self, new_speed):
        self.speed = new_speed
        
    def cleanUp(self):
        print('Cleaning up pins')
        self.right_motor_enable_pin.deinit()
        self.left_motor_enable_pin.deinit()

As you can see, the class has methods that mirror a real-life car object. It has methods named forward, reverse, turnLeft, turnRight or set_speed. Let us go over what each line of code does.

from machine import Pin, PWM

Import the necessary packages especially the PWM. We will be using PWM to control the speed of our MicroPython robot car.

"""
Class to represent our robot car
""""
class RobotCar():
    def __init__(self, enable_pins, motor_pins, speed):
        self.right_motor_enable_pin = PWM(Pin(enable_pins[0]), freq=2000)
        self.left_motor_enable_pin = PWM(Pin(enable_pins[1]), freq=2000)
        
        self.right_motor_control_1 = Pin(motor_pins[0], Pin.OUT)
        self.right_motor_control_2 = Pin(motor_pins[1], Pin.OUT)
        
        self.left_motor_control_1 = Pin(motor_pins[2], Pin.OUT)
        self.left_motor_control_2 = Pin(motor_pins[3], Pin.OUT)
        
        self.speed = speed

We declare the RobotCar class here and defined the constructor of our class. It is expected that we are passing a list of enable_pins and motor_pins and the initial speed.

We define a PWM pin for both the enabled pins and set all the motor pins as output pins.

The initial speed of our robot car is also defined here.

    def stop(self):
        print('Car stopping')
        self.right_motor_control_1.value(0)
        self.right_motor_control_2.value(0)
        self.right_motor_enable_pin.duty_u16(0)
        self.left_motor_enable_pin.duty_u16(0)
        
    def forward(self):
        print('Move forward')
        self.right_motor_enable_pin.duty_u16(self.speed)
        self.left_motor_enable_pin.duty_u16(self.speed)
        
        self.right_motor_control_1.value(1)
        self.right_motor_control_2.value(0)
        self.left_motor_control_1.value(1)
        self.left_motor_control_2.value(0)

    
    def reverse(self):
        print('Move reverse')
        self.right_motor_enable_pin.duty_u16(self.speed)
        self.left_motor_enable_pin.duty_u16(self.speed)
        
        self.right_motor_control_1.value(0)
        self.right_motor_control_2.value(1)
        self.left_motor_control_1.value(0)
        self.left_motor_control_2.value(1)
    
    def turnLeft(self):
        print('Turning Left')
        self.right_motor_enable_pin.duty_u16(self.speed)
        self.left_motor_enable_pin.duty_u16(self.speed)
        
        self.right_motor_control_1.value(1)
        self.right_motor_control_2.value(0)
        self.left_motor_control_1.value(0)
        self.left_motor_control_2.value(0)
    
    def turnRight(self):
        print('Turning Right')
        self.right_motor_enable_pin.duty_u16(self.speed)
        self.left_motor_enable_pin.duty_u16(self.speed)
        
        self.right_motor_control_1.value(0)
        self.right_motor_control_2.value(0)
        self.left_motor_control_1.value(1)
        self.left_motor_control_2.value(0)

For example, for our car to move forward then we can configure our forward() method to set the value of both IN pins to a HIGH (1) and LOW (0). Also, we need to set the speed of our Enable Pins by setting the appropriate duty cycle thru the method self.right_motor_enable_pin.duty_u16(self.speed) and self.left_motor_enable_pin.duty_u16(self.speed)

I am not going to explain how each of the functions works but by looking at the table above then you would know why the values are set appropriately.

    def set_speed(self, new_speed):
        self.speed = new_speed
        
    def cleanUp(self):
        print('Cleaning up pins')
        self.right_motor_enable_pin.deinit()
        self.left_motor_enable_pin.deinit()

The set_speed() function will set the speed of our motor. The cleanUp() function is needed to de-initialize our PWM pins setup once we exit our program.

boot.py

The boot.py is the first script that is executed whenever we restart our Microcontroller device thus in this project we are using it to connect to our Wifi.

# boot.py -- run on boot-up
import network, utime

# Replace the following with your WIFI Credentials
SSID = "<PLACE_YOUR_SSID_HERE>"
SSI_PASSWORD = "<PLACE_YOUR_WIFI_PASWORD_HERE>"

def do_connect():
    import network
    sta_if = network.WLAN(network.STA_IF)
    if not sta_if.isconnected():
        print('connecting to network...')
        sta_if.active(True)
        sta_if.connect(SSID, SSI_PASSWORD)
        while not sta_if.isconnected():
            pass
    print('Connected! Network config:', sta_if.ifconfig())
    
print("Connecting to your wifi...")
do_connect()

Import the necessary Python packages.

# boot.py -- run on boot-up
import network, utime

Replace the following entry with your Wifi credentials

# Replace the following with your WIFI Credentials
SSID = "<PLACE_YOUR_SSID_HERE>"
SSI_PASSWORD = "<PLACE_YOUR_WIFI_PASWORD_HERE>"

This will connect to your Wifi until it is able to retrieve the desired IP Address.

def do_connect():
    import network
    sta_if = network.WLAN(network.STA_IF)
    if not sta_if.isconnected():
        print('connecting to network...')
        sta_if.active(True)
        sta_if.connect(SSID, SSI_PASSWORD)
        while not sta_if.isconnected():
            pass
    print('Connected! Network config:', sta_if.ifconfig())
    
print("Connecting to your wifi...")
do_connect()

main.py

The main.py contains the logic of our MicroDot web server. I am not going to explain how to create a MicroDot web server but just follow my earlier post on how this is done.

The main.py code is below and it contains all the necessary routes in order to respond to HTTP requests.

from microdot_asyncio import Microdot, Response, send_file
from microdot_asyncio_websocket import with_websocket
from microdot_utemplate import render_template
from robot_car import RobotCar

app = Microdot()
Response.default_content_type = "text/html"

# Wifi Robot Car Configuration
MAX_POWER_LEVEL = 65535		# 100%
MEDIUM_POWER_LEVEL = 49151  # 75%
MIN_POWER_LEVEL = 32767		# 50%

enable_pins = [21, 22]
motor_pins = [18, 5, 33, 25]

robot_car = RobotCar(enable_pins, motor_pins, MEDIUM_POWER_LEVEL)

car_commands = {
    "forward": robot_car.forward,
    "reverse": robot_car.reverse,
    "left": robot_car.turnLeft,
    "right": robot_car.turnRight,
    "stop": robot_car.stop
}

speed_commands = {
    "slow-speed": MIN_POWER_LEVEL,
    "normal-speed": MEDIUM_POWER_LEVEL,
    "fast-speed": MAX_POWER_LEVEL
}


# App Route
@app.route("/")
async def index(request):
    return render_template("index.html")


@app.route("/ws")
@with_websocket
async def executeCarCommands(request, ws):
    while True:
        websocket_message = await ws.receive()
        print(f"receive websocket message : {websocket_message}")
        
        if "speed" in websocket_message:
            new_speed = speed_commands.get(websocket_message)
            robot_car.set_speed(new_speed)
        else:
            command = car_commands.get(websocket_message)
            command()
        await ws.send("OK")


@app.route("/shutdown")
async def shutdown(request):
    request.app.shutdown()
    return "The server is shutting down..."


@app.route("/static/<path:path>")
def static(request, path):
    if ".." in path:
        # directory traversal is not allowed
        return "Not found", 404
    return send_file("static/" + path)


if __name__ == "__main__":
    try:
        app.run()
    except KeyboardInterrupt:
        robot_car.cleanUp()

Configure the imports

from microdot_asyncio import Microdot, Response, send_file
from microdot_asyncio_websocket import with_websocket
from microdot_utemplate import render_template
from robot_car import RobotCar

Import all the necessary MicroDot-specific packages including our RobotCar class. We are also adding MicroDot WebSocket support.

app = Microdot()
Response.default_content_type = "text/html"

Create an instance of MicroDot and set the content type to text/html.

Set up Robot Car configurations

# Wifi Robot Car Configuration
MAX_POWER_LEVEL = 65535		# 100%
MEDIUM_POWER_LEVEL = 49151  # 75%
MIN_POWER_LEVEL = 32767		# 50%

enable_pins = [21, 22]
motor_pins = [18, 5, 33, 25]

We define the configurations of our duty cycle here. The duty cycle can be set from zero to 65535 but in order for our MicroPython robot car to run then I arbitrarily set the minimum to at least 50%. You can change this minimum value depending on your preferences.

The GPIO pins for the Enable and Motor pins are defined here as well.

robot_car = RobotCar(enable_pins, motor_pins, MEDIUM_POWER_LEVEL)

car_commands = {
    "forward": robot_car.forward,
    "reverse": robot_car.reverse,
    "left": robot_car.turnLeft,
    "right": robot_car.turnRight,
    "stop": robot_car.stop
}

speed_commands = {
    "slow-speed": MIN_POWER_LEVEL,
    "normal-speed": MEDIUM_POWER_LEVEL,
    "fast-speed": MAX_POWER_LEVEL
}

We create an instance of our RobotCar and pass in the Enable and Motor Pins plus the default speed which is the medium level.

I have defined a Python dictionary car_commands here to handle WebSocket messages. I have assigned the function that will be called on our RobotCar object upon receiving specific car commands.

Below is the list of possible WebSocket Messages coming from our Web Application.

WebSocket CommandDescription
forwardMove the car forward
reverseMove the car in reverse
leftTurn Left the car
rightTurn right the car
stopStop the car
slow-speedSet the car speed to minimum
normal-speedSet the car speed to normal
fast-speedSet the car speed to maximum

Making sense of the MicroDot routes

At this point of the program, we are defining the routes that will respond to HTTP requests.

# App Route
@app.route("/")
async def index(request):
    return render_template("index.html")

We define our index or root route here and it will serve the index.html page.

@app.route("/ws")
@with_websocket
async def executeCarCommands(request, ws):
    while True:
        websocket_message = await ws.receive()
        print(f"receive websocket message : {websocket_message}")
        
        if "speed" in websocket_message:
            new_speed = speed_commands.get(websocket_message)
            robot_car.set_speed(new_speed)
        else:
            command = car_commands.get(websocket_message)
            command()
        await ws.send("OK")

The WebSocket route is defined here and will respond to the path that starts with “/ws“. It will continually listen in a loop for any WebSocket message and if it receives one then it inspects if the message corresponds to speed change.

If the message is speed-related then we set the speed of our robot car by calling the robot_car.set_speed() else if the command is movement related then we call the corresponding robot car movement command.

We return “OK” message response for all WebSocket message exchanges.

@app.route("/shutdown")
async def shutdown(request):
    request.app.shutdown()
    return "The server is shutting down..."


@app.route("/static/<path:path>")
def static(request, path):
    if ".." in path:
        # directory traversal is not allowed
        return "Not found", 404
    return send_file("static/" + path)

The “/shutdown” route is used to shut down our Microdot application and can be triggered by typing the following in your browser.

http://<IP>:5000/shutdown

On the other hand, the “static” route is for serving static contents which in this case are our CSS (Cascading Style Sheets) and Javascript files.

if __name__ == "__main__":
    try:
        app.run()
    except KeyboardInterrupt:
        robot_car.cleanUp()

This is the entry point of our program and this will run our MicroDot web server. Also, take note of robot_car.cleanUp() the except block as this is important because PWM pins will continue to run even if we exit our MicroDot web server.

templates/index.html

The index.html will contain the D-Pad and the O-Pad controller which I have forked from one of the code pens. I would like to say shout out to the original creator of this excellent user interface which I have already used in different projects already like the ESP32/ESP8266.

Related Content:
ESP32 Robot Car Using Websockets
Create your own ESP32 Wifi Car using the library esp32-wifi-car

Please see below code pen if you would like to play around with it.

The complete index.html code is below.

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>MicroPython Wifi Robot Car</title>
    <link href="static/css/entireframework.min.css" rel="stylesheet" type="text/css">
    <link href="static/css/custom.css" rel="stylesheet" type="text/css">

</head>

<body>
    <nav class="nav" tabindex="-1" onclick="this.focus()">
        <div class="container">
            <a class="pagename current" href="https://www.donskytech.com">www.donskytech.com</a>
        </div>
    </nav>
    <button class="btn-close btn btn-sm">×</button>
    <div class="container">
        <div class="hero">
            <h2>MicroPython Wifi Robot Car</h2>
        </div>

        <div class="parent">
            <div class="speed-settings">Speed Settings</div>
            <div class="wrapper">
                <input type="radio" name="speed-settings" id="option-1" value="slow-speed" >
                <input type="radio" name="speed-settings" id="option-2" value="normal-speed" checked>
                <input type="radio" name="speed-settings" id="option-3" value="fast-speed" >
                <label for="option-1" class="option option-1">
                    <div class="dot"></div>
                    <span>Slow</span>
                </label>
                <label for="option-2" class="option option-2">
                    <div class="dot"></div>
                    <span>Normal</span>
                </label>
                <label for="option-3" class="option option-3">
                    <div class="dot"></div>
                    <span>Fast</span>
                </label>
            </div>

            <div class="set blue">
                <nav class="d-pad">
                    <a class="up control" data-direction="forward"></a>
                    <a class="right control" data-direction="right"></a>
                    <a class="down control" data-direction="reverse"></a>
                    <a class="left control" data-direction="left"></a>
                </nav>
                <nav class="o-pad">
                    <a class="up control" data-direction="forward"></a>
                    <a class="right control" data-direction="right"></a>
                    <a class="down control" data-direction="reverse"></a>
                    <a class="left control" data-direction="left"></a>
                </nav>
            </div>

        </div>
    </div>
    <script src="static/js/custom.js"></script>
    </div>
    </div>
</body>

</html>

Overall, this file only contains the bare HTML elements and needs to be stylized by our CSS files especially the D-Pad and our O-Pad controllers.

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>MicroPython Wifi Robot Car</title>
    <link href="static/css/entireframework.min.css" rel="stylesheet" type="text/css">
    <link href="static/css/custom.css" rel="stylesheet" type="text/css">

</head>

The head section contains the default meta elements, title and the CSS imports.

<body>
    <nav class="nav" tabindex="-1" onclick="this.focus()">
        <div class="container">
            <a class="pagename current" href="https://www.donskytech.com">www.donskytech.com</a>
        </div>
    </nav>
    <button class="btn-close btn btn-sm">×</button>
    <div class="container">
        <div class="hero">
            <h2>MicroPython Wifi Robot Car</h2>
        </div>

We define the navigation at the top of the page including our title.

       <div class="parent">
            <div class="speed-settings">Speed Settings</div>
            <div class="wrapper">
                <input type="radio" name="speed-settings" id="option-1" value="slow-speed" >
                <input type="radio" name="speed-settings" id="option-2" value="normal-speed" checked>
                <input type="radio" name="speed-settings" id="option-3" value="fast-speed" >
                <label for="option-1" class="option option-1">
                    <div class="dot"></div>
                    <span>Slow</span>
                </label>
                <label for="option-2" class="option option-2">
                    <div class="dot"></div>
                    <span>Normal</span>
                </label>
                <label for="option-3" class="option option-3">
                    <div class="dot"></div>
                    <span>Fast</span>
                </label>
            </div>

This is the section where we set the speed settings of our MicroPython Robot Car. Furthermore, take note of the value of the radio buttons. It has “slow-speed“, “normal-speed“, and “fast-speed” and they will be sent as WebSocket messages.

            <div class="set blue">
                <nav class="d-pad">
                    <a class="up control" data-direction="forward"></a>
                    <a class="right control" data-direction="right"></a>
                    <a class="down control" data-direction="reverse"></a>
                    <a class="left control" data-direction="left"></a>
                </nav>
                <nav class="o-pad">
                    <a class="up control" data-direction="forward"></a>
                    <a class="right control" data-direction="right"></a>
                    <a class="down control" data-direction="reverse"></a>
                    <a class="left control" data-direction="left"></a>
                </nav>
            </div>

        </div>
    </div>
    <script src="static/js/custom.js"></script>
    </div>
    </div>
</body>

</html>

Lastly, this section contains the HTML structure of our D-Pad and O-Pad controller. Also, take a close look at the “data-direction” attribute of each control elements. When these links are touch in our mobile phones then it would send the WebSocket messages to our web server.

The script custom.js is imported at the body section of our HTML page.

static/css/custom.css and static/css/entireframework.min.css

The two CSS files (custom.css and entireframework.min.css) provide the styling of our pages and their main function is to beautify the page. I am using the mincss as my CSS framework because of its size which is only a few kilobytes.

I won’t be explaining so much about the CSS classes of the mincss CSS framework but you can take a look at the example sections to figure out how it works.

The custom.css applies local styling to our user interface. Moreover, the speed settings and the D-Pad and O-Pad controller are being stylized by this file.

static/js/custom.js

The custom.js code is below and we will run through what each line of code does as this is the most important part of our user interface.

var targetUrl = `ws://${location.host}/ws`;
var websocket;
window.addEventListener("load", onLoad);

function onLoad() {
  initializeSocket();
  setDefaultSpeed();
}

function initializeSocket() {
  console.log("Opening WebSocket connection to ESP32 MicroPython Server...");
  websocket = new WebSocket(targetUrl);
  websocket.onopen = onOpen;
  websocket.onclose = onClose;
  websocket.onmessage = onMessage;
}
function onOpen(event) {
  console.log("Starting connection to WebSocket server..");
}
function onClose(event) {
  console.log("Closing connection to server..");
  setTimeout(initializeSocket, 2000);
}
function onMessage(event) {
  console.log("WebSocket message received:", event);
}

function sendMessage(message) {
  websocket.send(message);
}

/*
Speed Settings Handler
*/
var speedSettings = document.querySelectorAll(
  'input[type=radio][name="speed-settings"]'
);
speedSettings.forEach((radio) =>
  radio.addEventListener("change", () => {
    var speedSettings = radio.value;
    console.log("Speed Settings :: " + speedSettings);
    sendMessage(speedSettings);
  })
);

function setDefaultSpeed() {
  console.log("Setting default speed to normal..");
  let normalOption = document.getElementById("option-2");
  normalOption.checked = true;
}

/*
O-Pad/ D-Pad Controller and Javascript Code
*/
// Prevent scrolling on every click!
// super sweet vanilla JS delegated event handling!
document.body.addEventListener("click", function (e) {
  if (e.target && e.target.nodeName == "A") {
    e.preventDefault();
  }
});

function touchStartHandler(event) {
  var direction = event.target.dataset.direction;
  console.log("Touch Start :: " + direction);
  sendMessage(direction);
}

function touchEndHandler(event) {
  const stop_command = "stop";
  var direction = event.target.dataset.direction;
  console.log("Touch End :: " + direction);
  sendMessage(stop_command);
}

document.querySelectorAll(".control").forEach((item) => {
  item.addEventListener("touchstart", touchStartHandler);
});

document.querySelectorAll(".control").forEach((item) => {
  item.addEventListener("touchend", touchEndHandler);
});
var targetUrl = `ws://${location.host}/ws`;
var websocket;
window.addEventListener("load", onLoad);

function onLoad() {
  initializeSocket();
  setDefaultSpeed();
}

First, we define our WebSocket URL connection and WebSocket variables at the top. They will be used to connect to our web server.

Next, we added an event listener to the “load” event of our web page by attaching the onLoad function. The onLoad function calls the initializeSocket() and setDefaultSpeed() whose primary job are to open the WebSocket connection and set the default speed of our controller.

function initializeSocket() {
  console.log("Opening WebSocket connection to ESP32 MicroPython Server...");
  websocket = new WebSocket(targetUrl);
  websocket.onopen = onOpen;
  websocket.onclose = onClose;
  websocket.onmessage = onMessage;
}
function onOpen(event) {
  console.log("Starting connection to WebSocket server..");
}
function onClose(event) {
  console.log("Closing connection to server..");
  setTimeout(initializeSocket, 2000);
}
function onMessage(event) {
  console.log("WebSocket message received:", event);
}

function sendMessage(message) {
  websocket.send(message);
}

The initializeSocket() function opens a WebSocket connection to our web server and then attached event handlers to it using the onopen, onclose, and onmessage

The sendMessage() function is where we send the actual WebSocket message.

/*
Speed Settings Handler
*/
var speedSettings = document.querySelectorAll(
  'input[type=radio][name="speed-settings"]'
);
speedSettings.forEach((radio) =>
  radio.addEventListener("change", () => {
    var speedSettings = radio.value;
    console.log("Speed Settings :: " + speedSettings);
    sendMessage(speedSettings);
  })
);

function setDefaultSpeed() {
  console.log("Setting default speed to normal..");
  let normalOption = document.getElementById("option-2");
  normalOption.checked = true;
}

We add event handlers to all radio buttons such that when it is clicked then it will call the sendMessage() function passing in the speed settings value.

The function setDefaultSpeed() will set the default speed setting which in this case is the normal value.

/*
O-Pad/ D-Pad Controller and Javascript Code
*/
// Prevent scrolling on every click!
// super sweet vanilla JS delegated event handling!
document.body.addEventListener("click", function (e) {
  if (e.target && e.target.nodeName == "A") {
    e.preventDefault();
  }
});

function touchStartHandler(event) {
  var direction = event.target.dataset.direction;
  console.log("Touch Start :: " + direction);
  sendMessage(direction);
}

function touchEndHandler(event) {
  const stop_command = "stop";
  var direction = event.target.dataset.direction;
  console.log("Touch End :: " + direction);
  sendMessage(stop_command);
}

document.querySelectorAll(".control").forEach((item) => {
  item.addEventListener("touchstart", touchStartHandler);
});

document.querySelectorAll(".control").forEach((item) => {
  item.addEventListener("touchend", touchEndHandler);
});

The following functions above are for our D-Pad and O-Pad controller and they might be hard to understand when you first see it so let us try to break it down one by one.

// Prevent scrolling on every click!
// super sweet vanilla JS delegated event handling!
document.body.addEventListener("click", function (e) {
  if (e.target && e.target.nodeName == "A") {
    e.preventDefault();
  }
});

When we click all the buttons in our controller then we should prevent our page from flickering or being submitted so we should call e.preventDefault() on the event object.

function touchStartHandler(event) {
  var direction = event.target.dataset.direction;
  console.log("Touch Start :: " + direction);
  sendMessage(direction);
}

function touchEndHandler(event) {
  const stop_command = "stop";
  var direction = event.target.dataset.direction;
  console.log("Touch End :: " + direction);
  sendMessage(stop_command);
}

The touchStartHandler() and touchEndHandler() functions will extract the data-direction attribute from our links and whatever the value then it should send a WebSocket message to our web server.

document.querySelectorAll(".control").forEach((item) => {
  item.addEventListener("touchstart", touchStartHandler);
});

document.querySelectorAll(".control").forEach((item) => {
  item.addEventListener("touchend", touchEndHandler);
});

We are attaching event handlers for the touchstart and touched events of our D-Pad and O-Pad controller.

As a side note, touch events is only applicable to our mobile phones that supports touch gestures so it will not work on your laptop browser.

MicroDot specific files

The MicroDot specific files are copied from the GitHub repository of the said project. We are making use of the Asynchronous, Templating, and WebSocket extensions of this project.

That is all for the code and how it works!

How to deploy the project to your MicroPython Device?

First, connect your MicroPython device in your Thonny IDE and make sure that it is detected by your laptop or workstation.

Next, open the boot.py file and change the credentials to match those of your Wifi network.

Change Wifi Credentials in boot file

Next, install the uTemplate library in your MicroPython device. If you are unsure how this is done then you can follow my You can follow my How to install MicroPython libraries or packages in Thonny IDE?

Lastly, upload all the files to your MicroPython device

MicroPython Wifi Robot Car - Upload All Files

Soft reboot your MicroPython device and wait for the following messages to be displayed.

MPY: soft reboot
Connecting to your wifi...
Connected! Network config: ('192.168.100.36', '255.255.255.0', '192.168.100.1', '192.168.100.1')

Take note of the IP Address and then open the browser in your mobile phone and type in the following URL.

http://<IP-Address>:5000

This should open up the web application in your browser. Try to click the D-Pad and O-Pad controller and verify if it is working.

Once you have confirmed that this works then you can now remove your ESP32 USB connection and connect the Vs pin of the LM298N pin to that of the Vin pin of your ESP32.

Have fun playing with your shiny MicroPython-powered Wifi Robot car! 🙂

Wrap Up

This is quite a long post and contains lots of code but I hope you learn something from my MicroDot Wifi robot car project. If something is not clear then please comment on this blog post and I would try to clear your doubts.

I hope you learned something. Happy Exploring!

Read Next:
ESP32 Robot Car Using Websockets
Create your own ESP32 Wifi Car using the library esp32-wifi-car

If you like my post then please consider sharing this. Thanks!

4 responses to “Building a MicroPython Wifi Robot Car”

  1. sophie Avatar
    sophie

    Very nice project and well described ☺

    You give me ideas for others things with microdot

    Many thanks

    1. donsky Avatar
      donsky

      Yes, MicroDot is powerful! I am working on more project content soon. Happy exploring!

  2. How to Install MicroPython firmware on Raspberry Pi Pico?

    […] Content: Building a MicroPython Wifi Robot CarControl DS18B20 using MicroPython with a Weather Station […]

  3. How to enable Websockets in Mosquitto? – donskytech.com

    […] Next: Building a MicroPython Wifi Robot CarUsing Arduino with BME280 plus a weather station […]

Leave a Reply

Your email address will not be published. Required fields are marked *