Files
COPILOT/copilot-coordinator/Services/WsBroadcaster.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

123 lines
3.9 KiB
C#

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);
}
}
}
}
}