# P0 Implementation - Backend API Approach (RECOMMENDED) **This is the CORRECT approach for your architecture!** You already have: - ✅ FastAPI backend at `http://100.81.138.77:8000/api/v1` - ✅ Flutter app making REST calls - ✅ Swagger UI documentation - ✅ Authentication with Bearer tokens **DON'T create a native Windows plugin!** Instead, add GeViServer integration to your existing backend. --- ## Current Architecture (What You Have) ``` ┌─────────────────────────────────────────┐ │ Flutter App (Dart) │ │ ┌───────────────────────────────────┐ │ │ │ ActionMappingBloc │ │ │ │ ServerBloc │ │ │ └──────────────┬────────────────────┘ │ │ │ │ │ ┌──────────────▼────────────────────┐ │ │ │ DioClient (HTTP) │ │ │ │ http://100.81.138.77:8000 │ │ │ └──────────────┬────────────────────┘ │ └─────────────────┼────────────────────────┘ │ REST API ┌─────────────────▼────────────────────────┐ │ FastAPI Backend (Python) │ │ ┌───────────────────────────────────┐ │ │ │ /api/v1/configuration/... │ │ │ │ /api/v1/auth/... │ │ │ │ /api/v1/excel/... │ │ │ └───────────────────────────────────┘ │ │ │ │ Database: PostgreSQL/SQLite │ └───────────────────────────────────────────┘ ``` --- ## Target Architecture (What You Need) ``` ┌─────────────────────────────────────────┐ │ Flutter App (Dart) │ │ ┌───────────────────────────────────┐ │ │ │ GeViServerConnectionBloc │ │ ← New BLoC │ │ VideoControlBloc │ │ ← New BLoC │ └──────────────┬────────────────────┘ │ │ │ │ │ ┌──────────────▼────────────────────┐ │ │ │ DioClient (HTTP) │ │ │ │ Existing HTTP client │ │ │ └──────────────┬────────────────────┘ │ └─────────────────┼────────────────────────┘ │ REST API (new endpoints) ┌─────────────────▼────────────────────────┐ │ FastAPI Backend (Python) │ │ ┌───────────────────────────────────┐ │ │ │ NEW: /api/v1/geviserver/... │ │ ← Add these! │ │ - POST /connect │ │ │ │ - POST /disconnect │ │ │ │ - POST /send-action │ │ │ │ - GET /video-inputs │ │ │ │ - POST /crossswitch │ │ │ │ - POST /digital-io/close │ │ │ │ - GET /connection-status │ │ │ └──────────────┬────────────────────┘ │ │ │ │ │ ┌──────────────▼────────────────────┐ │ │ │ GeViServer Service (Python) │ │ ← New service │ │ - Wrapper around GeViProcAPI │ │ │ │ - Connection pooling │ │ │ │ - Message construction │ │ │ └──────────────┬────────────────────┘ │ │ │ │ │ ┌──────────────▼────────────────────┐ │ │ │ Python → GeViProcAPI.dll │ │ ← Integration │ │ (ctypes or subprocess) │ │ │ └──────────────┬────────────────────┘ │ └─────────────────┼─────────────────────────┘ │ Native calls ┌─────────────────▼─────────────────────────┐ │ GeViServer (Windows Service) │ │ C:\GEVISOFT\geviserver.exe │ └───────────────────────────────────────────┘ ``` --- ## Implementation Steps ### Step 1: Add GeViServer Integration to Backend **Location:** `geutebruck-api/src/api/routers/geviserver.py` #### 1.1 Install Python Dependencies ```bash cd C:\DEV\COPILOT\geutebruck-api pip install ctypes # Already included in Python ``` #### 1.2 Create GeViServer Service **File:** `geutebruck-api/src/services/geviserver_service.py` ```python import ctypes import logging from typing import Optional, Dict, Any from ctypes import POINTER, c_char_p, c_int, c_bool, c_void_p, CFUNCTYPE, Structure from dataclasses import dataclass from enum import Enum logger = logging.getLogger(__name__) class TServerNotification(Enum): NFServer_SetupModified = 0 NFServer_Disconnected = 10 NFServer_GoingShutdown = 11 NFServer_NewMessage = 12 class TConnectResult(Enum): connectOk = 0 connectWarningInvalidHeaders = 1 connectAborted = 100 connectGenericError = 101 connectRemoteUnknownUser = 302 connectRemoteConnectionLimitExceeded = 303 @dataclass class GeViServerConnectionInfo: address: str username: str is_connected: bool connected_at: Optional[str] = None class GeViServerService: """Service to interact with GeViServer via GeViProcAPI.dll""" def __init__(self, dll_path: str = "C:/GEVISOFT/GeViProcAPI.dll"): self.dll_path = dll_path self.dll = None self.database_handle = None self._load_dll() def _load_dll(self): """Load GeViProcAPI.dll and define function signatures""" try: self.dll = ctypes.WinDLL(self.dll_path) # Define function signatures # GeViAPI_InterfaceVersion self.dll.GeViAPI_InterfaceVersion.restype = c_int self.dll.GeViAPI_InterfaceVersion.argtypes = [] # GeViAPI_EncodeString self.dll.GeViAPI_EncodeString.restype = c_bool self.dll.GeViAPI_EncodeString.argtypes = [c_char_p, c_char_p, c_int] # GeViAPI_Database_Create self.dll.GeViAPI_Database_Create.restype = c_bool self.dll.GeViAPI_Database_Create.argtypes = [ POINTER(c_void_p), # Database handle (out) c_char_p, # Aliasname c_char_p, # Address c_char_p, # Username c_char_p, # Password c_char_p, # Username2 c_char_p, # Password2 ] # GeViAPI_Database_Connect self.dll.GeViAPI_Database_Connect.restype = c_bool self.dll.GeViAPI_Database_Connect.argtypes = [ c_void_p, # Database handle POINTER(c_int), # ConnectResult (out) c_void_p, # Callback (optional) c_void_p, # Instance (optional) ] # GeViAPI_Database_Connected self.dll.GeViAPI_Database_Connected.restype = c_bool self.dll.GeViAPI_Database_Connected.argtypes = [ c_void_p, # Database handle POINTER(c_bool), # IsConnected (out) ] # GeViAPI_Database_Disconnect self.dll.GeViAPI_Database_Disconnect.restype = c_bool self.dll.GeViAPI_Database_Disconnect.argtypes = [c_void_p] # GeViAPI_Database_Destroy self.dll.GeViAPI_Database_Destroy.restype = c_bool self.dll.GeViAPI_Database_Destroy.argtypes = [c_void_p] # GeViAPI_Database_SendPing self.dll.GeViAPI_Database_SendPing.restype = c_bool self.dll.GeViAPI_Database_SendPing.argtypes = [c_void_p] # GeViAPI_Database_SendMessage self.dll.GeViAPI_Database_SendMessage.restype = c_bool self.dll.GeViAPI_Database_SendMessage.argtypes = [ c_void_p, # Database handle c_char_p, # Message buffer c_int, # Buffer size ] # GeViAPI_GetLastError self.dll.GeViAPI_GetLastError.restype = c_bool self.dll.GeViAPI_GetLastError.argtypes = [ POINTER(c_int), # ExceptionClass (out) POINTER(c_int), # ErrorNo (out) POINTER(c_int), # ErrorClass (out) POINTER(c_char_p), # ErrorMessage (out) ] logger.info(f"Loaded GeViProcAPI.dll version: {self.dll.GeViAPI_InterfaceVersion()}") except Exception as e: logger.error(f"Failed to load GeViProcAPI.dll: {e}") raise def _encode_password(self, password: str) -> bytes: """Encode password using GeViAPI_EncodeString""" encoded = ctypes.create_string_buffer(33) # 32 bytes + null result = self.dll.GeViAPI_EncodeString( encoded, password.encode('utf-8'), 33 ) if not result: raise Exception("Failed to encode password") return encoded.value def _get_last_error(self) -> str: """Get last error from GeViProcAPI""" exception_class = c_int() error_no = c_int() error_class = c_int() error_message = c_char_p() result = self.dll.GeViAPI_GetLastError( ctypes.byref(exception_class), ctypes.byref(error_no), ctypes.byref(error_class), ctypes.byref(error_message) ) if result and error_message: return error_message.value.decode('utf-8', errors='ignore') return "Unknown error" def connect( self, address: str, username: str, password: str, username2: Optional[str] = None, password2: Optional[str] = None ) -> Dict[str, Any]: """ Connect to GeViServer Args: address: Server address (e.g., "localhost" or "192.168.1.100") username: Username for authentication password: Password (will be encrypted) username2: Optional second username password2: Optional second password Returns: Dict with success status and message """ try: # Disconnect if already connected if self.database_handle: self.disconnect() # Encode passwords encoded_password = self._encode_password(password) encoded_password2 = self._encode_password(password2) if password2 else None # Create database handle db_handle = c_void_p() result = self.dll.GeViAPI_Database_Create( ctypes.byref(db_handle), b"PythonAPI", address.encode('utf-8'), username.encode('utf-8'), encoded_password, username2.encode('utf-8') if username2 else None, encoded_password2 if encoded_password2 else None ) if not result or not db_handle.value: error_msg = self._get_last_error() logger.error(f"Failed to create database: {error_msg}") return { "success": False, "message": f"Database creation failed: {error_msg}" } self.database_handle = db_handle # Connect connect_result = c_int() result = self.dll.GeViAPI_Database_Connect( self.database_handle, ctypes.byref(connect_result), None, # Progress callback None # Instance ) if not result or connect_result.value != TConnectResult.connectOk.value: error_msg = self._get_last_error() connect_result_name = TConnectResult(connect_result.value).name if connect_result.value in [e.value for e in TConnectResult] else f"Unknown({connect_result.value})" logger.error(f"Connection failed: {connect_result_name}, {error_msg}") # Clean up self.dll.GeViAPI_Database_Destroy(self.database_handle) self.database_handle = None return { "success": False, "message": f"Connection failed: {connect_result_name}", "error": error_msg } logger.info(f"Successfully connected to GeViServer at {address}") return { "success": True, "message": "Connected to GeViServer", "address": address, "username": username } except Exception as e: logger.error(f"Exception during connection: {e}") return { "success": False, "message": f"Exception: {str(e)}" } def disconnect(self) -> Dict[str, Any]: """Disconnect from GeViServer""" try: if not self.database_handle: return { "success": True, "message": "Already disconnected" } # Disconnect self.dll.GeViAPI_Database_Disconnect(self.database_handle) # Destroy handle self.dll.GeViAPI_Database_Destroy(self.database_handle) self.database_handle = None logger.info("Disconnected from GeViServer") return { "success": True, "message": "Disconnected successfully" } except Exception as e: logger.error(f"Exception during disconnection: {e}") return { "success": False, "message": f"Disconnection error: {str(e)}" } def is_connected(self) -> bool: """Check if connected to GeViServer""" if not self.database_handle: return False connected = c_bool() result = self.dll.GeViAPI_Database_Connected( self.database_handle, ctypes.byref(connected) ) return result and connected.value def send_ping(self) -> Dict[str, Any]: """Send ping to GeViServer""" try: if not self.database_handle: return { "success": False, "message": "Not connected" } result = self.dll.GeViAPI_Database_SendPing(self.database_handle) if not result: error_msg = self._get_last_error() logger.error(f"Ping failed: {error_msg}") return { "success": False, "message": f"Ping failed: {error_msg}" } return { "success": True, "message": "Ping successful" } except Exception as e: logger.error(f"Exception during ping: {e}") return { "success": False, "message": f"Ping error: {str(e)}" } def send_message(self, message: str) -> Dict[str, Any]: """ Send action message to GeViServer Args: message: ASCII message string (e.g., "CrossSwitch(7,3,0)") Returns: Dict with success status """ try: if not self.database_handle: return { "success": False, "message": "Not connected" } message_bytes = message.encode('utf-8') result = self.dll.GeViAPI_Database_SendMessage( self.database_handle, message_bytes, len(message_bytes) ) if not result: error_msg = self._get_last_error() logger.error(f"Send message failed: {error_msg}") return { "success": False, "message": f"Send failed: {error_msg}" } logger.info(f"Sent message: {message}") return { "success": True, "message": "Message sent successfully", "sent_message": message } except Exception as e: logger.error(f"Exception sending message: {e}") return { "success": False, "message": f"Send error: {str(e)}" } def __del__(self): """Cleanup on destruction""" if self.database_handle: try: self.disconnect() except: pass # Global instance (singleton pattern) _geviserver_service: Optional[GeViServerService] = None def get_geviserver_service() -> GeViServerService: """Get or create GeViServer service instance""" global _geviserver_service if _geviserver_service is None: _geviserver_service = GeViServerService() return _geviserver_service ``` #### 1.3 Create API Router **File:** `geutebruck-api/src/api/routers/geviserver.py` ```python from fastapi import APIRouter, HTTPException, Depends from pydantic import BaseModel from typing import Optional, Dict, Any import logging from src.services.geviserver_service import get_geviserver_service, GeViServerService logger = logging.getLogger(__name__) router = APIRouter(prefix="/geviserver", tags=["GeViServer"]) # Request/Response Models class ConnectRequest(BaseModel): address: str username: str password: str username2: Optional[str] = None password2: Optional[str] = None class SendMessageRequest(BaseModel): message: str class ConnectionStatusResponse(BaseModel): is_connected: bool address: Optional[str] = None username: Optional[str] = None # Endpoints @router.post("/connect") async def connect_to_server( request: ConnectRequest, service: GeViServerService = Depends(get_geviserver_service) ) -> Dict[str, Any]: """ Connect to GeViServer Example: ```json { "address": "localhost", "username": "admin", "password": "admin" } ``` """ logger.info(f"Connecting to GeViServer at {request.address}") result = service.connect( address=request.address, username=request.username, password=request.password, username2=request.username2, password2=request.password2 ) if not result.get("success"): raise HTTPException(status_code=400, detail=result.get("message", "Connection failed")) return result @router.post("/disconnect") async def disconnect_from_server( service: GeViServerService = Depends(get_geviserver_service) ) -> Dict[str, Any]: """Disconnect from GeViServer""" logger.info("Disconnecting from GeViServer") result = service.disconnect() return result @router.get("/status") async def get_connection_status( service: GeViServerService = Depends(get_geviserver_service) ) -> ConnectionStatusResponse: """Get current connection status""" is_connected = service.is_connected() return ConnectionStatusResponse( is_connected=is_connected, address=None, # Could store this in service username=None ) @router.post("/ping") async def send_ping( service: GeViServerService = Depends(get_geviserver_service) ) -> Dict[str, Any]: """Send ping to GeViServer""" result = service.send_ping() if not result.get("success"): raise HTTPException(status_code=400, detail=result.get("message", "Ping failed")) return result @router.post("/send-message") async def send_message( request: SendMessageRequest, service: GeViServerService = Depends(get_geviserver_service) ) -> Dict[str, Any]: """ Send action message to GeViServer Example: ```json { "message": "CrossSwitch(7,3,0)" } ``` """ logger.info(f"Sending message: {request.message}") result = service.send_message(request.message) if not result.get("success"): raise HTTPException(status_code=400, detail=result.get("message", "Send failed")) return result # Video Control Endpoints @router.post("/video/crossswitch") async def crossswitch_video( video_input: int, video_output: int, switch_mode: int = 0, service: GeViServerService = Depends(get_geviserver_service) ) -> Dict[str, Any]: """ Cross-switch video input to output Args: video_input: Video input channel number video_output: Video output channel number switch_mode: Switch mode (0=normal, 1=...) """ message = f"CrossSwitch({video_input},{video_output},{switch_mode})" logger.info(f"Cross-switching: {message}") result = service.send_message(message) if not result.get("success"): raise HTTPException(status_code=400, detail=result.get("message", "CrossSwitch failed")) return { "success": True, "message": f"Routed video input {video_input} to output {video_output}", "video_input": video_input, "video_output": video_output } @router.post("/video/clear-output") async def clear_video_output( video_output: int, service: GeViServerService = Depends(get_geviserver_service) ) -> Dict[str, Any]: """Clear video output""" message = f"ClearOutput({video_output})" logger.info(f"Clearing output: {message}") result = service.send_message(message) if not result.get("success"): raise HTTPException(status_code=400, detail=result.get("message", "ClearOutput failed")) return { "success": True, "message": f"Cleared video output {video_output}", "video_output": video_output } # Digital I/O Endpoints @router.post("/digital-io/close-contact") async def close_digital_contact( contact_id: int, service: GeViServerService = Depends(get_geviserver_service) ) -> Dict[str, Any]: """Close digital output contact""" message = f"CloseContact({contact_id})" logger.info(f"Closing contact: {message}") result = service.send_message(message) if not result.get("success"): raise HTTPException(status_code=400, detail=result.get("message", "CloseContact failed")) return { "success": True, "message": f"Closed digital contact {contact_id}", "contact_id": contact_id } @router.post("/digital-io/open-contact") async def open_digital_contact( contact_id: int, service: GeViServerService = Depends(get_geviserver_service) ) -> Dict[str, Any]: """Open digital output contact""" message = f"OpenContact({contact_id})" logger.info(f"Opening contact: {message}") result = service.send_message(message) if not result.get("success"): raise HTTPException(status_code=400, detail=result.get("message", "OpenContact failed")) return { "success": True, "message": f"Opened digital contact {contact_id}", "contact_id": contact_id } ``` #### 1.4 Register Router **File:** `geutebruck-api/src/api/main.py` Add this to your main.py: ```python from src.api.routers import geviserver # ... existing imports and app setup ... # Register GeViServer router app.include_router(geviserver.router, prefix="/api/v1") ``` --- ### Step 2: Update Flutter App to Use API Your Flutter app **already has everything it needs** - just add new endpoints! #### 2.1 Update API Constants **File:** `geutebruck_app/lib/core/constants/api_constants.dart` ```dart class ApiConstants { // ... existing constants ... // GeViServer endpoints static const String geviServerConnect = '/geviserver/connect'; static const String geviServerDisconnect = '/geviserver/disconnect'; static const String geviServerStatus = '/geviserver/status'; static const String geviServerPing = '/geviserver/ping'; static const String geviServerSendMessage = '/geviserver/send-message'; // Video control static const String videoCrossSwitch = '/geviserver/video/crossswitch'; static const String videoClearOutput = '/geviserver/video/clear-output'; // Digital I/O static const String digitalIoCloseContact = '/geviserver/digital-io/close-contact'; static const String digitalIoOpenContact = '/geviserver/digital-io/open-contact'; } ``` #### 2.2 Create Remote Data Source **File:** `geutebruck_app/lib/data/data_sources/remote/geviserver_remote_data_source.dart` ```dart import 'package:dio/dio.dart'; import '../../../core/network/dio_client.dart'; import '../../../core/constants/api_constants.dart'; class GeViServerRemoteDataSource { final DioClient _dioClient; GeViServerRemoteDataSource({required DioClient dioClient}) : _dioClient = dioClient; /// Connect to GeViServer Future> connect({ required String address, required String username, required String password, }) async { try { final response = await _dioClient.post( ApiConstants.geviServerConnect, data: { 'address': address, 'username': username, 'password': password, }, ); return response.data; } on DioException catch (e) { throw Exception('Connection failed: ${e.message}'); } } /// Disconnect from GeViServer Future> disconnect() async { try { final response = await _dioClient.post( ApiConstants.geviServerDisconnect, ); return response.data; } on DioException catch (e) { throw Exception('Disconnection failed: ${e.message}'); } } /// Get connection status Future> getStatus() async { try { final response = await _dioClient.get( ApiConstants.geviServerStatus, ); return response.data; } on DioException catch (e) { throw Exception('Status check failed: ${e.message}'); } } /// Send ping Future> sendPing() async { try { final response = await _dioClient.post( ApiConstants.geviServerPing, ); return response.data; } on DioException catch (e) { throw Exception('Ping failed: ${e.message}'); } } /// Cross-switch video Future> crossSwitch({ required int videoInput, required int videoOutput, int switchMode = 0, }) async { try { final response = await _dioClient.post( ApiConstants.videoCrossSwitch, queryParameters: { 'video_input': videoInput, 'video_output': videoOutput, 'switch_mode': switchMode, }, ); return response.data; } on DioException catch (e) { throw Exception('CrossSwitch failed: ${e.message}'); } } /// Clear video output Future> clearOutput({ required int videoOutput, }) async { try { final response = await _dioClient.post( ApiConstants.videoClearOutput, queryParameters: { 'video_output': videoOutput, }, ); return response.data; } on DioException catch (e) { throw Exception('ClearOutput failed: ${e.message}'); } } /// Close digital contact Future> closeContact({ required int contactId, }) async { try { final response = await _dioClient.post( ApiConstants.digitalIoCloseContact, queryParameters: { 'contact_id': contactId, }, ); return response.data; } on DioException catch (e) { throw Exception('CloseContact failed: ${e.message}'); } } /// Open digital contact Future> openContact({ required int contactId, }) async { try { final response = await _dioClient.post( ApiConstants.digitalIoOpenContact, queryParameters: { 'contact_id': contactId, }, ); return response.data; } on DioException catch (e) { throw Exception('OpenContact failed: ${e.message}'); } } } ``` --- ### Step 3: Test the Integration #### 3.1 Start GeViServer ```bash cd C:\GEVISOFT geviserver.exe console ``` #### 3.2 Start Backend API ```bash cd C:\DEV\COPILOT\geutebruck-api python -m uvicorn src.api.main:app --reload --host 0.0.0.0 --port 8000 ``` #### 3.3 Test with Swagger UI Open: `http://localhost:8000/docs` **Test Sequence:** 1. POST `/api/v1/geviserver/connect` with: ```json { "address": "localhost", "username": "admin", "password": "admin" } ``` 2. GET `/api/v1/geviserver/status` - should show `is_connected: true` 3. POST `/api/v1/geviserver/ping` - should return success 4. POST `/api/v1/geviserver/video/crossswitch?video_input=7&video_output=3` 5. POST `/api/v1/geviserver/disconnect` #### 3.4 Test from Flutter App Your existing Flutter app can now call these endpoints using `DioClient`! --- ## Architecture Benefits ### ✅ Why This is Better 1. **No Native Code in Flutter** - Flutter stays platform-independent - Easier to test and maintain - Can run on mobile later (just API calls) 2. **Backend Handles Complexity** - Connection pooling - Error handling - Password encryption - Message construction 3. **Swagger Documentation** - All endpoints documented automatically - Easy for other clients to integrate - Built-in testing UI 4. **Centralized Logic** - Multiple Flutter clients can connect - Web dashboard can use same API - Mobile app can use same API 5. **Existing Architecture** - Uses your existing DioClient - Uses your existing authentication - Uses your existing error handling --- ## Next Steps 1. ✅ **Implement backend endpoints** (Step 1) 2. ✅ **Add Flutter data source** (Step 2) 3. ✅ **Test with Swagger** (Step 3) 4. **Add BLoC for connection state** 5. **Add UI screens for video control** 6. **Execute action mappings** --- ## Summary **DON'T create a native Windows plugin!** **DO extend your existing FastAPI backend** with GeViServer integration. Your architecture becomes: ``` Flutter (Dart) → HTTP → FastAPI (Python) → ctypes → GeViProcAPI.dll → GeViServer ``` This is the **CORRECT and RECOMMENDED** approach! 🎯