> **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 <led_mask> --duration <ms>`
- 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 (
    <div className="device-card" style={{ borderColor: getStatusColor() }}>
      {/* ... device info ... */}

      {showVersionWarning && (
        <div className="version-warning" style={{
          marginTop: 'var(--space-2)',
          padding: 'var(--space-2)',
          background: 'rgba(255, 200, 0, 0.1)',
          borderLeft: '3px solid var(--color-warning)',
          borderRadius: 'var(--radius)'
        }}>
          <div style={{ fontWeight: 600, marginBottom: 'var(--space-1)' }}>
            ⚠ Firmware Version Mismatch
          </div>
          <div style={{ fontSize: '0.875rem', marginBottom: 'var(--space-2)' }}>
            Device: {device.firmware_info.os_version} •
            Client: {device.firmware_info.client_version}
          </div>

          {device.status !== 'disabled' && (
            <div style={{ display: 'flex', gap: 'var(--space-2)' }}>
              <button
                className="btn btn-primary"
                style={{ fontSize: '0.875rem' }}
                onClick={() => onFlash(device.device_id)}
              >
                <span>⚡</span>
                <span>Update Firmware</span>
              </button>
              <button
                className="btn btn-secondary"
                style={{ fontSize: '0.875rem' }}
                onClick={() => onIgnore(device.device_id)}
              >
                <span>Ignore</span>
              </button>
            </div>
          )}

          {device.status === 'disabled' && (
            <button
              className="btn btn-secondary"
              style={{ fontSize: '0.875rem' }}
              onClick={() => onFlash(device.device_id)}
            >
              <span>⚡</span>
              <span>Enable by Updating Firmware</span>
            </button>
          )}
        </div>
      )}
    </div>
  );
}
```

#### 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<string | null>(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 (
    <div className="modal-overlay" onClick={onClose}>
      <div className="card" style={{ maxWidth: '500px' }} onClick={e => e.stopPropagation()}>
        <h3 className="card-title">Update Firmware</h3>

        <div style={{ marginBottom: 'var(--space-4)' }}>
          <strong>Device:</strong> {device.friendly_name || device.device_path}
          <br />
          <strong>Current Firmware:</strong> {device.firmware_info.os_version}
          <br />
          <strong>Target Firmware:</strong> {device.firmware_info.client_version}
        </div>

        {!flashing && (
          <>
            <div className="form-group">
              <label>
                <input
                  type="checkbox"
                  checked={flashBootrom}
                  onChange={(e) => setFlashBootrom(e.target.checked)}
                />
                <span style={{ marginLeft: 'var(--space-2)' }}>
                  Also update bootloader (advanced)
                </span>
              </label>
              {flashBootrom && (
                <div className="warning" style={{
                  marginTop: 'var(--space-2)',
                  padding: 'var(--space-2)',
                  background: 'rgba(255, 100, 100, 0.1)',
                  borderLeft: '3px solid var(--color-error)'
                }}>
                  ⚠ Warning: Bootloader flashing can brick your device if interrupted!
                </div>
              )}
            </div>

            <div style={{ display: 'flex', gap: 'var(--space-2)', marginTop: 'var(--space-4)' }}>
              <button className="btn btn-secondary" onClick={onClose}>
                Cancel
              </button>
              <button className="btn btn-primary" onClick={handleFlash}>
                <span>⚡</span>
                <span>Flash Firmware</span>
              </button>
            </div>
          </>
        )}

        {flashing && (
          <>
            <div style={{ marginBottom: 'var(--space-3)' }}>
              <div className="progress-bar">
                <div
                  className="progress-fill"
                  style={{ width: `${progress}%` }}
                />
              </div>
              <div style={{ textAlign: 'center', marginTop: 'var(--space-2)' }}>
                {progress}% - {status}
              </div>
            </div>

            <div style={{ fontSize: '0.875rem', color: 'var(--color-text-muted)' }}>
              Do not disconnect the device or close this window during flashing.
            </div>
          </>
        )}

        {error && (
          <div className="error" style={{
            marginTop: 'var(--space-3)',
            padding: 'var(--space-3)',
            background: 'rgba(255, 100, 100, 0.1)',
            border: '1px solid var(--color-error)'
          }}>
            {error}
          </div>
        )}
      </div>
    </div>
  );
}
```

### 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<Device[]>([]);
  const [identifying, setIdentifying] = useState<string | null>(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 (
    <div className="card">
      <h3 className="card-title">Select Proxmark3 Device</h3>

      {devices.length === 0 && (
        <div className="text-muted">
          No Proxmark3 devices detected. Please connect a device.
        </div>
      )}

      <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-2)' }}>
        {devices.map(device => (
          <div
            key={device.device_id}
            className={`device-card ${selectedDeviceId === device.device_id ? 'selected' : ''}`}
            style={{
              padding: 'var(--space-3)',
              border: '2px solid',
              borderColor: selectedDeviceId === device.device_id
                ? 'var(--color-primary)'
                : 'var(--color-border)',
              borderRadius: 'var(--radius)',
              cursor: device.in_use && selectedDeviceId !== device.device_id
                ? 'not-allowed'
                : 'pointer',
              opacity: device.in_use && selectedDeviceId !== device.device_id ? 0.5 : 1
            }}
            onClick={() => !device.in_use && onDeviceSelect(device.device_id)}
          >
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
              <div>
                <div style={{ fontWeight: 600, fontSize: '1.1rem' }}>
                  {device.friendly_name || device.device_path}
                </div>
                <div style={{ fontSize: '0.875rem', color: 'var(--color-text-muted)' }}>
                  {device.device_path}
                  {device.serial_number && ` • SN: ${device.serial_number}`}
                </div>
                <div style={{ fontSize: '0.75rem', marginTop: 'var(--space-1)' }}>
                  {device.connected ? (
                    <span style={{ color: 'var(--color-success)' }}>● Connected</span>
                  ) : (
                    <span style={{ color: 'var(--color-error)' }}>● Disconnected</span>
                  )}
                  {device.in_use && (
                    <span style={{ marginLeft: 'var(--space-2)', color: 'var(--color-warning)' }}>
                      🔒 In Use
                    </span>
                  )}
                  {device.version && (
                    <span style={{ marginLeft: 'var(--space-2)', color: 'var(--color-text-secondary)' }}>
                      {device.version}
                    </span>
                  )}
                </div>
              </div>

              <button
                className="btn btn-secondary"
                style={{ fontSize: '0.875rem' }}
                onClick={(e) => {
                  e.stopPropagation();
                  handleIdentify(device.device_id);
                  onIdentify(device.device_id);
                }}
                disabled={!device.connected || identifying === device.device_id}
              >
                {identifying === device.device_id ? (
                  <>
                    <span className="spinner"></span>
                    <span>Blinking...</span>
                  </>
                ) : (
                  <>
                    <span>💡</span>
                    <span>Identify</span>
                  </>
                )}
              </button>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}
```

