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 13, 2024
1 parent dd9fca9 commit fa22798
Show file tree
Hide file tree
Showing 5 changed files with 160 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 }}
107 changes: 107 additions & 0 deletions device/esp_tinyusb/test_apps/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# CI target runner setup

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

The main idea, comes from this reply 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, or disconnected from a host machine (CI target runner). Also, when a new USB device is enumerated by the host machine.
- Location: `/etc/udev/rules.d/99-docker-tty.rules`
- `99-docker-tty.rules` file content:

```
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 a USB devices to a running docker container
- Location: `/usr/local/bin/docker_tty.sh`
- `docker_tty.sh` file content:

```
#!/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
```
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`
- An example of log from the `docker_tty.log` file showing a flow of `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 which minor and major numbers Linux kernel assigns to devices, we want a docker container to have an access to.
In our case we want to access `/dev/ttyUSB0`, `/dev/ttyACM0` and `/dev/ttyACM1`

`/dev/ttyUSB0`: Major 188, Minor 0
```
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)
```
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 following extra options:
```
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 Minor)
- `--device-cgroup-rule='c 166:* rmw'`: allow access to `ttyACMx` (Major 166, all Minor)
- `--group-add plugdev`: add 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 fa22798

Please sign in to comment.