- 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>
123 lines
3.9 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|