Files
geutebruck/P0_Implementation_Guide.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

40 KiB

P0 Implementation Guide - GeViServer Connection Layer

Priority: P0 - Critical Foundation Estimated Time: 2-3 weeks Prerequisites: GeViSoft SDK installed at C:\GEVISOFT Status: In Progress


Overview

This guide will walk you through implementing the foundational connection layer to GeViServer, enabling your Flutter app to communicate with GeViSoft in real-time.

What We'll Build

  1. Native Windows Plugin - C++ wrapper around GeViProcAPI.dll
  2. Platform Channel - Flutter ↔ Windows communication bridge
  3. Connection Service - Dart service for managing GeViServer connections
  4. Message Classes - Dart classes for constructing GeViSoft messages
  5. Connection BLoC - State management for connection lifecycle

Architecture

┌─────────────────────────────────────────┐
│         Flutter App (Dart)              │
│  ┌───────────────────────────────────┐  │
│  │   GeViServerConnectionBLoC        │  │
│  └──────────────┬────────────────────┘  │
│                 │                        │
│  ┌──────────────▼────────────────────┐  │
│  │   GeViServerService               │  │
│  │   - connect()                     │  │
│  │   - disconnect()                  │  │
│  │   - sendMessage()                 │  │
│  │   - ping()                        │  │
│  └──────────────┬────────────────────┘  │
│                 │                        │
│  ┌──────────────▼────────────────────┐  │
│  │   MethodChannel                   │  │
│  │   'com.geutebruck/geviserver'     │  │
│  └──────────────┬────────────────────┘  │
└─────────────────┼────────────────────────┘
                  │ Platform Channel
┌─────────────────▼────────────────────────┐
│      Windows Plugin (C++)                │
│  ┌───────────────────────────────────┐   │
│  │   GeViServerPlugin                │   │
│  │   - HandleMethodCall()            │   │
│  │   - Connect()                     │   │
│  │   - Disconnect()                  │   │
│  │   - SendMessage()                 │   │
│  └──────────────┬────────────────────┘   │
│                 │                         │
│  ┌──────────────▼────────────────────┐   │
│  │   GeViProcAPI.dll                 │   │
│  │   - GeViAPI_Database_Create()     │   │
│  │   - GeViAPI_Database_Connect()    │   │
│  │   - GeViAPI_Database_SendMessage()│   │
│  └───────────────────────────────────┘   │
└──────────────────────────────────────────┘

Step 1: Create Windows Plugin Project

1.1 Navigate to Windows Directory

cd C:\DEV\COPILOT\geutebruck_app\windows

1.2 Create Plugin Files

We'll create a custom Windows plugin that wraps GeViProcAPI.dll.

File Structure:

windows/
├── geviserver_plugin/
│   ├── geviserver_plugin.h
│   ├── geviserver_plugin.cpp
│   └── CMakeLists.txt
└── CMakeLists.txt (update)

Step 2: GeViServer Plugin Implementation (C++)

2.1 Create geviserver_plugin.h

// File: windows/geviserver_plugin/geviserver_plugin.h

#ifndef GEVISERVER_PLUGIN_H
#define GEVISERVER_PLUGIN_H

#include <flutter/method_channel.h>
#include <flutter/plugin_registrar_windows.h>
#include <flutter/standard_method_codec.h>
#include <memory>
#include <string>
#include <map>

// Forward declare GeViProcAPI types
namespace GeViAPI_Namespace {
    struct GeViHandleStruct;
    typedef GeViHandleStruct* HGeViDatabase;

    enum TServerNotification {
        NFServer_SetupModified = 0,
        NFServer_Disconnected = 10,
        NFServer_GoingShutdown = 11,
        NFServer_NewMessage = 12,
        NFServer_DummyValue = 0x7FFFFFFF
    };

    enum TConnectResult {
        connectOk = 0,
        connectWarningInvalidHeaders = 1,
        connectAborted = 100,
        connectGenericError = 101,
        connectRemoteUnknownUser = 302,
        connectRemoteConnectionLimitExceeded = 303
    };

    struct TMessageEntry {
        char* Buffer;
        int Length;
    };
}

class GeViServerPlugin : public flutter::Plugin {
public:
    static void RegisterWithRegistrar(flutter::PluginRegistrarWindows* registrar);

    GeViServerPlugin(flutter::PluginRegistrarWindows* registrar);
    virtual ~GeViServerPlugin();

private:
    // Method call handler
    void HandleMethodCall(
        const flutter::MethodCall<flutter::EncodableValue>& method_call,
        std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);

