🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2715 lines
85 KiB
Markdown
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
|