Files
geutebruck/P0_Backend_API_Approach.md
Administrator a92b909539 feat: GeViScope SDK integration with C# Bridge and Flutter app
- Add GeViScope Bridge (C# .NET 8.0) on port 7720
  - Full SDK wrapper for camera control, PTZ, actions/events
  - 17 REST API endpoints for GeViScope server interaction
  - Support for MCS (Media Channel Simulator) with 16 test channels
  - Real-time action/event streaming via PLC callbacks

- Add GeViServer Bridge (C# .NET 8.0) on port 7710
  - Integration with GeViSoft orchestration layer
  - Input/output control and event management

- Update Python API with new routers
  - /api/geviscope/* - Proxy to GeViScope Bridge
  - /api/geviserver/* - Proxy to GeViServer Bridge
  - /api/excel/* - Excel import functionality

- Add Flutter app GeViScope integration
  - GeViScopeRemoteDataSource with 17 API methods
  - GeViScopeBloc for state management
  - GeViScopeScreen with PTZ controls
  - App drawer navigation to GeViScope

- Add SDK documentation (extracted from PDFs)
  - GeViScope SDK docs (7 parts + action reference)
  - GeViSoft SDK docs (12 chunks)

- Add .mcp.json for Claude Code MCP server config

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 08:14:17 +01:00

32 KiB

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

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

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

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:

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

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

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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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

cd C:\GEVISOFT
geviserver.exe console

3.2 Start Backend API

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:
    {
      "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! 🎯