    // GeViServer operations
    void Connect(
        const std::string& address,
        const std::string& username,
        const std::string& password,
        std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);

    void Disconnect(
        std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);

    void SendPing(
        std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);

    void SendMessage(
        const std::string& messageBuffer,
        std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);

    void IsConnected(
        std::unique_ptr<flutter::MethodResult<flutter::EncodableValue>> result);

    // Callbacks (static for C-style callback)
    static void __stdcall DatabaseNotificationCallback(
        void* Instance,
        GeViAPI_Namespace::TServerNotification Notification,
        void* Params);

    // Member variables
    flutter::PluginRegistrarWindows* registrar_;
    std::unique_ptr<flutter::MethodChannel<flutter::EncodableValue>> channel_;
    GeViAPI_Namespace::HGeViDatabase database_handle_;
    bool is_connected_;

    // Helper functions
    std::string EncodePassword(const std::string& password);
    std::string GetLastGeViError();
};

#endif // GEVISERVER_PLUGIN_H

2.2 Create geviserver_plugin.cpp

// File: windows/geviserver_plugin/geviserver_plugin.cpp

#include "geviserver_plugin.h"
#include <flutter/event_channel.h>
#include <flutter/event_stream_handler_functions.h>
#include <windows.h>

// Load GeViProcAPI.dll dynamically
#include "C:/GEVISOFT/Examples/VS2010CPP/GeViSoftSDK/Include/GeViProcAPI.h"

// Link against GeViProcAPI.lib
#pragma comment(lib, "C:/GEVISOFT/Examples/VS2010CPP/GeViSoftSDK/Lib/GeViProcAPI.lib")

using namespace GeViAPI_Namespace;
using flutter::EncodableValue;
using flutter::EncodableMap;
using flutter::EncodableList;

// Static instance for callbacks
static GeViServerPlugin* g_plugin_instance = nullptr;

void GeViServerPlugin::RegisterWithRegistrar(
    flutter::PluginRegistrarWindows* registrar) {

    auto channel = std::make_unique<flutter::MethodChannel<EncodableValue>>(
        registrar->messenger(),
        "com.geutebruck/geviserver",
        &flutter::StandardMethodCodec::GetInstance());

    auto plugin = std::make_unique<GeViServerPlugin>(registrar);

    channel->SetMethodCallHandler(
        [plugin_pointer = plugin.get()](const auto& call, auto result) {
            plugin_pointer->HandleMethodCall(call, std::move(result));
        });

    registrar->AddPlugin(std::move(plugin));
}

GeViServerPlugin::GeViServerPlugin(flutter::PluginRegistrarWindows* registrar)
    : registrar_(registrar),
      database_handle_(nullptr),
      is_connected_(false) {

    // Set global instance for callbacks
    g_plugin_instance = this;

    // Create method channel
    channel_ = std::make_unique<flutter::MethodChannel<EncodableValue>>(
        registrar->messenger(),
        "com.geutebruck/geviserver",
        &flutter::StandardMethodCodec::GetInstance());
}

GeViServerPlugin::~GeViServerPlugin() {
    if (is_connected_ && database_handle_) {
        GeViAPI_Database_Disconnect(database_handle_);
    }
    if (database_handle_) {
        GeViAPI_Database_Destroy(database_handle_);
    }
    g_plugin_instance = nullptr;
}

void GeViServerPlugin::HandleMethodCall(
    const flutter::MethodCall<EncodableValue>& method_call,
    std::unique_ptr<flutter::MethodResult<EncodableValue>> result) {

    const std::string& method_name = method_call.method_name();

    if (method_name == "connect") {
        const auto* arguments = std::get_if<EncodableMap>(method_call.arguments());
        if (!arguments) {
            result->Error("INVALID_ARGUMENT", "Arguments must be a map");
            return;
        }

        auto address_it = arguments->find(EncodableValue("address"));
        auto username_it = arguments->find(EncodableValue("username"));
        auto password_it = arguments->find(EncodableValue("password"));

        if (address_it == arguments->end() || username_it == arguments->end() ||
            password_it == arguments->end()) {
            result->Error("MISSING_ARGUMENT", "Missing required arguments");
            return;
        }

        std::string address = std::get<std::string>(address_it->second);
        std::string username = std::get<std::string>(username_it->second);
        std::string password = std::get<std::string>(password_it->second);

        Connect(address, username, password, std::move(result));
    }
    else if (method_name == "disconnect") {
        Disconnect(std::move(result));
    }
    else if (method_name == "sendPing") {
        SendPing(std::move(result));
    }
    else if (method_name == "sendMessage") {
        const auto* arguments = std::get_if<EncodableMap>(method_call.arguments());
        if (!arguments) {
            result->Error("INVALID_ARGUMENT", "Arguments must be a map");
            return;
        }

        auto message_it = arguments->find(EncodableValue("message"));
        if (message_it == arguments->end()) {
            result->Error("MISSING_ARGUMENT", "Missing message argument");
            return;
        }

        std::string message = std::get<std::string>(message_it->second);
        SendMessage(message, std::move(result));
    }
    else if (method_name == "isConnected") {
        IsConnected(std::move(result));
    }
    else {
        result->NotImplemented();
    }
}

