How to install python packages on the Arduino Q

ISSUE:

Recently a new PR was merged to enable spi peripheral interface . To test this on the Q you have to rebuild the core (only works on the 4gb version). After doing that I tried to run the example posted in the PR but ran into issues with spidev not installing correctly, for ref here is the whole app:
SPI3_Test.zip (2.9 KB)

TESTING:

Method 1: Use a requirements.txt file to specify use of spidev
This method tries to install and build spidev=3.8 but fails to build the package for the Q: see: How to install Python Packages - Development Tools / App Lab - Arduino Forum

Using CPython 3.13.9 interpreter at: /usr/local/bin/python
Creating virtual environment at: .cache/.venv
Activating python virtual environment
Traceback (most recent call last):
File "/app/python/main.py", line 4, in <module>
import spidev
ModuleNotFoundError: No module named 'spidev'
Activating python virtual environment
Using Python 3.13.9 environment at: .cache/.venv
Resolved 1 package in 270ms
Building spidev==3.8
× Failed to build `spidev==3.8`
├─▶ The build backend returned an error
╰─▶ Call to `setuptools.build_meta.build_wheel` failed (exit status: 1)
[stdout]
running bdist_wheel
running build
running build_ext
building 'spidev' extension
creating build/temp.linux-aarch64-cpython-313
gcc -fno-strict-overflow -Wsign-compare -DNDEBUG -g -O3
-Wall -fPIC -I/app/.cache/uv/builds-v0/.tmpKjKOOd/include
-I/usr/local/include/python3.13 -c spidev_module.c -o
build/temp.linux-aarch64-cpython-313/spidev_module.o
[stderr]
/app/.cache/uv/builds-v0/.tmpKjKOOd/lib/python3.13/site-packages/setuptools/dist.py:765:
SetuptoolsDeprecationWarning: License classifiers are deprecated.
!!

********************************************************************************
Please consider removing the following classifiers in favor of a
SPDX license expression:
License :: OSI Approved :: MIT License
See
https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#license
for details.

********************************************************************************
!!
self._finalize_license_expression()
error: command 'gcc' failed: No such file or directory
hint: This usually indicates a problem with the package or the build
environment.
Clearing cache at: .cache/uv
Removed 467 files (4.1MiB)
Traceback (most recent call last):
File "/app/python/main.py", line 4, in <module>
import spidev
ModuleNotFoundError: No module named 'spidev'
exited with code 1

METHOD 2:
Tried to use the method as described here: My problems with App Lab 0.5.0 - Development Tools / App Lab - Arduino Forum

import subprocess
import sys
#import spidev

try:
    import spidev
except ModuleNotFoundError:
    print("Installing spidev package...")
    subprocess.check_call([sys.executable, "-m", "pip", "install", "spidev", "--break-system-packages"])
    print("spidev package installed successfully!")
    import spidev

But still fails to build

Activating python virtual environment
Installing spidev package...
Collecting spidev
Downloading spidev-3.8.tar.gz (13 kB)
Installing build dependencies: started
Installing build dependencies: finished with status 'done'
Getting requirements to build wheel: started
Getting requirements to build wheel: finished with status 'done'
Preparing metadata (pyproject.toml): started
Preparing metadata (pyproject.toml): finished with status 'done'
Building wheels for collected packages: spidev
Building wheel for spidev (pyproject.toml): started
Building wheel for spidev (pyproject.toml): finished with status 'error'
error: subprocess-exited-with-error

× Building wheel for spidev (pyproject.toml) did not run successfully.
│ exit code: 1
╰─> [20 lines of output]
/tmp/pip-build-env-1xu4zdjs/overlay/lib/python3.13/site-packages/setuptools/dist.py:765: SetuptoolsDeprecationWarning: License classifiers are deprecated.
!!

********************************************************************************
Please consider removing the following classifiers in favor of a SPDX license expression:

License :: OSI Approved :: MIT License

See https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#license for details.
********************************************************************************

!!
self._finalize_license_expression()
running bdist_wheel
running build
running build_ext
building 'spidev' extension
creating build/temp.linux-aarch64-cpython-313
gcc -fno-strict-overflow -Wsign-compare -DNDEBUG -g -O3 -Wall -fPIC -I/app/.cache/.venv/include -I/usr/local/include/python3.13 -c spidev_module.c -o build/temp.linux-aarch64-cpython-313/spidev_module.o
error: command 'gcc' failed: No such file or directory
[end of output]

note: This error originates from a subprocess, and is likely not a problem with pip.
ERROR: Failed building wheel for spidev
Failed to build spidev
[notice] A new release of pip is available: 25.2 -> 26.0.1
[notice] To update, run: python -m pip install --upgrade pip
error: failed-wheel-build-for-install
× Failed to build installable wheels for some pyproject.toml based projects
╰─> spidev
Traceback (most recent call last):
File "/app/python/main.py", line 7, in <module>
import spidev
ModuleNotFoundError: No module named 'spidev'
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/app/python/main.py", line 10, in <module>
subprocess.check_call([sys.executable, "-m", "pip", "install", "spidev", "--break-system-packages"])
~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.13/subprocess.py", line 419, in check_call
raise CalledProcessError(retcode, cmd)
subprocess.CalledProcessError: Command '['/app/.cache/.venv/bin/python', '-m', 'pip', 'install', 'spidev', '--break-system-packages']' returned non-zero exit status 1.
exited with code 1