#### 5.2 Update Commands Page

**Changes to:** `app/frontend/app/routes/commands.tsx`

```tsx
export default function Commands() {
  const [selectedDeviceId, setSelectedDeviceId] = useState<string | null>(null);
  const [sessionId, setSessionId] = useState<string | null>(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 (
    <div className="container">
      <h1>PM3 Commands</h1>

      {/* Device Selector */}
      <DeviceSelector
        selectedDeviceId={selectedDeviceId}
        onDeviceSelect={setSelectedDeviceId}
        onIdentify={(id) => console.log('Identifying', id)}
      />

      {/* Command Interface (only shown when device selected) */}
      {selectedDeviceId && sessionId && (
        <>
          {/* Existing command UI */}
          <div className="card">
            <h3 className="card-title">
              Commands for {selectedDeviceId}
            </h3>
            {/* ... existing command interface ... */}
          </div>
        </>
      )}

      {selectedDeviceId && !sessionId && (
        <div className="card" style={{ borderColor: 'var(--color-warning)' }}>
          <h3>Session Conflict</h3>
          <p>This device is currently in use by another session.</p>
          <div style={{ display: 'flex', gap: 'var(--space-2)' }}>
            <button className="btn btn-secondary" onClick={() => setSelectedDeviceId(null)}>
              Choose Another Device
            </button>
            <button className="btn btn-primary" onClick={() => {/* Force takeover */}}>
              Take Over Session
            </button>
          </div>
        </div>
      )}
    </div>
  );
}
```

#### 5.3 Update Dashboard

**Changes to:** `app/frontend/app/routes/_index.tsx`

```tsx
export default function Dashboard() {
  const [devices, setDevices] = useState<Device[]>([]);

  // Show all devices with quick status
  return (
    <div className="container">
      <h1>Dashboard</h1>

      <div className="card">
        <h3 className="card-title">Proxmark3 Devices ({devices.length})</h3>
        <div className="grid" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))' }}>
          {devices.map(device => (
            <div key={device.device_id} className="status-card">
              <div className="status-indicator">
                {device.connected ? '●' : '○'}
              </div>
              <div>
                <strong>{device.friendly_name || device.device_path}</strong>
                <div className="text-muted">{device.version}</div>
                <div style={{ marginTop: 'var(--space-2)' }}>
                  {device.in_use ? (
                    <span className="badge badge-warning">In Use</span>
                  ) : (
                    <span className="badge badge-success">Available</span>
                  )}
                </div>
              </div>
            </div>
          ))}
        </div>
      </div>

      {/* ... rest of dashboard ... */}
    </div>
  );
}
```

---

### 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
