Skip to content
README.md 8.21 KiB
Newer Older
# **Py**thon **E**mbedded **D**evice **M**ana**g**e**r**

Discover, flash and control ARM FVPs and boards from Python.

## Example

```python
import asyncio

from pyedmgr import Host

async def main(host:Host):
    await host.establish_connections()
    for board in host.connected_devices:
        await board.channel.write_async(b'Hello, world\r\n')

if __name__ == '__main__':
    host = Host.default()
    asyncio.run(main(host))
```

### Example using PyTest integration

```python
from pyedmgr import fixture_test_case

import pytest

@pytest.mark.asyncio
@pytest.mark.parametrize(
    'fixture_test_case',
    [
        {
            'FVP_Corstone_SSE-300_Ethos-U55': 'firmware.elf'
        },
    ],
    indirect=True
)
async def test__example_test(fixture_test_case):
    async with fixture_test_case as context:
        line:bytes = context.allocated_devices[0].channel.readline_async()
        assert b'hello, world' in line
```

## Setup

### Installation

Use `setuptools` to install to user or virtual-env site-packages:

```shell
virtualenv venv     # optional
. venv/bin/activate # optional
python setup.py install
```

### Create firmware binary (optional)

```shell
tools/mkbinary.py tools/firmware.elf "arbitrary string"
```

The text "arbitrary string" will be printed by boards/FVPs flashed with the resulting `firmware.bin` and `firmware.patched.elf` files.

### Set FVP install directory

If FVP binaries are in the system `PATH`, `pyedmgr` should automatically discover them. Otherwise, the installation directory should be passed as an environment variable, e.g. `FVP_CS300_INSTALL_DIR` for the CS300 FVP(s). It can also be more specific: `FVP_CS300_U55_INSTALL_DIR` matches the Ethos-U55 variant only.

### Set Iris install directory

The `IRIS_INSTALL_DIR` should be set as Iris is used for flashing; however `pyedmgr` can automatically locate it if the FVP install directory is found/given.

## PyTest Integration

### Usage

#### FVPs

You may add, remove or change FVP parameters by placing a file called `MPS3.conf` in the working directory. For a list of available parameters, run the FVP with the `--list-params` flag.

#### Boards

Connect the board and mount its filesystem somewhere to make it flashable. If running the as a normal user, mount with `-o umask=000` or make the mount point writeable.

### Test Cases

The test infrastructure can be used by injecting the [fixture_test_case](pyedmgr/test/__init__.py) fixture into a function and entering the context of the returned [TestCase](pyedmgr/test/test_case.py):

```python
async def test_example_test(fixture_test_case:TestCase):
    async with fixture_test_case as context:
        # ...
```

In this sample, `async with` enters the `TestCase`'s context, which instantiates the needed test infrastructure ([RealDeviceRegistry](pyedmgr/registry/real.py) and [Host](pyedmgr/host.py)) and binds it to `context`, an instance of [TestCaseContext](pyedmgr/test/test_case_context.py). This gives access to the `Host` and `Registry`, and all [TestDevices](pyedmgr/test/test_device.py) that have been discovered. None of the devices will be flashed with firmware. Everything will be de-initialised and marked for garbage collection when the `async with` scope exits.

`fixture_test_case` can be parameterised to flash devices with [Firmware](pyedmgr/firmware/firmware.py):

```python
@pytest.mark.asyncio
@pytest.mark.parametrize(
    'fixture_test_case',
    [
        {
            'NRF52840_DK': {
                1: '{{FIRMWARE_DIR}}/nrf52.bin',
                2: '{{FIRMWARE_DIR}}/nrf52.bin'
            }
        },
        {
            'FVP_Corstone_SSE-300_Ethos-U55': {
                1: ['{{FIRMWARE_DIR}}/bl2.axf', ('{{FIRMWARE_DIR}}/tfm_s.bin',0x38000000), ('{{FIRMWARE_DIR}}/tfm_ns.bin',0x28060000)],
                2: ['{{FIRMWARE_DIR}}/bl2.axf', ('{{FIRMWARE_DIR}}/tfm_s.bin',0x38000000), ('{{FIRMWARE_DIR}}/tfm_ns.bin',0x28060000)]
            }
        }
    ],
    indirect=True
)
async def test__example_test(fixture_test_case:TestCase):
    async with fixture_test_case as context:
        print(len(context.allocated_devices), 'devices allocated')
        for device in context.allocated_devices:
            print(device.name, ':', device.firmware.path)

```

