- 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>
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
- Native Windows Plugin - C++ wrapper around GeViProcAPI.dll
- Platform Channel - Flutter ↔ Windows communication bridge
- Connection Service - Dart service for managing GeViServer connections
- Message Classes - Dart classes for constructing GeViSoft messages
- 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
- Start GeViServer:
C:\GEVISOFT\geviserver.exe console - Open the GeViServer Test screen in your app
- Enter:
- Address:
localhost - Username:
admin - Password:
admin(default)
- Address:
- Click "Connect"
- Verify connection successful
- Click "Send Ping" to test ping
- Click "Disconnect"
Success Criteria
✅ P0 Complete When:
- Flutter app can connect to GeViServer
- Connection status is tracked correctly
- Ping works and auto-reconnect functions
- Messages can be sent (basic text messages)
- 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!