feat: GeViScope SDK integration with C# Bridge and Flutter app

- Add GeViScope Bridge (C# .NET 8.0) on port 7720
  - Full SDK wrapper for camera control, PTZ, actions/events
  - 17 REST API endpoints for GeViScope server interaction
  - Support for MCS (Media Channel Simulator) with 16 test channels
  - Real-time action/event streaming via PLC callbacks

- Add GeViServer Bridge (C# .NET 8.0) on port 7710
  - Integration with GeViSoft orchestration layer
  - Input/output control and event management

- Update Python API with new routers
  - /api/geviscope/* - Proxy to GeViScope Bridge
  - /api/geviserver/* - Proxy to GeViServer Bridge
  - /api/excel/* - Excel import functionality

- Add Flutter app GeViScope integration
  - GeViScopeRemoteDataSource with 17 API methods
  - GeViScopeBloc for state management
  - GeViScopeScreen with PTZ controls
  - App drawer navigation to GeViScope

- Add SDK documentation (extracted from PDFs)
  - GeViScope SDK docs (7 parts + action reference)
  - GeViSoft SDK docs (12 chunks)

- Add .mcp.json for Claude Code MCP server config

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Administrator
2026-01-19 08:14:17 +01:00
parent c9e83e4277
commit a92b909539
76 changed files with 62101 additions and 176 deletions

View File

@@ -0,0 +1,499 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../data/data_sources/remote/geviscope_remote_data_source.dart';
import 'geviscope_event.dart';
import 'geviscope_state.dart';
class GeViScopeBloc extends Bloc<GeViScopeEvent, GeViScopeState> {
final GeViScopeRemoteDataSource remoteDataSource;
GeViScopeBloc({required this.remoteDataSource})
: super(const GeViScopeState()) {
on<ConnectGeViScopeEvent>(_onConnect);
on<DisconnectGeViScopeEvent>(_onDisconnect);
on<CheckGeViScopeStatusEvent>(_onCheckStatus);
on<LoadChannelsEvent>(_onLoadChannels);
on<RefreshChannelsEvent>(_onRefreshChannels);
on<SendGeViScopeCrossSwitchEvent>(_onSendCrossSwitch);
on<CameraPanEvent>(_onCameraPan);
on<CameraTiltEvent>(_onCameraTilt);
on<CameraZoomEvent>(_onCameraZoom);
on<CameraStopEvent>(_onCameraStop);
on<CameraPresetEvent>(_onCameraPreset);
on<GeViScopeCloseContactEvent>(_onCloseContact);
on<GeViScopeOpenContactEvent>(_onOpenContact);
on<SendGeViScopeCustomActionEvent>(_onSendCustomAction);
on<SendGeViScopeActionEvent>(_onSendAction);
on<ClearGeViScopeActionResultEvent>(_onClearActionResult);
}
Future<void> _onConnect(
ConnectGeViScopeEvent event,
Emitter<GeViScopeState> emit,
) async {
emit(state.copyWith(
connectionStatus: GeViScopeConnectionStatus.connecting,
isLoading: true,
clearErrorMessage: true,
));
try {
final result = await remoteDataSource.connect(
address: event.address,
username: event.username,
password: event.password,
);
if (result['success'] == true) {
emit(state.copyWith(
connectionStatus: GeViScopeConnectionStatus.connected,
serverAddress: event.address,
username: event.username,
channelCount: result['channelCount'] ?? 0,
connectedAt: DateTime.now(),
isLoading: false,
lastActionResult: 'Connected to GeViScope at ${event.address}',
lastActionSuccess: true,
));
// Auto-load channels after connection
add(const LoadChannelsEvent());
} else {
emit(state.copyWith(
connectionStatus: GeViScopeConnectionStatus.error,
isLoading: false,
errorMessage: result['message'] ?? result['error'] ?? 'Connection failed',
lastActionResult: result['message'] ?? result['error'] ?? 'Connection failed',
lastActionSuccess: false,
));
}
} catch (e) {
emit(state.copyWith(
connectionStatus: GeViScopeConnectionStatus.error,
isLoading: false,
errorMessage: e.toString(),
lastActionResult: 'Connection error: $e',
lastActionSuccess: false,
));
}
}
Future<void> _onDisconnect(
DisconnectGeViScopeEvent event,
Emitter<GeViScopeState> emit,
) async {
emit(state.copyWith(isLoading: true));
try {
await remoteDataSource.disconnect();
emit(state.copyWith(
connectionStatus: GeViScopeConnectionStatus.disconnected,
isLoading: false,
channelCount: 0,
channels: const [],
clearServerAddress: true,
clearUsername: true,
clearConnectedAt: true,
lastActionResult: 'Disconnected from GeViScope',
lastActionSuccess: true,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
errorMessage: e.toString(),
lastActionResult: 'Disconnect error: $e',
lastActionSuccess: false,
));
}
}
Future<void> _onCheckStatus(
CheckGeViScopeStatusEvent event,
Emitter<GeViScopeState> emit,
) async {
try {
final result = await remoteDataSource.getStatus();
final isConnected = result['is_connected'] == true;
emit(state.copyWith(
connectionStatus: isConnected
? GeViScopeConnectionStatus.connected
: GeViScopeConnectionStatus.disconnected,
serverAddress: result['address']?.toString(),
username: result['username']?.toString(),
channelCount: result['channel_count'] ?? 0,
));
} catch (e) {
emit(state.copyWith(
connectionStatus: GeViScopeConnectionStatus.error,
errorMessage: e.toString(),
));
}
}
Future<void> _onLoadChannels(
LoadChannelsEvent event,
Emitter<GeViScopeState> emit,
) async {
try {
final result = await remoteDataSource.getChannels();
if (result['channels'] != null) {
final channelsList = (result['channels'] as List)
.map((c) => MediaChannel.fromJson(c as Map<String, dynamic>))
.toList();
emit(state.copyWith(
channels: channelsList,
channelCount: result['count'] ?? channelsList.length,
));
}
} catch (e) {
emit(state.copyWith(
lastActionResult: 'Load channels error: $e',
lastActionSuccess: false,
));
}
}
Future<void> _onRefreshChannels(
RefreshChannelsEvent event,
Emitter<GeViScopeState> emit,
) async {
emit(state.copyWith(isLoading: true));
try {
await remoteDataSource.refreshChannels();
add(const LoadChannelsEvent());
emit(state.copyWith(
isLoading: false,
lastActionResult: 'Channels refreshed',
lastActionSuccess: true,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
lastActionResult: 'Refresh channels error: $e',
lastActionSuccess: false,
));
}
}
Future<void> _onSendCrossSwitch(
SendGeViScopeCrossSwitchEvent event,
Emitter<GeViScopeState> emit,
) async {
emit(state.copyWith(isLoading: true));
try {
final result = await remoteDataSource.crossSwitch(
videoInput: event.videoInput,
videoOutput: event.videoOutput,
switchMode: event.switchMode,
);
final message = 'CrossSwitch(${event.videoInput}, ${event.videoOutput}, ${event.switchMode})';
_addToLog(emit, message);
emit(state.copyWith(
isLoading: false,
lastActionResult: result['success'] == true
? 'CrossSwitch sent successfully'
: result['message'] ?? 'CrossSwitch failed',
lastActionSuccess: result['success'] == true,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
lastActionResult: 'CrossSwitch error: $e',
lastActionSuccess: false,
));
}
}
Future<void> _onCameraPan(
CameraPanEvent event,
Emitter<GeViScopeState> emit,
) async {
emit(state.copyWith(isLoading: true));
try {
final result = await remoteDataSource.cameraPan(
camera: event.camera,
direction: event.direction,
speed: event.speed,
);
final message = 'CameraPan(${event.camera}, ${event.direction}, ${event.speed})';
_addToLog(emit, message);
emit(state.copyWith(
isLoading: false,
lastActionResult: result['success'] == true
? 'Camera pan ${event.direction}'
: result['message'] ?? 'Camera pan failed',
lastActionSuccess: result['success'] == true,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
lastActionResult: 'Camera pan error: $e',
lastActionSuccess: false,
));
}
}
Future<void> _onCameraTilt(
CameraTiltEvent event,
Emitter<GeViScopeState> emit,
) async {
emit(state.copyWith(isLoading: true));
try {
final result = await remoteDataSource.cameraTilt(
camera: event.camera,
direction: event.direction,
speed: event.speed,
);
final message = 'CameraTilt(${event.camera}, ${event.direction}, ${event.speed})';
_addToLog(emit, message);
emit(state.copyWith(
isLoading: false,
lastActionResult: result['success'] == true
? 'Camera tilt ${event.direction}'
: result['message'] ?? 'Camera tilt failed',
lastActionSuccess: result['success'] == true,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
lastActionResult: 'Camera tilt error: $e',
lastActionSuccess: false,
));
}
}
Future<void> _onCameraZoom(
CameraZoomEvent event,
Emitter<GeViScopeState> emit,
) async {
emit(state.copyWith(isLoading: true));
try {
final result = await remoteDataSource.cameraZoom(
camera: event.camera,
direction: event.direction,
speed: event.speed,
);
final message = 'CameraZoom(${event.camera}, ${event.direction}, ${event.speed})';
_addToLog(emit, message);
emit(state.copyWith(
isLoading: false,
lastActionResult: result['success'] == true
? 'Camera zoom ${event.direction}'
: result['message'] ?? 'Camera zoom failed',
lastActionSuccess: result['success'] == true,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
lastActionResult: 'Camera zoom error: $e',
lastActionSuccess: false,
));
}
}
Future<void> _onCameraStop(
CameraStopEvent event,
Emitter<GeViScopeState> emit,
) async {
emit(state.copyWith(isLoading: true));
try {
final result = await remoteDataSource.cameraStop(camera: event.camera);
final message = 'CameraStop(${event.camera})';
_addToLog(emit, message);
emit(state.copyWith(
isLoading: false,
lastActionResult: result['success'] == true
? 'Camera stopped'
: result['message'] ?? 'Camera stop failed',
lastActionSuccess: result['success'] == true,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
lastActionResult: 'Camera stop error: $e',
lastActionSuccess: false,
));
}
}
Future<void> _onCameraPreset(
CameraPresetEvent event,
Emitter<GeViScopeState> emit,
) async {
emit(state.copyWith(isLoading: true));
try {
final result = await remoteDataSource.cameraPreset(
camera: event.camera,
preset: event.preset,
);
final message = 'CameraPreset(${event.camera}, ${event.preset})';
_addToLog(emit, message);
emit(state.copyWith(
isLoading: false,
lastActionResult: result['success'] == true
? 'Camera moved to preset ${event.preset}'
: result['message'] ?? 'Camera preset failed',
lastActionSuccess: result['success'] == true,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
lastActionResult: 'Camera preset error: $e',
lastActionSuccess: false,
));
}
}
Future<void> _onCloseContact(
GeViScopeCloseContactEvent event,
Emitter<GeViScopeState> emit,
) async {
emit(state.copyWith(isLoading: true));
try {
final result = await remoteDataSource.digitalIoClose(contactId: event.contactId);
final message = 'DigitalIO_Close(${event.contactId})';
_addToLog(emit, message);
emit(state.copyWith(
isLoading: false,
lastActionResult: result['success'] == true
? 'Digital output ${event.contactId} closed'
: result['message'] ?? 'Close contact failed',
lastActionSuccess: result['success'] == true,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
lastActionResult: 'Close contact error: $e',
lastActionSuccess: false,
));
}
}
Future<void> _onOpenContact(
GeViScopeOpenContactEvent event,
Emitter<GeViScopeState> emit,
) async {
emit(state.copyWith(isLoading: true));
try {
final result = await remoteDataSource.digitalIoOpen(contactId: event.contactId);
final message = 'DigitalIO_Open(${event.contactId})';
_addToLog(emit, message);
emit(state.copyWith(
isLoading: false,
lastActionResult: result['success'] == true
? 'Digital output ${event.contactId} opened'
: result['message'] ?? 'Open contact failed',
lastActionSuccess: result['success'] == true,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
lastActionResult: 'Open contact error: $e',
lastActionSuccess: false,
));
}
}
Future<void> _onSendCustomAction(
SendGeViScopeCustomActionEvent event,
Emitter<GeViScopeState> emit,
) async {
emit(state.copyWith(isLoading: true));
try {
final result = await remoteDataSource.sendCustomAction(
typeId: event.typeId,
text: event.text,
);
final message = 'CustomAction(${event.typeId}, "${event.text}")';
_addToLog(emit, message);
emit(state.copyWith(
isLoading: false,
lastActionResult: result['success'] == true
? 'CustomAction sent'
: result['message'] ?? 'CustomAction failed',
lastActionSuccess: result['success'] == true,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
lastActionResult: 'CustomAction error: $e',
lastActionSuccess: false,
));
}
}
Future<void> _onSendAction(
SendGeViScopeActionEvent event,
Emitter<GeViScopeState> emit,
) async {
emit(state.copyWith(isLoading: true));
try {
final result = await remoteDataSource.sendAction(event.action);
_addToLog(emit, event.action);
emit(state.copyWith(
isLoading: false,
lastActionResult: result['success'] == true
? 'Action sent'
: result['message'] ?? 'Action failed',
lastActionSuccess: result['success'] == true,
));
} catch (e) {
emit(state.copyWith(
isLoading: false,
lastActionResult: 'Send action error: $e',
lastActionSuccess: false,
));
}
}
void _onClearActionResult(
ClearGeViScopeActionResultEvent event,
Emitter<GeViScopeState> emit,
) {
emit(state.copyWith(clearLastActionResult: true, clearErrorMessage: true));
}
void _addToLog(Emitter<GeViScopeState> emit, String message) {
final timestamp = DateTime.now().toIso8601String().substring(11, 19);
final logEntry = '[$timestamp] $message';
final newLog = [...state.messageLog, logEntry];
// Keep only last 100 messages
if (newLog.length > 100) {
newLog.removeRange(0, newLog.length - 100);
}
emit(state.copyWith(messageLog: newLog));
}
}