Method 3: Tried a fake out by creating a python script that only runs the hello world script so I would have a .venv created for the sketch and tried to install manually but:

arduino@arduinoQ4:~/ArduinoApps/copy-of-spi3_test$ source .cache/.venv/bin/activate
(.venv) arduino@arduinoQ4:~/ArduinoApps/copy-of-spi3_test$ which python
(.venv) arduino@arduinoQ4:~/ArduinoApps/copy-of-spi3_test$ which pip
/usr/bin/pip
(.venv) arduino@arduinoQ4:~/ArduinoApps/copy-of-spi3_test$ pip3 install spidev
error: externally-managed-environment

notice when I do a which pip it still points to /usr/bin/pip and gives the standard error about managed environment. Usually to get around that I would use sudo apt-get install python3-spidev. So let try that next.

Method 3.
issued command sudo apt-get install python3-spidev.
In requirements.txt said to use spidev==3.6 you just get the error

Activating python virtual environment
Traceback (most recent call last):
File "/app/python/main.py", line 4, in <module>
import spidev
ModuleNotFoundError: No module named 'spidev'
exited with code 1

So the big question is how to you install package 'spidev for use on the Q?

Note: I also tried the requirements.txt approach,

I tried also spidev=3.6
it failed the same way.

Hi @Merlin513.

Unfortunately the Spidev Python package has a lot of dependencies. The cause of the error is that these dependencies are not present in the Docker container in which the App's Python script runs. You would need to install them in the container.

There are a lot of advantages to using containers. However, it also introduces extra complexity to an endeavor. So if you are only interested in general experimentation, your alternative approach of working directly in the standard Linux machine of the UNO Q might be the best place to start. Once you achieve success in that environment, you could use what you learned to set up a container.

When you run an App, it creates a virtual Python dependencies environment inside the Docker container in which the Python script component of the App runs. So if you are trying to work in the standard environment of the UNO Q's Linux machine, you will not have any success trying to use the App to generate an environment. You should instead generate the environment directly.

I would normally recommend using my favorite tool, Poetry for this purpose. However, the tool chosen by the developers of the Arduino App framework is uv. It makes sense to utilize the same tool used in the App's container, and I believe this is actually more lightweight than Poetry, and thus well suited for use on the UNO Q where we must conserve our use of computing resources.

So let's start by installing uv:

https://docs.astral.sh/uv/getting-started/installation/

I used this method:

curl -LsSf https://astral.sh/uv/install.sh | sh && source $HOME/.local/bin/env

Now create a uv project:

uv init ~/spidevtest && cd ~/spidevtest

Now we would expect to be able to install the project's package dependencies via uv add. However, if we try to do this for the spidev package, we encounter the same type of problem of missing dependencies:

$ uv add spidev
Using CPython 3.13.5 interpreter at: /usr/bin/python3.13
Creating virtual environment at: .venv
Resolved 2 packages in 558ms
  × Failed to build `spidev==3.8`
  ├─▶ The build backend returned an error
  ╰─▶ Call to `setuptools.build_meta.build_wheel` failed (exit status: 1)

      [stdout]
      running bdist_wheel
      running build
      running build_ext
      building 'spidev' extension
      creating build/temp.linux-aarch64-cpython-313
      aarch64-linux-gnu-gcc -fno-strict-overflow -Wsign-compare -DNDEBUG -g -O2 -Wall -fPIC -I/home/arduino/.cache/uv/builds-v0/.tmp42K2yj/include -I/usr/include/python3.13 -c spidev_module.c -o build/temp.linux-aarch64-cpython-313/spidev_module.o

      [stderr]
      /home/arduino/.cache/uv/builds-v0/.tmp42K2yj/lib/python3.13/site-packages/setuptools/dist.py:765: SetuptoolsDeprecationWarning: License classifiers are deprecated.
      !!

              ********************************************************************************
              Please consider removing the following classifiers in favor of a SPDX license expression:

              License :: OSI Approved :: MIT License

              See https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#license for details.
              ********************************************************************************

      !!
        self._finalize_license_expression()
      error: command 'aarch64-linux-gnu-gcc' failed: No such file or directory

      hint: This usually indicates a problem with the package or the build environment.
  help: If you want to add the package regardless of the failed resolution, provide the `--frozen` flag to skip locking and syncing.

It seems like the logical next step is installing GCC, which I did like so:

sudo apt install gcc-aarch64-linux-gnu

I then had another go at installing the spidev package:

