Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions examples/arduino/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ adapt the build folder appropriately when run from a different location.
On success, this will create a `build` directory under the `hello_world`
example.

The Arduino service requires only the build directory to work properly.
The app path is not required but can be used to derive the build directory. If not specified, it will be set to the current working directory.

The build directory is the directory that contains the binary and configuration files.
It can be specified as an absolute path or a relative path to the app path.
If nothing is specified, it will look for the `build` directory in the app path. If it still doesn't find it, it will assume the build directory is the app path.

### Run the tests

```shell
Expand All @@ -33,3 +40,9 @@ $ pytest examples/arduino -k test_hello_arduino

This will parse the `build` directory created earlier, flash the chip and
expect the `Hello Arduino!` text to be printed.

You can run the tests specifiying the build directory used to build the example:

```shell
$ pytest --build-dir build examples/arduino -k test_hello_arduino
```
94 changes: 54 additions & 40 deletions pytest-embedded-arduino/pytest_embedded_arduino/app.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
import logging
import os
from typing import ClassVar

from pytest_embedded.app import App

Expand All @@ -13,60 +13,74 @@ class ArduinoApp(App):
sketch (str): Sketch name.
fqbn (str): Fully Qualified Board Name.
target (str) : ESPxx chip.
flash_files (List[Tuple[int, str, str]]): List of (offset, file path, encrypted) of files need to be flashed in.
flash_settings (dict[str, str]): Flash settings for the target.
binary_file (str): Merged binary file path.
elf_file (str): ELF file path.
"""

#: dict of flash settings
flash_settings: ClassVar[dict[str, dict[str, str]]] = {
'esp32': {'flash-mode': 'dio', 'flash-size': 'detect', 'flash-freq': '80m'},
'esp32c2': {'flash-mode': 'dio', 'flash-size': 'detect', 'flash-freq': '60m'},
'esp32c3': {'flash-mode': 'dio', 'flash-size': 'detect', 'flash-freq': '80m'},
'esp32c5': {'flash-mode': 'dio', 'flash-size': 'detect', 'flash-freq': '80m'},
'esp32c6': {'flash-mode': 'dio', 'flash-size': 'detect', 'flash-freq': '80m'},
'esp32c61': {'flash-mode': 'dio', 'flash-size': 'detect', 'flash-freq': '80m'},
'esp32h2': {'flash-mode': 'dio', 'flash-size': 'detect', 'flash-freq': '48m'},
'esp32p4': {'flash-mode': 'dio', 'flash-size': 'detect', 'flash-freq': '80m'},
'esp32s2': {'flash-mode': 'dio', 'flash-size': 'detect', 'flash-freq': '80m'},
'esp32s3': {'flash-mode': 'dio', 'flash-size': 'detect', 'flash-freq': '80m'},
}

#: dict of binaries' offset.
binary_offsets: ClassVar[dict[str, list[int]]] = {
'esp32': [0x1000, 0x8000, 0x10000],
'esp32c2': [0x0, 0x8000, 0x10000],
'esp32c3': [0x0, 0x8000, 0x10000],
'esp32c5': [0x2000, 0x8000, 0x10000],
'esp32c6': [0x0, 0x8000, 0x10000],
'esp32c61': [0x0, 0x8000, 0x10000],
'esp32h2': [0x0, 0x8000, 0x10000],
'esp32p4': [0x2000, 0x8000, 0x10000],
'esp32s2': [0x1000, 0x8000, 0x10000],
'esp32s3': [0x0, 0x8000, 0x10000],
}

def __init__(
self,
**kwargs,
):
super().__init__(**kwargs)

self.sketch = os.path.basename(self.app_path)
# If no valid binary path is found, assume the build directory is the app path
if not self.binary_path and self.app_path:
self.binary_path = self.app_path

# Extract sketch name from binary files in the build directory
self.sketch = self._get_sketch_name(self.binary_path)
self.fqbn = self._get_fqbn(self.binary_path)
self.target = self.fqbn.split(':')[2]
self.flash_files = self._get_bin_files(self.binary_path, self.sketch, self.target)
self.flash_settings = self._get_flash_settings()
self.binary_file = os.path.realpath(os.path.join(self.binary_path, self.sketch + '.ino.merged.bin'))
self.elf_file = os.path.realpath(os.path.join(self.binary_path, self.sketch + '.ino.elf'))

def _get_fqbn(self, build_path) -> str:
logging.debug(f'Sketch name: {self.sketch}')
logging.debug(f'FQBN: {self.fqbn}')
logging.debug(f'Target: {self.target}')
logging.debug(f'Flash settings: {self.flash_settings}')
logging.debug(f'Binary file: {self.binary_file}')
logging.debug(f'ELF file: {self.elf_file}')

def _get_sketch_name(self, build_path: str) -> str:
"""Extract sketch name from binary files in the build directory."""
if not build_path or not os.path.isdir(build_path):
logging.warning('No build path found. Using default sketch name "sketch".')
return 'sketch'

# Look for .ino.bin or .ino.merged.bin files
for filename in os.listdir(build_path):
if filename.endswith('.ino.bin') or filename.endswith('.ino.merged.bin'):
# Extract sketch name (everything before .ino.bin or .ino.merged.bin)
if filename.endswith('.ino.merged.bin'):
return filename[: -len('.ino.merged.bin')]
else:
return filename[: -len('.ino.bin')]

# If no .ino.bin or .ino.merged.bin files found, raise an error
raise ValueError(f'No .ino.bin or .ino.merged.bin file found in {build_path}')

def _get_fqbn(self, build_path: str) -> str:
"""Get FQBN from build.options.json file."""
options_file = os.path.realpath(os.path.join(build_path, 'build.options.json'))
with open(options_file) as f:
options = json.load(f)
fqbn = options['fqbn']
return fqbn

def _get_bin_files(self, build_path, sketch, target) -> list[tuple[int, str, bool]]:
bootloader = os.path.realpath(os.path.join(build_path, sketch + '.ino.bootloader.bin'))
partitions = os.path.realpath(os.path.join(build_path, sketch + '.ino.partitions.bin'))
app = os.path.realpath(os.path.join(build_path, sketch + '.ino.bin'))
files = [bootloader, partitions, app]
offsets = self.binary_offsets[target]
return [(offsets[i], files[i], False) for i in range(3)]
def _get_flash_settings(self) -> dict[str, str]:
"""Get flash settings from flash_args file."""
flash_args_file = os.path.realpath(os.path.join(self.binary_path, 'flash_args'))
with open(flash_args_file) as f:
flash_args = f.readline().split(' ')

flash_settings = {}
for i, arg in enumerate(flash_args):
if arg.startswith('--'):
flash_settings[arg[2:].strip()] = flash_args[i + 1].strip()

if flash_settings == {}:
raise ValueError(f'Flash settings not found in {flash_args_file}')

return flash_settings
16 changes: 9 additions & 7 deletions pytest-embedded-arduino/pytest_embedded_arduino/serial.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,9 @@ def flash(self) -> None:
"""
Flash the binary files to the board.
"""
flash_files = []
for offset, path, encrypted in self.app.flash_files:
if encrypted:
continue
flash_files.extend((str(offset), path))

flash_settings = []
for k, v in self.app.flash_settings[self.app.target].items():
for k, v in self.app.flash_settings.items():
flash_settings.append(f'--{k}')
flash_settings.append(v)

Expand All @@ -55,7 +50,14 @@ def flash(self) -> None:

try:
esptool.main(
['--chip', self.app.target, 'write-flash', *flash_files, *flash_settings],
[
'--chip',
self.app.target,
'write-flash',
'0x0', # Merged binary is flashed at offset 0
self.app.binary_file,
*flash_settings,
],
esp=self.esp,
)
except Exception:
Expand Down
29 changes: 23 additions & 6 deletions pytest-embedded-arduino/tests/test_arduino.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,33 @@


def test_arduino_serial_flash(testdir):
testdir.makepyfile("""
bin_path = os.path.join(testdir.tmpdir, 'hello_world_arduino', 'build', 'hello_world_arduino.ino.merged.bin')

testdir.makepyfile(f"""
import pexpect
import pytest

def test_arduino_app(app, dut):
assert len(app.flash_files) == 3
expected_bin = '{bin_path}'
assert app.binary_file == expected_bin
assert app.target == 'esp32'
assert app.fqbn == 'espressif:esp32:esp32:PSRAM=enabled,PartitionScheme=huge_app'
expected_fqbn = (
"espressif:esp32:esp32:"
"UploadSpeed=921600,"
"CPUFreq=240,"
"FlashFreq=80,"
"FlashMode=qio,"
"FlashSize=4M,"
"PartitionScheme=huge_app,"
"DebugLevel=none,"
"PSRAM=enabled,"
"LoopCore=1,"
"EventsCore=1,"
"EraseFlash=none,"
"JTAGAdapter=default,"
"ZigbeeMode=default"
)
assert app.fqbn == expected_fqbn
dut.expect('Hello Arduino!')
with pytest.raises(pexpect.TIMEOUT):
dut.expect('foo bar not found', timeout=1)
Expand All @@ -19,10 +38,8 @@ def test_arduino_app(app, dut):
'-s',
'--embedded-services',
'arduino,esp',
'--app-path',
os.path.join(testdir.tmpdir, 'hello_world_arduino'),
'--build-dir',
'build',
os.path.join(testdir.tmpdir, 'hello_world_arduino', 'build'),
)

result.assert_outcomes(passed=1)
8 changes: 4 additions & 4 deletions pytest-embedded-wokwi/tests/test_wokwi.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def test_pexpect_by_wokwi(dut):
dut.expect('Hello world!')
dut.expect('Restarting')
with pytest.raises(pexpect.TIMEOUT):
dut.expect('foo bar not found', timeout=1)
dut.expect('Hello world! or Restarting not found', timeout=1)
""")

result = testdir.runpytest(
Expand All @@ -40,15 +40,15 @@ def test_pexpect_by_wokwi_esp32_arduino(testdir):
def test_pexpect_by_wokwi(dut):
dut.expect('Hello Arduino!')
with pytest.raises(pexpect.TIMEOUT):
dut.expect('foo bar not found', timeout=1)
dut.expect('Hello Arduino! not found', timeout=1)
""")

result = testdir.runpytest(
'-s',
'--embedded-services',
'arduino,wokwi',
'--app-path',
os.path.join(testdir.tmpdir, 'hello_world_arduino'),
'--build-dir',
os.path.join(testdir.tmpdir, 'hello_world_arduino', 'build'),
'--wokwi-diagram',
os.path.join(testdir.tmpdir, 'hello_world_arduino/esp32.diagram.json'),
)
Expand Down
14 changes: 6 additions & 8 deletions tests/fixtures/hello_world_arduino/build/build.options.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
{
"additionalFiles": "",
"builtInLibrariesFolders": "",
"builtInToolsFolders": "/Applications/Arduino.app/Contents/Java/tools-builder",
"builtInLibrariesFolders": "/Users/lucassvaz/Library/Arduino15/libraries",
"compiler.optimization_flags": "-Os",
"customBuildProperties": "",
"fqbn": "espressif:esp32:esp32:PSRAM=enabled,PartitionScheme=huge_app",
"hardwareFolders": "/Users/prochy/Documents/Arduino/hardware",
"otherLibrariesFolders": "/Users/prochy/Documents/Arduino/libraries",
"runtime.ide.version": "10810",
"sketchLocation": "/Users/prochy/Documents/Arduino/hardware/espressif/esp32/tests/hello_world/hello_world.ino"
"customBuildProperties": "build.warn_data_percentage=75",
"fqbn": "espressif:esp32:esp32:UploadSpeed=921600,CPUFreq=240,FlashFreq=80,FlashMode=qio,FlashSize=4M,PartitionScheme=huge_app,DebugLevel=none,PSRAM=enabled,LoopCore=1,EventsCore=1,EraseFlash=none,JTAGAdapter=default,ZigbeeMode=default",
"hardwareFolders": "/Users/lucassvaz/Library/Arduino15/packages,/Users/lucassvaz/Espressif/Arduino/hardware",
"otherLibrariesFolders": "/Users/lucassvaz/Espressif/Arduino/libraries",
"sketchLocation": "/Users/lucassvaz/Espressif/Arduino/sketches/hello_world_arduino"
}
5 changes: 5 additions & 0 deletions tests/fixtures/hello_world_arduino/build/flash_args
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
--flash-mode dio --flash-freq 80m --flash-size 4MB
0x1000 hello_world_arduino.ino.bootloader.bin
0x8000 hello_world_arduino.ino.partitions.bin
0xe000 boot_app0.bin
0x10000 hello_world_arduino.ino.bin
Binary file not shown.
Binary file not shown.
Binary file modified tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.elf
100644 → 100755
Binary file not shown.

This file was deleted.

Binary file not shown.
Binary file not shown.
Loading