Python Save File in a Thread

A friend asked me how to run compute while it is saving data to a file in the background on a Raspberry PI. The whole program will not be async, so the simplest way is to use old school threading.

The first version will use the threading Python module. The data is an image downloaded from picsum.photos but saved in a thread without blocking. Some logging is added to show execution order and timings.

import logging
import threading
import time
from pathlib import Path

import requests

logger = logging.getLogger(__name__)

def get_random_image():
    r = requests.get("https://picsum.photos/1000/1000")
    return r.content

def save_data(name, data):
    logger.info("start save")
    with Path(name).open("wb") as fp:
        fp.write(data)
    logger.info("end save")

def main():
    logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
    x = threading.Thread(target=save_data, args=("image.jpg", get_random_image()))
    x.start()
    logger.info("start compute")
    # add some real compute here instead
    time.sleep(1)
    logger.info("end compute")
    # actually not needed; the program will wait until the thread finishes
    x.join()

if __name__ == "__main__":
    main()

When run this is printed:

2024-04-16 22:19:10,489 start save
2024-04-16 22:19:10,489 start compute
2024-04-16 22:19:10,490 end save
2024-04-16 22:19:11,490 end compute

Compute ends last, because the sleep of 1 second is actually slower than saving a file to an SSD on my notebook.

The second version is using ThreadPoolExecutor from the concurrent.futures module. The ThreadPoolExecutor will execute code in a pool of threads the same as the previous example but with a newer API.

The same example as before, but using concurrent.futures.ThreadPoolExecutor:

import logging
import time
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path

import requests

logger = logging.getLogger(__name__)

def get_random_image():
    r = requests.get("https://picsum.photos/1000/1000")
    return r.content

def save_data(name, data):
    logger.info("start save")
    with Path(name).open("wb") as fp:
        fp.write(data)
    logger.info("end save")

def main():
    logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
    # start 1 threadpool executor
    x = ThreadPoolExecutor(1)
    x.submit(save_data, "image.jpg", get_random_image())
    logger.info("start compute")
    # add some real compute here instead
    time.sleep(1)
    logger.info("end compute")
    # remove the executor savely; wait=True is the default
    x.shutdown()

if __name__ == "__main__":
    main()

Running this code returned for me:

2024-04-16 22:38:25,391 start save
2024-04-16 22:38:25,391 start compute
2024-04-16 22:38:25,392 end save
2024-04-16 22:38:26,392 end compute

The final option is to replace ThreadPoolExecutor with ProcessPoolExecutor. The API works the same, but the ProcessPoolExecutor is using the multiprocessing module which will start extra processes. Using extra processes helps with the Global Interpreter Lock, but all data moved between them has to be serializable. For us this is a binary stream of content (the image) and a filename, so no issue here. But more complex data structures may need some additional serialization to be moved around.

Using a ProcessPoolExecutor will return nearly the same results as the previous versions:

2024-04-16 22:44:24,757 start compute
2024-04-16 22:44:24,759 start save
2024-04-16 22:44:24,759 end save
2024-04-16 22:44:25,757 end compute

I would probably use the ThreadPoolExecutor while developing my program and replace it later with ProcessPoolExecutor if it is actually faster without breaking anything.

ESP32 with Rust

Since Embassy only support ESP32C3 and I only have old ESP32 lying around I have to use a different toolchain than last week. As last week with the Pico this time I want to get a Rust program installed on an ESP32. I did not use probe-rs as last time, but cargo espflash.

First we need to install some Rust prerequisites listed in the ESP Rust book. We need Rust obviously. Best to install via rustup and not your system package. Then some ESP specific things:

cargo install espup
# install toolchain
~/.cargo/bin/espup install
# source the environment variables needed
. ~/export-esp.sh
# install ldproxy
cargo install ldproxy
# install cargo-espflash (binstall if you can)
cargo install cargo-espflash

To get which chip you have cargo-espflash can help:

$ cargo espflash board-info

Returns for my old ESP32:

Chip type:         esp32 (revision v1.0)
Crystal frequency: 40 MHz
Flash size:        4MB
Features:          WiFi, BT, Dual Core, 240MHz, Coding Scheme None
MAC address:       b4:e6:2d:97:d2:79

Next we need an example. To generate one we generate a new project based on a template:

# install generate
cargo install cargo-generate
# use generate to get a minimal example with a working cargo.toml
cargo generate esp-rs/esp-template -n blinky
# I chose "esp32" as MCU; without advanced options

