> **ARCHIVED**: This document is superseded by [REFACTORING_ROADMAP.md](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: ```python # 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 1. **No Code Duplication** - Business logic written once, used everywhere 2. **Easier Testing** - Test services independently of transport layer 3. **Consistent Behavior** - REST and BLE behave identically 4. **Simpler Maintenance** - Fix bugs in one place 5. **Future-Proof** - Easy to add new interfaces (CLI, gRPC, etc.) --- ## Implementation Roadmap ### Phase 1: Create Service Layer (Week 1) **Create Service Structure**: ```bash 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): - [x] Command execution with session validation - [x] Status queries - [x] Connection management - [x] Session lifecycle - [x] Error handling with codes **System Service**: ```python 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**: ```python 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): ```python # 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): ```python # 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**: ```bash app/backend/ble/ ├── __init__.py ├── gatt_server.py # BLE GATT server ├── characteristics.py # GATT characteristics └── handlers.py # Characteristic handlers ``` **BLE GATT Implementation**: ```python # 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**: ```python # 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**: ```python # 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**: ```python # 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**: ```python # 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**: ```python # 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 ```python # 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 ```python # 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 - [x] Create `app/backend/services/` directory (✅ complete) - [x] Implement `PM3Service` (✅ complete) - [x] Implement `SystemService` (✅ complete) - [x] Implement `WiFiService` (✅ complete) - [x] Implement `UpdateService` (✅ complete) - [x] Create `ServiceContainer` for DI (✅ complete) - [x] Write service unit tests (✅ complete - 4 test files, 100+ tests) ### Week 2: REST API Refactoring - [x] Refactor `api/pm3.py` to use `PM3Service` (✅ complete) - [x] Refactor `api/system.py` to use `SystemService` (✅ complete) - [x] Refactor `api/wifi.py` to use `WiFiService` (✅ complete) - [x] Refactor `api/updates.py` to use `UpdateService` (✅ complete) - [x] Update integration tests (✅ complete - API endpoint tests added) - [x] Create comprehensive test suite (✅ complete - pytest config, fixtures, docs) ### Week 3: BLE Implementation - [x] Create `app/backend/ble/` directory (✅ complete) - [x] Implement `DangerousPiGATTServer` (✅ complete) - [x] Define GATT service and characteristics (✅ complete) - [x] Implement characteristic handlers using services (✅ complete - PM3, WiFi, System, Update) - [x] Write comprehensive BLE tests (✅ complete - full test coverage) - [x] 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 1. **Review this plan** - Approve refactoring approach 2. **Start Week 1** - Implement service layer foundation 3. **Gradual migration** - Refactor one endpoint at a time 4. **BLE implementation** - Add GATT server using services 5. **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.