In the example, the test has been parameterised with two test cases:

1. In the first test case, two `nRF52840`s will be flashed with a binary `nrf52.bin` found in the directory named by the environment variable `FIRMWARE_DIR` (whatever is between `{{` and `}}` will be replaced by an environment variable). Mbed boards such as the `nRF52840` can only be flashed with a single flat binary.

2. In the second, three files are being copied to two FVPs: an ELF binary containing the `bl2` bootloader, and flat binaries containing secure and non-secure TF-M binaries. The TF-M binaries will be copied to specific memory addresses. The firmware list for FVPs may contain any number of `(file,address)` pairs with at most one firmware (an ELF binary with no explicit address). For Mbed boards, only a single firmware instance is flashed, with any `(file,address)` pairs being ignored.

The `indirect` flag is set to tell PyTest that `fixture_test_case` (the parameter) is a fixture and the values in the list are to be passed to the fixture, rather than being bound to `fixture_test_case`. The return value of the fixture is bound to the variable instead. If there are multiple fixtures you can pass a list containing
the names of the indirect ones instead of a boolean value.

Each test case will use unique instances of the infrastructural objects and thus won't conflict with one another.

#### JSON

It is possible to move test case descriptors to an external JSON file and have it loaded dynamically during testing:

##### Example 1

`test_json.json`
```json
{
    "FVP_Corstone_SSE-300_Ethos-U55": ["example-firmware.elf"]
}
```

`test_json.py`
```python
@pytest.mark.asyncio
@pytest.mark.parametrize(
    'test_case_descriptor',
    ['@test_json.json']
)
async def test__example_test(test_case_descriptor:Mapping[str,FirmwareOrFileAddressList]):
    async with TestCase(test_case_descriptor) as context:
        # ...
```

In this example, `@test_json.json` tells the test case fixture to find a file called `test_json.json` and load the test case descriptor from it. Processing then continues as normal. The JSON file may either contain a single test case or several. If it contains a single test-case it must be enclosed inside an array and can be passed to `TestCase()` manually or via `fixture_test_case`. In this example `TestCase()` is called directly so the `indirect` flag is not set.

##### Example 2

`test_json.json`
```json
[
    {
        "FVP_Corstone_SSE-300_Ethos-U55": ["example-firmware.elf"]
    }
]
```

`test_json.py`
```python
@pytest.mark.asyncio
@pytest.mark.parametrize(
    'fixture_test_case',
    '@test_json.json',
    indirect=True
)
async def test__example_test(fixture_test_case:Iterable[TestCase]):
    for case in fixture_test_case:
        async with case as context:
            # ...
```

In this example the JSON file contains the array of cases and is passed to `fixture_test_case` which returns an iterable of test cases when the JSON file contains an array. It is not possible to get PyTest to treat the items in the JSON array as separate test cases, so we have to handle this inside the test.

# License and contributions

The software is provided under the Apache-2.0 license. All contributions to software and documents are licensed by contributors under the same license model as the software/document itself (ie. inbound == outbound licensing). Open IoT SDK may reuse software already licensed under another license, provided the license is permissive in nature and compatible with Apache v2.0.

Folders containing files under different permissive license than Apache 2.0 are listed in the LICENSE file.

Please see [CONTRIBUTING.md](CONTRIBUTING.md) for more information.

## Security issues reporting

If you find any security vulnerabilities, please do not report it in the GitLab issue tracker. Instead, send an email to the security team at arm-security@arm.com stating that you may have found a security vulnerability in the Open IoT SDK.

More details can be found at [Arm Developer website](https://developer.arm.com/support/arm-security-updates/report-security-vulnerabilities).