void GeViServerPlugin::Connect(
    const std::string& address,
    const std::string& username,
    const std::string& password,
    std::unique_ptr<flutter::MethodResult<EncodableValue>> result) {

    // Disconnect if already connected
    if (is_connected_ && database_handle_) {
        GeViAPI_Database_Disconnect(database_handle_);
        is_connected_ = false;
    }

    // Destroy old handle if exists
    if (database_handle_) {
        GeViAPI_Database_Destroy(database_handle_);
        database_handle_ = nullptr;
    }

    // Encode password
    std::string encoded_password = EncodePassword(password);

    // Create database handle
    bool create_result = GeViAPI_Database_Create(
        database_handle_,
        "FlutterApp",          // Alias name
        address.c_str(),       // Server address (e.g., "localhost" or "192.168.1.100")
        username.c_str(),      // Username
        encoded_password.c_str(), // Encrypted password
        nullptr,               // Username2 (optional)
        nullptr);              // Password2 (optional)

    if (!create_result || !database_handle_) {
        result->Error("CREATE_FAILED", GetLastGeViError());
        return;
    }

    // Set notification callback
    GeViAPI_Database_SetCBNotification(
        database_handle_,
        &GeViServerPlugin::DatabaseNotificationCallback,
        this);

    // Connect to database
    TConnectResult connect_result;
    bool connect_ok = GeViAPI_Database_Connect(
        database_handle_,
        connect_result,
        nullptr,  // Progress callback (optional)
        nullptr); // Instance

    if (!connect_ok || connect_result != connectOk) {
        std::string error_msg = GetLastGeViError();
        result->Error("CONNECT_FAILED",
            "Connect result: " + std::to_string((int)connect_result) +
            ", Error: " + error_msg);

        GeViAPI_Database_Destroy(database_handle_);
        database_handle_ = nullptr;
        return;
    }

    is_connected_ = true;

    EncodableMap response;
    response[EncodableValue("success")] = EncodableValue(true);
    response[EncodableValue("message")] = EncodableValue("Connected to GeViServer");

    result->Success(EncodableValue(response));
}

void GeViServerPlugin::Disconnect(
    std::unique_ptr<flutter::MethodResult<EncodableValue>> result) {

    if (!database_handle_) {
        result->Error("NOT_CONNECTED", "Not connected to GeViServer");
        return;
    }

    if (is_connected_) {
        GeViAPI_Database_Disconnect(database_handle_);
        is_connected_ = false;
    }

    GeViAPI_Database_Destroy(database_handle_);
    database_handle_ = nullptr;

    EncodableMap response;
    response[EncodableValue("success")] = EncodableValue(true);
    response[EncodableValue("message")] = EncodableValue("Disconnected from GeViServer");

    result->Success(EncodableValue(response));
}

void GeViServerPlugin::SendPing(
    std::unique_ptr<flutter::MethodResult<EncodableValue>> result) {

    if (!is_connected_ || !database_handle_) {
        result->Error("NOT_CONNECTED", "Not connected to GeViServer");
        return;
    }

    bool ping_result = GeViAPI_Database_SendPing(database_handle_);

    if (!ping_result) {
        result->Error("PING_FAILED", GetLastGeViError());
        return;
    }

    EncodableMap response;
    response[EncodableValue("success")] = EncodableValue(true);
    response[EncodableValue("message")] = EncodableValue("Ping successful");

    result->Success(EncodableValue(response));
}

void GeViServerPlugin::SendMessage(
    const std::string& messageBuffer,
    std::unique_ptr<flutter::MethodResult<EncodableValue>> result) {

    if (!is_connected_ || !database_handle_) {
        result->Error("NOT_CONNECTED", "Not connected to GeViServer");
        return;
    }

    bool send_result = GeViAPI_Database_SendMessage(
        database_handle_,
        messageBuffer.c_str(),
        static_cast<int>(messageBuffer.length()));

    if (!send_result) {
        result->Error("SEND_FAILED", GetLastGeViError());
        return;
    }

    EncodableMap response;
    response[EncodableValue("success")] = EncodableValue(true);
    response[EncodableValue("message")] = EncodableValue("Message sent successfully");

    result->Success(EncodableValue(response));
}

