> **REFERENCE DOCUMENT**: For current progress and remaining tasks, see [REFACTORING_ROADMAP.md](REFACTORING_ROADMAP.md). > This document contains detailed design decisions and is kept as the authoritative reference for multi-PM3 architecture. # Multi-Proxmark3 Support - Refactoring Plan **Date:** 2025-11-26 **Target Device:** Proxmark3 Easy (Iceman Firmware) **Objective:** Support N Proxmark3 devices connected via USB hub with LED identification **Status:** 65% Implemented - Core device management complete, SessionManager update pending --- ## Executive Summary This plan outlines the refactoring required to transform Dangerous Pi from a single-PM3 platform to a **multi-PM3 platform** capable of: ### Core Capabilities - **Detecting and managing N Proxmark3 Easy devices simultaneously** - **Real-time device hotplug detection** via udev (with polling fallback) - **Visual LED identification** of selected devices for physical identification - **Strict firmware version management** with zero tolerance for mismatches - **Seamless device selection** across REST API, BLE, and frontend interfaces - **Session conflict resolution** with alternative device selection or takeover - **Batch firmware updates** for maintaining device fleet consistency - **Battery-aware operations** with safety checks for firmware flashing ### Key Design Decisions (User-Approved) **Device Management:** - Interface-based auto-naming (ttyACM0, ttyACM1) + user customization - Udev event-driven detection (no polling overhead) - No session persistence across restarts - Graceful "no devices connected" empty state **Firmware Strategy:** - **STRICT version enforcement** - exact match required, no tolerance - **Bundled firmware as primary source** for project stability - Auto-update all devices when Dangerous Pi system upgrades - Battery ≥80% required for bootloader flashing - Dynamic parallel flashing based on AC/battery power **Safety Features:** - Power source detection (AC vs battery) - Battery level monitoring during operations - USB hub overload warnings (>2 devices) - JTAG recovery documentation for bricked devices **User Experience:** - Zero-config device detection - One-click "Update All Devices" for fleet management - Real-time progress tracking for firmware operations - Clear visual indicators for device status (connected, in-use, version mismatch, disabled) --- ## Current Architecture Analysis ### Current State - **Single Device Assumption:** Hardcoded `/dev/ttyACM0` in config - **Single Worker:** One `PM3Worker` instance in service container - **Session Manager:** Designed for single-user, single-device access - **Frontend:** No device selection UI - **BLE:** No device selection support ### Key Components Affected 1. **PM3Worker** (`app/backend/workers/pm3_worker.py`) 2. **PM3Service** (`app/backend/services/pm3_service.py`) 3. **SessionManager** (`app/backend/managers/session_manager.py`) 4. **ServiceContainer** (`app/backend/services/container.py`) 5. **Frontend UI** (all routes) 6. **BLE GATT Server** (`app/backend/ble/gatt_server.py`) 7. **API Endpoints** (`app/backend/api/pm3.py`, `app/backend/api/system.py`) --- ## LED Control Research Summary ### PM3 Easy LED Capabilities **Hardware:** - 4 status LEDs: A, B, C, D (Red, Orange, Green, Red2) - 1 power LED (not controllable) - 1 button **Firmware LED Control Functions:** ```c // From armsrc/util.h void LED(int led, int ms); // Control individual LED void LEDsoff(); // Turn off all LEDs void LEDson(); // Turn on all LEDs void LEDsinvert(); // Invert all LED states // LED Constants #define LED_RED 1 // LED A #define LED_ORANGE 2 // LED B #define LED_GREEN 4 // LED C #define LED_RED2 8 // LED D ``` **Current CLI Status:** - ❌ No built-in `hw led` command in standard PM3 client - ✅ LEDs controllable via firmware code - ✅ Python pm3 module can execute commands - 🔧 **Implementation Required:** Custom command or firmware modification ### LED Identification Strategy **Option A: Firmware Modification (Recommended)** - Add `hw led` command to PM3 client/firmware - Syntax: `hw led --set --duration ` - Example: `hw led --set 15 --duration 1000` (blink all LEDs for 1s) - Pros: Clean, reusable, follows PM3 conventions - Cons: Requires firmware compilation and flashing **Option B: Direct USB Communication** - Send raw USB packets to control LEDs - Bypass PM3 client command interface - Pros: No firmware changes needed - Cons: Complex, fragile, firmware-version dependent **Option C: Workaround with Existing Commands** - Use existing commands that trigger LED patterns - Example: `hw tune` briefly activates LEDs - Pros: No firmware changes - Cons: Inconsistent, slower, not reliable for identification **Recommendation:** Option A with Option C as fallback during development --- ## Firmware Version Management ### Overview With multiple PM3 devices, firmware version synchronization becomes critical. Dangerous Pi must: - Detect firmware version on each device - Compare against expected/local client version - Notify users of mismatches - Provide flashing capabilities - Handle devices safely during firmware operations ### Version Detection Strategy **On Device Discovery:** 1. Query `hw version` command 2. Parse firmware version, bootloader version, client version 3. Compare against local PM3 client version 4. Set device status based on compatibility **Version Information Structure:** ```python @dataclass class PM3FirmwareInfo: """Firmware version information.""" bootrom_version: str # e.g., "v4.14831" os_version: str # e.g., "v4.14831" client_version: str # Local client version compatible: bool # Versions match needs_upgrade: bool # Device firmware older needs_downgrade: bool # Device firmware newer bootloader_outdated: bool # Bootloader needs update ``` ### Version Comparison Logic **Compatibility Rules:** ```python class FirmwareCompatibility: """Firmware version compatibility checker.""" @staticmethod def check_compatibility( device_version: str, client_version: str ) -> CompatibilityResult: """Check if device firmware is compatible with client. Args: device_version: Device firmware version (e.g., "v4.14831") client_version: Local client version (e.g., "v4.14831") Returns: CompatibilityResult with status and recommended action """ # Parse semantic versions device_major, device_minor, device_patch = parse_version(device_version) client_major, client_minor, client_patch = parse_version(client_version) # Major version must match if device_major != client_major: return CompatibilityResult( compatible=False, severity="critical", action="flash_required", message=f"Major version mismatch: {device_version} vs {client_version}" ) # Minor version mismatch is a warning if device_minor != client_minor: return CompatibilityResult( compatible=True, # Works but not ideal severity="warning", action="flash_recommended", message=f"Minor version mismatch: {device_version} vs {client_version}" ) # Patch version difference is acceptable if device_patch != client_patch: return CompatibilityResult( compatible=True, severity="info", action="flash_optional", message=f"Patch version difference: {device_version} vs {client_version}" ) # Perfect match return CompatibilityResult( compatible=True, severity="ok", action="none", message="Firmware versions match" ) ``` ### Device Status Based on Firmware **Device States:** ```python class DeviceStatus(Enum): """Device availability status.""" CONNECTED = "connected" # Ready to use DISCONNECTED = "disconnected" # Not detected IN_USE = "in_use" # Active session ERROR = "error" # Communication error VERSION_MISMATCH = "version_mismatch" # Firmware incompatible FLASHING = "flashing" # Firmware update in progress BOOTLOADER_MODE = "bootloader_mode" # In bootloader, needs flash DISABLED = "disabled" # Mismatch ignored by user ``` **UI Treatment:** - `CONNECTED`: Green indicator, selectable - `VERSION_MISMATCH`: Yellow indicator, show warning banner, offer flash - `DISABLED`: Gray indicator, show "Update firmware to enable" button - `FLASHING`: Blue indicator with progress bar, not selectable - `BOOTLOADER_MODE`: Orange indicator, show "Flash firmware" action ### Firmware Flashing Integration #### Flashing Tools Detection **New File:** `app/backend/utils/pm3_flasher.py` ```python """Proxmark3 firmware flashing utilities.""" import asyncio import subprocess from pathlib import Path from typing import Optional, Callable class PM3Flasher: """Firmware flashing for Proxmark3 devices.""" def __init__(self): self.flash_tools = self._detect_flash_tools() self.firmware_dir = self._find_firmware_directory() def _detect_flash_tools(self) -> dict: """Detect available PM3 flashing tools. Returns: Dict of tool paths: { 'pm3_flash_all': '/usr/bin/pm3-flash-all', 'pm3_client': '/usr/bin/proxmark3', 'flasher': '/usr/bin/flasher' } """ tools = {} # Check for modern pm3-flash-* scripts for tool in ['pm3-flash-all', 'pm3-flash-bootrom', 'pm3-flash-fullimage']: path = shutil.which(tool) if path: tools[tool] = path # Check for proxmark3 client with --flash support pm3_path = shutil.which('proxmark3') if pm3_path: tools['proxmark3'] = pm3_path # Check for legacy flasher tool flasher_path = shutil.which('flasher') if flasher_path: tools['flasher'] = flasher_path return tools def _find_firmware_directory(self) -> Optional[Path]: """Locate firmware files. Standard locations: - /usr/share/proxmark3/firmware/ - /opt/proxmark3/firmware/ - ./firmware/ (development) Returns: Path to firmware directory or None """ candidates = [ Path('/usr/share/proxmark3/firmware'), Path('/opt/proxmark3/firmware'), Path('/usr/local/share/proxmark3/firmware'), Path('./firmware'), ] for path in candidates: if path.exists() and (path / 'fullimage.elf').exists(): return path return None async def flash_device( self, device_path: str, flash_bootrom: bool = False, progress_callback: Optional[Callable[[int, str], None]] = None ) -> FlashResult: """Flash firmware to device. Args: device_path: Device path (e.g., /dev/ttyACM0) flash_bootrom: Also flash bootloader (dangerous!) progress_callback: Callback for progress updates (percent, message) Returns: FlashResult with success status and details """ if not self.firmware_dir: return FlashResult( success=False, error="Firmware files not found. Please install proxmark3 firmware." ) try: # Step 1: Check if device is in bootloader mode in_bootloader = await self._is_bootloader_mode(device_path) if not in_bootloader: # Need to enter bootloader mode if progress_callback: progress_callback(10, "Entering bootloader mode...") await self._enter_bootloader_mode(device_path) await asyncio.sleep(2) # Wait for bootloader # Step 2: Flash bootrom if requested (DANGEROUS!) if flash_bootrom: if progress_callback: progress_callback(20, "Flashing bootloader (this may take a while)...") bootrom_result = await self._flash_bootrom(device_path) if not bootrom_result.success: return FlashResult( success=False, error=f"Bootloader flash failed: {bootrom_result.error}" ) await asyncio.sleep(2) # Step 3: Flash fullimage if progress_callback: progress_callback(60, "Flashing firmware...") fullimage_result = await self._flash_fullimage(device_path, progress_callback) if not fullimage_result.success: return FlashResult( success=False, error=f"Firmware flash failed: {fullimage_result.error}" ) # Step 4: Verify if progress_callback: progress_callback(90, "Verifying firmware...") await asyncio.sleep(2) # Wait for reboot version = await self._verify_flash(device_path) if progress_callback: progress_callback(100, "Flash complete!") return FlashResult( success=True, new_version=version, message="Firmware flashed successfully" ) except Exception as e: return FlashResult( success=False, error=f"Flash failed: {str(e)}" ) async def _flash_fullimage( self, device_path: str, progress_callback: Optional[Callable] = None ) -> FlashResult: """Flash fullimage.elf to device.""" fullimage_path = self.firmware_dir / 'fullimage.elf' if 'proxmark3' in self.flash_tools: # Modern flashing method cmd = [ self.flash_tools['proxmark3'], device_path, '--flash', '--image', str(fullimage_path) ] elif 'pm3-flash-fullimage' in self.flash_tools: # Helper script method cmd = [self.flash_tools['pm3-flash-fullimage']] else: return FlashResult(success=False, error="No flash tool available") # Execute flash command process = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) stdout, stderr = await process.communicate() if process.returncode == 0: return FlashResult(success=True) else: return FlashResult( success=False, error=stderr.decode() if stderr else "Unknown error" ) async def _is_bootloader_mode(self, device_path: str) -> bool: """Check if device is in bootloader mode. Bootloader mode indicators: - Red and yellow LEDs stay lit - Device responds to bootloader commands """ try: # Try to communicate with device # If it responds to normal commands, not in bootloader import pm3 device = pm3.open(device_path) # If this succeeds, device is in normal mode return False except: # Failed to open normally, might be in bootloader # Check for bootloader USB descriptor return True async def _enter_bootloader_mode(self, device_path: str): """Put device into bootloader mode. Methods: 1. Send special command (if firmware supports it) 2. Instruct user to manually enter bootloader """ # TODO: Implement automatic bootloader entry # For now, requires manual intervention raise NotImplementedError( "Automatic bootloader entry not implemented. " "Please manually enter bootloader mode: " "Press and hold button while connecting device." ) ``` ### User Interface for Version Mismatch #### Device Card Enhancement ```tsx // In DeviceSelector component interface Device { // ... existing fields firmware_info: { os_version: string; bootrom_version: string; client_version: string; compatible: boolean; compatibility_status: 'ok' | 'warning' | 'critical'; action: 'none' | 'flash_optional' | 'flash_recommended' | 'flash_required'; }; status: DeviceStatus; } function DeviceCard({ device, onFlash, onIgnore }: DeviceCardProps) { const getStatusColor = () => { if (device.status === 'version_mismatch') return 'var(--color-warning)'; if (device.status === 'disabled') return 'var(--color-text-muted)'; if (device.status === 'connected') return 'var(--color-success)'; // ... etc }; const showVersionWarning = device.firmware_info.compatibility_status !== 'ok'; return (
{/* ... device info ... */} {showVersionWarning && (
⚠ Firmware Version Mismatch
Device: {device.firmware_info.os_version} • Client: {device.firmware_info.client_version}
{device.status !== 'disabled' && (
)} {device.status === 'disabled' && ( )}
)}
); } ``` #### Firmware Flash Dialog **New File:** `app/frontend/app/components/FirmwareFlashDialog.tsx` ```tsx interface FirmwareFlashDialogProps { device: Device; onClose: () => void; } export default function FirmwareFlashDialog({ device, onClose }: Props) { const [flashBootrom, setFlashBootrom] = useState(false); const [flashing, setFlashing] = useState(false); const [progress, setProgress] = useState(0); const [status, setStatus] = useState(''); const [error, setError] = useState(null); const handleFlash = async () => { setFlashing(true); setProgress(0); setError(null); try { const response = await fetch(`/api/pm3/devices/${device.device_id}/flash`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ flash_bootrom: flashBootrom }) }); // Poll for progress const progressInterval = setInterval(async () => { const progressResponse = await fetch( `/api/pm3/devices/${device.device_id}/flash/progress` ); const data = await progressResponse.json(); setProgress(data.progress); setStatus(data.status); if (data.complete) { clearInterval(progressInterval); setFlashing(false); if (data.success) { // Success! setTimeout(() => onClose(), 2000); } else { setError(data.error); } } }, 1000); } catch (err) { setError(`Flash failed: ${err}`); setFlashing(false); } }; return (
e.stopPropagation()}>

