Skip to content

Repository files navigation

espzero

PyPI versionLicense

A single MicroPython library that ports picozero to the ESP32 family (WROOM, S3, C3, and more).
"One codebase, many boards" — hardware differences are absorbed by the Board Profile system.


Installation

For your ESP32 board (Recommended)

Use mpremote to install directly from PyPI to your board:

mpremote mip install espzero

Or manually copy the espzero/ directory to your board's root.

For your PC (Development)

To get autocomplete and linting in your IDE:

pip install espzero

Quick Start

importespzeroespzero.begin() # initialise: auto-detect board# Built-in LED — available after begin()fromespzeroimportesp_ledfromtimeimportsleepwhileTrue:
esp_led.on()
sleep(1)
esp_led.off()
sleep(1)

Specify a board explicitly instead of auto-detection:

importespzeroespzero.begin("esp32_38pin_nodemcu") # or "esp32_devkit_v1", "esp8266_lolin_v3", ...

Use other components after begin():

fromespzeroimportLED, Button, Servo, WiFiled=LED("internal") # built-in LED — profile maps alias to real GPIObtn=Button(0) # GPIO 0 (BOOT button)led.blink(on_time=0.5, n=5) # identical API to picozero# WiFi (ESP32-specific)wifi=WiFi()
ip=wifi.connect("MySSID", "password")
print("IP:", ip)
# Capacitive touch (WROOM/WROVER only)fromespzeroimportCapTouchtouch=CapTouch(pin=4) # GPIO 4 = T0iftouch.is_touched:
esp_led.on()
# Servoservo=Servo(13)
servo.mid()
servo.value=0.75# 0–1 range, same as picozero

1. File Structure

espzero/
├── __init__.py # Public API entry point + begin()
├── _hal.py # Hardware Abstraction Layer (HAL)
├── _core.py # Ported picozero core logic
├── _wifi.py # ESP32-specific: WiFi class
├── _touch.py # ESP32-specific: capacitive touch class
└── profiles/
├── _base.py # Abstract BoardProfile base class
├── auto.py # Runtime board auto-detection
└── esp32_boards.py # Profile definitions for all supported boards

2. Board Profile System

2-A. Base Class (profiles/_base.py)

classBoardProfile:
NAME="unknown"# Human-readable board nameCHIP="esp32"# esp32 / esp32s3 / esp32c3 / esp8266# Pin aliases — lets users write LED("internal") instead of LED(2)PIN_ALIASES= {}
# ADC settingsADC_MAX_RAW=4095# 12-bit resolution (0–4095)ADC_SCALE=65535# Scale target for picozero read_u16() compatibilityADC_ATTEN=None# e.g. machine.ADC.ATTN_11DB; None = firmware defaultADC_VREF=3.3# Maximum input voltage (tied to attenuation)# PWM settingsPWM_DEFAULT_FREQ=1000# Hz — Pico default was 100 Hz; 1 kHz recommended for ESP32PWM_DUTY_MAX=65535# Internal scale is always 16-bit# ServoSERVO_FREQ=50# 50 Hz (20 ms frame) — same as picozero# Built-in LED type# "digital" — standard GPIO LED# "neopixel" — WS2812 RGB (e.g. ESP32-S3 DevKit, M5Stack ATOM)INTERNAL_LED_TYPE="digital"INTERNAL_LED_ACTIVE_HIGH=True# Boot strapping pins — connecting a button here may cause boot failuresSTRAPPING_PINS= [] # e.g. [0, 2, 5, 12, 15]# ADC2 pins — cannot be used while WiFi is activeADC2_PINS= [] # e.g. [0, 2, 4, 12, 13, 14, 15, 25, 26, 27]

2-B. Board Profile Examples (profiles/esp32_boards.py)

