- 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>
1389 lines
40 KiB
Markdown
1389 lines
40 KiB
Markdown
# 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 <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`
|
|
|
|
```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`:
|
|
|
|
```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<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`
|
|
|
|
```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`
|
|
|
|
```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`
|
|
|
|
```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:
|
|
|
|
```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>(() => GeViServerService());
|
|
|
|
// Register GeViServerConnectionBloc
|
|
getIt.registerFactory<GeViServerConnectionBloc>(
|
|
() => GeViServerConnectionBloc(geviServerService: getIt<GeViServerService>()),
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## 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<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:
|
|
|
|
```dart
|
|
// 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
|
|
|
|
```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!**
|