Update Firmware

Device: {device.friendly_name || device.device_path}
Current Firmware: {device.firmware_info.os_version}
Target Firmware: {device.firmware_info.client_version}
{!flashing && ( <>
{flashBootrom && (
⚠ Warning: Bootloader flashing can brick your device if interrupted!
)}
)} {flashing && ( <>
{progress}% - {status}
Do not disconnect the device or close this window during flashing.
)} {error && (
{error}
)}
); } ``` ### API Endpoints for Firmware Management **Changes to:** `app/backend/api/pm3.py` ```python # NEW ENDPOINTS @router.post("/devices/{device_id}/flash") async def flash_firmware( device_id: str, request: FlashRequest ): """Flash firmware to device. Args: device_id: Device ID request: Flash options (flash_bootrom, etc.) Returns: Flash job ID for tracking progress """ result = await container.pm3_service.flash_firmware( device_id=device_id, flash_bootrom=request.flash_bootrom ) return { "success": result.success, "job_id": result.data.get("job_id") if result.success else None, "error": result.error.message if not result.success else None } @router.get("/devices/{device_id}/flash/progress") async def get_flash_progress(device_id: str, job_id: str): """Get firmware flash progress.""" result = await container.pm3_service.get_flash_progress( device_id=device_id, job_id=job_id ) return result.data @router.post("/devices/{device_id}/ignore-version-mismatch") async def ignore_version_mismatch(device_id: str): """Mark device as disabled due to version mismatch.""" result = await container.pm3_service.set_device_status( device_id=device_id, status=DeviceStatus.DISABLED ) return {"success": result.success} @router.get("/firmware/info") async def get_local_firmware_info(): """Get local firmware version and availability.""" result = await container.pm3_service.get_local_firmware_info() return { "client_version": result.data.get("client_version"), "firmware_available": result.data.get("firmware_files_found"), "firmware_path": result.data.get("firmware_path"), "can_flash": result.data.get("flash_tools_available") } ``` ### Additional Considerations #### 1. **Bootloader Detection & Safety** **Issue:** Devices in bootloader mode vs. normal mode appear differently on USB **Solution:** ```python # In PM3DeviceManager async def detect_bootloader_devices(self) -> List[PM3Device]: """Detect devices in bootloader mode. Bootloader devices: - May appear at different USB endpoint - Don't respond to normal pm3 commands - Show specific LED pattern (red/yellow on) """ # Check USB devices with bootloader VID/PID # Different from normal operation VID/PID pass ``` **UI Treatment:** - Show bootloader devices separately - Indicate "Ready to flash" status - Don't allow command execution - Provide "Flash Firmware" button #### 2. **Firmware File Management** **Issue:** Where to store/source firmware files? **Options:** **Option A: Use System-Installed Firmware** ```python # Firmware from proxmark3 package installation FIRMWARE_PATHS = [ '/usr/share/proxmark3/firmware/', '/opt/proxmark3/firmware/' ] ``` - Pros: No duplication, matches client version - Cons: Requires proxmark3 package installed **Option B: Bundle Firmware with Dangerous Pi** ``` dangerous-pi/ firmware/ bootrom.elf fullimage.elf version.txt ``` - Pros: Self-contained, always available - Cons: Needs updating when PM3 client updates - Cons: Licensing considerations (GPL) **Option C: Download on Demand** ```python # Download from Proxmark3 releases await download_firmware( version=desired_version, target_dir=FIRMWARE_CACHE ) ``` - Pros: Always up-to-date - Cons: Requires internet - Cons: May be slow **Recommendation:** Option A with Option B as fallback #### 3. **Multi-Device Flash Operations** **Issue:** User has 5 devices, all need updates **Solution:** Batch flash operation ```python async def flash_multiple_devices( device_ids: List[str], flash_bootrom: bool = False, sequential: bool = True # vs parallel ) -> List[FlashResult]: """Flash multiple devices. Args: device_ids: List of device IDs to flash flash_bootrom: Also flash bootloader sequential: Flash one at a time (safer) vs parallel Returns: List of flash results """ if sequential: results = [] for device_id in device_ids: result = await flash_device(device_id, flash_bootrom) results.append(result) await asyncio.sleep(2) # Brief pause between devices return results else: # Parallel flashing (risky - high USB bus load) tasks = [ flash_device(device_id, flash_bootrom) for device_id in device_ids ] return await asyncio.gather(*tasks) ``` **UI:** "Update All Devices" button with batch progress #### 4. **Firmware Rollback/Recovery** **Issue:** Flash fails, device bricked **Solutions:** 1. **Backup before flash** (if possible) 2. **JTAG recovery instructions** for worst case 3. **Bootloader preservation** - avoid flashing bootrom unless necessary 4. **Verification before completion** ```python async def flash_with_safety(device_path: str): """Flash with safety checks and rollback.""" # 1. Verify device is responding await verify_device_communication(device_path) # 2. Read current firmware version (for logs) old_version = await get_firmware_version(device_path) # 3. Flash firmware flash_result = await flash_fullimage(device_path) # 4. Verify new firmware if flash_result.success: await asyncio.sleep(2) new_version = await get_firmware_version(device_path) if new_version is None: return FlashResult( success=False, error="Flash verification failed - device not responding" ) return flash_result ``` #### 5. **Version Mismatch Persistence** **Issue:** User clicks "Ignore" but setting isn't saved **Solution:** Store in database ```sql -- Add to devices table CREATE TABLE IF NOT EXISTS devices ( device_id TEXT PRIMARY KEY, -- ... existing fields version_mismatch_ignored BOOLEAN DEFAULT FALSE, ignored_at TIMESTAMP, ignored_version TEXT -- Which version was ignored ); ``` **Behavior:** - User clicks "Ignore" → Set `version_mismatch_ignored = TRUE` - On next detection, check if version changed - If version same as ignored_version → Keep disabled - If version different → Re-prompt user #### 6. **Client Version Detection** **Issue:** How to determine "correct" firmware version? **Solution:** ```python def get_local_client_version() -> str: """Get version of locally installed proxmark3 client. Methods (in order of preference): 1. Execute `proxmark3 --version` 2. Check package manager (dpkg, rpm, etc.) 3. Parse version file if bundled """ try: result = subprocess.run( ['proxmark3', '--version'], capture_output=True, text=True, timeout=5 ) # Parse output: "Proxmark3 RFID instrument\n client: RRG/Iceman/master/v4.14831" match = re.search(r'v(\d+\.\d+)', result.stdout) if match: return match.group(1) except: pass # Fallback to package manager # ... return "unknown" ``` #### 7. **BLE Firmware Notifications** **Issue:** Users on mobile need firmware update notifications **Solution:** ```python # In BLE GATT Server async def notify_firmware_mismatch(device_id: str, device_info: Device): """Notify BLE clients of firmware mismatch.""" notification = { "type": "firmware_mismatch", "device_id": device_id, "device_version": device_info.firmware_info.os_version, "client_version": device_info.firmware_info.client_version, "severity": device_info.firmware_info.compatibility_status, "action_required": device_info.firmware_info.action } await self._notify_characteristic( PM3CharacteristicUUIDs.FIRMWARE_STATUS, json.dumps(notification).encode('utf-8') ) ``` **Note:** BLE clients can't directly flash firmware (requires USB), but can: - Be notified of mismatch - Be directed to web UI for flashing - See device status #### 8. **Flashing Progress via SSE** **Issue:** Long-running flash operation needs real-time updates **Solution:** Server-Sent Events ```python # In SSE event broadcaster async def broadcast_flash_progress( device_id: str, progress: int, status: str ): """Broadcast flash progress to all connected clients.""" await event_broadcaster.send_event({ "type": "flash_progress", "device_id": device_id, "progress": progress, "status": status }) ``` **Frontend:** ```tsx useEffect(() => { const eventSource = new EventSource('/api/sse/events'); eventSource.addEventListener('flash_progress', (event) => { const data = JSON.parse(event.data); if (data.device_id === selectedDevice) { setFlashProgress(data.progress); setFlashStatus(data.status); } }); return () => eventSource.close(); }, [selectedDevice]); ``` #### 9. **Automatic Version Check on Connect** **Issue:** User connects new device mid-session **Solution:** Automatic device discovery with version check ```python # In PM3DeviceManager async def start_device_monitor(self): """Monitor USB bus for device changes.""" while True: # Scan for devices every 30 seconds await asyncio.sleep(30) current_devices = await self.discover_devices() # Check for new devices for device in current_devices: if device.device_id not in self._known_devices: # New device detected! logger.info(f"New PM3 device detected: {device.device_id}") # Check firmware version compat = check_firmware_compatibility(device) # Notify clients via SSE/BLE await notify_new_device(device, compat) # Check for removed devices # ... ``` #### 10. **Firmware Update Logs** **Issue:** Need audit trail of firmware updates **Solution:** Log all flash operations ```python # Database schema CREATE TABLE IF NOT EXISTS firmware_flash_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_id TEXT NOT NULL, device_path TEXT NOT NULL, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, old_version TEXT, new_version TEXT, flash_bootrom BOOLEAN, success BOOLEAN, error_message TEXT, duration_seconds INTEGER, user_ip TEXT ); ``` **Benefits:** - Troubleshooting flash failures - Audit compliance - Statistics (how often devices need updates) --- ## Refactoring Plan #### 1.1 Create Device Manager **New File:** `app/backend/managers/pm3_device_manager.py` ```python class PM3Device: """Represents a single Proxmark3 device.""" device_id: str # Unique ID (hash of serial + device path) device_path: str # /dev/ttyACM0, /dev/ttyACM1, etc. serial_number: str # USB serial number (if available) friendly_name: str # User-assigned name (e.g., "PM3-Living Room") usb_vid: str # USB Vendor ID (0x9AC4 or 0x502D) usb_pid: str # USB Product ID (0x4B8F or 0x502D) status: DeviceStatus # CONNECTED, DISCONNECTED, IN_USE, ERROR version: str # Firmware version (from hw version) last_seen: datetime # Last detection timestamp worker: PM3Worker # Dedicated worker instance class PM3DeviceManager: """Manages multiple Proxmark3 devices.""" async def discover_devices() -> List[PM3Device]: """Scan USB ports for PM3 devices.""" # 1. Enumerate /dev/ttyACM* devices # 2. Filter by USB VID/PID (0x9AC4:0x4B8F or 0x502D:0x502D) # 3. Query each for hw version # 4. Create PM3Device objects async def get_device(device_id: str) -> Optional[PM3Device]: """Get device by ID.""" async def get_available_devices() -> List[PM3Device]: """Get devices not currently in use.""" async def identify_device(device_id: str, duration_ms: int = 2000): """Blink LEDs on specific device for identification.""" # Send LED control command to device async def update_device_status(): """Refresh device list (handle hotplug).""" ``` **Dependencies:** - `pyudev` or `pyserial.tools.list_ports` for USB enumeration - `glob` for /dev/ttyACM* discovery #### 1.2 Modify PM3Worker **Changes to:** `app/backend/workers/pm3_worker.py` - Remove hardcoded device path from constructor - Add device metadata to worker - Support device-specific initialization ```python class PM3Worker: def __init__(self, device_path: str, device_id: str = None): self.device_path = device_path self.device_id = device_id or device_path # ... existing code ``` --- ### Phase 2: Session Management Refactoring #### 2.1 Update Session Model **Changes to:** `app/backend/managers/session_manager.py` ```python @dataclass class Session: session_id: str device_id: str # NEW: Which PM3 device client_ip: str user_agent: Optional[str] created_at: float last_activity: float class SessionManager: """Manages sessions across multiple devices.""" # Change from single session to dict of sessions per device _active_sessions: Dict[str, Session] = {} # device_id -> Session def has_active_session(self, device_id: str = None) -> bool: """Check if device has active session.""" if device_id is None: # Any active session? return len(self._active_sessions) > 0 return device_id in self._active_sessions async def create_session( self, device_id: str, # NEW: Required client_ip: str, user_agent: Optional[str] = None, force_takeover: bool = False ) -> tuple[bool, Optional[str], Optional[str]]: """Create session for specific device.""" def get_available_devices( self, all_devices: List[PM3Device] ) -> List[PM3Device]: """Filter devices without active sessions.""" return [d for d in all_devices if d.device_id not in self._active_sessions] ``` **Key Changes:** - Multi-device session tracking - Device-specific session validation - Available device filtering #### 2.2 Update Database Schema **Changes to:** `app/backend/models/database.py` ```sql CREATE TABLE IF NOT EXISTS sessions ( session_id TEXT PRIMARY KEY, device_id TEXT NOT NULL, -- NEW device_path TEXT NOT NULL, -- NEW client_ip TEXT, user_agent TEXT, created_at TIMESTAMP, last_activity TIMESTAMP, released_at TIMESTAMP ); CREATE TABLE IF NOT EXISTS devices ( -- NEW TABLE device_id TEXT PRIMARY KEY, device_path TEXT NOT NULL, serial_number TEXT, friendly_name TEXT, usb_vid TEXT, usb_pid TEXT, first_seen TIMESTAMP, last_seen TIMESTAMP, metadata TEXT -- JSON for firmware version, etc. ); ``` --- ### Phase 3: Service Layer Refactoring #### 3.1 Update PM3Service **Changes to:** `app/backend/services/pm3_service.py` ```python class PM3Service: """PM3 service supporting multiple devices.""" def __init__( self, device_manager: PM3DeviceManager, # NEW session_manager: SessionManager ): self.device_manager = device_manager self.session_manager = session_manager # Remove single pm3_worker - now managed by device_manager async def execute_command( self, command: str, device_id: str, # NEW: Required session_id: Optional[str] = None, timeout: Optional[int] = None ) -> PM3ServiceResult: """Execute command on specific device.""" # 1. Validate session for this device # 2. Get device from device_manager # 3. Execute command via device's worker async def get_status( self, device_id: str = None # NEW: Optional (all devices if None) ) -> PM3ServiceResult: """Get status for device(s).""" if device_id is None: # Return status for all devices devices = await self.device_manager.discover_devices() return PM3ServiceResult( success=True, data={ "devices": [ { "device_id": d.device_id, "device_path": d.device_path, "friendly_name": d.friendly_name, "connected": d.status == DeviceStatus.CONNECTED, "in_use": self.session_manager.has_active_session(d.device_id), "version": d.version } for d in devices ] } ) else: # Return status for specific device # ... existing single-device logic async def identify_device( self, device_id: str, duration_ms: int = 2000 ) -> PM3ServiceResult: """Blink LEDs on device for identification.""" await self.device_manager.identify_device(device_id, duration_ms) return PM3ServiceResult(success=True, data={"message": "Device identified"}) async def list_available_devices(self) -> PM3ServiceResult: """Get devices without active sessions.""" all_devices = await self.device_manager.discover_devices() available = self.session_manager.get_available_devices(all_devices) return PM3ServiceResult( success=True, data={"devices": [asdict(d) for d in available]} ) ``` #### 3.2 Update ServiceContainer **Changes to:** `app/backend/services/container.py` ```python class ServiceContainer: def __init__(self): # Create shared managers self._device_manager = PM3DeviceManager() # NEW self._session_manager = SessionManager() self._wifi_manager = WiFiManager() self._update_manager = get_update_manager() # Create services with updated dependencies self._pm3_service = PM3Service( device_manager=self._device_manager, # NEW session_manager=self._session_manager ) # ... rest of services @property def device_manager(self) -> PM3DeviceManager: # NEW """Get device manager instance.""" return self._device_manager ``` --- ### Phase 4: API Endpoint Updates #### 4.1 New Device Endpoints **Changes to:** `app/backend/api/pm3.py` ```python # NEW ENDPOINTS @router.get("/devices", response_model=DevicesResponse) async def list_devices(): """List all detected Proxmark3 devices.""" result = await container.pm3_service.get_status() # Returns list of all devices with status @router.get("/devices/available", response_model=DevicesResponse) async def list_available_devices(): """List devices without active sessions.""" result = await container.pm3_service.list_available_devices() @router.post("/devices/{device_id}/identify") async def identify_device(device_id: str, duration: int = 2000): """Blink LEDs on device for identification.""" result = await container.pm3_service.identify_device( device_id=device_id, duration_ms=duration ) @router.get("/devices/{device_id}/status") async def get_device_status(device_id: str): """Get status for specific device.""" result = await container.pm3_service.get_status(device_id=device_id) # UPDATED ENDPOINTS @router.get("/status", response_model=StatusResponse) async def get_status(device_id: str = None): """Get PM3 status (all devices or specific device).""" result = await container.pm3_service.get_status(device_id=device_id) @router.post("/command", response_model=CommandResponse) async def execute_command(request: CommandRequest): """Execute PM3 command on specific device.""" # CommandRequest now includes device_id field result = await container.pm3_service.execute_command( command=request.command, device_id=request.device_id, # NEW: Required session_id=request.session_id ) ``` #### 4.2 Update Request/Response Models ```python class CommandRequest(BaseModel): command: str device_id: str # NEW: Required session_id: Optional[str] = None class DeviceInfo(BaseModel): device_id: str device_path: str friendly_name: str serial_number: Optional[str] connected: bool in_use: bool version: Optional[str] session_id: Optional[str] # If device has active session class DevicesResponse(BaseModel): success: bool devices: List[DeviceInfo] class StatusResponse(BaseModel): # For single device connected: bool device: DeviceInfo # OR for all devices devices: Optional[List[DeviceInfo]] ``` #### 4.3 Update Session Endpoints **Changes to:** `app/backend/api/system.py` ```python class CreateSessionRequest(BaseModel): device_id: str # NEW: Required force_takeover: bool = False @router.post("/session/create", response_model=CreateSessionResponse) async def create_session(request: Request, body: CreateSessionRequest): """Create session for specific device.""" # Updated to include device_id @router.get("/session/{device_id}/status") async def get_session_status(device_id: str): """Get session status for specific device.""" # NEW: Per-device session status ``` --- ### Phase 5: Frontend Refactoring #### 5.1 Device Selection Component **New File:** `app/frontend/app/components/DeviceSelector.tsx` ```tsx interface DeviceSelectorProps { selectedDeviceId: string | null; onDeviceSelect: (deviceId: string) => void; onIdentify: (deviceId: string) => void; } export default function DeviceSelector({ selectedDeviceId, onDeviceSelect, onIdentify }: DeviceSelectorProps) { const [devices, setDevices] = useState([]); const [identifying, setIdentifying] = useState(null); // Fetch devices every 5 seconds useEffect(() => { const fetchDevices = async () => { const response = await fetch('/api/pm3/devices'); const data = await response.json(); setDevices(data.devices); }; fetchDevices(); const interval = setInterval(fetchDevices, 5000); return () => clearInterval(interval); }, []); const handleIdentify = async (deviceId: string) => { setIdentifying(deviceId); await fetch(`/api/pm3/devices/${deviceId}/identify`, { method: 'POST', body: JSON.stringify({ duration: 2000 }) }); setTimeout(() => setIdentifying(null), 2100); }; return (