void GeViServerPlugin::IsConnected(
    std::unique_ptr<flutter::MethodResult<EncodableValue>> result) {

    bool connected = false;

    if (database_handle_) {
        GeViAPI_Database_Connected(database_handle_, connected);
    }

    EncodableMap response;
    response[EncodableValue("isConnected")] = EncodableValue(connected);

    result->Success(EncodableValue(response));
}

// Callback for database notifications
void __stdcall GeViServerPlugin::DatabaseNotificationCallback(
    void* Instance,
    TServerNotification Notification,
    void* Params) {

    GeViServerPlugin* plugin = static_cast<GeViServerPlugin*>(Instance);
    if (!plugin || !plugin->channel_) return;

    EncodableMap event;
    event[EncodableValue("type")] = EncodableValue("notification");

    switch (Notification) {
    case NFServer_NewMessage:
        {
            TMessageEntry* entry = static_cast<TMessageEntry*>(Params);
            if (entry && entry->Buffer && entry->Length > 0) {
                std::string message_data(entry->Buffer, entry->Length);
                event[EncodableValue("notification")] = EncodableValue("newMessage");
                event[EncodableValue("data")] = EncodableValue(message_data);
            }
        }
        break;
    case NFServer_Disconnected:
        plugin->is_connected_ = false;
        event[EncodableValue("notification")] = EncodableValue("disconnected");
        break;
    case NFServer_GoingShutdown:
        event[EncodableValue("notification")] = EncodableValue("shutdown");
        break;
    case NFServer_SetupModified:
        event[EncodableValue("notification")] = EncodableValue("setupModified");
        break;
    default:
        return;
    }

    // Send event to Flutter via method channel
    plugin->channel_->InvokeMethod("onNotification", std::make_unique<EncodableValue>(event));
}

std::string GeViServerPlugin::EncodePassword(const std::string& password) {
    char encoded[33] = {0}; // 32 bytes + null terminator

    bool result = GeViAPI_EncodeString(encoded, password.c_str(), sizeof(encoded));

    if (!result) {
        return "";
    }

    return std::string(encoded);
}

std::string GeViServerPlugin::GetLastGeViError() {
    int exception_class, error_no, error_class;
    char* error_message = nullptr;

    bool result = GeViAPI_GetLastError(
        exception_class,
        error_no,
        error_class,
        error_message);

    if (!result || !error_message) {
        return "Unknown error";
    }

    std::string error_str(error_message);
    GeViAPI_FreePointer(error_message);

    return error_str;
}

2.3 Update windows/CMakeLists.txt

Add this to your windows/CMakeLists.txt:

# Add GeViServer plugin
add_subdirectory(geviserver_plugin)

# Link plugin to runner
target_link_libraries(${BINARY_NAME} PRIVATE geviserver_plugin)

2.4 Create windows/geviserver_plugin/CMakeLists.txt

cmake_minimum_required(VERSION 3.14)
project(geviserver_plugin)

set(PLUGIN_NAME "geviserver_plugin")

add_library(${PLUGIN_NAME} STATIC
  "geviserver_plugin.cpp"
  "geviserver_plugin.h"
)

# Include directories
target_include_directories(${PLUGIN_NAME} PUBLIC
  "${CMAKE_CURRENT_SOURCE_DIR}"
  "C:/GEVISOFT/Examples/VS2010CPP/GeViSoftSDK/Include"
)

# Link directories
target_link_directories(${PLUGIN_NAME} PUBLIC
  "C:/GEVISOFT/Examples/VS2010CPP/GeViSoftSDK/Lib"
)

# Link libraries
target_link_libraries(${PLUGIN_NAME} PRIVATE
  flutter
  flutter_wrapper_plugin
)

# Platform-specific compile definitions
target_compile_definitions(${PLUGIN_NAME} PRIVATE
  UNICODE
  _UNICODE
)

set_target_properties(${PLUGIN_NAME} PROPERTIES
  CXX_VISIBILITY_PRESET hidden)

Step 3: Dart Service Layer

Now let's create the Dart side of the implementation.

3.1 Create lib/data/services/geviserver_service.dart

import 'dart:async';
import 'package:flutter/services.dart';

class GeViServerService {
  static const MethodChannel _channel =
      MethodChannel('com.geutebruck/geviserver');

  static GeViServerService? _instance;

  // Singleton
  factory GeViServerService() {
    _instance ??= GeViServerService._internal();
    return _instance!;
  }

