klab-pytest-toolkit-embedded 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,178 @@
1
+ # Created by https://www.toptal.com/developers/gitignore/api/python
2
+ # Edit at https://www.toptal.com/developers/gitignore?templates=python
3
+
4
+ ### Custom ###
5
+ pytest.xml
6
+ pytest-coverage.txt
7
+ tmp/
8
+
9
+ ### Python ###
10
+ # Byte-compiled / optimized / DLL files
11
+ __pycache__/
12
+ *.py[cod]
13
+ *$py.class
14
+
15
+ # Distribution / packaging
16
+ .Python
17
+ build/
18
+ develop-eggs/
19
+ dist/
20
+ downloads/
21
+ eggs/
22
+ .eggs/
23
+ lib/
24
+ lib64/
25
+ parts/
26
+ sdist/
27
+ var/
28
+ wheels/
29
+ share/python-wheels/
30
+ *.egg-info/
31
+ .installed.cfg
32
+ *.egg
33
+ MANIFEST
34
+
35
+ # PyInstaller
36
+ # Usually these files are written by a python script from a template
37
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
38
+ *.manifest
39
+ *.spec
40
+
41
+ # Installer logs
42
+ pip-log.txt
43
+ pip-delete-this-directory.txt
44
+
45
+ # Unit test / coverage reports
46
+ htmlcov/
47
+ .tox/
48
+ .nox/
49
+ .coverage
50
+ .coverage.*
51
+ .cache
52
+ nosetests.xml
53
+ coverage.xml
54
+ *.cover
55
+ *.py,cover
56
+ .hypothesis/
57
+ .pytest_cache/
58
+ cover/
59
+
60
+ # Translations
61
+ *.mo
62
+ *.pot
63
+
64
+ # Django stuff:
65
+ *.log
66
+ local_settings.py
67
+ db.sqlite3
68
+ db.sqlite3-journal
69
+
70
+ # Flask stuff:
71
+ instance/
72
+ .webassets-cache
73
+
74
+ # Scrapy stuff:
75
+ .scrapy
76
+
77
+ # Sphinx documentation
78
+ docs/_build/
79
+
80
+ # PyBuilder
81
+ .pybuilder/
82
+ target/
83
+
84
+ # Jupyter Notebook
85
+ .ipynb_checkpoints
86
+
87
+ # IPython
88
+ profile_default/
89
+ ipython_config.py
90
+
91
+ # pyenv
92
+ # For a library or package, you might want to ignore these files since the code is
93
+ # intended to run in multiple environments; otherwise, check them in:
94
+ # .python-version
95
+
96
+ # pipenv
97
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
98
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
99
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
100
+ # install all needed dependencies.
101
+ #Pipfile.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ #pdm.lock
113
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114
+ # in version control.
115
+ # https://pdm.fming.dev/#use-with-ide
116
+ .pdm.toml
117
+
118
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
119
+ __pypackages__/
120
+
121
+ # Celery stuff
122
+ celerybeat-schedule
123
+ celerybeat.pid
124
+
125
+ # SageMath parsed files
126
+ *.sage.py
127
+
128
+ # Environments
129
+ .env
130
+ .venv
131
+ env/
132
+ venv/
133
+ ENV/
134
+ env.bak/
135
+ venv.bak/
136
+
137
+ # Spyder project settings
138
+ .spyderproject
139
+ .spyproject
140
+
141
+ # Rope project settings
142
+ .ropeproject
143
+
144
+ # mkdocs documentation
145
+ /site
146
+
147
+ # mypy
148
+ .mypy_cache/
149
+ .dmypy.json
150
+ dmypy.json
151
+
152
+ # Pyre type checker
153
+ .pyre/
154
+
155
+ # pytype static type analyzer
156
+ .pytype/
157
+
158
+ # Cython debug symbols
159
+ cython_debug/
160
+
161
+ # PyCharm
162
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
163
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
164
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
165
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
166
+ #.idea/
167
+
168
+ ### Python Patch ###
169
+ # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
170
+ poetry.toml
171
+
172
+ # ruff
173
+ .ruff_cache/
174
+
175
+ # LSP config files
176
+ pyrightconfig.json
177
+
178
+ .ms-playwright
@@ -0,0 +1,217 @@
1
+ Metadata-Version: 2.5
2
+ Name: klab-pytest-toolkit-embedded
3
+ Version: 1.1.0
4
+ Summary: Pytest embedded testing fixtures for the Klab Pytest Toolkit
5
+ Project-URL: Changelog, https://github.com/klab365/klab-pytest-toolkit/blob/main/CHANGELOG.md
6
+ Project-URL: Repository, https://github.com/klab365/klab-pytest-toolkit
7
+ Project-URL: Issues, https://github.com/klab365/klab-pytest-toolkit/issues
8
+ Author-email: Burak Kizilkaya <burak.kizilkaya@outlook.com>
9
+ License-Expression: MIT
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Requires-Python: <4,>=3.11
15
+ Requires-Dist: esptool>=5.1.0
16
+ Requires-Dist: pyserial>=3.5
17
+ Requires-Dist: pytest>=8.3.5
18
+ Description-Content-Type: text/markdown
19
+
20
+ # Klab Pytest Toolkit - Embedded
21
+
22
+ [![PyPI](https://img.shields.io/pypi/v/klab-pytest-toolkit-embedded)](https://pypi.org/project/klab-pytest-toolkit-embedded/)
23
+ [![Python](https://img.shields.io/pypi/pyversions/klab-pytest-toolkit-embedded)](https://pypi.org/project/klab-pytest-toolkit-embedded/)
24
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](../../LICENSE)
25
+
26
+ Custom pytest fixtures for embedded systems testing.
27
+ The goal is to allow testers to easily test embedded devices with reusable components for programming, resetting, and communicating with boards.
28
+
29
+ At the moment the package provides the following components:
30
+
31
+ - `Board`: Main orchestration class for managing board operations, including programming, resetting, and communication.
32
+ - Debug Probes:
33
+ - `EspTool`: Debug probe implementation for ESP32 devices using `esptool`.
34
+ - Communicators:
35
+ - `SerialCommunicator`: Serial port communication interface for UART/USB connections.
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install klab-pytest-toolkit-embedded
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ### Board Class
46
+
47
+ The `Board` class orchestrates board operations by combining a debug probe (for programming and resetting) with a communicator (for sending and receiving data).
48
+
49
+ **Create a fixture**
50
+
51
+ ```python
52
+ import pytest
53
+ from pathlib import Path
54
+ from typing import Generator
55
+ from klab_pytest_toolkit_embedded.board import Board
56
+ from klab_pytest_toolkit_embedded.debug_probes import EspTool
57
+ from klab_pytest_toolkit_embedded.communicators import SerialCommunicator
58
+
59
+ @pytest.fixture
60
+ def dut() -> Generator[Board]:
61
+ """Fixture to provide a Board instance for Device Under Test (DUT)."""
62
+ PORT = "/dev/ttyUSB0"
63
+
64
+ communicator = SerialCommunicator(port=PORT, baudrate=115200)
65
+ debug_probe = EspTool(port=PORT, baudrate=1500000, address="0x0")
66
+
67
+ with Board(debug_probe=debug_probe, communicator=communicator) as board:
68
+ yield board
69
+ ```
70
+
71
+ **Programming and Reset**
72
+
73
+ ```python
74
+ def test_program_firmware(dut: Board):
75
+ """Test programming firmware to the board."""
76
+ firmware_file = "path/to/firmware.bin"
77
+ dut.program(firmware_file)
78
+ # Firmware is now flashed to the device
79
+
80
+ def test_reset_board(dut: Board):
81
+ """Test resetting the board."""
82
+ dut.reset()
83
+ # Board has been reset
84
+ ```
85
+
86
+ **Communication**
87
+
88
+ The `Board` class provides methods for sending and receiving data:
89
+
90
+ ```python
91
+ def test_send_data(dut: Board):
92
+ """Test sending data to the board."""
93
+ dut.send(b"Hello Device!\n")
94
+
95
+ def test_receive_data(dut: Board):
96
+ """Test receiving data from the board."""
97
+ data = dut.receive_some(num_bytes=1024)
98
+ print(data.decode('utf-8', errors='ignore'))
99
+
100
+ def test_wait_for_boot_message(dut: Board):
101
+ """Test waiting for a specific message during boot."""
102
+ firmware_file = "path/to/firmware.bin"
103
+ dut.program(firmware_file)
104
+
105
+ # Wait for boot message with regex
106
+ boot_message = b"Firmware Ready!"
107
+ assert dut.wait_for_regex_in_line(boot_message, timeout_s=10, log=True)
108
+ ```
109
+
110
+ ### Serial Communicator
111
+
112
+ The `SerialCommunicator` provides serial communication functionality with configurable parameters:
113
+
114
+ ```python
115
+ from klab_pytest_toolkit_embedded.communicators import SerialCommunicator
116
+
117
+ # Create a serial communicator
118
+ communicator = SerialCommunicator(
119
+ port="/dev/ttyUSB0",
120
+ baudrate=115200,
121
+ timeout=1.0
122
+ )
123
+
124
+ # Send data
125
+ communicator.send(b"AT\r\n")
126
+
127
+ # Receive data
128
+ data = communicator.receive(num_bytes=100)
129
+
130
+ # Check available bytes
131
+ available = communicator.bytes_available()
132
+
133
+ # Flush buffers
134
+ communicator.flush_input()
135
+ communicator.flush_output()
136
+
137
+ # Close when done
138
+ communicator.close()
139
+ ```
140
+
141
+ ### ESP Debug Probe
142
+
143
+ The `EspTool` class provides programming and reset functionality for ESP32 devices:
144
+
145
+ ```python
146
+ from klab_pytest_toolkit_embedded.debug_probes import EspTool
147
+
148
+ # Create ESP debug probe
149
+ esp_probe = EspTool(
150
+ port="/dev/ttyUSB0",
151
+ baudrate=1500000,
152
+ address="0x0"
153
+ )
154
+
155
+ # Program firmware
156
+ esp_probe.program("path/to/firmware.bin")
157
+
158
+ # Reset the device
159
+ esp_probe.reset()
160
+
161
+ # Close (no persistent connection for esptool)
162
+ esp_probe.close()
163
+ ```
164
+
165
+ ## Examples
166
+
167
+ See the test files for comprehensive examples.
168
+
169
+ ## Best Practices
170
+
171
+ ### Use Context Managers
172
+
173
+ The `Board` class supports context managers to ensure proper cleanup of resources:
174
+
175
+ ```python
176
+ with Board(debug_probe=debug_probe, communicator=communicator) as board:
177
+ board.program(firmware_file)
178
+ board.wait_for_regex_in_line(b"Ready", timeout_s=10)
179
+ # Resources are automatically closed when exiting the context
180
+ ```
181
+
182
+ ### Timeout Configuration
183
+
184
+ When waiting for messages from the device, always specify appropriate timeouts to prevent tests from hanging:
185
+
186
+ ```python
187
+ # Wait with custom timeout
188
+ dut.wait_for_regex_in_line(
189
+ regex=b"Boot complete",
190
+ timeout_s=30,
191
+ log=True # Enable logging to see device output
192
+ )
193
+ ```
194
+
195
+ ### Hardware Availability
196
+
197
+ For tests that require physical hardware, use `pytest.mark.skipif` to conditionally skip tests when hardware is not available:
198
+
199
+ ```python
200
+ @pytest.mark.skipif(
201
+ not hardware_available(),
202
+ reason="This test requires a physical ESP32 device connected."
203
+ )
204
+ def test_with_hardware(dut: Board):
205
+ # Test code here
206
+ pass
207
+ ```
208
+
209
+ ## Links
210
+
211
+ - [Source code](https://github.com/klab365/klab-pytest-toolkit/tree/main/packages/klab-pytest-toolkit-embedded)
212
+ - [PyPI](https://pypi.org/project/klab-pytest-toolkit-embedded/)
213
+ - [Issue tracker](https://github.com/klab365/klab-pytest-toolkit/issues)
214
+
215
+ ## License
216
+
217
+ MIT
@@ -0,0 +1,198 @@
1
+ # Klab Pytest Toolkit - Embedded
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/klab-pytest-toolkit-embedded)](https://pypi.org/project/klab-pytest-toolkit-embedded/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/klab-pytest-toolkit-embedded)](https://pypi.org/project/klab-pytest-toolkit-embedded/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](../../LICENSE)
6
+
7
+ Custom pytest fixtures for embedded systems testing.
8
+ The goal is to allow testers to easily test embedded devices with reusable components for programming, resetting, and communicating with boards.
9
+
10
+ At the moment the package provides the following components:
11
+
12
+ - `Board`: Main orchestration class for managing board operations, including programming, resetting, and communication.
13
+ - Debug Probes:
14
+ - `EspTool`: Debug probe implementation for ESP32 devices using `esptool`.
15
+ - Communicators:
16
+ - `SerialCommunicator`: Serial port communication interface for UART/USB connections.
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ pip install klab-pytest-toolkit-embedded
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### Board Class
27
+
28
+ The `Board` class orchestrates board operations by combining a debug probe (for programming and resetting) with a communicator (for sending and receiving data).
29
+
30
+ **Create a fixture**
31
+
32
+ ```python
33
+ import pytest
34
+ from pathlib import Path
35
+ from typing import Generator
36
+ from klab_pytest_toolkit_embedded.board import Board
37
+ from klab_pytest_toolkit_embedded.debug_probes import EspTool
38
+ from klab_pytest_toolkit_embedded.communicators import SerialCommunicator
39
+
40
+ @pytest.fixture
41
+ def dut() -> Generator[Board]:
42
+ """Fixture to provide a Board instance for Device Under Test (DUT)."""
43
+ PORT = "/dev/ttyUSB0"
44
+
45
+ communicator = SerialCommunicator(port=PORT, baudrate=115200)
46
+ debug_probe = EspTool(port=PORT, baudrate=1500000, address="0x0")
47
+
48
+ with Board(debug_probe=debug_probe, communicator=communicator) as board:
49
+ yield board
50
+ ```
51
+
52
+ **Programming and Reset**
53
+
54
+ ```python
55
+ def test_program_firmware(dut: Board):
56
+ """Test programming firmware to the board."""
57
+ firmware_file = "path/to/firmware.bin"
58
+ dut.program(firmware_file)
59
+ # Firmware is now flashed to the device
60
+
61
+ def test_reset_board(dut: Board):
62
+ """Test resetting the board."""
63
+ dut.reset()
64
+ # Board has been reset
65
+ ```
66
+
67
+ **Communication**
68
+
69
+ The `Board` class provides methods for sending and receiving data:
70
+
71
+ ```python
72
+ def test_send_data(dut: Board):
73
+ """Test sending data to the board."""
74
+ dut.send(b"Hello Device!\n")
75
+
76
+ def test_receive_data(dut: Board):
77
+ """Test receiving data from the board."""
78
+ data = dut.receive_some(num_bytes=1024)
79
+ print(data.decode('utf-8', errors='ignore'))
80
+
81
+ def test_wait_for_boot_message(dut: Board):
82
+ """Test waiting for a specific message during boot."""
83
+ firmware_file = "path/to/firmware.bin"
84
+ dut.program(firmware_file)
85
+
86
+ # Wait for boot message with regex
87
+ boot_message = b"Firmware Ready!"
88
+ assert dut.wait_for_regex_in_line(boot_message, timeout_s=10, log=True)
89
+ ```
90
+
91
+ ### Serial Communicator
92
+
93
+ The `SerialCommunicator` provides serial communication functionality with configurable parameters:
94
+
95
+ ```python
96
+ from klab_pytest_toolkit_embedded.communicators import SerialCommunicator
97
+
98
+ # Create a serial communicator
99
+ communicator = SerialCommunicator(
100
+ port="/dev/ttyUSB0",
101
+ baudrate=115200,
102
+ timeout=1.0
103
+ )
104
+
105
+ # Send data
106
+ communicator.send(b"AT\r\n")
107
+
108
+ # Receive data
109
+ data = communicator.receive(num_bytes=100)
110
+
111
+ # Check available bytes
112
+ available = communicator.bytes_available()
113
+
114
+ # Flush buffers
115
+ communicator.flush_input()
116
+ communicator.flush_output()
117
+
118
+ # Close when done
119
+ communicator.close()
120
+ ```
121
+
122
+ ### ESP Debug Probe
123
+
124
+ The `EspTool` class provides programming and reset functionality for ESP32 devices:
125
+
126
+ ```python
127
+ from klab_pytest_toolkit_embedded.debug_probes import EspTool
128
+
129
+ # Create ESP debug probe
130
+ esp_probe = EspTool(
131
+ port="/dev/ttyUSB0",
132
+ baudrate=1500000,
133
+ address="0x0"
134
+ )
135
+
136
+ # Program firmware
137
+ esp_probe.program("path/to/firmware.bin")
138
+
139
+ # Reset the device
140
+ esp_probe.reset()
141
+
142
+ # Close (no persistent connection for esptool)
143
+ esp_probe.close()
144
+ ```
145
+
146
+ ## Examples
147
+
148
+ See the test files for comprehensive examples.
149
+
150
+ ## Best Practices
151
+
152
+ ### Use Context Managers
153
+
154
+ The `Board` class supports context managers to ensure proper cleanup of resources:
155
+
156
+ ```python
157
+ with Board(debug_probe=debug_probe, communicator=communicator) as board:
158
+ board.program(firmware_file)
159
+ board.wait_for_regex_in_line(b"Ready", timeout_s=10)
160
+ # Resources are automatically closed when exiting the context
161
+ ```
162
+
163
+ ### Timeout Configuration
164
+
165
+ When waiting for messages from the device, always specify appropriate timeouts to prevent tests from hanging:
166
+
167
+ ```python
168
+ # Wait with custom timeout
169
+ dut.wait_for_regex_in_line(
170
+ regex=b"Boot complete",
171
+ timeout_s=30,
172
+ log=True # Enable logging to see device output
173
+ )
174
+ ```
175
+
176
+ ### Hardware Availability
177
+
178
+ For tests that require physical hardware, use `pytest.mark.skipif` to conditionally skip tests when hardware is not available:
179
+
180
+ ```python
181
+ @pytest.mark.skipif(
182
+ not hardware_available(),
183
+ reason="This test requires a physical ESP32 device connected."
184
+ )
185
+ def test_with_hardware(dut: Board):
186
+ # Test code here
187
+ pass
188
+ ```
189
+
190
+ ## Links
191
+
192
+ - [Source code](https://github.com/klab365/klab-pytest-toolkit/tree/main/packages/klab-pytest-toolkit-embedded)
193
+ - [PyPI](https://pypi.org/project/klab-pytest-toolkit-embedded/)
194
+ - [Issue tracker](https://github.com/klab365/klab-pytest-toolkit/issues)
195
+
196
+ ## License
197
+
198
+ MIT
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "klab-pytest-toolkit-embedded"
7
+ dynamic = ["version"]
8
+ description = "Pytest embedded testing fixtures for the Klab Pytest Toolkit"
9
+ authors = [{ name = "Burak Kizilkaya", email = "burak.kizilkaya@outlook.com" }]
10
+ requires-python = ">=3.11,<4"
11
+ readme = "README.md"
12
+ license = "MIT"
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Programming Language :: Python :: 3",
18
+ ]
19
+ dependencies = [
20
+ "esptool>=5.1.0",
21
+ "pyserial>=3.5",
22
+ "pytest>=8.3.5",
23
+ ]
24
+
25
+ [project.urls]
26
+ Changelog = "https://github.com/klab365/klab-pytest-toolkit/blob/main/CHANGELOG.md"
27
+ Repository = "https://github.com/klab365/klab-pytest-toolkit"
28
+ Issues = "https://github.com/klab365/klab-pytest-toolkit/issues"
29
+
30
+ [project.entry-points.pytest11]
31
+ klab_pytest_toolkit_embedded = "klab_pytest_toolkit_embedded.plugin"
32
+
33
+ [tool.hatch.version]
34
+ path = "src/klab_pytest_toolkit_embedded/__init__.py"
35
+
36
+ [tool.hatch.build.targets.sdist]
37
+ exclude = [
38
+ "tests",
39
+ ]
40
+
41
+ [tool.pytest.ini_options]
42
+ asyncio_mode = "strict"
43
+ asyncio_default_fixture_loop_scope = "function"
44
+ pythonpath = [
45
+ "."
46
+ ]
@@ -0,0 +1,4 @@
1
+ """Klab Pytest Toolkit - Embedded package."""
2
+
3
+ __version__ = "1.1.0"
4
+ __all__ = []
@@ -0,0 +1,107 @@
1
+ import re
2
+ import time
3
+
4
+ from klab_pytest_toolkit_embedded.communicators import CommunicatorInterface
5
+ from klab_pytest_toolkit_embedded.debug_probes import DebugProbe
6
+
7
+
8
+ class Board:
9
+ """Main class for the orchestration of board operations.
10
+
11
+ This class uses a debug probe to program and reset the board,
12
+ and a communication interface to send and receive data.
13
+
14
+ It could be, that both functionalities are provided by the same physical device,
15
+ but this is abstracted away by using separate interfaces.
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ debug_probe: DebugProbe,
21
+ communicator: CommunicatorInterface,
22
+ ):
23
+ """Initialize the board.
24
+
25
+ Args:
26
+ debug_probe: Debug probe instance (e.g., EspTool)
27
+ communication_interface: Communication interface instance (e.g., SerialCommunicator)
28
+ """
29
+ self._debug_probe = debug_probe
30
+ self._communicator = communicator
31
+
32
+ def program(self, fw_image: str) -> None:
33
+ """Flash the firmware image to the board.
34
+
35
+ Args:
36
+ fw_image (str): Path to the firmware image.
37
+ """
38
+ self._debug_probe.program(fw_image)
39
+
40
+ def reset(self) -> None:
41
+ """Reset the board."""
42
+ self._debug_probe.reset()
43
+
44
+ def receive_some(self, num_bytes: int = 1024) -> bytes:
45
+ """Receive some data from the communication interface.
46
+
47
+ Args:
48
+ num_bytes (int, optional): Number of bytes to receive. Defaults to 1024.
49
+
50
+ Returns:
51
+ bytes: Received data.
52
+ """
53
+ return self._communicator.receive(num_bytes)
54
+
55
+ def send(self, data: bytes) -> None:
56
+ """Send data to the device through the communication interface.
57
+
58
+ Args:
59
+ data: Bytes to send
60
+ """
61
+ self._communicator.send(data)
62
+
63
+ def wait_for_regex_in_line(self, regex, timeout_s=20, log=True) -> bool:
64
+ """Wait for a line matching the regex from the communication interface.
65
+
66
+ Args:
67
+ regex: Regular expression to match.
68
+ timeout_s (int, optional): Timeout in seconds. Defaults to 20.
69
+ log (bool, optional): Whether to log the output. Defaults to True.
70
+
71
+ Returns:
72
+ bool: True if a matching line is found, False otherwise.
73
+ """
74
+ received = b"" # Start with bytes, not string
75
+ start_time = time.time()
76
+ while True:
77
+ # Check for timeout
78
+ if time.time() - start_time > timeout_s:
79
+ raise TimeoutError(f"Timeout waiting for regex: {regex}")
80
+
81
+ # Check in already received data
82
+ lines = received.splitlines(keepends=True)
83
+
84
+ for _, line in enumerate(lines):
85
+ regex_search = re.search(
86
+ regex,
87
+ line.replace(b"\r", b"").replace(b"\n", b"").decode("utf-8", errors="ignore"),
88
+ )
89
+ if regex_search:
90
+ return True
91
+
92
+ # Receive more data
93
+ chunk = self.receive_some()
94
+
95
+ if log:
96
+ print(chunk.replace(b"\r", b"").decode("utf-8", errors="ignore"), end="")
97
+
98
+ received = received + chunk
99
+
100
+ def __enter__(self):
101
+ """Enter context manager."""
102
+ return self
103
+
104
+ def __exit__(self, exc_type, exc_value, traceback):
105
+ """Exit context manager."""
106
+ self._communicator.close()
107
+ self._debug_probe.close()
@@ -0,0 +1,4 @@
1
+ from klab_pytest_toolkit_embedded.communicators.interface import CommunicatorInterface
2
+ from klab_pytest_toolkit_embedded.communicators.serial import SerialCommunicator
3
+
4
+ __all__ = ["CommunicatorInterface", "SerialCommunicator"]
@@ -0,0 +1,22 @@
1
+ """Communication interface definition for embedded boards."""
2
+
3
+ import abc
4
+
5
+
6
+ class CommunicatorInterface(abc.ABC):
7
+ """Abstract interface for MCU communication."""
8
+
9
+ @abc.abstractmethod
10
+ def send(self, data: bytes) -> None:
11
+ """Send data to the device."""
12
+ raise NotImplementedError
13
+
14
+ @abc.abstractmethod
15
+ def receive(self, num_bytes: int) -> bytes:
16
+ """Receive data from the device."""
17
+ raise NotImplementedError
18
+
19
+ @abc.abstractmethod
20
+ def close(self) -> None:
21
+ """Close the communication channel."""
22
+ raise NotImplementedError
@@ -0,0 +1,113 @@
1
+ """Serial communication implementation for embedded boards."""
2
+
3
+ import serial
4
+
5
+ from klab_pytest_toolkit_embedded.communicators.interface import CommunicatorInterface
6
+
7
+
8
+ class SerialCommunicator(CommunicatorInterface):
9
+ """Serial port communication interface for UART/USB connections."""
10
+
11
+ def __init__(
12
+ self,
13
+ port: str,
14
+ baudrate: int = 115200,
15
+ timeout: float = 1.0,
16
+ bytesize: int = serial.EIGHTBITS,
17
+ parity: str = serial.PARITY_NONE,
18
+ stopbits: float = serial.STOPBITS_ONE,
19
+ ):
20
+ """Initialize serial communication.
21
+
22
+ Args:
23
+ port: Serial port path (e.g., '/dev/ttyUSB0', 'COM3')
24
+ baudrate: Communication speed in bits per second (default: 115200)
25
+ timeout: Read timeout in seconds (default: 1.0)
26
+ bytesize: Number of data bits (default: 8)
27
+ parity: Parity checking mode (default: None)
28
+ stopbits: Number of stop bits (default: 1)
29
+ """
30
+ self.port = port
31
+ self.baudrate = baudrate
32
+ self._serial: serial.Serial | None = None
33
+
34
+ # Open the serial connection
35
+ self._serial = serial.Serial(
36
+ port=port,
37
+ baudrate=baudrate,
38
+ timeout=timeout,
39
+ bytesize=bytesize,
40
+ parity=parity,
41
+ stopbits=stopbits,
42
+ )
43
+
44
+ def send(self, data: bytes) -> None:
45
+ """Send data to the device.
46
+
47
+ Args:
48
+ data: Bytes to send
49
+
50
+ Raises:
51
+ RuntimeError: If serial port is not open
52
+ """
53
+ if not self._serial or not self._serial.is_open:
54
+ raise RuntimeError("Serial port is not open")
55
+
56
+ self._serial.write(data)
57
+ self._serial.flush()
58
+
59
+ def receive(self, num_bytes: int) -> bytes:
60
+ """Receive data from the device.
61
+
62
+ Args:
63
+ num_bytes: Maximum number of bytes to receive
64
+
65
+ Returns:
66
+ Received bytes (may be less than num_bytes if timeout occurs)
67
+
68
+ Raises:
69
+ RuntimeError: If serial port is not open
70
+ """
71
+ if not self._serial or not self._serial.is_open:
72
+ raise RuntimeError("Serial port is not open")
73
+
74
+ return self._serial.read(num_bytes)
75
+
76
+ def close(self) -> None:
77
+ """Close the serial connection."""
78
+ if self._serial and self._serial.is_open:
79
+ self._serial.close()
80
+
81
+ def flush_input(self) -> None:
82
+ """Flush input buffer, discarding all pending data."""
83
+ if self._serial and self._serial.is_open:
84
+ self._serial.reset_input_buffer()
85
+
86
+ def flush_output(self) -> None:
87
+ """Flush output buffer, waiting for all data to be transmitted."""
88
+ if self._serial and self._serial.is_open:
89
+ self._serial.reset_output_buffer()
90
+
91
+ def bytes_available(self) -> int:
92
+ """Get number of bytes available in the input buffer.
93
+
94
+ Returns:
95
+ Number of bytes available to read
96
+ """
97
+ if not self._serial or not self._serial.is_open:
98
+ return 0
99
+
100
+ return self._serial.in_waiting
101
+
102
+ def __enter__(self) -> "SerialCommunicator":
103
+ """Context manager entry."""
104
+ return self
105
+
106
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
107
+ """Context manager exit - ensures cleanup."""
108
+ self.close()
109
+
110
+ def __repr__(self) -> str:
111
+ """String representation."""
112
+ status = "open" if (self._serial and self._serial.is_open) else "closed"
113
+ return f"<SerialCommunicator(port='{self.port}', baudrate={self.baudrate}, {status})>"
@@ -0,0 +1,4 @@
1
+ from klab_pytest_toolkit_embedded.debug_probes.interface import DebugProbe
2
+ from klab_pytest_toolkit_embedded.debug_probes.esp import EspTool
3
+
4
+ __all__ = ["DebugProbe", "EspTool"]
@@ -0,0 +1,48 @@
1
+ from klab_pytest_toolkit_embedded.debug_probes.interface import DebugProbe
2
+
3
+ import esptool
4
+
5
+
6
+ class EspTool(DebugProbe):
7
+ """ESP debug probe using esptool.py."""
8
+
9
+ def __init__(self, port: str, baudrate: int = 1500000, address: str = "0x0"):
10
+ self._port = port
11
+ self._baudrate = baudrate
12
+ self._address = address
13
+
14
+ def program(self, fw_image: str) -> None:
15
+ cmd = [
16
+ "--chip",
17
+ "esp32",
18
+ "--port",
19
+ self._port,
20
+ "--baud",
21
+ str(self._baudrate),
22
+ "--after",
23
+ "hard_reset",
24
+ "write_flash",
25
+ "-e",
26
+ self._address,
27
+ fw_image,
28
+ ]
29
+
30
+ esptool.main(cmd)
31
+
32
+ def reset(self) -> None:
33
+ cmd = [
34
+ "--chip",
35
+ "esp32",
36
+ "--port",
37
+ self._port,
38
+ "--baud",
39
+ str(self._baudrate),
40
+ "reset",
41
+ ]
42
+
43
+ esptool.main(cmd)
44
+
45
+ def close(self) -> None:
46
+ """Close the debug probe connection."""
47
+ # esptool does not maintain a persistent connection, so nothing to close.
48
+ pass
@@ -0,0 +1,26 @@
1
+ """Debug probe interface definition for embedded boards."""
2
+
3
+ import abc
4
+
5
+
6
+ class DebugProbe(abc.ABC):
7
+ """Abstract base class for debug probes."""
8
+
9
+ @abc.abstractmethod
10
+ def program(self, fw_image: str) -> None:
11
+ """Flash the firmware image to the target device.
12
+
13
+ Args:
14
+ fw_image (str): Path to the firmware image.
15
+ """
16
+ raise NotImplementedError("program method must be implemented by subclasses")
17
+
18
+ @abc.abstractmethod
19
+ def reset(self) -> None:
20
+ """Reset the target device."""
21
+ raise NotImplementedError("reset method must be implemented by subclasses")
22
+
23
+ @abc.abstractmethod
24
+ def close(self) -> None:
25
+ """Close the debug probe connection."""
26
+ raise NotImplementedError("close method must be implemented by subclasses")
@@ -0,0 +1,6 @@
1
+ """Pytest plugin for embedded testing."""
2
+
3
+
4
+ def pytest_configure(config):
5
+ """Register custom markers and configuration for klab-pytest-toolkit-embedded."""
6
+ pass