-
Notifications
You must be signed in to change notification settings - Fork 1.1k
python-ecosys/debugpy: Add VS Code debugging support for MicroPython. #1022
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鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
andrewleech
wants to merge
32
commits into
micropython:master
Choose a base branch
from
andrewleech:add-debugpy-support
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
c98b355
python-ecosys/debugpy: Add VS Code debugging support for MicroPython.
pi-anl 9adb886
test_vscode: Add global variables to show vaiable tracking and hover.
Josverl 3ed2d89
debugpy: Improve variable retrievals.
Josverl c4202e4
dap_monitor: Exit session on debugger disconnect.
Josverl 5d491e0
debugpy: Fix VS Code path mapping to prevent read-only file copies.
pi-anl c35c6be
debugpy: Enhance PDB adapter with special variable processing.
Josverl 5d081a5
debugpy/dap_monitor: Add cli for target and ports.
Josverl f5d2977
debugpy/debug_session: Detect bare metal ports.
Josverl 19883c1
debugpy : Format code.
Josverl 2eb9cb5
debugpy : Decode debugger IP address on connect.
Josverl bb6dc8b
debugpy: Add type hints and improve path mapping logic in PDB adapter
Josverl 756d174
debugpy: Enhance path mapping handling in PDB adapter and debug session.
Josverl 14b2e27
debugpy: Enhance breakpoint handling and path mapping in PdbAdapter
Josverl 817f5ec
debugpy: Refactor breakpoint storage to use sets for improved efficiency
Josverl 26c6e3a
debugpy: Add complex variable handling and caching.
Josverl 16de387
debugpy: Format code.
Josverl 3c5fb37
debugpy : Optimize memory management and performance in PDB adapter.
Josverl 49005b4
Merge branch 'pdb_support/perf2' into debugpy/performance
Josverl 3435c93
revert: Overly complex variable preview.
Josverl 5c41be6
debugpy: Performance improvements.
Josverl 3bf4761
debugpy: Performance - aviod method access.
Josverl b45fba4
py-ecosys/debugpy: Add pause functionality.
Josverl 2f8e9b8
python-ecosys/debugpy: Add set Variable functionality.
Josverl 8a45947
python-ecosys/debugpy: Add local variable modification while debugging.
Josverl 80ef022
python-ecosys/debugpy: Format code with ruff.
Josverl 9fb263a
python-ecosys/debugpy: Code cleanup.
Josverl 1b49992
pdbadapeter: fixup "Special".
Josverl 61c8905
debugpy: reassemble DAP messages split across recv() calls
pi-anl e359a95
debugpy: add wait_for_client, capability probe, read-only locals
pi-anl 4fabcb3
debugpy: Execute statements from repl and clipboard evaluate contexts.
pi-anl 0c13480
debugpy: Return the bound endpoint from listen() before accepting.
pi-anl 3432190
debugpy: Stop nested message pumps from clobbering the socket timeout.
pi-anl 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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| # MicroPython debugpy | ||
|
|
||
| A minimal implementation of debugpy for MicroPython, enabling remote debugging | ||
| such as VS Code debugging support. | ||
|
|
||
| ## Features | ||
|
|
||
| - Debug Adapter Protocol (DAP) support for VS Code integration | ||
| - Basic debugging operations: | ||
| - Breakpoints | ||
| - Step over/into/out | ||
| - Stack trace inspection | ||
| - Variable inspection (globals, locals generally not supported) | ||
| - Expression evaluation | ||
| - Pause/continue execution | ||
|
|
||
| ## Requirements | ||
|
|
||
| - MicroPython with `sys.settrace` support (enabled with `MICROPY_PY_SYS_SETTRACE`) | ||
| - Socket support for network communication | ||
| - JSON support for DAP message parsing | ||
|
|
||
| ## Usage | ||
|
|
||
| ### Basic Usage | ||
|
|
||
| ```python | ||
| import debugpy | ||
|
|
||
| # Start listening for debugger connections | ||
| host, port = debugpy.listen() # Default: 127.0.0.1:5678 | ||
| print(f"Debugger listening on {host}:{port}") | ||
|
|
||
| # Enable debugging for current thread | ||
| debugpy.debug_this_thread() | ||
|
|
||
| # Your code here... | ||
| def my_function(): | ||
| x = 10 | ||
| y = 20 | ||
| result = x + y # Set breakpoint here in VS Code | ||
| return result | ||
|
|
||
| result = my_function() | ||
| print(f"Result: {result}") | ||
|
|
||
| # Manual breakpoint | ||
| debugpy.breakpoint() | ||
| ``` | ||
|
|
||
| ### VS Code Configuration | ||
|
|
||
| Create a `.vscode/launch.json` file in your project: | ||
|
|
||
| ```json | ||
| { | ||
| "version": "0.2.0", | ||
| "configurations": [ | ||
| { | ||
| "name": "Attach to MicroPython", | ||
| "type": "python", | ||
| "request": "attach", | ||
| "connect": { | ||
| "host": "127.0.0.1", | ||
| "port": 5678 | ||
| }, | ||
| "pathMappings": [ | ||
| { | ||
| "localRoot": "${workspaceFolder}", | ||
| "remoteRoot": "." | ||
| } | ||
| ], | ||
| "justMyCode": false | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| ### Testing | ||
|
|
||
| 1. Build the MicroPython Unix coverage port: | ||
| ```bash | ||
| cd ports/unix | ||
| make CFLAGS_EXTRA="-DMICROPY_PY_SYS_SETTRACE=1" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this flag |
||
| ``` | ||
|
|
||
| 2. Run the test script: | ||
| ```bash | ||
| cd lib/micropython-lib/python-ecosys/debugpy | ||
| ../../../../ports/unix/build-coverage/micropython test_debugpy.py | ||
| ``` | ||
|
|
||
| 3. In VS Code, open the debugpy folder and press F5 to attach the debugger | ||
|
|
||
| 4. Set breakpoints in the test script and observe debugging functionality | ||
|
|
||
| ## API Reference | ||
|
|
||
| ### `debugpy.listen(port=5678, host="127.0.0.1")` | ||
|
|
||
| Start listening for debugger connections. | ||
|
|
||
| **Parameters:** | ||
| - `port`: Port number to listen on (default: 5678) | ||
| - `host`: Host address to bind to (default: "127.0.0.1") | ||
|
|
||
| **Returns:** Tuple of (host, port) actually used | ||
|
|
||
| ### `debugpy.debug_this_thread()` | ||
|
|
||
| Enable debugging for the current thread by installing the trace function. | ||
|
|
||
| ### `debugpy.breakpoint()` | ||
|
|
||
| Trigger a manual breakpoint that will pause execution if a debugger is attached. | ||
|
|
||
| ### `debugpy.wait_for_client()` | ||
|
|
||
| Wait for the debugger client to connect and initialize. | ||
|
|
||
| ### `debugpy.is_client_connected()` | ||
|
|
||
| Check if a debugger client is currently connected. | ||
|
|
||
| **Returns:** Boolean indicating connection status | ||
|
|
||
| ### `debugpy.disconnect()` | ||
|
|
||
| Disconnect from the debugger client and clean up resources. | ||
|
|
||
| ## Architecture | ||
|
|
||
| The implementation consists of several key components: | ||
|
|
||
| 1. **Public API** (`public_api.py`): Main entry points for users | ||
| 2. **Debug Session** (`server/debug_session.py`): Handles DAP protocol communication | ||
| 3. **PDB Adapter** (`server/pdb_adapter.py`): Bridges DAP and MicroPython's trace system | ||
| 4. **Messaging** (`common/messaging.py`): JSON message handling for DAP | ||
| 5. **Constants** (`common/constants.py`): DAP protocol constants | ||
|
|
||
| ## Limitations | ||
|
|
||
| This is a minimal implementation with the following limitations: | ||
|
|
||
| - Single-threaded debugging only | ||
| - No conditional breakpoints | ||
| - No function breakpoints | ||
| - Limited variable inspection (no nested object expansion) | ||
| - No step back functionality | ||
| - No hot code reloading | ||
| - Simplified stepping implementation | ||
|
|
||
| ## Compatibility | ||
|
|
||
| Tested with: | ||
| - MicroPython Unix port | ||
| - VS Code with Python/debugpy extension | ||
| - CPython 3.x (for comparison) | ||
|
|
||
| ## Contributing | ||
|
|
||
| This implementation provides a foundation for MicroPython debugging. Contributions are welcome to add: | ||
|
|
||
| - Conditional breakpoint support | ||
| - Better variable inspection | ||
| - Multi-threading support | ||
| - Performance optimizations | ||
| - Additional DAP features | ||
|
|
||
| ## License | ||
|
|
||
| MIT License - see the MicroPython project license for details. | ||
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,199 @@ | ||
| #!/usr/bin/env python3 | ||
| """DAP protocol monitor - sits between VS Code and MicroPython debugpy.""" | ||
|
|
||
| import socket | ||
| import threading | ||
| import json | ||
| import time | ||
| import sys | ||
| import argparse | ||
|
|
||
|
|
||
| class DAPMonitor: | ||
| def __init__(self, listen_port=5679, target_host="127.0.0.1", target_port=5678): | ||
| self.disconnect = False | ||
| self.listen_port = listen_port | ||
| self.target_host = target_host | ||
| self.target_port = target_port | ||
| self.client_sock = None | ||
| self.server_sock = None | ||
|
|
||
| def start(self): | ||
| """Start the DAP monitor proxy.""" | ||
| print(f"DAP Monitor starting on port {self.listen_port}") | ||
| print(f"Will forward to {self.target_host}:{self.target_port}") | ||
| print("Start MicroPython debugpy server first, then connect VS Code to port 5679") | ||
|
|
||
| # Create listening socket | ||
| listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
| listener.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | ||
| listener.bind(("127.0.0.1", self.listen_port)) | ||
| listener.listen(1) | ||
|
|
||
| print(f"Listening for VS Code connection on port {self.listen_port}...") | ||
|
|
||
| try: | ||
| # Wait for VS Code to connect | ||
| self.client_sock, client_addr = listener.accept() | ||
| print(f"VS Code connected from {client_addr}") | ||
|
|
||
| # Connect to MicroPython debugpy server | ||
| self.server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||
| self.server_sock.connect((self.target_host, self.target_port)) | ||
| print(f"Connected to MicroPython debugpy at {self.target_host}:{self.target_port}") | ||
|
|
||
| # Start forwarding threads | ||
| threading.Thread(target=self.forward_client_to_server, daemon=True).start() | ||
| threading.Thread(target=self.forward_server_to_client, daemon=True).start() | ||
|
|
||
| print("DAP Monitor active - press Ctrl+C to stop") | ||
| while not self.disconnect: | ||
| time.sleep(1) | ||
|
|
||
| except KeyboardInterrupt: | ||
| print("\nStopping DAP Monitor...") | ||
| except Exception as e: | ||
| print(f"Error: {e}") | ||
| finally: | ||
| self.cleanup() | ||
|
|
||
| def forward_client_to_server(self): | ||
| """Forward messages from VS Code client to MicroPython server.""" | ||
| try: | ||
| while True: | ||
| data = self.receive_dap_message(self.client_sock, "VS Code") | ||
| if data is None: | ||
| break | ||
| self.send_raw_data(self.server_sock, data) | ||
| except Exception as e: | ||
| print(f"Client->Server forwarding error: {e}") | ||
|
|
||
| def forward_server_to_client(self): | ||
| """Forward messages from MicroPython server to VS Code client.""" | ||
| try: | ||
| while True: | ||
| data = self.receive_dap_message(self.server_sock, "MicroPython") | ||
| if data is None: | ||
| break | ||
| self.send_raw_data(self.client_sock, data) | ||
| except Exception as e: | ||
| print(f"Server->Client forwarding error: {e}") | ||
|
|
||
| def receive_dap_message(self, sock, source): | ||
| """Receive and log a DAP message.""" | ||
| try: | ||
| # Read headers | ||
| header = b"" | ||
| while b"\r\n\r\n" not in header: | ||
| byte = sock.recv(1) | ||
| if not byte: | ||
| return None | ||
| header += byte | ||
|
|
||
| # Parse content length | ||
| header_str = header.decode("utf-8") | ||
| content_length = 0 | ||
| for line in header_str.split("\r\n"): | ||
| if line.startswith("Content-Length:"): | ||
| content_length = int(line.split(":", 1)[1].strip()) | ||
| break | ||
|
|
||
| if content_length == 0: | ||
| return None | ||
|
|
||
| # Read content | ||
| content = b"" | ||
| while len(content) < content_length: | ||
| chunk = sock.recv(content_length - len(content)) | ||
| if not chunk: | ||
| return None | ||
| content += chunk | ||
|
|
||
| # Parse and Log the message | ||
| message = self.parse_dap(source, content) | ||
| self.log_dap_message(source, message) | ||
| # Check for disconnect command | ||
| if message: | ||
| if "disconnect" == message.get("command", message.get("event", "unknown")): | ||
| print(f"\n[{source}] Disconnect command received, stopping monitor.") | ||
| self.disconnect = True | ||
| return header + content | ||
| except Exception as e: | ||
| print(f"Error receiving from {source}: {e}") | ||
| return None | ||
|
|
||
| def parse_dap(self, source, content): | ||
| """Parse DAP message and log it.""" | ||
| try: | ||
| message = json.loads(content.decode("utf-8")) | ||
| return message | ||
| except json.JSONDecodeError: | ||
| print(f"\n[{source}] Invalid JSON: {content}") | ||
| return None | ||
|
|
||
| def log_dap_message(self, source, message): | ||
| """Log DAP message details.""" | ||
| msg_type = message.get("type", "unknown") | ||
| command = message.get("command", message.get("event", "unknown")) | ||
| seq = message.get("seq", 0) | ||
|
|
||
| print(f"\n[{source}] {msg_type.upper()}: {command} (seq={seq})") | ||
|
|
||
| if msg_type == "request": | ||
| args = message.get("arguments", {}) | ||
| if args: | ||
| print(f" Arguments: {json.dumps(args, indent=2)}") | ||
| elif msg_type == "response": | ||
| success = message.get("success", False) | ||
| req_seq = message.get("request_seq", 0) | ||
| print(f" Success: {success}, Request Seq: {req_seq}") | ||
| body = message.get("body") | ||
| if body: | ||
| print(f" Body: {json.dumps(body, indent=2)}") | ||
| msg = message.get("message") | ||
| if msg: | ||
| print(f" Message: {msg}") | ||
| elif msg_type == "event": | ||
| body = message.get("body", {}) | ||
| if body: | ||
| print(f" Body: {json.dumps(body, indent=2)}") | ||
|
|
||
| def send_raw_data(self, sock, data): | ||
| """Send raw data to socket.""" | ||
| try: | ||
| sock.send(data) | ||
| except Exception as e: | ||
| print(f"Error sending data: {e}") | ||
|
|
||
| def cleanup(self): | ||
| """Clean up sockets.""" | ||
| if self.client_sock: | ||
| self.client_sock.close() | ||
| if self.server_sock: | ||
| self.server_sock.close() | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| parser = argparse.ArgumentParser(description="DAP protocol monitor proxy") | ||
| parser.add_argument( | ||
| "--target-host", | ||
| "--th", | ||
| default="127.0.0.1", | ||
| help="Target debugpy host (default: 127.0.0.1)", | ||
| ) | ||
| parser.add_argument( | ||
| "--target-port", "--tp", type=int, default=5678, help="Target debugpy port (default: 5678)" | ||
| ) | ||
| parser.add_argument( | ||
| "--listen-port", | ||
| "--lp", | ||
| type=int, | ||
| default=5679, | ||
| help="Port to listen for VS Code (default: 5679)", | ||
| ) | ||
| args = parser.parse_args() | ||
|
|
||
| monitor = DAPMonitor( | ||
| listen_port=args.listen_port, target_host=args.target_host, target_port=args.target_port | ||
| ) | ||
| monitor.start() | ||
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"type": "debugpy",
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, yeah I fixed that in the examples file, missed it here