Skip to main content

Running Python on ESP32-S3 with NuttX

·8 mins·
NuttX Apache Python ESP32-S3 POSIX
Author
Tiago Medicci
Embedded Software Engineer at Espressif
Table of Contents

Running Python on ESP32-S3 with NuttX
#

Yes, you heard it right: Apache NuttX now supports the Python interpreter on ESP32-S3!

NuttX is a platform that can run applications built with programming languages other than traditional C. C++, Zig, Rust, Lua, BASIC, MicroPython, and, now, Python.

Why?
#

According to the IEEE Spectrum’s 11th annual rankings, Python leads as the most utilized programming language among IEEE members, with adoption rates over twice as high as the second-ranked language. The 2024 Stack Overflow Developer Survey reinforces this trend, reporting that Python was employed by over 51% of developers in the past year, cementing its position as the top-ranked language outside web-specific domains.

By integrating Python with NuttX, developers–including makers and those outside traditional embedded programming–gain access to a familiar ecosystem for building embedded applications, supported by Python’s vast library ecosystem and open-source tools. NuttX further complements this by offering a POSIX-compliant interface, enabling seamless porting of Python projects (which rely on POSIX APIs) and allowing Python applications to interact with hardware through socket interfaces and character drivers.

In essence, Python on NuttX creates a unified framework for hardware interaction. Developers can leverage Python scripts to directly access buses, peripherals, and other NuttX-supported hardware components.

Critics may argue that Python was never intended for resource-constrained devices (typical in NuttX RTOS environments) and, instead, advocate for alternative embedded tools. However, recent advancements–particularly Python’s optimizations for WebAssembly–have significantly reduced its memory footprint and system demands. These improvements make Python increasingly viable even for low-resource environments.

You can find more information on how (and why!) Python was ported to NuttX in this article: Apache NuttX: Porting Python to NuttX. Now, let’s try Python on ESP32-S3!

Building Python for ESP32-S3 on NuttX
#

Hardware Requirements
#

A ESP32-S3 board with at least 16MiB of flash and an external PSRAM of 8MiB or more is required to run Python.

Check the ESP Product Selector to find suitable modules providing the required flash size and memory. The example below uses the ESP32-S3-DevKitC-1 v1.1 board that integrates the ESP32-S3-WROOM-2-N32R8V module:

ESP32-S3-DevKitC-1 v1.1

Software Requirements
#

For those new to NuttX, we recommend reviewing the guide Getting Started with NuttX and ESP32 to configure your development environment for building NuttX applications.

Compiling and Flashing
#

Clean any previous configuration and set the defconfig to enable the configurations required for building Python on ESP32-S3:

make -j distclean && ./tools/configure.sh esp32s3-devkit:python

To build and flash NuttX, run:

make flash ESPTOOL_BINDIR=./ ESPTOOL_PORT=/dev/ttyUSB0 -s -j$(nproc)

Now you can grab a coffee ☕ (Yes, we are building Python libraries and modules. It will take a while to build and flash).

After compilation and flashing finish, open the serial terminal instance to interface with NuttX’s NuttShell (NSH).

Running Python on ESP32-S3
#

After successful building and flashing, run the following command to open the NuttShell:

minicom -D /dev/ttyUSB0

Type help to check the available applications on NuttX:

