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:
122
copilot-coordinator/Services/WsBroadcaster.cs
Normal file
122
copilot-coordinator/Services/WsBroadcaster.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using CopilotCoordinator.Models;
|
||||
|
||||
namespace CopilotCoordinator.Services;
|
||||
|
||||
/// <summary>
|
||||
/// WebSocket broadcaster. Manages connected keyboard clients and sends events.
|
||||
/// </summary>
|
||||
public class WsBroadcaster
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, (WebSocket Socket, string KeyboardId)> _clients = new();
|
||||
private readonly ILogger<WsBroadcaster> _logger;
|
||||
|
||||
public WsBroadcaster(ILogger<WsBroadcaster> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public IReadOnlyList<KeyboardInfo> GetConnectedKeyboards()
|
||||
{
|
||||
return _clients.Values
|
||||
.Select(c => new KeyboardInfo(c.KeyboardId, null, DateTime.UtcNow))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task HandleConnection(WebSocket ws, string clientId, string keyboardId)
|
||||
{
|
||||
_clients[clientId] = (ws, keyboardId);
|
||||
_logger.LogInformation("Keyboard {KeyboardId} connected (client {ClientId})", keyboardId, clientId);
|
||||
|
||||
await Broadcast("keyboard_online", new { keyboardId });
|
||||
|
||||
try
|
||||
{
|
||||
var buffer = new byte[4096];
|
||||
while (ws.State == WebSocketState.Open)
|
||||
{
|
||||
var result = await ws.ReceiveAsync(buffer, CancellationToken.None);
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
break;
|
||||
|
||||
// Parse incoming messages (keyboard can send commands via WebSocket too)
|
||||
if (result.MessageType == WebSocketMessageType.Text)
|
||||
{
|
||||
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
|
||||
_logger.LogDebug("Received from {KeyboardId}: {Message}", keyboardId, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (WebSocketException ex)
|
||||
{
|
||||
_logger.LogWarning("WebSocket error for {KeyboardId}: {Message}", keyboardId, ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_clients.TryRemove(clientId, out _);
|
||||
_logger.LogInformation("Keyboard {KeyboardId} disconnected", keyboardId);
|
||||
await Broadcast("keyboard_offline", new { keyboardId });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Broadcast a message to all connected keyboards.
|
||||
/// </summary>
|
||||
public async Task Broadcast(string type, object? data = null)
|
||||
{
|
||||
var json = WsMessage.Serialize(type, data);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
|
||||
var deadClients = new List<string>();
|
||||
|
||||
foreach (var (clientId, (socket, _)) in _clients)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (socket.State == WebSocketState.Open)
|
||||
{
|
||||
await socket.SendAsync(bytes, WebSocketMessageType.Text, true, CancellationToken.None);
|
||||
}
|
||||
else
|
||||
{
|
||||
deadClients.Add(clientId);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
deadClients.Add(clientId);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var id in deadClients)
|
||||
_clients.TryRemove(id, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send a message to a specific keyboard.
|
||||
/// </summary>
|
||||
public async Task SendTo(string keyboardId, string type, object? data = null)
|
||||
{
|
||||
var json = WsMessage.Serialize(type, data);
|
||||
var bytes = Encoding.UTF8.GetBytes(json);
|
||||
|
||||
foreach (var (clientId, (socket, kbId)) in _clients)
|
||||
{
|
||||
if (kbId.Equals(keyboardId, StringComparison.OrdinalIgnoreCase) &&
|
||||
socket.State == WebSocketState.Open)
|
||||
{
|
||||
try
|
||||
{
|
||||
await socket.SendAsync(bytes, WebSocketMessageType.Text, true, CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning("Failed to send to {KeyboardId}: {Error}", keyboardId, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user