  GeViServerService._internal() {
    _channel.setMethodCallHandler(_handleNotification);
  }

  // Connection state
  bool _isConnected = false;
  String? _connectedAddress;

  // Notification stream
  final StreamController<GeViServerNotification> _notificationController =
      StreamController<GeViServerNotification>.broadcast();

  Stream<GeViServerNotification> get notificationStream =>
      _notificationController.stream;

  bool get isConnected => _isConnected;
  String? get connectedAddress => _connectedAddress;

  /// Connect to GeViServer
  Future<GeViServerConnectionResult> connect({
    required String address,
    required String username,
    required String password,
  }) async {
    try {
      final Map<String, dynamic> result = Map<String, dynamic>.from(
        await _channel.invokeMethod('connect', {
          'address': address,
          'username': username,
          'password': password,
        }),
      );

      if (result['success'] == true) {
        _isConnected = true;
        _connectedAddress = address;

        return GeViServerConnectionResult(
          success: true,
          message: result['message'] as String? ?? 'Connected successfully',
        );
      } else {
        return GeViServerConnectionResult(
          success: false,
          message: result['message'] as String? ?? 'Connection failed',
        );
      }
    } on PlatformException catch (e) {
      _isConnected = false;
      _connectedAddress = null;

      return GeViServerConnectionResult(
        success: false,
        message: 'Connection error: ${e.message}',
        errorCode: e.code,
      );
    } catch (e) {
      _isConnected = false;
      _connectedAddress = null;

      return GeViServerConnectionResult(
        success: false,
        message: 'Unknown error: $e',
      );
    }
  }

  /// Disconnect from GeViServer
  Future<GeViServerConnectionResult> disconnect() async {
    try {
      final Map<String, dynamic> result = Map<String, dynamic>.from(
        await _channel.invokeMethod('disconnect'),
      );

      _isConnected = false;
      _connectedAddress = null;

      return GeViServerConnectionResult(
        success: result['success'] as bool? ?? true,
        message: result['message'] as String? ?? 'Disconnected successfully',
      );
    } on PlatformException catch (e) {
      return GeViServerConnectionResult(
        success: false,
        message: 'Disconnect error: ${e.message}',
        errorCode: e.code,
      );
    }
  }

  /// Send ping to check connection
  Future<bool> sendPing() async {
    try {
      final Map<String, dynamic> result = Map<String, dynamic>.from(
        await _channel.invokeMethod('sendPing'),
      );

      return result['success'] as bool? ?? false;
    } on PlatformException catch (e) {
      print('Ping failed: ${e.message}');
      return false;
    }
  }

  /// Check if connected
  Future<bool> checkConnection() async {
    try {
      final Map<String, dynamic> result = Map<String, dynamic>.from(
        await _channel.invokeMethod('isConnected'),
      );

      _isConnected = result['isConnected'] as bool? ?? false;
      return _isConnected;
    } on PlatformException catch (e) {
      print('Check connection failed: ${e.message}');
      _isConnected = false;
      return false;
    }
  }

  /// Send message to GeViServer
  Future<bool> sendMessage(String message) async {
    try {
      final Map<String, dynamic> result = Map<String, dynamic>.from(
        await _channel.invokeMethod('sendMessage', {
          'message': message,
        }),
      );

      return result['success'] as bool? ?? false;
    } on PlatformException catch (e) {
      print('Send message failed: ${e.message}');
      return false;
    }
  }

  /// Handle notifications from native side
  Future<void> _handleNotification(MethodCall call) async {
    if (call.method == 'onNotification') {
      final Map<String, dynamic> data = Map<String, dynamic>.from(call.arguments);
      final String? notificationType = data['notification'] as String?;

      if (notificationType != null) {
        final notification = GeViServerNotification(
          type: notificationType,
          data: data['data'] as String?,
        );

        _notificationController.add(notification);

        // Update connection state
        if (notificationType == 'disconnected') {
          _isConnected = false;
          _connectedAddress = null;
        }
      }
    }
  }

  void dispose() {
    _notificationController.close();
  }
}

/// Connection result
class GeViServerConnectionResult {
  final bool success;
  final String message;
  final String? errorCode;

  GeViServerConnectionResult({
    required this.success,
    required this.message,
    this.errorCode,
  });
}

/// Server notification
class GeViServerNotification {
  final String type; // 'newMessage', 'disconnected', 'shutdown', 'setupModified'
  final String? data;

  GeViServerNotification({
    required this.type,
    this.data,
  });
}

Step 4: Connection BLoC

4.1 Create lib/presentation/blocs/geviserver_connection/geviserver_connection_event.dart

