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>
This commit is contained in:
159
copilot-coordinator/Program.cs
Normal file
159
copilot-coordinator/Program.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
using System.Net.WebSockets;
|
||||
using CopilotCoordinator.Models;
|
||||
using CopilotCoordinator.Services;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Configure port from config (default 8090)
|
||||
var port = builder.Configuration.GetValue("Port", 8090);
|
||||
builder.WebHost.UseUrls($"http://0.0.0.0:{port}");
|
||||
|
||||
// Register services
|
||||
builder.Services.AddSingleton<WsBroadcaster>();
|
||||
builder.Services.AddSingleton<LockManager>();
|
||||
builder.Services.AddSingleton<SequenceRunner>();
|
||||
builder.Services.AddHttpClient();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Load sequence definitions from config file if present
|
||||
var seqRunner = app.Services.GetRequiredService<SequenceRunner>();
|
||||
var seqConfig = builder.Configuration.GetSection("Sequences");
|
||||
if (seqConfig.Exists())
|
||||
{
|
||||
var sequences = seqConfig.Get<List<SequenceDefinition>>() ?? new();
|
||||
var categories = builder.Configuration.GetSection("SequenceCategories").Get<List<SequenceCategory>>() ?? new();
|
||||
seqRunner.LoadSequences(sequences, categories);
|
||||
}
|
||||
|
||||
// --- Lock expiration background timer (1 second, like legacy CameraLockExpirationWorker) ---
|
||||
var lockManager = app.Services.GetRequiredService<LockManager>();
|
||||
var expirationTimer = new PeriodicTimer(TimeSpan.FromSeconds(1));
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
while (await expirationTimer.WaitForNextTickAsync())
|
||||
{
|
||||
try { await lockManager.CheckExpirations(); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
app.Logger.LogError(ex, "Lock expiration check failed");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// --- WebSocket endpoint ---
|
||||
app.UseWebSockets();
|
||||
app.Map("/ws", async (HttpContext ctx) =>
|
||||
{
|
||||
if (!ctx.WebSockets.IsWebSocketRequest)
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
var keyboardId = ctx.Request.Query["keyboard"].FirstOrDefault() ?? "unknown";
|
||||
var ws = await ctx.WebSockets.AcceptWebSocketAsync();
|
||||
var clientId = Guid.NewGuid().ToString();
|
||||
var broadcaster = ctx.RequestServices.GetRequiredService<WsBroadcaster>();
|
||||
|
||||
await broadcaster.HandleConnection(ws, clientId, keyboardId);
|
||||
});
|
||||
|
||||
// --- Health & Status ---
|
||||
app.MapGet("/health", () => Results.Ok(new { status = "ok", timestamp = DateTime.UtcNow }));
|
||||
|
||||
app.MapGet("/status", async (LockManager locks, SequenceRunner sequences, WsBroadcaster ws) =>
|
||||
{
|
||||
var allLocks = await locks.GetAllLocks();
|
||||
var running = await sequences.GetRunning();
|
||||
var keyboards = ws.GetConnectedKeyboards();
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
status = "ok",
|
||||
keyboards = keyboards.Count,
|
||||
keyboardList = keyboards,
|
||||
activeLocks = allLocks.Count,
|
||||
locks = allLocks,
|
||||
runningSequences = running.Count,
|
||||
sequences = running
|
||||
});
|
||||
});
|
||||
|
||||
// --- Lock endpoints ---
|
||||
app.MapPost("/locks/try", async (LockRequest req, LockManager mgr) =>
|
||||
{
|
||||
var priority = Enum.TryParse<CameraLockPriority>(req.Priority, true, out var p) ? p : CameraLockPriority.Low;
|
||||
var result = await mgr.TryLock(req.CameraId, req.KeyboardId, priority);
|
||||
return Results.Ok(result);
|
||||
});
|
||||
|
||||
app.MapPost("/locks/release", async (UnlockRequest req, LockManager mgr) =>
|
||||
{
|
||||
await mgr.Unlock(req.CameraId, req.KeyboardId);
|
||||
return Results.Ok(new { success = true });
|
||||
});
|
||||
|
||||
app.MapPost("/locks/takeover", async (TakeoverRequest req, LockManager mgr) =>
|
||||
{
|
||||
var priority = Enum.TryParse<CameraLockPriority>(req.Priority, true, out var p) ? p : CameraLockPriority.Low;
|
||||
await mgr.RequestTakeover(req.CameraId, req.KeyboardId, priority);
|
||||
return Results.Ok(new { success = true });
|
||||
});
|
||||
|
||||
app.MapPost("/locks/confirm", async (TakeoverConfirmRequest req, LockManager mgr) =>
|
||||
{
|
||||
await mgr.ConfirmTakeover(req.CameraId, req.KeyboardId, req.Confirm);
|
||||
return Results.Ok(new { success = true });
|
||||
});
|
||||
|
||||
app.MapPost("/locks/reset", async (ResetExpirationRequest req, LockManager mgr) =>
|
||||
{
|
||||
await mgr.ResetExpiration(req.CameraId, req.KeyboardId);
|
||||
return Results.Ok(new { success = true });
|
||||
});
|
||||
|
||||
app.MapGet("/locks", async (LockManager mgr) =>
|
||||
{
|
||||
var locks = await mgr.GetAllLocks();
|
||||
return Results.Ok(locks);
|
||||
});
|
||||
|
||||
app.MapGet("/locks/{keyboardId}", async (string keyboardId, LockManager mgr) =>
|
||||
{
|
||||
var ids = await mgr.GetLockedCameraIds(keyboardId);
|
||||
return Results.Ok(ids);
|
||||
});
|
||||
|
||||
// --- Sequence endpoints ---
|
||||
app.MapPost("/sequences/start", async (SequenceStartRequest req, SequenceRunner runner) =>
|
||||
{
|
||||
var result = await runner.Start(req.ViewerId, req.SequenceId);
|
||||
return result != null ? Results.Ok(result) : Results.NotFound(new { error = "Sequence not found" });
|
||||
});
|
||||
|
||||
app.MapPost("/sequences/stop", async (SequenceStopRequest req, SequenceRunner runner) =>
|
||||
{
|
||||
await runner.Stop(req.ViewerId);
|
||||
return Results.Ok(new { success = true });
|
||||
});
|
||||
|
||||
app.MapGet("/sequences/running", async (SequenceRunner runner) =>
|
||||
{
|
||||
var running = await runner.GetRunning();
|
||||
return Results.Ok(running);
|
||||
});
|
||||
|
||||
app.MapGet("/sequences", (int? categoryId, SequenceRunner runner) =>
|
||||
{
|
||||
var sequences = runner.GetSequences(categoryId);
|
||||
return Results.Ok(sequences);
|
||||
});
|
||||
|
||||
app.MapGet("/sequences/categories", (SequenceRunner runner) =>
|
||||
{
|
||||
return Results.Ok(runner.GetCategories());
|
||||
});
|
||||
|
||||
app.Logger.LogInformation("COPILOT Coordinator starting on port {Port}", port);
|
||||
app.Run();
|
||||
Reference in New Issue
Block a user