Skip to main content

Educational tool for physical computing and ESP32/ESP8266 development.

Project description

espzero

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

Copy the espzero/ directory to the root of your ESP32 filesystem using Mu Editor, Thonny, or mpremote:

mpremote cp -r espzero/ :espzero/

Quick Start

import espzero
espzero.begin()                # initialise: auto-detect board

# Built-in LED — available after begin()
from espzero import esp_led
from time import sleep

while True:
    esp_led.on()
    sleep(1)
    esp_led.off()
    sleep(1)

Specify a board explicitly instead of auto-detection:

import espzero
espzero.begin("esp32_38pin_nodemcu")   # or "esp32_devkit_v1", "esp8266_lolin_v3", ...

Use other components after begin():

from espzero import LED, Button, Servo, WiFi

led = LED("internal")              # built-in LED — profile maps alias to real GPIO
btn = 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)
from espzero import CapTouch
touch = CapTouch(pin=4)            # GPIO 4 = T0
if touch.is_touched:
    esp_led.on()

# Servo
servo = 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)

class BoardProfile:
    NAME = "unknown"            # Human-readable board name
    CHIP = "esp32"              # esp32 / esp32s3 / esp32c3 / esp8266

    # Pin aliases — lets users write LED("internal") instead of LED(2)
    PIN_ALIASES = {}

    # ADC settings
    ADC_MAX_RAW  = 4095         # 12-bit resolution (0–4095)
    ADC_SCALE    = 65535        # Scale target for picozero read_u16() compatibility
    ADC_ATTEN    = None         # e.g. machine.ADC.ATTN_11DB; None = firmware default
    ADC_VREF     = 3.3          # Maximum input voltage (tied to attenuation)

    # PWM settings
    PWM_DEFAULT_FREQ  = 1000    # Hz — Pico default was 100 Hz; 1 kHz recommended for ESP32
    PWM_DUTY_MAX      = 65535   # Internal scale is always 16-bit

    # Servo
    SERVO_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 failures
    STRAPPING_PINS = []         # e.g. [0, 2, 5, 12, 15]

    # ADC2 pins — cannot be used while WiFi is active
    ADC2_PINS = []              # e.g. [0, 2, 4, 12, 13, 14, 15, 25, 26, 27]

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

class ESP32DevKitV1(BoardProfile):
    NAME = "esp32_devkit_v1"
    CHIP = "esp32"
    PIN_ALIASES = {"internal": 2, "led": 2}
    ADC_ATTEN            = ADC.ATTN_11DB   # 0–3.6 V
    ADC_VREF             = 3.6
    INTERNAL_LED_TYPE        = "digital"
    INTERNAL_LED_ACTIVE_HIGH = False       # active-low
    STRAPPING_PINS       = [0, 2, 5, 12, 15]
    ADC2_PINS            = [0, 2, 4, 12, 13, 14, 15, 25, 26, 27]


class ESP32S3DevKit(BoardProfile):
    NAME = "esp32_s3_devkit"
    CHIP = "esp32s3"
    PIN_ALIASES = {"internal": 48, "led": 48}
    INTERNAL_LED_TYPE = "neopixel"         # WS2812 RGB on GPIO 48
    STRAPPING_PINS    = [0, 3, 45, 46]
    ADC2_PINS         = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]


class ESP32C3Mini(BoardProfile):
    NAME = "esp32_c3_mini"
    CHIP = "esp32c3"
    PIN_ALIASES = {"internal": 8, "led": 8}
    INTERNAL_LED_ACTIVE_HIGH = False
    STRAPPING_PINS = [2, 8, 9]
    ADC2_PINS      = []                    # C3 has no ADC2

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

import sys

def detect() -> 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()
    except AttributeError:
        return "esp32_devkit_v1"   # fallback

    if "esp32s3" in machine_str or "esp32-s3" in machine_str:
        return "esp32_s3_devkit"
    elif "esp32c3" in machine_str or "esp32-c3" in machine_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 connected

def make_digital_in(pin, pull_up=False):
    """Create input Pin. Warns if GPIO is a strapping pin."""
    gpio = resolve_pin(pin)
    if gpio in get_profile().STRAPPING_PINS:
        print("[espzero] WARNING: GPIO {} is a strapping pin. "
              "Attaching a button may cause boot issues.".format(gpio))
    ...

def set_duty_u16(pwm_obj, value):
    """Set duty cycle. Falls back to duty(0–1023) on firmware < 1.19."""
    try:
        pwm_obj.duty_u16(value)
    except AttributeError:
        pwm_obj.duty(value >> 6)    # 16-bit → 10-bit

def make_adc(pin):
    """Create ADC. Warns if WiFi is active and pin is in ADC2 group."""
    if _wifi_active and gpio in get_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

