Introduction
This post will show you how to create a Raspberry Pi Pico WiFi robot car that you can control thru your mobile phone. We will be using MicroPython in programming our Raspberry Pi Pico W and use the MicroDot library for the WebSocket message exchange. 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.
Raspberry Pi Pico W WiFi Robot Car
We are going to build our own WiFi-controlled robot car using simple components without the need for you to buy a pricy car kit. The movement and directions will be controlled by our MicroPython code using our own D-Pad controller on our mobile phone.
At the same time, we will be using the MicroDot library in controlling our WiFi robot car. Using this library will help us focus on developing the user interface and the controls of our project without giving focus on how to create a Web Server.
Design
The image above is the overall design of our Raspberry Pi Pico WiFi Robot Car project.
The following are the 3 important components that power our project:
- Robot Car Chassis
- Raspberry Pi Pico W
- MicroDot web server
You can select any cheap robot car chassis available around you. The MicroDot web server is deployed inside the file system of our Raspberry Pi Pico W.
The MicroDot Web Server creates a web application that we can 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 WiFi robot car by inspecting the WebSocket messages that we are receiving from our web application.
Parts/Components Required
The following are the components needed to follow along with this MicroPython robot car project.
- Raspberry Pi Pico W – Amazon | AliExpress
- Robot Car Chassis with Gear TT Motor (2 Wheels) – Amazon | AliExpress
- DRV883 H -Bridge Motor Driver – Amazon | AliExpress
- LM2596 DC-DC Buck Converter – Amazon | AliExpress
- Battery Pack (I used 3 18650 batteries) – Amazon | AliExpress | Bangood
- Breadboard – Amazon | AliExpress | Bangood
- Jumper Wires – Amazon | AliExpress | Bangood
Disclosure: These are affiliate links and I will earn small commissions to support my site when you buy through these links.
Prerequisites
You should have installed the latest MicroPython firmware on your Raspberry Pi Pico W.
Related Content:
How to Install MicroPython Firmware on Raspberry Pi Pico?
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
The below image shows the wiring and the schematic that I have followed for this project.
Also, the wiring table summary for quick reference. If your battery or power source is less than 5.5V then you can remove the LM2596 DC-DC buck converter and connect it directly to the VSYS pin (GPIO39) of your Raspberry Pi Pico W.
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/raspberry-pi-pico-w-wifi-robot-car.git
cd raspberry-pi-pico-w-wifi-robot-car
The image below is the project file layout of our program.
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 WiFi Robot Car object. The logic to control the motor driver DRV8833 H-Bridge Motor Driver is embedded in this file.
Let us go through and explain in detail the code for each file.
A little note about the DRV8833 H-Bridge Motor Driver
The DRV8833 H-Bridge motor driver is such a very nice motor controller even with a very small form factor compared to the traditional LM298N or L293D modules. We will be using Pulse Width Modulation (PWM) in driving this module.
This motor driver however has two modes when you program it which are the Fast Decay and Slow Decay options. Both are useful depending on the type of circuit that you are driving. In our case for this Raspberry Pi Pico W WiFi robot car, we will be using the Slow Decay mode. The Slow Decay Mode is ideal as we want the DC motors of our robot car chassis to stop instantaneously when we command it to stop.
As we will be using PWM in driving the input pins of this H-Bridge motor controller then we will be using the below table from the DRV8833 datasheet.
If you are not familiar with this motor driver then please see this post that I have written about it.
Related Content:
Raspberry Pi Pico W Motor Control using the DRV8833
robot_car.py
We have created a dedicated class that will be our interface to our robot car chassis.
from machine import Pin
from machine import PWM
import utime
"""
Class to represent our robot car
"""
class RobotCar:
MAX_DUTY_CYCLE = 65535
MIN_DUTY_CYCLE = 0
MAX_DUTY_CYCLE_SETTINGS = 30000
MIN_DUTY_CYCLE_SETTINGS = 55000
MAX_SPEED_VALUE = 100
MIN_SPEED_VALUE = 0
def __init__(self, motor_pins, frequency=20000):
self.left_motor_pin1 = PWM(Pin(motor_pins[0], mode=Pin.OUT))
self.left_motor_pin2 = PWM(Pin(motor_pins[1], mode=Pin.OUT))
self.right_motor_pin1 = PWM(Pin(motor_pins[2], mode=Pin.OUT))
self.right_motor_pin2 = PWM(Pin(motor_pins[3], mode=Pin.OUT))
# set PWM frequency
self.left_motor_pin1.freq(frequency)
self.left_motor_pin2.freq(frequency)
self.right_motor_pin1.freq(frequency)
self.right_motor_pin2.freq(frequency)
# Initialize the current speed to 100% or 30000 (the lower the PWM the faster the motor)
self.current_speed = RobotCar.MAX_SPEED_VALUE
self.current_duty_cycle = RobotCar.MAX_DUTY_CYCLE_SETTINGS
def move_forward(self):
print("Car is moving forward")
self.left_motor_pin1.duty_u16(RobotCar.MAX_DUTY_CYCLE) # 1 (HIGH)
self.left_motor_pin2.duty_u16(self.__get_current_duty_cycle()) # PWM
self.right_motor_pin1.duty_u16(RobotCar.MAX_DUTY_CYCLE) # 1 (HIGH)
self.right_motor_pin2.duty_u16(self.__get_current_duty_cycle()) # PWM
def move_backward(self):
print("Car is moving backward...")
self.left_motor_pin1.duty_u16(self.__get_current_duty_cycle()) # PWM
self.left_motor_pin2.duty_u16(RobotCar.MAX_DUTY_CYCLE) # 1 (HIGH)
self.right_motor_pin1.duty_u16(self.__get_current_duty_cycle()) # PWM
self.right_motor_pin2.duty_u16(RobotCar.MAX_DUTY_CYCLE) # 1 (HIGH)
def turn_left(self):
print("Car is moving left...")
self.left_motor_pin1.duty_u16(RobotCar.MAX_DUTY_CYCLE) # OFF (Not Moving)
self.left_motor_pin2.duty_u16(RobotCar.MAX_DUTY_CYCLE) # OFF (Not Moving)
self.right_motor_pin1.duty_u16(RobotCar.MAX_DUTY_CYCLE) # 1 (HIGH)
self.right_motor_pin2.duty_u16(self.__get_current_duty_cycle()) # PWM
def turn_right(self):
print("Car is moving right...")
self.left_motor_pin1.duty_u16(RobotCar.MAX_DUTY_CYCLE) # 1 (HIGH)
self.left_motor_pin2.duty_u16(self.__get_current_duty_cycle()) # PWM
self.right_motor_pin1.duty_u16(RobotCar.MAX_DUTY_CYCLE) # OFF (Not Moving)
self.right_motor_pin2.duty_u16(RobotCar.MAX_DUTY_CYCLE) # OFF (Not Moving)
def stop(self):
print("Stopping car...")
self.left_motor_pin1.duty_u16(RobotCar.MAX_DUTY_CYCLE)
self.left_motor_pin2.duty_u16(RobotCar.MAX_DUTY_CYCLE)
self.right_motor_pin1.duty_u16(RobotCar.MAX_DUTY_CYCLE)
self.right_motor_pin2.duty_u16(RobotCar.MAX_DUTY_CYCLE)
"""
Map duty cycle values from 0-100 to duty cycle 30000-55000
The lower the PWM, the faster the motor moves
0 - 55000
50 - 42500
100 - 30000
"""
def __map_range(self, x, in_min, in_max, out_min, out_max):
return (x - in_min) * (out_max - out_min) // (in_max - in_min) + out_min
""" new_speed is a value from 0% - 100% """
def change_speed(self, new_speed):
# Compute the current duty cyle based on the new_speed percentage
self.current_duty_cycle = self.__map_range(
int(new_speed),
RobotCar.MIN_SPEED_VALUE,
RobotCar.MAX_SPEED_VALUE,
RobotCar.MIN_DUTY_CYCLE_SETTINGS,
RobotCar.MAX_DUTY_CYCLE_SETTINGS,
)
self.current_speed = new_speed
def get_current_speed(self):
return self.current_speed
def __get_current_duty_cycle(self):
return self.current_duty_cycle
def deinit(self):
"""deinit PWM Pins"""
print("Deinitializing PWM Pins")
self.stop()
utime.sleep_us(1)
self.left_motor_pin1.deinit()
self.left_motor_pin2.deinit()
self.right_motor_pin1.deinit()
self.right_motor_pin2.deinit()
As you can see, the class has methods that mirror a real-life car object. It has methods named move_forward()
, move_backward()
, turn_left()
, turn_right
, and stop()
. Let us go over what each line of code does.
Import packages
from machine import Pin
from machine import PWM
import utime
Import the necessary packages, especially the PWM. We will be using PWM to control the speed of our Raspberry Pi Pico W WiFi robot car.
Define our RobotCar class
class RobotCar:
MAX_DUTY_CYCLE = 65535
MIN_DUTY_CYCLE = 0
MAX_DUTY_CYCLE_SETTINGS = 30000
MIN_DUTY_CYCLE_SETTINGS = 55000
MAX_SPEED_VALUE = 100
MIN_SPEED_VALUE = 0
def __init__(self, motor_pins, frequency=20000):
self.left_motor_pin1 = PWM(Pin(motor_pins[0], mode=Pin.OUT))
self.left_motor_pin2 = PWM(Pin(motor_pins[1], mode=Pin.OUT))
self.right_motor_pin1 = PWM(Pin(motor_pins[2], mode=Pin.OUT))
self.right_motor_pin2 = PWM(Pin(motor_pins[3], mode=Pin.OUT))
# set PWM frequency
self.left_motor_pin1.freq(frequency)
self.left_motor_pin2.freq(frequency)
self.right_motor_pin1.freq(frequency)
self.right_motor_pin2.freq(frequency)
# Initialize the current speed to 100% or 30000 (the lower the PWM the faster the motor)
self.current_speed = RobotCar.MAX_SPEED_VALUE
self.current_duty_cycle = RobotCar.MAX_DUTY_CYCLE_SETTINGS
We declare the RobotCar class here and defined the constructor of our class. It is expected that we are passing a list of motor pins and the chosen frequency.
These are constant class variables that we have defined and here are their uses.
- MAX_DUTY_CYCLE/MIN_DUTY_CYCLE – these are the maximum/minimum duty cycle that you can assign in a MicroPython PWM pin when you call the method PWM.duty_16().
- MAX_DUTY_CYCLE_SETTINGS/MIN_DUTY_CYCLE_SETTINGS – these are the maximum and minimum PWM duty cycles that we can assign that will make the DC motor become much more responsive. The lower the PWM values the faster the motor.
- MAX_SPEED_VALUE/MIN_SPEED_VALUE – these are values that will control the speed of our raspberry pi pico w robot car that we set using our web application. The values are in the range of 1 – 100.
The initial speed of our robot car is also defined here as well as the current duty cycle that will be assigned per motor. The variablescurrent_speed
and current_duty_cycle
are proportional to each other.
RobotCar movement
def move_forward(self):
print("Car is moving forward")
self.left_motor_pin1.duty_u16(RobotCar.MAX_DUTY_CYCLE) # 1 (HIGH)
self.left_motor_pin2.duty_u16(self.__get_current_duty_cycle()) # PWM
self.right_motor_pin1.duty_u16(RobotCar.MAX_DUTY_CYCLE) # 1 (HIGH)
self.right_motor_pin2.duty_u16(self.__get_current_duty_cycle()) # PWM
def move_backward(self):
print("Car is moving backward...")
self.left_motor_pin1.duty_u16(self.__get_current_duty_cycle()) # PWM
self.left_motor_pin2.duty_u16(RobotCar.MAX_DUTY_CYCLE) # 1 (HIGH)
self.right_motor_pin1.duty_u16(self.__get_current_duty_cycle()) # PWM
self.right_motor_pin2.duty_u16(RobotCar.MAX_DUTY_CYCLE) # 1 (HIGH)
def turn_left(self):
print("Car is moving left...")
self.left_motor_pin1.duty_u16(RobotCar.MAX_DUTY_CYCLE) # OFF (Not Moving)
self.left_motor_pin2.duty_u16(RobotCar.MAX_DUTY_CYCLE) # OFF (Not Moving)
self.right_motor_pin1.duty_u16(RobotCar.MAX_DUTY_CYCLE) # 1 (HIGH)
self.right_motor_pin2.duty_u16(self.__get_current_duty_cycle()) # PWM
def turn_right(self):
print("Car is moving right...")
self.left_motor_pin1.duty_u16(RobotCar.MAX_DUTY_CYCLE) # 1 (HIGH)
self.left_motor_pin2.duty_u16(self.__get_current_duty_cycle()) # PWM
self.right_motor_pin1.duty_u16(RobotCar.MAX_DUTY_CYCLE) # OFF (Not Moving)
self.right_motor_pin2.duty_u16(RobotCar.MAX_DUTY_CYCLE) # OFF (Not Moving)
def stop(self):
print("Stopping car...")
self.left_motor_pin1.duty_u16(RobotCar.MAX_DUTY_CYCLE)
self.left_motor_pin2.duty_u16(RobotCar.MAX_DUTY_CYCLE)
self.right_motor_pin1.duty_u16(RobotCar.MAX_DUTY_CYCLE)
self.right_motor_pin2.duty_u16(RobotCar.MAX_DUTY_CYCLE)
The following functions will dictate how the motors in our robot car chassis will move. The idea is really simple and that is to follow the DRV8833 input table above.
For example, if we take a look at the table above and if we want to move the dc motor forward in slow decay mode then we could send the xIN1 to 1 and xIN2 to PWM. The function move_forward()
is shown below. Since we have two DC motors then the same code is applied to both the left and right motors.
def move_forward(self):
print("Car is moving forward")
self.left_motor_pin1.duty_u16(RobotCar.MAX_DUTY_CYCLE) # 1 (HIGH)
self.left_motor_pin2.duty_u16(self.__get_current_duty_cycle()) # PWM
self.right_motor_pin1.duty_u16(RobotCar.MAX_DUTY_CYCLE) # 1 (HIGH)
self.right_motor_pin2.duty_u16(self.__get_current_duty_cycle()) # PWM
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.
Utility functions
def __map_range(self, x, in_min, in_max, out_min, out_max):
return (x - in_min) * (out_max - out_min) // (in_max - in_min) + out_min
""" new_speed is a value from 0% - 100% """
def change_speed(self, new_speed):
# Compute the current duty cyle based on the new_speed percentage
self.current_duty_cycle = self.__map_range(
int(new_speed),
RobotCar.MIN_SPEED_VALUE,
RobotCar.MAX_SPEED_VALUE,
RobotCar.MIN_DUTY_CYCLE_SETTINGS,
RobotCar.MAX_DUTY_CYCLE_SETTINGS,
)
self.current_speed = new_speed
def get_current_speed(self):
return self.current_speed
def __get_current_duty_cycle(self):
return self.current_duty_cycle
def deinit(self):
"""deinit PWM Pins"""
print("Deinitializing PWM Pins")
self.stop()
utime.sleep_us(1)
self.left_motor_pin1.deinit()
self.left_motor_pin2.deinit()
self.right_motor_pin1.deinit()
self.right_motor_pin2.deinit()
The function __map_range()
is our utility function that converts values 1 to 100 into our PWM values. The higher the speed values then the lower the PWM duty cycle assignment is. Based on my testing the 30000-55000 range is what makes the DC motors responsive on my setup. Anything higher or lower leads to the motor not being controllable as it will not stop when I say that it should stop.
Speed | PWM Duty Cycle |
---|---|
100 | 30000 |
50 | 42500 |
0 | 55000 |
In order for us to change the speed of our Raspberry Pi Pico W-powered robot car then we need the function change_speed()
. As mentioned above, I am expecting 1-100 coming from our web application. We then use the function __map_range()
to convert it to the appropriate duty cycle.
The functions get_current_speed()
and __get_current_duty_cycle()
just returns the current class instance variables. Once we exit our program then we call the function deinit()
to stop the GPIO pins from outputting PWM signals.
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 display our D-Pad controller thru our web application. It also handles the WebSocket message exchange.
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"
# Pico W GPIO Pin
LEFT_MOTOR_PIN_1 = 16
LEFT_MOTOR_PIN_2 = 17
RIGHT_MOTOR_PIN_1 = 18
RIGHT_MOTOR_PIN_2 = 19
motor_pins = [LEFT_MOTOR_PIN_1, LEFT_MOTOR_PIN_2, RIGHT_MOTOR_PIN_1, RIGHT_MOTOR_PIN_2]
# Create an instance of our robot car
robot_car = RobotCar(motor_pins, 20000)
car_commands = {
"forward": robot_car.move_forward,
"reverse": robot_car.move_backward,
"left": robot_car.turn_left,
"right": robot_car.turn_right,
"stop": robot_car.stop,
}
# App Route
@app.route("/")
async def index(request):
print(f"Current Speed: {robot_car.get_current_speed()}")
return render_template("index.html", current_speed=robot_car.get_current_speed())
@app.route("/ws")
@with_websocket
async def executeCarCommands(request, ws):
while True:
websocket_message = await ws.receive()
if "speed" in websocket_message:
# WebSocket message format: "speed : 20"
speedMessage = websocket_message.split(":")
robot_car.change_speed(speedMessage[1])
else:
command = car_commands.get(websocket_message)
if command is not None:
command()
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.deinit()
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
# Pico W GPIO Pin
LEFT_MOTOR_PIN_1 = 16
LEFT_MOTOR_PIN_2 = 17
RIGHT_MOTOR_PIN_1 = 18
RIGHT_MOTOR_PIN_2 = 19
motor_pins = [LEFT_MOTOR_PIN_1, LEFT_MOTOR_PIN_2, RIGHT_MOTOR_PIN_1, RIGHT_MOTOR_PIN_2]
We define the Raspberry Pi Pico W GPIO pins that we will use to control the movement and speed of our robot cars’ DC motors.
# Create an instance of our robot car
robot_car = RobotCar(motor_pins, 20000)
car_commands = {
"forward": robot_car.move_forward,
"reverse": robot_car.move_backward,
"left": robot_car.turn_left,
"right": robot_car.turn_right,
"stop": robot_car.stop,
}
We create an instance of our RobotCar and pass in the motor pins and the frequency of the PWM
I have defined a Python dictionary car_commands
here to handle WebSocket messages exchange. 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 Command | Description |
---|---|
forward | Move the car forward |
reverse | Move the car in reverse |
left | Turn Left the car |
right | Turn right the car |
stop | Stop the car |
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):
print(f"Current Speed: {robot_car.get_current_speed()}")
return render_template("index.html", current_speed=robot_car.get_current_speed())
We define our index or root route here and it will serve the index.html page which will display our own D-pad controller. Also, we passed in the current speed of our robot car so that we could display it on our controller.
@app.route("/ws")
@with_websocket
async def executeCarCommands(request, ws):
while True:
websocket_message = await ws.receive()
if "speed" in websocket_message:
# WebSocket message format: "speed : 20"
speedMessage = websocket_message.split(":")
robot_car.change_speed(speedMessage[1])
else:
command = car_commands.get(websocket_message)
if command is not None:
command()
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.change_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.
If you want to see a sample of this WebSocket message exchange then please see the below image where we use the Chrome Developer Tools to inspect the messages 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)
if __name__ == "__main__":
try:
app.run()
except KeyboardInterrupt:
robot_car.deinit()
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/<path:path>"
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.deinit()
This is the entry point of our program and this will run our MicroDot web server. Also, take note of robot_car.deinit()
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 controller which I have forked from one of the code pens I have done earlier. 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.
{% args current_speed %}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Raspberry Pi Pico 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 blue" 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>Raspberry Pi Pico W WiFi Robot Car</h2>
</div>
<div class="row">
<div class="col c12 controller">
<div class="set blue">
<div>
<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>
</div>
</div>
<div class="range-slider">
<h2>Speed</h2>
<input
class="input-range"
type="range"
step="1"
value="{{ current_speed }}"
min="1"
max="100"
id="currentSpeed"
/>
<span class="speed-value" id="speedValue"
>{% if current_speed %}{{ current_speed }}{% endif %}</span
>
</div>
</div>
</div>
</div>
<script src="../static/js/custom.js"></script>
</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.
{% args current_speed %}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Raspberry Pi Pico 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 CSS imports. We are also expecting an argument to be passed into this template which is the current_speed.
<nav class="nav blue" 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>Raspberry Pi Pico W WiFi Robot Car</h2>
</div>
We define the navigation at the top of the page including our title.
<div class="row">
<div class="col c12 controller">
<div class="set blue">
<div>
<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>
</div>
</div>
<div class="range-slider">
<h2>Speed</h2>
<input
class="input-range"
type="range"
step="1"
value="{{ current_speed }}"
min="1"
max="100"
id="currentSpeed"
/>
<span class="speed-value" id="speedValue"
>{% if current_speed %}{{ current_speed }}{% endif %}</span
>
</div>
</div>
</div>
</div>
<script src="../static/js/custom.js"></script>
</body>
</html>
This is the section where we draw our D-Pad controller and the range slider that we are using to control the speed.
The script custom.js is imported to 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 controller are stylized by this file.
static/js/custom.js
The custom.js code is below and it handles the WebSocket message exchanges between our controller and our robot car.
var websocket;
window.addEventListener("load", onLoad);
function onLoad() {
initializeSocket();
}
function initializeSocket() {
console.log(
"Opening WebSocket connection to Raspberry Pi Pico W MicroPython Server..."
);
var targetUrl = `ws://${location.host}/ws`;
websocket = new WebSocket(targetUrl);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage;
websocket.onerror = onError;
}
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 onError(event) {
console.log(
"Error encountered while communicating with WebSocket server..",
event
);
}
function sendMessage(message) {
websocket.send(message);
}
/*
Speed Settings Handler
*/
const speed = document.querySelector("#currentSpeed");
const speedValue = document.querySelector("#speedValue");
speed.addEventListener("input", () => {
speedValue.innerHTML = speed.value;
sendMessage("speed : " + speed.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);
});
We will run through what each line of code does as this is the most important part of our user interface.
var websocket;
window.addEventListener("load", onLoad);
function onLoad() {
initializeSocket();
}
First, we define our WebSocket client object which we will use to communicate with our WebSocket 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 function initializeSocket()
whose primary job is to open the WebSocket connection.
function initializeSocket() {
console.log(
"Opening WebSocket connection to Raspberry Pi Pico W MicroPython Server..."
);
var targetUrl = `ws://${location.host}/ws`;
websocket = new WebSocket(targetUrl);
websocket.onopen = onOpen;
websocket.onclose = onClose;
websocket.onmessage = onMessage;
websocket.onerror = onError;
}
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 onError(event) {
console.log(
"Error encountered while communicating with WebSocket server..",
event
);
}
function sendMessage(message) {
websocket.send(message);
}
The functioninitializeSocket()
opens a WebSocket connection to our web server and then attached event handlers to it using the onopen
, onclose
, and onmessage
The function sendMessage()
is where we send the actual WebSocket message.
/*
Speed Settings Handler
*/
const speed = document.querySelector("#currentSpeed");
const speedValue = document.querySelector("#speedValue");
speed.addEventListener("input", () => {
speedValue.innerHTML = speed.value;
sendMessage("speed : " + speed.value);
});
We add an event handler to our range slider such that when we change the speed then we send a WebSocket message to our server to adjust the speed settings of our robot car.
/*
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 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 functions touchStartHandler()
and touchEndHandler()
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 controller. Specifically, we are listening to the events touchstart
and touchend
.
As a side note, touch events are only applicable to our mobile phones that support touch gestures so it will not work on your laptop browser unless you use the developer tools and set it to become mobile responsive.
MicroDot specific files
The MicroDot-specific files are copied from the GitHub repository of the said project. We are making use of the Asynchronous MicroDot, 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 to 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.
Next, install the uTemplate library on your MicroPython device. If you are unsure how this is done then you can follow my How to install MicroPython libraries or packages in Thonny IDE?
Lastly, upload all the files to your MicroPython device
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 on 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 controller and verify if it is working. Change the speed of your motor by moving the range slider.
Have fun playing with your shiny Raspberry Pi Pico W WiFi robot car! 🙂
Wrap Up
This is quite a long post and contains lots of code but I hope you learn something from my Raspberry Pi Pico W-powered 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:
Raspberry Pi Pico W: BME280 Weather Station Dashboard
Pico W -MicroPython MQTT – BMP/BME 280 Weather Station
Leave a Reply