Ultrasonic Sonar Scanner with Raspberry Pi Pico Clone
Table of Contents
Description
On one sleeeless night, I was browsing deals on Aliexpress, like one usually does. A bundle deal offered me an Ultrasonic Module HC-SR04, which was a very interesting deal. But I had no idea what I wanted to build with it. A few clicks later, after I saw a crazy $3 offer for a Raspberry Pi Pico W clone, I knew exactly what I wanted to build! What about a rotating sonar scanner? Sounds interesting, right? Lastly, I snapped up a small servo and a motor driver, just in case I wanted to try some leftover DC motors I have.
Project roadmap:
- On the Pico, separate the web server and servo control / sensor reading into different cores.
- Build a frontend. We want a nice UI that will periodically fetch data from our Pico and display the current reading and servo rotation.
Raspberry Pi Pico
A very interesting microcontroller… you can code it in MicroPython to make things easy, and you even have 2 cores on it. But I don’t really understand the decision to operate it on 3.3V. Maybe the goal is a minimal setup powered by one small 3.7V battery? My point is, most things I have operate on 5V minimum, so I need to be careful. Later, after discovering all the secrets of the Pico, I understood why this clone is so much cheaper than the original… but more about that later.
The servo made this project much easier. After reading the documentation on how to use it, all I needed to do was write a function that rotates the servo to the angle I need. The code for the ultrasonic sensor is copy-paste from the documentation too: set high/low on the trigger pin and then catch the echo. The last thing I needed was to use the built-in Pico library for WiFi, and I would be done…
And this is the point where I discovered why this clone is so cheap. It’s not a Pico W… it’s a Pico with a soldered ESP WiFi chip on the board. Oh boy…
So my WiFi functionality is limited, but for this project it’s sufficient. To communicate with the WiFi chip on clone boards, you need to start a UART connection. Check your board, on mine, I had markings for ESP TX and ESP RX next to pins 0 and 1.
Frontend
Don’t overcomplicate things. It’s simple HTML/JS code, and I fetch data from the Pico every 150–300 ms.
Code
Frontend.zip
4.4 Kb
import machine
import time
import _thread
import math
from machine import UART, Pin, PWM, time_pulse_us
# ==========================================
# 1. CONFIGURATION
# ==========================================
# --- Wi-Fi Settings ---
SSID = "PiedPiper"
PASSWORD = "ErlichBachmanIsAFat"
# --- Servo Calibration ---
# Freq: 50Hz. Standard duty is 1ms to 2ms (approx 1638 to 8192 in u16).
# Depending on your specific servo, you might need to widen these ranges.
SERVO_PIN = 15
SERVO_MIN_US = 1638 # Value for 0 degrees
SERVO_MAX_US = 8192 # Value for 180 degrees
SCAN_DELAY = 0.1 # Time to wait for sensor between steps
# --- Ultrasonic Pins ---
TRIG_PIN = 16
ECHO_PIN = 17
# ==========================================
# 2. SHARED GLOBAL VARIABLES
# ==========================================
servo = PWM(Pin(SERVO_PIN))
servo.freq(50)
trig = Pin(TRIG_PIN, Pin.OUT)
echo = Pin(ECHO_PIN, Pin.IN)
# Thread Lock to prevent reading/writing at same time
data_lock = _thread.allocate_lock()
# The data to be served via JSON
shared_data = {
"dis": 0, # Distance in cm
"rot": 0 # Current Angle (0-180)
}
# ==========================================
# 3. CORE 1: MOTOR & SENSOR CONTROL
# ==========================================
def set_angle(angle):
duty = SERVO_MIN_US + (SERVO_MAX_US - SERVO_MIN_US) * (angle / 180)
servo.duty_u16(int(duty))
def get_distance():
dist_cm = 0
try:
trig.low()
time.sleep_us(2)
trig.high()
time.sleep_us(10)
trig.low()
# Timeout 30ms (approx 5 meters max) to prevent blocking
duration = time_pulse_us(echo, 1, 30000)
if duration > 0:
dist_cm = int((duration / 2) / 29.1)
if dist_cm > 200: dist_cm = 200 # Cap max distance
else:
dist_cm = -1 # Out of range
except OSError:
dist_cm = -1
return dist_cm
def core1_task():
trig.low()
time.sleep(0.1)
print("[Core 1] Radar Scan Started")
current_angle = 0
accm = 1
while True:
set_angle(current_angle)
time.sleep(SCAN_DELAY)
dist_cm = get_distance()
if current_angle >= 180: accm = -1
if current_angle <= 0: accm = 1
current_angle += accm
with data_lock:
shared_data["dis"] = dist_cm
shared_data["rot"] = current_angle
print(shared_data)
# ==========================================
# 4. CORE 0: WEB SERVER
# ==========================================
uart = UART(0, baudrate=115200, tx=Pin(0), rx=Pin(1))
def send_at(cmd, wait=1):
uart.write(cmd + '\r\n')
time.sleep(wait)
if uart.any(): return uart.read()
return b""
def init_wifi():
print("[Core 0] Init Wi-Fi...")
send_at("AT+RST", 2) # Reset module
send_at("AT+CWMODE=1")
send_at(f'AT+CWJAP="{SSID}","{PASSWORD}"', 8)
ip_info = send_at("AT+CIFSR", 2)
print("[Core 0] IP info:", ip_info)
send_at("AT+CIPMUX=1")
send_at("AT+CIPSERVER=1,80")
print("[Core 0] Server Ready.")
def server_loop():
buffer = ""
while True:
if uart.any():
try:
chunk = uart.read().decode('utf-8', 'ignore')
buffer += chunk
# Check for Request
if "+IPD," in buffer:
# Parse Connection ID (usually the char after +IPD,)
idx = buffer.find("+IPD,")
conn_id = buffer[idx + 5]
# --- READ SHARED DATA SAFELY ---
local_dis = 0
local_rot = 0
with data_lock:
local_dis = shared_data["dis"]
local_rot = shared_data["rot"]
# --- BUILD RESPONSE ---
# We are sending the simplest Valid JSON
json_body = '{"dis":' + str(local_dis) + ',"rot":' + str(local_rot) + '}'
header = "HTTP/1.1 200 OK\r\n"
header += "Content-Type: application/json\r\n"
header += "Access-Control-Allow-Origin: *\r\n" # CORS
header += "Connection: close\r\n"
header += "Content-Length: " + str(len(json_body)) + "\r\n\r\n"
full_resp = header + json_body
# Send Command
uart.write(f"AT+CIPSEND={conn_id},{len(full_resp)}\r\n")
time.sleep(0.02) # Short wait for prompt >
uart.write(full_resp)
# Close Command
time.sleep(0.02)
uart.write(f"AT+CIPCLOSE={conn_id}\r\n")
buffer = "" # Clear buffer for next request
except Exception as e:
# If UART garbage causes error, reset buffer
print("Error:", e)
buffer = ""
# ==========================================
# 5. EXECUTION START
# ==========================================
print("Reset radar position")
set_angle(0)
time.sleep(0.5)
# Run Core 0 (Wi-Fi) in main thread
init_wifi()
time.sleep(10)
# Start Core 1 (Servo/Sensor) in background
_thread.start_new_thread(core1_task, ())
server_loop() Sources
JiriBilek
WiFi library for RP2040 Pico-W with the ESP8285 chip
https://github.com/JiriBilek/RP2040_PicoW_ESP8285_LibraryThe Engineering Mindset
Servo Motors, how do they work?
https://youtu.be/1WnGv-DPexcAliexpress
TZT 123 - Raspberry Pi Pico
https://www.aliexpress.com/item/1005009171613000.html