Skip to content

Commit

Permalink
ci(esp_usb): Run esp_tinyusb test apps in CI
Browse files Browse the repository at this point in the history
    - New CI target runner esp32s2 usb_device
    - Runner setup to allow docker container an access to USB devices
  • Loading branch information
peter-marcisovsky committed Nov 15, 2024
1 parent dd9fca9 commit 626f793
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 41 deletions.
22 changes: 12 additions & 10 deletions .github/workflows/build_and_run_test_app_usb.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,26 +33,28 @@ jobs:
with:
name: usb_test_app_bin_${{ matrix.idf_ver }}
path: |
**/test_app/build_esp*/bootloader/bootloader.bin
**/test_app/build_esp*/partition_table/partition-table.bin
**/test_app/build_esp*/test_app_usb_*.bin
**/test_app/build_esp*/test_app_usb_*.elf
**/test_app/build_esp*/flasher_args.json
**/test_app*/**/build_esp*/bootloader/bootloader.bin
**/test_app*/**/build_esp*/partition_table/partition-table.bin
**/test_app*/**/build_esp*/test_app_*.bin
**/test_app*/**/build_esp*/test_app_*.elf
**/test_app*/**/build_esp*/flasher_args.json
**/test_app*/**/build_esp*/config/sdkconfig.json
if-no-files-found: error

run-target-usb-host:
name: Run USB Host TestApp on target
run-target:
name: Run USB Host and Device TestApps on target
if: ${{ github.repository_owner == 'espressif' }}
needs: build
strategy:
fail-fast: false
matrix:
idf_ver: ["release-v5.0", "release-v5.1", "release-v5.2", "release-v5.3", "latest"]
idf_target: ["esp32s2"]
runs-on: [self-hosted, linux, docker, "${{ matrix.idf_target }}", "usb_host"]
runner_tag: ["usb_host", "usb_device"]
runs-on: [self-hosted, linux, docker, "${{ matrix.idf_target }}", "${{ matrix.runner_tag }}"]
container:
image: python:3.11-bookworm
options: --privileged # Privileged mode has access to serial ports
options: --privileged --device-cgroup-rule="c 188:* rmw" --device-cgroup-rule="c 166:* rmw" --group-add plugdev
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
Expand All @@ -63,4 +65,4 @@ jobs:
PIP_EXTRA_INDEX_URL: "https://dl.espressif.com/pypi/"
run: pip install --only-binary cryptography pytest-embedded pytest-embedded-serial-esp pytest-embedded-idf pyserial pyusb
- name: Run USB Test App on target
run: pytest --embedded-services esp,idf --target=${{ matrix.idf_target }} -m usb_host --build-dir=build_${{ matrix.idf_target }}
run: pytest --embedded-services esp,idf --target=${{ matrix.idf_target }} -m ${{ matrix.runner_tag }} --build-dir=build_${{ matrix.idf_target }}
109 changes: 109 additions & 0 deletions device/esp_tinyusb/test_apps/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# CI target runner setup

To allow a Docker container, running on a CI target runner, to access USB devices connected to the CI target runner, some modifications must be made.
In our case, it's an `RPI` target runner.