$ uv add spidev
Resolved 2 packages in 13ms
  × Failed to build `spidev==3.8`
  ├─▶ The build backend returned an error
  ╰─▶ Call to `setuptools.build_meta.build_wheel` failed (exit status: 1)

      [stdout]
      running bdist_wheel
      running build
      running build_ext
      building 'spidev' extension
      aarch64-linux-gnu-gcc -fno-strict-overflow -Wsign-compare -DNDEBUG -g -O2 -Wall -fPIC -I/home/arduino/.cache/uv/builds-v0/.tmpYCinb5/include -I/usr/include/python3.13 -c spidev_module.c -o build/temp.linux-aarch64-cpython-313/spidev_module.o

      [stderr]
      /home/arduino/.cache/uv/builds-v0/.tmpYCinb5/lib/python3.13/site-packages/setuptools/dist.py:765: SetuptoolsDeprecationWarning: License classifiers are deprecated.
      !!

              ********************************************************************************
              Please consider removing the following classifiers in favor of a SPDX license expression:

              License :: OSI Approved :: MIT License

              See https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#license for details.
              ********************************************************************************

      !!
        self._finalize_license_expression()
      spidev_module.c:28:10: fatal error: Python.h: No such file or directory
         28 | #include <Python.h>
            |          ^~~~~~~~~~
      compilation terminated.
      error: command '/usr/bin/aarch64-linux-gnu-gcc' failed with exit code 1

      hint: This error likely indicates that you need to install a library that provides "Python.h" for `spidev@3.8`
  help: If you want to add the package regardless of the failed resolution, provide the `--frozen` flag to skip locking and syncing.

Looks like progress, but not success. I next tried this:

sudo apt install python3-dev

Then another attempt at spidev:

$ uv add spidev

[...]

      aarch64-linux-gnu-gcc: fatal error: cannot execute ‘as’: posix_spawnp: No such file or directory
      compilation terminated.
      error: command '/usr/bin/aarch64-linux-gnu-gcc' failed with exit code 1

      hint: This usually indicates a problem with the package or the build environment.

Looks like more progress, but not success. I next tried this:

sudo apt install binutils

Now another try for spidev:

$ uv add spidev
Resolved 2 packages in 14ms
      Built spidev==3.8
Prepared 1 package in 6.12s
Installed 1 package in 4ms
 + spidev==3.8

Success!

Next it is time to try using the thing. I'll create a minimal script:

echo 'import spidev
spi = spidev.SpiDev()
spi.open_path("/dev/spidev0.0")
spi.writebytes([1])
spi.close()' \
> spidevtest.py

Now run the script:

$ uv run spidevtest.py
Traceback (most recent call last):
  File "/home/arduino/spidev-test/spidevtest.py", line 3, in <module>
    spi.open_path("/dev/spidev0.0")
    ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
PermissionError: [Errno 13] Permission denied

The problem is that the user account does not have write permission for /dev/spidev0.0. The modern way to manage these permissions is through udev rules:

The arduino user is automatically included in the gpiod user group:

$ groups
arduino adm dialout sudo audio video users netdev bluetooth docker sysupgrade render input gpiod

For the sake of simplicity, I'll use that existing group in this udev rule:

$ echo \
'# See: https://github.com/doceme/py-spidev/blob/v3.8/99-local-spi-example-udev.rules#L31
KERNEL=="spidev*", GROUP="gpiod", MODE="0660"' \
| \
  sudo \
    tee \
      "/etc/udev/rules.d/60-spi.rules" \
&& \
sudo \
  udevadm control \
    --reload-rules \
&& \
sudo \
  udevadm trigger
$ sudo groupadd spi
$ sudo usermod --append --groups spi arduino

Now run the script once again:

$ uv run spidevtest.py && echo $?

Looks like success!

I didn't go beyond that to verify communication with sketch on the microcontroller, but hopefully this will be enough to allow you to progress at least.

All the above is simply me sharing the results from my fumbling trial and error. It might well be that there is a better/correct way to go about this. Others are of course very welcome to suggest corrections or improvements on what I did.

@ptillisch
first thanks for the detailed response it helps especially on how to use uv. Was beginning to look at that but giving me a headache.

I learned about the pros and cons of using docker when I was playing with Ros2 - different thread. Have no issue with experimenting with using python directly on the Q but not sure how they are going to get it working within applab but that is another story.

Going to do what you suggested to see how it is working and will keep you all posted. But have to absorb all that you wrote and play around more with it.

Stay tuned.

EDIT:

thought gcc was already installed?

Thanks @ptillisch,
:woozy_face:

My guess is that these steps will currently only work on a Q4 (or better).

EDIT:

What guess I don't understand, is when I did:

I could then at the command prompt do: python3
And the import spidev
And it would find it...

I did not verify it could talk through it or not.

It did not appear to need GCC and the like, yes I know it is not in the container, but
neither was GCC in your instructions, until you apt install gcc...

Again thanks!

@ptillisch - @KurtE
Followed your instructions and working using the arduino sketch as posted in the PR and the following spidevtest.py script:

import spidev
import time
import sys

def init_spi(bus=0, device=0, max_speed_hz=5000000, mode=0):
    """
    Initialize and return an SPI connection.
    """
    spi = spidev.SpiDev()
    try:
        spi.open_path("/dev/spidev0.0")  # Open SPI bus and device
        spi.max_speed_hz = max_speed_hz
        spi.mode = mode  # SPI mode (0, 1, 2, or 3)
        spi.bits_per_word = 8
        print(f"SPI initialized: bus={bus}, device={device}, speed={max_speed_hz}Hz, mode={mode}")
    except FileNotFoundError:
        sys.exit("Error: SPI device not found. Enable SPI in raspi-config or check /dev/spidevX.Y")
    except PermissionError:
        sys.exit("Error: Permission denied. Try running with sudo.")
    return spi

