Initial commit - Phase 3/4
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import os
|
||||
|
||||
from .. import config
|
||||
|
||||
@@ -35,7 +36,41 @@ class PM3Worker:
|
||||
"""Import the pm3 module (lazy loading)."""
|
||||
if self._pm3_module is None:
|
||||
try:
|
||||
# The pm3 module should be available after pm3 client installation
|
||||
# Setup Python path for pm3 module
|
||||
# The pm3 module is in proxmark3/client/pyscripts
|
||||
# The _pm3.so is in proxmark3/client/experimental_lib/example_py
|
||||
|
||||
# Try multiple possible locations for pyscripts
|
||||
# The pm3 module is in proxmark3/client/pyscripts
|
||||
possible_pyscripts = [
|
||||
os.path.expanduser("~/.pm3/proxmark3/client/pyscripts"), # Pi install location
|
||||
"/home/dt/.pm3/proxmark3/client/pyscripts", # Pi install (explicit path)
|
||||
os.path.expanduser("~/.pm3/client/pyscripts"), # Alternative structure
|
||||
"/usr/local/share/proxmark3/pyscripts", # System install
|
||||
"/usr/share/proxmark3/pyscripts", # System install alt
|
||||
os.path.join(os.path.dirname(__file__), "../../../.pm3-test/proxmark3/client/pyscripts"), # Dev build
|
||||
]
|
||||
|
||||
pm3_pyscripts = None
|
||||
for path in possible_pyscripts:
|
||||
pm3_py = os.path.join(path, "pm3.py")
|
||||
if os.path.exists(pm3_py):
|
||||
pm3_pyscripts = path
|
||||
break
|
||||
|
||||
if not pm3_pyscripts:
|
||||
raise ImportError(f"Could not find pm3.py in any of: {possible_pyscripts}")
|
||||
|
||||
# experimental_lib is at the same level as pyscripts (client/experimental_lib)
|
||||
pm3_lib = os.path.join(os.path.dirname(pm3_pyscripts), "experimental_lib/example_py")
|
||||
|
||||
# Add to Python path if not already there
|
||||
if pm3_pyscripts not in sys.path:
|
||||
sys.path.insert(0, pm3_pyscripts)
|
||||
if pm3_lib not in sys.path:
|
||||
sys.path.insert(0, pm3_lib)
|
||||
|
||||
# Import pm3 module
|
||||
import pm3
|
||||
self._pm3_module = pm3
|
||||
except ImportError as e:
|
||||
@@ -45,25 +80,29 @@ class PM3Worker:
|
||||
)
|
||||
return self._pm3_module
|
||||
|
||||
async def _connect_internal(self):
|
||||
"""Internal connect without locking (caller must hold lock)."""
|
||||
if self._connected:
|
||||
return
|
||||
|
||||
try:
|
||||
pm3 = await self._import_pm3()
|
||||
|
||||
# Run blocking pm3.pm3() constructor in executor
|
||||
loop = asyncio.get_event_loop()
|
||||
self._device = await loop.run_in_executor(
|
||||
None,
|
||||
pm3.pm3,
|
||||
self.device_path
|
||||
)
|
||||
self._connected = True
|
||||
except Exception as e:
|
||||
raise ConnectionError(f"Failed to connect to PM3 at {self.device_path}: {e}")
|
||||
|
||||
async def connect(self):
|
||||
"""Connect to the Proxmark3 device."""
|
||||
async with self._lock:
|
||||
if self._connected:
|
||||
return
|
||||
|
||||
try:
|
||||
pm3 = await self._import_pm3()
|
||||
|
||||
# Run blocking pm3.open() in executor
|
||||
loop = asyncio.get_event_loop()
|
||||
self._device = await loop.run_in_executor(
|
||||
None,
|
||||
pm3.open,
|
||||
self.device_path
|
||||
)
|
||||
self._connected = True
|
||||
except Exception as e:
|
||||
raise ConnectionError(f"Failed to connect to PM3 at {self.device_path}: {e}")
|
||||
await self._connect_internal()
|
||||
|
||||
async def disconnect(self):
|
||||
"""Disconnect from the Proxmark3 device."""
|
||||
@@ -95,9 +134,9 @@ class PM3Worker:
|
||||
"""
|
||||
async with self._lock:
|
||||
try:
|
||||
# Ensure we're connected
|
||||
# Ensure we're connected (use internal method since we hold the lock)
|
||||
if not self._connected:
|
||||
await self.connect()
|
||||
await self._connect_internal()
|
||||
|
||||
if not self._device:
|
||||
return PM3CommandResult(
|
||||
@@ -110,14 +149,18 @@ class PM3Worker:
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
try:
|
||||
# Use .cmd() method to execute command
|
||||
output = await loop.run_in_executor(
|
||||
None,
|
||||
self._device.cmd,
|
||||
command
|
||||
)
|
||||
# Helper function to run command and get output in same executor call
|
||||
def run_command():
|
||||
try:
|
||||
self._device.console(command)
|
||||
output = self._device.grabbed_output
|
||||
return output
|
||||
except Exception as e:
|
||||
raise Exception(f"Error in run_command: {e}, device type: {type(self._device)}")
|
||||
|
||||
# Execute command and get output
|
||||
output = await loop.run_in_executor(None, run_command)
|
||||
|
||||
# pm3.cmd() returns the output string
|
||||
return PM3CommandResult(
|
||||
success=True,
|
||||
output=str(output) if output else "",
|
||||
@@ -148,6 +191,7 @@ class MockPM3Worker(PM3Worker):
|
||||
"hw version": "Proxmark3 RFID instrument\n client: RRG/Iceman/master/v4.14831",
|
||||
"hw status": "Device: PM3 GENERIC\nBootrom: master/v4.14831\nOS: master/v4.14831",
|
||||
"hw tune": "Measuring antenna characteristics, please wait...\n# LF antenna: 50.00 V @ 125.00 kHz",
|
||||
"hw led": "[+] LED control command executed",
|
||||
}
|
||||
|
||||
async def connect(self):
|
||||
@@ -181,3 +225,128 @@ class MockPM3Worker(PM3Worker):
|
||||
output=f"Mock response for: {command}",
|
||||
error=None
|
||||
)
|
||||
|
||||
|
||||
class SubprocessPM3Worker(PM3Worker):
|
||||
"""PM3 worker using subprocess to call pm3 CLI directly.
|
||||
|
||||
This is a fallback for when the Python SWIG bindings aren't working.
|
||||
It executes pm3 CLI commands via subprocess which is more robust.
|
||||
"""
|
||||
|
||||
def __init__(self, device_path: str = None):
|
||||
super().__init__(device_path)
|
||||
self._pm3_path = None
|
||||
self._find_pm3_executable()
|
||||
|
||||
def _find_pm3_executable(self):
|
||||
"""Find the pm3 executable."""
|
||||
possible_paths = [
|
||||
"/usr/local/bin/pm3",
|
||||
os.path.expanduser("~/.pm3/proxmark3/client/proxmark3"),
|
||||
"/home/dt/.pm3/proxmark3/client/proxmark3",
|
||||
"/usr/bin/pm3",
|
||||
]
|
||||
|
||||
for path in possible_paths:
|
||||
if os.path.exists(path) and os.access(path, os.X_OK):
|
||||
self._pm3_path = path
|
||||
break
|
||||
|
||||
if not self._pm3_path:
|
||||
# Try to find in PATH
|
||||
import shutil
|
||||
pm3_in_path = shutil.which("pm3") or shutil.which("proxmark3")
|
||||
if pm3_in_path:
|
||||
self._pm3_path = pm3_in_path
|
||||
|
||||
async def connect(self):
|
||||
"""Check that pm3 executable exists and device is available."""
|
||||
async with self._lock:
|
||||
if self._connected:
|
||||
return
|
||||
|
||||
if not self._pm3_path:
|
||||
raise ConnectionError("pm3 executable not found")
|
||||
|
||||
if not Path(self.device_path).exists():
|
||||
raise ConnectionError(f"PM3 device not found at {self.device_path}")
|
||||
|
||||
self._connected = True
|
||||
|
||||
async def disconnect(self):
|
||||
"""Mark as disconnected."""
|
||||
async with self._lock:
|
||||
self._connected = False
|
||||
|
||||
async def is_connected(self) -> bool:
|
||||
"""Check if device exists."""
|
||||
return Path(self.device_path).exists() if self.device_path else False
|
||||
|
||||
async def execute_command(self, command: str) -> PM3CommandResult:
|
||||
"""Execute a PM3 command via subprocess.
|
||||
|
||||
Args:
|
||||
command: PM3 command to execute (e.g., "hw version")
|
||||
|
||||
Returns:
|
||||
PM3CommandResult with success status, output, and optional error
|
||||
"""
|
||||
async with self._lock:
|
||||
try:
|
||||
if not self._pm3_path:
|
||||
return PM3CommandResult(
|
||||
success=False,
|
||||
output="",
|
||||
error="pm3 executable not found"
|
||||
)
|
||||
|
||||
# Build command: pm3 -p /dev/ttyACM0 -c "command"
|
||||
cmd = [
|
||||
self._pm3_path,
|
||||
"-p", self.device_path,
|
||||
"-c", command
|
||||
]
|
||||
|
||||
# Run subprocess
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
|
||||
try:
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
process.communicate(),
|
||||
timeout=30.0 # 30 second timeout
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
process.kill()
|
||||
return PM3CommandResult(
|
||||
success=False,
|
||||
output="",
|
||||
error="Command timed out after 30 seconds"
|
||||
)
|
||||
|
||||
output = stdout.decode("utf-8", errors="replace")
|
||||
error_output = stderr.decode("utf-8", errors="replace")
|
||||
|
||||
if process.returncode == 0:
|
||||
return PM3CommandResult(
|
||||
success=True,
|
||||
output=output,
|
||||
error=None
|
||||
)
|
||||
else:
|
||||
return PM3CommandResult(
|
||||
success=False,
|
||||
output=output,
|
||||
error=error_output or f"Command failed with exit code {process.returncode}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return PM3CommandResult(
|
||||
success=False,
|
||||
output="",
|
||||
error=str(e)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user