Select Proxmark3 Device

{devices.length === 0 && (
No Proxmark3 devices detected. Please connect a device.
)}
{devices.map(device => (
!device.in_use && onDeviceSelect(device.device_id)} >
{device.friendly_name || device.device_path}
{device.device_path} {device.serial_number && ` • SN: ${device.serial_number}`}
{device.connected ? ( ● Connected ) : ( ● Disconnected )} {device.in_use && ( 🔒 In Use )} {device.version && ( {device.version} )}
))}
); } ``` #### 5.2 Update Commands Page **Changes to:** `app/frontend/app/routes/commands.tsx` ```tsx export default function Commands() { const [selectedDeviceId, setSelectedDeviceId] = useState(null); const [sessionId, setSessionId] = useState(null); // Create session when device is selected useEffect(() => { if (selectedDeviceId) { async function createSession() { const response = await fetch('/api/system/session/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ device_id: selectedDeviceId, force_takeover: false }) }); const data = await response.json(); if (data.success) { setSessionId(data.session_id); } else { // Show conflict UI - offer takeover option } } createSession(); } }, [selectedDeviceId]); return (

PM3 Commands

{/* Device Selector */} console.log('Identifying', id)} /> {/* Command Interface (only shown when device selected) */} {selectedDeviceId && sessionId && ( <> {/* Existing command UI */}

Commands for {selectedDeviceId}

{/* ... existing command interface ... */}
)} {selectedDeviceId && !sessionId && (

Session Conflict

This device is currently in use by another session.

)}
); } ``` #### 5.3 Update Dashboard **Changes to:** `app/frontend/app/routes/_index.tsx` ```tsx export default function Dashboard() { const [devices, setDevices] = useState([]); // Show all devices with quick status return (

Dashboard

Proxmark3 Devices ({devices.length})

{devices.map(device => (
{device.connected ? '●' : '○'}
{device.friendly_name || device.device_path}
{device.version}
{device.in_use ? ( In Use ) : ( Available )}
))}
{/* ... rest of dashboard ... */}
); } ``` --- ### Phase 6: BLE GATT Updates #### 6.1 Update BLE Characteristics **Changes to:** `app/backend/ble/characteristics.py` ```python class PM3CharacteristicUUIDs: """UUID constants for PM3 GATT characteristics.""" # Existing UUIDs COMMAND_WRITE = "00002a01-0000-1000-8000-00805f9b34fb" COMMAND_RESULT = "00002a02-0000-1000-8000-00805f9b34fb" STATUS = "00002a03-0000-1000-8000-00805f9b34fb" # NEW UUIDs for multi-device support DEVICES_LIST = "00002a10-0000-1000-8000-00805f9b34fb" # NEW DEVICE_SELECT = "00002a11-0000-1000-8000-00805f9b34fb" # NEW DEVICE_IDENTIFY = "00002a12-0000-1000-8000-00805f9b34fb" # NEW ``` #### 6.2 Update GATT Server Handlers **Changes to:** `app/backend/ble/gatt_server.py` ```python def _register_pm3_characteristics(self): """Register PM3 GATT characteristics.""" self._characteristic_handlers.update({ # Existing characteristics... # NEW: Device management characteristics PM3CharacteristicUUIDs.DEVICES_LIST: CharacteristicHandler( uuid=PM3CharacteristicUUIDs.DEVICES_LIST, properties=["read", "notify"], read_handler=self._handle_devices_list_read, description="List all PM3 devices" ), PM3CharacteristicUUIDs.DEVICE_SELECT: CharacteristicHandler( uuid=PM3CharacteristicUUIDs.DEVICE_SELECT, properties=["write"], write_handler=self._handle_device_select_write, description="Select PM3 device for session" ), PM3CharacteristicUUIDs.DEVICE_IDENTIFY: CharacteristicHandler( uuid=PM3CharacteristicUUIDs.DEVICE_IDENTIFY, properties=["write"], write_handler=self._handle_device_identify_write, description="Identify PM3 device (blink LEDs)" ), }) async def _handle_devices_list_read(self) -> bytes: """Handle device list read via BLE.""" result = await container.pm3_service.get_status() if result.success: return json.dumps(result.data).encode('utf-8') # ... error handling async def _handle_device_identify_write(self, value: bytes) -> Dict[str, Any]: """Handle device identification via BLE.""" data = json.loads(value.decode('utf-8')) device_id = data.get("device_id") duration = data.get("duration_ms", 2000) result = await container.pm3_service.identify_device( device_id=device_id, duration_ms=duration ) return {"success": result.success} async def _handle_pm3_command_write(self, value: bytes) -> Dict[str, Any]: """Handle PM3 command execution via BLE.""" data = json.loads(value.decode('utf-8')) command = data.get("command") device_id = data.get("device_id") # NEW: Required session_id = data.get("session_id") result = await container.pm3_service.execute_command( command=command, device_id=device_id, # NEW: Required session_id=session_id ) # ... rest of handler ``` --- ### Phase 7: LED Identification Implementation #### 7.1 LED Control Command Implementation **Option A: Custom PM3 Client Command (Preferred)** Create wrapper script: `app/backend/utils/pm3_led_control.py` ```python """LED control utilities for Proxmark3.""" import asyncio from typing import Optional # LED bit masks (from armsrc/util.h) LED_RED = 1 # LED A LED_ORANGE = 2 # LED B LED_GREEN = 4 # LED C LED_RED2 = 8 # LED D LED_ALL = 15 # All LEDs class PM3LEDController: """Control Proxmark3 LEDs for device identification.""" @staticmethod async def blink_pattern( device_path: str, pattern: str = "all", duration_ms: int = 2000, blink_count: int = 3 ) -> bool: """Blink LEDs in a specific pattern. Args: device_path: Device path (e.g., /dev/ttyACM0) pattern: "all", "alternating", "chase", or custom LED mask duration_ms: Total duration in milliseconds blink_count: Number of blinks Returns: True if successful """ try: # Import pm3 module import pm3 # Open device device = await asyncio.get_event_loop().run_in_executor( None, pm3.open, device_path ) if pattern == "all": await cls._blink_all(device, duration_ms, blink_count) elif pattern == "alternating": await cls._blink_alternating(device, duration_ms, blink_count) elif pattern == "chase": await cls._blink_chase(device, duration_ms, blink_count) else: # Custom pattern await cls._blink_custom(device, int(pattern), duration_ms, blink_count) return True except Exception as e: print(f"LED control error: {e}") return False @staticmethod async def _blink_all(device, duration_ms: int, count: int): """Blink all LEDs.""" interval_ms = duration_ms // (count * 2) for _ in range(count): # Turn on all LEDs using hw tune (generates LED activity) # OR if hw led command is available: # await device.cmd(f"hw led --set {LED_ALL} --duration {interval_ms}") # Workaround: Use hw tune which briefly activates LEDs await asyncio.get_event_loop().run_in_executor( None, device.cmd, "hw tune --lf --duration 50" # Brief LF tune ) await asyncio.sleep(interval_ms / 1000) # LEDs off (wait) await asyncio.sleep(interval_ms / 1000) @staticmethod async def _blink_alternating(device, duration_ms: int, count: int): """Blink LEDs in alternating pattern (A/C then B/D).""" # Implementation similar to _blink_all but with alternating LEDs pass @staticmethod async def _blink_chase(device, duration_ms: int, count: int): """Chase LEDs A -> B -> C -> D.""" # Implementation with sequential LED activation pass ``` **Option B: Firmware Modification** If implementing custom `hw led` command in PM3 firmware: 1. Add command to `client/src/cmdhw.c`: ```c static int CmdHwLed(const char *Cmd) { uint8_t led_mask = 0; uint16_duration_ms = 1000; // Parse arguments // ... // Send LED command to device SendCommandNG(CMD_LED_CONTROL, &led_mask, sizeof(led_mask)); return PM3_SUCCESS; } ``` 2. Add handler to `armsrc/appmain.c` 3. Rebuild PM3 client and firmware 4. Flash firmware to devices #### 7.2 Integration with Device Manager ```python # In PM3DeviceManager async def identify_device(self, device_id: str, duration_ms: int = 2000): """Blink LEDs on device for identification.""" device = await self.get_device(device_id) if not device: raise ValueError(f"Device {device_id} not found") # Use LED controller success = await PM3LEDController.blink_pattern( device_path=device.device_path, pattern="all", # Or "chase" for cooler effect duration_ms=duration_ms, blink_count=3 ) if not success: raise RuntimeError(f"Failed to identify device {device_id}") ``` --- ## Implementation Timeline ### Sprint 1: Foundation (Week 1) - [ ] Create `PM3DeviceManager` class - [ ] Implement USB device enumeration - [ ] Add device detection tests - [ ] Update database schema ### Sprint 2: Core Refactoring (Week 2) - [ ] Refactor `SessionManager` for multi-device - [ ] Update `PM3Service` with device_id parameter - [ ] Update `ServiceContainer` with new dependencies - [ ] Write migration script for existing sessions ### Sprint 3: API Updates (Week 3) - [ ] Add new device endpoints - [ ] Update existing endpoints with device_id - [ ] Update request/response models - [ ] API integration tests ### Sprint 4: Frontend (Week 4) - [ ] Create `DeviceSelector` component - [ ] Update Commands page - [ ] Update Dashboard - [ ] Add session conflict UI ### Sprint 5: BLE Integration (Week 5) - [ ] Add BLE device characteristics - [ ] Update GATT server handlers - [ ] BLE integration tests ### Sprint 6: LED Identification (Week 6) - [ ] Implement LED control (Option A or B) - [ ] Test LED patterns on hardware - [ ] Integrate with device manager - [ ] Polish UX ### Sprint 7: Testing & Polish (Week 7) - [ ] End-to-end testing with multiple devices - [ ] Performance testing - [ ] Bug fixes - [ ] Documentation updates --- ## Testing Strategy ### Unit Tests ```python # tests/unit/managers/test_pm3_device_manager.py @pytest.mark.asyncio async def test_discover_devices(): """Test device discovery.""" manager = PM3DeviceManager() devices = await manager.discover_devices() assert isinstance(devices, list) # More assertions... @pytest.mark.asyncio async def test_device_identification(): """Test LED identification.""" manager = PM3DeviceManager() # Mock device await manager.identify_device("test-device-id", duration_ms=1000) # Assert LED control was called ``` ### Integration Tests ```python # tests/integration/test_multi_device.py @pytest.mark.asyncio async def test_multiple_devices_session_isolation(): """Test that sessions are isolated per device.""" # Create session on device 1 session1 = await create_session(device_id="dev1") # Create session on device 2 (should succeed) session2 = await create_session(device_id="dev2") assert session1.session_id != session2.session_id assert session1.device_id == "dev1" assert session2.device_id == "dev2" @pytest.mark.asyncio async def test_device_session_conflict(): """Test session conflict on same device.""" # Create session on device 1 session1 = await create_session(device_id="dev1") # Try to create another session on device 1 (should fail) with pytest.raises(SessionConflictError): await create_session(device_id="dev1", force_takeover=False) # With force_takeover (should succeed) session2 = await create_session(device_id="dev1", force_takeover=True) assert session2.session_id != session1.session_id ``` ### Hardware Tests ```python # tests/hardware/test_led_control.py @pytest.mark.hardware @pytest.mark.asyncio async def test_led_blink_all(): """Test LED blinking on real hardware.""" controller = PM3LEDController() success = await controller.blink_pattern( device_path="/dev/ttyACM0", pattern="all", duration_ms=2000, blink_count=3 ) assert success ``` --- ## Migration Strategy ### Backward Compatibility **Phase 1: Dual Mode** - Support both old API (single device) and new API (multi-device) - Old endpoints default to first available device - Add deprecation warnings ```python # Old endpoint (deprecated but functional) @router.post("/command") # No device_id required async def execute_command_legacy(request: CommandRequest): """DEPRECATED: Use /devices/{device_id}/command instead.""" # Get first available device devices = await container.device_manager.discover_devices() if not devices: raise HTTPException(404, "No devices found") # Use first device device_id = devices[0].device_id # Call new implementation return await execute_command_v2( device_id=device_id, request=request ) ``` **Phase 2: Migration** - Update frontend to use new API - Provide migration guide - Update documentation **Phase 3: Deprecation** - Remove old endpoints - Clean up backward compatibility code ### Data Migration ```python # scripts/migrate_sessions_to_multidevice.py async def migrate_sessions(): """Migrate existing sessions to multi-device format.""" # 1. Get current device (assume /dev/ttyACM0) default_device_id = "default-pm3" # 2. Update all sessions in database async with database.get_connection() as conn: await conn.execute(""" UPDATE sessions SET device_id = ?, device_path = '/dev/ttyACM0' WHERE device_id IS NULL """, (default_device_id,)) # 3. Create device entry await device_manager.register_device( device_id=default_device_id, device_path="/dev/ttyACM0" ) ``` --- ## Configuration Changes ### Environment Variables **New Variables:** ```bash # .env # Device Management PM3_AUTO_DISCOVER=true # Auto-discover devices on startup PM3_DISCOVERY_INTERVAL=30 # Re-scan interval (seconds) PM3_DEVICE_TIMEOUT=300 # Mark device as disconnected after N seconds # LED Identification PM3_LED_PATTERN=all # Default LED pattern (all, chase, alternating) PM3_LED_DURATION=2000 # Default LED blink duration (ms) PM3_LED_BLINK_COUNT=3 # Number of blinks # Session Management (per device) SESSION_TIMEOUT=300 # Session timeout (seconds) - unchanged MAX_SESSIONS_PER_DEVICE=1 # Max concurrent sessions per device ``` ### Config File ```python # app/backend/config.py # Device Management PM3_AUTO_DISCOVER = os.getenv("PM3_AUTO_DISCOVER", "true").lower() == "true" PM3_DISCOVERY_INTERVAL = int(os.getenv("PM3_DISCOVERY_INTERVAL", "30")) PM3_DEVICE_TIMEOUT = int(os.getenv("PM3_DEVICE_TIMEOUT", "300")) # USB Identification PM3_USB_VID_PRIMARY = "0x9AC4" # Standard PM3 PM3_USB_PID_PRIMARY = "0x4B8F" PM3_USB_VID_EASY = "0x502D" # PM3 Easy PM3_USB_PID_EASY = "0x502D" # LED Control PM3_LED_PATTERN = os.getenv("PM3_LED_PATTERN", "all") PM3_LED_DURATION = int(os.getenv("PM3_LED_DURATION", "2000")) PM3_LED_BLINK_COUNT = int(os.getenv("PM3_LED_BLINK_COUNT", "3")) ``` --- ## Dependencies ### New Python Packages ``` # requirements.txt # Existing dependencies... # NEW: For USB device enumeration pyudev>=0.24.0 # Linux udev bindings for device detection pyserial>=3.5 # Serial port enumeration (cross-platform) ``` ### Optional Dependencies ``` # requirements-led.txt (if building custom LED control) # For custom firmware compilation (if needed) # arm-none-eabi-gcc (system package, not pip) ``` --- ## Documentation Updates ### New Documentation Files 1. **`MULTI_DEVICE_GUIDE.md`** - How to connect multiple PM3 devices - USB hub recommendations - Device identification workflow - Troubleshooting 2. **`LED_IDENTIFICATION.md`** - LED control technical details - Firmware modification guide (if applicable) - Custom patterns guide 3. **`API_MIGRATION.md`** - Breaking changes - Migration from v1 to v2 API - Code examples ### Updated Documentation 1. **`README.md`** - Multi-device support feature - Updated architecture diagram 2. **`PROJECT_STATUS.md`** - Multi-device refactoring status 3. **`API.md`** (if exists, or create) - Complete API reference with device_id parameter --- ## Risks & Mitigation ### Risk 1: LED Control Not Working on PM3 Easy **Likelihood:** Medium **Impact:** Medium **Mitigation:** - Implement fallback: Use existing commands (hw tune) that trigger LED activity - Alternative: Physical labeling system (stickers with QR codes) - Last resort: Manual device selection by path ### Risk 2: USB Device Enumeration Issues **Likelihood:** Low **Impact:** High **Mitigation:** - Test on multiple Linux distributions - Provide manual device configuration option - Extensive error handling and logging - Document udev rules if needed ### Risk 3: Session Management Complexity **Likelihood:** Medium **Impact:** Medium **Mitigation:** - Comprehensive unit tests - Clear session conflict UI - Session debugging tools - Fallback to single-device mode ### Risk 4: Performance with Many Devices **Likelihood:** Low **Impact:** Low **Mitigation:** - Efficient device polling (only when needed) - Caching device status - Async operations throughout - Performance testing with 10+ devices --- ## Success Criteria ### Must Have (MVP) - ✅ Detect and list N Proxmark3 devices - ✅ Create sessions per device - ✅ Execute commands on specific device - ✅ Basic device selection UI - ✅ Device identification (LED or alternative method) - ✅ Session conflict resolution - ✅ BLE support for multi-device ### Should Have - ✅ Friendly device names - ✅ Device discovery polling - ✅ LED blink patterns (if firmware supports) - ✅ Available vs. in-use device indication - ✅ Auto-select device if only one available ### Nice to Have - ⭐ Device persistence (remember friendly names) -- yes - ⭐ Device statistics (usage time, command count) -- oh, cool, sure - ⭐ Device grouping/tagging -- ok - ⭐ Advanced LED patterns (custom sequences) - ⭐ Device health monitoring -- I am curious as to what this would entail -- elaborate? --- ## Decisions & Implementation Notes ### Core Features - DECIDED ✅ 1. **LED Control Implementation:** ✅ **DECIDED** - **Decision:** Modify PM3 firmware to add `hw led` command - **Notes:** - Document current firmware version and lock to it until MVP complete - Start with workaround (hw tune) for immediate testing - Contribute `hw led` command upstream to RRG/Iceman fork after testing - **Implementation:** Sprint 6 2. **Device Naming:** ✅ **DECIDED** - **Decision:** Auto-generate using interface name + allow user customization - **Default naming:** Use USB interface name (e.g., "ttyACM0", "ttyACM1") - **Fallback naming:** "PM3-1", "PM3-2" if interface name unavailable - **User customization:** Allow setting friendly name in UI - **Edge case:** Handle "no PM3s connected" state gracefully with helpful messaging - **Implementation:** Sprint 4 3. **Session Persistence:** ✅ **DECIDED** - **Decision:** Do NOT persist sessions across server restarts - **Storage:** Memory only - **Behavior:** All sessions invalidated on server restart - **Implementation:** Sprint 2 4. **Device Hotplug:** ✅ **DECIDED** - **Decision:** Use OS-level device detection (udev events) instead of polling - **Notification:** Notify users via SSE/BLE when devices connect/disconnect - **Fallback:** Polling as backup if udev unavailable - **Implementation:** Sprint 1 ### Firmware Management - DECIDED ✅ 5. **Firmware Source Strategy:** ✅ **DECIDED** - **Decision:** Multi-source approach - **Priority order:** 1. **Bundled firmware** (PRIMARY) - crucial for guided tools and project stability 2. System-installed firmware (/usr/share/proxmark3/) 3. Download from Dangerous Pi project releases (not upstream PM3 repo) - **Rationale:** Project stability paramount to usability - **Bundled version:** Lock to specific tested version - **Implementation:** Sprint 6 6. **Firmware Version Tolerance:** ✅ **DECIDED** - **Decision:** STRICT - Exact version match required - **Policy:** Firmware must exactly match expected version - **No tolerance:** No version mismatches allowed - **Behavior:** Devices with mismatched firmware shown as disabled until updated - **Implementation:** Sprint 2 7. **Bootloader Flashing Policy:** ✅ **DECIDED** - **Decision:** Allow with safety checks - **Requirements:** - Strong warnings and multiple confirmations - Power source check: Must be plugged into AC power - Battery check: If on battery, must be ≥80% charge - Battery capacity: Target 2500-5000mAh - **Safety:** Prevent flashing if conditions not met - **Recovery:** Provide JTAG recovery documentation - **Implementation:** Sprint 6 8. **Automatic Firmware Updates:** ✅ **DECIDED** - **Decision:** Auto-update devices when Dangerous Pi system updates - **Trigger:** System upgrade process - **Behavior:** Flash all connected PM3s to bundled firmware version - **User control:** Prompt with option to skip - **Implementation:** Sprint 5 9. **Version Mismatch "Ignore" Behavior:** ✅ **DECIDED** - **Decision:** No persistence, strict enforcement - **Ignore behavior:** Does NOT persist across sessions - **Re-prompt:** Always re-prompt if firmware version changes - **Re-enable:** Devices only become enabled when firmware matches - **No workaround:** Users must update firmware to use device - **Implementation:** Sprint 5 ### Hardware & Infrastructure - DECIDED ✅ 10. **USB Hub Power:** ✅ **DECIDED** - **Decision:** Active monitoring and warnings - **Threshold:** Warn when >2 PM3 devices connected - **Recommendation:** Document powered USB hub requirement - **Detection:** Attempt to detect unpowered hubs (voltage monitoring) - **Warning UI:** Show power consumption estimates - **Implementation:** Documentation + Sprint 3 11. **Simultaneous Flashing:** ✅ **DECIDED** - **Decision:** Dynamic parallel flashing based on power/load - **When plugged in:** Allow parallel flashing - **When on battery:** Dynamic limiting based on battery level and load - **Algorithm:** Calculate safe parallel count based on: - Current battery level - Estimated flash power consumption per device - USB bus bandwidth availability - System load - **Safety margins:** Conservative estimates to prevent issues - **Implementation:** Sprint 6 12. **Bootloader Mode Detection:** ✅ **DECIDED** - **Decision:** Use PM3 client's firmware verification techniques - **Method:** Same detection approach as proxmark3 client - **Checks:** USB descriptor + firmware response validation - **Fallback:** Manual user override option - **Implementation:** Sprint 6 --- ## New Implementation Questions ### Battery & Power Management 13. **Battery Level Monitoring for Firmware Operations:** - How to accurately read battery level during flash operations? - Should we prevent starting new flashes if battery drops below threshold mid-operation? - What's the safe battery drain rate during multi-device flashing? - **Decision needed by:** Sprint 6 - **Recommendation:** Use UPS manager battery readings, halt new flashes at 75%, continue in-progress 14. **Dynamic Flash Concurrency Algorithm:** - How many devices can flash simultaneously on battery vs. AC? - Power consumption per PM3 during flash? - How to estimate remaining battery capacity during operation? - **Decision needed by:** Sprint 6 - **Recommendation:** - AC power: Up to 4 concurrent flashes - Battery 50-100%: 1 at a time - Battery <50%: Warn and recommend AC ### Device Detection & Identification 15. **Udev Event Implementation:** - Use pyudev library or direct udev monitoring? - How to handle udev permissions (requires udev rules)? - Fallback gracefully on systems without udev? - **Decision needed by:** Sprint 1 - **Recommendation:** Use pyudev with polling fallback, provide udev rules in install 16. **Interface Name Extraction:** - Parse interface name from /dev/ttyACM* path? - Store interface name in device database? - Handle interface changes on reconnection? - **Decision needed by:** Sprint 4 - **Recommendation:** Extract from path, store in DB, match by serial if interface changes ### Bundled Firmware Management 17. **Bundled Firmware Versioning:** - How to version bundled firmware files? - Where to store in project structure? - How to handle firmware updates in Dangerous Pi releases? - **Decision needed by:** Sprint 6 - **Recommendation:** ``` dangerous-pi/ firmware/ version.txt # Current bundled version fullimage.elf bootrom.elf FIRMWARE_VERSION.md # Changelog ``` 18. **Firmware Compatibility Matrix:** - Document which Dangerous Pi version works with which PM3 firmware? - Prevent downgrades that break features? - **Decision needed by:** Sprint 6 - **Recommendation:** Maintain compatibility matrix in docs, warn on downgrades ### Edge Cases 19. **No PM3 Devices Connected:** - What should UI show? - Prevent errors in device enumeration? - Helpful onboarding messages? - **Decision needed by:** Sprint 4 - **Recommendation:** Friendly empty state with "Connect a Proxmark3 to get started" 20. **All Devices Disabled (Version Mismatch):** - Show "Update All" button prominently? - Explain why devices are disabled? - Streamline bulk update flow? - **Decision needed by:** Sprint 5 - **Recommendation:** Large "Update All Devices" CTA, explain version requirements --- ## Summary of Additional Considerations Beyond the core multi-device refactoring, this plan addresses: ### Firmware & Version Management - ✅ Automatic firmware version detection - ✅ Version compatibility checking (semantic versioning) - ✅ Firmware flashing integration - ✅ Bootloader safety mechanisms - ✅ Version mismatch handling (flash or ignore) - ✅ Batch device updates - ✅ Flash progress tracking - ✅ Firmware update audit logs ### Device Identification - ✅ LED blinking patterns for identification - ✅ Multiple identification modes (all, chase, alternating) - ✅ Works in both WiFi and BLE modes - ✅ Visual feedback in UI during identification ### Safety & Recovery - ✅ Bootloader preservation by default - ✅ Flash verification - ✅ Rollback on failure - ✅ JTAG recovery documentation - ✅ Strong warnings for dangerous operations ### User Experience - ✅ Clear version mismatch warnings - ✅ One-click firmware updates - ✅ Batch "Update All" operation - ✅ Real-time flash progress - ✅ Device status indicators - ✅ BLE firmware notifications ### Performance & Reliability - ✅ Efficient device polling - ✅ Async flash operations - ✅ USB bus load management - ✅ Device hotplug detection - ✅ Connection status monitoring --- ## Conclusion This refactoring will transform Dangerous Pi from a single-device tool into a **scalable multi-device platform** suitable for labs, workshops, hackerspaces, and power users. The phased approach ensures backward compatibility while systematically updating all layers of the application. ### Key Implementation Decisions **Firmware Management:** - ✅ **Strict version enforcement:** No tolerance for mismatched firmware - ✅ **Bundled firmware primary:** Ships with tested, locked firmware version - ✅ **Auto-update on system upgrade:** Keeps fleet consistent - ✅ **Battery safety:** 80% minimum for bootloader flashing - ✅ **Dynamic parallel flashing:** Based on power availability **Device Management:** - ✅ **Udev-based detection:** Real-time device hotplug notifications - ✅ **Interface-based naming:** Uses ttyACM0, ttyACM1, etc. - ✅ **LED identification:** Custom firmware command for physical ID - ✅ **Session isolation:** Per-device sessions, no persistence across restarts **User Experience:** - ✅ **Zero-config device detection:** Works out of the box - ✅ **Graceful degradation:** Helpful messaging when no devices connected - ✅ **Power awareness:** Warnings and dynamic behavior based on battery/AC - ✅ **Bulk operations:** Update all devices with one click ### Updated Estimates **Estimated Effort:** 9-10 weeks (updated from 8-9 weeks) - Sprint 1: Device enumeration & udev (1.5 weeks) - Sprint 2: Session management refactoring (1.5 weeks) - Sprint 3: Service layer & API updates (2 weeks) - Sprint 4: Frontend implementation (1.5 weeks) - Sprint 5: BLE integration (1 week) - Sprint 6: Firmware management & LED control (2 weeks) - Sprint 7: Testing, polish, battery safety (1 week) **Additional scope from user decisions:** - Udev integration (+0.5 weeks) - Battery monitoring & dynamic flashing (+0.5 weeks) - Bundled firmware packaging (+0.5 weeks) - Strict version enforcement logic (+0.5 weeks) **Complexity:** Very High **Value:** Extremely High **Risk:** Medium (mitigated by phased approach and testing) ### Hardware Requirements for Testing **Minimum:** - 2-3 Proxmark3 Easy devices (different firmware versions) - Powered USB 3.0 hub (7+ ports, 2A+ per port) - Raspberry Pi Zero 2 W - UPS HAT with 2500-5000mAh battery **Recommended:** - 5+ Proxmark3 Easy devices for stress testing - Mix of firmware versions (intentional mismatches) - Power meter for USB bus load monitoring - Optional: Device with bricked firmware for recovery testing - Optional: Different USB hubs for compatibility testing ### Next Steps - Ready to Begin! **Phase 1: Foundation (Sprint 1 - Week 1-1.5)** 1. ✅ Plan approved with user decisions 2. ⏭ **START HERE:** Implement PM3DeviceManager with udev integration 3. ⏭ Document current PM3 firmware version (lock for MVP) 4. ⏭ Set up udev rules for device detection 5. ⏭ Implement device enumeration tests **Phase 2: Core Architecture (Sprints 2-3 - Weeks 2-5)** 6. ⏭ Refactor SessionManager for multi-device 7. ⏭ Update database schema with device & firmware tables 8. ⏭ Implement strict firmware version checking 9. ⏭ Update PM3Service and ServiceContainer 10. ⏭ Create new API endpoints for devices **Phase 3: User Interface (Sprint 4 - Weeks 6-7)** 11. ⏭ Build DeviceSelector component with no-devices empty state 12. ⏭ Implement firmware mismatch warnings 13. ⏭ Add battery level indicators 14. ⏭ Create FirmwareFlashDialog with progress tracking **Phase 4: Advanced Features (Sprints 5-6 - Weeks 8-10)** 15. ⏭ BLE multi-device support 16. ⏭ Bundle firmware files in project 17. ⏭ Implement LED control (firmware mod + fallback) 18. ⏭ Dynamic parallel flashing algorithm 19. ⏭ Battery safety checks for flashing **Phase 5: Testing & Polish (Sprint 7 - Week 10)** 20. ⏭ End-to-end testing with multiple devices 21. ⏭ Battery/power testing scenarios 22. ⏭ Firmware mismatch scenarios 23. ⏭ Documentation updates 24. ⏭ Performance optimization **Critical Path Items:** - 🔴 Udev integration (blocker for real-time detection) - 🔴 Bundled firmware packaging (blocker for version enforcement) - 🔴 Battery monitoring (blocker for safe flashing) - 🟡 LED control firmware mod (nice-to-have, has workaround) **Success Criteria:** - ✅ Support 5+ concurrent PM3 devices - ✅ Real-time device hotplug detection - ✅ Zero tolerance for firmware mismatches - ✅ Safe firmware flashing with battery checks - ✅ LED identification working on all devices - ✅ Graceful handling of zero devices - ✅ BLE support for multi-device - ✅ <2 second device switching latency --- ## References ### Web Resources **LED Control:** - [Proxmark3 RDV4 LEDs Discussion](http://www.proxmark.org/forum/viewtopic.php?id=6514) - LED control discussion - [Proxmark3 armsrc/util.h](https://github.com/Proxmark/proxmark3/blob/master/armsrc/util.h) - LED control functions (LEDson, LEDsoff, LED, LEDsinvert) - [Proxmark3 Standalone Mode](https://github.com/RfidResearchGroup/proxmark3/wiki/Standalone-mode) - LED usage examples **Firmware & Flashing:** - [Proxmark3 Flashing Guide](https://github.com/Proxmark/proxmark3/wiki/flashing) - Official flashing documentation - [Proxmark3 Troubleshooting](https://github.com/RfidResearchGroup/proxmark3/blob/master/doc/md/Installation_Instructions/Troubleshooting.md) - Bootloader and flashing issues - [Proxmark3 Bootloader Fix Guide](https://tagbase.ksec.co.uk/resources/proxmark3-rdv4-bootloader-fix/) - Bootloader recovery - [RfidResearchGroup Proxmark3](https://github.com/RfidResearchGroup/proxmark3) - Iceman fork (PM3 Easy firmware source) **Hardware & Device Info:** - [Proxmark3 Commands Wiki](https://github.com/proxmark/proxmark3/wiki/commands) - Command reference - [Proxmark3 Easy Specs](https://jg.sn.sg/pm3/) - Hardware specifications - [Proxmark3 Easy Product Page](https://dangerousthings.com/product/proxmark3-easy/) - Dangerous Things official page - [USB Serial Number Issues](https://github.com/RfidResearchGroup/proxmark3/issues/1904) - Device identification challenges - [Proxmark3 PM3 Easy LED Indicators](https://forum.dangerousthings.com/t/what-do-the-leds-indicate-on-pm3-easy/9678) - LED meanings ### Internal Documentation - `PROJECT_STATUS.md` - Current project status - `claude.md` - Development guidelines - `UI_GUIDELINES.md` - Frontend design principles - `app/backend/services/pm3_service.py` - Current PM3 service implementation - `app/backend/managers/session_manager.py` - Current session management --- **Document Version:** 2.0 **Last Updated:** 2025-11-26 **Author:** AI Planning Agent (with user requirements input) **Status:** Approved - Ready for Implementation **Changelog:** - v2.0 (2025-11-26): User decisions incorporated, new implementation questions added - Converted open questions to decided items - Added strict firmware version policy - Added battery/power management requirements - Added udev-based device detection - Added bundled firmware as primary source - Added 20 new implementation questions - v1.1 (2025-11-26): Added comprehensive firmware version management section - v1.0 (2025-11-26): Initial multi-device refactoring plan