def continuous_read(spi, read_command, bytes_to_read=512, delay=0.5):
    """
    Continuously read a fixed number of bytes from an SPI device.
    :param spi: spidev.SpiDev object
    :param read_command: list of bytes to send before reading (e.g., [0x0B, 0x00, 0x00, 0x00])
    :param bytes_to_read: number of bytes to read from the device
    :param delay: delay between reads in seconds
    """
    try:
        while True:
            # Send read command + dummy bytes to receive data
            #tx = read_command + [0x00] * bytes_to_read
            #x = spi.xfer2(tx)  # Full-duplex transfer
            data = spi.readbytes(512)
            print(f"Read {len(data)} bytes: {data[:16]} ...")  # Show first 16 bytes for brevity
            time.sleep(delay)
    except KeyboardInterrupt:
        print("\nStopping continuous read.")
    finally:
        spi.close()

if __name__ == "__main__":
    # Example: SPI bus 0, device 0
    spi = init_spi(bus=0, device=0, max_speed_hz=2000000, mode=0)

    # Example read command for a generic SPI device
    # Replace with your device's datasheet command
    # For SPI flash: 0x0B (Fast Read) + 3-byte address + 1 dummy byte
    READ_CMD = [0x0B, 0x00, 0x00, 0x00, 0x00]  # Example: start at address 0x000000

    continuous_read(spi, READ_CMD, bytes_to_read=512, delay=0.1)

output from script

