"""Proxmark3 worker for executing commands.""" import asyncio from dataclasses import dataclass from typing import Optional from pathlib import Path import sys import os from .. import config @dataclass class PM3CommandResult: """Result from a PM3 command execution.""" success: bool output: str error: Optional[str] = None class PM3Worker: """Worker for executing Proxmark3 commands using the built-in pm3 module.""" def __init__(self, device_path: str = None): """Initialize the PM3 worker. Args: device_path: Path to PM3 device (default: from config) """ self.device_path = device_path or config.PM3_DEVICE self._device = None self._connected = False self._lock = asyncio.Lock() self._pm3_module = None async def _import_pm3(self): """Import the pm3 module (lazy loading).""" if self._pm3_module is None: try: # 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: raise ImportError( "pm3 module not found. Ensure Proxmark3 client is installed with Python support. " f"Error: {e}" ) 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: await self._connect_internal() async def disconnect(self): """Disconnect from the Proxmark3 device.""" async with self._lock: if self._device: try: # Close the device connection # Note: pm3 module may not have explicit close, connection closes when object is deleted self._device = None self._connected = False except Exception as e: print(f"Error disconnecting from PM3: {e}") async def is_connected(self) -> bool: """Check if connected to PM3 device.""" if not self._connected: # Try to check if device exists return Path(self.device_path).exists() return self._connected async def execute_command(self, command: str) -> PM3CommandResult: """Execute a Proxmark3 command. Args: command: PM3 command to execute (e.g., "hw version") Returns: PM3CommandResult with success status, output, and optional error """ async with self._lock: try: # Ensure we're connected (use internal method since we hold the lock) if not self._connected: await self._connect_internal() if not self._device: return PM3CommandResult( success=False, output="", error="Not connected to PM3 device" ) # Execute command in executor (blocking call) loop = asyncio.get_event_loop() try: # 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) return PM3CommandResult( success=True, output=str(output) if output else "", error=None ) except Exception as cmd_error: return PM3CommandResult( success=False, output="", error=f"Command execution failed: {cmd_error}" ) except Exception as e: return PM3CommandResult( success=False, output="", error=str(e) ) # Mock PM3 Worker for testing without actual hardware class MockPM3Worker(PM3Worker): """Mock PM3 worker for testing without hardware.""" def __init__(self, device_path: str = None): super().__init__(device_path) self._mock_responses = { "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): """Mock connect.""" self._connected = True async def disconnect(self): """Mock disconnect.""" self._connected = False async def is_connected(self) -> bool: """Mock connection check.""" return True async def execute_command(self, command: str) -> PM3CommandResult: """Mock command execution.""" await asyncio.sleep(0.1) # Simulate command delay # Return mock response if available for cmd_prefix, response in self._mock_responses.items(): if command.startswith(cmd_prefix): return PM3CommandResult( success=True, output=response, error=None ) # Default response for unknown commands return PM3CommandResult( success=True, 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) )