Files
pi-pm3/docs/archive/MULTI_PM3_REFACTORING_PLAN.md
michael 4f35df1781 Initial commit - Phase 3/4
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-06 13:46:22 -08:00

2715 lines
85 KiB
Markdown

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