nsh> help
help usage:  help [-v] [<cmd>]

    .           cmp         fdinfo      ls          pwd         truncate
    [           dirname     free        lsmod       readlink    uname
    ?           dd          help        mkdir       rm          umount
    alias       df          hexdump     mkfifo      rmdir       unset
    unalias     dmesg       ifconfig    mkrd        rmmod       uptime
    arp         echo        ifdown      mount       set         usleep
    basename    env         ifup        mv          sleep       watch
    break       exec        insmod      nslookup    source      wget
    cat         exit        kill        pidof       test        xd
    cd          expr        pkill       printf      time        wait
    cp          false       ln          ps          true

Builtin Apps:
    nsh       ping      renew     wapi      ws2812
    ostest    python    sh        wget

As you can see, python is one of the built-in applications that can run on NuttShell:

nsh> python
Python 3.13.0 (main, Feb 17 2025, 16:20:05) [GCC 12.2.0] on nuttx
Type "help", "copyright", "credits" or "license" for more information.
>>>

Here we are! ESP32-S3 is running the Python interpreter as an application on NuttX 🚀

Well, let’s quit Python interpreter and experiment with Python on NuttX!

>>> quit()

Creating a Python Script
#

NuttX provides a POSIX-compatible interface that can be used by Python scripts directly. Python’s built-in functions like open and write can be used to open and write to a character driver directly. Also, information about the active tasks and other system information are available through the PROCFS filesystem mounted at /proc/, which can be read directly with Python’s read function, for instance.

Considering that the ESP32-S3-DevKitC-1 board features a WS2812 LED (addressable RGB LED), a simple Python script could be used to monitor the CPU usage and provide an indication of the CPU load in a scale that goes from green to red when CPU usage varies from 0% to 100%.

After some prompts on DeepSeek (or ChatGPT, grab your favorite LLM!), the following script was generated:

import sys
import struct
import select

def get_cpu_load():
    try:
        with open('/proc/cpuload', 'r') as f:
            content = f.read().strip()
            # Extract numeric value and remove percentage sign
            percent_str = content.replace('%', '').strip()
            load_percent = float(percent_str)
            normalized_load = load_percent / 100.0
            return max(0.0, min(normalized_load, 1.0))
    except IOError as e:
        print(f"Error reading /proc/cpuload: {e}")
        sys.exit(1)
    except ValueError:
        print(f"Invalid data in /proc/cpuload: '{content}'")
        sys.exit(1)

def main():
    try:
        while True:
            load = get_cpu_load()

            # Calculate RGB values with proper rounding
            r = int(load * 255 + 0.5)
            g = int((1 - load) * 255 + 0.5)
            b = 0

            # Pack as BGR0 (4 bytes) for the LED
            data = struct.pack('4B', b, g, r, 0)

            # Write to device
            try:
                with open('/dev/leds0', 'wb') as f:
                    f.write(data)
            except IOError as e:
                print(f"Error writing to device: {e}")
                sys.exit(1)

            # Wait 100ms using select (instead of time.sleep)
            select.select([], [], [], 0.1)

    except KeyboardInterrupt:
        print("\nExiting...")
        sys.exit(0)

if __name__ == '__main__':
    main()

It reads CPU usage from /proc/cpuload, transforms it to a scale that goes from green to red, and, then, sends it to the registered character driver of the RGB LED.

Save this file on your host computer with the name cpumon.py, for instance.

How should we run this script on the board?

Transfering the Python Script
#

We could create a ROMFS partition containing this script and then run it on NuttX. However, the embedded application concept is somehow related to the ability to run it without needing to reflash a device. So, can we send it through the Wi-Fi network to the board?

Yes, we can!

Connect the board to your Wi-Fi network:

nsh> wapi psk wlan0 <password> 3
nsh> wapi essid wlan0 <ssid> 1
nsh> renew wlan0

On the host computer, create a simple HTTP server with Python in the folder that contains the Python script to be sent:

python -m http.server 8080

Now, check if the script is available through the network by accessing the host computer’s IP address (if possible, test it on a different machine). For instance, try to open in the web browser with http://<host_computer_ip>:8080/cpumon.py.

Once everything is set on the host side, download the Python script to the board:

nsh> wget /tmp/cpumon.py http://<host_computer_ip>:8080/cpumon.py

And, then, check if it was successfully downloaded:

nsh> cat /tmp/cpumon.py
import sys
import struct
import select

def get_cpu_load():
.
.
.

Running the Python Script
#

Finally, run the Python script in the background:

nsh> python /tmp/cpumon.py &
python [12:100]

In a few seconds, the LED will show the CPU load average while the Python script is running in the background. To test it properly, create one or more instances of the cpuload app that increases CPU usage:

nsh> cpuload &
cpuload [13:253]
nsh> cpuload &

With 3 (three) instances of cpuload, the LED should be reddish as CPU load usage reaches nearly 90%:

nsh> python /tmp/cpumon.py &
python [12:100]
nsh> cpuload &
cpuload [13:253]
nsh> cpuload &
cpuload [14:253]
nsh> cpuload &
cpuload [15:253]
nsh> ps
  PID GROUP CPU PRI POLICY   TYPE    NPX STATE    EVENT     SIGMASK            STACK    CPU COMMAND
    0     0   0   0 FIFO     Kthread   - Assigned           0000000000000000 0003008  6.4% CPU0 IDLE
    1     0   1   0 FIFO     Kthread   - Assigned           0000000000000000 0003008  8.5% CPU1 IDLE
    2     0 --- 100 RR       Kthread   - Waiting  Semaphore 0000000000000000 0001928  0.0% lpwork 0x3fcaa2c4 0x3fcaa24
    3     3   1 100 RR       Task      - Running            0000000000000000 0002976  0.0% nsh_main
    4     0 --- 255 RR       Kthread   - Waiting  Semaphore 0000000000000000 0000656  0.0% spiflash_op 0x3fcd0d4c
    5     0 --- 255 RR       Kthread   - Waiting  Semaphore 0000000000000000 0000656  0.0% spiflash_op 0x3fcd0d4c
    6     0 --- 223 RR       Kthread   - Waiting  Semaphore 0000000000000000 0001944  0.0% rt_timer
    7     0 --- 253 RR       Kthread   - Waiting  MQ empty  0000000000000000 0006576  0.0% wifi
   12    12   0 100 RR       Task      - Assigned           0000000000000000 0307088 12.0% python /tmp/cpumon.py
   13    13   1 253 RR       Task      - Running            0000000000000000 0001968 26.9% cpuload
   14    14 --- 253 RR       Task      - Waiting  Signal    0000000000000000 0001960 25.1% cpuload
   15    15 --- 253 RR       Task      - Waiting  Signal    0000000000000000 0001960 22.3% cpuload
nsh> cat /proc/cpuload
 89.3%

The following video shows this demo: the RGB LED goes from green to red when the CPU load average goes from 0% to 100%.

Conclusion
#

Running the Python interpreter on NuttX is possible mainly due to its POSIX-compatible interface. In addition to that, NuttX exposes the drivers for each board’s peripherals as block or character drivers. Then, accessing the drivers is as easy as reading and writing to a file (and using the ioctl interface), just like any other Unix-based system.

With that in mind, building applications on Python that access boards’ peripherals is just straightforward: relatively complex applications can be built in Python in minutes and, then, run on NuttX without the need of reflashing the device or compiling anything externally. That’s why we call them embedded applications: those applications are developed and tested externally–on a host PC, for instance–and, once validated, they can be transferred and run on NuttX without the need of knowing the inner details of the NuttX RTOS because it offers a well-known interface and well-known programming language.

Stay tuned for more updates about Python on NuttX!

Useful Links#

Related

Building Applications on NuttX: Understanding the Build System
·12 mins
NuttX Apache ESP32 POSIX Linux Tutorial
Getting Started with NuttX and ESP32
·8 mins
NuttX Apache ESP32 POSIX Linux
Using Wokwi with NuttX
·6 mins
NuttX Apache ESP32 POSIX Linux Wokwi Simulation