using GEUTEBRUECK.GeViScope.Wrapper.DBI; using GEUTEBRUECK.GeViScope.Wrapper.Actions; using GEUTEBRUECK.GeViScope.Wrapper.Actions.SystemActions; using GEUTEBRUECK.GeViScope.Wrapper.Actions.DigitalContactsActions; using GEUTEBRUECK.GeViScope.Wrapper.Actions.ActionDispatcher; using GEUTEBRUECK.GeViScope.Wrapper.MediaPlayer; using Microsoft.OpenApi.Models; using System.Collections; using System.Collections.Concurrent; using System.Net.WebSockets; using System.Text; using System.Text.Json; var builder = WebApplication.CreateBuilder(args); // Add CORS for web app access builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => { policy.AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader(); }); }); // Add Swagger services builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "GeViScope Bridge API", Version = "v1", Description = "REST API bridge for Geutebruck GeViScope Camera Server SDK. Provides access to camera control, video routing, PTZ, and action/event handling." }); }); // GeViScope connection state GscServer? gscServer = null; GscPLCWrapper? gscPLC = null; GscActionDispatcher? actionDispatcher = null; string? currentAddress = null; string? currentUsername = null; DateTime? connectedAt = null; int eventCount = 0; List receivedMessages = new List(); List mediaChannels = new List(); // WebSocket clients for event streaming ConcurrentDictionary wsClients = new ConcurrentDictionary(); // Monitor state tracking (populated from ViewerConnected/ViewerCleared events) ConcurrentDictionary monitorStates = new ConcurrentDictionary(); // Alarm state tracking (populated from EventStarted/EventStopped events) ConcurrentDictionary alarmStates = new ConcurrentDictionary(); // Update monitor state from event void UpdateMonitorState(string actionName, int viewer, int channel = 0, int playMode = 0) { if (actionName == "ViewerConnected" || actionName == "ViewerSelectionChanged") { monitorStates[viewer] = new MonitorState { ViewerId = viewer, CurrentChannel = channel, PlayMode = playMode, LastUpdated = DateTime.UtcNow }; } else if (actionName == "ViewerCleared") { monitorStates[viewer] = new MonitorState { ViewerId = viewer, CurrentChannel = 0, PlayMode = 0, LastUpdated = DateTime.UtcNow }; } } // Update alarm state from event void UpdateAlarmState(string eventType, long eventId, string eventName, long typeId, long foreignKey) { if (eventType == "EventStarted") { alarmStates[eventId] = new AlarmState { EventId = eventId, EventName = eventName, TypeId = typeId, ForeignKey = foreignKey, StartedAt = DateTime.UtcNow, IsActive = true }; } else if (eventType == "EventStopped") { if (alarmStates.TryGetValue(eventId, out var alarm)) { alarm.IsActive = false; alarm.StoppedAt = DateTime.UtcNow; } } } // Broadcast event to all connected WebSocket clients async Task BroadcastEvent(object eventData) { var json = JsonSerializer.Serialize(eventData); var buffer = Encoding.UTF8.GetBytes(json); var segment = new ArraySegment(buffer); var deadClients = new List(); foreach (var (clientId, ws) in wsClients) { if (ws.State == WebSocketState.Open) { try { await ws.SendAsync(segment, WebSocketMessageType.Text, true, CancellationToken.None); } catch { deadClients.Add(clientId); } } else { deadClients.Add(clientId); } } // Clean up dead connections foreach (var clientId in deadClients) { wsClients.TryRemove(clientId, out _); } } // Parse action string into structured event (e.g., "ViewerConnected(5, 101, 11)" -> {action: "ViewerConnected", params: {...}}) object? ParseActionToEvent(string actionStr) { // Match pattern: ActionName(params) var match = System.Text.RegularExpressions.Regex.Match(actionStr, @"(\w+)\(([^)]*)\)"); if (!match.Success) return null; var actionName = match.Groups[1].Value; var paramsStr = match.Groups[2].Value; var paramValues = paramsStr.Split(',').Select(p => p.Trim().Trim('"')).ToArray(); // Update local state tracking based on action type if (actionName == "ViewerConnected" && paramValues.Length >= 3) { int.TryParse(paramValues[0], out var viewer); int.TryParse(paramValues[1], out var channel); int.TryParse(paramValues[2], out var playMode); UpdateMonitorState(actionName, viewer, channel, playMode); } else if (actionName == "ViewerCleared" && paramValues.Length >= 1) { int.TryParse(paramValues[0], out var viewer); UpdateMonitorState(actionName, viewer); } else if (actionName == "ViewerSelectionChanged" && paramValues.Length >= 3) { int.TryParse(paramValues[0], out var viewer); int.TryParse(paramValues[1], out var channel); int.TryParse(paramValues[2], out var playMode); UpdateMonitorState(actionName, viewer, channel, playMode); } return actionName switch { "ViewerConnected" when paramValues.Length >= 3 => new { timestamp = DateTime.UtcNow.ToString("o"), server = currentAddress, action = "ViewerConnected", @params = new { Viewer = int.TryParse(paramValues[0], out var v) ? v : 0, Channel = int.TryParse(paramValues[1], out var c) ? c : 0, PlayMode = int.TryParse(paramValues[2], out var pm) ? pm : 0 } }, "ViewerCleared" when paramValues.Length >= 1 => new { timestamp = DateTime.UtcNow.ToString("o"), server = currentAddress, action = "ViewerCleared", @params = new { Viewer = int.TryParse(paramValues[0], out var v) ? v : 0 } }, "ViewerSelectionChanged" when paramValues.Length >= 3 => new { timestamp = DateTime.UtcNow.ToString("o"), server = currentAddress, action = "ViewerSelectionChanged", @params = new { Viewer = int.TryParse(paramValues[0], out var v) ? v : 0, Channel = int.TryParse(paramValues[1], out var c) ? c : 0, PlayMode = int.TryParse(paramValues[2], out var pm) ? pm : 0 } }, "DigitalInput" when paramValues.Length >= 2 => new { timestamp = DateTime.UtcNow.ToString("o"), server = currentAddress, action = "DigitalInput", @params = new { Contact = int.TryParse(paramValues[0], out var contact) ? contact : 0, State = int.TryParse(paramValues[1], out var state) ? state : 0 } }, "VCAlarmQueueNotification" when paramValues.Length >= 4 => new { timestamp = DateTime.UtcNow.ToString("o"), server = currentAddress, action = "VCAlarmQueueNotification", @params = new { Viewer = int.TryParse(paramValues[0], out var v) ? v : 0, Notification = int.TryParse(paramValues[1], out var n) ? n : 0, AlarmID = int.TryParse(paramValues[2], out var aid) ? aid : 0, TypeID = int.TryParse(paramValues[3], out var tid) ? tid : 0 } }, _ => new { timestamp = DateTime.UtcNow.ToString("o"), server = currentAddress, action = actionName, raw = actionStr } }; } // PlayMode string to numeric value mapping (PlcViewerPlayMode enum) var playModeMap = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "unknown", 0 }, { "play stop", 1 }, { "play forward", 2 }, { "play backward", 3 }, { "fast forward", 4 }, { "fast backward", 5 }, { "step forward", 6 }, { "step backward", 7 }, { "play BOD", 8 }, { "play EOD", 9 }, { "quasi live", 10 }, { "live", 11 }, { "next event", 12 }, { "prev event", 13 }, { "peek live picture", 14 }, { "next detected motion", 17 }, { "prev detected motion", 18 } }; // Helper function to get numeric PlayMode value int GetPlayModeValue(string playMode) { if (playModeMap.TryGetValue(playMode, out int value)) return value; // Try parsing as numeric value if not found in map if (int.TryParse(playMode, out int numValue)) return numValue; // Default to play stop (1) if unknown Console.WriteLine($"Unknown PlayMode '{playMode}', defaulting to 'play stop' (1)"); return 1; } var app = builder.Build(); // Enable CORS app.UseCors(); // Action dispatcher event handlers void OnCustomAction(object? sender, GscAct_CustomActionEventArgs e) { var msg = $"[{DateTime.Now:HH:mm:ss}] CustomAction({e.aInt}, \"{e.aString}\")"; Console.WriteLine(msg); receivedMessages.Add(msg); } void OnDigitalInput(object? sender, GscAct_DigitalInputEventArgs e) { var msg = $"[{DateTime.Now:HH:mm:ss}] DigitalInput(GlobalNo={e.aContact.GlobalNo})"; Console.WriteLine(msg); receivedMessages.Add(msg); } // PLC callback handler void OnPLCCallback(object? sender, PLCCallbackEventArgs e) { try { eventCount++; if (e.PlcNotification.GetNotificationType() == GscPlcNotificationType.plcnNewActionData) { var action = e.PlcNotification.GetAction(); if (action != null && actionDispatcher != null) { var actionStr = action.ToString() ?? ""; if (!actionDispatcher.Dispatch(action)) { var msg = $"[{DateTime.Now:HH:mm:ss}] Action: {actionStr}"; Console.WriteLine(msg); receivedMessages.Add(msg); // Broadcast to WebSocket clients var eventData = ParseActionToEvent(actionStr); if (eventData != null && wsClients.Count > 0) { _ = BroadcastEvent(eventData); } } } } else if (e.PlcNotification.GetNotificationType() == GscPlcNotificationType.plcnNewEventData) { var eventData = e.PlcNotification.GetEventData(); if (eventData != null) { var eventType = eventData.EventNotificationType switch { GscPlcEventNotificationType.plcenEventStarted => "started", GscPlcEventNotificationType.plcenEventStopped => "stopped", GscPlcEventNotificationType.plcenEventRetriggered => "retriggered", _ => "unknown" }; var msg = $"[{DateTime.Now:HH:mm:ss}] Event: {eventData.EventHeader.EventName} {eventData.EventHeader.EventID} {eventType}"; Console.WriteLine(msg); receivedMessages.Add(msg); // Update local alarm state tracking var actionType = eventType == "started" ? "EventStarted" : eventType == "stopped" ? "EventStopped" : "EventRetriggered"; UpdateAlarmState(actionType, eventData.EventHeader.EventID, eventData.EventHeader.EventName ?? "", eventData.EventHeader.EventTypeID, Convert.ToInt64(eventData.EventHeader.ForeignKey)); // Broadcast event notifications to WebSocket clients var wsEvent = new { timestamp = DateTime.UtcNow.ToString("o"), server = currentAddress, action = actionType, @params = new { EventID = eventData.EventHeader.EventID, EventName = eventData.EventHeader.EventName, TypeID = eventData.EventHeader.EventTypeID, ForeignKey = eventData.EventHeader.ForeignKey } }; if (wsClients.Count > 0) { _ = BroadcastEvent(wsEvent); } } } else if (e.PlcNotification.GetNotificationType() == GscPlcNotificationType.plcnPushCallbackLost) { var msg = $"[{DateTime.Now:HH:mm:ss}] Connection lost!"; Console.WriteLine(msg); receivedMessages.Add(msg); // Broadcast connection lost to WebSocket clients var wsEvent = new { timestamp = DateTime.UtcNow.ToString("o"), server = currentAddress, action = "ConnectionLost", @params = new { } }; if (wsClients.Count > 0) { _ = BroadcastEvent(wsEvent); } } } catch (Exception ex) { Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] PLC Callback error: {ex.Message}"); } } // Helper function to create PLC and register callbacks void CreatePLC() { if (gscServer == null) return; gscPLC = gscServer.CreatePLC(); gscPLC.PLCCallback += OnPLCCallback; gscPLC.OpenPushCallback(); actionDispatcher = new GscActionDispatcher(); actionDispatcher.OnCustomAction += OnCustomAction; actionDispatcher.OnDigitalInput += OnDigitalInput; gscPLC.SubscribeActionsAll(); gscPLC.SubscribeEventsAll(); } // Helper function to destroy PLC void DestroyPLC(bool connectionLost = false) { if (gscPLC != null) { if (!connectionLost) { gscPLC.UnsubscribeAll(); gscPLC.CloseCallback(); } if (actionDispatcher != null) { actionDispatcher.Dispose(); actionDispatcher = null; } gscPLC.Dispose(); gscPLC = null; } } // Helper function to load media channels void LoadMediaChannels() { mediaChannels.Clear(); if (gscServer == null) { Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] LoadMediaChannels: gscServer is null"); return; } Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] LoadMediaChannels: Starting channel query..."); Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Server connected: {gscServer.IsConnected}"); try { var channelList = new ArrayList(); Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Calling MediaPlayerHelperFunctions.QueryMediaChannelList..."); MediaPlayerHelperFunctions.QueryMediaChannelList(gscServer, out channelList); Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] QueryMediaChannelList returned, channelList is null: {channelList == null}"); if (channelList != null) { Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Found {channelList.Count} total channels from server"); foreach (GscMediaChannelData channel in channelList) { // Include ALL channels (both active and inactive) mediaChannels.Add(new MediaChannelInfo { ChannelID = channel.ChannelID, GlobalNumber = channel.GlobalNumber, Name = channel.Name, Description = channel.Desc, IsActive = channel.IsActive }); Console.WriteLine($" - Channel {channel.ChannelID}: {channel.Name} (Active: {channel.IsActive})"); } } else { Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] channelList is NULL!"); } } catch (Exception ex) { Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Error loading media channels: {ex.Message}"); Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Stack trace: {ex.StackTrace}"); } } // ============================================================================ // API ENDPOINTS // ============================================================================ // Connection endpoint app.MapPost("/connect", (ConnectRequest request) => { try { // Disconnect existing connection if (gscServer != null) { DestroyPLC(); gscServer.Disconnect(5000); gscServer.Dispose(); gscServer = null; } // Create new connection gscServer = new GscServer(); // Encode password var encodedPassword = DBIHelperFunctions.EncodePassword(request.Password); // Set connection parameters using var connectParams = new GscServerConnectParams(request.Address, request.Username, encodedPassword); gscServer.SetConnectParams(connectParams); // Connect var result = gscServer.Connect(); if (result == GscServerConnectResult.connectOk) { currentAddress = request.Address; currentUsername = request.Username; connectedAt = DateTime.UtcNow; eventCount = 0; // Create PLC for actions/events CreatePLC(); // Load media channels LoadMediaChannels(); Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Connected to GeViScope at {request.Address}"); return Results.Ok(new { success = true, message = "Connected to GeViScope", address = request.Address, username = request.Username, channelCount = mediaChannels.Count, connected_at = connectedAt }); } else { return Results.BadRequest(new { success = false, error = "Connection failed", message = result.ToString() }); } } catch (Exception ex) { return Results.BadRequest(new { success = false, error = "Connection error", message = ex.Message, stack_trace = ex.StackTrace }); } }); // Disconnect endpoint app.MapPost("/disconnect", () => { try { DestroyPLC(); if (gscServer != null) { gscServer.Disconnect(5000); gscServer.Dispose(); gscServer = null; } currentAddress = null; currentUsername = null; connectedAt = null; eventCount = 0; mediaChannels.Clear(); return Results.Ok(new { success = true, message = "Disconnected successfully" }); } catch (Exception ex) { return Results.BadRequest(new { success = false, error = "Disconnect error", message = ex.Message }); } }); // Status endpoint (enhanced for monitoring) app.MapGet("/status", () => { return Results.Ok(new { is_connected = gscServer != null, address = currentAddress, username = currentUsername, channel_count = mediaChannels.Count, connected_at = connectedAt, connection_duration_sec = connectedAt.HasValue ? (int)(DateTime.UtcNow - connectedAt.Value).TotalSeconds : 0, event_count = eventCount, websocket_clients = wsClients.Count, plc_active = gscPLC != null }); }); // Health check endpoint (for load balancers) app.MapGet("/health", () => { var isHealthy = gscServer != null && gscPLC != null; return isHealthy ? Results.Ok(new { status = "healthy", server = "geviscope-bridge" }) : Results.Ok(new { status = "degraded", server = "geviscope-bridge", message = "Not connected to GeViScope server" }); }); // Get media channels app.MapGet("/channels", () => { if (gscServer == null) { return Results.BadRequest(new { error = "Not connected" }); } return Results.Ok(new { count = mediaChannels.Count, channels = mediaChannels }); }); // Refresh media channels app.MapPost("/channels/refresh", () => { if (gscServer == null) { return Results.BadRequest(new { error = "Not connected" }); } LoadMediaChannels(); return Results.Ok(new { count = mediaChannels.Count, channels = mediaChannels }); }); // Get current monitor states (tracked from ViewerConnected/ViewerCleared events) app.MapGet("/monitors", () => { return Results.Ok(new { server = currentAddress, count = monitorStates.Count, monitors = monitorStates.Values.OrderBy(m => m.ViewerId).Select(m => new { viewer_id = m.ViewerId, current_channel = m.CurrentChannel, play_mode = m.PlayMode, last_updated = m.LastUpdated }), note = "State is tracked from ViewerConnected/ViewerCleared events. Initial state requires event activity or server query." }); }); // Get specific monitor state app.MapGet("/monitors/{viewerId}", (int viewerId) => { if (monitorStates.TryGetValue(viewerId, out var state)) { return Results.Ok(new { viewer_id = state.ViewerId, current_channel = state.CurrentChannel, play_mode = state.PlayMode, last_updated = state.LastUpdated }); } return Results.NotFound(new { error = "Monitor not found or no events received yet" }); }); // Get active alarms (tracked from EventStarted/EventStopped notifications) app.MapGet("/alarms/active", () => { var activeAlarms = alarmStates.Values.Where(a => a.IsActive).OrderBy(a => a.StartedAt); return Results.Ok(new { server = currentAddress, count = activeAlarms.Count(), alarms = activeAlarms.Select(a => new { event_id = a.EventId, event_name = a.EventName, type_id = a.TypeId, foreign_key = a.ForeignKey, started_at = a.StartedAt }), note = "Alarms tracked from EventStarted/EventStopped notifications. For complete alarm state on startup, query GeViServer bridge." }); }); // Get all alarms (including stopped) app.MapGet("/alarms", () => { return Results.Ok(new { server = currentAddress, total = alarmStates.Count, active = alarmStates.Values.Count(a => a.IsActive), alarms = alarmStates.Values.OrderByDescending(a => a.StartedAt).Select(a => new { event_id = a.EventId, event_name = a.EventName, type_id = a.TypeId, foreign_key = a.ForeignKey, started_at = a.StartedAt, stopped_at = a.StoppedAt, is_active = a.IsActive }) }); }); // Send action (generic) app.MapPost("/action", (ActionRequest request) => { try { if (gscServer == null || gscPLC == null) { return Results.BadRequest(new { error = "Not connected" }); } Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Sending action: {request.Action}"); var action = GscAction.Decode(request.Action); if (action != null) { gscPLC.SendAction(action); action.Dispose(); var logMsg = $"[{DateTime.Now:HH:mm:ss}] SENT: {request.Action}"; receivedMessages.Add(logMsg); return Results.Ok(new { success = true, message = "Action sent", action = request.Action }); } else { return Results.BadRequest(new { success = false, error = "Invalid action", message = "Could not decode action string" }); } } catch (Exception ex) { return Results.BadRequest(new { success = false, error = "Action error", message = ex.Message }); } }); // Send CustomAction app.MapPost("/custom-action", (CustomActionRequest request) => { try { if (gscServer == null || gscPLC == null) { return Results.BadRequest(new { error = "Not connected" }); } var action = new GscAct_CustomAction(request.TypeId, request.Text ?? ""); gscPLC.SendAction(action); action.Dispose(); var logMsg = $"[{DateTime.Now:HH:mm:ss}] SENT: CustomAction({request.TypeId}, \"{request.Text}\")"; receivedMessages.Add(logMsg); return Results.Ok(new { success = true, message = "CustomAction sent", type_id = request.TypeId, text = request.Text }); } catch (Exception ex) { return Results.BadRequest(new { success = false, error = "CustomAction error", message = ex.Message }); } }); // ViewerConnect - connect a viewer to a channel with play mode app.MapPost("/viewer/connect", (ViewerConnectRequest request) => { try { if (gscServer == null || gscPLC == null) { return Results.BadRequest(new { error = "Not connected" }); } var playModeValue = GetPlayModeValue(request.PlayMode); var actionStr = $"ViewerConnect({request.Viewer}, {request.Channel}, {playModeValue})"; var action = GscAction.Decode(actionStr); if (action != null) { gscPLC.SendAction(action); action.Dispose(); var logMsg = $"[{DateTime.Now:HH:mm:ss}] SENT: {actionStr}"; Console.WriteLine(logMsg); receivedMessages.Add(logMsg); return Results.Ok(new { success = true, message = "ViewerConnect sent", viewer = request.Viewer, channel = request.Channel, play_mode = request.PlayMode }); } else { return Results.BadRequest(new { success = false, error = "Failed to create ViewerConnect action" }); } } catch (Exception ex) { return Results.BadRequest(new { success = false, error = "ViewerConnect error", message = ex.Message }); } }); // ViewerConnectLive - connect a viewer to a channel (GeViScope equivalent of CrossSwitch) app.MapPost("/viewer/connect-live", (ViewerConnectLiveRequest request) => { try { if (gscServer == null || gscPLC == null) { return Results.BadRequest(new { error = "Not connected" }); } var actionStr = $"ViewerConnectLive({request.Viewer}, {request.Channel})"; var action = GscAction.Decode(actionStr); if (action != null) { gscPLC.SendAction(action); action.Dispose(); var logMsg = $"[{DateTime.Now:HH:mm:ss}] SENT: {actionStr}"; Console.WriteLine(logMsg); receivedMessages.Add(logMsg); return Results.Ok(new { success = true, message = "ViewerConnectLive sent", viewer = request.Viewer, channel = request.Channel }); } else { return Results.BadRequest(new { success = false, error = "Failed to create ViewerConnectLive action" }); } } catch (Exception ex) { return Results.BadRequest(new { success = false, error = "ViewerConnectLive error", message = ex.Message }); } }); // ViewerClear - clear viewer display app.MapPost("/viewer/clear", (ViewerClearRequest request) => { try { if (gscServer == null || gscPLC == null) { return Results.BadRequest(new { error = "Not connected" }); } var actionStr = $"ViewerClear({request.Viewer})"; var action = GscAction.Decode(actionStr); if (action != null) { gscPLC.SendAction(action); action.Dispose(); var logMsg = $"[{DateTime.Now:HH:mm:ss}] SENT: {actionStr}"; Console.WriteLine(logMsg); receivedMessages.Add(logMsg); return Results.Ok(new { success = true, message = "ViewerClear sent", viewer = request.Viewer }); } else { return Results.BadRequest(new { success = false, error = "Failed to create ViewerClear action" }); } } catch (Exception ex) { return Results.BadRequest(new { success = false, error = "ViewerClear error", message = ex.Message }); } }); // CrossSwitch - redirects to ViewerConnectLive for GeViScope (for backward compatibility) app.MapPost("/crossswitch", (CrossSwitchRequest request) => { try { if (gscServer == null || gscPLC == null) { return Results.BadRequest(new { error = "Not connected" }); } // For GeViScope, use ViewerConnectLive instead of CrossSwitch // VideoOutput = Viewer, VideoInput = Channel var actionStr = $"ViewerConnectLive({request.VideoOutput}, {request.VideoInput})"; var action = GscAction.Decode(actionStr); if (action != null) { gscPLC.SendAction(action); action.Dispose(); var logMsg = $"[{DateTime.Now:HH:mm:ss}] SENT: {actionStr} (via /crossswitch)"; Console.WriteLine(logMsg); receivedMessages.Add(logMsg); return Results.Ok(new { success = true, message = "ViewerConnectLive sent (CrossSwitch mapped)", viewer = request.VideoOutput, channel = request.VideoInput }); } else { return Results.BadRequest(new { success = false, error = "Failed to create ViewerConnectLive action" }); } } catch (Exception ex) { return Results.BadRequest(new { success = false, error = "CrossSwitch error", message = ex.Message }); } }); // PTZ Camera Control - Pan app.MapPost("/camera/pan", (CameraPanRequest request) => { try { if (gscServer == null || gscPLC == null) { return Results.BadRequest(new { error = "Not connected" }); } // Correct action names: PanLeft, PanRight (not CameraPanLeft/Right) var direction = request.Direction.ToLower() == "left" ? "Left" : "Right"; var actionStr = $"Pan{direction}({request.Camera}, {request.Speed})"; Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] PTZ Pan: {actionStr}"); var action = GscAction.Decode(actionStr); if (action != null) { gscPLC.SendAction(action); action.Dispose(); return Results.Ok(new { success = true, message = $"Camera pan {direction}", camera = request.Camera, speed = request.Speed }); } return Results.BadRequest(new { error = "Failed to create pan action" }); } catch (Exception ex) { return Results.BadRequest(new { error = ex.Message }); } }); // PTZ Camera Control - Tilt app.MapPost("/camera/tilt", (CameraTiltRequest request) => { try { if (gscServer == null || gscPLC == null) { return Results.BadRequest(new { error = "Not connected" }); } // Correct action names: TiltUp, TiltDown (not CameraTiltUp/Down) var direction = request.Direction.ToLower() == "up" ? "Up" : "Down"; var actionStr = $"Tilt{direction}({request.Camera}, {request.Speed})"; Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] PTZ Tilt: {actionStr}"); var action = GscAction.Decode(actionStr); if (action != null) { gscPLC.SendAction(action); action.Dispose(); return Results.Ok(new { success = true, message = $"Camera tilt {direction}", camera = request.Camera, speed = request.Speed }); } return Results.BadRequest(new { error = "Failed to create tilt action" }); } catch (Exception ex) { return Results.BadRequest(new { error = ex.Message }); } }); // PTZ Camera Control - Zoom app.MapPost("/camera/zoom", (CameraZoomRequest request) => { try { if (gscServer == null || gscPLC == null) { return Results.BadRequest(new { error = "Not connected" }); } // Correct action names: ZoomIn, ZoomOut (not CameraZoomIn/Out) var direction = request.Direction.ToLower() == "in" ? "In" : "Out"; var actionStr = $"Zoom{direction}({request.Camera}, {request.Speed})"; Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] PTZ Zoom: {actionStr}"); var action = GscAction.Decode(actionStr); if (action != null) { gscPLC.SendAction(action); action.Dispose(); return Results.Ok(new { success = true, message = $"Camera zoom {direction}", camera = request.Camera, speed = request.Speed }); } return Results.BadRequest(new { error = "Failed to create zoom action" }); } catch (Exception ex) { return Results.BadRequest(new { error = ex.Message }); } }); // PTZ Camera Control - Stop all movement app.MapPost("/camera/stop", (CameraStopRequest request) => { try { if (gscServer == null || gscPLC == null) { return Results.BadRequest(new { error = "Not connected" }); } // CameraStopAll is the correct action name var actionStr = $"CameraStopAll({request.Camera})"; Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] PTZ Stop: {actionStr}"); var action = GscAction.Decode(actionStr); if (action != null) { gscPLC.SendAction(action); action.Dispose(); return Results.Ok(new { success = true, message = "Camera stopped", camera = request.Camera }); } return Results.BadRequest(new { error = "Failed to create stop action" }); } catch (Exception ex) { return Results.BadRequest(new { error = ex.Message }); } }); // PTZ Camera Control - Stop pan movement app.MapPost("/camera/pan-stop", (CameraStopRequest request) => { try { if (gscServer == null || gscPLC == null) { return Results.BadRequest(new { error = "Not connected" }); } var actionStr = $"PanStop({request.Camera})"; Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] PTZ PanStop: {actionStr}"); var action = GscAction.Decode(actionStr); if (action != null) { gscPLC.SendAction(action); action.Dispose(); return Results.Ok(new { success = true, message = "Pan stopped", camera = request.Camera }); } return Results.BadRequest(new { error = "Failed to create pan stop action" }); } catch (Exception ex) { return Results.BadRequest(new { error = ex.Message }); } }); // PTZ Camera Control - Stop tilt movement app.MapPost("/camera/tilt-stop", (CameraStopRequest request) => { try { if (gscServer == null || gscPLC == null) { return Results.BadRequest(new { error = "Not connected" }); } var actionStr = $"TiltStop({request.Camera})"; Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] PTZ TiltStop: {actionStr}"); var action = GscAction.Decode(actionStr); if (action != null) { gscPLC.SendAction(action); action.Dispose(); return Results.Ok(new { success = true, message = "Tilt stopped", camera = request.Camera }); } return Results.BadRequest(new { error = "Failed to create tilt stop action" }); } catch (Exception ex) { return Results.BadRequest(new { error = ex.Message }); } }); // PTZ Camera Control - Stop zoom movement app.MapPost("/camera/zoom-stop", (CameraStopRequest request) => { try { if (gscServer == null || gscPLC == null) { return Results.BadRequest(new { error = "Not connected" }); } var actionStr = $"ZoomStop({request.Camera})"; Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] PTZ ZoomStop: {actionStr}"); var action = GscAction.Decode(actionStr); if (action != null) { gscPLC.SendAction(action); action.Dispose(); return Results.Ok(new { success = true, message = "Zoom stopped", camera = request.Camera }); } return Results.BadRequest(new { error = "Failed to create zoom stop action" }); } catch (Exception ex) { return Results.BadRequest(new { error = ex.Message }); } }); // PTZ Camera Control - Go to preset app.MapPost("/camera/preset", (CameraPresetRequest request) => { try { if (gscServer == null || gscPLC == null) { return Results.BadRequest(new { error = "Not connected" }); } // Correct action name: PrePosCallUp (not CameraGotoPreset) var actionStr = $"PrePosCallUp({request.Camera}, {request.Preset})"; Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] PTZ Preset: {actionStr}"); var action = GscAction.Decode(actionStr); if (action != null) { gscPLC.SendAction(action); action.Dispose(); return Results.Ok(new { success = true, message = $"Camera going to preset {request.Preset}", camera = request.Camera, preset = request.Preset }); } return Results.BadRequest(new { error = "Failed to create preset action" }); } catch (Exception ex) { return Results.BadRequest(new { error = ex.Message }); } }); // Digital Output - Close contact app.MapPost("/digital-io/close", (DigitalContactRequest request) => { try { if (gscServer == null || gscPLC == null) { return Results.BadRequest(new { error = "Not connected" }); } var actionStr = $"CloseDigitalOutput({request.ContactId})"; var action = GscAction.Decode(actionStr); if (action != null) { gscPLC.SendAction(action); action.Dispose(); return Results.Ok(new { success = true, message = "Digital output closed", contact_id = request.ContactId }); } return Results.BadRequest(new { error = "Failed to create action" }); } catch (Exception ex) { return Results.BadRequest(new { error = ex.Message }); } }); // Digital Output - Open contact app.MapPost("/digital-io/open", (DigitalContactRequest request) => { try { if (gscServer == null || gscPLC == null) { return Results.BadRequest(new { error = "Not connected" }); } var actionStr = $"OpenDigitalOutput({request.ContactId})"; var action = GscAction.Decode(actionStr); if (action != null) { gscPLC.SendAction(action); action.Dispose(); return Results.Ok(new { success = true, message = "Digital output opened", contact_id = request.ContactId }); } return Results.BadRequest(new { error = "Failed to create action" }); } catch (Exception ex) { return Results.BadRequest(new { error = ex.Message }); } }); // ============================================================================ // PLAYBACK CONTROL ENDPOINTS // ============================================================================ // ViewerSetPlayMode - Set playback mode and speed // PlayMode values: "step forward", "step backward", "fast forward", "fast backward", // "play forward", "play backward", "play stop", "live", "play BOD", "play EOD" app.MapPost("/viewer/set-play-mode", (ViewerSetPlayModeRequest request) => { try { if (gscServer == null || gscPLC == null) { return Results.BadRequest(new { error = "Not connected" }); } var playModeValue = GetPlayModeValue(request.PlayMode); var actionStr = $"ViewerSetPlayMode({request.Viewer}, {playModeValue}, {request.PlaySpeed})"; Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Playback: {actionStr}"); var action = GscAction.Decode(actionStr); if (action != null) { gscPLC.SendAction(action); action.Dispose(); var logMsg = $"[{DateTime.Now:HH:mm:ss}] SENT: {actionStr}"; receivedMessages.Add(logMsg); return Results.Ok(new { success = true, message = "ViewerSetPlayMode sent", viewer = request.Viewer, play_mode = request.PlayMode, play_speed = request.PlaySpeed }); } return Results.BadRequest(new { success = false, error = "Failed to create ViewerSetPlayMode action" }); } catch (Exception ex) { return Results.BadRequest(new { success = false, error = "ViewerSetPlayMode error", message = ex.Message }); } }); // ViewerPlayFromTime - Go to specific date/time // Time format: "2024/01/15 14:30:00,000 GMT+01:00" app.MapPost("/viewer/play-from-time", (ViewerPlayFromTimeRequest request) => { try { if (gscServer == null || gscPLC == null) { return Results.BadRequest(new { error = "Not connected" }); } var playModeValue = GetPlayModeValue(request.PlayMode); var actionStr = $"ViewerPlayFromTime({request.Viewer}, {request.Channel}, {playModeValue}, \"{request.Time}\")"; Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Playback: {actionStr}"); var action = GscAction.Decode(actionStr); if (action != null) { gscPLC.SendAction(action); action.Dispose(); var logMsg = $"[{DateTime.Now:HH:mm:ss}] SENT: {actionStr}"; receivedMessages.Add(logMsg); return Results.Ok(new { success = true, message = "ViewerPlayFromTime sent", viewer = request.Viewer, channel = request.Channel, play_mode = request.PlayMode, time = request.Time }); } return Results.BadRequest(new { success = false, error = "Failed to create ViewerPlayFromTime action" }); } catch (Exception ex) { return Results.BadRequest(new { success = false, error = "ViewerPlayFromTime error", message = ex.Message }); } }); // ViewerJumpByTime - Jump playback position by seconds app.MapPost("/viewer/jump-by-time", (ViewerJumpByTimeRequest request) => { try { if (gscServer == null || gscPLC == null) { return Results.BadRequest(new { error = "Not connected" }); } var playModeValue = GetPlayModeValue(request.PlayMode); var actionStr = $"ViewerJumpByTime({request.Viewer}, {request.Channel}, {playModeValue}, {request.TimeInSec})"; Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Playback: {actionStr}"); var action = GscAction.Decode(actionStr); if (action != null) { gscPLC.SendAction(action); action.Dispose(); var logMsg = $"[{DateTime.Now:HH:mm:ss}] SENT: {actionStr}"; receivedMessages.Add(logMsg); return Results.Ok(new { success = true, message = "ViewerJumpByTime sent", viewer = request.Viewer, channel = request.Channel, play_mode = request.PlayMode, time_in_sec = request.TimeInSec }); } return Results.BadRequest(new { success = false, error = "Failed to create ViewerJumpByTime action" }); } catch (Exception ex) { return Results.BadRequest(new { success = false, error = "ViewerJumpByTime error", message = ex.Message }); } }); // DEBUG: Test action decoding - helps identify which actions are supported by SDK app.MapPost("/debug/test-action", (ActionRequest request) => { var results = new List(); // Test the provided action var action = GscAction.Decode(request.Action); results.Add(new { action_string = request.Action, decoded = action != null, action_type = action?.ToString() ?? "null" }); if (action != null) { action.Dispose(); } return Results.Ok(new { test_results = results, note = "If decoded=true, the action can be sent via PLC" }); }); // DEBUG: Test all playback action formats app.MapGet("/debug/test-playback-actions", () => { var testActions = new[] { // Numeric PlayMode tests "ViewerConnect(1001, 1, 1)", "ViewerConnect(1001, 1, 6)", "ViewerSetPlayMode(1001, 1, 1)", "ViewerSetPlayMode(1001, 6, 1)", "ViewerPlayFromTime(1001, 1, 2, \"2024/01/15 14:30:00,000 GMT+01:00\")", "ViewerJumpByTime(1001, 1, 2, 60)", // String PlayMode tests (old format) "ViewerConnect(1001, 1, \"play stop\")", "ViewerSetPlayMode(1001, \"step forward\", 1)", // Working actions for comparison "ViewerConnectLive(1001, 1)", "ViewerClear(1001)", "CustomAction(1, \"test\")", "PanLeft(1, 50)" }; var results = new List(); foreach (var actionStr in testActions) { var action = GscAction.Decode(actionStr); results.Add(new { action_string = actionStr, decoded = action != null, can_send = action != null }); if (action != null) { action.Dispose(); } } return Results.Ok(new { test_results = results, summary = $"Decoded: {results.Count(r => ((dynamic)r).decoded)} / {results.Count}" }); }); // Get message log app.MapGet("/messages", () => { return Results.Ok(new { count = receivedMessages.Count, messages = receivedMessages.TakeLast(100).ToList() }); }); // Clear message log app.MapPost("/messages/clear", () => { receivedMessages.Clear(); return Results.Ok(new { message = "Message log cleared" }); }); // Enable WebSocket support app.UseWebSockets(); // WebSocket endpoint for event streaming app.Map("/ws/events", async context => { if (context.WebSockets.IsWebSocketRequest) { var ws = await context.WebSockets.AcceptWebSocketAsync(); var clientId = Guid.NewGuid().ToString(); wsClients[clientId] = ws; Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] WebSocket client connected: {clientId}"); // Send initial connection confirmation var welcome = JsonSerializer.Serialize(new { type = "connected", clientId = clientId, server = currentAddress, message = "Connected to GeViScope Bridge event stream" }); await ws.SendAsync( new ArraySegment(Encoding.UTF8.GetBytes(welcome)), WebSocketMessageType.Text, true, CancellationToken.None ); // Keep connection open and handle incoming messages (ping/pong, close) var buffer = new byte[1024]; try { while (ws.State == WebSocketState.Open) { var result = await ws.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); if (result.MessageType == WebSocketMessageType.Close) { await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client requested close", CancellationToken.None); break; } } } catch (WebSocketException) { // Client disconnected } finally { wsClients.TryRemove(clientId, out _); Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] WebSocket client disconnected: {clientId}"); } } else { context.Response.StatusCode = 400; } }); Console.WriteLine("========================================"); Console.WriteLine("GeViScope Bridge starting on port 7720"); Console.WriteLine("WebSocket events: ws://localhost:7720/ws/events"); Console.WriteLine("========================================"); app.Run("http://localhost:7720"); // Request/Response Models record ConnectRequest(string Address, string Username, string Password); record ActionRequest(string Action); record CustomActionRequest(int TypeId, string? Text); record ViewerConnectRequest(int Viewer, int Channel, string PlayMode = "live"); record ViewerConnectLiveRequest(int Viewer, int Channel); record ViewerClearRequest(int Viewer); record CrossSwitchRequest(int VideoInput, int VideoOutput, int SwitchMode = 0); record CameraPanRequest(int Camera, string Direction, int Speed = 50); record CameraTiltRequest(int Camera, string Direction, int Speed = 50); record CameraZoomRequest(int Camera, string Direction, int Speed = 50); record CameraStopRequest(int Camera); record CameraPresetRequest(int Camera, int Preset); record DigitalContactRequest(int ContactId); record ViewerSetPlayModeRequest(int Viewer, string PlayMode, double PlaySpeed = 1.0); record ViewerPlayFromTimeRequest(int Viewer, int Channel, string PlayMode, string Time); record ViewerJumpByTimeRequest(int Viewer, int Channel, string PlayMode, int TimeInSec); class MediaChannelInfo { public long ChannelID { get; set; } public long GlobalNumber { get; set; } public string Name { get; set; } = ""; public string Description { get; set; } = ""; public bool IsActive { get; set; } } // Monitor state tracked from ViewerConnected/ViewerCleared events class MonitorState { public int ViewerId { get; set; } public int CurrentChannel { get; set; } public int PlayMode { get; set; } public DateTime LastUpdated { get; set; } } // Alarm state tracked from EventStarted/EventStopped notifications class AlarmState { public long EventId { get; set; } public string EventName { get; set; } = ""; public long TypeId { get; set; } public long ForeignKey { get; set; } public DateTime StartedAt { get; set; } public DateTime? StoppedAt { get; set; } public bool IsActive { get; set; } }