Files
COPILOT/gcore-bridge/GCoreBridge/Program.cs
klas b2887b67db Add C# bridges and coordinator service
- geviscope-bridge: GeViScope SDK REST wrapper (:7720)
- gcore-bridge: G-Core SDK REST wrapper (:7721)
- geviserver-bridge: GeViServer REST wrapper (:7710)
- copilot-coordinator: WebSocket coordination hub (:8090)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 15:24:20 +01:00

1470 lines
44 KiB
C#

// G-Core Bridge - REST API for Geutebruck G-Core Server SDK
// Uses GEUTEBRUECK.Gng namespaces from G-Core SDK
using GEUTEBRUECK.Gng.Wrapper.Exceptions;
using GEUTEBRUECK.Gng.Wrapper.DBI;
using GEUTEBRUECK.Gng.Wrapper.MediaPlayer;
using GEUTEBRUECK.Gng;
using GEUTEBRUECK.Gng.PLC;
using GEUTEBRUECK.Gng.Actions;
using GEUTEBRUECK.Gng.Actions.System;
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 Swagger services
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "G-Core Bridge API",
Version = "1.0.0",
Description = "REST API bridge for Geutebruck G-Core Server SDK. Provides access to camera control, video routing, PTZ, and action/event handling. G-Core is the successor to GeViScope."
});
});
// G-Core connection state
GngServer? gngServer = null;
GngPLCWrapper? gngPLC = null;
Dispatcher? actionDispatcher = null;
string? currentAddress = null;
string? currentUsername = null;
DateTime? connectedAt = null;
int eventCount = 0;
List<string> receivedMessages = new List<string>();
List<MediaChannelInfo> mediaChannels = new List<MediaChannelInfo>();
bool sdkIntegrated = true;
// WebSocket clients for event streaming
ConcurrentDictionary<string, WebSocket> wsClients = new ConcurrentDictionary<string, WebSocket>();
// Monitor state tracking (populated from ViewerConnected/ViewerCleared events)
ConcurrentDictionary<int, MonitorState> monitorStates = new ConcurrentDictionary<int, MonitorState>();
// Alarm state tracking (populated from EventStarted/EventStopped events)
ConcurrentDictionary<long, AlarmState> alarmStates = new ConcurrentDictionary<long, AlarmState>();
// 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<byte>(buffer);
var deadClients = new List<string>();
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);
}
}
foreach (var clientId in deadClients)
{
wsClients.TryRemove(clientId, out _);
}
}
// Parse action string into structured event
object? ParseActionToEvent(string actionStr)
{
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 (compatible with GeViScope action format)
var playModeMap = new Dictionary<string, int>(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;
if (int.TryParse(playMode, out int numValue))
return numValue;
Console.WriteLine($"Unknown PlayMode '{playMode}', defaulting to 'play stop' (1)");
return 1;
}
// Helper to add log message
void AddLog(string message)
{
var logMsg = $"[{DateTime.Now:HH:mm:ss}] {message}";
Console.WriteLine(logMsg);
receivedMessages.Add(logMsg);
// Keep only last 1000 messages
if (receivedMessages.Count > 1000)
receivedMessages.RemoveAt(0);
}
// Helper to destroy PLC
void DestroyPLC(bool connectionLost = false)
{
if (gngPLC != null)
{
try
{
if (!connectionLost)
{
gngPLC.UnsubscribeAll();
gngPLC.CloseCallback();
}
gngPLC.Dispose();
}
catch { }
gngPLC = null;
}
actionDispatcher = null;
}
// Helper to create PLC for actions
void CreatePLC()
{
if (gngServer == null) return;
try
{
DestroyPLC();
// Create PLC from server
gngPLC = gngServer.CreatePLC();
// Set up push callback for receiving actions/events
gngPLC.PLCCallback += (sender, e) =>
{
try
{
eventCount++;
if (e.PlcNotification.GetNotificationType() == GngPlcNotificationType.plcnNewActionData)
{
// Decode received action
byte[] actionBuf = e.PlcNotification.GetAction();
var receivedAction = GngAction.Create(actionBuf);
if (receivedAction != null)
{
var actionText = receivedAction.ActionText ?? "";
AddLog($"Action received: {actionText}");
// Broadcast to WebSocket clients
var wsEvent = ParseActionToEvent(actionText);
if (wsEvent != null && wsClients.Count > 0)
{
_ = BroadcastEvent(wsEvent);
}
}
}
else if (e.PlcNotification.GetNotificationType() == GngPlcNotificationType.plcnNewEventData)
{
var eventData = e.PlcNotification.GetEventData();
if (eventData != null)
{
var eventType = eventData.EventNotificationType switch
{
GngPlcEventNotificationType.plcenEventStarted => "started",
GngPlcEventNotificationType.plcenEventStopped => "stopped",
GngPlcEventNotificationType.plcenEventRetriggered => "retriggered",
_ => "unknown"
};
AddLog($"Event: {eventData.EventHeader.EventName} {eventData.EventHeader.EventID} {eventType}");
// 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() == GngPlcNotificationType.plcnPushCallbackLost)
{
AddLog("Connection lost!");
// 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)
{
AddLog($"PLC callback error: {ex.Message}");
}
};
// Open push callback
gngPLC.OpenPushCallback();
// Initialize action dispatcher
actionDispatcher = new Dispatcher();
// Subscribe to all actions and events
gngPLC.SubscribeActionsAll();
gngPLC.SubscribeEventsAll();
AddLog("PLC created and subscribed successfully");
}
catch (Exception ex)
{
AddLog($"Failed to create PLC: {ex.Message}");
}
}
// Helper to load media channels
void LoadMediaChannels()
{
if (gngServer == null) return;
try
{
mediaChannels.Clear();
ArrayList channels = new ArrayList();
MediaPlayerHelperFunctions.QueryMediaChannelList(gngServer, out channels);
foreach (GngMediaChannelData mc in channels)
{
if (mc.IsActive)
{
mediaChannels.Add(new MediaChannelInfo
{
ChannelID = mc.ChannelID,
GlobalNumber = mc.GlobalNumber,
Name = mc.Name ?? $"Channel {mc.GlobalNumber}",
Description = mc.Desc ?? "",
IsActive = mc.IsActive
});
}
}
AddLog($"Loaded {mediaChannels.Count} media channels");
}
catch (Exception ex)
{
AddLog($"Failed to load media channels: {ex.Message}");
}
}
var app = builder.Build();
// Enable Swagger
app.UseSwagger();
app.UseSwaggerUI();
// ============================================================================
// API ENDPOINTS
// ============================================================================
// Health check
app.MapGet("/", () => Results.Ok(new
{
service = "G-Core Bridge",
status = "running",
version = "1.0.0",
sdk_integrated = sdkIntegrated,
connected = gngServer != null
}));
// Connection endpoint
app.MapPost("/connect", (ConnectRequest request) =>
{
try
{
AddLog($"Connecting to {request.Address} as {request.Username}...");
// Disconnect if already connected
if (gngServer != null)
{
DestroyPLC();
try
{
gngServer.Disconnect(5000);
gngServer.Dispose();
}
catch { }
gngServer = null;
mediaChannels.Clear();
}
// Create new server connection
gngServer = new GngServer();
// Encode password
string encodedPassword = DBIHelperFunctions.EncodePassword(request.Password);
// Set connection parameters and connect
using (var connectParams = new GngServerConnectParams(request.Address, request.Username, encodedPassword))
{
gngServer.SetConnectParams(connectParams);
var result = gngServer.Connect();
if (result == GngServerConnectResult.connectOk)
{
currentAddress = request.Address;
currentUsername = request.Username;
connectedAt = DateTime.Now;
// Create PLC for actions
CreatePLC();
// Load media channels
LoadMediaChannels();
AddLog($"Connected to {request.Address} successfully");
return Results.Ok(new
{
success = true,
message = "Connected successfully",
address = currentAddress,
username = currentUsername,
channel_count = mediaChannels.Count,
connected_at = connectedAt
});
}
else
{
var errorMsg = result.ToString();
gngServer.Dispose();
gngServer = null;
AddLog($"Connection failed: {errorMsg}");
return Results.BadRequest(new
{
success = false,
error = errorMsg,
result_code = result.ToString()
});
}
}
}
catch (Exception ex)
{
AddLog($"Connection error: {ex.Message}");
if (gngServer != null)
{
try { gngServer.Dispose(); } catch { }
gngServer = null;
}
return Results.BadRequest(new
{
success = false,
error = "Connection error",
message = ex.Message
});
}
});
// Disconnect endpoint
app.MapPost("/disconnect", () =>
{
try
{
if (gngServer != null)
{
AddLog("Disconnecting...");
DestroyPLC();
gngServer.Disconnect(5000);
gngServer.Dispose();
gngServer = null;
}
currentAddress = null;
currentUsername = null;
connectedAt = null;
mediaChannels.Clear();
AddLog("Disconnected");
return Results.Ok(new
{
success = true,
message = "Disconnected successfully"
});
}
catch (Exception ex)
{
AddLog($"Disconnect error: {ex.Message}");
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 = gngServer != 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 = gngPLC != null,
sdk_integrated = sdkIntegrated
});
});
// Health check endpoint (for load balancers)
app.MapGet("/health", () =>
{
var isHealthy = gngServer != null && gngPLC != null;
return isHealthy
? Results.Ok(new { status = "healthy", server = "gcore-bridge" })
: Results.Ok(new { status = "degraded", server = "gcore-bridge", message = "Not connected to G-Core server" });
});
// Get media channels
app.MapGet("/channels", () =>
{
return Results.Ok(new
{
count = mediaChannels.Count,
channels = mediaChannels
});
});
// Refresh media channels
app.MapPost("/channels/refresh", () =>
{
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
{
AddLog($"Sending action: {request.Action}");
if (gngPLC == null)
{
return Results.BadRequest(new
{
success = false,
error = "Not connected",
message = "Connect to a server first"
});
}
// Create action from string
var action = GngAction.Create(request.Action);
if (action != null)
{
gngPLC.SendAction(action);
return Results.Ok(new
{
success = true,
action = request.Action
});
}
return Results.BadRequest(new
{
success = false,
error = "Invalid action",
message = "Could not decode action string"
});
}
catch (Exception ex)
{
AddLog($"Action error: {ex.Message}");
return Results.BadRequest(new
{
success = false,
error = "Action error",
message = ex.Message
});
}
});
// Send CustomAction
app.MapPost("/custom-action", (CustomActionRequest request) =>
{
try
{
AddLog($"Sending CustomAction({request.TypeId}, \"{request.Text}\")");
if (gngPLC == null)
{
return Results.BadRequest(new
{
success = false,
error = "Not connected"
});
}
// Create CustomAction directly
var customAction = new CustomAction(request.TypeId, request.Text ?? "");
gngPLC.SendAction(customAction);
return Results.Ok(new
{
success = true,
type_id = request.TypeId,
text = request.Text
});
}
catch (Exception ex)
{
AddLog($"CustomAction error: {ex.Message}");
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
{
var playModeValue = GetPlayModeValue(request.PlayMode);
var actionStr = $"ViewerConnect({request.Viewer}, {request.Channel}, {playModeValue})";
AddLog($"Sending: {actionStr}");
if (gngPLC == null)
{
return Results.BadRequest(new { success = false, error = "Not connected" });
}
var action = GngAction.Create(actionStr);
if (action != null)
{
gngPLC.SendAction(action);
return Results.Ok(new
{
success = true,
viewer = request.Viewer,
channel = request.Channel,
play_mode = request.PlayMode
});
}
return Results.BadRequest(new { success = false, error = "Failed to decode action" });
}
catch (Exception ex)
{
return Results.BadRequest(new { success = false, error = ex.Message });
}
});
// ViewerConnectLive - connect viewer to live channel
app.MapPost("/viewer/connect-live", (ViewerConnectLiveRequest request) =>
{
try
{
var actionStr = $"ViewerConnectLive({request.Viewer}, {request.Channel})";
AddLog($"Sending: {actionStr}");
if (gngPLC == null)
{
return Results.BadRequest(new { success = false, error = "Not connected" });
}
var action = GngAction.Create(actionStr);
if (action != null)
{
gngPLC.SendAction(action);
return Results.Ok(new
{
success = true,
viewer = request.Viewer,
channel = request.Channel
});
}
return Results.BadRequest(new { success = false, error = "Failed to decode action" });
}
catch (Exception ex)
{
return Results.BadRequest(new { success = false, error = ex.Message });
}
});
// ViewerClear - clear viewer display
app.MapPost("/viewer/clear", (ViewerClearRequest request) =>
{
try
{
var actionStr = $"ViewerClear({request.Viewer})";
AddLog($"Sending: {actionStr}");
if (gngPLC == null)
{
return Results.BadRequest(new { success = false, error = "Not connected" });
}
var action = GngAction.Create(actionStr);
if (action != null)
{
gngPLC.SendAction(action);
return Results.Ok(new { success = true, viewer = request.Viewer });
}
return Results.BadRequest(new { success = false, error = "Failed to decode action" });
}
catch (Exception ex)
{
return Results.BadRequest(new { success = false, error = ex.Message });
}
});
// CrossSwitch compatibility endpoint
app.MapPost("/crossswitch", (CrossSwitchRequest request) =>
{
try
{
var actionStr = $"ViewerConnectLive({request.VideoOutput}, {request.VideoInput})";
AddLog($"CrossSwitch: {actionStr}");
if (gngPLC == null)
{
return Results.BadRequest(new { success = false, error = "Not connected" });
}
var action = GngAction.Create(actionStr);
if (action != null)
{
gngPLC.SendAction(action);
return Results.Ok(new
{
success = true,
viewer = request.VideoOutput,
channel = request.VideoInput
});
}
return Results.BadRequest(new { success = false, error = "Failed to decode action" });
}
catch (Exception ex)
{
return Results.BadRequest(new { success = false, error = ex.Message });
}
});
// PTZ Camera Control - Pan
app.MapPost("/camera/pan", (CameraPanRequest request) =>
{
try
{
var direction = request.Direction.ToLower() == "left" ? "Left" : "Right";
var actionStr = $"Pan{direction}({request.Camera}, {request.Speed})";
AddLog($"PTZ: {actionStr}");
if (gngPLC == null)
{
return Results.BadRequest(new { success = false, error = "Not connected" });
}
var action = GngAction.Create(actionStr);
if (action != null)
{
gngPLC.SendAction(action);
return Results.Ok(new { success = true, camera = request.Camera, direction, speed = request.Speed });
}
return Results.BadRequest(new { success = false, error = "Failed to decode action" });
}
catch (Exception ex)
{
return Results.BadRequest(new { error = ex.Message });
}
});
// PTZ Camera Control - Tilt
app.MapPost("/camera/tilt", (CameraTiltRequest request) =>
{
try
{
var direction = request.Direction.ToLower() == "up" ? "Up" : "Down";
var actionStr = $"Tilt{direction}({request.Camera}, {request.Speed})";
AddLog($"PTZ: {actionStr}");
if (gngPLC == null)
{
return Results.BadRequest(new { success = false, error = "Not connected" });
}
var action = GngAction.Create(actionStr);
if (action != null)
{
gngPLC.SendAction(action);
return Results.Ok(new { success = true, camera = request.Camera, direction, speed = request.Speed });
}
return Results.BadRequest(new { success = false, error = "Failed to decode action" });
}
catch (Exception ex)
{
return Results.BadRequest(new { error = ex.Message });
}
});
// PTZ Camera Control - Zoom
app.MapPost("/camera/zoom", (CameraZoomRequest request) =>
{
try
{
var direction = request.Direction.ToLower() == "in" ? "In" : "Out";
var actionStr = $"Zoom{direction}({request.Camera}, {request.Speed})";
AddLog($"PTZ: {actionStr}");
if (gngPLC == null)
{
return Results.BadRequest(new { success = false, error = "Not connected" });
}
var action = GngAction.Create(actionStr);
if (action != null)
{
gngPLC.SendAction(action);
return Results.Ok(new { success = true, camera = request.Camera, direction, speed = request.Speed });
}
return Results.BadRequest(new { success = false, error = "Failed to decode action" });
}
catch (Exception ex)
{
return Results.BadRequest(new { error = ex.Message });
}
});
// PTZ Camera Control - Stop all movement
app.MapPost("/camera/stop", (CameraStopRequest request) =>
{
try
{
var actionStr = $"CameraStopAll({request.Camera})";
AddLog($"PTZ: {actionStr}");
if (gngPLC == null)
{
return Results.BadRequest(new { success = false, error = "Not connected" });
}
var action = GngAction.Create(actionStr);
if (action != null)
{
gngPLC.SendAction(action);
return Results.Ok(new { success = true, camera = request.Camera });
}
return Results.BadRequest(new { success = false, error = "Failed to decode action" });
}
catch (Exception ex)
{
return Results.BadRequest(new { error = ex.Message });
}
});
// PTZ Camera Control - Stop pan
app.MapPost("/camera/pan-stop", (CameraStopRequest request) =>
{
try
{
var actionStr = $"PanStop({request.Camera})";
AddLog($"PTZ: {actionStr}");
if (gngPLC == null)
{
return Results.BadRequest(new { success = false, error = "Not connected" });
}
var action = GngAction.Create(actionStr);
if (action != null)
{
gngPLC.SendAction(action);
return Results.Ok(new { success = true, camera = request.Camera });
}
return Results.BadRequest(new { success = false, error = "Failed to decode action" });
}
catch (Exception ex)
{
return Results.BadRequest(new { error = ex.Message });
}
});
// PTZ Camera Control - Stop tilt
app.MapPost("/camera/tilt-stop", (CameraStopRequest request) =>
{
try
{
var actionStr = $"TiltStop({request.Camera})";
AddLog($"PTZ: {actionStr}");
if (gngPLC == null)
{
return Results.BadRequest(new { success = false, error = "Not connected" });
}
var action = GngAction.Create(actionStr);
if (action != null)
{
gngPLC.SendAction(action);
return Results.Ok(new { success = true, camera = request.Camera });
}
return Results.BadRequest(new { success = false, error = "Failed to decode action" });
}
catch (Exception ex)
{
return Results.BadRequest(new { error = ex.Message });
}
});
// PTZ Camera Control - Stop zoom
app.MapPost("/camera/zoom-stop", (CameraStopRequest request) =>
{
try
{
var actionStr = $"ZoomStop({request.Camera})";
AddLog($"PTZ: {actionStr}");
if (gngPLC == null)
{
return Results.BadRequest(new { success = false, error = "Not connected" });
}
var action = GngAction.Create(actionStr);
if (action != null)
{
gngPLC.SendAction(action);
return Results.Ok(new { success = true, camera = request.Camera });
}
return Results.BadRequest(new { success = false, error = "Failed to decode action" });
}
catch (Exception ex)
{
return Results.BadRequest(new { error = ex.Message });
}
});
// PTZ Camera Control - Go to preset
app.MapPost("/camera/preset", (CameraPresetRequest request) =>
{
try
{
var actionStr = $"PrePosCallUp({request.Camera}, {request.Preset})";
AddLog($"PTZ: {actionStr}");
if (gngPLC == null)
{
return Results.BadRequest(new { success = false, error = "Not connected" });
}
var action = GngAction.Create(actionStr);
if (action != null)
{
gngPLC.SendAction(action);
return Results.Ok(new { success = true, camera = request.Camera, preset = request.Preset });
}
return Results.BadRequest(new { success = false, error = "Failed to decode action" });
}
catch (Exception ex)
{
return Results.BadRequest(new { error = ex.Message });
}
});
// Digital Output - Close contact
app.MapPost("/digital-io/close", (DigitalContactRequest request) =>
{
try
{
var actionStr = $"CloseDigitalOutput({request.ContactId})";
AddLog($"DigitalIO: {actionStr}");
if (gngPLC == null)
{
return Results.BadRequest(new { success = false, error = "Not connected" });
}
var action = GngAction.Create(actionStr);
if (action != null)
{
gngPLC.SendAction(action);
return Results.Ok(new { success = true, contact_id = request.ContactId });
}
return Results.BadRequest(new { success = false, error = "Failed to decode action" });
}
catch (Exception ex)
{
return Results.BadRequest(new { error = ex.Message });
}
});
// Digital Output - Open contact
app.MapPost("/digital-io/open", (DigitalContactRequest request) =>
{
try
{
var actionStr = $"OpenDigitalOutput({request.ContactId})";
AddLog($"DigitalIO: {actionStr}");
if (gngPLC == null)
{
return Results.BadRequest(new { success = false, error = "Not connected" });
}
var action = GngAction.Create(actionStr);
if (action != null)
{
gngPLC.SendAction(action);
return Results.Ok(new { success = true, contact_id = request.ContactId });
}
return Results.BadRequest(new { success = false, error = "Failed to decode action" });
}
catch (Exception ex)
{
return Results.BadRequest(new { error = ex.Message });
}
});
// ============================================================================
// PLAYBACK CONTROL ENDPOINTS
// ============================================================================
// ViewerSetPlayMode - Set playback mode and speed
app.MapPost("/viewer/set-play-mode", (ViewerSetPlayModeRequest request) =>
{
try
{
var playModeValue = GetPlayModeValue(request.PlayMode);
var actionStr = $"ViewerSetPlayMode({request.Viewer}, {playModeValue}, {request.PlaySpeed})";
AddLog($"Playback: {actionStr}");
if (gngPLC == null)
{
return Results.BadRequest(new { success = false, error = "Not connected" });
}
var action = GngAction.Create(actionStr);
if (action != null)
{
gngPLC.SendAction(action);
return Results.Ok(new
{
success = true,
viewer = request.Viewer,
play_mode = request.PlayMode,
play_speed = request.PlaySpeed
});
}
return Results.BadRequest(new { success = false, error = "Failed to decode action" });
}
catch (Exception ex)
{
return Results.BadRequest(new { success = false, error = ex.Message });
}
});
// ViewerPlayFromTime - Go to specific date/time
app.MapPost("/viewer/play-from-time", (ViewerPlayFromTimeRequest request) =>
{
try
{
var playModeValue = GetPlayModeValue(request.PlayMode);
var actionStr = $"ViewerPlayFromTime({request.Viewer}, {request.Channel}, {playModeValue}, \"{request.Time}\")";
AddLog($"Playback: {actionStr}");
if (gngPLC == null)
{
return Results.BadRequest(new { success = false, error = "Not connected" });
}
var action = GngAction.Create(actionStr);
if (action != null)
{
gngPLC.SendAction(action);
return Results.Ok(new
{
success = true,
viewer = request.Viewer,
channel = request.Channel,
play_mode = request.PlayMode,
time = request.Time
});
}
return Results.BadRequest(new { success = false, error = "Failed to decode action" });
}
catch (Exception ex)
{
return Results.BadRequest(new { success = false, error = ex.Message });
}
});
// ViewerJumpByTime - Jump playback position by seconds
app.MapPost("/viewer/jump-by-time", (ViewerJumpByTimeRequest request) =>
{
try
{
var playModeValue = GetPlayModeValue(request.PlayMode);
var actionStr = $"ViewerJumpByTime({request.Viewer}, {request.Channel}, {playModeValue}, {request.TimeInSec})";
AddLog($"Playback: {actionStr}");
if (gngPLC == null)
{
return Results.BadRequest(new { success = false, error = "Not connected" });
}
var action = GngAction.Create(actionStr);
if (action != null)
{
gngPLC.SendAction(action);
return Results.Ok(new
{
success = true,
viewer = request.Viewer,
channel = request.Channel,
play_mode = request.PlayMode,
time_in_sec = request.TimeInSec
});
}
return Results.BadRequest(new { success = false, error = "Failed to decode action" });
}
catch (Exception ex)
{
return Results.BadRequest(new { success = false, error = ex.Message });
}
});
// 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 G-Core Bridge event stream"
});
await ws.SendAsync(
new ArraySegment<byte>(Encoding.UTF8.GetBytes(welcome)),
WebSocketMessageType.Text,
true,
CancellationToken.None
);
// Keep connection open and handle incoming messages
var buffer = new byte[1024];
try
{
while (ws.State == WebSocketState.Open)
{
var result = await ws.ReceiveAsync(new ArraySegment<byte>(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("G-Core Bridge v1.0.0");
Console.WriteLine("========================================");
Console.WriteLine("Starting on port 7721...");
Console.WriteLine("Swagger UI: http://localhost:7721/swagger");
Console.WriteLine("WebSocket events: ws://localhost:7721/ws/events");
Console.WriteLine("========================================");
app.Run("http://localhost:7721");
// 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; }
}