Skip to content

Commit 2980f00

Browse files
pi-anlclaude
andcommitted
python-ecosys/debugpy: Add VS Code debugging support for MicroPython.
This adds a minimal port of debugpy that enables VS Code debugging of MicroPython applications using the Debug Adapter Protocol (DAP). Features implemented: - DAP protocol support for VS Code integration - Line breakpoints with verification - Step over/into/out debugging operations - Stack trace inspection with frame navigation - Variable inspection (locals and globals scopes) - Expression evaluation in debugging context - Network communication via TCP socket listener The implementation integrates with MicroPython's sys.settrace functionality and provides a familiar debugging experience similar to CPython debugpy. Usage: import debugpy debugpy.listen() # Start debug server debugpy.debug_this_thread() # Enable debugging # Connect VS Code debugger to 127.0.0.1:5678 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent b7c81d7 commit 2980f00

15 files changed

+1448
-0
lines changed

python-ecosys/debugpy/README.md

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# MicroPython debugpy
2+
3+
A minimal implementation of debugpy for MicroPython, enabling VS Code debugging support.
4+
5+
## Features
6+
7+
- Debug Adapter Protocol (DAP) support for VS Code integration
8+
- Basic debugging operations:
9+
- Breakpoints
10+
- Step over/into/out
11+
- Stack trace inspection
12+
- Variable inspection (locals and globals)
13+
- Expression evaluation
14+
- Pause/continue execution
15+
16+
## Requirements
17+
18+
- MicroPython with `sys.settrace` support (enabled in Unix coverage build)
19+
- Socket support for network communication
20+
- JSON support for DAP message parsing
21+
22+
## Usage
23+
24+
### Basic Usage
25+
26+
```python
27+
import debugpy
28+
29+
# Start listening for debugger connections
30+
host, port = debugpy.listen() # Default: 127.0.0.1:5678
31+
print(f"Debugger listening on {host}:{port}")
32+
33+
# Enable debugging for current thread
34+
debugpy.debug_this_thread()
35+
36+
# Your code here...
37+
def my_function():
38+
x = 10
39+
y = 20
40+
result = x + y # Set breakpoint here in VS Code
41+
return result
42+
43+
result = my_function()
44+
print(f"Result: {result}")
45+
46+
# Manual breakpoint
47+
debugpy.breakpoint()
48+
```
49+
50+
### VS Code Configuration
51+
52+
Create a `.vscode/launch.json` file in your project:
53+
54+
```json
55+
{
56+
"version": "0.2.0",
57+
"configurations": [
58+
{
59+
"name": "Attach to MicroPython",
60+
"type": "python",
61+
"request": "attach",
62+
"connect": {
63+
"host": "127.0.0.1",
64+
"port": 5678
65+
},
66+
"pathMappings": [
67+
{
68+
"localRoot": "${workspaceFolder}",
69+
"remoteRoot": "."
70+
}
71+
],
72+
"justMyCode": false
73+
}
74+
]
75+
}
76+
```
77+
78+
### Testing
79+
80+
1. Build the MicroPython Unix coverage port:
81+
```bash
82+
cd ports/unix
83+
make VARIANT=coverage
84+
```
85+
86+
2. Run the test script:
87+
```bash
88+
cd lib/micropython-lib/python-ecosys/debugpy
89+
../../../../ports/unix/build-coverage/micropython test_debugpy.py
90+
```
91+
92+
3. In VS Code, open the debugpy folder and press F5 to attach the debugger
93+
94+
4. Set breakpoints in the test script and observe debugging functionality
95+
96+
## API Reference
97+
98+
### `debugpy.listen(port=5678, host="127.0.0.1")`
99+
100+
Start listening for debugger connections.
101+
102+
**Parameters:**
103+
- `port`: Port number to listen on (default: 5678)
104+
- `host`: Host address to bind to (default: "127.0.0.1")
105+
106+
**Returns:** Tuple of (host, port) actually used
107+
108+
### `debugpy.debug_this_thread()`
109+
110+
Enable debugging for the current thread by installing the trace function.
111+
112+
### `debugpy.breakpoint()`
113+
114+
Trigger a manual breakpoint that will pause execution if a debugger is attached.
115+
116+
### `debugpy.wait_for_client()`
117+
118+
Wait for the debugger client to connect and initialize.
119+
120+
### `debugpy.is_client_connected()`
121+
122+
Check if a debugger client is currently connected.
123+
124+
**Returns:** Boolean indicating connection status
125+
126+
### `debugpy.disconnect()`
127+
128+
Disconnect from the debugger client and clean up resources.
129+
130+
## Architecture
131+
132+
The implementation consists of several key components:
133+
134+
1. **Public API** (`public_api.py`): Main entry points for users
135+
2. **Debug Session** (`server/debug_session.py`): Handles DAP protocol communication
136+
3. **PDB Adapter** (`server/pdb_adapter.py`): Bridges DAP and MicroPython's trace system
137+
4. **Messaging** (`common/messaging.py`): JSON message handling for DAP
138+
5. **Constants** (`common/constants.py`): DAP protocol constants
139+
140+
## Limitations
141+
142+
This is a minimal implementation with the following limitations:
143+
144+
- Single-threaded debugging only
145+
- No conditional breakpoints
146+
- No function breakpoints
147+
- Limited variable inspection (no nested object expansion)
148+
- No step back functionality
149+
- No hot code reloading
150+
- Simplified stepping implementation
151+
152+
## Compatibility
153+
154+
Tested with:
155+
- MicroPython Unix port (coverage variant)
156+
- VS Code with Python extension
157+
- CPython 3.x (for comparison)
158+
159+
## Contributing
160+
161+
This implementation provides a foundation for MicroPython debugging. Contributions are welcome to add:
162+
163+
- Conditional breakpoint support
164+
- Better variable inspection
165+
- Multi-threading support
166+
- Performance optimizations
167+
- Additional DAP features
168+
169+
## License
170+
171+
MIT License - see the MicroPython project license for details.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""MicroPython debugpy implementation.
2+
3+
A minimal port of debugpy for MicroPython to enable VS Code debugging support.
4+
This implementation focuses on the core DAP (Debug Adapter Protocol) functionality
5+
needed for basic debugging operations like breakpoints, stepping, and variable inspection.
6+
"""
7+
8+
__version__ = "0.1.0"
9+
10+
from .public_api import listen, wait_for_client, breakpoint, debug_this_thread
11+
from .common.constants import DEFAULT_HOST, DEFAULT_PORT
12+
13+
__all__ = [
14+
"listen",
15+
"wait_for_client",
16+
"breakpoint",
17+
"debug_this_thread",
18+
"DEFAULT_HOST",
19+
"DEFAULT_PORT",
20+
]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Common utilities and constants for debugpy
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Constants used throughout debugpy."""
2+
3+
# Default networking settings
4+
DEFAULT_HOST = "127.0.0.1"
5+
DEFAULT_PORT = 5678
6+
7+
# DAP message types
8+
MSG_TYPE_REQUEST = "request"
9+
MSG_TYPE_RESPONSE = "response"
10+
MSG_TYPE_EVENT = "event"
11+
12+
# DAP events
13+
EVENT_INITIALIZED = "initialized"
14+
EVENT_STOPPED = "stopped"
15+
EVENT_CONTINUED = "continued"
16+
EVENT_THREAD = "thread"
17+
EVENT_BREAKPOINT = "breakpoint"
18+
EVENT_OUTPUT = "output"
19+
EVENT_TERMINATED = "terminated"
20+
EVENT_EXITED = "exited"
21+
22+
# DAP commands
23+
CMD_INITIALIZE = "initialize"
24+
CMD_LAUNCH = "launch"
25+
CMD_ATTACH = "attach"
26+
CMD_SET_BREAKPOINTS = "setBreakpoints"
27+
CMD_CONTINUE = "continue"
28+
CMD_NEXT = "next"
29+
CMD_STEP_IN = "stepIn"
30+
CMD_STEP_OUT = "stepOut"
31+
CMD_PAUSE = "pause"
32+
CMD_STACK_TRACE = "stackTrace"
33+
CMD_SCOPES = "scopes"
34+
CMD_VARIABLES = "variables"
35+
CMD_EVALUATE = "evaluate"
36+
CMD_DISCONNECT = "disconnect"
37+
38+
# Stop reasons
39+
STOP_REASON_STEP = "step"
40+
STOP_REASON_BREAKPOINT = "breakpoint"
41+
STOP_REASON_EXCEPTION = "exception"
42+
STOP_REASON_PAUSE = "pause"
43+
STOP_REASON_ENTRY = "entry"
44+
45+
# Thread reasons
46+
THREAD_REASON_STARTED = "started"
47+
THREAD_REASON_EXITED = "exited"
48+
49+
# Trace events
50+
TRACE_CALL = "call"
51+
TRACE_LINE = "line"
52+
TRACE_RETURN = "return"
53+
TRACE_EXCEPTION = "exception"
54+
55+
# Scope types
56+
SCOPE_LOCALS = "locals"
57+
SCOPE_GLOBALS = "globals"
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
"""JSON message handling for DAP protocol."""
2+
3+
import json
4+
import socket
5+
from .constants import MSG_TYPE_REQUEST, MSG_TYPE_RESPONSE, MSG_TYPE_EVENT
6+
7+
8+
class JsonMessageChannel:
9+
"""Handles JSON message communication over a socket using DAP format."""
10+
11+
def __init__(self, sock):
12+
self.sock = sock
13+
self.seq = 0
14+
self.closed = False
15+
self._recv_buffer = b""
16+
17+
def send_message(self, msg_type, command=None, **kwargs):
18+
"""Send a DAP message."""
19+
if self.closed:
20+
return
21+
22+
self.seq += 1
23+
message = {
24+
"seq": self.seq,
25+
"type": msg_type,
26+
}
27+
28+
if command:
29+
if msg_type == MSG_TYPE_REQUEST:
30+
message["command"] = command
31+
if kwargs:
32+
message["arguments"] = kwargs
33+
elif msg_type == MSG_TYPE_RESPONSE:
34+
message["command"] = command
35+
message["request_seq"] = kwargs.get("request_seq", 0)
36+
message["success"] = kwargs.get("success", True)
37+
if "body" in kwargs:
38+
message["body"] = kwargs["body"]
39+
if "message" in kwargs:
40+
message["message"] = kwargs["message"]
41+
elif msg_type == MSG_TYPE_EVENT:
42+
message["event"] = command
43+
if kwargs:
44+
message["body"] = kwargs
45+
46+
json_str = json.dumps(message)
47+
content = json_str.encode("utf-8")
48+
header = f"Content-Length: {len(content)}\r\n\r\n".encode("utf-8")
49+
50+
try:
51+
self.sock.send(header + content)
52+
except OSError:
53+
self.closed = True
54+
55+
def send_request(self, command, **kwargs):
56+
"""Send a request message."""
57+
self.send_message(MSG_TYPE_REQUEST, command, **kwargs)
58+
59+
def send_response(self, command, request_seq, success=True, body=None, message=None):
60+
"""Send a response message."""
61+
kwargs = {"request_seq": request_seq, "success": success}
62+
if body is not None:
63+
kwargs["body"] = body
64+
if message is not None:
65+
kwargs["message"] = message
66+
self.send_message(MSG_TYPE_RESPONSE, command, **kwargs)
67+
68+
def send_event(self, event, **kwargs):
69+
"""Send an event message."""
70+
self.send_message(MSG_TYPE_EVENT, event, **kwargs)
71+
72+
def recv_message(self):
73+
"""Receive a DAP message."""
74+
if self.closed:
75+
return None
76+
77+
try:
78+
# Read headers
79+
while b"\r\n\r\n" not in self._recv_buffer:
80+
data = self.sock.recv(1024)
81+
if not data:
82+
self.closed = True
83+
return None
84+
self._recv_buffer += data
85+
86+
header_end = self._recv_buffer.find(b"\r\n\r\n")
87+
header_str = self._recv_buffer[:header_end].decode("utf-8")
88+
self._recv_buffer = self._recv_buffer[header_end + 4:]
89+
90+
# Parse Content-Length
91+
content_length = 0
92+
for line in header_str.split("\r\n"):
93+
if line.startswith("Content-Length:"):
94+
content_length = int(line.split(":", 1)[1].strip())
95+
break
96+
97+
if content_length == 0:
98+
return None
99+
100+
# Read body
101+
while len(self._recv_buffer) < content_length:
102+
data = self.sock.recv(content_length - len(self._recv_buffer))
103+
if not data:
104+
self.closed = True
105+
return None
106+
self._recv_buffer += data
107+
108+
body = self._recv_buffer[:content_length]
109+
self._recv_buffer = self._recv_buffer[content_length:]
110+
111+
# Parse JSON
112+
try:
113+
return json.loads(body.decode("utf-8"))
114+
except (ValueError, UnicodeDecodeError):
115+
return None
116+
117+
except OSError:
118+
self.closed = True
119+
return None
120+
121+
def close(self):
122+
"""Close the channel."""
123+
self.closed = True
124+
try:
125+
self.sock.close()
126+
except OSError:
127+
pass

0 commit comments

Comments
 (0)