-
Couldn't load subscription status.
- 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’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
c98b355
9adb886
3ed2d89
c4202e4
5d491e0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
This implementation provides a Debug Adapter Protocol (DAP) server that enables VS Code to debug MicroPython code with full breakpoint, stepping, and variable inspection capabilities. Features: - Manual breakpoints via debugpy.breakpoint() - Line breakpoints set from VS Code - Stack trace inspection - Variable scopes (locals/globals) - Source code viewing - Stepping (into/over/out) - Non-blocking architecture for MicroPython's single-threaded environment - Conditional debug logging based on VS Code's logToFile setting Implementation highlights: - Uses MicroPython's sys.settrace() for execution monitoring - Handles path mapping between VS Code and MicroPython - Efficient O(n) fibonacci demo (was O(2^n) recursive) - Compatible with MicroPython's limited frame object attributes - Comprehensive DAP protocol support Files: - debugpy/: Core debugging implementation - test_vscode.py: VS Code integration test - VSCODE_TESTING_GUIDE.md: Setup and usage instructions - dap_monitor.py: Protocol debugging utility Usage: ```python import debugpy debugpy.listen() # Start debug server debugpy.debug_this_thread() # Enable tracing debugpy.breakpoint() # Manual breakpoint ``` 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
- Loading branch information
There are no files selected for viewing
| 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" | ||
|
||
| ``` | ||
|
|
||
| 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. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,162 @@ | ||
| #!/usr/bin/env python3 | ||
| """DAP protocol monitor - sits between VS Code and MicroPython debugpy.""" | ||
|
|
||
| import socket | ||
| import threading | ||
| import json | ||
| import time | ||
| import sys | ||
|
|
||
| class DAPMonitor: | ||
| def __init__(self, listen_port=5679, target_host='127.0.0.1', target_port=5678): | ||
| 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 True: | ||
| 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 | ||
|
|
||
| # Log the message | ||
| try: | ||
| message = json.loads(content.decode('utf-8')) | ||
| 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)}") | ||
|
|
||
| except json.JSONDecodeError: | ||
| print(f"\n[{source}] Invalid JSON: {content}") | ||
|
|
||
| return header + content | ||
|
|
||
| except Exception as e: | ||
| print(f"Error receiving from {source}: {e}") | ||
| return None | ||
|
|
||
| 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__": | ||
| monitor = DAPMonitor() | ||
| monitor.start() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| """MicroPython debugpy implementation. | ||
|
|
||
| A minimal port of debugpy for MicroPython to enable VS Code debugging support. | ||
| This implementation focuses on the core DAP (Debug Adapter Protocol) functionality | ||
| needed for basic debugging operations like breakpoints, stepping, and variable inspection. | ||
| """ | ||
|
|
||
| __version__ = "0.1.0" | ||
|
|
||
| from .public_api import listen, wait_for_client, breakpoint, debug_this_thread | ||
| from .common.constants import DEFAULT_HOST, DEFAULT_PORT | ||
|
|
||
| __all__ = [ | ||
| "listen", | ||
| "wait_for_client", | ||
| "breakpoint", | ||
| "debug_this_thread", | ||
| "DEFAULT_HOST", | ||
| "DEFAULT_PORT", | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| # Common utilities and constants for debugpy |