- 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>
1693 lines
51 KiB
C#
1693 lines
51 KiB
C#
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<string> receivedMessages = new List<string>();
|
|
List<MediaChannelInfo> mediaChannels = new List<MediaChannelInfo>();
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// 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<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;
|
|
// 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<object>();
|
|
|
|
// 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<object>();
|
|
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<byte>(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<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("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; }
|
|
}
|