Status: Accepted (2026-07-28). Extends ADR-0015, ADR-0016, and ADR-0017. It does not add a PBLE/1 operation, board profile, pin catalog, or agent-mediated hardware path.
MicroPython's pinned ESP32 port normally freezes the MIT neopixel package,
whose NeoPixel class drives WS2812-compatible addressable LEDs through the
standard machine.bitstream primitive. PyBLE's lean classic-ESP32 and
ESP32-S3 manifests replaced the upstream board manifest but failed to require
that standard package; the ESP32-C3 overlay inherited it transitively. The
result is inconsistent firmware and an ImportError on boards whose onboard
status LED is an addressable RGB pixel.
Digital machine.Pin.value() blocks cannot drive a WS2812 data stream. A
special board-specific "onboard LED" block would hide this distinction and
would be wrong for generic boards whose LED pin, pixel count, colour order, or
presence differs.
Every PyBLE ESP32-family firmware image freezes upstream MicroPython's
standard neopixel package, and Blocks wraps only its public API with explicit
user-supplied hardware values.
-
Firmware uses upstream unchanged. Each
esp32,esp32-s3, andesp32-c3build resolves exactly one standardneopixel.pyfrom the pinned MicroPython/micropython-lib tree. PyBLE adds no copied driver, upstream patch, PBLE/1 capability, or agent GPIO abstraction.from neopixel import NeoPixelmust work offline in file/source runs and after soft reboot. -
No board assumption. Firmware and app store no NeoPixel GPIO, pixel count, colour, named onboard component, or chip/board mapping. The program supplies them. GPIO 48 is therefore a valid user choice for a board whose documentation assigns its RGB LED there, but it is never a PyBLE default.
-
Five composable Blocks types. A localized NeoPixel toolbox category contains:
Type Shape Standard MicroPython generation pyble_neopixel_createoutput NeoPixel; requiredPIN: Pin, requiredPIXELS: NumberNeoPixel(pin, pixels)pyble_neopixel_rgboutput NeoPixelColor; requiredRED/GREEN/BLUE: Number(red, green, blue)pyble_neopixel_set_pixelstatement; required STRIP: NeoPixel,INDEX: Number,COLOR: NeoPixelColorstrip[index] = colorpyble_neopixel_fillstatement; required STRIP: NeoPixel,COLOR: NeoPixelColorstrip.fill(color)pyble_neopixel_writestatement; required STRIP: NeoPixelstrip.write()The constructor reuses ADR-0015's explicit
Pinvalue; itsPIXELSis a positive safe integer literal with no shadow/default. RGB, index, and receiver inputs must be connected and single-line; numeric expressions remain composable and the standard runtime owns their dynamic range/type errors. The generator reservesNeoPixeland owns exactly onefrom neopixel import NeoPixeldefinition.Pinimport ownership remains withpyble_gpio_pin. -
Buffer mutation stays explicit. Set-pixel and fill change only the NeoPixel buffer.
write()is a separate visible block because that is when the standard driver transmits. Off is RGB(0, 0, 0)plus set/fill and write; blink composes those operations with standard loops and the existing Time block. There is no hidden auto-write, implicit delay, brightness transform, or monolithic blink helper. RGBW and non-default timing are outside this increment. -
The bounded reverse converter expands with the generated subset. ADR-0017 additionally admits the exact unaliased, use-dependent import
from neopixel import NeoPixel,NeoPixel(Pin(...), positive_count), three-item RGB tuples, NeoPixel index assignment,.fill(rgb), and.write(). Receivers must be definitely bound NeoPixel variables on every reachable control-flow path and loop iteration. Loop backedges are validated to a conservative fixed point, and a possibly-zero-iterationwhileexit intersects the entry and body-exit bindings. Malformed arity, unsupported RGBW/timing/keyword forms, invalid literal counts, and non-NeoPixel receivers fail the complete conversion. There is no partial or raw-code fallback. -
A generic seventh example is a versioned catalog amendment. Catalog version 2 adds
blink-neopixelafterblink-led. It creates one pixel, sets index zero to a dim RGB value and off, explicitly writes after each buffer change, and uses finite repeats/delays. Its GPIO role is disconnected in the shipped JSON and must be entered by the user before preview/copy. The example never names an S3 board, assumes an onboard LED, stores GPIO 48, or runs automatically. -
Tests are the executable boundary. Red tests precede implementation and prove resolved manifest parity, no copied driver/profile/default, fresh three-chip builds, runtime import, block shapes/imports/calls/name safety, invalid-workspace retention, catalog v2 materialization/generation, bounded Python conversion, sidecar exact reopen, localization/offline policy, and real iPadOS/Android WebView generation. Hardware colour tests use an explicit operator-supplied GPIO, remain dim and bounded, and turn the pixel off in
finally.
PyBLE again matches the standard MicroPython ESP32 user-code surface and can generate portable, inspectable addressable-LED programs. Users must still know their board's GPIO and pixel topology; that is the honest tradeoff for generic ESP32 support. The frozen driver adds a small flash cost to the two lean images, recorded by the existing footprint/build gates, and no new Flutter dependency.