# 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 ```bash 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` ```cpp // File: windows/geviserver_plugin/geviserver_plugin.h #ifndef GEVISERVER_PLUGIN_H #define GEVISERVER_PLUGIN_H #include #include #include #include #include #include // 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& method_call, std::unique_ptr> result); // GeViServer operations void Connect( const std::string& address, const std::string& username, const std::string& password, std::unique_ptr> result); void Disconnect( std::unique_ptr> result); void SendPing( std::unique_ptr> result); void SendMessage( const std::string& messageBuffer, std::unique_ptr> result); void IsConnected( std::unique_ptr> 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> 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` ```cpp // File: windows/geviserver_plugin/geviserver_plugin.cpp #include "geviserver_plugin.h" #include #include #include // 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>( registrar->messenger(), "com.geutebruck/geviserver", &flutter::StandardMethodCodec::GetInstance()); auto plugin = std::make_unique(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>( 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& method_call, std::unique_ptr> result) { const std::string& method_name = method_call.method_name(); if (method_name == "connect") { const auto* arguments = std::get_if(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(address_it->second); std::string username = std::get(username_it->second); std::string password = std::get(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(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(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> 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> 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> 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> 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(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> 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(Instance); if (!plugin || !plugin->channel_) return; EncodableMap event; event[EncodableValue("type")] = EncodableValue("notification"); switch (Notification) { case NFServer_NewMessage: { TMessageEntry* entry = static_cast(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(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`: ```cmake # 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 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` ```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 _notificationController = StreamController.broadcast(); Stream get notificationStream => _notificationController.stream; bool get isConnected => _isConnected; String? get connectedAddress => _connectedAddress; /// Connect to GeViServer Future connect({ required String address, required String username, required String password, }) async { try { final Map result = Map.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 disconnect() async { try { final Map result = Map.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 sendPing() async { try { final Map result = Map.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 checkConnection() async { try { final Map result = Map.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 sendMessage(String message) async { try { final Map result = Map.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 _handleNotification(MethodCall call) async { if (call.method == 'onNotification') { final Map data = Map.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` ```dart import 'package:equatable/equatable.dart'; abstract class GeViServerConnectionEvent extends Equatable { const GeViServerConnectionEvent(); @list List 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 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 get props => [notificationType, data]; } ``` ### 4.2 Create `lib/presentation/blocs/geviserver_connection/geviserver_connection_state.dart` ```dart import 'package:equatable/equatable.dart'; abstract class GeViServerConnectionState extends Equatable { const GeViServerConnectionState(); @override List 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 get props => [address, connectedAt]; } class GeViServerDisconnected extends GeViServerConnectionState { final String? reason; const GeViServerDisconnected({this.reason}); @override List get props => [reason]; } class GeViServerConnectionError extends GeViServerConnectionState { final String message; final String? errorCode; const GeViServerConnectionError({ required this.message, this.errorCode, }); @override List get props => [message, errorCode]; } class GeViServerPingSuccess extends GeViServerConnectionState { final DateTime timestamp; const GeViServerPingSuccess({required this.timestamp}); @override List get props => [timestamp]; } ``` ### 4.3 Create `lib/presentation/blocs/geviserver_connection/geviserver_connection_bloc.dart` ```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 { final GeViServerService _geviServerService; StreamSubscription? _notificationSubscription; Timer? _pingTimer; GeViServerConnectionBloc({GeViServerService? geviServerService}) : _geviServerService = geviServerService ?? GeViServerService(), super(GeViServerConnectionInitial()) { on(_onConnect); on(_onDisconnect); on(_onCheckConnection); on(_onSendPing); on(_onServerNotification); // Listen to server notifications _notificationSubscription = _geviServerService.notificationStream.listen((notification) { add(ServerNotificationReceivedEvent( notificationType: notification.type, data: notification.data, )); }); } Future _onConnect( ConnectToServerEvent event, Emitter 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 _onDisconnect( DisconnectFromServerEvent event, Emitter emit, ) async { _stopPingTimer(); final result = await _geviServerService.disconnect(); emit(GeViServerDisconnected(reason: result.message)); } Future _onCheckConnection( CheckConnectionEvent event, Emitter emit, ) async { final isConnected = await _geviServerService.checkConnection(); if (!isConnected && state is GeViServerConnected) { emit(const GeViServerDisconnected(reason: 'Connection lost')); } } Future _onSendPing( SendPingEvent event, Emitter 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 _onServerNotification( ServerNotificationReceivedEvent event, Emitter 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 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: ```cpp #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` ```dart // Register GeViServer service getIt.registerLazySingleton(() => GeViServerService()); // Register GeViServerConnectionBloc getIt.registerFactory( () => GeViServerConnectionBloc(geviServerService: getIt()), ); ``` --- ## Step 6: Testing ### 6.1 Start GeViServer ```bash cd C:\GEVISOFT geviserver.exe console ``` ### 6.2 Create Test Screen Create `lib/presentation/screens/geviserver_test_screen.dart`: ```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 createState() => _GeViServerTestScreenState(); } class _GeViServerTestScreenState extends State { 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( builder: (context, state) { if (state is GeViServerConnecting) { return const Center(child: CircularProgressIndicator()); } if (state is GeViServerConnected) { return Column( children: [ ElevatedButton( onPressed: () { context .read() .add(const DisconnectFromServerEvent()); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.red, ), child: const Text('Disconnect'), ), const SizedBox(height: 16), ElevatedButton( onPressed: () { context .read() .add(const SendPingEvent()); }, child: const Text('Send Ping'), ), ], ); } return ElevatedButton( onPressed: () { context.read().add( ConnectToServerEvent( address: _addressController.text, username: _usernameController.text, password: _passwordController.text, ), ); }, child: const Text('Connect'), ); }, ), const SizedBox(height: 24), Expanded( child: BlocBuilder( 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: ```dart // In main.dart or routes MaterialPageRoute( builder: (_) => BlocProvider( create: (_) => getIt(), child: const GeViServerTestScreen(), ), ) ``` --- ## Step 7: Build and Test ### 7.1 Build the Windows App ```bash cd C:\DEV\COPILOT\geutebruck_app flutter build windows ``` ### 7.2 Run the App ```bash 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: ```bash 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!**