Topic picozero (Pico) espzero (ESP32) How
Pin creation Pin(num, ...) directly _hal.make_digital_out(pin) HAL wrapper
PWM channel conflict PIN_TO_PWM_CHANNEL[] table Removed (ESP32 LEDC: any pin, any channel) Deleted
PWM duty write duty_u16(val) Same, with fallback for firmware < 1.19 _hal.set_duty_u16()
ADC read adc.read_u16() adc_read_u16() → scales read()×16 internally HAL
ADC attenuation None ATTN_11DB (set in profile) Profile
Built-in LED LED("LED") or LED(25) LED("internal") → profile maps to real pin Alias system
Built-in temp pico_temp_sensor (ADC ch.4) esp_temp_sensor (ESP32 esp32 module) Separate class
PWM default freq 100 Hz 1000 Hz (PWM_DEFAULT_FREQ in profile) Profile value
Servo freq 50 Hz 50 Hz (unchanged)
WiFi None WiFi class New
Touch TouchSensor (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 conflicts

class PWMOutputDevice(OutputDevice, PinMixin):
    def __init__(self, pin, freq=None, duty_factor=65535, ...):
        self._pwm = _hal.make_pwm(pin, freq)   # routed through HAL

    def _write(self, value):
        _hal.set_duty_u16(self._pwm, self._value_to_state(value))  # with fallback

5-B. AnalogInputDevice — ADC scale correction

class AnalogInputDevice(InputDevice, PinMixin):
    def __init__(self, pin, ...):
        self._adc = _hal.make_adc(pin)         # attenuation applied in profile

    def _read(self):
        raw_u16 = _hal.adc_read_u16(self._adc) # scaled to 0–65535
        return self._state_to_value(raw_u16)   # upper logic unchanged

    @property
    def voltage(self):
        return self.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" alias
esp_temp_sensor = ESPTemperatureSensor()   # uses esp32.raw_temperature()

6. ESP32-Specific Classes

6-A. WiFi (_wifi.py)

from espzero import WiFi

wifi = WiFi()
ip = wifi.connect("MySSID", "password", timeout=10)
print("Connected:", ip)         # blocks until connected or raises OSError

wifi.scan()                     # returns list of nearby APs
wifi.is_connected               # True / False
wifi.ip                         # current IP string
wifi.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)

from espzero import CapTouch

touch = CapTouch(pin=4, threshold=300)  # GPIO 4 = T0 on WROOM
if touch.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)

class NeoPixelLED:
    """
    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:

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

8. Supported Boards

Board Built-in LED ADC1 Pins ADC2 Pins* Touch Pins
ESP32 DevKit V1 (WROOM) GPIO 2 (active-low) 32–39 0,2,4,12–15,25–27 T0(4)–T9(32)
ESP32-S3 DevKit GPIO 48 (RGB) 1–10 11–20 T1–T14
ESP32-C3 Mini GPIO 8 (active-low) 0–4 None None
M5Stack ATOM Lite GPIO 27 (RGB) 33,35,36 0,2,4,12–15,25–27 T0(4),T3(15)
Wemos D1 Mini32 GPIO 2 (active-low) 32–39 0,2,4,12–15,25–27 T0(4)–T9(32)
NodeMCU V3 Lolin (ESP8266) GPIO 2 (active-low) A0 only (10-bit, 0–1.0 V) None None

* 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

# Topic Decision
1 ADC2 + WiFi warning _hal.make_adc() checks _wifi_active flag and prints a warning
2 duty_u16() fallback _hal.set_duty_u16() wrapper silently falls back to duty(val>>6) on firmware < 1.19
3 Strapping pin warning make_digital_in() checks STRAPPING_PINS and prints a warning
4 NeoPixel built-in LED INTERNAL_LED_TYPE = "neopixel" profile field selects NeoPixelLED wrapper automatically
5 Lazy profile loading _PROFILE_MAP dict + importlib — only the selected board's module is imported
6 Autocomplete Jedi 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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

espzero-0.0.1.tar.gz (10.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

espzero-0.0.1-py3-none-any.whl (12.3 kB view details)

Uploaded Python 3

File details

Details for the file espzero-0.0.1.tar.gz.

File metadata

  • Download URL: espzero-0.0.1.tar.gz
  • Upload date:
  • Size: 10.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for espzero-0.0.1.tar.gz
Algorithm Hash digest
SHA256 e1c031c831760a5652c9658cf103c2e2bbfb387a23f033a3eb80429be210ca33
MD5 fa26a7eae229775f9cbff5777e8fcb85
BLAKE2b-256 38c60f997966f8feb0e707b104db0a28412a45c56af68448fd7c0cb34a88d1ef

See more details on using hashes here.

Provenance

The following attestation bundles were made for espzero-0.0.1.tar.gz:

Publisher: workflow.yml on roboticsware/espzero

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file espzero-0.0.1-py3-none-any.whl.

File metadata

  • Download URL: espzero-0.0.1-py3-none-any.whl
  • Upload date:
  • Size: 12.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for espzero-0.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 8ba0d0560a4e4178acdc90cc5c1ac774d5831ac758cf5966c244ff87b4a4059c
MD5 c54a450d942671bceed8bc2608b346e9
BLAKE2b-256 26b5a721d59fa7b04150bc3435593d852eaf214985d352e3d7736bbc4217eb88

See more details on using hashes here.

Provenance

The following attestation bundles were made for espzero-0.0.1-py3-none-any.whl:

Publisher: workflow.yml on roboticsware/espzero

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page