|
| 1 | +--- |
| 2 | +title: "Running Python on ESP32-S3 with NuttX" |
| 3 | +date: 2025-03-07T08:00:00-03:00 |
| 4 | +tags: ["NuttX", "Apache", "Python", "ESP32-S3", "POSIX"] |
| 5 | +series: ["nuttx-apps"] |
| 6 | +series_order: 1 |
| 7 | +showAuthor: false |
| 8 | +authors: |
| 9 | + - "tiago-medicci" |
| 10 | +--- |
| 11 | + |
| 12 | +# Running Python on ESP32-S3 with NuttX |
| 13 | + |
| 14 | +Yes, you heard it right: Apache NuttX now supports the Python interpreter on ESP32-S3! |
| 15 | + |
| 16 | +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**. |
| 17 | + |
| 18 | +## Why? |
| 19 | + |
| 20 | +According to the [IEEE Spectrum’s 11th annual rankings](https://spectrum.ieee.org/top-programming-languages-2024), 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](https://survey.stackoverflow.co/2024/technology/#1-programming-scripting-and-markup-languages) 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. |
| 21 | + |
| 22 | +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. |
| 23 | + |
| 24 | +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. |
| 25 | + |
| 26 | +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. |
| 27 | + |
| 28 | +You can find more information on how (and why!) Python was ported to NuttX in this article: [Apache NuttX: Porting Python to NuttX](https://tmedicci.github.io/articles/2025/01/08/python_on_nuttx.html). Now, let's try Python on ESP32-S3! |
| 29 | + |
| 30 | +## Building Python for ESP32-S3 on NuttX |
| 31 | + |
| 32 | +### Hardware Requirements |
| 33 | + |
| 34 | +A ESP32-S3 board with at least 16MiB of flash and an external PSRAM of 8MiB or more is required to run Python. |
| 35 | + |
| 36 | +Check the [ESP Product Selector](https://products.espressif.com/) to find suitable modules providing the required flash size and memory. The example below uses the [*ESP32-S3-DevKitC-1 v1.1*](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32s3/esp32-s3-devkitc-1/user_guide.html) board that integrates the `ESP32-S3-WROOM-2-N32R8V` module: |
| 37 | + |
| 38 | + |
| 39 | + |
| 40 | +### Software Requirements |
| 41 | + |
| 42 | +For those new to NuttX, we recommend reviewing the guide [Getting Started with NuttX and ESP32](../../../nuttx-getting-started) to configure your development environment for building NuttX applications. |
| 43 | + |
| 44 | +### Compiling and Flashing |
| 45 | + |
| 46 | +Clean any previous configuration and set the `defconfig` to enable the configurations required for building Python on ESP32-S3: |
| 47 | + |
| 48 | +``` |
| 49 | +make -j distclean && ./tools/configure.sh esp32s3-devkit:python |
| 50 | +``` |
| 51 | + |
| 52 | +To build and flash NuttX, run: |
| 53 | + |
| 54 | +``` |
| 55 | +make flash ESPTOOL_BINDIR=./ ESPTOOL_PORT=/dev/ttyUSB0 -s -j$(nproc) |
| 56 | +``` |
| 57 | + |
| 58 | +Now you can grab a coffee :coffee: (*Yes, we are building Python libraries and modules. It will take a while to build and flash*). |
| 59 | + |
| 60 | +After compilation and flashing finish, open the serial terminal instance to interface with NuttX's *NuttShell* (NSH). |
| 61 | + |
| 62 | +## Running Python on ESP32-S3 |
| 63 | + |
| 64 | +After successful building and flashing, run the following command to open the *NuttShell*: |
| 65 | + |
| 66 | +``` |
| 67 | +minicom -D /dev/ttyUSB0 |
| 68 | +``` |
| 69 | + |
| 70 | +Type `help` to check the available applications on NuttX: |
| 71 | + |
| 72 | +``` |
| 73 | +nsh> help |
| 74 | +help usage: help [-v] [<cmd>] |
| 75 | +
|
| 76 | + . cmp fdinfo ls pwd truncate |
| 77 | + [ dirname free lsmod readlink uname |
| 78 | + ? dd help mkdir rm umount |
| 79 | + alias df hexdump mkfifo rmdir unset |
| 80 | + unalias dmesg ifconfig mkrd rmmod uptime |
| 81 | + arp echo ifdown mount set usleep |
| 82 | + basename env ifup mv sleep watch |
| 83 | + break exec insmod nslookup source wget |
| 84 | + cat exit kill pidof test xd |
| 85 | + cd expr pkill printf time wait |
| 86 | + cp false ln ps true |
| 87 | +
|
| 88 | +Builtin Apps: |
| 89 | + nsh ping renew wapi ws2812 |
| 90 | + ostest python sh wget |
| 91 | +``` |
| 92 | + |
| 93 | +As you can see, `python` is one of the built-in applications that can run on *NuttShell*: |
| 94 | + |
| 95 | +``` |
| 96 | +nsh> python |
| 97 | +Python 3.13.0 (main, Feb 17 2025, 16:20:05) [GCC 12.2.0] on nuttx |
| 98 | +Type "help", "copyright", "credits" or "license" for more information. |
| 99 | +>>> |
| 100 | +``` |
| 101 | + |
| 102 | +Here we are! ESP32-S3 is running the Python interpreter as an application on NuttX :rocket: |
| 103 | + |
| 104 | +Well, let's quit Python interpreter and experiment with Python on NuttX! |
| 105 | + |
| 106 | +``` |
| 107 | +>>> quit() |
| 108 | +``` |
| 109 | + |
| 110 | +### Creating a Python Script |
| 111 | + |
| 112 | +NuttX provides a POSIX-compatible interface that can be used by Python scripts directly. Python's built-in functions like [`open`](https://docs.python.org/3/library/functions.html#open) and [`write`](https://docs.python.org/3/library/io.html#io.TextIOBase.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`](https://docs.python.org/3/library/io.html#io.TextIOBase.read) function, for instance. |
| 113 | + |
| 114 | +Considering that the [ESP32-S3-DevKitC-1](https://nuttx.apache.org/docs/latest/platforms/xtensa/esp32s3/boards/esp32s3-devkit/index.html#board-leds) 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%. |
| 115 | + |
| 116 | +After some prompts on *DeepSeek* (or *ChatGPT*, grab your favorite LLM!), the following script was generated: |
| 117 | + |
| 118 | +{{< highlight python >}} |
| 119 | +import sys |
| 120 | +import struct |
| 121 | +import select |
| 122 | + |
| 123 | +def get_cpu_load(): |
| 124 | + try: |
| 125 | + with open('/proc/cpuload', 'r') as f: |
| 126 | + content = f.read().strip() |
| 127 | + # Extract numeric value and remove percentage sign |
| 128 | + percent_str = content.replace('%', '').strip() |
| 129 | + load_percent = float(percent_str) |
| 130 | + normalized_load = load_percent / 100.0 |
| 131 | + return max(0.0, min(normalized_load, 1.0)) |
| 132 | + except IOError as e: |
| 133 | + print(f"Error reading /proc/cpuload: {e}") |
| 134 | + sys.exit(1) |
| 135 | + except ValueError: |
| 136 | + print(f"Invalid data in /proc/cpuload: '{content}'") |
| 137 | + sys.exit(1) |
| 138 | + |
| 139 | +def main(): |
| 140 | + try: |
| 141 | + while True: |
| 142 | + load = get_cpu_load() |
| 143 | + |
| 144 | + # Calculate RGB values with proper rounding |
| 145 | + r = int(load * 255 + 0.5) |
| 146 | + g = int((1 - load) * 255 + 0.5) |
| 147 | + b = 0 |
| 148 | + |
| 149 | + # Pack as BGR0 (4 bytes) for the LED |
| 150 | + data = struct.pack('4B', b, g, r, 0) |
| 151 | + |
| 152 | + # Write to device |
| 153 | + try: |
| 154 | + with open('/dev/leds0', 'wb') as f: |
| 155 | + f.write(data) |
| 156 | + except IOError as e: |
| 157 | + print(f"Error writing to device: {e}") |
| 158 | + sys.exit(1) |
| 159 | + |
| 160 | + # Wait 100ms using select (instead of time.sleep) |
| 161 | + select.select([], [], [], 0.1) |
| 162 | + |
| 163 | + except KeyboardInterrupt: |
| 164 | + print("\nExiting...") |
| 165 | + sys.exit(0) |
| 166 | + |
| 167 | +if __name__ == '__main__': |
| 168 | + main() |
| 169 | +{{< /highlight >}} |
| 170 | + |
| 171 | +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. |
| 172 | + |
| 173 | +Save this file on your host computer with the name `cpumon.py`, for instance. |
| 174 | + |
| 175 | +How should we run this script on the board? |
| 176 | + |
| 177 | +### Transfering the Python Script |
| 178 | + |
| 179 | +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? |
| 180 | + |
| 181 | +*Yes, we can!* |
| 182 | + |
| 183 | +Connect the board to your Wi-Fi network: |
| 184 | + |
| 185 | +``` |
| 186 | +nsh> wapi psk wlan0 <password> 3 |
| 187 | +nsh> wapi essid wlan0 <ssid> 1 |
| 188 | +nsh> renew wlan0 |
| 189 | +``` |
| 190 | + |
| 191 | +On the host computer, create a simple HTTP server with Python in the folder that contains the Python script to be sent: |
| 192 | + |
| 193 | +``` |
| 194 | +python -m http.server 8080 |
| 195 | +``` |
| 196 | + |
| 197 | +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`. |
| 198 | + |
| 199 | +Once everything is set on the host side, download the Python script to the board: |
| 200 | + |
| 201 | +``` |
| 202 | +nsh> wget /tmp/cpumon.py http://<host_computer_ip>:8080/cpumon.py |
| 203 | +``` |
| 204 | + |
| 205 | +And, then, check if it was successfully downloaded: |
| 206 | + |
| 207 | +``` |
| 208 | +nsh> cat /tmp/cpumon.py |
| 209 | +import sys |
| 210 | +import struct |
| 211 | +import select |
| 212 | +
|
| 213 | +def get_cpu_load(): |
| 214 | +. |
| 215 | +. |
| 216 | +. |
| 217 | +``` |
| 218 | + |
| 219 | +### Running the Python Script |
| 220 | + |
| 221 | +Finally, run the Python script in the background: |
| 222 | + |
| 223 | +``` |
| 224 | +nsh> python /tmp/cpumon.py & |
| 225 | +python [12:100] |
| 226 | +``` |
| 227 | + |
| 228 | +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: |
| 229 | + |
| 230 | +``` |
| 231 | +nsh> cpuload & |
| 232 | +cpuload [13:253] |
| 233 | +nsh> cpuload & |
| 234 | +``` |
| 235 | + |
| 236 | +With 3 (three) instances of `cpuload`, the LED should be *reddish* as CPU load usage reaches nearly 90%: |
| 237 | + |
| 238 | +``` |
| 239 | +nsh> python /tmp/cpumon.py & |
| 240 | +python [12:100] |
| 241 | +nsh> cpuload & |
| 242 | +cpuload [13:253] |
| 243 | +nsh> cpuload & |
| 244 | +cpuload [14:253] |
| 245 | +nsh> cpuload & |
| 246 | +cpuload [15:253] |
| 247 | +nsh> ps |
| 248 | + PID GROUP CPU PRI POLICY TYPE NPX STATE EVENT SIGMASK STACK CPU COMMAND |
| 249 | + 0 0 0 0 FIFO Kthread - Assigned 0000000000000000 0003008 6.4% CPU0 IDLE |
| 250 | + 1 0 1 0 FIFO Kthread - Assigned 0000000000000000 0003008 8.5% CPU1 IDLE |
| 251 | + 2 0 --- 100 RR Kthread - Waiting Semaphore 0000000000000000 0001928 0.0% lpwork 0x3fcaa2c4 0x3fcaa24 |
| 252 | + 3 3 1 100 RR Task - Running 0000000000000000 0002976 0.0% nsh_main |
| 253 | + 4 0 --- 255 RR Kthread - Waiting Semaphore 0000000000000000 0000656 0.0% spiflash_op 0x3fcd0d4c |
| 254 | + 5 0 --- 255 RR Kthread - Waiting Semaphore 0000000000000000 0000656 0.0% spiflash_op 0x3fcd0d4c |
| 255 | + 6 0 --- 223 RR Kthread - Waiting Semaphore 0000000000000000 0001944 0.0% rt_timer |
| 256 | + 7 0 --- 253 RR Kthread - Waiting MQ empty 0000000000000000 0006576 0.0% wifi |
| 257 | + 12 12 0 100 RR Task - Assigned 0000000000000000 0307088 12.0% python /tmp/cpumon.py |
| 258 | + 13 13 1 253 RR Task - Running 0000000000000000 0001968 26.9% cpuload |
| 259 | + 14 14 --- 253 RR Task - Waiting Signal 0000000000000000 0001960 25.1% cpuload |
| 260 | + 15 15 --- 253 RR Task - Waiting Signal 0000000000000000 0001960 22.3% cpuload |
| 261 | +nsh> cat /proc/cpuload |
| 262 | + 89.3% |
| 263 | +``` |
| 264 | + |
| 265 | +The following video shows this demo: the RGB LED goes from green to red when the CPU load average goes from 0% to 100%. |
| 266 | + |
| 267 | +{{< youtube EpDpqbnYJyo >}} |
| 268 | + |
| 269 | +## Conclusion |
| 270 | + |
| 271 | +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. |
| 272 | + |
| 273 | +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. |
| 274 | + |
| 275 | +*Stay tuned for more updates about Python on NuttX!* |
| 276 | + |
| 277 | +## Useful Links |
| 278 | + |
| 279 | +- [NuttX Documentation: Python](https://nuttx.apache.org/docs/latest/applications/interpreters/python/index.html) |
| 280 | +- [Apache NuttX: Porting Python to NuttX](https://tmedicci.github.io/articles/2025/01/08/python_on_nuttx.html) |
| 281 | +- [NuttX GitHub](https://github.com/apache/nuttx) |
| 282 | +- [NuttX channel on Youtube](https://www.youtube.com/nuttxchannel) |
| 283 | +- [Developer Mailing List](https://nuttx.apache.org/community/#mailing-list) |
0 commit comments