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:
michael
2026-01-06 13:45:29 -08:00
parent 1da6730735
commit 4f35df1781
323 changed files with 98287 additions and 1195 deletions

View File

@@ -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)
)