import 'package:equatable/equatable.dart';

abstract class GeViServerConnectionEvent extends Equatable {
  const GeViServerConnectionEvent();

  @list
  List<Object?> get props => [];
}

class ConnectToServerEvent extends GeViServerConnectionEvent {
  final String address;
  final String username;
  final String password;

  const ConnectToServerEvent({
    required this.address,
    required this.username,
    required this.password,
  });

  @override
  List<Object?> get props => [address, username, password];
}

class DisconnectFromServerEvent extends GeViServerConnectionEvent {
  const DisconnectFromServerEvent();
}

class CheckConnectionEvent extends GeViServerConnectionEvent {
  const CheckConnectionEvent();
}

class SendPingEvent extends GeViServerConnectionEvent {
  const SendPingEvent();
}

class ServerNotificationReceivedEvent extends GeViServerConnectionEvent {
  final String notificationType;
  final String? data;

  const ServerNotificationReceivedEvent({
    required this.notificationType,
    this.data,
  });

  @override
  List<Object?> get props => [notificationType, data];
}

4.2 Create lib/presentation/blocs/geviserver_connection/geviserver_connection_state.dart

import 'package:equatable/equatable.dart';

abstract class GeViServerConnectionState extends Equatable {
  const GeViServerConnectionState();

  @override
  List<Object?> get props => [];
}

class GeViServerConnectionInitial extends GeViServerConnectionState {}

class GeViServerConnecting extends GeViServerConnectionState {}

class GeViServerConnected extends GeViServerConnectionState {
  final String address;
  final DateTime connectedAt;

  const GeViServerConnected({
    required this.address,
    required this.connectedAt,
  });

  @override
  List<Object?> get props => [address, connectedAt];
}

class GeViServerDisconnected extends GeViServerConnectionState {
  final String? reason;

  const GeViServerDisconnected({this.reason});

  @override
  List<Object?> get props => [reason];
}

class GeViServerConnectionError extends GeViServerConnectionState {
  final String message;
  final String? errorCode;

  const GeViServerConnectionError({
    required this.message,
    this.errorCode,
  });

  @override
  List<Object?> get props => [message, errorCode];
}

class GeViServerPingSuccess extends GeViServerConnectionState {
  final DateTime timestamp;

  const GeViServerPingSuccess({required this.timestamp});

  @override
  List<Object?> get props => [timestamp];
}

4.3 Create lib/presentation/blocs/geviserver_connection/geviserver_connection_bloc.dart

import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../data/services/geviserver_service.dart';
import 'geviserver_connection_event.dart';
import 'geviserver_connection_state.dart';

class GeViServerConnectionBloc
    extends Bloc<GeViServerConnectionEvent, GeViServerConnectionState> {
  final GeViServerService _geviServerService;
  StreamSubscription? _notificationSubscription;
  Timer? _pingTimer;

  GeViServerConnectionBloc({GeViServerService? geviServerService})
      : _geviServerService = geviServerService ?? GeViServerService(),
        super(GeViServerConnectionInitial()) {
    on<ConnectToServerEvent>(_onConnect);
    on<DisconnectFromServerEvent>(_onDisconnect);
    on<CheckConnectionEvent>(_onCheckConnection);
    on<SendPingEvent>(_onSendPing);
    on<ServerNotificationReceivedEvent>(_onServerNotification);

    // Listen to server notifications
    _notificationSubscription =
        _geviServerService.notificationStream.listen((notification) {
      add(ServerNotificationReceivedEvent(
        notificationType: notification.type,
        data: notification.data,
      ));
    });
  }

  Future<void> _onConnect(
    ConnectToServerEvent event,
    Emitter<GeViServerConnectionState> emit,
  ) async {
    emit(GeViServerConnecting());

    final result = await _geviServerService.connect(
      address: event.address,
      username: event.username,
      password: event.password,
    );

    if (result.success) {
      emit(GeViServerConnected(
        address: event.address,
        connectedAt: DateTime.now(),
      ));

      // Start periodic ping (every 10 seconds)
      _startPingTimer();
    } else {
      emit(GeViServerConnectionError(
        message: result.message,
        errorCode: result.errorCode,
      ));
    }
  }

  Future<void> _onDisconnect(
    DisconnectFromServerEvent event,
    Emitter<GeViServerConnectionState> emit,
  ) async {
    _stopPingTimer();

    final result = await _geviServerService.disconnect();

    emit(GeViServerDisconnected(reason: result.message));
  }

  Future<void> _onCheckConnection(
    CheckConnectionEvent event,
    Emitter<GeViServerConnectionState> emit,
  ) async {
    final isConnected = await _geviServerService.checkConnection();

    if (!isConnected && state is GeViServerConnected) {
      emit(const GeViServerDisconnected(reason: 'Connection lost'));
    }
  }

  Future<void> _onSendPing(
    SendPingEvent event,
    Emitter<GeViServerConnectionState> emit,
  ) async {
    final success = await _geviServerService.sendPing();

    if (success) {
      emit(GeViServerPingSuccess(timestamp: DateTime.now()));

      // Restore previous connected state
      if (_geviServerService.connectedAddress != null) {
        emit(GeViServerConnected(
          address: _geviServerService.connectedAddress!,
          connectedAt: DateTime.now(),
        ));
      }
    } else {
      emit(const GeViServerDisconnected(reason: 'Ping failed'));
    }
  }

  Future<void> _onServerNotification(
    ServerNotificationReceivedEvent event,
    Emitter<GeViServerConnectionState> emit,
  ) async {
    if (event.notificationType == 'disconnected') {
      _stopPingTimer();
      emit(const GeViServerDisconnected(reason: 'Server disconnected'));
    } else if (event.notificationType == 'shutdown') {
      _stopPingTimer();
      emit(const GeViServerDisconnected(reason: 'Server shutting down'));
    }
  }

  void _startPingTimer() {
    _stopPingTimer();
    _pingTimer = Timer.periodic(const Duration(seconds: 10), (_) {
      add(const SendPingEvent());
    });
  }

  void _stopPingTimer() {
    _pingTimer?.cancel();
    _pingTimer = null;
  }

  @override
  Future<void> close() {
    _stopPingTimer();
    _notificationSubscription?.cancel();
    return super.close();
  }
}

