Skip to content
Open
Show file tree
Hide file tree
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 Jun 11, 2025
9adb886
test_vscode: Add global variables to show vaiable tracking and hover.
Josverl Jun 12, 2025
3ed2d89
debugpy: Improve variable retrievals.
Josverl Jun 12, 2025
c4202e4
dap_monitor: Exit session on debugger disconnect.
Josverl Jun 12, 2025
5d491e0
debugpy: Fix VS Code path mapping to prevent read-only file copies.
pi-anl Jun 16, 2025
c35c6be
debugpy: Enhance PDB adapter with special variable processing.
Josverl Jun 18, 2025
5d081a5
debugpy/dap_monitor: Add cli for target and ports.
Josverl Jun 19, 2025
f5d2977
debugpy/debug_session: Detect bare metal ports.
Josverl Jun 19, 2025
19883c1
debugpy : Format code.
Josverl Jun 19, 2025
2eb9cb5
debugpy : Decode debugger IP address on connect.
Josverl Jun 24, 2025
bb6dc8b
debugpy: Add type hints and improve path mapping logic in PDB adapter
Josverl Jun 29, 2025
756d174
debugpy: Enhance path mapping handling in PDB adapter and debug session.
Josverl Jun 29, 2025
14b2e27
debugpy: Enhance breakpoint handling and path mapping in PdbAdapter
Josverl Jun 30, 2025
817f5ec
debugpy: Refactor breakpoint storage to use sets for improved efficiency
Josverl Jun 30, 2025
26c6e3a
debugpy: Add complex variable handling and caching.
Josverl Jun 30, 2025
16de387
debugpy: Format code.
Josverl Jun 30, 2025
3c5fb37
debugpy : Optimize memory management and performance in PDB adapter.
Josverl Jun 30, 2025
49005b4
Merge branch 'pdb_support/perf2' into debugpy/performance
Josverl Jun 30, 2025
3435c93
revert: Overly complex variable preview.
Josverl Jun 30, 2025
5c41be6
debugpy: Performance improvements.
Josverl Jul 1, 2025
3bf4761
debugpy: Performance - aviod method access.
Josverl Jul 1, 2025
b45fba4
py-ecosys/debugpy: Add pause functionality.
Josverl Jul 1, 2025
2f8e9b8
python-ecosys/debugpy: Add set Variable functionality.
Josverl Jul 1, 2025
8a45947
python-ecosys/debugpy: Add local variable modification while debugging.
Josverl Jul 1, 2025
80ef022
python-ecosys/debugpy: Format code with ruff.
Josverl Jul 1, 2025
9fb263a
python-ecosys/debugpy: Code cleanup.
Josverl Jul 1, 2025
1b49992
pdbadapeter: fixup "Special".
Josverl Jul 14, 2025
61c8905
debugpy: reassemble DAP messages split across recv() calls
pi-anl Jul 5, 2026
e359a95
debugpy: add wait_for_client, capability probe, read-only locals
pi-anl Jul 5, 2026
4fabcb3
debugpy: Execute statements from repl and clipboard evaluate contexts.
pi-anl Jul 14, 2026
0c13480
debugpy: Return the bound endpoint from listen() before accepting.
pi-anl Aug 5, 2026
3432190
debugpy: Stop nested message pumps from clobbering the socket timeout.
pi-anl Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions python-ecosys/debugpy/README.md
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",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"type": "debugpy",

Copy link
Copy Markdown
Contributor Author

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

"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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this flag MICROPY_PY_SYS_SETTRACE conflicts with the referenced PR where this is already set unconditionally

```

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.
199 changes: 199 additions & 0 deletions python-ecosys/debugpy/dap_monitor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
#!/usr/bin/env python3

Check failure on line 1 in python-ecosys/debugpy/dap_monitor.py

View workflow job for this annotation

GitHub Actions / ruff

Ruff (EXE001)

python-ecosys/debugpy/dap_monitor.py:1:1: EXE001 Shebang is present but file is not executable
"""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()
Loading
Loading