diff --git a/examples/arduino/README.md b/examples/arduino/README.md index 876b4785..dc62cde8 100644 --- a/examples/arduino/README.md +++ b/examples/arduino/README.md @@ -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 @@ -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 +``` diff --git a/pytest-embedded-arduino/pytest_embedded_arduino/app.py b/pytest-embedded-arduino/pytest_embedded_arduino/app.py index f9abfa0b..bd5634fc 100644 --- a/pytest-embedded-arduino/pytest_embedded_arduino/app.py +++ b/pytest-embedded-arduino/pytest_embedded_arduino/app.py @@ -1,6 +1,6 @@ import json +import logging import os -from typing import ClassVar from pytest_embedded.app import App @@ -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 diff --git a/pytest-embedded-arduino/pytest_embedded_arduino/serial.py b/pytest-embedded-arduino/pytest_embedded_arduino/serial.py index 0402f9de..f2fbbd92 100644 --- a/pytest-embedded-arduino/pytest_embedded_arduino/serial.py +++ b/pytest-embedded-arduino/pytest_embedded_arduino/serial.py @@ -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) @@ -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: diff --git a/pytest-embedded-arduino/tests/test_arduino.py b/pytest-embedded-arduino/tests/test_arduino.py index ee47751f..98e696cb 100644 --- a/pytest-embedded-arduino/tests/test_arduino.py +++ b/pytest-embedded-arduino/tests/test_arduino.py @@ -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) @@ -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) diff --git a/pytest-embedded-wokwi/tests/test_wokwi.py b/pytest-embedded-wokwi/tests/test_wokwi.py index 39e7bcaf..20dfa75e 100644 --- a/pytest-embedded-wokwi/tests/test_wokwi.py +++ b/pytest-embedded-wokwi/tests/test_wokwi.py @@ -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( @@ -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'), ) diff --git a/tests/fixtures/hello_world_arduino/build/build.options.json b/tests/fixtures/hello_world_arduino/build/build.options.json index 0c723026..044b3c0b 100644 --- a/tests/fixtures/hello_world_arduino/build/build.options.json +++ b/tests/fixtures/hello_world_arduino/build/build.options.json @@ -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" } \ No newline at end of file diff --git a/tests/fixtures/hello_world_arduino/build/flash_args b/tests/fixtures/hello_world_arduino/build/flash_args new file mode 100644 index 00000000..a7357ada --- /dev/null +++ b/tests/fixtures/hello_world_arduino/build/flash_args @@ -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 diff --git a/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.bin b/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.bin deleted file mode 100644 index 13daeede..00000000 Binary files a/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.bin and /dev/null differ diff --git a/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.bootloader.bin b/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.bootloader.bin deleted file mode 100644 index 6f553760..00000000 Binary files a/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.bootloader.bin and /dev/null differ diff --git a/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.elf b/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.elf old mode 100644 new mode 100755 index 189830a1..bc4f7b15 Binary files a/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.elf and b/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.elf differ diff --git a/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.merged.bin b/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.merged.bin deleted file mode 120000 index 4c209aa1..00000000 --- a/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.merged.bin +++ /dev/null @@ -1 +0,0 @@ -hello_world_arduino.ino.bin \ No newline at end of file diff --git a/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.merged.bin b/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.merged.bin new file mode 100644 index 00000000..221b440d Binary files /dev/null and b/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.merged.bin differ diff --git a/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.partitions.bin b/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.partitions.bin deleted file mode 100644 index 1954582f..00000000 Binary files a/tests/fixtures/hello_world_arduino/build/hello_world_arduino.ino.partitions.bin and /dev/null differ