How to Use ESP32 + MicroPython to Automate Anything
The world of IoT and automation is vast, but often intimidating due to its blend of hardware and software. Enter the ESP32, a remarkably versatile and inexpensive microcontroller with Wi-Fi and Bluetooth, and MicroPython, a lean implementation of Python 3 optimized for microcontrollers. Together, they form an incredibly potent combination for rapid prototyping and deploying robust automation solutions.
This isn’t a theoretical deep dive. This is a pragmatic, hands-on guide for developers who learn by doing. We’ll start from scratch and build up to real-world examples, all from your command line.
Why ESP32 + MicroPython?
- Python Simplicity: Leverage your existing Python knowledge for embedded development. No complex C/C++ build systems, cryptic syntax, or obscure pointers.
- Rapid Prototyping: Iterate quickly. Write code, upload, test. It’s that fast.
- Connectivity Built-in: Wi-Fi and Bluetooth Low Energy (BLE) on a single, low-cost chip, opening doors to networked automation.
- Powerful Hardware: Dual-core processor, ample RAM/Flash, and a rich peripheral set (ADC, DAC, I2C, SPI, UART, PWM, etc.).
- Cost-Effective: ESP32 development boards are incredibly cheap, making experimentation accessible.
If you’ve ever wanted to control physical devices, read sensors, or build your own smart home gadgets without getting bogged down in low-level details, you’re in the right place.
Prerequisites
Before we begin, ensure you have the following:
- ESP32 Development Board: Any common board like the ESP32-WROOM-32 DevKitC, ESP32-S series, or NodeMCU-32S will work. Ensure it has a USB port for power and data.
- USB A-to-Micro B or USB A-to-C Cable: Depends on your ESP32 board. Crucially, it must be a data cable, not just a charging cable.
- Python 3: Installed on your host machine (Windows, macOS, Linux).
pip
: Python’s package installer, usually bundled with Python 3.
Step 1: Flashing MicroPython Firmware
The first order of business is to get MicroPython onto your ESP32. This process involves erasing the existing firmware and then writing the MicroPython .bin
file. We’ll use esptool.py
, Espressif’s official utility.
1.1 Install esptool.py
Open your terminal and install esptool.py
using pip
:
pip install esptool
Collecting esptool
Downloading esptool-4.7.0-py3-none-any.whl (124 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 124.9/124.9 kB 5.9 MB/s eta 0:00:00
Collecting ecdsa<0.19.0,>=0.16.0 (from esptool)
Downloading ecdsa-0.18.0-py2.py3-none-any.whl (142 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 142.1/142.1 kB 8.7 MB/s eta 0:00:00
... (other dependencies) ...
Successfully installed ecdsa-0.18.0 esptool-4.7.0 pyyaml-6.0.1
1.2 Download MicroPython Firmware
Head to the official MicroPython downloads page for ESP32: micropython.org/download/esp32/.
Download the latest stable .bin
file (e.g., esp32-20231026-v1.21.0.bin
). Place it in a convenient directory, like ~/Downloads/
.
1.3 Identify Your ESP32’s Serial Port
Connect your ESP32 board to your computer via the USB cable.
- Linux: Check
dmesg
orls /dev/tty*
. It will likely appear as/dev/ttyUSB0
or/dev/ttyACM0
. - macOS: Check
ls /dev/cu.*
. It will often be/dev/cu.usbserial-XXXX
or/dev/cu.SLAB_USBtoUART
. - Windows: Check Device Manager under “Ports (COM & LPT)”. It will be
COMx
(e.g.,COM3
).
For the rest of this guide, I’ll assume your port is /dev/ttyUSB0
. Adjust accordingly.
1.4 Erase Flash
It’s good practice to erase the entire flash memory before flashing new firmware. This prevents conflicts from previous installations.
esptool.py --port /dev/ttyUSB0 erase_flash
esptool.py v4.7.0
Serial port /dev/ttyUSB0
Connecting...
Chip is ESP32-D0WDQ6 (revision 1)
Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme V1
Crystal is 40MHz
MAC: ac:67:b2:0a:b2:ac
Uploading stub...
Running stub...
Stub running...
Erasing flash (this may take a while)...
Chip erase completed successfully in 8.0s
Hard resetting via RTS pin...
Note: If Connecting...
fails, try holding down the BOOT
or FLASH
button on your ESP32 board while executing the command, then release it once Connecting...
appears. Some boards (especially older NodeMCU-32S) might require this.
1.5 Flash MicroPython Firmware
Now, flash the downloaded .bin
file. Replace esp32-latest.bin
with the actual filename and path to your downloaded firmware.
esptool.py --port /dev/ttyUSB0 --baud 460800 write_flash --flash_size=detect 0 ~/Downloads/esp32-latest.bin
We use --baud 460800
for faster flashing, and --flash_size=detect
to automatically determine your board’s flash size. The 0
indicates the memory address where the firmware starts.
esptool.py v4.7.0
Serial port /dev/ttyUSB0
Connecting...
Chip is ESP32-D0WDQ6 (revision 1)
Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme V1
Crystal is 40MHz
MAC: ac:67:b2:0a:b2:ac
Uploading stub...
Running stub...
Stub running...
Flash params set to 0x0220 (DIO, 40MHz)
Configuring flash size...
Flash size will be detected automatically.
Compressed 1133376 bytes to 726889...
Wrote 1133376 bytes (726889 compressed) at 0x00000000 in 15.8 seconds (effective 573.7 kbit/s)...
Hash of data verified.
Leaving...
Hard resetting via RTS pin...
Congratulations! Your ESP32 now runs MicroPython.
Step 2: Interacting with the ESP32 - The REPL
REPL stands for Read-Eval-Print Loop. It’s an interactive prompt, similar to Python on your computer, but running directly on the ESP32. This is your primary way to interact, test code, and debug on the device.
2.1 Connect via Serial Terminal
You’ll need a serial terminal program.
- Linux:
picocom
orscreen
(install via your package manager, e.g.,sudo apt install picocom
). - macOS:
screen
is pre-installed. - Windows: PuTTY is a common choice, or use
pyserial
with a simple Python script. For command-line consistency, considerminicom
if you use WSL, or follow theampy
examples which abstract away direct serial interaction once files are on the device. I’ll usepicocom
for demonstration.
picocom --baud 115200 /dev/ttyUSB0
Once connected, press Ctrl+J
(Enter) a few times to get the >>>
prompt. If nothing happens, press the EN
(Enable/Reset) button on your ESP32 board.
picocom v3.1
port is : /dev/ttyUSB0
flow control : none
baudrate is : 115200
parity is : none
databits are : 8
stopbits are : 1
escape is : C-a
local echo is : no
noinit is : no
noreset is : no
nolock is : no
send_cmd is : sz -vv
receive_cmd is : rz -vv -E
imap is :
omap is :
emap is : crcrlf
logfile is : none
initstring : none
exitstring : none
exit with C-x C-q, C-c C-x
Type C-a C-h for help.
>>>
MicroPython v1.21.0 on 2023-10-26; ESP32 module with ESP32
Type "help()" for more information.
>>>
Common REPL Commands:
help()
: Get general help.help('modules')
: List available built-in modules.import machine
: Import the core hardware control module.dir(machine)
: See what functions/classes are available in themachine
module.Ctrl+D
: Soft reboot the ESP32. This re-runsboot.py
andmain.py
if they exist.Ctrl+C
: Interrupt a running script.Ctrl+X
: Exitpicocom
.
2.2 Blink an LED (Hello World of Embedded)
Let’s make the onboard LED blink. Most ESP32 boards have an LED connected to GPIO2.
>>> from machine import Pin
>>> led = Pin(2, Pin.OUT)
>>> led.value(1) # Turn LED ON (or OFF, depending on board's wiring - active high/low)
>>> led.value(0) # Turn LED OFF (or ON)
To make it blink repeatedly:
>>> import time
>>> from machine import Pin
>>>
>>> led = Pin(2, Pin.OUT)
>>>
>>> while True:
... led.value(not led.value())
... time.sleep(0.5)
...
To stop this, press Ctrl+C
.
Step 3: Transferring Files with ampy
Typing code directly into the REPL is fine for testing, but impractical for larger scripts. We need a way to transfer files from our computer to the ESP32’s filesystem. ampy
(Adafruit MicroPython Tool) is an excellent choice.
3.1 Install ampy
pip install adafruit-ampy
Collecting adafruit-ampy
Downloading adafruit_ampy-1.0.1-py3-none-any.whl (16 kB)
... (dependencies) ...
Successfully installed adafruit-ampy-1.0.1 click-8.1.7 pyserial-3.5
3.2 Basic ampy
Commands
Ensure your serial terminal program (like picocom
) is not connected to the ESP32 when using ampy
, as ampy
needs exclusive access to the serial port.
ampy --port /dev/ttyUSB0 ls
: List files on the ESP32.ampy --port /dev/ttyUSB0 put <local_file> [remote_file]
: Upload a file.ampy --port /dev/ttyUSB0 get <remote_file> [local_file]
: Download a file.ampy --port /dev/ttyUSB0 rm <remote_file>
: Delete a file.ampy --port /dev/ttyUSB0 run <local_script>
: Run a local script on the ESP32 without saving it permanently.ampy --port /dev/ttyUSB0 reset
: Soft reset the board.
Let’s put our LED blink script into main.py
. MicroPython automatically runs boot.py
(for setup) and then main.py
(your main application code) on startup or soft reboot.
Create a file named main.py
on your computer:
# main.py
import time
from machine import Pin
# Pin 2 is often the onboard LED on ESP32 DevKits
# Check your board's schematic if it's different
led = Pin(2, Pin.OUT)
print("Starting LED blinker...")
try:
while True:
led.value(not led.value())
print(f"LED is now {'ON' if led.value() else 'OFF'}")
time.sleep(1) # Blink every 1 second
except KeyboardInterrupt:
print("Script interrupted.")
led.value(0) # Ensure LED is off on exit
Now, upload it:
ampy --port /dev/ttyUSB0 put main.py
Uploading main.py to /main.py...
After uploading, soft reset the ESP32 to run main.py
:
ampy --port /dev/ttyUSB0 reset
Soft resetting...
Now, re-connect with your serial terminal (e.g., picocom --baud 115200 /dev/ttyUSB0
) to see the output.
>>>
MicroPython v1.21.0 on 2023-10-26; ESP32 module with ESP32
Type "help()" for more information.
>>> Starting LED blinker...
LED is now ON
LED is now OFF
LED is now ON
LED is now OFF
...
You can now disconnect the terminal, and the ESP32 will continue blinking the LED as long as it’s powered. This is the foundation of automated tasks.
Step 4: Networking with MicroPython (Wi-Fi)
The ESP32’s built-in Wi-Fi is a game-changer for automation, allowing your devices to connect to the internet, communicate with other devices, or host web interfaces.
4.1 Connecting to Wi-Fi
The network
module handles Wi-Fi. It’s often best to put Wi-Fi connection logic in boot.py
so that main.py
can assume connectivity.
Create a boot.py
file on your computer:
# boot.py
import esp
import gc
import network
import time
# Disable debug output to save memory and reduce noise
esp.osdebug(None)
gc.collect()
# Your Wi-Fi credentials
SSID = 'YOUR_WIFI_SSID'
PASSWORD = 'YOUR_WIFI_PASSWORD'
print(f"Connecting to WiFi: {SSID}...")
sta_if = network.WLAN(network.STA_IF)
sta_if.active(True)
sta_if.connect(SSID, PASSWORD)
max_attempts = 10
for i in range(max_attempts):
if sta_if.isconnected():
print("WiFi connected!")
print('Network config:', sta_if.ifconfig())
break
print(f"Attempt {i+1}/{max_attempts}: Waiting for WiFi connection...")
time.sleep(1)
else:
print("Failed to connect to WiFi after multiple attempts.")
# Consider adding fallback logic or rebooting here
# machine.reset() # Uncomment to reset if connection fails
# Ensure the AP interface is disabled if not needed, saves memory
ap_if = network.WLAN(network.AP_IF)
ap_if.active(False)
print("Boot script finished.")
Replace YOUR_WIFI_SSID
and YOUR_WIFI_PASSWORD
with your actual Wi-Fi credentials.
Upload boot.py
:
ampy --port /dev/ttyUSB0 put boot.py
Uploading boot.py to /boot.py...
Now, reset the ESP32 and connect to the serial terminal to observe the connection process:
ampy --port /dev/ttyUSB0 reset
picocom --baud 115200 /dev/ttyUSB0
Soft resetting...
Connecting to WiFi: MyHomeNetwork...
Attempt 1/10: Waiting for WiFi connection...
Attempt 2/10: Waiting for WiFi connection...
WiFi connected!
Network config: ('192.168.1.100', '255.255.255.0', '192.168.1.1', '192.168.1.1')
Boot script finished.
>>> Starting LED blinker...
LED is now ON
...
You now have a connected ESP32!
4.2 Hosting a Simple Web Server
Let’s build a tiny web server on the ESP32 to control the LED. This demonstrates how your ESP32 can act as a local control point.
Modify your main.py
(after boot.py
ensures connection):
# main.py
import time
from machine import Pin
import socket # For networking
import network # To check if connected
# Pin 2 is often the onboard LED on ESP32 DevKits
led = Pin(2, Pin.OUT)
led_state = "OFF" # Keep track of the LED state
# Basic HTML page for controlling the LED
def web_page():
html = """<!DOCTYPE html>
<html>
<head>
<title>ESP32 LED Control</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: sans-serif; text-align: center; margin-top: 50px; }
.button {
display: inline-block;
padding: 15px 30px;
font-size: 24px;
cursor: pointer;
text-align: center;
text-decoration: none;
outline: none;
color: #fff;
background-color: #4CAF50;
border: none;
border-radius: 15px;
box-shadow: 0 9px #999;
}
.button:hover { background-color: #3e8e41 }
.button:active {
background-color: #3e8e41;
box-shadow: 0 5px #666;
transform: translateY(4px);
}
.off { background-color: #f44336; }
.off:hover { background-color: #da190b; }
</style>
</head>
<body>
<h1>ESP32 LED Control</h1>
<p>LED State: <strong>""" + led_state + """</strong></p>
<p>
<a href="/?led=on"><button class="button">LED ON</button></a>
<a href="/?led=off"><button class="button off">LED OFF</button></a>
</p>
</body>
</html>"""
return html
# Check if connected to Wi-Fi before starting the server
sta_if = network.WLAN(network.STA_IF)
if not sta_if.isconnected():
print("WiFi not connected. Web server will not start.")
else:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('', 80)) # Bind to port 80 (HTTP)
s.listen(5) # Listen for up to 5 connections
print(f"Web server active on http://{sta_if.ifconfig()[0]}/")
while True:
try:
conn, addr = s.accept()
print('Got a connection from %s' % str(addr))
request = conn.recv(1024)
request = str(request)
print('Content = %s' % request)
# Parse request to find 'led=on' or 'led=off'
led_on = request.find('/?led=on')
led_off = request.find('/?led=off')
if led_on == 6: # Index 6 means it's the main path
led.value(1)
led_state = "ON"
if led_off == 6:
led.value(0)
led_state = "OFF"
response = web_page()
conn.send('HTTP/1.1 200 OK\n')
conn.send('Content-Type: text/html\n')
conn.send('Connection: close\n\n')
conn.sendall(response)
conn.close()
except OSError as e:
conn.close()
print('Connection error:', e)
except KeyboardInterrupt:
print("Web server stopped by user.")
led.value(0) # Ensure LED is off
break
Upload the updated main.py
:
ampy --port /dev/ttyUSB0 put main.py
Uploading main.py to /main.py...
Reset the ESP32:
ampy --port /dev/ttyUSB0 reset
Now, check your serial output (picocom
) for the IP address. Open a web browser on your computer and navigate to http://<YOUR_ESP32_IP_ADDRESS>/
. You should see a simple page with ON/OFF buttons for the LED.
This is a powerful concept: your ESP32 can be a tiny server providing control over its attached hardware.
Step 5: Automating I/O - Sensors and Actuators
The machine
module is your interface to the ESP32’s hardware peripherals.
5.1 Digital Input (Button) and Output (LED)
Let’s combine a physical button with an LED. When you press the button, the LED state flips.
Hardware Setup:
- Connect an LED (with a 220-330 ohm resistor in series) between GPIO16 and GND (short leg of LED).
- Connect a push button between GPIO17 and GND. Use the internal pull-up resistor on GPIO17.
Update main.py
:
# main.py
import time
from machine import Pin
# Define pins
LED_PIN = 16 # Example: Connect LED (with resistor) to GPIO16
BUTTON_PIN = 17 # Example: Connect push button to GPIO17
led = Pin(LED_PIN, Pin.OUT)
# Configure button with internal PULL_UP resistor.
# When pressed, it connects to GND, so its value becomes 0.
button = Pin(BUTTON_PIN, Pin.IN, Pin.PULL_UP)
print("Monitoring button press...")
# Initial state for the LED
current_led_state = 0 # Off
led.value(current_led_state)
last_button_state = button.value()
debounce_time_ms = 50 # milliseconds for debounce
try:
while True:
# Read current button state
new_button_state = button.value()
# Check for state change (button pressed - going from HIGH to LOW)
if new_button_state != last_button_state:
# Simple debounce: wait a bit and re-read
time.sleep_ms(debounce_time_ms)
if new_button_state == button.value(): # Check if still changed
if new_button_state == 0: # Button pressed (active low)
current_led_state = 1 - current_led_state # Toggle state
led.value(current_led_state)
print(f"Button pressed! LED is now {'ON' if current_led_state else 'OFF'}")
last_button_state = new_button_state
time.sleep_ms(10) # Small delay to avoid busy-waiting too much
except KeyboardInterrupt:
print("Script interrupted.")
led.value(0) # Ensure LED is off
Upload and reset. Press the button, and the LED should toggle.
5.2 Analog Input (LDR Light Sensor)
The ESP32 has an Analog-to-Digital Converter (ADC) to read analog voltages, perfect for sensors like an LDR (Light Dependent Resistor) or potentiometer.
Hardware Setup:
- Connect an LDR in a voltage divider circuit.
- One side of LDR to 3.3V.
- The other side of LDR to a 10k Ohm resistor.
- The other side of the 10k Ohm resistor to GND.
- The point between the LDR and the 10k Ohm resistor connects to an ADC pin (e.g., GPIO34, GPIO35, GPIO36). These pins are input-only.
- Let’s use
GPIO34
.
Modify main.py
:
# main.py
import time
from machine import ADC, Pin
# ADC pin for LDR (e.g., GPIO34)
# Note: On some ESP32 boards, specific ADC pins are available.
# Common ones are GPIO32-39. GPIO34 is a good choice as it's input-only.
LDR_PIN = 34
adc = ADC(Pin(LDR_PIN))
# Set attenuation to increase voltage range (0-3.3V)
# ADC.ATTN_0DB: 0-1.0V
# ADC.ATTN_2_5DB: 0-1.25V
# ADC.ATTN_6DB: 0-1.5V
# ADC.ATTN_11DB: 0-3.3V (Recommended for full range)
adc.atten(ADC.ATTN_11DB)
# Set resolution (e.g., 12-bit for 0-4095 range)
adc.width(ADC.WIDTH_12BIT)
print("Reading LDR sensor values...")
try:
while True:
ldr_value = adc.read() # Read raw ADC value (0-4095 for 12-bit)
print(f"LDR Raw Value: {ldr_value}")
# You can map this to a more meaningful value (e.g., light intensity)
# For example, if 0 is max light and 4095 is min light:
light_intensity = round((1 - (ldr_value / 4095)) * 100, 2)
print(f"Estimated Light Intensity: {light_intensity}%")
time.sleep(1)
except KeyboardInterrupt:
print("Script interrupted.")
Upload, reset, and observe. Cover the LDR, and you should see the LDR Raw Value
change significantly.
5.3 I2C Communication (BMP280 Temperature/Pressure Sensor)
I2C (Inter-Integrated Circuit) is a common serial communication protocol for connecting multiple low-speed peripheral devices to a microcontroller. Most sensors use it.
Hardware Setup:
- BMP280 Module:
- VCC -> 3.3V (ESP32)
- GND -> GND (ESP32)
- SDA -> GPIO21 (ESP32)
- SCL -> GPIO22 (ESP32)
First, let’s write a simple I2C scanner to ensure the sensor is detected. MicroPython typically uses GPIO22 for SCL and GPIO21 for SDA by default for I2C.
# i2c_scanner.py
from machine import Pin, I2C
import time
# I2C bus setup (default pins for ESP32)
# SDA: GPIO21, SCL: GPIO22
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400000)
print('I2C scan started...')
devices = i2c.scan()
if devices:
print('I2C devices found:', len(devices))
for device in devices:
print("Hex address: ", hex(device))
else:
print('No I2C devices found.')
Upload and run this script temporarily:
ampy --port /dev/ttyUSB0 put i2c_scanner.py
ampy --port /dev/ttyUSB0 run i2c_scanner.py
I2C scan started...
I2C devices found: 1
Hex address: 0x76
The BMP280 typically has an I2C address of 0x76
or 0x77
. If you see one of these, your wiring is correct.
Now, for the actual sensor reading, we’ll need a driver. MicroPython’s micropython-lib
repository often has pure Python drivers. For BMP280, there’s bmp280.py
.
Download bmp280.py
from micropython-lib (or copy its content into a file). Place it in the same directory as your main.py
.
Upload bmp280.py
to the ESP32:
ampy --port /dev/ttyUSB0 put bmp280.py
Now, modify main.py
to use the BMP280:
# main.py
import time
from machine import Pin, I2C
from bmp280 import BMP280 # Import the driver
# I2C bus setup
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400000)
# Check if BMP280 is present at its common addresses
try:
sensor = BMP280(i2c, addr=0x76) # Try common address 0x76
print("BMP280 sensor initialized (address 0x76).")
except OSError:
try:
sensor = BMP280(i2c, addr=0x77) # Try common address 0x77
print("BMP280 sensor initialized (address 0x77).")
except OSError:
print("BMP280 sensor not found at 0x76 or 0x77. Check wiring/address.")
sensor = None # Set sensor to None if not found
if sensor:
print("Reading BMP280 sensor values...")
try:
while True:
temperature = sensor.temperature # Reads in Celsius
pressure = sensor.pressure / 100 # Reads in Pascals, convert to hPa/mbar
print(f"Temperature: {temperature:.2f} C")
print(f"Pressure: {pressure:.2f} hPa")
time.sleep(5) # Read every 5 seconds
except KeyboardInterrupt:
print("Script interrupted.")
except Exception as e:
print(f"An error occurred during sensor reading: {e}")
Upload and reset. You should see temperature and pressure readings in your serial terminal.
Step 6: Beyond Basics - Timers and Deep Sleep
For more sophisticated automation, you’ll need to manage tasks efficiently and save power.
6.1 machine.Timer
for Non-Blocking Periodic Tasks
Instead of time.sleep()
, which blocks the entire program, machine.Timer
allows you to schedule functions to run periodically without blocking the main loop. This is crucial for applications that need to respond to multiple events (e.g., web requests and sensor readings).
# main.py (Example with Timer)
from machine import Pin, Timer
import time
led = Pin(2, Pin.OUT)
timer0 = Timer(0) # Create a timer object
def toggle_led(timer):
"""Callback function to toggle the LED."""
led.value(not led.value())
print(f"LED toggled by timer. Current state: {'ON' if led.value() else 'OFF'}")
print("Starting LED blinker with Timer...")
# Initialize timer to call toggle_led every 1000ms (1 second)
# mode=Timer.PERIODIC means it will repeat
# callback=toggle_led is the function to call
timer0.init(period=1000, mode=Timer.PERIODIC, callback=toggle_led)
try:
# The main loop can now do other things or just sleep
while True:
# This loop continues to run, allowing the web server or other tasks to operate
# The timer callback happens in the background.
print("Main loop running...")
time.sleep(5) # Simulate other work
except KeyboardInterrupt:
print("Script interrupted. De-initializing timer.")
timer0.deinit() # Stop the timer
led.value(0) # Ensure LED is off
Upload and reset. You’ll see “LED toggled by timer” messages alongside “Main loop running…”, demonstrating non-blocking execution.
6.2 machine.deepsleep
for Power Efficiency
For battery-powered IoT devices, deepsleep
is critical. It turns off most of the ESP32’s components, reducing power consumption drastically. The device wakes up after a specified time or external interrupt, runs boot.py
and main.py
again, and then goes back to sleep.
# main.py (Example with Deep Sleep)
import machine
import time
# Pin for a dummy "sensor" (e.g., LDR or button) that wakes up the device
# Configure wake-up source. For timer, no external pin needed.
# For external wake-up, connect a switch to a specific GPIO and GND.
# Example: Use GPIO32 for external wake-up
# wake_pin = machine.Pin(32, machine.Pin.IN, machine.Pin.PULL_UP)
# machine.wake_reason() will tell you how it woke up.
sleep_duration_seconds = 10
print(f"Going into deep sleep for {sleep_duration_seconds} seconds...")
# Optional: Print wake reason (only valid after a deep sleep wake-up)
# This will be `None` on first boot after power-up/reset
wake_reason = machine.wake_reason()
if wake_reason == machine.DEEPSLEEP_RESET:
print("Woke up from deep sleep (timer or external pin).")
elif wake_reason == machine.EXT0_RESET:
print("Woke up from deep sleep (external pin 0).")
elif wake_reason == machine.EXT1_RESET:
print("Woke up from deep sleep (external pin 1).")
elif wake_reason == machine.PWRON_RESET:
print("Woke up from power-on reset.")
else:
print(f"Woke up from: {wake_reason}") # Other reset types
# Before sleeping, perform any tasks (e.g., read sensors, send data)
# For demonstration, toggle an LED briefly to show activity
led = machine.Pin(2, machine.Pin.OUT)
led.value(1)
time.sleep(0.1)
led.value(0)
time.sleep(0.1)
led.value(1)
time.sleep(0.1)
led.value(0) # Ensure LED is off before deep sleep
# Set up timer for deep sleep
machine.deepsleep(sleep_duration_seconds * 1000) # Argument is in milliseconds
# Code after deepsleep() will not be executed unless wake-up fails
print("This line will not be printed if deep sleep successful.")
Upload and reset. You’ll see the print statements, a quick LED blink, then silence for 10 seconds. After 10 seconds, the ESP32 will reboot and the cycle repeats. This is ideal for battery-powered sensor nodes.
Step 7: Practical Automation Examples
Let’s put it all together into more complete scenarios.
7.1 Sending Sensor Data to a Cloud Service (HTTP POST)
Many cloud services (like Adafruit IO, ThingSpeak, or your custom API) accept data via HTTP POST. We’ll use urequests
(MicroPython’s equivalent of requests
) to send our BMP280 data.
First, you might need to install urequests
on your ESP32 if it’s not pre-installed with your firmware.
# This is typically not needed for recent firmware, but good to know
# It pulls from micropython-lib if the module is not already on your board
ampy --port /dev/ttyUSB0 put lib/urequests.py /urequests.py
# If urequests is not directly available, you might need to find a simpler HTTP client
# Or download urequests.py manually and upload it.
Note: Some MicroPython firmwares come with urequests
pre-compiled as a built-in module, accessible via import urequests
. If ampy put
doesn’t work directly from a lib
path, you can manually download urequests.py
from the micropython-lib repo and upload it to the root of your ESP32’s filesystem.
urequests
is a simplified version of requests
, useful for basic HTTP client needs.
Now, modify your main.py
to read BMP280 and POST data. We’ll use httpbin.org/post
as a dummy endpoint to see the request. For a real application, you’d use your specific API endpoint.
# main.py
import time
import urequests # For HTTP requests
from machine import Pin, I2C
from bmp280 import BMP280 # Assuming bmp280.py is on your board
import network
# --- Sensor Setup (from previous example) ---
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400000)
try:
sensor = BMP280(i2c, addr=0x76)
except OSError:
try:
sensor = BMP280(i2c, addr=0x77)
except OSError:
print("BMP280 sensor not found.")
sensor = None
# --- Cloud Service Configuration ---
# Replace with your actual API endpoint
# For testing, httpbin.org/post will echo your POST data back
API_URL = "http://httpbin.org/post"
# Example for Adafruit IO (replace with your AIO_USERNAME, AIO_KEY, FEED_NAME)
# API_URL = "https://io.adafruit.com/api/v2/{AIO_USERNAME}/feeds/{FEED_NAME}/data"
# HEADERS = {'X-AIO-Key': '{AIO_KEY}', 'Content-Type': 'application/json'} # Example headers
# Check if connected to Wi-Fi
sta_if = network.WLAN(network.STA_IF)
if not sta_if.isconnected():
print("WiFi not connected. Cannot send data.")
while True:
time.sleep(5) # Loop indefinitely if no WiFi
else:
print(f"Connected to WiFi. Sending data to {API_URL}...")
try:
while True:
if sensor:
temperature = sensor.temperature
pressure = sensor.pressure / 100 # hPa
print(f"Reading: Temp={temperature:.2f}C, Pressure={pressure:.2f}hPa")
# Prepare data payload (JSON is common)
data_payload = {
"temperature": round(temperature, 2),
"pressure": round(pressure, 2),
"device_id": "esp32-sensor-001" # Unique ID for your device
}
try:
# Send HTTP POST request
# For basic httpbin.org, a simple JSON payload without custom headers is fine
response = urequests.post(API_URL, json=data_payload)
# If you need custom headers for a service like Adafruit IO:
# response = urequests.post(API_URL, json=data_payload, headers=HEADERS)
print(f"HTTP POST Status: {response.status_code}")
# print(f"HTTP POST Response: {response.text}") # Uncomment to see full response
response.close() # Important to close the response object to free memory
except Exception as e:
print(f"Failed to send data via HTTP POST: {e}")
else:
print("Sensor not initialized, skipping data send.")
time.sleep(30) # Send data every 30 seconds
except KeyboardInterrupt:
print("Script interrupted.")
except Exception as e:
print(f"An unhandled error occurred: {e}")
Upload main.py
and bmp280.py
(if you haven’t already), then reset. Observe the serial output. You should see successful POST requests. If you use httpbin.org/post
, you can paste the URL into your browser to see the data your ESP32 sent.
7.2 MQTT Client for Pub/Sub Automation
MQTT (Message Queuing Telemetry Transport) is a lightweight messaging protocol perfect for IoT. It uses a publish/subscribe model, ideal for event-driven automation.
We’ll use umqtt.simple
(often built-in or easily available). We’ll connect to a public MQTT broker (e.g., broker.hivemq.com
) and publish sensor data, and also subscribe to a topic to control an LED.
Hardware: Same LED (GPIO2) and BMP280 setup as before.
# main.py (MQTT example)
import time
from machine import Pin, I2C
import network
from umqtt.simple import MQTTClient # MicroPython MQTT client
from bmp280 import BMP280 # Assuming bmp280.py is on your board
# --- MQTT Configuration ---
MQTT_BROKER = "broker.hivemq.com" # Public broker for testing
MQTT_PORT = 1883
CLIENT_ID = b"esp32_sensor_001" # Unique client ID, must be bytes
# Topic to publish sensor data
PUBLISH_TOPIC = b"esp32/home/sensor/data"
# Topic to subscribe to for control commands
SUBSCRIBE_TOPIC = b"esp32/home/led/control"
# --- Hardware Setup ---
led = Pin(2, Pin.OUT)
led.value(0) # Start with LED off
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400000)
try:
sensor = BMP280(i2c, addr=0x76)
except OSError:
try:
sensor = BMP280(i2c, addr=0x77)
except OSError:
print("BMP280 sensor not found.")
sensor = None
# --- MQTT Callback Function ---
def mqtt_callback(topic, msg):
"""Called when a message is received on a subscribed topic."""
print(f"Received MQTT message on topic '{topic.decode()}': {msg.decode()}")
if topic == SUBSCRIBE_TOPIC:
if msg == b"on":
led.value(1)
print("LED turned ON by MQTT.")
elif msg == b"off":
led.value(0)
print("LED turned OFF by MQTT.")
else:
print("Unknown LED command.")
# --- MQTT Connection ---
def connect_mqtt():
sta_if = network.WLAN(network.STA_IF)
if not sta_if.isconnected():
print("WiFi not connected. Cannot connect to MQTT.")
return None
try:
client = MQTTClient(CLIENT_ID, MQTT_BROKER, port=MQTT_PORT)
client.set_callback(mqtt_callback) # Set the message callback
client.connect()
client.subscribe(SUBSCRIBE_TOPIC) # Subscribe to LED control topic
print(f"Connected to MQTT broker: {MQTT_BROKER}")
print(f"Subscribed to topic: {SUBSCRIBE_TOPIC.decode()}")
return client
except Exception as e:
print(f"Failed to connect to MQTT broker: {e}")
return None
# Main loop
client = None
last_publish_time = time.time()
publish_interval_seconds = 10
while True:
if client is None:
client = connect_mqtt()
if client is None:
time.sleep(5) # Wait before retrying connection
continue
try:
client.check_msg() # Check for new messages from MQTT broker (non-blocking)
current_time = time.time()
if current_time - last_publish_time >= publish_interval_seconds:
if sensor:
temperature = sensor.temperature
pressure = sensor.pressure / 100
payload = f'{{"temperature": {temperature:.2f}, "pressure": {pressure:.2f}}}'
print(f"Publishing: {payload} to {PUBLISH_TOPIC.decode()}")
client.publish(PUBLISH_TOPIC, payload.encode())
else:
print("Sensor not initialized, skipping publish.")
last_publish_time = current_time
except Exception as e:
print(f"MQTT error: {e}. Reconnecting...")
client.disconnect()
client = None # Force re-connection on next loop
time.sleep(1) # Small delay to prevent busy-looping
Upload main.py
and bmp280.py
(and umqtt/simple.py
if not built-in). Reset.
Testing:
- Publishing: Observe the serial output. Every 10 seconds, you should see the ESP32 publishing sensor data to
esp32/home/sensor/data
. You can use an MQTT client (likemqtt-explorer
ormosquitto_pub
CLI) to subscribe to this topic and see the data.mosquitto_sub -h broker.hivemq.com -t esp32/home/sensor/data
- Subscribing/Controlling: Use an MQTT client to publish to
esp32/home/led/control
. To turn LED ON:mosquitto_pub -h broker.hivemq.com -t esp32/home/led/control -m "on"
mosquitto_pub -h broker.hivemq.com -t esp32/home/led/control -m "off"
This bidirectional communication is the essence of powerful IoT automation.
Best Practices and Troubleshooting
- Error Handling: Always use
try...except
blocks for network operations, sensor readings, and file I/O. Connections can drop, sensors can fail. - Memory Management: MicroPython is lean, but the ESP32 still has limited RAM.
- Use
gc.collect()
periodically, especially after large operations, to free up memory. - Avoid creating large data structures or long strings.
- Use
urequests
for HTTP,umqtt
for MQTT. Preferujson
,ustruct
, etc., over fulljson
,struct
where possible. - Print messages sparingly in production code to save memory (and CPU cycles).
- Consider
esp.osdebug(None)
inboot.py
to disable verbose debug output from the ESP-IDF layer, freeing up memory.
- Use
boot.py
vs.main.py
:boot.py
: Runs immediately on power-up/reset. Use for network configuration (Wi-Fi), setting up common libraries, or initial hardware configuration. It should be minimal and robust.main.py
: Your main application logic.
ampy
vs.webrepl
: For basic file transfer,ampy
is straightforward. For advanced interaction over Wi-Fi (without a USB cable),webrepl
is an option. It’s more complex to set up but powerful. For beginners,ampy
is simpler.- Power Cycling: If your ESP32 becomes unresponsive (e.g., due to an infinite loop without
KeyboardInterrupt
), simply remove and re-apply power. - Serial Port Issues: If
esptool.py
orampy
fail to connect:- Ensure no other program (like
picocom
) is using the serial port. - Double-check the correct
/dev/tty...
orCOMx
path. - Try holding the
BOOT
/FLASH
button while plugging in or running the command, then release.
- Ensure no other program (like
- “Out of memory” Errors: Simplify your code, reduce variable usage, use smaller data types where possible, or optimize libraries you’re using.
gc.collect()
might help, but fundamental re-architecting might be needed. - Firmware Updates: Occasionally update your MicroPython firmware for bug fixes, new features, and performance improvements. Follow the flashing steps again.
Conclusion
You’ve now got the tools and knowledge to wield the ESP32 and MicroPython for a vast array of automation projects. From blinking an LED to reading sensors, hosting web servers, and communicating via MQTT, you’ve seen how to connect the physical world to the digital realm with concise, readable Python code.
The power lies in combining these concepts. Imagine a battery-powered sensor sending temperature and humidity data via MQTT every hour (using deep sleep), while also providing a local web interface for diagnostics. Or a smart light switch controlled by a button, a web interface, and an MQTT command.
Start small, build by example, and iterate. The ESP32 and MicroPython democratize embedded systems, putting powerful automation capabilities directly into the hands of developers. Go forth and automate anything!
**Self-Correction/Reflection during the process:**
* Initially thought about including a full `webrepl` setup, but decided against it to keep the focus on command-line simplicity and avoid overcomplicating for a beginner. `ampy` is sufficient for file transfer. Mentioned `webrepl` as an alternative.
* For the HTTP POST example, instead of a specific cloud provider (which would require API keys and setup), opted for `httpbin.org/post` to demonstrate the POST request and response, which is more universal and easier to test without external accounts. Added comments for how it might look for Adafruit IO.
* For the I2C sensor, ensured to include the I2C scanner first. This is a crucial troubleshooting step that beginners often miss.
* Emphasized `boot.py` for Wi-Fi setup and `main.py` for the main application, which is a standard and robust pattern.
* Made sure to include `response.close()` for `urequests` as it's a common memory leak source if forgotten on constrained devices.
* Added `try...except` around major loops and network operations to emphasize robust coding, a must for unattended automation.
* Reiterated the importance of `time.sleep_ms` vs `time.sleep` and `machine.Timer` for non-blocking operations.
* Added `gc.collect()` and `esp.osdebug(None)` as crucial memory-saving tips.
* Ensured all code blocks have language specifiers (e.g., `python`, `bash`, `output`).
* The "Automate Anything" claim is ambitious, but the examples (sensor reading, web control, MQTT, deep sleep) cover the fundamental building blocks that, when combined, truly enable a vast range of automation possibilities. The post focuses on the *how-to* of these building blocks.
```markdown
---
title: How to Use ESP32 + MicroPython to Automate Anything
date: 2023-10-27T10:00:00Z
description: Unlock the power of the ESP32 and MicroPython to build robust, connected automation solutions. From flashing firmware to controlling hardware and integrating with the cloud, this guide provides hands-on examples for developers.
tags: [ESP32, MicroPython, IoT, Automation, Embedded, Python, Firmware, CLI, Hardware, Networking]
categories: [IoT, Embedded Systems, Programming, Automation]
comments: true
---
The world of IoT and automation is vast, but often intimidating due to its blend of hardware and software. Enter the ESP32, a remarkably versatile and inexpensive microcontroller with Wi-Fi and Bluetooth, and MicroPython, a lean implementation of Python 3 optimized for microcontrollers. Together, they form an incredibly potent combination for rapid prototyping and deploying robust automation solutions.
This isn't a theoretical deep dive. This is a pragmatic, hands-on guide for developers who learn by doing. We'll start from scratch and build up to real-world examples, all from your command line.
* **Python Simplicity**: Leverage your existing Python knowledge for embedded development. No complex C/C++ build systems, cryptic syntax, or obscure pointers.
* **Rapid Prototyping**: Iterate quickly. Write code, upload, test. It's that fast.
* **Connectivity Built-in**: Wi-Fi and Bluetooth Low Energy (BLE) on a single, low-cost chip, opening doors to networked automation.
* **Powerful Hardware**: Dual-core processor, ample RAM/Flash, and a rich peripheral set (ADC, DAC, I2C, SPI, UART, PWM, etc.).
* **Cost-Effective**: ESP32 development boards are incredibly cheap, making experimentation accessible.
If you've ever wanted to control physical devices, read sensors, or build your own smart home gadgets without getting bogged down in low-level details, you're in the right place.
Before we begin, ensure you have the following:
1. **ESP32 Development Board**: Any common board like the ESP32-WROOM-32 DevKitC, ESP32-S series, or NodeMCU-32S will work. Ensure it has a USB port for power and data.
2. **USB A-to-Micro B or USB A-to-C Cable**: Depends on your ESP32 board. Crucially, it must be a *data cable*, not just a charging cable.
3. **Python 3**: Installed on your host machine (Windows, macOS, Linux).
4. **`pip`**: Python's package installer, usually bundled with Python 3.
The first order of business is to get MicroPython onto your ESP32. This process involves erasing the existing firmware and then writing the MicroPython `.bin` file. We'll use `esptool.py`, Espressif's official utility.
Open your terminal and install `esptool.py` using `pip`:
```bash
pip install esptool
Collecting esptool
Downloading esptool-4.7.0-py3-none-any.whl (124 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 124.9/124.9 kB 5.9 MB/s eta 0:00:00
Collecting ecdsa<0.19.0,>=0.16.0 (from esptool)
Downloading ecdsa-0.18.0-py2.py3-none-any.whl (142 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 142.1/142.1 kB 8.7 MB/s eta 0:00:00
... (other dependencies) ...
Successfully installed ecdsa-0.18.0 esptool-4.7.0 pyyaml-6.0.1
Head to the official MicroPython downloads page for ESP32: micropython.org/download/esp32/.
Download the latest stable .bin
file (e.g., esp32-20231026-v1.21.0.bin
). Place it in a convenient directory, like ~/Downloads/
.
Connect your ESP32 board to your computer via the USB cable.
- Linux: Check
dmesg
orls /dev/tty*
. It will likely appear as/dev/ttyUSB0
or/dev/ttyACM0
. - macOS: Check
ls /dev/cu.*
. It will often be/dev/cu.usbserial-XXXX
or/dev/cu.SLAB_USBtoUART
. - Windows: Check Device Manager under “Ports (COM & LPT)”. It will be
COMx
(e.g.,COM3
).
For the rest of this guide, I’ll assume your port is /dev/ttyUSB0
. Adjust accordingly.
It’s good practice to erase the entire flash memory before flashing new firmware. This prevents conflicts from previous installations.
esptool.py --port /dev/ttyUSB0 erase_flash
esptool.py v4.7.0
Serial port /dev/ttyUSB0
Connecting...
Chip is ESP32-D0WDQ6 (revision 1)
Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme V1
Crystal is 40MHz
MAC: ac:67:b2:0a:b2:ac
Uploading stub...
Running stub...
Stub running...
Erasing flash (this may take a while)...
Chip erase completed successfully in 8.0s
Hard resetting via RTS pin...
Note: If Connecting...
fails, try holding down the BOOT
or FLASH
button on your ESP32 board while executing the command, then release it once Connecting...
appears. Some boards (especially older NodeMCU-32S) might require this.
Now, flash the downloaded .bin
file. Replace esp32-latest.bin
with the actual filename and path to your downloaded firmware.
esptool.py --port /dev/ttyUSB0 --baud 460800 write_flash --flash_size=detect 0 ~/Downloads/esp32-latest.bin
We use --baud 460800
for faster flashing, and --flash_size=detect
to automatically determine your board’s flash size. The 0
indicates the memory address where the firmware starts.
esptool.py v4.7.0
Serial port /dev/ttyUSB0
Connecting...
Chip is ESP32-D0WDQ6 (revision 1)
Features: WiFi, BT, Dual Core, 240MHz, VRef calibration in efuse, Coding Scheme V1
Crystal is 40MHz
MAC: ac:67:b2:0a:b2:ac
Uploading stub...
Running stub...
Stub running...
Flash params set to 0x0220 (DIO, 40MHz)
Configuring flash size...
Flash size will be detected automatically.
Compressed 1133376 bytes to 726889...
Wrote 1133376 bytes (726889 compressed) at 0x00000000 in 15.8 seconds (effective 573.7 kbit/s)...
Hash of data verified.
Leaving...
Hard resetting via RTS pin...
Congratulations! Your ESP32 now runs MicroPython.
REPL stands for Read-Eval-Print Loop. It’s an interactive prompt, similar to Python on your computer, but running directly on the ESP32. This is your primary way to interact, test code, and debug on the device.
You’ll need a serial terminal program.
- Linux:
picocom
orscreen
(install via your package manager, e.g.,sudo apt install picocom
). - macOS:
screen
is pre-installed. - Windows: PuTTY is a common choice, or use
pyserial
with a simple Python script. For command-line consistency, considerminicom
if you use WSL, or follow theampy
examples which abstract away direct serial interaction once files are on the device. I’ll usepicocom
for demonstration.
picocom --baud 115200 /dev/ttyUSB0
Once connected, press Ctrl+J
(Enter) a few times to get the >>>
prompt. If nothing happens, press the EN
(Enable/Reset) button on your ESP32 board.
picocom v3.1
port is : /dev/ttyUSB0
flow control : none
baudrate is : 115200
parity is : none
databits are : 8
stopbits are : 1
escape is : C-a
local echo is : no
noinit is : no
noreset is : no
nolock is : no
send_cmd is : sz -vv
receive_cmd is : rz -vv -E
imap is :
omap is :
emap is : crcrlf
logfile is : none
initstring : none
exitstring : none
exit with C-x C-q, C-c C-x
Type C-a C-h for help.
>>>
MicroPython v1.21.0 on 2023-10-26; ESP32 module with ESP32
Type "help()" for more information.
>>>
Common REPL Commands:
help()
: Get general help.help('modules')
: List available built-in modules.import machine
: Import the core hardware control module.dir(machine)
: See what functions/classes are available in themachine
module.Ctrl+D
: Soft reboot the ESP32. This re-runsboot.py
andmain.py
if they exist.Ctrl+C
: Interrupt a running script.Ctrl+X
: Exitpicocom
.
Let’s make the onboard LED blink. Most ESP32 boards have an LED connected to GPIO2.
>>> from machine import Pin
>>> led = Pin(2, Pin.OUT)
>>> led.value(1) # Turn LED ON (or OFF, depending on board's wiring - active high/low)
>>> led.value(0) # Turn LED OFF (or ON)
To make it blink repeatedly:
>>> import time
>>> from machine import Pin
>>>
>>> led = Pin(2, Pin.OUT)
>>>
>>> while True:
... led.value(not led.value())
... time.sleep(0.5)
...
To stop this, press Ctrl+C
.
Typing code directly into the REPL is fine for testing, but impractical for larger scripts. We need a way to transfer files from our computer to the ESP32’s filesystem. ampy
(Adafruit MicroPython Tool) is an excellent choice.
pip install adafruit-ampy
Collecting adafruit-ampy
Downloading adafruit_ampy-1.0.1-py3-none-any.whl (16 kB)
... (dependencies) ...
Successfully installed adafruit-ampy-1.0.1 click-8.1.7 pyserial-3.5
Ensure your serial terminal program (like picocom
) is not connected to the ESP32 when using ampy
, as ampy
needs exclusive access to the serial port.
ampy --port /dev/ttyUSB0 ls
: List files on the ESP32.ampy --port /dev/ttyUSB0 put <local_file> [remote_file]
: Upload a file.ampy --port /dev/ttyUSB0 get <remote_file> [local_file]
: Download a file.ampy --port /dev/ttyUSB0 rm <remote_file>
: Delete a file.ampy --port /dev/ttyUSB0 run <local_script>
: Run a local script on the ESP32 without saving it permanently.ampy --port /dev/ttyUSB0 reset
: Soft reset the board.
Let’s put our LED blink script into main.py
. MicroPython automatically runs boot.py
(for setup) and then main.py
(your main application code) on startup or soft reboot.
Create a file named main.py
on your computer:
# main.py
import time
from machine import Pin
# Pin 2 is often the onboard LED on ESP32 DevKits
# Check your board's schematic if it's different
led = Pin(2, Pin.OUT)
print("Starting LED blinker...")
try:
while True:
led.value(not led.value())
print(f"LED is now {'ON' if led.value() else 'OFF'}")
time.sleep(1) # Blink every 1 second
except KeyboardInterrupt:
print("Script interrupted.")
led.value(0) # Ensure LED is off on exit
Now, upload it:
ampy --port /dev/ttyUSB0 put main.py
Uploading main.py to /main.py...
After uploading, soft reset the ESP32 to run main.py
:
ampy --port /dev/ttyUSB0 reset
Soft resetting...
Now, re-connect with your serial terminal (e.g., picocom --baud 115200 /dev/ttyUSB0
) to see the output.
>>>
MicroPython v1.21.0 on 2023-10-26; ESP32 module with ESP32
Type "help()" for more information.
>>> Starting LED blinker...
LED is now ON
LED is now OFF
LED is now ON
LED is now OFF
...
You can now disconnect the terminal, and the ESP32 will continue blinking the LED as long as it’s powered. This is the foundation of automated tasks.
The ESP32’s built-in Wi-Fi is a game-changer for automation, allowing your devices to connect to the internet, communicate with other devices, or host web interfaces.
The network
module handles Wi-Fi. It’s often best to put Wi-Fi connection logic in boot.py
so that main.py
can assume connectivity.
Create a boot.py
file on your computer:
# boot.py
import esp
import gc
import network
import time
# Disable debug output to save memory and reduce noise
esp.osdebug(None)
gc.collect()
# Your Wi-Fi credentials
SSID = 'YOUR_WIFI_SSID'
PASSWORD = 'YOUR_WIFI_PASSWORD'
print(f"Connecting to WiFi: {SSID}...")
sta_if = network.WLAN(network.STA_IF)
sta_if.active(True)
sta_if.connect(SSID, PASSWORD)
max_attempts = 10
for i in range(max_attempts):
if sta_if.isconnected():
print("WiFi connected!")
print('Network config:', sta_if.ifconfig())
break
print(f"Attempt {i+1}/{max_attempts}: Waiting for WiFi connection...")
time.sleep(1)
else:
print("Failed to connect to WiFi after multiple attempts.")
# Consider adding fallback logic or rebooting here
# machine.reset() # Uncomment to reset if connection fails
# Ensure the AP interface is disabled if not needed, saves memory
ap_if = network.WLAN(network.AP_IF)
ap_if.active(False)
print("Boot script finished.")
Replace YOUR_WIFI_SSID
and YOUR_WIFI_PASSWORD
with your actual Wi-Fi credentials.
Upload boot.py
:
ampy --port /dev/ttyUSB0 put boot.py
Uploading boot.py to /boot.py...
Now, reset the ESP32 and connect to the serial terminal to observe the connection process:
ampy --port /dev/ttyUSB0 reset
picocom --baud 115200 /dev/ttyUSB0
Soft resetting...
Connecting to WiFi: MyHomeNetwork...
Attempt 1/10: Waiting for WiFi connection...
Attempt 2/10: Waiting for WiFi connection...
WiFi connected!
Network config: ('192.168.1.100', '255.255.255.0', '192.168.1.1', '192.168.1.1')
Boot script finished.
>>> Starting LED blinker...
LED is now ON
...
You now have a connected ESP32!
Let’s build a tiny web server on the ESP32 to control the LED. This demonstrates how your ESP32 can act as a local control point.
Modify your main.py
(after boot.py
ensures connection):
# main.py
import time
from machine import Pin
import socket # For networking
import network # To check if connected
# Pin 2 is often the onboard LED on ESP32 DevKits
led = Pin(2, Pin.OUT)
led_state = "OFF" # Keep track of the LED state
# Basic HTML page for controlling the LED
def web_page():
html = """<!DOCTYPE html>
<html>
<head>
<title>ESP32 LED Control</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body { font-family: sans-serif; text-align: center; margin-top: 50px; }
.button {
display: inline-block;
padding: 15px 30px;
font-size: 24px;
cursor: pointer;
text-align: center;
text-decoration: none;
outline: none;
color: #fff;
background-color: #4CAF50;
border: none;
border-radius: 15px;
box-shadow: 0 9px #999;
}
.button:hover { background-color: #3e8e41 }
.button:active {
background-color: #3e8e41;
box-shadow: 0 5px #666;
transform: translateY(4px);
}
.off { background-color: #f44336; }
.off:hover { background-color: #da190b; }
</style>
</head>
<body>
<h1>ESP32 LED Control</h1>
<p>LED State: <strong>""" + led_state + """</strong></p>
<p>
<a href="/?led=on"><button class="button">LED ON</button></a>
<a href="/?led=off"><button class="button off">LED OFF</button></a>
</p>
</body>
</html>"""
return html
# Check if connected to Wi-Fi before starting the server
sta_if = network.WLAN(network.STA_IF)
if not sta_if.isconnected():
print("WiFi not connected. Web server will not start.")
else:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('', 80)) # Bind to port 80 (HTTP)
s.listen(5) # Listen for up to 5 connections
print(f"Web server active on http://{sta_if.ifconfig()[0]}/")
while True:
try:
conn, addr = s.accept()
print('Got a connection from %s' % str(addr))
request = conn.recv(1024)
request = str(request)
print('Content = %s' % request)
# Parse request to find 'led=on' or 'led=off'
led_on = request.find('/?led=on')
led_off = request.find('/?led=off')
if led_on == 6: # Index 6 means it's the main path
led.value(1)
led_state = "ON"
if led_off == 6:
led.value(0)
led_state = "OFF"
response = web_page()
conn.send('HTTP/1.1 200 OK\n')
conn.send('Content-Type: text/html\n')
conn.send('Connection: close\n\n')
conn.sendall(response)
conn.close()
except OSError as e:
conn.close()
print('Connection error:', e)
except KeyboardInterrupt:
print("Web server stopped by user.")
led.value(0) # Ensure LED is off
break
Upload the updated main.py
:
ampy --port /dev/ttyUSB0 put main.py
Uploading main.py to /main.py...
Reset the ESP32:
ampy --port /dev/ttyUSB0 reset
Now, check your serial output (picocom
) for the IP address. Open a web browser on your computer and navigate to http://<YOUR_ESP32_IP_ADDRESS>/
. You should see a simple page with ON/OFF buttons for the LED.
This is a powerful concept: your ESP32 can be a tiny server providing control over its attached hardware.
The machine
module is your interface to the ESP32’s hardware peripherals.
Let’s combine a physical button with an LED. When you press the button, the LED state flips.
Hardware Setup:
- Connect an LED (with a 220-330 ohm resistor in series) between GPIO16 and GND (short leg of LED).
- Connect a push button between GPIO17 and GND. Use the internal pull-up resistor on GPIO17.
Update main.py
:
# main.py
import time
from machine import Pin
# Define pins
LED_PIN = 16 # Example: Connect LED (with resistor) to GPIO16
BUTTON_PIN = 17 # Example: Connect push button to GPIO17
led = Pin(LED_PIN, Pin.OUT)
# Configure button with internal PULL_UP resistor.
# When pressed, it connects to GND, so its value becomes 0.
button = Pin(BUTTON_PIN, Pin.IN, Pin.PULL_UP)
print("Monitoring button press...")
# Initial state for the LED
current_led_state = 0 # Off
led.value(current_led_state)
last_button_state = button.value()
debounce_time_ms = 50 # milliseconds for debounce
try:
while True:
# Read current button state
new_button_state = button.value()
# Check for state change (button pressed - going from HIGH to LOW)
if new_button_state != last_button_state:
# Simple debounce: wait a bit and re-read
time.sleep_ms(debounce_time_ms)
if new_button_state == button.value(): # Check if still changed
if new_button_state == 0: # Button pressed (active low)
current_led_state = 1 - current_led_state # Toggle state
led.value(current_led_state)
print(f"Button pressed! LED is now {'ON' if current_led_state else 'OFF'}")
last_button_state = new_button_state
time.sleep_ms(10) # Small delay to avoid busy-waiting too much
except KeyboardInterrupt:
print("Script interrupted.")
led.value(0) # Ensure LED is off
Upload and reset. Press the button, and the LED should toggle.
The ESP32 has an Analog-to-Digital Converter (ADC) to read analog voltages, perfect for sensors like an LDR (Light Dependent Resistor) or potentiometer.
Hardware Setup:
- Connect an LDR in a voltage divider circuit.
- One side of LDR to 3.3V.
- The other side of LDR to a 10k Ohm resistor.
- The other side of the 10k Ohm resistor to GND.
- The point between the LDR and the 10k Ohm resistor connects to an ADC pin (e.g., GPIO34, GPIO35, GPIO36). These pins are input-only.
- Let’s use
GPIO34
.
Modify main.py
:
# main.py
import time
from machine import ADC, Pin
# ADC pin for LDR (e.g., GPIO34)
# Note: On some ESP32 boards, specific ADC pins are available.
# Common ones are GPIO32-39. GPIO34 is a good choice as it's input-only.
LDR_PIN = 34
adc = ADC(Pin(LDR_PIN))
# Set attenuation to increase voltage range (0-3.3V)
# ADC.ATTN_0DB: 0-1.0V
# ADC.ATTN_2_5DB: 0-1.25V
# ADC.ATTN_6DB: 0-1.5V
# ADC.ATTN_11DB: 0-3.3V (Recommended for full range)
adc.atten(ADC.ATTN_11DB)
# Set resolution (e.g., 12-bit for 0-4095 range)
adc.width(ADC.WIDTH_12BIT)
print("Reading LDR sensor values...")
try:
while True:
ldr_value = adc.read() # Read raw ADC value (0-4095 for 12-bit)
print(f"LDR Raw Value: {ldr_value}")
# You can map this to a more meaningful value (e.g., light intensity)
# For example, if 0 is max light and 4095 is min light:
light_intensity = round((1 - (ldr_value / 4095)) * 100, 2)
print(f"Estimated Light Intensity: {light_intensity}%")
time.sleep(1)
except KeyboardInterrupt:
print("Script interrupted.")
Upload, reset, and observe. Cover the LDR, and you should see the LDR Raw Value
change significantly.
I2C (Inter-Integrated Circuit) is a common serial communication protocol for connecting multiple low-speed peripheral devices to a microcontroller. Most sensors use it.
Hardware Setup:
- BMP280 Module:
- VCC -> 3.3V (ESP32)
- GND -> GND (ESP32)
- SDA -> GPIO21 (ESP32)
- SCL -> GPIO22 (ESP32)
First, let’s write a simple I2C scanner to ensure the sensor is detected. MicroPython typically uses GPIO22 for SCL and GPIO21 for SDA by default for I2C.
# i2c_scanner.py
from machine import Pin, I2C
import time
# I2C bus setup (default pins for ESP32)
# SDA: GPIO21, SCL: GPIO22
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400000)
print('I2C scan started...')
devices = i2c.scan()
if devices:
print('I2C devices found:', len(devices))
for device in devices:
print("Hex address: ", hex(device))
else:
print('No I2C devices found.')
Upload and run this script temporarily:
ampy --port /dev/ttyUSB0 put i2c_scanner.py
ampy --port /dev/ttyUSB0 run i2c_scanner.py
I2C scan started...
I2C devices found: 1
Hex address: 0x76
The BMP280 typically has an I2C address of 0x76
or 0x77
. If you see one of these, your wiring is correct.
Now, for the actual sensor reading, we’ll need a driver. MicroPython’s micropython-lib
repository often has pure Python drivers. For BMP280, there’s bmp280.py
.
Download bmp280.py
from micropython-lib (or copy its content into a file). Place it in the same directory as your main.py
.
Upload bmp280.py
to the ESP32:
ampy --port /dev/ttyUSB0 put bmp280.py
Now, modify main.py
to use the BMP280:
# main.py
import time
from machine import Pin, I2C
from bmp280 import BMP280 # Import the driver
# I2C bus setup
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400000)
# Check if BMP280 is present at its common addresses
try:
sensor = BMP280(i2c, addr=0x76) # Try common address 0x76
print("BMP280 sensor initialized (address 0x76).")
except OSError:
try:
sensor = BMP280(i2c, addr=0x77) # Try common address 0x77
print("BMP280 sensor initialized (address 0x77).")
except OSError:
print("BMP280 sensor not found at 0x76 or 0x77. Check wiring/address.")
sensor = None # Set sensor to None if not found
if sensor:
print("Reading BMP280 sensor values...")
try:
while True:
temperature = sensor.temperature # Reads in Celsius
pressure = sensor.pressure / 100 # Reads in Pascals, convert to hPa/mbar
print(f"Temperature: {temperature:.2f} C")
print(f"Pressure: {pressure:.2f} hPa")
time.sleep(5) # Read every 5 seconds
except KeyboardInterrupt:
print("Script interrupted.")
except Exception as e:
print(f"An error occurred during sensor reading: {e}")
Upload and reset. You should see temperature and pressure readings in your serial terminal.
For more sophisticated automation, you’ll need to manage tasks efficiently and save power.
Instead of time.sleep()
, which blocks the entire program, machine.Timer
allows you to schedule functions to run periodically without blocking the main loop. This is crucial for applications that need to respond to multiple events (e.g., web requests and sensor readings).
# main.py (Example with Timer)
from machine import Pin, Timer
import time
led = Pin(2, Pin.OUT)
timer0 = Timer(0) # Create a timer object
def toggle_led(timer):
"""Callback function to toggle the LED."""
led.value(not led.value())
print(f"LED toggled by timer. Current state: {'ON' if led.value() else 'OFF'}")
print("Starting LED blinker with Timer...")
# Initialize timer to call toggle_led every 1000ms (1 second)
# mode=Timer.PERIODIC means it will repeat
# callback=toggle_led is the function to call
timer0.init(period=1000, mode=Timer.PERIODIC, callback=toggle_led)
try:
# The main loop can now do other things or just sleep
while True:
# This loop continues to run, allowing the web server or other tasks to operate
# The timer callback happens in the background.
print("Main loop running...")
time.sleep(5) # Simulate other work
except KeyboardInterrupt:
print("Script interrupted. De-initializing timer.")
timer0.deinit() # Stop the timer
led.value(0) # Ensure LED is off
Upload and reset. You’ll see “LED toggled by timer” messages alongside “Main loop running…”, demonstrating non-blocking execution.
For battery-powered IoT devices, deepsleep
is critical. It turns off most of the ESP32’s components, reducing power consumption drastically. The device wakes up after a specified time or external interrupt, runs boot.py
and main.py
again, and then goes back to sleep.
# main.py (Example with Deep Sleep)
import machine
import time
# Pin for a dummy "sensor" (e.g., LDR or button) that wakes up the device
# Configure wake-up source. For timer, no external pin needed.
# For external wake-up, connect a switch to a specific GPIO and GND.
# Example: Use GPIO32 for external wake-up
# wake_pin = machine.Pin(32, machine.Pin.IN, machine.Pin.PULL_UP)
# machine.wake_reason() will tell you how it woke up.
sleep_duration_seconds = 10
print(f"Going into deep sleep for {sleep_duration_seconds} seconds...")
# Optional: Print wake reason (only valid after a deep sleep wake-up)
# This will be `None` on first boot after power-up/reset
wake_reason = machine.wake_reason()
if wake_reason == machine.DEEPSLEEP_RESET:
print("Woke up from deep sleep (timer or external pin).")
elif wake_reason == machine.EXT0_RESET:
print("Woke up from deep sleep (external pin 0).")
elif wake_reason == machine.EXT1_RESET:
print("Woke up from deep sleep (external pin 1).")
elif wake_reason == machine.PWRON_RESET:
print("Woke up from power-on reset.")
else:
print(f"Woke up from: {wake_reason}") # Other reset types
# Before sleeping, perform any tasks (e.g., read sensors, send data)
# For demonstration, toggle an LED briefly to show activity
led = machine.Pin(2, machine.Pin.OUT)
led.value(1)
time.sleep(0.1)
led.value(0)
time.sleep(0.1)
led.value(1)
time.sleep(0.1)
led.value(0) # Ensure LED is off before deep sleep
# Set up timer for deep sleep
machine.deepsleep(sleep_duration_seconds * 1000) # Argument is in milliseconds
# Code after deepsleep() will not be executed unless wake-up fails
print("This line will not be printed if deep sleep successful.")
Upload and reset. You’ll see the print statements, a quick LED blink, then silence for 10 seconds. After 10 seconds, the ESP32 will reboot and the cycle repeats. This is ideal for battery-powered sensor nodes.
Let’s put it all together into more complete scenarios.
Many cloud services (like Adafruit IO, ThingSpeak, or your custom API) accept data via HTTP POST. We’ll use urequests
(MicroPython’s equivalent of requests
) to send our BMP280 data.
First, you might need to install urequests
on your ESP32 if it’s not pre-installed with your firmware.
# This is typically not needed for recent firmware, but good to know
# It pulls from micropython-lib if the module is not already on your board
# If urequests is not directly available, you might need to find a simpler HTTP client
# Or download urequests.py manually from micropython-lib and upload it.
# ampy --port /dev/ttyUSB0 put urequests.py
Note: Some MicroPython firmwares come with urequests
pre-compiled as a built-in module, accessible via import urequests
. If ampy put
doesn’t work directly from a lib
path, you can manually download urequests.py
from the micropython-lib repo and upload it to the root of your ESP32’s filesystem.
urequests
is a simplified version of requests
, useful for basic HTTP client needs.
Now, modify your main.py
to read BMP280 and POST data. We’ll use httpbin.org/post
as a dummy endpoint to see the request. For a real application, you’d use your specific API endpoint.
# main.py
import time
import urequests # For HTTP requests
from machine import Pin, I2C
from bmp280 import BMP280 # Assuming bmp280.py is on your board
import network
# --- Sensor Setup (from previous example) ---
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400000)
try:
sensor = BMP280(i2c, addr=0x76)
except OSError:
try:
sensor = BMP280(i2c, addr=0x77)
except OSError:
print("BMP280 sensor not found.")
sensor = None
# --- Cloud Service Configuration ---
# Replace with your actual API endpoint
# For testing, httpbin.org/post will echo your POST data back
API_URL = "http://httpbin.org/post"
# Example for Adafruit IO (replace with your AIO_USERNAME, AIO_KEY, FEED_NAME)
# API_URL = "https://io.adafruit.com/api/v2/{AIO_USERNAME}/feeds/{FEED_NAME}/data"
# HEADERS = {'X-AIO-Key': '{AIO_KEY}', 'Content-Type': 'application/json'} # Example headers
# Check if connected to Wi-Fi
sta_if = network.WLAN(network.STA_IF)
if not sta_if.isconnected():
print("WiFi not connected. Cannot send data.")
while True:
time.sleep(5) # Loop indefinitely if no WiFi
else:
print(f"Connected to WiFi. Sending data to {API_URL}...")
try:
while True:
if sensor:
temperature = sensor.temperature
pressure = sensor.pressure / 100 # hPa
print(f"Reading: Temp={temperature:.2f}C, Pressure={pressure:.2f}hPa")
# Prepare data payload (JSON is common)
data_payload = {
"temperature": round(temperature, 2),
"pressure": round(pressure, 2),
"device_id": "esp32-sensor-001" # Unique ID for your device
}
try:
# Send HTTP POST request
# For basic httpbin.org, a simple JSON payload without custom headers is fine
response = urequests.post(API_URL, json=data_payload)
# If you need custom headers for a service like Adafruit IO:
# response = urequests.post(API_URL, json=data_payload, headers=HEADERS)
print(f"HTTP POST Status: {response.status_code}")
# print(f"HTTP POST Response: {response.text}") # Uncomment to see full response
response.close() # Important to close the response object to free memory
except Exception as e:
print(f"Failed to send data via HTTP POST: {e}")
else:
print("Sensor not initialized, skipping data send.")
time.sleep(30) # Send data every 30 seconds
except KeyboardInterrupt:
print("Script interrupted.")
except Exception as e:
print(f"An unhandled error occurred: {e}")
Upload main.py
and bmp280.py
(if you haven’t already), then reset. Observe the serial output. You should see successful POST requests. If you use httpbin.org/post
, you can paste the URL into your browser to see the data your ESP32 sent.
MQTT (Message Queuing Telemetry Transport) is a lightweight messaging protocol perfect for IoT. It uses a publish/subscribe model, ideal for event-driven automation.
We’ll use umqtt.simple
(often built-in or easily available). We’ll connect to a public MQTT broker (e.g., broker.hivemq.com
) and publish sensor data, and also subscribe to a topic to control an LED.
Hardware: Same LED (GPIO2) and BMP280 setup as before.
# main.py (MQTT example)
import time
from machine import Pin, I2C
import network
from umqtt.simple import MQTTClient # MicroPython MQTT client
from bmp280 import BMP280 # Assuming bmp280.py is on your board
# --- MQTT Configuration ---
MQTT_BROKER = "broker.hivemq.com" # Public broker for testing
MQTT_PORT = 1883
CLIENT_ID = b"esp32_sensor_001" # Unique client ID, must be bytes
# Topic to publish sensor data
PUBLISH_TOPIC = b"esp32/home/sensor/data"
# Topic to subscribe to for control commands
SUBSCRIBE_TOPIC = b"esp32/home/led/control"
# --- Hardware Setup ---
led = Pin(2, Pin.OUT)
led.value(0) # Start with LED off
i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400000)
try:
sensor = BMP280(i2c, addr=0x76)
except OSError:
try:
sensor = BMP280(i2c, addr=0x77)
except OSError:
print("BMP280 sensor not found.")
sensor = None
# --- MQTT Callback Function ---
def mqtt_callback(topic, msg):
"""Called when a message is received on a subscribed topic."""
print(f"Received MQTT message on topic '{topic.decode()}': {msg.decode()}")
if topic == SUBSCRIBE_TOPIC:
if msg == b"on":
led.value(1)
print("LED turned ON by MQTT.")
elif msg == b"off":
led.value(0)
print("LED turned OFF by MQTT.")
else:
print("Unknown LED command.")
# --- MQTT Connection ---
def connect_mqtt():
sta_if = network.WLAN(network.STA_IF)
if not sta_if.isconnected():
print("WiFi not connected. Cannot connect to MQTT.")
return None
try:
client = MQTTClient(CLIENT_ID, MQTT_BROKER, port=MQTT_PORT)
client.set_callback(mqtt_callback) # Set the message callback
client.connect()
client.subscribe(SUBSCRIBE_TOPIC) # Subscribe to LED control topic
print(f"Connected to MQTT broker: {MQTT_BROKER}")
print(f"Subscribed to topic: {SUBSCRIBE_TOPIC.decode()}")
return client
except Exception as e:
print(f"Failed to connect to MQTT broker: {e}")
return None
# Main loop
client = None
last_publish_time = time.time()
publish_interval_seconds = 10
while True:
if client is None:
client = connect_mqtt()
if client is None:
time.sleep(5) # Wait before retrying connection
continue
try:
client.check_msg() # Check for new messages from MQTT broker (non-blocking)
current_time = time.time()
if current_time - last_publish_time >= publish_interval_seconds:
if sensor:
temperature = sensor.temperature
pressure = sensor.pressure / 100
payload = f'{{"temperature": {temperature:.2f}, "pressure": {pressure:.2f}}}'
print(f"Publishing: {payload} to {PUBLISH_TOPIC.decode()}")
client.publish(PUBLISH_TOPIC, payload.encode())
else:
print("Sensor not initialized, skipping publish.")
last_publish_time = current_time
except Exception as e:
print(f"MQTT error: {e}. Reconnecting...")
client.disconnect()
client = None # Force re-connection on next loop
time.sleep(1) # Small delay to prevent busy-looping
Upload main.py
and bmp280.py
(and umqtt/simple.py
if not built-in). Reset.
Testing:
- Publishing: Observe the serial output. Every 10 seconds, you should see the ESP32 publishing sensor data to
esp32/home/sensor/data
. You can use an MQTT client (likemqtt-explorer
ormosquitto_pub
CLI) to subscribe to this topic and see the data.mosquitto_sub -h broker.hivemq.com -t esp32/home/sensor/data
- Subscribing/Controlling: Use an MQTT client to publish to
esp32/home/led/control
. To turn LED ON:mosquitto_pub -h broker.hivemq.com -t esp32/home/led/control -m "on"
mosquitto_pub -h broker.hivemq.com -t esp32/home/led/control -m "off"
This bidirectional communication is the essence of powerful IoT automation.
- Error Handling: Always use
try...except
blocks for network operations, sensor readings, and file I/O. Connections can drop, sensors can fail. - Memory Management: MicroPython is lean, but the ESP32 still has limited RAM.
- Use
gc.collect()
periodically, especially after large operations, to free up memory. - Avoid creating large data structures or long strings.
- Use
urequests
for HTTP,umqtt
for MQTT. Preferujson
,ustruct
, etc., over fulljson
,struct
where possible. - Print messages sparingly in production code to save memory (and CPU cycles).
- Consider
esp.osdebug(None)
inboot.py
to disable verbose debug output from the ESP-IDF layer, freeing up memory.
- Use
boot.py
vs.main.py
:boot.py
: Runs immediately on power-up/reset. Use for network configuration (Wi-Fi), setting up common libraries, or initial hardware configuration. It should be minimal and robust.main.py
: Your main application logic.
ampy
vs.webrepl
: For basic file transfer,ampy
is straightforward. For advanced interaction over Wi-Fi (without a USB cable),webrepl
is an option. It’s more complex to set up but powerful. For beginners,ampy
is simpler.- Power Cycling: If your ESP32 becomes unresponsive (e.g., due to an infinite loop without
KeyboardInterrupt
), simply remove and re-apply power. - Serial Port Issues: If
esptool.py
orampy
fail to connect:- Ensure no other program (like
picocom
) is using the serial port. - Double-check the correct
/dev/tty...
orCOMx
path. - Try holding the
BOOT
/FLASH
button while plugging in or running the command, then release.
- Ensure no other program (like
- “Out of memory” Errors: Simplify your code, reduce variable usage, use smaller data types where possible, or optimize libraries you’re using.
gc.collect()
might help, but fundamental re-architecting might be needed. - Firmware Updates: Occasionally update your MicroPython firmware for bug fixes, new features, and performance improvements. Follow the flashing steps again.
You’ve now got the tools and knowledge to wield the ESP32 and MicroPython for a vast array of automation projects. From blinking an LED to reading sensors, hosting web servers, and communicating via MQTT, you’ve seen how to connect the physical world to the digital realm with concise, readable Python code.
The power lies in combining these concepts. Imagine a battery-powered sensor sending temperature and humidity data via MQTT every hour (using deep sleep), while also providing a local web interface for diagnostics. Or a smart light switch controlled by a button, a web interface, and an MQTT command.
Start small, build by example, and iterate. The ESP32 and MicroPython democratize embedded systems, putting powerful automation capabilities directly into the hands of developers. Go forth and automate anything!