Skip to content

Commit fcf7919

Browse files
authored
Merge pull request #393 from lucasssvaz/feat/arduino_app
change(arduino): Rework arduino app
2 parents 6ff8c06 + 70b3581 commit fcf7919

File tree

13 files changed

+135
-64
lines changed

13 files changed

+135
-64
lines changed

examples/arduino/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ adapt the build folder appropriately when run from a different location.
1818
On success, this will create a `build` directory under the `hello_world`
1919
example.
2020

21+
The Arduino service requires only the build directory to work properly.
22+
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.
23+
24+
The build directory is the directory that contains the binary and configuration files.
25+
It can be specified as an absolute path or a relative path to the app path.
26+
If nothing is specified, it will use the app path instead.
27+
It will recursively search for the binary file in the build directory and its subdirectories. If no binary file is found, it will raise an error.
28+
2129
### Run the tests
2230

2331
```shell
@@ -33,3 +41,9 @@ $ pytest examples/arduino -k test_hello_arduino
3341

3442
This will parse the `build` directory created earlier, flash the chip and
3543
expect the `Hello Arduino!` text to be printed.
44+
45+
You can run the tests specifiying the build directory used to build the example:
46+
47+
```shell
48+
$ pytest --build-dir build examples/arduino -k test_hello_arduino
49+
```
Lines changed: 75 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import json
2+
import logging
23
import os
3-
from typing import ClassVar
44

55
from pytest_embedded.app import App
66

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

19-
#: dict of flash settings
20-
flash_settings: ClassVar[dict[str, dict[str, str]]] = {
21-
'esp32': {'flash-mode': 'dio', 'flash-size': 'detect', 'flash-freq': '80m'},
22-
'esp32c2': {'flash-mode': 'dio', 'flash-size': 'detect', 'flash-freq': '60m'},
23-
'esp32c3': {'flash-mode': 'dio', 'flash-size': 'detect', 'flash-freq': '80m'},
24-
'esp32c5': {'flash-mode': 'dio', 'flash-size': 'detect', 'flash-freq': '80m'},
25-
'esp32c6': {'flash-mode': 'dio', 'flash-size': 'detect', 'flash-freq': '80m'},
26-
'esp32c61': {'flash-mode': 'dio', 'flash-size': 'detect', 'flash-freq': '80m'},
27-
'esp32h2': {'flash-mode': 'dio', 'flash-size': 'detect', 'flash-freq': '48m'},
28-
'esp32p4': {'flash-mode': 'dio', 'flash-size': 'detect', 'flash-freq': '80m'},
29-
'esp32s2': {'flash-mode': 'dio', 'flash-size': 'detect', 'flash-freq': '80m'},
30-
'esp32s3': {'flash-mode': 'dio', 'flash-size': 'detect', 'flash-freq': '80m'},
31-
}
32-
33-
#: dict of binaries' offset.
34-
binary_offsets: ClassVar[dict[str, list[int]]] = {
35-
'esp32': [0x1000, 0x8000, 0x10000],
36-
'esp32c2': [0x0, 0x8000, 0x10000],
37-
'esp32c3': [0x0, 0x8000, 0x10000],
38-
'esp32c5': [0x2000, 0x8000, 0x10000],
39-
'esp32c6': [0x0, 0x8000, 0x10000],
40-
'esp32c61': [0x0, 0x8000, 0x10000],
41-
'esp32h2': [0x0, 0x8000, 0x10000],
42-
'esp32p4': [0x2000, 0x8000, 0x10000],
43-
'esp32s2': [0x1000, 0x8000, 0x10000],
44-
'esp32s3': [0x0, 0x8000, 0x10000],
45-
}
46-
4721
def __init__(
4822
self,
4923
**kwargs,
5024
):
5125
super().__init__(**kwargs)
5226

53-
self.sketch = os.path.basename(self.app_path)
27+
# Extract information from binary files in the build directory
28+
self.binary_path = self._get_build_path()
29+
self.sketch = self._get_sketch_name(self.binary_path)
5430
self.fqbn = self._get_fqbn(self.binary_path)
5531
self.target = self.fqbn.split(':')[2]
56-
self.flash_files = self._get_bin_files(self.binary_path, self.sketch, self.target)
32+
self.flash_settings = self._get_flash_settings()
33+
self.binary_file = os.path.realpath(os.path.join(self.binary_path, self.sketch + '.ino.merged.bin'))
5734
self.elf_file = os.path.realpath(os.path.join(self.binary_path, self.sketch + '.ino.elf'))
5835

59-
def _get_fqbn(self, build_path) -> str:
36+
logging.debug(f'Build path: {self.binary_path}')
37+
logging.debug(f'Sketch name: {self.sketch}')
38+
logging.debug(f'FQBN: {self.fqbn}')
39+
logging.debug(f'Target: {self.target}')
40+
logging.debug(f'Flash settings: {self.flash_settings}')
41+
logging.debug(f'Binary file: {self.binary_file}')
42+
logging.debug(f'ELF file: {self.elf_file}')
43+
44+
def _get_build_path(self) -> str:
45+
"""Infer build path from binary path or app path."""
46+
47+
# Prioritize binary path over app path
48+
build_path = self.binary_path or self.app_path
49+
50+
if not build_path:
51+
raise ValueError('No binary path or app path provided.')
52+
53+
if os.path.isdir(build_path):
54+
# If build path is a directory, we need to check if it contains a .ino.bin or .ino.merged.bin file
55+
# If not we need to recursively check the subdirectories
56+
for root, dirs, files in os.walk(build_path):
57+
for filename in files:
58+
if filename.endswith('.ino.bin') or filename.endswith('.ino.merged.bin'):
59+
return root
60+
raise ValueError(f'Could not find a valid binary file in {build_path} or its subdirectories.')
61+
elif os.path.isfile(build_path) and (build_path.endswith('.ino.merged.bin') or build_path.endswith('.ino.bin')):
62+
# If build path is a recognized binary file, use the directory of the file
63+
return os.path.dirname(build_path)
64+
else:
65+
raise ValueError(f'Path {build_path} is not a directory or valid binary file.')
66+
67+
def _get_sketch_name(self, build_path: str) -> str:
68+
"""Extract sketch name from binary files in the build directory."""
69+
if not build_path or not os.path.isdir(build_path):
70+
logging.warning('No build path found. Using default sketch name "sketch".')
71+
return 'sketch'
72+
73+
# Look for .ino.bin or .ino.merged.bin files
74+
for filename in os.listdir(build_path):
75+
if filename.endswith('.ino.bin') or filename.endswith('.ino.merged.bin'):
76+
# Extract sketch name (everything before .ino.bin or .ino.merged.bin)
77+
if filename.endswith('.ino.merged.bin'):
78+
return filename[: -len('.ino.merged.bin')]
79+
else:
80+
return filename[: -len('.ino.bin')]
81+
82+
# If no .ino.bin or .ino.merged.bin files found, raise an error
83+
raise ValueError(f'No .ino.bin or .ino.merged.bin file found in {build_path}')
84+
85+
def _get_fqbn(self, build_path: str) -> str:
86+
"""Get FQBN from build.options.json file."""
6087
options_file = os.path.realpath(os.path.join(build_path, 'build.options.json'))
6188
with open(options_file) as f:
6289
options = json.load(f)
6390
fqbn = options['fqbn']
6491
return fqbn
6592

66-
def _get_bin_files(self, build_path, sketch, target) -> list[tuple[int, str, bool]]:
67-
bootloader = os.path.realpath(os.path.join(build_path, sketch + '.ino.bootloader.bin'))
68-
partitions = os.path.realpath(os.path.join(build_path, sketch + '.ino.partitions.bin'))
69-
app = os.path.realpath(os.path.join(build_path, sketch + '.ino.bin'))
70-
files = [bootloader, partitions, app]
71-
offsets = self.binary_offsets[target]
72-
return [(offsets[i], files[i], False) for i in range(3)]
93+
def _get_flash_settings(self) -> dict[str, str]:
94+
"""Get flash settings from flash_args file."""
95+
flash_args_file = os.path.realpath(os.path.join(self.binary_path, 'flash_args'))
96+
with open(flash_args_file) as f:
97+
flash_args = f.readline().split(' ')
98+
99+
flash_settings = {}
100+
for i, arg in enumerate(flash_args):
101+
if arg.startswith('--'):
102+
flash_settings[arg[2:].strip()] = flash_args[i + 1].strip()
103+
104+
if flash_settings == {}:
105+
raise ValueError(f'Flash settings not found in {flash_args_file}')
106+
107+
return flash_settings

pytest-embedded-arduino/pytest_embedded_arduino/serial.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,9 @@ def flash(self) -> None:
3939
"""
4040
Flash the binary files to the board.
4141
"""
42-
flash_files = []
43-
for offset, path, encrypted in self.app.flash_files:
44-
if encrypted:
45-
continue
46-
flash_files.extend((str(offset), path))
4742

4843
flash_settings = []
49-
for k, v in self.app.flash_settings[self.app.target].items():
44+
for k, v in self.app.flash_settings.items():
5045
flash_settings.append(f'--{k}')
5146
flash_settings.append(v)
5247

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

5651
try:
5752
esptool.main(
58-
['--chip', self.app.target, 'write-flash', *flash_files, *flash_settings],
53+
[
54+
'--chip',
55+
self.app.target,
56+
'write-flash',
57+
'0x0', # Merged binary is flashed at offset 0
58+
self.app.binary_file,
59+
*flash_settings,
60+
],
5961
esp=self.esp,
6062
)
6163
except Exception:

pytest-embedded-arduino/tests/test_arduino.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,33 @@
22

33

44
def test_arduino_serial_flash(testdir):
5-
testdir.makepyfile("""
5+
bin_path = os.path.join(testdir.tmpdir, 'hello_world_arduino', 'build', 'hello_world_arduino.ino.merged.bin')
6+
7+
testdir.makepyfile(f"""
68
import pexpect
79
import pytest
810
911
def test_arduino_app(app, dut):
10-
assert len(app.flash_files) == 3
12+
expected_bin = '{bin_path}'
13+
assert app.binary_file == expected_bin
1114
assert app.target == 'esp32'
12-
assert app.fqbn == 'espressif:esp32:esp32:PSRAM=enabled,PartitionScheme=huge_app'
15+
expected_fqbn = (
16+
"espressif:esp32:esp32:"
17+
"UploadSpeed=921600,"
18+
"CPUFreq=240,"
19+
"FlashFreq=80,"
20+
"FlashMode=qio,"
21+
"FlashSize=4M,"
22+
"PartitionScheme=huge_app,"
23+
"DebugLevel=none,"
24+
"PSRAM=enabled,"
25+
"LoopCore=1,"
26+
"EventsCore=1,"
27+
"EraseFlash=none,"
28+
"JTAGAdapter=default,"
29+
"ZigbeeMode=default"
30+
)
31+
assert app.fqbn == expected_fqbn
1332
dut.expect('Hello Arduino!')
1433
with pytest.raises(pexpect.TIMEOUT):
1534
dut.expect('foo bar not found', timeout=1)
@@ -19,10 +38,8 @@ def test_arduino_app(app, dut):
1938
'-s',
2039
'--embedded-services',
2140
'arduino,esp',
22-
'--app-path',
23-
os.path.join(testdir.tmpdir, 'hello_world_arduino'),
2441
'--build-dir',
25-
'build',
42+
os.path.join(testdir.tmpdir, 'hello_world_arduino', 'build'),
2643
)
2744

2845
result.assert_outcomes(passed=1)

pytest-embedded-wokwi/pytest_embedded_wokwi/arduino.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ class ArduinoFirmwareResolver:
1212

1313
def resolve_firmware(self, app: 'ArduinoApp'):
1414
# get path of ino.bin file
15-
return Path(app.binary_path, app.sketch + '.ino.merged.bin')
15+
return Path(app.binary_file)

pytest-embedded-wokwi/tests/test_wokwi.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def test_pexpect_by_wokwi(dut):
1818
dut.expect('Hello world!')
1919
dut.expect('Restarting')
2020
with pytest.raises(pexpect.TIMEOUT):
21-
dut.expect('foo bar not found', timeout=1)
21+
dut.expect('Hello world! or Restarting not found', timeout=1)
2222
""")
2323

2424
result = testdir.runpytest(
@@ -40,7 +40,7 @@ def test_pexpect_by_wokwi_esp32_arduino(testdir):
4040
def test_pexpect_by_wokwi(dut):
4141
dut.expect('Hello Arduino!')
4242
with pytest.raises(pexpect.TIMEOUT):
43-
dut.expect('foo bar not found', timeout=1)
43+
dut.expect('Hello Arduino! not found', timeout=1)
4444
""")
4545

4646
result = testdir.runpytest(
Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
{
22
"additionalFiles": "",
3-
"builtInLibrariesFolders": "",
4-
"builtInToolsFolders": "/Applications/Arduino.app/Contents/Java/tools-builder",
3+
"builtInLibrariesFolders": "/Users/lucassvaz/Library/Arduino15/libraries",
54
"compiler.optimization_flags": "-Os",
6-
"customBuildProperties": "",
7-
"fqbn": "espressif:esp32:esp32:PSRAM=enabled,PartitionScheme=huge_app",
8-
"hardwareFolders": "/Users/prochy/Documents/Arduino/hardware",
9-
"otherLibrariesFolders": "/Users/prochy/Documents/Arduino/libraries",
10-
"runtime.ide.version": "10810",
11-
"sketchLocation": "/Users/prochy/Documents/Arduino/hardware/espressif/esp32/tests/hello_world/hello_world.ino"
5+
"customBuildProperties": "build.warn_data_percentage=75",
6+
"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",
7+
"hardwareFolders": "/Users/lucassvaz/Library/Arduino15/packages,/Users/lucassvaz/Espressif/Arduino/hardware",
8+
"otherLibrariesFolders": "/Users/lucassvaz/Espressif/Arduino/libraries",
9+
"sketchLocation": "/Users/lucassvaz/Espressif/Arduino/sketches/hello_world_arduino"
1210
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
--flash-mode dio --flash-freq 80m --flash-size 4MB
2+
0x1000 hello_world_arduino.ino.bootloader.bin
3+
0x8000 hello_world_arduino.ino.partitions.bin
4+
0xe000 boot_app0.bin
5+
0x10000 hello_world_arduino.ino.bin
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)