Step 5: Register Plugin and Update Dependencies

5.1 Update windows/runner/flutter_window.cpp

Add plugin registration:

#include "geviserver_plugin/geviserver_plugin.h"

// In the CreateAndShow method, after other plugin registrations:
GeViServerPlugin::RegisterWithRegistrar(
    window->GetRegistrar(window->GetPluginRegistrar()));

5.2 Update lib/injection.dart

// Register GeViServer service
getIt.registerLazySingleton<GeViServerService>(() => GeViServerService());

// Register GeViServerConnectionBloc
getIt.registerFactory<GeViServerConnectionBloc>(
  () => GeViServerConnectionBloc(geviServerService: getIt<GeViServerService>()),
);

Step 6: Testing

6.1 Start GeViServer

cd C:\GEVISOFT
geviserver.exe console

6.2 Create Test Screen

Create lib/presentation/screens/geviserver_test_screen.dart:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/geviserver_connection/geviserver_connection_bloc.dart';
import '../blocs/geviserver_connection/geviserver_connection_event.dart';
import '../blocs/geviserver_connection/geviserver_connection_state.dart';

class GeViServerTestScreen extends StatefulWidget {
  const GeViServerTestScreen({Key? key}) : super(key: key);

  @override
  State<GeViServerTestScreen> createState() => _GeViServerTestScreenState();
}

class _GeViServerTestScreenState extends State<GeViServerTestScreen> {
  final _addressController = TextEditingController(text: 'localhost');
  final _usernameController = TextEditingController(text: 'admin');
  final _passwordController = TextEditingController(text: 'admin');