Read 512 bytes: [217, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ...
Read 512 bytes: [218, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ...
Read 512 bytes: [219, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ...
Read 512 bytes: [220, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ...
Read 512 bytes: [221, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ...
Read 512 bytes: [222, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ...
Read 512 bytes: [223, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ...
Read 512 bytes: [224, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ...
Read 512 bytes: [225, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] ...

now going to try something from within applab

@ptillisch - @KurtE

Converted the samples into something a little more useful. Now have it sending 180 floats over SPI3 to the MPU at 1mhz - really fast and reliable - no issues compared to RPC.

Arduino Sketch


#include <stdint.h>
#include <stddef.h>
#include <string.h>

#include "SPIPeripheral.h"
#include "Arduino_RouterBridge.h"

#define num_vals 180

SPIPeripheralClass<1024> spi;

void floats_to_bytes(const float* src, size_t float_count, uint8_t* dst) {
    if (!src || !dst || float_count == 0) return;
    memcpy(dst, src, float_count * sizeof(float));
}

float sensorValues[num_vals];
long randNumber ;

void get_sensor_data() {
    randomSeed(analogRead(A0));

    for(uint8_t i; i < num_vals; i++) {
      randNumber  = random(300);
      sensorValues[i] = float(randNumber) /10.0;
    }
  
}
void setup() {
  Monitor.begin();
  delay(2000);
  Monitor.println("Begin SPI3 Test....");
  spi.begin();
}

void loop() {
  uint8_t bytes[num_vals * sizeof(float)];
  
  // Convert float array → bytes
  get_sensor_data();
  floats_to_bytes(sensorValues, num_vals, bytes);

  spi.populate(bytes, num_vals * sizeof(float) );
  spi.ready();

}

Python Script:

import spidev
import time
import sys
import struct

def bytes_to_float_array(b, byte_order='little'):
    if len(b) % 4 != 0:
        raise ValueError("Byte length must be a multiple of 4")

    fmt = ('<' if byte_order == 'little' else '>') + f'{len(b)//4}f'
    return list(struct.unpack(fmt, b))

def init_spi(bus=0, device=0, max_speed_hz=5000000, mode=0):
    """
    Initialize and return an SPI connection.
    """
    spi = spidev.SpiDev()
    try:
        spi.open_path("/dev/spidev0.0")  # Open SPI bus and device
        spi.max_speed_hz = max_speed_hz
        spi.mode = mode  # SPI mode (0, 1, 2, or 3)
        spi.bits_per_word = 8
        print(f"SPI initialized: bus={bus}, device={device}, speed={max_speed_hz}Hz, mode={mode}")
    except FileNotFoundError:
        sys.exit("Error: SPI device not found. Enable SPI in raspi-config or check /dev/spidevX.Y")
    except PermissionError:
        sys.exit("Error: Permission denied. Try running with sudo.")
    return spi

def continuous_read(spi, read_command, bytes_to_read=1024, delay=0.5):
    """
    Continuously read a fixed number of bytes from an SPI device.
    :param spi: spidev.SpiDev object
    :param read_command: list of bytes to send before reading (e.g., [0x0B, 0x00, 0x00, 0x00])
    :param bytes_to_read: number of bytes to read from the device
    :param delay: delay between reads in seconds
    """
    try:
        while True:
            # Send read command + dummy bytes to receive data
            #tx = read_command + [0x00] * bytes_to_read
            #x = spi.xfer2(tx)  # Full-duplex transfer
            data = spi.readbytes(1024)
            print(f"Read {len(data)} bytes: {data[:16]} ...")  # Show first 16 bytes for brevity
            raw_bytes = bytes(data[:720])

            count = len(raw_bytes) // 4
            fmt = '<' + str(count) + 'f'

            floats = struct.unpack(fmt, raw_bytes)
            print(", ".join(f"{v:.4f}" for v in floats))
            print("\n")
            time.sleep(delay)
    except KeyboardInterrupt:
        print("\nStopping continuous read.")
    finally:
        spi.close()

if __name__ == "__main__":
    # Example: SPI bus 0, device 0
    spi = init_spi(bus=0, device=0, max_speed_hz=2000000, mode=0)

    # Example read command for a generic SPI device
    # Replace with your device's datasheet command
    # For SPI flash: 0x0B (Fast Read) + 3-byte address + 1 dummy byte
    READ_CMD = [0x0B, 0x00, 0x00, 0x00, 0x00]  # Example: start at address 0x000000

    continuous_read(spi, READ_CMD, bytes_to_read=1024, delay=0.25)

OUPUT:

Read 1024 bytes: [51, 51, 195, 65, 102, 102, 218, 65, 102, 102, 182, 64, 154, 153, 201, 65] ...
24.4000, 27.3000, 5.7000, 25.2000, 3.8000, 19.5000, 11.5000, 4.0000, 11.1000, 2.1000, 24.5000, 2.4000, 23.5000, 17.1000, 21.0000, 4.6000, 18.6000, 28.0000, 12.0000, 1.3000, 21.3000, 4.3000, 0.6000, 29.6000, 28.8000, 2.6000, 23.8000, 3.2000, 1.7000, 24.3000, 15.8000, 23.6000, 20.7000, 1.6000, 23.3000, 3.6000, 18.2000, 29.8000, 18.7000, 28.0000, 26.8000, 1.0000, 27.8000, 21.8000, 0.5000, 25.7000, 17.5000, 2.9000, 27.1000, 28.2000, 23.7000, 22.1000, 5.9000, 29.4000, 22.4000, 1.6000, 28.2000, 29.0000, 20.0000, 21.2000, 25.1000, 9.9000, 17.0000, 2.8000, 5.2000, 5.2000, 14.4000, 12.8000, 27.6000, 3.0000, 19.2000, 22.0000, 19.3000, 6.8000, 0.1000, 3.8000, 23.1000, 28.0000, 8.2000, 11.7000, 22.8000, 12.6000, 6.2000, 6.3000, 12.5000, 2.0000, 25.0000, 18.2000, 15.8000, 18.5000, 27.1000, 29.1000, 16.9000, 2.5000, 2.6000, 11.7000, 3.1000, 13.9000, 14.2000, 11.7000, 6.1000, 15.4000, 18.0000, 27.6000, 15.0000, 21.4000, 2.2000, 9.6000, 24.9000, 9.7000, 13.2000, 6.6000, 12.6000, 21.1000, 15.3000, 15.0000, 27.5000, 21.1000, 13.3000, 1.5000, 17.8000, 9.8000, 18.2000, 22.3000, 16.1000, 7.9000, 4.1000, 21.1000, 10.4000, 2.0000, 12.2000, 25.7000, 13.2000, 16.4000, 0.8000, 5.9000, 6.8000, 8.2000, 13.5000, 4.8000, 20.4000, 22.0000, 3.7000, 23.0000, 7.0000, 10.7000, 3.4000, 17.0000, 16.5000, 4.1000, 20.5000, 18.3000, 17.3000, 21.9000, 20.1000, 29.3000, 27.4000, 10.8000, 27.0000, 23.3000, 3.9000, 12.4000, 18.1000, 14.9000, 2.6000, 1.4000, 10.6000, 4.6000, 18.1000, 23.6000, 21.9000, 21.2000, 3.2000, 0.7000, 25.1000, 10.3000, 15.4000, 22.7000, 21.6000, 19.6000

Oops forgot SPIPeripheral.h

#ifndef SPI_PERIPHERAL_H
#define SPI_PERIPHERAL_H

#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/init.h>
#include <zephyr/drivers/spi.h>

#ifdef CONFIG_BOARD_ARDUINO_UNO_Q

#define SPI_PERIPHERAL_NODE DT_COMPAT_GET_ANY_STATUS_OKAY(zephyr_spi_slave)

template <int SPI_MAX_MESSAGE>
class SPIPeripheralClass {
public:
    SPIPeripheralClass() {
        spi_cfg.frequency = 1000000;
        spi_cfg.operation = SPI_WORD_SET(8) | SPI_OP_MODE_SLAVE;
        rx.buf = rxmsg;
        rx.len = SPI_MAX_MESSAGE;
        rx_bufs.buffers = &rx;
        rx_bufs.count = 1;
        tx.buf = txmsg;
        tx.len = SPI_MAX_MESSAGE;
        tx_bufs.buffers = &tx;
        tx_bufs.count = 1;

    }
    int begin() {
        int ret = device_init(spi_peripheral);
        return ret;
    }

    void* populate(uint8_t* buf, size_t len) {
        return memcpy(tx.buf, buf, len);
    }

    int ready() {
        return spi_transceive(spi_peripheral, &spi_cfg, &tx_bufs, &rx_bufs);
    }
private:

    const struct device *const spi_peripheral = DEVICE_DT_GET(DT_BUS(SPI_PERIPHERAL_NODE));
    struct spi_config spi_cfg;

    uint8_t rxmsg[SPI_MAX_MESSAGE];
    struct spi_buf rx ;
    struct spi_buf_set rx_bufs;

    uint8_t txmsg[SPI_MAX_MESSAGE];
    struct spi_buf tx ;
    struct spi_buf_set tx_bufs;
};

#endif
#endif //SPI_PERIPHERAL_H

Great stuff

I used my 2 GB UNO Q model to perform that procedure.

It is true that it installs a significant quantity of stuff, so if you are already running low on storage space (regardless of which model you have), then you may indeed run out of space.

I didn't investigate the python3-spidev approach because I had gotten the impression from what you and @Merlin513 wrote that it didn't work. I don't find any information about what that package is.

I see. Unfortunately it is not available in the environment used by uv:

(this is on a fresh machine)

arduino@tobey:~/spidevtest$ sudo apt install python3-spidev

[sudo] password for arduino: 
Installing:
  python3-spidev

Summary:
  Upgrading: 0, Installing: 1, Removing: 0, Not Upgrading: 0
  Download size: 14.4 kB
  Space needed: 95.2 kB / 2,423 MB available

Get:1 http://deb.debian.org/debian trixie/main arm64 python3-spidev arm64 3.6-1+b6 [14.4 kB]
Fetched 14.4 kB in 0s (44.5 kB/s)
Selecting previously unselected package python3-spidev.
(Reading database ... 63859 files and directories currently installed.)
Preparing to unpack .../python3-spidev_3.6-1+b6_arm64.deb ...
Unpacking python3-spidev (3.6-1+b6) ...
Setting up python3-spidev (3.6-1+b6) ...
Scanning processes...
Scanning processor microcode...
Scanning linux images...

Running kernel seems to be up-to-date.

Failed to check for processor microcode upgrades.

No services need to be restarted.

No containers need to be restarted.

No user sessions are running outdated binaries.

No VM guests are running outdated hypervisor (qemu) binaries on this host.

arduino@tobey:~/spidevtest$ "import spidev" > test.py

arduino@tobey:~/spidevtest$ uv run test.py

Traceback (most recent call last):
  File "/home/arduino/spidevtest/test.py", line 1, in <module>
    import spidev
ModuleNotFoundError: No module named 'spidev'

I don't know how to make that package available when using uv. So it is a reasonable approach when you are working in the standard Linux machine if you don't care about using a virtual environment, but I don't know how it could be utilized in a solution for Arduino Apps.

Nice!

Why not using Bridge.call()/notify()/provide() functions?

I can verify that if you use in standard linux, i.e.

arduino@arduinoQ4:~$ sudo apt-get install python3-spidev
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
python3-spidev is already the newest version (3.6-1+b6).
0 upgraded, 0 newly installed, 0 to remove and 4 not upgraded.

Then if I run my test sketch - it works:

arduino@arduinoQ4:~$ python3 spidevtest2.py
SPI initialized: bus=0, device=0, speed=2000000Hz, mode=0
Read 1024 bytes: [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255] ...
nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, nan, ...

but for some reason still does not work from within the ArduinoApp environment.

For instance, if I try and run

import time
import spidev

from arduino.app_utils import App

print("Hello world!")


def loop():
    """This function is called repeatedly by the App framework."""
    # You can replace this with any code you want your App to run repeatedly.
    spi = spidev.SpiDev()
    #spi.open_path("/dev/spidev0.0")
    spi.open(0,0)
    spi.max_speed_hz = 20000000
    spi.readbytes(512)
    time.sleep(10)


# See: https://docs.arduino.cc/software/app-lab/tutorials/getting-started/#app-run
App.run(user_loop=loop)

assuming spidev is already installed, you get

import spidev
ModuleNotFoundError: No module named 'spidev'
Activating python virtual environment
Traceback (most recent call last):
File "/app/python/main.py", line 2, in <module>
import spidev
ModuleNotFoundError: No module named 'spidev'
exited with code 1

and even if I add a requirements.txt file with spidev==3.6 it tries to build 3.6 and we get the failures noted previously.

But at least we have a path forward for now

Issues I found for what I am experimenting with:

  1. Transfers are way too slow.
  2. Fails to transfer data if you run the app a couple of times and have to power cycle the Q.
  3. With SPI3 you can transfer large amounts of data.

I know I have seen other posts complaining about similar experiences with Bridge commands

This is because the Python script of the App runs in an isolated Docker container. Installing packages in the primary Linux machine of the UNO Q has absolutely no effect on the environment of the container.

Just a side question:
The Python codes are converted to machine codes as per instruction set of the QBR2210 MPU. At which site of the net, we can find the Instruction Set of the QBR2210 MPU?

Yep should be interesting get to work in Applab. Thanks for your help

@KurtE - @ptillisch

Decided to play a bit more with SPI3 and modified the SPIPeripheral class to also transmit bytes and receive data back from the MCU based on the command.

So if I tell it to send a read command on 0x0B it will transmit data back to MPU using the spidev lib xfer2 command. Note if you just want to send bytes from the MPU to the MCU use writebytes.

Here is the modified class

#ifndef SPI_PERIPHERAL_H
#define SPI_PERIPHERAL_H

#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/init.h>
#include <zephyr/drivers/spi.h>

#ifdef CONFIG_BOARD_ARDUINO_UNO_Q

#define SPI_PERIPHERAL_NODE DT_COMPAT_GET_ANY_STATUS_OKAY(zephyr_spi_slave)

template <int SPI_MAX_MESSAGE>
class SPIPeripheralClass {
public:
    SPIPeripheralClass() {
        spi_cfg.frequency = 1000000;
        spi_cfg.operation = SPI_WORD_SET(8) | SPI_OP_MODE_SLAVE;
        rx.buf = rxmsg;
        rx.len = SPI_MAX_MESSAGE;
        rx_bufs.buffers = &rx;
        rx_bufs.count = 1;
        tx.buf = txmsg;
        tx.len = SPI_MAX_MESSAGE;
        tx_bufs.buffers = &tx;
        tx_bufs.count = 1;

    }
    int begin() {
        int ret = device_init(spi_peripheral);
        return ret;
    }

    void depopulate(uint8_t &buf, size_t len) {
        spi_transceive(spi_peripheral, &spi_cfg, &tx_bufs, &rx_bufs);
        uint8_t* rx_bytes = static_cast<uint8_t*>(rx_bufs.buffers[0].buf);
        buf = rx_bytes[0];
    }

    void* populate(uint8_t* buf, size_t len) {
        return memcpy(tx.buf, buf, len);
    }

    int ready() {
        return spi_transceive(spi_peripheral, &spi_cfg, &tx_bufs, &rx_bufs);
    }
private:

    const struct device *const spi_peripheral = DEVICE_DT_GET(DT_BUS(SPI_PERIPHERAL_NODE));
    struct spi_config spi_cfg;

    uint8_t rxmsg[SPI_MAX_MESSAGE];
    struct spi_buf rx ;
    struct spi_buf_set rx_bufs;

    uint8_t txmsg[SPI_MAX_MESSAGE];
    struct spi_buf tx ;
    struct spi_buf_set tx_bufs;
};

#endif
#endif //SPI_PERIPHERAL_H

associated sketch


#include <stdint.h>
#include <stddef.h>
#include <string.h>

#include "Arduino_RouterBridge.h"
#include "SPIPeripheral.h"


#define num_vals 180

SPIPeripheralClass<1024> spi;

void floats_to_bytes(const float* src, size_t float_count, uint8_t* dst) {
    if (!src || !dst || float_count == 0) return;
    memcpy(dst, src, float_count * sizeof(float));
}

float sensorValues[num_vals];
uint8_t buffer[12];
long randNumber ;

void get_sensor_data() {
    randomSeed(analogRead(A0));

    for(uint8_t i; i < num_vals; i++) {
      randNumber  = random(300);
      sensorValues[i] = float(randNumber) /10.0;
    }
  
}
void setup() {
  Monitor.begin();
  delay(2000);
  Monitor.println("Begin SPI3 Test....");
  spi.begin();
}

void loop() {
  uint8_t bytes[num_vals * sizeof(float)];
  
  spi.depopulate(*buffer, 12);
  for(uint8_t i = 0; i < 12; i++) {
    Monitor.print(buffer[i], HEX); Monitor.print(", ");
  } Monitor.println();

  if(buffer[0] == 0x0B) {
    // Convert float array → bytes
    get_sensor_data();
    floats_to_bytes(sensorValues, num_vals, bytes);

    spi.populate(bytes, num_vals * sizeof(float) );
    spi.ready();
  }

}

and new python script:

import spidev
import time
import sys
import struct

def bytes_to_float_array(b, byte_order='little'):
    if len(b) % 4 != 0:
        raise ValueError("Byte length must be a multiple of 4")

    fmt = ('<' if byte_order == 'little' else '>') + f'{len(b)//4}f'
    return list(struct.unpack(fmt, b))

def init_spi(bus=0, device=0, max_speed_hz=5000000, mode=0):
    """
    Initialize and return an SPI connection.
    """
    spi = spidev.SpiDev()
    try:
        spi.open_path("/dev/spidev0.0")  # Open SPI bus and device
        spi.max_speed_hz = max_speed_hz
        spi.mode = mode  # SPI mode (0, 1, 2, or 3)
        spi.bits_per_word = 8
        print(f"SPI initialized: bus={bus}, device={device}, speed={max_speed_hz}Hz, mode={mode}")
    except FileNotFoundError:
        sys.exit("Error: SPI device not found. Enable SPI in raspi-config or check /dev/spidevX.Y")
    except PermissionError:
        sys.exit("Error: Permission denied. Try running with sudo.")
    return spi

def continuous_read(spi, read_command, bytes_to_read=1024, delay=0.5):
    """
    Continuously read a fixed number of bytes from an SPI device.
    :param spi: spidev.SpiDev object
    :param read_command: list of bytes to send before reading (e.g., [0x0B, 0x00, 0x00, 0x00])
    :param bytes_to_read: number of bytes to read from the device
    :param delay: delay between reads in seconds
    """
    try:
        while True:
            # Send read command + dummy bytes to receive data
            #spi.writebytes(read_command)  # Full-duplex transfer
            #data = spi.readbytes(1024)
            data = spi.xfer2(read_command+ [0x00] * bytes_to_read)
            print(f"Read {len(data)} bytes: {data[:16]} ...")  # Show first 16 bytes for brevity
            raw_bytes = bytes(data[:720])

            count = len(raw_bytes) // 4
            fmt = '<' + str(count) + 'f'

            floats = struct.unpack(fmt, raw_bytes)
            print(", ".join(f"{v:.4f}" for v in floats))
            print("\n")
            time.sleep(delay)
    except KeyboardInterrupt:
        print("\nStopping continuous read.")
    finally:
        spi.close()

if __name__ == "__main__":
    # Example: SPI bus 0, device 0
    spi = init_spi(bus=0, device=0, max_speed_hz=2000000, mode=0)

    # Example read command for a generic SPI device
    # Replace with your device's datasheet command
    # For SPI flash: 0x0B (Fast Read) + 3-byte address + 1 dummy byte
    READ_CMD = [0x0B, 0x00, 0x00, 0x00, 0x00]  # Example: start at address 0x000000

    continuous_read(spi, READ_CMD, bytes_to_read=1024, delay=0.5)

so when the MCU receive the byte (0x0B)

B, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
B, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
B, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,

it will send data back to the MPU:

Read 1029 bytes: [51, 51, 183, 65, 102, 102, 226, 65, 205, 204, 140, 65, 51, 51, 175, 65] ...
22.9000, 28.3000, 17.6000, 21.9000, 0.8000, 0.3000, 6.0000, 14.3000, 1.2000, 3.6000, 28.5000, 21.6000, 22.7000, 18.1000, 6.6000, 17.9000, 27.5000, 1.5000, 7.5000, 2.4000, 15.1000, 17.2000, 23.1000, 4.0000, 20.0000, 26.9000, 1.7000, 27.5000, 8.9000, 14.8000, 3.9000, 11.1000, 17.1000, 15.0000, 4.3000, 22.2000, 20.6000, 6.4000, 3.9000, 25.0000, 4.7000, 14.7000, 28.8000, 9.6000, 4.5000, 6.0000, 12.5000, 29.7000, 4.9000, 9.8000, 23.7000, 7.5000, 14.8000, 8.5000, 2.9000, 16.5000, 27.9000, 4.1000, 11.7000, 5.5000, 6.9000, 10.7000, 15.9000, 25.1000, 23.6000, 17.9000, 23.3000, 12.1000, 0.1000, 5.0000, 5.1000, 20.5000, 28.3000, 7.4000, 17.8000, 3.0000, 10.9000, 6.0000, 20.1000, 21.9000, 3.5000, 10.6000, 12.0000, 1.4000, 8.8000, 21.8000, 25.4000, 22.7000, 25.5000, 14.7000, 4.0000, 4.6000, 3.2000, 21.5000, 16.6000, 12.4000, 28.9000, 10.0000, 20.1000, 16.5000, 7.6000, 26.9000, 29.6000, 13.8000, 19.5000, 0.2000, 28.3000, 1.3000, 22.8000, 21.0000, 19.2000, 15.0000, 5.8000, 2.8000, 8.7000, 7.9000, 18.8000, 5.6000, 22.0000, 8.4000, 14.2000, 18.1000, 20.8000, 24.1000, 17.7000, 3.5000, 24.0000, 27.4000, 25.3000, 19.3000, 0.8000, 28.3000, 20.6000, 7.2000, 11.8000, 27.5000, 18.9000, 13.0000, 16.0000, 24.2000, 16.8000, 15.3000, 8.3000, 23.2000, 11.7000, 11.5000, 22.9000, 0.2000, 5.7000, 12.4000, 14.7000, 25.8000, 11.9000, 29.8000, 3.5000, 2.7000, 9.2000, 28.7000, 4.5000, 16.3000, 6.5000, 9.3000, 14.7000, 1.9000, 4.0000, 12.2000, 20.1000, 15.2000, 27.1000, 23.2000, 26.4000, 25.5000, 0.8000, 9.3000, 2.8000, 1.1000, 3.3000, 5.5000, 25.0000, 7.7000

Enjoy.

Again looks like fun,

Might be fun to now try to mix and match the SPI stuff with the Bridge stuff.
Like for example, suppose I wish for the STM board to logically read the contents
of an SD Card that is on the main processor, and so it maybe sends a command over
the Bridge to open/read from file X... And then transfer using SPI back to the STM board, that
maybe displays the contents on a TFT display...

Edit: would the be interesting to see how the speed compares versus simply connecting SPI SD Card adapter to the STM32 chip...

Could be fun... Also opens Log file to write to and uses SPI to then send contents...

When is Arduino Dev Team going to implement SPI3-based Router Bridge so that high speed data exchange would take place btween MPU and MCU with few commands like:?

Bridge.spi3Call()
Bridge.spi3Provide()
etc.

Interesting question:

Suppose the developers, speed up the Serial interface, probably including RTS/CTS pins, to maybe 1Mhz. Also assume that the stuff that @Merlin513 has done with @ptillisch suggestions, such that spidev is built into their packages.

Is it better to add appis, as you mentioned to do SPI transfers as part of the Bridge interface.
Or allow apps to define their own mechanisms?