classESP32DevKitV1(BoardProfile):
NAME="esp32_devkit_v1"CHIP="esp32"PIN_ALIASES= {"internal": 2, "led": 2}
ADC_ATTEN=ADC.ATTN_11DB# 0–3.6 VADC_VREF=3.6INTERNAL_LED_TYPE="digital"INTERNAL_LED_ACTIVE_HIGH=False# active-lowSTRAPPING_PINS= [0, 2, 5, 12, 15]
ADC2_PINS= [0, 2, 4, 12, 13, 14, 15, 25, 26, 27]
classESP32S3DevKit(BoardProfile):
NAME="esp32_s3_devkit"CHIP="esp32s3"PIN_ALIASES= {"internal": 48, "led": 48}
INTERNAL_LED_TYPE="neopixel"# WS2812 RGB on GPIO 48STRAPPING_PINS= [0, 3, 45, 46]
ADC2_PINS= [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
classESP32C3Mini(BoardProfile):
NAME="esp32_c3_mini"CHIP="esp32c3"PIN_ALIASES= {"internal": 8, "led": 8}
INTERNAL_LED_ACTIVE_HIGH=FalseSTRAPPING_PINS= [2, 8, 9]
ADC2_PINS= [] # C3 has no ADC2

2-C. Auto-detection (profiles/auto.py)

importsysdefdetect() ->str:
""" Identify the chip from sys.implementation._machine. Returns a board key that matches an entry in espzero._PROFILE_MAP. """try:
machine_str=sys.implementation._machine.lower()
exceptAttributeError:
return"esp32_devkit_v1"# fallbackif"esp32s3"inmachine_stror"esp32-s3"inmachine_str:
return"esp32_s3_devkit"elif"esp32c3"inmachine_stror"esp32-c3"inmachine_str:
return"esp32_c3_mini"else:
return"esp32_devkit_v1"# default: WROOM

3. Hardware Abstraction Layer (_hal.py)

All machine.Pin / machine.PWM / machine.ADC calls are routed through this layer.
Swapping the board profile is enough to support a new board — _core.py requires no changes.

_profile=None# Set by begin() to a BoardProfile instance_wifi_active=False# True while WiFi is connecteddefmake_digital_in(pin, pull_up=False):
"""Create input Pin. Warns if GPIO is a strapping pin."""gpio=resolve_pin(pin)
ifgpioinget_profile().STRAPPING_PINS:
print("[espzero] WARNING: GPIO {} is a strapping pin. ""Attaching a button may cause boot issues.".format(gpio))
...
defset_duty_u16(pwm_obj, value):
"""Set duty cycle. Falls back to duty(0–1023) on firmware < 1.19."""try:
pwm_obj.duty_u16(value)
exceptAttributeError:
pwm_obj.duty(value>>6) # 16-bit → 10-bitdefmake_adc(pin):
"""Create ADC. Warns if WiFi is active and pin is in ADC2 group."""if_wifi_activeandgpioinget_profile().ADC2_PINS:
print("[espzero] WARNING: WiFi is active. ADC2 (GPIO {}) ""cannot be used. Switch to an ADC1 pin.".format(gpio))
...

4. picozero → espzero Change Table

Topicpicozero (Pico)espzero (ESP32)How
Pin creationPin(num, ...) directly_hal.make_digital_out(pin)HAL wrapper
PWM channel conflictPIN_TO_PWM_CHANNEL[] tableRemoved (ESP32 LEDC: any pin, any channel)Deleted
PWM duty writeduty_u16(val)Same, with fallback for firmware < 1.19_hal.set_duty_u16()
ADC readadc.read_u16()adc_read_u16() → scales read()×16 internallyHAL
ADC attenuationNoneATTN_11DB (set in profile)Profile
Built-in LEDLED("LED") or LED(25)LED("internal") → profile maps to real pinAlias system
Built-in temppico_temp_sensor (ADC ch.4)esp_temp_sensor (ESP32 esp32 module)Separate class
PWM default freq100 Hz1000 Hz (PWM_DEFAULT_FREQ in profile)Profile value
Servo freq50 Hz50 Hz (unchanged)
WiFiNoneWiFi classNew
TouchTouchSensor (external TTP223)+ CapTouch (ESP32 built-in touch peripheral)New

5. Key _core.py Changes

5-A. PWMOutputDevice — Channel restriction removed

# picozero: PIN_TO_PWM_CHANNEL table + _check_pwm_channel() → removed# espzero: ESP32 LEDC assigns each pin an independent channel — no conflictsclassPWMOutputDevice(OutputDevice, PinMixin):
def__init__(self, pin, freq=None, duty_factor=65535, ...):
self._pwm=_hal.make_pwm(pin, freq) # routed through HALdef_write(self, value):
_hal.set_duty_u16(self._pwm, self._value_to_state(value)) # with fallback

5-B. AnalogInputDevice — ADC scale correction

classAnalogInputDevice(InputDevice, PinMixin):
def__init__(self, pin, ...):
self._adc=_hal.make_adc(pin) # attenuation applied in profiledef_read(self):
raw_u16=_hal.adc_read_u16(self._adc) # scaled to 0–65535returnself._state_to_value(raw_u16) # upper logic unchanged@propertydefvoltage(self):
returnself.value*_hal.get_profile().ADC_VREF

5-C. Built-in objects renamed

# picozero: pico_led, pico_temp_sensor# espzero:esp_led=LED("internal") # profile resolves "internal" aliasesp_temp_sensor=ESPTemperatureSensor() # uses esp32.raw_temperature()

6. ESP32-Specific Classes

6-A. WiFi (_wifi.py)

fromespzeroimportWiFiwifi=WiFi()
ip=wifi.connect("MySSID", "password", timeout=10)
print("Connected:", ip) # blocks until connected or raises OSErrorwifi.scan() # returns list of nearby APswifi.is_connected# True / Falsewifi.ip# current IP stringwifi.disconnect()

Note: After connect(), the HAL sets _wifi_active = True automatically.
Any attempt to use an ADC2 pin after this will print a warning.

6-B. CapTouch — Capacitive touch (_touch.py)

fromespzeroimportCapTouchtouch=CapTouch(pin=4, threshold=300) # GPIO 4 = T0 on WROOMiftouch.is_touched:
print("Touched! Raw:", touch.value)

Unlike TouchSensor (which wraps an external TTP223 IC), CapTouch uses the ESP32's built-in capacitive touch peripheral directly. Available on pins T0–T9 of WROOM/WROVER modules.

6-C. NeoPixelLED — Built-in RGB LED wrapper (_core.py)

classNeoPixelLED:
""" Treats a single WS2812 pixel as a simple on/off LED. Automatically used as esp_led on boards where INTERNAL_LED_TYPE == 'neopixel' (e.g. ESP32-S3 DevKit GPIO 48, M5Stack ATOM GPIO 27). """

7. Safety Nets for Beginners

espzero includes three runtime warnings designed to save beginners from common hardware pitfalls:

#TriggerWarning
1Using an ADC2 pin while WiFi is active[espzero] WARNING: WiFi is active. ADC2 (GPIO N) cannot be used...
2Attaching a button to a strapping pin[espzero] WARNING: GPIO N is a strapping pin. Boot issues may occur.
3Running on firmware < 1.19 (no duty_u16)Silently falls back to duty() — no crash

8. Supported Boards

BoardBuilt-in LEDADC1 PinsADC2 Pins*Touch Pins
ESP32 DevKit V1 (WROOM)GPIO 2 (active-low)32–390,2,4,12–15,25–27T0(4)–T9(32)
ESP32-S3 DevKitGPIO 48 (RGB)1–1011–20T1–T14
ESP32-C3 MiniGPIO 8 (active-low)0–4NoneNone
M5Stack ATOM LiteGPIO 27 (RGB)33,35,360,2,4,12–15,25–27T0(4),T3(15)
Wemos D1 Mini32GPIO 2 (active-low)32–390,2,4,12–15,25–27T0(4)–T9(32)
NodeMCU V3 Lolin (ESP8266)GPIO 2 (active-low)A0 only (10-bit, 0–1.0 V)NoneNone

* ADC2 pins cannot be used while WiFi is active. For educational use, stick to ADC1 pins only.

ESP8266 ADC note: A0 accepts 0–1.0 V only. Use a voltage divider (e.g. 220 kΩ + 100 kΩ) to measure 3.3 V signals safely.


9. Implementation Roadmap

Phase 1 — Core port (complete)
[x] profiles/_base.py — BoardProfile abstract base class
[x] profiles/esp32_boards.py — WROOM, S3, C3, M5Stack, Wemos profiles
[x] profiles/auto.py — Runtime auto-detection
[x] _hal.py — HAL wrappers
[x] _core.py — Ported picozero core logic
· PWMOutputDevice: removed channel-collision check
· AnalogInputDevice: ADC read routed through HAL
· pico_led / pico_temp_sensor → esp_led / esp_temp_sensor
[x] __init__.py — begin() + public API
Phase 2 — ESP32-specific features (complete)
[x] _wifi.py — WiFi class
[x] _touch.py — CapTouch class
[x] Additional board profiles — S3, C3, M5Stack, Wemos
Phase 3 — Mu Editor integration (complete)
[x] mu/resources/esp32/ — Library bundled with Mu Editor
[x] mu/logic.py — esp32_lib auto-provisioned to mu_code/
[x] mu/interface/editor.py — Jedi search path includes esp32_lib
(dynamic autocomplete via source analysis)

10. Design Decisions

#TopicDecision
1ADC2 + WiFi warning_hal.make_adc() checks _wifi_active flag and prints a warning
2duty_u16() fallback_hal.set_duty_u16() wrapper silently falls back to duty(val>>6) on firmware < 1.19
3Strapping pin warningmake_digital_in() checks STRAPPING_PINS and prints a warning
4NeoPixel built-in LEDINTERNAL_LED_TYPE = "neopixel" profile field selects NeoPixelLED wrapper automatically
5Lazy profile loading_PROFILE_MAP dict + importlib — only the selected board's module is imported
6AutocompleteJedi analyses live source in mu/resources/esp32/ — no separate static API file needed

License

MIT License — contributions welcome.
This library is part of the Mu Editor project for educational IoT programming.

About

The port of Picozero lib for ESP32

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages