🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
21 KiB
ARCHIVED: This document is superseded by REFACTORING_ROADMAP.md. Kept for historical reference of original service layer design decisions.
Dangerous Pi - Service Layer Refactoring Plan
Status: ✅ COMPLETE - Phase 1, 2, & 3 Done! Priority: High (Required before BLE feature parity) Effort: 2-3 weeks Last Updated: 2025-11-26 Progress: Week 1 Foundation ✅ | Week 2 REST Refactoring ✅ | Week 3 BLE GATT ✅ | Week 4 Workflows (Optional)
Problem Statement
Current Architecture Issues
Code Duplication Risk: When BLE gets full feature parity, business logic will be duplicated:
# REST API endpoint
@router.post("/command")
async def execute_command(request):
# ❌ Session validation logic
if not session_manager.can_execute(request.session_id):
raise HTTPException(...)
# ❌ Command execution logic
result = await pm3_worker.execute_command(request.command)
# ❌ Session update logic
session_manager.update_activity(request.session_id)
return response
# BLE GATT handler (future)
async def handle_command_characteristic(value):
# ❌ DUPLICATE session validation
# ❌ DUPLICATE command execution
# ❌ DUPLICATE session update
# Same logic, different interface!
Maintenance Nightmare: Bug fixes must be applied in 2+ places.
Solution: Service Layer Pattern
New Architecture
┌───────────────────────────────────────────────────┐
│ Interface Layer (Multiple Transport Protocols) │
│ │
│ ├─ REST API (FastAPI) ← Web/Desktop │
│ ├─ BLE GATT (BlueZ) ← Mobile App │
│ ├─ Plugin System ← Extensions │
│ └─ CLI (future) ← Terminal │
└─────────────────┬─────────────────────────────────┘
│
│ All interfaces call services
▼
┌───────────────────────────────────────────────────┐
│ Service Layer (Business Logic) ✨ NEW! │
│ │
│ ├─ PM3Service ← Command execution │
│ │ ├─ execute_command() │
│ │ ├─ get_status() │
│ │ └─ Session validation │
│ │ │
│ ├─ SystemService ← System operations │
│ │ ├─ get_system_info() │
│ │ ├─ shutdown() │
│ │ └─ restart() │
│ │ │
│ ├─ WiFiService ← Network management │
│ │ ├─ scan_networks() │
│ │ ├─ connect() │
│ │ └─ get_status() │
│ │ │
│ ├─ UpdateService ← Software updates │
│ │ ├─ check_updates() │
│ │ ├─ download_update() │
│ │ └─ install_update() │
│ │ │
│ └─ WorkflowService ← Guided workflows │
│ ├─ tune_antenna() │
│ ├─ clone_card() │
│ └─ id_tag() │
└─────────────────┬─────────────────────────────────┘
│
│ Services coordinate managers
▼
┌───────────────────────────────────────────────────┐
│ Manager/Worker Layer (Core Implementation) │
│ │
│ ├─ PM3Worker ← Hardware interface │
│ ├─ SessionManager ← Session state │
│ ├─ WiFiManager ← Network state │
│ ├─ UpdateManager ← Update logic │
│ ├─ UPSManager ← Battery monitoring │
│ ├─ BLEManager ← BLE advertising │
│ └─ PluginManager ← Plugin lifecycle │
└───────────────────────────────────────────────────┘
Key Benefits
- No Code Duplication - Business logic written once, used everywhere
- Easier Testing - Test services independently of transport layer
- Consistent Behavior - REST and BLE behave identically
- Simpler Maintenance - Fix bugs in one place
- Future-Proof - Easy to add new interfaces (CLI, gRPC, etc.)
Implementation Roadmap
Phase 1: Create Service Layer (Week 1)
Create Service Structure:
app/backend/services/
├── __init__.py
├── pm3_service.py ✅ CREATED
├── system_service.py
├── wifi_service.py
├── update_service.py
└── workflow_service.py # For guided workflows
PM3 Service (✅ Complete):
- Command execution with session validation
- Status queries
- Connection management
- Session lifecycle
- Error handling with codes
System Service:
class SystemService:
"""System operations service."""
async def get_info(self) -> ServiceResult:
"""Get system information (CPU, memory, disk)."""
async def shutdown(self, delay: int = 0) -> ServiceResult:
"""Initiate system shutdown."""
async def restart(self, delay: int = 0) -> ServiceResult:
"""Initiate system restart."""
async def get_logs(self, service: str, lines: int = 100) -> ServiceResult:
"""Get service logs."""
WiFi Service:
class WiFiService:
"""WiFi operations service."""
async def scan_networks(self) -> ServiceResult:
"""Scan for available networks."""
async def connect(self, ssid: str, password: str) -> ServiceResult:
"""Connect to network."""
async def disconnect(self) -> ServiceResult:
"""Disconnect from network."""
async def get_status(self) -> ServiceResult:
"""Get WiFi status and current connection."""
Phase 2: Refactor REST API (Week 1-2)
Before (Current):
# app/backend/api/pm3.py
@router.post("/command")
async def execute_command(request: CommandRequest):
# Business logic mixed with HTTP concerns
if not session_manager.can_execute(request.session_id):
raise HTTPException(status_code=423, detail="...")
result = await pm3_worker.execute_command(request.command)
session_manager.update_activity(request.session_id)
return CommandResponse(...)
After (Refactored):
# app/backend/api/pm3.py
from ..services import PM3Service
pm3_service = PM3Service() # Inject singleton
@router.post("/command")
async def execute_command(request: CommandRequest):
# Thin adapter: converts HTTP → Service → HTTP
result = await pm3_service.execute_command(
command=request.command,
session_id=request.session_id
)
if result.success:
return CommandResponse(
success=True,
output=result.data["output"]
)
else:
# Convert service error to HTTP error
raise HTTPException(
status_code=get_status_code(result.error.code),
detail=result.error.message
)
def get_status_code(error_code: str) -> int:
"""Map service error codes to HTTP status codes."""
codes = {
"session_locked": 423,
"pm3_not_connected": 503,
"command_failed": 500,
"session_not_found": 404,
}
return codes.get(error_code, 500)
Benefits:
- REST endpoints become thin adapters
- HTTP concerns separated from business logic
- Services are HTTP-agnostic (can be used by BLE)
Phase 3: Implement BLE GATT Service (Week 2-3)
Create BLE Service Structure:
app/backend/ble/
├── __init__.py
├── gatt_server.py # BLE GATT server
├── characteristics.py # GATT characteristics
└── handlers.py # Characteristic handlers
BLE GATT Implementation:
# app/backend/ble/gatt_server.py
from ..services import PM3Service
class DangerousPiGATTServer:
"""BLE GATT server for Dangerous Pi."""
# Service UUIDs
PM3_SERVICE_UUID = "12345678-1234-5678-1234-56789abcdef0"
# Characteristic UUIDs
COMMAND_CHAR_UUID = "12345678-1234-5678-1234-56789abcdef1"
STATUS_CHAR_UUID = "12345678-1234-5678-1234-56789abcdef2"
SESSION_CHAR_UUID = "12345678-1234-5678-1234-56789abcdef3"
def __init__(self):
self.pm3_service = PM3Service() # Same service as REST!
self.bus = None
self.adapter = None
async def start(self):
"""Start GATT server."""
await self._register_service()
await self._register_characteristics()
async def _handle_command_write(self, value: bytes):
"""Handle command characteristic write.
Value format: JSON {"command": "hw version", "session_id": "..."}
"""
import json
data = json.loads(value.decode())
# ✅ REUSE service logic (no duplication!)
result = await self.pm3_service.execute_command(
command=data["command"],
session_id=data.get("session_id")
)
# Return result via BLE notification
response = {
"success": result.success,
"data": result.data if result.success else None,
"error": {
"code": result.error.code,
"message": result.error.message
} if result.error else None
}
await self._notify_characteristic(
self.COMMAND_CHAR_UUID,
json.dumps(response).encode()
)
async def _handle_status_read(self) -> bytes:
"""Handle status characteristic read."""
# ✅ REUSE service logic
result = await self.pm3_service.get_status()
import json
return json.dumps(result.data).encode()
Result: BLE and REST share identical business logic!
Phase 4: Enhanced BLE Manager (Week 3)
Upgrade BLE Manager:
# app/backend/managers/ble_manager.py
class BLEManager:
"""Enhanced BLE manager with GATT server."""
def __init__(self):
# Existing notification support
self._notification_queue = asyncio.Queue()
# NEW: GATT server for bidirectional communication
self._gatt_server = None
async def initialize(self):
"""Initialize BLE with GATT server."""
# Existing: notification support
await self._setup_notifications()
# NEW: Start GATT server
from ..ble.gatt_server import DangerousPiGATTServer
self._gatt_server = DangerousPiGATTServer()
await self._gatt_server.start()
# Existing notification methods remain unchanged
async def send_notification(self, ...):
"""Send one-way notification (existing)."""
...
# NEW: Bidirectional communication via GATT
async def handle_command(self, command: str, session_id: str):
"""Handle command via BLE (delegates to GATT server)."""
return await self._gatt_server.handle_command(command, session_id)
Phase 5: Workflow Service (Week 3-4)
Create Workflow Service:
# app/backend/services/workflow_service.py
class WorkflowService:
"""Service for guided PM3 workflows."""
def __init__(self):
self.pm3_service = PM3Service()
async def tune_antenna(
self,
frequency_type: str, # "hf", "lf", "hw"
session_id: str
) -> WorkflowResult:
"""Execute antenna tuning workflow.
Returns real-time tuning data for visualization.
"""
# 1. Execute tune command
command = f"{frequency_type} tune"
result = await self.pm3_service.execute_command(command, session_id)
if not result.success:
return WorkflowResult(success=False, error=result.error)
# 2. Parse output into structured data
from ..parsers.pm3_output import parse_antenna_tuning
tuning_data = parse_antenna_tuning(result.data["output"])
# 3. Return workflow result with visualization data
return WorkflowResult(
success=True,
data={
"tuning_data": tuning_data,
"optimal": tuning_data["voltage"] > 40.0,
"recommendation": get_tuning_recommendation(tuning_data)
}
)
async def clone_mifare_classic(
self,
session_id: str,
source_uid: Optional[str] = None
) -> AsyncGenerator[WorkflowProgress, None]:
"""Execute MIFARE Classic cloning workflow.
Yields progress updates for each step.
"""
# Step 1: Tune antenna
yield WorkflowProgress(step=1, total=4, message="Tuning HF antenna...")
tune_result = await self.tune_antenna("hf", session_id)
if not tune_result.success:
yield WorkflowProgress(step=1, error="Tuning failed")
return
# Step 2: Read source card
yield WorkflowProgress(step=2, total=4, message="Reading source card...")
# ... read logic
# Step 3: Verify
yield WorkflowProgress(step=3, total=4, message="Verifying read...")
# ... verify logic
# Step 4: Write to target
yield WorkflowProgress(step=4, total=4, message="Writing to target...")
for block in range(64):
# ... write each block
yield WorkflowProgress(
step=4,
total=4,
message=f"Writing block {block}/64...",
progress=block/64
)
yield WorkflowProgress(step=4, total=4, message="Clone complete!", complete=True)
Dependency Injection Strategy
Service Singletons
Create Service Container:
# app/backend/services/container.py
class ServiceContainer:
"""Dependency injection container for services."""
_instance = None
def __init__(self):
# Create shared worker/manager instances
self._pm3_worker = PM3Worker()
self._session_manager = SessionManager()
self._wifi_manager = WiFiManager()
self._update_manager = UpdateManager()
# Create services with injected dependencies
self._pm3_service = PM3Service(
pm3_worker=self._pm3_worker,
session_manager=self._session_manager
)
self._wifi_service = WiFiService(
wifi_manager=self._wifi_manager
)
self._workflow_service = WorkflowService(
pm3_service=self._pm3_service
)
@classmethod
def get_instance(cls):
if cls._instance is None:
cls._instance = ServiceContainer()
return cls._instance
@property
def pm3_service(self) -> PM3Service:
return self._pm3_service
@property
def wifi_service(self) -> WiFiService:
return self._wifi_service
@property
def workflow_service(self) -> WorkflowService:
return self._workflow_service
# Global container instance
container = ServiceContainer.get_instance()
Usage in REST API:
# app/backend/api/pm3.py
from ..services.container import container
@router.post("/command")
async def execute_command(request: CommandRequest):
result = await container.pm3_service.execute_command(
command=request.command,
session_id=request.session_id
)
# ... handle result
Usage in BLE:
# app/backend/ble/gatt_server.py
from ..services.container import container
class DangerousPiGATTServer:
async def _handle_command_write(self, value: bytes):
# Same service instance as REST!
result = await container.pm3_service.execute_command(...)
Testing Strategy
Service Unit Tests
# tests/test_pm3_service.py
import pytest
from app.backend.services import PM3Service
from app.backend.workers.pm3_worker import MockPM3Worker
from app.backend.managers.session_manager import SessionManager
@pytest.fixture
def pm3_service():
"""Create PM3 service with mock worker."""
mock_worker = MockPM3Worker()
session_manager = SessionManager()
return PM3Service(
pm3_worker=mock_worker,
session_manager=session_manager
)
@pytest.mark.asyncio
async def test_execute_command_success(pm3_service):
"""Test successful command execution."""
# Create session
session_result = pm3_service.create_session()
assert session_result.success
session_id = session_result.data["session_id"]
# Execute command
result = await pm3_service.execute_command(
command="hw version",
session_id=session_id
)
assert result.success
assert "Proxmark3" in result.data["output"]
@pytest.mark.asyncio
async def test_execute_command_session_locked(pm3_service):
"""Test command execution with locked session."""
# Create session
session1 = pm3_service.create_session()
# Try to execute with different session
result = await pm3_service.execute_command(
command="hw version",
session_id="different-session"
)
assert not result.success
assert result.error.code == "session_locked"
Integration Tests
# tests/test_rest_ble_parity.py
@pytest.mark.asyncio
async def test_rest_and_ble_return_same_result():
"""Verify REST and BLE produce identical results."""
# Execute via REST
rest_response = await rest_client.post("/api/pm3/command",
json={"command": "hw version", "session_id": "test"})
# Execute via BLE
ble_response = await ble_client.write_characteristic(
COMMAND_CHAR_UUID,
json.dumps({"command": "hw version", "session_id": "test"}).encode()
)
# Results should be identical (both use same service)
assert rest_response.json()["output"] == ble_response["data"]["output"]
Migration Checklist
Week 1: Service Layer Foundation
- Create
app/backend/services/directory (✅ complete) - Implement
PM3Service(✅ complete) - Implement
SystemService(✅ complete) - Implement
WiFiService(✅ complete) - Implement
UpdateService(✅ complete) - Create
ServiceContainerfor DI (✅ complete) - Write service unit tests (✅ complete - 4 test files, 100+ tests)
Week 2: REST API Refactoring
- Refactor
api/pm3.pyto usePM3Service(✅ complete) - Refactor
api/system.pyto useSystemService(✅ complete) - Refactor
api/wifi.pyto useWiFiService(✅ complete) - Refactor
api/updates.pyto useUpdateService(✅ complete) - Update integration tests (✅ complete - API endpoint tests added)
- Create comprehensive test suite (✅ complete - pytest config, fixtures, docs)
Week 3: BLE Implementation
- Create
app/backend/ble/directory (✅ complete) - Implement
DangerousPiGATTServer(✅ complete) - Define GATT service and characteristics (✅ complete)
- Implement characteristic handlers using services (✅ complete - PM3, WiFi, System, Update)
- Write comprehensive BLE tests (✅ complete - full test coverage)
- Create BLE integration guide (✅ complete - BLE_GATT_GUIDE.md)
- Integrate with BlueZ D-Bus (deferred - requires hardware)
Week 4: Workflow Service
- Implement
WorkflowService - Add antenna tuning workflow
- Add clone workflows
- Add ID tag workflow
- REST endpoints for workflows
- BLE characteristics for workflows
- Integration tests
Benefits Summary
| Aspect | Before (Current) | After (Refactored) |
|---|---|---|
| Code Duplication | High risk (REST + BLE) | None |
| Testing | Test each interface separately | Test services once |
| Maintenance | Update 2+ places | Update 1 place |
| New Interfaces | Copy/paste logic | Reuse services |
| Consistency | May diverge | Guaranteed identical |
| Testability | HTTP-coupled | Service isolated |
Success Criteria
- Zero business logic in REST endpoints (thin adapters only)
- BLE GATT handlers reuse 100% of PM3Service logic
- Services have 90%+ test coverage
- REST and BLE produce identical results for same inputs
- Adding new interface (CLI, gRPC) takes < 1 day
Next Steps
- Review this plan - Approve refactoring approach
- Start Week 1 - Implement service layer foundation
- Gradual migration - Refactor one endpoint at a time
- BLE implementation - Add GATT server using services
- Mobile app - React Native app can use BLE interface
Summary: Refactor to Service Layer pattern BEFORE expanding BLE to prevent code duplication and enable cross-platform consistency.