- 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>
1033 lines
32 KiB
Markdown
1033 lines
32 KiB
Markdown
# 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<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
|
|
|
|
```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! 🎯
|