Add smart port conflict detection and comprehensive documentation update
## New Features: - Smart port conflict detection with process identification - Enhanced serial_connect with automatic conflict prevention - New serial_check_port tool for diagnosing port issues - Professional error messages with specific solutions ## Enhancements: - Updated README with complete 70+ tool inventory - Added MCP resources documentation (8 resources) - Enhanced WireViz templates (fixed YAML bugs) - Comprehensive testing and validation ## Technical: - New PortConflictDetector class with lsof/fuser integration - Process-specific conflict resolution suggestions - Graceful fallbacks when detection tools unavailable - Memory-safe circular buffer architecture highlighted ## Documentation: - Updated badge: 60+ → 70+ tools - Complete tool reference with categories - Smart features section added - Professional positioning and capabilities showcase
This commit is contained in:
parent
101e3bb0a8
commit
4ad3b87ca8
165
README.md
165
README.md
@ -6,7 +6,7 @@
|
|||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://pypi.org/project/mcp-arduino/)
|
[](https://pypi.org/project/mcp-arduino/)
|
||||||
[](https://www.python.org/downloads/)
|
[](https://www.python.org/downloads/)
|
||||||
[](https://git.supported.systems/MCP/mcp-arduino)
|
[](https://git.supported.systems/MCP/mcp-arduino)
|
||||||
|
|
||||||
**The Arduino development server that speaks your language.**
|
**The Arduino development server that speaks your language.**
|
||||||
|
|
||||||
@ -235,6 +235,21 @@ Claude: → serial_read with cursor navigation
|
|||||||
✓ Memory usage: still just 10MB (fixed size)
|
✓ Memory usage: still just 10MB (fixed size)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 🆕 **Smart Port Conflict Detection**
|
||||||
|
No more mysterious "device busy" errors - know exactly what's using your ports:
|
||||||
|
```
|
||||||
|
You: "Connect to my Arduino"
|
||||||
|
Claude: → serial_check_port /dev/ttyUSB0
|
||||||
|
❌ Port conflict detected!
|
||||||
|
|
||||||
|
🔧 Arduino IDE (PID 12345) is using this port
|
||||||
|
💡 Solution: Close Arduino IDE Serial Monitor
|
||||||
|
💡 Or use: kill 12345
|
||||||
|
|
||||||
|
→ serial_connect with automatic conflict prevention
|
||||||
|
✓ Connected successfully after conflict resolved
|
||||||
|
```
|
||||||
|
|
||||||
### 🔍 **Auto-Detect Everything**
|
### 🔍 **Auto-Detect Everything**
|
||||||
No more guessing board types or ports:
|
No more guessing board types or ports:
|
||||||
```
|
```
|
||||||
@ -273,14 +288,158 @@ Claude: → arduino_debug_start
|
|||||||
|
|
||||||
## 📦 What You Get
|
## 📦 What You Get
|
||||||
|
|
||||||
**60+ Tools** organized into logical groups:
|
**70+ Professional Tools** organized into logical groups:
|
||||||
- **Sketch Operations**: Create, read, write, compile, upload
|
- **Sketch Operations**: Create, read, write, compile, upload
|
||||||
- **Library Management**: Search, install, dependency resolution
|
- **Library Management**: Search, install, dependency resolution
|
||||||
- **Board Management**: Detection, configuration, core installation
|
- **Board Management**: Detection, configuration, core installation
|
||||||
- **Serial Monitoring**: Memory-safe buffering, cursor pagination
|
- **Serial Monitoring**: Memory-safe buffering, cursor pagination, conflict detection
|
||||||
- **Debugging**: GDB integration, breakpoints, memory inspection
|
- **Debugging**: GDB integration, breakpoints, memory inspection
|
||||||
- **Project Templates**: WiFi, sensor, serial, blink patterns
|
- **Project Templates**: WiFi, sensor, serial, blink patterns
|
||||||
- **Circuit Diagrams**: Generate wiring diagrams from descriptions
|
- **Circuit Diagrams**: Generate wiring diagrams from descriptions
|
||||||
|
- **Client Debugging**: MCP capability detection and troubleshooting
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 Complete Tool & Resource Reference
|
||||||
|
|
||||||
|
### 📝 **Arduino Sketch Management (8 tools)**
|
||||||
|
- `arduino_create_sketch` - Create new sketch with boilerplate code
|
||||||
|
- `arduino_list_sketches` - List all Arduino sketches
|
||||||
|
- `arduino_read_sketch` - Read sketch file contents
|
||||||
|
- `arduino_write_sketch` - Write/update sketch files
|
||||||
|
- `arduino_compile_sketch` - Compile sketch without uploading
|
||||||
|
- `arduino_upload_sketch` - Compile and upload to board
|
||||||
|
- `arduino_sketch_new` - Create sketch from templates (blink, sensor, wifi, etc.)
|
||||||
|
- `arduino_sketch_archive` - Create archive of sketch for sharing
|
||||||
|
|
||||||
|
### 📚 **Library Management (12 tools)**
|
||||||
|
- `arduino_search_libraries` - Search Arduino library index
|
||||||
|
- `arduino_install_library` - Install library from index
|
||||||
|
- `arduino_uninstall_library` - Remove installed library
|
||||||
|
- `arduino_list_library_examples` - List examples from library
|
||||||
|
- `arduino_lib_deps` - Check library dependencies and compatibility
|
||||||
|
- `arduino_lib_download` - Download libraries without installing
|
||||||
|
- `arduino_lib_install_missing` - Install all missing dependencies automatically
|
||||||
|
- `arduino_lib_examples` - List examples from installed libraries
|
||||||
|
- `arduino_lib_list` - List installed libraries with version info
|
||||||
|
- `arduino_lib_upgrade` - Upgrade libraries to latest versions
|
||||||
|
- `arduino_update_index` - Update libraries and boards index
|
||||||
|
- `arduino_outdated` - List outdated libraries and cores
|
||||||
|
|
||||||
|
### 🔧 **Board Management (11 tools)**
|
||||||
|
- `arduino_list_boards` - List connected Arduino boards
|
||||||
|
- `arduino_search_boards` - Search board definitions
|
||||||
|
- `arduino_install_core` - Install board support packages
|
||||||
|
- `arduino_list_cores` - List installed cores
|
||||||
|
- `arduino_update_cores` - Update all cores to latest
|
||||||
|
- `arduino_install_esp32` - Install ESP32 board support with proper config
|
||||||
|
- `arduino_board_attach` - Attach board to sketch persistently
|
||||||
|
- `arduino_board_details` - Get detailed board information and properties
|
||||||
|
- `arduino_board_identify` - Auto-detect board type from connected port
|
||||||
|
- `arduino_board_listall` - List all available boards from installed cores
|
||||||
|
- `arduino_board_search_online` - Search for boards in online index
|
||||||
|
|
||||||
|
### 🐛 **Debug Tools (15 tools)**
|
||||||
|
- `arduino_debug_start` - Start GDB debug session
|
||||||
|
- `arduino_debug_stop` - Stop debug session and cleanup
|
||||||
|
- `arduino_debug_interactive` - Interactive debugging with AI assistance
|
||||||
|
- `arduino_debug_break` - Set breakpoints in code
|
||||||
|
- `arduino_debug_run` - Run/continue/step execution
|
||||||
|
- `arduino_debug_print` - Print variable values/expressions
|
||||||
|
- `arduino_debug_backtrace` - Show call stack
|
||||||
|
- `arduino_debug_watch` - Monitor variable changes
|
||||||
|
- `arduino_debug_memory` - Examine memory contents at addresses
|
||||||
|
- `arduino_debug_registers` - Show CPU register values
|
||||||
|
- `arduino_debug_list_breakpoints` - List all active breakpoints
|
||||||
|
- `arduino_debug_delete_breakpoint` - Delete specific or all breakpoints
|
||||||
|
- `arduino_debug_enable_breakpoint` - Enable/disable breakpoints
|
||||||
|
- `arduino_debug_condition_breakpoint` - Add conditional breakpoints
|
||||||
|
- `arduino_debug_save_breakpoints` - Save breakpoints to file
|
||||||
|
- `arduino_debug_restore_breakpoints` - Restore saved breakpoints
|
||||||
|
|
||||||
|
### 📡 **Serial Monitor Tools (15 tools) - 🆕 Enhanced with Smart Conflict Detection**
|
||||||
|
- `serial_check_port` - **🆕 Check port availability and detect conflicts**
|
||||||
|
- `serial_connect` - **🆕 Enhanced connection with automatic conflict detection**
|
||||||
|
- `serial_disconnect` - Disconnect from serial port
|
||||||
|
- `serial_send` - Send data/commands to serial port
|
||||||
|
- `serial_read` - Read data with cursor-based pagination
|
||||||
|
- `serial_list_ports` - List available serial ports (with Arduino detection)
|
||||||
|
- `serial_clear_buffer` - Clear buffered serial data
|
||||||
|
- `serial_reset_board` - Reset Arduino board (DTR/RTS/1200bps methods)
|
||||||
|
- `serial_monitor_state` - Get current monitor state and statistics
|
||||||
|
- `serial_cursor_info` - Get detailed cursor information
|
||||||
|
- `serial_list_cursors` - List all active cursors
|
||||||
|
- `serial_delete_cursor` - Delete specific cursor
|
||||||
|
- `serial_cleanup_cursors` - Remove invalid/stale cursors
|
||||||
|
- `serial_buffer_stats` - Get detailed circular buffer statistics
|
||||||
|
- `serial_resize_buffer` - Resize circular buffer (100-1M entries)
|
||||||
|
|
||||||
|
### 🎨 **WireViz Circuit Diagrams (2 tools)**
|
||||||
|
- `wireviz_generate_from_yaml` - Create circuit diagram from YAML definition
|
||||||
|
- `wireviz_generate_from_description` - **AI-powered circuit generation from natural language**
|
||||||
|
|
||||||
|
### ⚙️ **Advanced System Tools (13 tools)**
|
||||||
|
- `arduino_compile_advanced` - Advanced compilation with custom build properties
|
||||||
|
- `arduino_size_analysis` - Analyze binary size and memory usage breakdown
|
||||||
|
- `arduino_export_compiled_binary` - Export compiled binaries to custom location
|
||||||
|
- `arduino_cache_clean` - Clean Arduino build cache
|
||||||
|
- `arduino_build_show_properties` - Show all build properties for board
|
||||||
|
- `arduino_burn_bootloader` - Burn bootloader using external programmer
|
||||||
|
- `arduino_config_init` - Initialize Arduino CLI configuration
|
||||||
|
- `arduino_config_get` - Get specific configuration values
|
||||||
|
- `arduino_config_set` - Set configuration values
|
||||||
|
- `arduino_config_dump` - Dump entire Arduino CLI configuration
|
||||||
|
- `arduino_monitor_advanced` - Use Arduino CLI's built-in advanced serial monitor
|
||||||
|
- `arduino_show_directories` - Show current directory configuration and MCP roots
|
||||||
|
- `arduino_sketch_archive` - Create distributable archives of sketches
|
||||||
|
|
||||||
|
### 🔍 **Client Debug Tools (5 tools)**
|
||||||
|
- `client_debug_info` - Show comprehensive MCP client debug information
|
||||||
|
- `client_capability_check` - Test specific client capabilities (sampling, etc.)
|
||||||
|
- `client_declared_capabilities` - Show what capabilities client declares
|
||||||
|
- `client_test_sampling` - Test client sampling functionality with simple prompt
|
||||||
|
- `client_fix_capabilities` - Suggest fixes for common capability issues
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 **MCP Resources (8 resources)**
|
||||||
|
|
||||||
|
Real-time information resources that Claude can access to understand your current Arduino environment:
|
||||||
|
|
||||||
|
### 🚀 **Development Resources**
|
||||||
|
- **`arduino://sketches`** - List all Arduino sketches with sizes and paths
|
||||||
|
- **`arduino://libraries`** - List all installed Arduino libraries with versions
|
||||||
|
- **`arduino://boards`** - List all connected Arduino boards with details
|
||||||
|
|
||||||
|
### 🔧 **Active Session Monitoring**
|
||||||
|
- **`arduino://debug/sessions`** - List all active debug sessions with status
|
||||||
|
- **`arduino://serial/state`** - Get current serial monitor state and connections
|
||||||
|
|
||||||
|
### 📚 **Documentation & Configuration**
|
||||||
|
- **`wireviz://instructions`** - WireViz usage instructions and circuit examples
|
||||||
|
- **`server://info`** - Complete server configuration and capabilities overview
|
||||||
|
- **`arduino://roots`** - Current MCP roots configuration and directory setup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 **Smart Features**
|
||||||
|
|
||||||
|
### 🆕 **Intelligent Port Conflict Detection**
|
||||||
|
- **Process Identification**: Recognizes Arduino IDE, PlatformIO, terminal programs
|
||||||
|
- **Specific Solutions**: Tailored advice for each type of conflicting application
|
||||||
|
- **Command Suggestions**: Provides exact kill commands and troubleshooting steps
|
||||||
|
- **Graceful Fallbacks**: Works even when detection tools are unavailable
|
||||||
|
|
||||||
|
### 🧠 **Memory-Safe Architecture**
|
||||||
|
- **Circular Buffer**: Fixed 10MB memory usage regardless of runtime duration
|
||||||
|
- **Cursor Pagination**: Navigate through millions of serial entries efficiently
|
||||||
|
- **Auto-Cleanup**: Removes stale cursors and manages buffer health
|
||||||
|
|
||||||
|
### 🔗 **MCP Roots Integration**
|
||||||
|
- **Automatic Detection**: Uses client-provided project directories
|
||||||
|
- **Smart Selection**: Prefers Arduino-specific directories
|
||||||
|
- **Environment Override**: `MCP_SKETCH_DIR` for custom locations
|
||||||
|
- **Multi-Client Support**: Works across different MCP clients
|
||||||
|
|
||||||
## 🎓 Perfect For
|
## 🎓 Perfect For
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ from .arduino_sketch import ArduinoSketch
|
|||||||
from .circular_buffer import CircularSerialBuffer
|
from .circular_buffer import CircularSerialBuffer
|
||||||
from .client_capabilities import ClientCapabilitiesInfo
|
from .client_capabilities import ClientCapabilitiesInfo
|
||||||
from .client_debug import ClientDebugInfo
|
from .client_debug import ClientDebugInfo
|
||||||
|
from .port_checker import PortConflictDetector, port_checker
|
||||||
from .serial_manager import SerialConnectionManager
|
from .serial_manager import SerialConnectionManager
|
||||||
from .wireviz import WireViz
|
from .wireviz import WireViz
|
||||||
|
|
||||||
@ -19,4 +20,6 @@ __all__ = [
|
|||||||
"ClientCapabilitiesInfo",
|
"ClientCapabilitiesInfo",
|
||||||
"SerialConnectionManager",
|
"SerialConnectionManager",
|
||||||
"CircularSerialBuffer",
|
"CircularSerialBuffer",
|
||||||
|
"PortConflictDetector",
|
||||||
|
"port_checker",
|
||||||
]
|
]
|
||||||
|
@ -12,6 +12,7 @@ from pydantic import Field
|
|||||||
|
|
||||||
from .circular_buffer import CircularSerialBuffer, SerialDataType
|
from .circular_buffer import CircularSerialBuffer, SerialDataType
|
||||||
from .serial_manager import SerialConnectionManager
|
from .serial_manager import SerialConnectionManager
|
||||||
|
from .port_checker import port_checker
|
||||||
|
|
||||||
# Use CircularSerialBuffer directly
|
# Use CircularSerialBuffer directly
|
||||||
SerialDataBuffer = CircularSerialBuffer
|
SerialDataBuffer = CircularSerialBuffer
|
||||||
@ -97,6 +98,49 @@ class ArduinoSerial(MCPMixin):
|
|||||||
{json.dumps(state['connections'], indent=2)}
|
{json.dumps(state['connections'], indent=2)}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@mcp_tool(name="serial_check_port", description="Check if a serial port is available or detect conflicts")
|
||||||
|
async def check_port(
|
||||||
|
self,
|
||||||
|
port: str = Field(..., description="Serial port path to check (e.g., /dev/ttyUSB0)"),
|
||||||
|
ctx: Context = None
|
||||||
|
) -> dict:
|
||||||
|
"""Check serial port availability and detect conflicts"""
|
||||||
|
try:
|
||||||
|
usage = await port_checker.check_port_usage(port)
|
||||||
|
|
||||||
|
if usage is None:
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"available": True,
|
||||||
|
"port": port,
|
||||||
|
"message": f"✅ Port {port} is available for connection"
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
conflict_message = port_checker.generate_conflict_message(port, usage)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"available": False,
|
||||||
|
"port": port,
|
||||||
|
"conflicts": [
|
||||||
|
{
|
||||||
|
"pid": proc.pid,
|
||||||
|
"process_name": proc.process_name,
|
||||||
|
"command": proc.command,
|
||||||
|
"user": proc.user,
|
||||||
|
"description": str(proc)
|
||||||
|
}
|
||||||
|
for proc in usage
|
||||||
|
],
|
||||||
|
"message": conflict_message,
|
||||||
|
"suggestions": "Use the provided commands to resolve conflicts before connecting"
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Error checking port {port}: {str(e)}",
|
||||||
|
"port": port
|
||||||
|
}
|
||||||
|
|
||||||
@mcp_tool(name="serial_connect", description="Connect to a serial port for monitoring")
|
@mcp_tool(name="serial_connect", description="Connect to a serial port for monitoring")
|
||||||
async def connect(
|
async def connect(
|
||||||
self,
|
self,
|
||||||
@ -110,12 +154,43 @@ class ArduinoSerial(MCPMixin):
|
|||||||
dsrdtr: bool = Field(False, description="Enable hardware (DSR/DTR) flow control"),
|
dsrdtr: bool = Field(False, description="Enable hardware (DSR/DTR) flow control"),
|
||||||
auto_monitor: bool = Field(True, description="Start monitoring automatically"),
|
auto_monitor: bool = Field(True, description="Start monitoring automatically"),
|
||||||
exclusive: bool = Field(False, description="Disconnect other ports first"),
|
exclusive: bool = Field(False, description="Disconnect other ports first"),
|
||||||
|
force: bool = Field(False, description="Skip port conflict check and attempt connection anyway"),
|
||||||
ctx: Context = None
|
ctx: Context = None
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Connect to a serial port"""
|
"""Connect to a serial port with smart conflict detection"""
|
||||||
if not self._initialized:
|
if not self._initialized:
|
||||||
await self.initialize()
|
await self.initialize()
|
||||||
|
|
||||||
|
# Smart port conflict detection (unless forced)
|
||||||
|
if not force:
|
||||||
|
try:
|
||||||
|
usage = await port_checker.check_port_usage(port)
|
||||||
|
if usage:
|
||||||
|
conflict_message = port_checker.generate_conflict_message(port, usage)
|
||||||
|
self.data_buffer.add_entry(port, "Connection blocked: Port in use", SerialDataType.ERROR)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "Port conflict detected",
|
||||||
|
"port": port,
|
||||||
|
"conflicts": [
|
||||||
|
{
|
||||||
|
"pid": proc.pid,
|
||||||
|
"process_name": proc.process_name,
|
||||||
|
"command": proc.command,
|
||||||
|
"user": proc.user,
|
||||||
|
"description": str(proc)
|
||||||
|
}
|
||||||
|
for proc in usage
|
||||||
|
],
|
||||||
|
"message": conflict_message,
|
||||||
|
"hint": "Use 'serial_check_port' for detailed conflict info, or set 'force=true' to override"
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
# If conflict detection fails, log warning but continue
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logger.warning(f"Port conflict check failed for {port}: {e}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
conn = await self.connection_manager.connect(
|
conn = await self.connection_manager.connect(
|
||||||
port=port,
|
port=port,
|
||||||
@ -146,11 +221,24 @@ class ArduinoSerial(MCPMixin):
|
|||||||
"rtscts": rtscts,
|
"rtscts": rtscts,
|
||||||
"dsrdtr": dsrdtr
|
"dsrdtr": dsrdtr
|
||||||
},
|
},
|
||||||
"state": conn.state.value
|
"state": conn.state.value,
|
||||||
|
"message": f"✅ Successfully connected to {port}"
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.data_buffer.add_entry(port, str(e), SerialDataType.ERROR)
|
# Enhanced error message with suggestions
|
||||||
return {"success": False, "error": str(e)}
|
error_msg = str(e)
|
||||||
|
if "permission denied" in error_msg.lower():
|
||||||
|
error_msg += "\n💡 Try: sudo usermod -a -G dialout $USER (then logout/login)"
|
||||||
|
elif "device or resource busy" in error_msg.lower() or "multiple access" in error_msg.lower():
|
||||||
|
error_msg += f"\n💡 Another process is using {port}. Use 'serial_check_port' to identify it."
|
||||||
|
|
||||||
|
self.data_buffer.add_entry(port, error_msg, SerialDataType.ERROR)
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": error_msg,
|
||||||
|
"port": port,
|
||||||
|
"hint": f"Use 'serial_check_port {port}' to diagnose connection issues"
|
||||||
|
}
|
||||||
|
|
||||||
@mcp_tool(name="serial_disconnect", description="Disconnect from a serial port")
|
@mcp_tool(name="serial_disconnect", description="Disconnect from a serial port")
|
||||||
async def disconnect(
|
async def disconnect(
|
||||||
|
282
src/mcp_arduino_server/components/port_checker.py
Normal file
282
src/mcp_arduino_server/components/port_checker.py
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
"""
|
||||||
|
Smart Port Conflict Detection for Arduino MCP Server
|
||||||
|
Proactively detects port usage conflicts and provides helpful guidance
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PortUsage:
|
||||||
|
"""Information about a process using a port"""
|
||||||
|
pid: int
|
||||||
|
command: str
|
||||||
|
user: str
|
||||||
|
fd_type: str
|
||||||
|
process_name: str
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
return f"{self.process_name} (PID {self.pid}) by {self.user}"
|
||||||
|
|
||||||
|
|
||||||
|
class PortConflictDetector:
|
||||||
|
"""Detects and reports serial port usage conflicts"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.common_arduino_processes = {
|
||||||
|
'arduino': 'Arduino IDE',
|
||||||
|
'arduino-cli': 'Arduino CLI',
|
||||||
|
'platformio': 'PlatformIO',
|
||||||
|
'pio': 'PlatformIO CLI',
|
||||||
|
'esptool': 'ESP Flash Tool',
|
||||||
|
'avrdude': 'AVR Programming Tool',
|
||||||
|
'python': 'Python Script (possibly MCP server)',
|
||||||
|
'minicom': 'Minicom Terminal',
|
||||||
|
'screen': 'GNU Screen',
|
||||||
|
'picocom': 'Picocom Terminal',
|
||||||
|
'cu': 'Unix Terminal',
|
||||||
|
'putty': 'PuTTY',
|
||||||
|
'tio': 'TIO Terminal'
|
||||||
|
}
|
||||||
|
|
||||||
|
async def check_port_usage(self, port: str) -> Optional[List[PortUsage]]:
|
||||||
|
"""
|
||||||
|
Check if a serial port is being used by another process
|
||||||
|
Returns list of processes using the port, or None if available
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Try lsof first (most detailed info)
|
||||||
|
usage = await self._check_with_lsof(port)
|
||||||
|
if usage:
|
||||||
|
return usage
|
||||||
|
|
||||||
|
# Fallback to fuser if lsof fails
|
||||||
|
usage = await self._check_with_fuser(port)
|
||||||
|
if usage:
|
||||||
|
return usage
|
||||||
|
|
||||||
|
# Try direct file access test
|
||||||
|
if await self._test_direct_access(port):
|
||||||
|
# Port is busy but we can't identify the process
|
||||||
|
return [PortUsage(
|
||||||
|
pid=0,
|
||||||
|
command="unknown",
|
||||||
|
user="unknown",
|
||||||
|
fd_type="unknown",
|
||||||
|
process_name="Unknown Process"
|
||||||
|
)]
|
||||||
|
|
||||||
|
return None # Port is available
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error checking port usage for {port}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _check_with_lsof(self, port: str) -> Optional[List[PortUsage]]:
|
||||||
|
"""Check port usage using lsof command"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
['lsof', port],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0 and result.stdout:
|
||||||
|
return self._parse_lsof_output(result.stdout)
|
||||||
|
|
||||||
|
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError):
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _check_with_fuser(self, port: str) -> Optional[List[PortUsage]]:
|
||||||
|
"""Check port usage using fuser command"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
['fuser', port],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=5
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0 and result.stdout.strip():
|
||||||
|
pids = [int(pid.strip()) for pid in result.stdout.split() if pid.strip().isdigit()]
|
||||||
|
return [await self._get_process_info(pid) for pid in pids]
|
||||||
|
|
||||||
|
except (subprocess.TimeoutExpired, FileNotFoundError, subprocess.SubprocessError, ValueError):
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _test_direct_access(self, port: str) -> bool:
|
||||||
|
"""Test if port is accessible by trying to open it briefly"""
|
||||||
|
try:
|
||||||
|
import serial
|
||||||
|
# Very brief test - just try to open and immediately close
|
||||||
|
with serial.Serial(port, timeout=0.1) as ser:
|
||||||
|
pass
|
||||||
|
return False # Successfully opened, so available
|
||||||
|
except (serial.SerialException, PermissionError, FileNotFoundError):
|
||||||
|
# If we can't open it, something else might be using it
|
||||||
|
# But could also be permissions or non-existent port
|
||||||
|
if Path(port).exists():
|
||||||
|
return True # Port exists but can't open - likely in use
|
||||||
|
return False # Port doesn't exist
|
||||||
|
|
||||||
|
def _parse_lsof_output(self, output: str) -> List[PortUsage]:
|
||||||
|
"""Parse lsof output to extract process information"""
|
||||||
|
processes = []
|
||||||
|
lines = output.strip().split('\n')[1:] # Skip header
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
parts = line.split(None, 8) # Split into max 9 parts
|
||||||
|
if len(parts) >= 8:
|
||||||
|
try:
|
||||||
|
processes.append(PortUsage(
|
||||||
|
pid=int(parts[1]),
|
||||||
|
command=parts[0],
|
||||||
|
user=parts[2],
|
||||||
|
fd_type=parts[4],
|
||||||
|
process_name=parts[0]
|
||||||
|
))
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return processes
|
||||||
|
|
||||||
|
async def _get_process_info(self, pid: int) -> PortUsage:
|
||||||
|
"""Get detailed process information for a PID"""
|
||||||
|
try:
|
||||||
|
# Get process name and command from /proc/pid/comm and /proc/pid/cmdline
|
||||||
|
comm_path = f"/proc/{pid}/comm"
|
||||||
|
cmdline_path = f"/proc/{pid}/cmdline"
|
||||||
|
|
||||||
|
process_name = "unknown"
|
||||||
|
command = "unknown"
|
||||||
|
|
||||||
|
if Path(comm_path).exists():
|
||||||
|
with open(comm_path, 'r') as f:
|
||||||
|
process_name = f.read().strip()
|
||||||
|
|
||||||
|
if Path(cmdline_path).exists():
|
||||||
|
with open(cmdline_path, 'r') as f:
|
||||||
|
cmdline = f.read().replace('\0', ' ').strip()
|
||||||
|
command = cmdline if cmdline else process_name
|
||||||
|
|
||||||
|
# Get user from process status
|
||||||
|
user = "unknown"
|
||||||
|
try:
|
||||||
|
result = subprocess.run(['ps', '-p', str(pid), '-o', 'user='],
|
||||||
|
capture_output=True, text=True, timeout=2)
|
||||||
|
if result.returncode == 0:
|
||||||
|
user = result.stdout.strip()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return PortUsage(
|
||||||
|
pid=pid,
|
||||||
|
command=command,
|
||||||
|
user=user,
|
||||||
|
fd_type="chr",
|
||||||
|
process_name=process_name
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
return PortUsage(
|
||||||
|
pid=pid,
|
||||||
|
command="unknown",
|
||||||
|
user="unknown",
|
||||||
|
fd_type="unknown",
|
||||||
|
process_name="unknown"
|
||||||
|
)
|
||||||
|
|
||||||
|
def generate_conflict_message(self, port: str, usage: List[PortUsage]) -> str:
|
||||||
|
"""Generate a helpful error message for port conflicts"""
|
||||||
|
if not usage:
|
||||||
|
return f"Port {port} is available"
|
||||||
|
|
||||||
|
message_parts = [f"❌ **Port {port} is currently in use:**\n"]
|
||||||
|
|
||||||
|
for proc in usage:
|
||||||
|
# Try to identify the type of application
|
||||||
|
app_type = self._identify_application_type(proc.process_name.lower())
|
||||||
|
|
||||||
|
if proc.pid > 0:
|
||||||
|
message_parts.append(f" • **{app_type}** - {proc} (FD: {proc.fd_type})")
|
||||||
|
else:
|
||||||
|
message_parts.append(f" • **{app_type}** - Process details unavailable")
|
||||||
|
|
||||||
|
message_parts.extend([
|
||||||
|
"\n**🔧 Suggested Solutions:**",
|
||||||
|
self._get_solutions_for_processes(usage),
|
||||||
|
"\n**💡 Quick Commands:**"
|
||||||
|
])
|
||||||
|
|
||||||
|
# Add specific commands based on detected processes
|
||||||
|
commands = self._get_suggested_commands(port, usage)
|
||||||
|
message_parts.extend(commands)
|
||||||
|
|
||||||
|
return "\n".join(message_parts)
|
||||||
|
|
||||||
|
def _identify_application_type(self, process_name: str) -> str:
|
||||||
|
"""Identify the type of application using the port"""
|
||||||
|
for pattern, app_name in self.common_arduino_processes.items():
|
||||||
|
if pattern in process_name:
|
||||||
|
return app_name
|
||||||
|
return "Unknown Application"
|
||||||
|
|
||||||
|
def _get_solutions_for_processes(self, usage: List[PortUsage]) -> str:
|
||||||
|
"""Generate process-specific solutions"""
|
||||||
|
solutions = []
|
||||||
|
|
||||||
|
for proc in usage:
|
||||||
|
process_lower = proc.process_name.lower()
|
||||||
|
|
||||||
|
if 'arduino' in process_lower:
|
||||||
|
solutions.append(" 1. Close Arduino IDE serial monitor (Tools → Serial Monitor)")
|
||||||
|
elif 'platformio' in process_lower:
|
||||||
|
solutions.append(" 1. Stop PlatformIO monitor: `pio device monitor --exit`")
|
||||||
|
elif 'python' in process_lower:
|
||||||
|
solutions.append(" 1. Stop the Python script/MCP server using the port")
|
||||||
|
elif any(term in process_lower for term in ['minicom', 'screen', 'picocom', 'cu']):
|
||||||
|
solutions.append(f" 1. Exit the terminal program: {proc.process_name}")
|
||||||
|
elif 'esptool' in process_lower or 'avrdude' in process_lower:
|
||||||
|
solutions.append(" 1. Wait for programming/flashing to complete")
|
||||||
|
else:
|
||||||
|
solutions.append(f" 1. Stop or close {proc.process_name}")
|
||||||
|
|
||||||
|
if not solutions:
|
||||||
|
solutions.append(" 1. Identify and close the application using the port")
|
||||||
|
|
||||||
|
solutions.append(" 2. Unplug and reconnect the Arduino device")
|
||||||
|
solutions.append(" 3. Try a different USB port")
|
||||||
|
|
||||||
|
return "\n".join(solutions)
|
||||||
|
|
||||||
|
def _get_suggested_commands(self, port: str, usage: List[PortUsage]) -> List[str]:
|
||||||
|
"""Generate helpful commands based on detected processes"""
|
||||||
|
commands = []
|
||||||
|
|
||||||
|
valid_pids = [proc.pid for proc in usage if proc.pid > 0]
|
||||||
|
|
||||||
|
if valid_pids:
|
||||||
|
commands.append(f" • **Kill process:** `kill {' '.join(map(str, valid_pids))}`")
|
||||||
|
commands.append(f" • **Force kill:** `kill -9 {' '.join(map(str, valid_pids))}`")
|
||||||
|
|
||||||
|
commands.extend([
|
||||||
|
f" • **Check port:** `lsof {port}`",
|
||||||
|
f" • **List all processes:** `fuser {port}`",
|
||||||
|
" • **List Arduino ports:** Use `serial_list_ports` with `arduino_only=true`"
|
||||||
|
])
|
||||||
|
|
||||||
|
return commands
|
||||||
|
|
||||||
|
|
||||||
|
# Global instance for easy access
|
||||||
|
port_checker = PortConflictDetector()
|
@ -550,7 +550,6 @@ options:
|
|||||||
fontname: arial
|
fontname: arial
|
||||||
bgcolor: white
|
bgcolor: white
|
||||||
color_mode: full
|
color_mode: full
|
||||||
notes: Pull-up resistor (10kΩ) connects D2 to 5V
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _generate_display_template(self, description: str) -> str:
|
def _generate_display_template(self, description: str) -> str:
|
||||||
@ -601,7 +600,6 @@ options:
|
|||||||
fontname: arial
|
fontname: arial
|
||||||
bgcolor: white
|
bgcolor: white
|
||||||
color_mode: full
|
color_mode: full
|
||||||
notes: I2C communication at 0x3C or 0x27 address
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def _clean_yaml_content(self, content: str) -> str:
|
def _clean_yaml_content(self, content: str) -> str:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user