  @override
  void dispose() {
    _addressController.dispose();
    _usernameController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('GeViServer Connection Test'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            TextField(
              controller: _addressController,
              decoration: const InputDecoration(
                labelText: 'Server Address',
                hintText: 'localhost or IP address',
              ),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _usernameController,
              decoration: const InputDecoration(
                labelText: 'Username',
              ),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _passwordController,
              decoration: const InputDecoration(
                labelText: 'Password',
              ),
              obscureText: true,
            ),
            const SizedBox(height: 24),
            BlocBuilder<GeViServerConnectionBloc, GeViServerConnectionState>(
              builder: (context, state) {
                if (state is GeViServerConnecting) {
                  return const Center(child: CircularProgressIndicator());
                }

                if (state is GeViServerConnected) {
                  return Column(
                    children: [
                      ElevatedButton(
                        onPressed: () {
                          context
                              .read<GeViServerConnectionBloc>()
                              .add(const DisconnectFromServerEvent());
                        },
                        style: ElevatedButton.styleFrom(
                          backgroundColor: Colors.red,
                        ),
                        child: const Text('Disconnect'),
                      ),
                      const SizedBox(height: 16),
                      ElevatedButton(
                        onPressed: () {
                          context
                              .read<GeViServerConnectionBloc>()
                              .add(const SendPingEvent());
                        },
                        child: const Text('Send Ping'),
                      ),
                    ],
                  );
                }

                return ElevatedButton(
                  onPressed: () {
                    context.read<GeViServerConnectionBloc>().add(
                          ConnectToServerEvent(
                            address: _addressController.text,
                            username: _usernameController.text,
                            password: _passwordController.text,
                          ),
                        );
                  },
                  child: const Text('Connect'),
                );
              },
            ),
            const SizedBox(height: 24),
            Expanded(
              child: BlocBuilder<GeViServerConnectionBloc,
                  GeViServerConnectionState>(
                builder: (context, state) {
                  return Card(
                    child: Padding(
                      padding: const EdgeInsets.all(16.0),
                      child: Column(
                        crossAxisAlignment: CrossAxisAlignment.start,
                        children: [
                          const Text(
                            'Status:',
                            style: TextStyle(
                              fontWeight: FontWeight.bold,
                              fontSize: 18,
                            ),
                          ),
                          const SizedBox(height: 8),
                          Text(_getStatusText(state)),
                          if (state is GeViServerConnected) ...[
                            const SizedBox(height: 8),
                            Text('Connected to: ${state.address}'),
                            Text('At: ${state.connectedAt}'),
                          ],
                          if (state is GeViServerConnectionError) ...[
                            const SizedBox(height: 8),
                            Text(
                              'Error: ${state.message}',
                              style: const TextStyle(color: Colors.red),
                            ),
                            if (state.errorCode != null)
                              Text('Code: ${state.errorCode}'),
                          ],
                        ],
                      ),
                    ),
                  );
                },
              ),
            ),
          ],
        ),
      ),
    );
  }

  String _getStatusText(GeViServerConnectionState state) {
    if (state is GeViServerConnectionInitial) {
      return 'Not connected';
    } else if (state is GeViServerConnecting) {
      return 'Connecting...';
    } else if (state is GeViServerConnected) {
      return '✓ Connected';
    } else if (state is GeViServerDisconnected) {
      return 'Disconnected: ${state.reason ?? "Unknown"}';
    } else if (state is GeViServerConnectionError) {
      return 'Error';
    } else if (state is GeViServerPingSuccess) {
      return '✓ Ping successful';
    }
    return 'Unknown state';
  }
}

6.3 Update App Routes

Add the test screen to your app:

// In main.dart or routes
MaterialPageRoute(
  builder: (_) => BlocProvider(
    create: (_) => getIt<GeViServerConnectionBloc>(),
    child: const GeViServerTestScreen(),
  ),
)

Step 7: Build and Test

7.1 Build the Windows App

cd C:\DEV\COPILOT\geutebruck_app
flutter build windows

7.2 Run the App

flutter run -d windows

7.3 Test Connection

  1. Start GeViServer: C:\GEVISOFT\geviserver.exe console
  2. Open the GeViServer Test screen in your app
  3. Enter:
    • Address: localhost
    • Username: admin
    • Password: admin (default)
  4. Click "Connect"
  5. Verify connection successful
  6. Click "Send Ping" to test ping
  7. Click "Disconnect"

Success Criteria

P0 Complete When:

  1. Flutter app can connect to GeViServer
  2. Connection status is tracked correctly
  3. Ping works and auto-reconnect functions
  4. Messages can be sent (basic text messages)
  5. Disconnection is clean

Next Steps (P1)

Once P0 is complete, you'll be ready for:

  • P1 Phase 1: Video control actions (CrossSwitch, ClearOutput)
  • P1 Phase 2: Digital I/O actions
  • P1 Phase 3: State queries

Troubleshooting

Common Issues

1. DLL Not Found

Error: GeViProcAPI.dll not found

Solution: Copy DLLs to output directory:

copy C:\GEVISOFT\GeViProcAPI.dll C:\DEV\COPILOT\geutebruck_app\build\windows\runner\Release\
copy C:\GEVISOFT\GscActions.dll C:\DEV\COPILOT\geutebruck_app\build\windows\runner\Release\

2. Connection Failed

Error: connectRemoteUnknownUser

Solution: Check GeViServer is running and credentials are correct

3. Build Errors

Error: Cannot open include file: 'GeViProcAPI.h'

Solution: Verify paths in CMakeLists.txt match your installation


Summary

You now have:

  • Native Windows plugin wrapping GeViProcAPI.dll
  • Platform channel for Flutter ↔ Native communication
  • Dart service layer for GeViServer operations
  • BLoC for connection state management
  • Test screen to verify functionality

Ready to implement? Let's start with Step 1!