Plugin the ESP32 on any USB port. To have write permissions you may need to add your user to a group that is allowed to write. I described this in a previous blog post.

Then build and install:

cargo build
# flash using cargo-espflash
cargo espflash flash --monitor

The code should now run on the ESP32 and printing first "Hello world!" and then "Loop..." until Ctrl-C is pressed.

Now to the actual blinking (of an LED). The blue LED of my ESP-WROOM-32 is GPIO2. If your ESP32 doesn't have an LED you can connect one to a GPIO pin with a resistor (i.e. red LED and 330Ω) and connect the other side to ground. I used GPIO 23 for the example below because it is at the end of the ESP32.

Change the src/main.rs to:

#![no_std]
#![no_main]

use esp_backtrace as _;
use esp_hal::{clock::ClockControl, peripherals::Peripherals, prelude::*, Delay, IO};

#[entry]
fn main() -> ! {
    let peripherals = Peripherals::take();
    let system = peripherals.SYSTEM.split();

    let clocks = ClockControl::max(system.clock_control).freeze();
    let mut delay = Delay::new(&clocks);

    let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);
    // change to the GPIO you used, GPIO2 for onboard blue LED (if you have)
    let mut led = io.pins.gpio23.into_push_pull_output();

    loop {
        delay.delay_ms(500u32);
        led.toggle().unwrap();
    }
}

Using GPIO2 on an ESP-WROOM-32:

img1

and using an external LED with a resistor:

img2

Raspberry PI Pico with Rust (Embassy)

After I failed to do HTTP requests in Micropython as described in a previous post, I felt like it is time to try Rust on the Raspberry PI Pico. Installing code on the Pico with Rust can be done using UF2 as with Micropython. But because there is no REPL there is no way to see the outputs of the program this way. So using a Debugprobe as described in the Embassy getting started is a good plan.

We need to prepare a second Pico as Debugprobe. The UF2 needed for this can be build as described in Appendix A of the Getting started with Raspberry Pi Pico. But building the UF2 is not neccessary because there is already a version available that can be easily flashed on the Pico by dragging it on the Pico filesystem after button-press connecting it to your computer.

Now lets build an example firmware with Embassy. Important here there are two versions of blinky: blinky.rs is for the Pico and the wifi_blinky.rs for the Pico W. I have a Pico W connected so I did this to compile (as described in the Getting started but with some Archlinux):

# setup rust (on Archlinux)
sudo pacman -S rustup cargo-binstall
# choose stable rust
rustup default stable

git clone https://github.com/embassy-rs/embassy.git
cd embassy/examples/rp
# build Pico W example
cargo build --bin wifi_blinky --release

# install probe-rs software - the binary will land in ~/.cargo/bin/probe-rs
cargo binstall probe-rs

Wire the Debugprobe Pico to the Pico we want to get our code on. My version of this:

img1

In the Appendix A of the Getting started with Raspberry Pi Pico is a schematic which is lot clearer than my photo!

I don't want to use sudo to use the Debugprobe, so we need a udev rule. This is described in the getting started of probe-rs:

curl https://probe.rs/files/69-probe-rs.rules > 69-probe-rs.rules
sudo mv 69-probe-rs.rules /etc/udev/rules.d
sudo udevadm control --reload
sudo udevadm trigger

Check that probe-rs is finding the Debugprobe Pico:

~/.cargo/bin/probe-rs list
# should return:
[0]: Debugprobe on Pico (CMSIS-DAP) (VID: 2e8a, PID: 000c, Serial: E660C06213776B27, CmsisDap

Finally run the blinky example:

cargo run --bin wifi_blinky --release

If successful this shows a lot of prints like this on your shell after flashing and initializing:

1.238451 INFO  led on!
└─ wifi_blinky::____embassy_main_task::{async_fn#0} @ src/bin/wifi_blinky.rs:58
1.238481 DEBUG set gpioout = [01, 00, 00, 00, 01, 00, 00, 00]
└─ cyw43::control::{impl#1}::set_iovar_v::{async_fn#0} @ /home/mfa/pico/embassy/cyw43/src/fmt.rs:130
2.239012 INFO  led off!
└─ wifi_blinky::____embassy_main_task::{async_fn#0} @ src/bin/wifi_blinky.rs:62
2.239030 DEBUG set gpioout = [01, 00, 00, 00, 00, 00, 00, 00]
└─ cyw43::control::{impl#1}::set_iovar_v::{async_fn#0} @ /home/mfa/pico/embassy/cyw43/src/fmt.rs:130