The main idea comes from this response on [stackoverflow](https://stackoverflow.com/a/66427245/19840830). The same approach is also recommended in the official Docker [documentation](https://docs.docker.com/reference/cli/docker/container/run/#device-cgroup-rule)


### Following changes shall be made on a CI target runner

- [`UDEV rules`](#udev-rules)
- [`Docker tty script`](#docker-tty-script)
- [`Logging`](#logging)
- [`Running a docker container`](#running-a-docker-container)

## UDEV rules

- This UDEV rule will trigger a `docker_tty.sh` script every time a USB device is connected, disconnected, or enumerated by the host machine (CI target runner)
- Location: `/etc/udev/rules.d/99-docker-tty.rules`
- `99-docker-tty.rules` file content:

``` sh
ACTION=="add", SUBSYSTEM=="tty", RUN+="/usr/local/bin/docker_tty.sh 'added' '%E{DEVNAME}' '%M' '%m'"
ACTION=="remove", SUBSYSTEM=="tty", RUN+="/usr/local/bin/docker_tty.sh 'removed' '%E{DEVNAME}' '%M' '%m'"
```

## Docker tty script

- This `.sh` script, triggered by the UDEV rule above, will propagate USB devices to a running Docker container.
- Location: `/usr/local/bin/docker_tty.sh`
- `docker_tty.sh` file content:

``` sh
#!/usr/bin/env bash

# Log the USB event with parameters
echo "USB event: $1 $2 $3 $4" >> /tmp/docker_tty.log

# Find a running Docker container (using the first one found)
docker_name=$(docker ps --format "{{.Names}}" | head -n 1)

# Check if a container was found
if [ ! -z "$docker_name" ]; then
if [ "$1" == "added" ]; then
docker exec -u 0 "$docker_name" mknod $2 c $3 $4
docker exec -u 0 "$docker_name" chmod -R 777 $2
echo "Adding $2 to Docker container $docker_name" >> /tmp/docker_tty.log
else
docker exec -u 0 "$docker_name" rm $2
echo "Removing $2 from Docker container $docker_name" >> /tmp/docker_tty.log
fi
else
echo "No running Docker containers found." >> /tmp/docker_tty.log
fi
```

### Making the script executable

Don't forget to make the created script executable:
``` sh
root@~$ chmod +x /usr/local/bin/docker_tty.sh
```

## Logging

- The `docker_tty.sh` script logs information about the USB devices it processes.
- Location: `/tmp/docker_tty.log`
- Example of a log from the `docker_tty.log` file, showing a flow of the `pytest_usb_device.py` test

```
USB event: added /dev/ttyACM0 166 0
USB event: added /dev/ttyACM1 166 1
Adding /dev/ttyACM0 to Docker container d5e5c774174b435b8befea864f8fcb7f_python311bookworm_6a975d
Adding /dev/ttyACM1 to Docker container d5e5c774174b435b8befea864f8fcb7f_python311bookworm_6a975d
USB event: removed /dev/ttyACM0 166 0
USB event: removed /dev/ttyACM1 166 1
```

## Running a docker container

### Check Major and Minor numbers of connected devices

Check the Major and Minor numbers assigned by the Linux kernel to devices that you want the Docker container to access.
In our case, we want to access `/dev/ttyUSB0`, `/dev/ttyACM0` and `/dev/ttyACM1`

`/dev/ttyUSB0`: Major 188, Minor 0
``` sh
peter@BrnoRPIG007:~ $ ls -l /dev/ttyUSB0
crw-rw-rw- 1 root dialout 188, 0 Nov 12 11:08 /dev/ttyUSB0
```

`/dev/ttyACM0` and `/dev/ttyACM1`: Major 166, Minor 0 (1)
``` sh
peter@BrnoRPIG007:~ $ ls -l /dev/ttyACM0
crw-rw---- 1 root dialout 166, 0 Nov 13 10:26 /dev/ttyACM0
peter@BrnoRPIG007:~ $ ls -l /dev/ttyACM1
crw-rw---- 1 root dialout 166, 1 Nov 13 10:26 /dev/ttyACM1
```

### Run a docker container

Run a Docker container with the following extra options:
``` sh
docker run --device-cgroup-rule='c 188:* rmw' --device-cgroup-rule='c 166:* rmw' --group-add plugdev --privileged ..
```
- `--device-cgroup-rule='c 188:* rmw'`: allow access to `ttyUSBx` (Major 188, all Minors)
- `--device-cgroup-rule='c 166:* rmw'`: allow access to `ttyACMx` (Major 166, all Minors)
- `--group-add plugdev`: add the plugdev group

68 changes: 39 additions & 29 deletions device/esp_tinyusb/test_apps/cdc_and_usb_device/pytest_cdc.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pytest
from pytest_embedded_idf.dut import IdfDut
from time import sleep
from serial import Serial
from serial import Serial, SerialException
from serial.tools.list_ports import comports


Expand Down Expand Up @@ -32,43 +32,53 @@ def test_usb_device_cdc(dut) -> None:
# Find devices with Espressif TinyUSB VID/PID
s = []
ports = comports()

for port, _, hwid in ports:
if '303A:4002' in hwid:
s.append(port)

if len(s) != 2:
raise Exception('TinyUSB COM port not found')

with Serial(s[0]) as cdc0:
with Serial(s[1]) as cdc1:
# Write dummy string and check for echo
cdc0.write('text\r\n'.encode())
res = cdc0.readline()
assert b'text' in res
if b'novfs' in res:
novfs_cdc = cdc0
vfs_cdc = cdc1
try:
with Serial(s[0]) as cdc0:
with Serial(s[1]) as cdc1:
# Write dummy string and check for echo
cdc0.write('text\r\n'.encode())
res = cdc0.readline()
assert b'text' in res
if b'novfs' in res:
novfs_cdc = cdc0
vfs_cdc = cdc1

cdc1.write('text\r\n'.encode())
res = cdc1.readline()
assert b'text' in res
if b'novfs' in res:
novfs_cdc = cdc1
vfs_cdc = cdc0

# Write more than MPS, check that the transfer is not divided
novfs_cdc.write(bytes(100))
dut.expect_exact("Intf 0, RX 100 bytes")

cdc1.write('text\r\n'.encode())
res = cdc1.readline()
assert b'text' in res
if b'novfs' in res:
novfs_cdc = cdc1
vfs_cdc = cdc0
# Write more than RX buffer, check correct reception
novfs_cdc.write(bytes(600))
transfer_len1 = int(dut.expect(r'Intf 0, RX (\d+) bytes')[1].decode())
transfer_len2 = int(dut.expect(r'Intf 0, RX (\d+) bytes')[1].decode())
assert transfer_len1 + transfer_len2 == 600

# Write more than MPS, check that the transfer is not divided
novfs_cdc.write(bytes(100))
dut.expect_exact("Intf 0, RX 100 bytes")
# The VFS is setup for CRLF RX and LF TX
vfs_cdc.write('text\r\n'.encode())
res = vfs_cdc.readline()
assert b'text\n' in res

# Write more than RX buffer, check correct reception
novfs_cdc.write(bytes(600))
transfer_len1 = int(dut.expect(r'Intf 0, RX (\d+) bytes')[1].decode())
transfer_len2 = int(dut.expect(r'Intf 0, RX (\d+) bytes')[1].decode())
assert transfer_len1 + transfer_len2 == 600
return

# The VFS is setup for CRLF RX and LF TX
vfs_cdc.write('text\r\n'.encode())
res = vfs_cdc.readline()
assert b'text\n' in res
except SerialException as e:
print(f"SerialException occurred: {e}")
raise

return
except Exception as e:
print(f"An unexpected error occurred: {e}")
raise
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
@pytest.mark.esp32s2
@pytest.mark.esp32s3
@pytest.mark.esp32p4
@pytest.mark.usb_device
#@pytest.mark.usb_device Disable in CI: missing tinyusb teardown
def test_usb_device_esp_tinyusb(dut: IdfDut) -> None:
dut.run_all_single_board_cases(group='usb_device')
2 changes: 1 addition & 1 deletion device/esp_tinyusb/test_apps/vendor/pytest_vendor.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def ep_write(buf):
@pytest.mark.esp32s2
@pytest.mark.esp32s3
@pytest.mark.esp32p4
@pytest.mark.usb_device
#@pytest.mark.usb_device Disable in CI, for now, not possible to run this test in Docker container
def test_usb_device_vendor(dut: IdfDut) -> None:
'''
Running the test locally:
Expand Down

0 comments on commit 626f793

Please sign in to comment.