-
Notifications
You must be signed in to change notification settings - Fork 36
refactor: replace WokwiCLI with Wokwi class (RDT-1428) #365
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 10 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
4cbc9ce
refactor: replace WokwiCLI with Wokwi class and update related refere…
JakubAndrysek 7409898
refactor: remove deprecated Wokwi CLI options and related code
JakubAndrysek e6f0697
fix(wokwi): update method calls for serial monitoring and writing
JakubAndrysek 7453c82
fix: update wokwi-client dependency to use stable release
JakubAndrysek fa35a20
fix: update wokwi-client dependency to require a minimum version
JakubAndrysek 27a9b48
feat(wokwi): log upload status before starting simulation
JakubAndrysek 3f29de1
fix: update wokwi-client dependency to require version 0.1.2
JakubAndrysek 0415a15
fix: remove unused wokwi-cli requirement from tests
JakubAndrysek 938717c
fix: update wokwi-client dependency to require version 0.1.3
JakubAndrysek e5cb868
feat(tests): add merged binary for hello_world_arduino example
JakubAndrysek 9ff0543
fix: update wokwi-client dependency to require version 0.2.0, remove …
JakubAndrysek 9ed5fda
chore: Merge remote-tracking branch 'esp/main' into wokwi-python-client
JakubAndrysek 7eb79f9
refactor: update type hints for wokwi diagram and write method
JakubAndrysek 4b425c8
revert: revert back to the working logger with the wokwi
JakubAndrysek File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,205 @@ | ||
| import json | ||
| import logging | ||
| import os | ||
| import typing as t | ||
| from pathlib import Path | ||
|
|
||
| from packaging.version import Version | ||
| from pytest_embedded.log import DuplicateStdoutPopen, MessageQueue | ||
| from pytest_embedded.utils import Meta | ||
| from wokwi_client import GET_TOKEN_URL, WokwiClientSync | ||
|
|
||
| from pytest_embedded_wokwi import WOKWI_CLI_MINIMUM_VERSION | ||
|
|
||
| from .idf import IDFFirmwareResolver | ||
|
|
||
| if t.TYPE_CHECKING: # pragma: no cover | ||
| from pytest_embedded_idf.app import IdfApp | ||
|
|
||
|
|
||
| target_to_board = { | ||
| 'esp32': 'board-esp32-devkit-c-v4', | ||
| 'esp32c3': 'board-esp32-c3-devkitm-1', | ||
| 'esp32c6': 'board-esp32-c6-devkitc-1', | ||
| 'esp32h2': 'board-esp32-h2-devkitm-1', | ||
| 'esp32p4': 'board-esp32-p4-function-ev', | ||
| 'esp32s2': 'board-esp32-s2-devkitm-1', | ||
| 'esp32s3': 'board-esp32-s3-devkitc-1', | ||
| } | ||
|
|
||
|
|
||
| class Wokwi(DuplicateStdoutPopen): | ||
| """Synchronous Wokwi integration that inherits from DuplicateStdoutPopen. | ||
|
|
||
| This class provides a synchronous interface to the Wokwi simulator while maintaining | ||
| compatibility with pytest-embedded's logging and message queue infrastructure. | ||
| """ | ||
|
|
||
| SOURCE = 'Wokwi' | ||
| REDIRECT_CLS = None # We'll handle output redirection manually | ||
|
|
||
| def __init__( | ||
| self, | ||
| msg_queue: MessageQueue, | ||
| firmware_resolver: IDFFirmwareResolver, | ||
| wokwi_diagram: t.Optional[str] = None, | ||
| app: t.Optional['IdfApp'] = None, | ||
| meta: t.Optional[Meta] = None, | ||
| **kwargs, | ||
| ): | ||
| self.app = app | ||
|
|
||
| # Get Wokwi API token | ||
| token = os.getenv('WOKWI_CLI_TOKEN') | ||
| if not token: | ||
| raise SystemExit(f'Set WOKWI_CLI_TOKEN in your environment. You can get it from {GET_TOKEN_URL}.') | ||
|
|
||
| # Initialize synchronous Wokwi client | ||
| self.client = WokwiClientSync(token) | ||
|
|
||
| # Check version compatibility | ||
| if Version(self.client.version) < Version(WOKWI_CLI_MINIMUM_VERSION): | ||
| logging.warning( | ||
| 'Wokwi client version %s < required %s (compatibility not guaranteed)', | ||
| self.client.version, | ||
| WOKWI_CLI_MINIMUM_VERSION, | ||
| ) | ||
| logging.info('Wokwi client library version: %s', self.client.version) | ||
|
|
||
| # Prepare diagram file if not supplied | ||
| if wokwi_diagram is None: | ||
| self.create_diagram_json() | ||
| wokwi_diagram = os.path.join(self.app.app_path, 'diagram.json') | ||
|
|
||
| # Initialize parent class | ||
| super().__init__(msg_queue=msg_queue, meta=meta, **kwargs) | ||
|
|
||
| # Connect and start simulation | ||
| try: | ||
| flasher_args = firmware_resolver.resolve_firmware(app) | ||
| firmware_path = Path(flasher_args).as_posix() | ||
| elf_path = Path(app.elf_file).as_posix() | ||
|
|
||
| self._setup_simulation(wokwi_diagram, firmware_path, elf_path) | ||
| self._start_serial_monitoring() | ||
| except Exception as e: | ||
| self.close() | ||
| raise e | ||
|
|
||
| def _setup_simulation(self, diagram: str, firmware_path: str, elf_path: str): | ||
| """Set up the Wokwi simulation.""" | ||
| hello = self.client.connect() | ||
| logging.info('Connected to Wokwi Simulator, server version: %s', hello.get('version', 'unknown')) | ||
|
|
||
| # Upload files | ||
| self.client.upload_file('diagram.json', diagram) | ||
| firmware = self.client.upload_file('pytest.bin', firmware_path) | ||
|
|
||
| self.client.upload_file('pytest.elf', elf_path) | ||
|
|
||
| logging.info('Uploaded diagram and firmware to Wokwi. Starting simulation...') | ||
|
|
||
| # Start simulation | ||
| self.client.start_simulation(firmware, elf='pytest.elf') | ||
|
|
||
| def _start_serial_monitoring(self): | ||
| """Start monitoring serial output and forward to stdout and message queue.""" | ||
|
|
||
| def serial_callback(data: bytes): | ||
| # Write to stdout for live monitoring | ||
| try: | ||
| decoded = data.decode('utf-8', errors='replace') | ||
| print(decoded, end='', flush=True) | ||
| except Exception as e: | ||
| logging.debug(f'Error writing to stdout: {e}') | ||
|
|
||
| # Write to log file if available | ||
| try: | ||
| if hasattr(self, '_fw') and self._fw and not self._fw.closed: | ||
| decoded = data.decode('utf-8', errors='replace') | ||
| self._fw.write(decoded) | ||
| self._fw.flush() | ||
| except Exception as e: | ||
| logging.debug(f'Error writing to log file: {e}') | ||
|
|
||
| # Put in message queue for expect() functionality | ||
| try: | ||
| if hasattr(self, '_q') and self._q: | ||
| self._q.put(data) | ||
| except Exception as e: | ||
| logging.debug(f'Error putting data in message queue: {e}') | ||
|
|
||
| # Start monitoring in background | ||
| self.client.serial_monitor(serial_callback) | ||
|
|
||
| def write(self, s: t.Union[str, bytes]) -> None: | ||
| """Write data to the Wokwi serial interface.""" | ||
| try: | ||
| data = s if isinstance(s, bytes) else s.encode('utf-8') | ||
| self.client.serial_write(data) | ||
| logging.debug(f'{self.SOURCE} ->: {s}') | ||
| except Exception as e: | ||
| logging.error(f'Failed to write to Wokwi serial: {e}') | ||
|
|
||
| def close(self): | ||
| """Clean up resources.""" | ||
| try: | ||
| if hasattr(self, 'client') and self.client: | ||
| self.client.disconnect() | ||
| except Exception as e: | ||
| logging.debug(f'Error during Wokwi cleanup: {e}') | ||
| finally: | ||
| super().close() | ||
|
|
||
| def __del__(self): | ||
| """Destructor to ensure cleanup when object is garbage collected.""" | ||
| self.close() | ||
| super().__del__() | ||
|
|
||
| def terminate(self): | ||
| """Terminate the Wokwi connection.""" | ||
| self.close() | ||
| super().terminate() | ||
|
|
||
| def create_diagram_json(self): | ||
| """Create a diagram.json file for the simulation.""" | ||
| app = self.app | ||
| target_board = target_to_board[app.target] | ||
|
|
||
| # Check for existing diagram.json file | ||
| diagram_json_path = os.path.join(app.app_path, 'diagram.json') | ||
| if os.path.exists(diagram_json_path): | ||
| with open(diagram_json_path) as f: | ||
| json_data = json.load(f) | ||
| if not any(part['type'] == target_board for part in json_data['parts']): | ||
| logging.warning( | ||
| f'diagram.json exists, no part with type "{target_board}" found. ' | ||
| + 'You may need to update the diagram.json file manually to match the target board.' | ||
| ) | ||
| return | ||
|
|
||
| # Create default diagram | ||
| if app.target == 'esp32p4': | ||
| rx_pin = '38' | ||
| tx_pin = '37' | ||
| else: | ||
| rx_pin = 'RX' | ||
| tx_pin = 'TX' | ||
|
|
||
| diagram = { | ||
| 'version': 1, | ||
| 'author': 'Uri Shaked', | ||
| 'editor': 'wokwi', | ||
| 'parts': [{'type': target_board, 'id': 'esp'}], | ||
| 'connections': [ | ||
| ['esp:' + tx_pin, '$serialMonitor:RX', ''], | ||
| ['esp:' + rx_pin, '$serialMonitor:TX', ''], | ||
| ], | ||
| } | ||
|
|
||
| with open(diagram_json_path, 'w') as f: | ||
| json.dump(diagram, f, indent=2) | ||
|
|
||
| def _hard_reset(self): | ||
| """Fake hard_reset to maintain API consistency.""" | ||
| raise NotImplementedError('Hard reset not supported in Wokwi simulation') |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.