Broker native Eve dashboards

This commit is contained in:
Meta Vens 2026-05-31 01:23:15 +01:00
commit f2ae17f342
7 changed files with 559 additions and 80 deletions

View file

@ -7,7 +7,8 @@ using System.Text.Json;
using System.Text.Json.Serialization;
var port = ParseInt(args, "--port", 8795);
using var server = new EveDashboardServer(port);
var providers = EveDashboardProviderCatalog.Create(ParseProviderSpecs(args));
using var server = new EveDashboardServer(port, providers);
await server.RunAsync();
static int ParseInt(IReadOnlyList<string> args, string name, int fallback)
@ -24,7 +25,21 @@ static int ParseInt(IReadOnlyList<string> args, string name, int fallback)
return fallback;
}
internal sealed class EveDashboardServer(int port) : IDisposable
static IReadOnlyList<string> ParseProviderSpecs(IReadOnlyList<string> args)
{
var specs = new List<string>();
for (var index = 0; index < args.Count - 1; index++)
{
if (string.Equals(args[index], "--provider", StringComparison.OrdinalIgnoreCase))
{
specs.Add(args[index + 1]);
}
}
return specs;
}
internal sealed class EveDashboardServer(int port, EveDashboardProviderCatalog providers) : IDisposable
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
@ -35,12 +50,13 @@ internal sealed class EveDashboardServer(int port) : IDisposable
private readonly TcpListener listener = new(IPAddress.Any, port);
private readonly CancellationTokenSource stopping = new();
private readonly ConcurrentDictionary<Guid, DashboardSocket> clients = new();
private readonly DashboardState state = DashboardState.CreateDefault();
private string activeProviderId = providers.DefaultProviderId;
public async Task RunAsync()
{
listener.Start();
Console.WriteLine($"Mimir Eve dashboard listening on ws://0.0.0.0:{port}/eve/dashboard");
Console.WriteLine($"Mimir Eve dashboard broker listening on ws://0.0.0.0:{port}/eve/deck");
Console.WriteLine($"Compatibility endpoint remains ws://0.0.0.0:{port}/eve/dashboard");
while (!stopping.IsCancellationRequested)
{
var client = await listener.AcceptTcpClientAsync(stopping.Token).ConfigureAwait(false);
@ -54,19 +70,50 @@ internal sealed class EveDashboardServer(int port) : IDisposable
var request = await ReadHttpRequestAsync(stream).ConfigureAwait(false);
Console.WriteLine($"EVE dashboard connected: {tcpClient.Client.RemoteEndPoint} {request.Path}");
Console.Out.Flush();
if (request.Path == "/health")
{
var health = JsonSerializer.Serialize(new { ok = true, clients = clients.Count, state.Version }, JsonOptions);
var provider = providers.Get(activeProviderId);
var health = JsonSerializer.Serialize(new
{
ok = true,
clients = clients.Count,
activeProviderId,
providerVersion = provider.State.Version,
providers = providers.Manifests.Count,
transport = "eve-deck-ws",
cultMeshDocument = "mimir.eve_dashboard_state",
}, JsonOptions);
await WriteHttpResponseAsync(stream, "200 OK", "application/json", Encoding.UTF8.GetBytes(health)).ConfigureAwait(false);
return;
}
if (request.Path != "/eve/dashboard" || !request.Headers.TryGetValue("Sec-WebSocket-Key", out var key))
if (request.Path is "/eve/deck/manifest" or "/eve/dashboard/manifest")
{
var manifest = JsonSerializer.Serialize(providers.BrokerManifest, JsonOptions);
await WriteHttpResponseAsync(stream, "200 OK", "application/json", Encoding.UTF8.GetBytes(manifest)).ConfigureAwait(false);
return;
}
if (request.Path == "/eve/deck/providers")
{
var catalog = JsonSerializer.Serialize(new DashboardProviderCatalogDocument(providers.Manifests), JsonOptions);
await WriteHttpResponseAsync(stream, "200 OK", "application/json", Encoding.UTF8.GetBytes(catalog)).ConfigureAwait(false);
return;
}
if (!IsDashboardPath(request.Path, out var requestedProviderId) ||
!request.Headers.TryGetValue("Sec-WebSocket-Key", out var key))
{
await WriteHttpResponseAsync(stream, "404 Not Found", "text/plain", Encoding.UTF8.GetBytes("not found")).ConfigureAwait(false);
return;
}
if (!string.IsNullOrWhiteSpace(requestedProviderId) && providers.Contains(requestedProviderId))
{
activeProviderId = requestedProviderId;
}
await WriteWebSocketHandshakeAsync(stream, key).ConfigureAwait(false);
var id = Guid.NewGuid();
var socket = new DashboardSocket(tcpClient, stream);
@ -101,6 +148,24 @@ internal sealed class EveDashboardServer(int port) : IDisposable
}
}
private static bool IsDashboardPath(string path, out string providerId)
{
providerId = "";
if (path == "/eve/dashboard" || path == "/eve/deck")
{
return true;
}
const string providerPrefix = "/eve/deck/";
if (path.StartsWith(providerPrefix, StringComparison.Ordinal))
{
providerId = Uri.UnescapeDataString(path[providerPrefix.Length..]);
return !string.IsNullOrWhiteSpace(providerId);
}
return false;
}
private bool ApplyCommand(string text)
{
DashboardCommand? command;
@ -113,59 +178,46 @@ internal sealed class EveDashboardServer(int port) : IDisposable
return false;
}
if (command == null || string.IsNullOrWhiteSpace(command.NodeId))
if (command == null)
{
return false;
}
var node = state.Nodes.FirstOrDefault(candidate => string.Equals(candidate.Id, command.NodeId, StringComparison.Ordinal));
if (node == null)
if (string.Equals(command.Type, "open-provider", StringComparison.OrdinalIgnoreCase))
{
var providerId = command.ProviderId;
if (string.IsNullOrWhiteSpace(providerId) && !string.IsNullOrWhiteSpace(command.NodeId))
{
providerId = providers.Get(activeProviderId).State.Nodes
.FirstOrDefault(node => string.Equals(node.Id, command.NodeId, StringComparison.Ordinal))
?.ProviderId;
}
if (!string.IsNullOrWhiteSpace(providerId) && providers.Contains(providerId))
{
activeProviderId = providerId;
Console.WriteLine($"EVE dashboard provider switched: {activeProviderId}");
Console.Out.Flush();
return true;
}
return false;
}
switch ((command.Type ?? "").Trim().ToLowerInvariant())
var provider = providers.Get(activeProviderId);
if (provider.ApplyCommand(command))
{
case "select":
state.SelectedNodeId = node.Id;
break;
case "move":
state.SelectedNodeId = node.Id;
node.X = Clamp(command.X ?? node.X, -1.0, 1.0);
node.Y = Clamp(command.Y ?? node.Y, -1.0, 1.0);
break;
case "scale":
state.SelectedNodeId = node.Id;
node.Scale = Clamp(command.Scale ?? node.Scale, 0.25, 3.0);
break;
case "rotate":
state.SelectedNodeId = node.Id;
node.Rotation = command.Rotation ?? node.Rotation;
break;
case "toggle-visibility":
state.SelectedNodeId = node.Id;
node.Visible = command.Visible ?? !node.Visible;
break;
case "reset-transform":
state.SelectedNodeId = node.Id;
node.X = node.DefaultX;
node.Y = node.DefaultY;
node.Rotation = 0.0;
node.Scale = 1.0;
break;
default:
return false;
Console.WriteLine($"EVE dashboard command({activeProviderId}): {text}");
Console.Out.Flush();
return true;
}
state.Version++;
state.UpdatedAt = DateTimeOffset.UtcNow;
Console.WriteLine($"EVE dashboard command: {text}");
Console.Out.Flush();
return true;
return false;
}
private async Task SendStateAsync(DashboardSocket socket)
{
var state = providers.Get(activeProviderId).State;
await SendTextFrameAsync(socket.Stream, JsonSerializer.Serialize(state, JsonOptions)).ConfigureAwait(false);
}
@ -184,8 +236,6 @@ internal sealed class EveDashboardServer(int port) : IDisposable
}
}
private static double Clamp(double value, double min, double max) => Math.Min(max, Math.Max(min, value));
private static async Task<HttpRequest> ReadHttpRequestAsync(NetworkStream stream)
{
var bytes = new List<byte>();
@ -350,16 +400,335 @@ internal sealed class EveDashboardServer(int port) : IDisposable
}
}
internal sealed class EveDashboardProviderCatalog
{
private readonly Dictionary<string, IDashboardProvider> providers;
private EveDashboardProviderCatalog(IEnumerable<IDashboardProvider> providers)
{
this.providers = providers.ToDictionary(provider => provider.Manifest.Id, StringComparer.Ordinal);
DefaultProviderId = DashboardBrokerProvider.ProviderId;
Manifests = this.providers.Values.Select(provider => provider.Manifest).ToArray();
BrokerManifest = new DashboardProviderManifest(
"eve.dashboard.broker",
"Eve Dashboard Broker",
"Native retained dashboard router for LAN and SSH-tunneled control surfaces.",
"1",
"/eve/deck",
["scene2d", "provider-switching", "touch-commands", "cultmesh-state-documents"],
UsesCultMesh: true,
Transport: "WebSocket now; CultMesh typed state when available.");
}
public string DefaultProviderId { get; }
public IReadOnlyList<DashboardProviderManifest> Manifests { get; }
public DashboardProviderManifest BrokerManifest { get; }
public static EveDashboardProviderCatalog Create(IReadOnlyList<string> remoteSpecs)
{
var providers = new List<IDashboardProvider>();
var remoteProviders = remoteSpecs.Select(RemoteDashboardProvider.FromSpec).OfType<RemoteDashboardProvider>().ToArray();
providers.Add(new DashboardBrokerProvider(remoteProviders));
providers.Add(new MimirStreamLayoutProvider());
providers.Add(new YggdrasilStreamPixelsProvider());
providers.AddRange(remoteProviders);
return new EveDashboardProviderCatalog(providers);
}
public bool Contains(string providerId) => providers.ContainsKey(providerId);
public IDashboardProvider Get(string providerId) =>
providers.TryGetValue(providerId, out var provider) ? provider : providers[DefaultProviderId];
}
internal interface IDashboardProvider
{
DashboardProviderManifest Manifest { get; }
DashboardState State { get; }
bool ApplyCommand(DashboardCommand command);
}
internal abstract class MutableDashboardProvider : IDashboardProvider
{
protected MutableDashboardProvider(DashboardProviderManifest manifest, DashboardState state)
{
Manifest = manifest;
State = state;
}
public DashboardProviderManifest Manifest { get; }
public DashboardState State { get; }
public virtual bool ApplyCommand(DashboardCommand command)
{
if (string.IsNullOrWhiteSpace(command.NodeId))
{
return false;
}
var node = State.Nodes.FirstOrDefault(candidate => string.Equals(candidate.Id, command.NodeId, StringComparison.Ordinal));
if (node == null)
{
return false;
}
switch ((command.Type ?? "").Trim().ToLowerInvariant())
{
case "select":
State.SelectedNodeId = node.Id;
break;
case "move":
State.SelectedNodeId = node.Id;
node.X = Clamp(command.X ?? node.X, -1.0, 1.0);
node.Y = Clamp(command.Y ?? node.Y, -1.0, 1.0);
break;
case "scale":
State.SelectedNodeId = node.Id;
node.Scale = Clamp(command.Scale ?? node.Scale, 0.25, 3.0);
break;
case "rotate":
State.SelectedNodeId = node.Id;
node.Rotation = command.Rotation ?? node.Rotation;
break;
case "toggle-visibility":
State.SelectedNodeId = node.Id;
node.Visible = command.Visible ?? !node.Visible;
break;
case "reset-transform":
State.SelectedNodeId = node.Id;
node.X = node.DefaultX;
node.Y = node.DefaultY;
node.Rotation = 0.0;
node.Scale = 1.0;
break;
default:
return false;
}
Touch();
return true;
}
protected void Touch()
{
State.Version++;
State.UpdatedAt = DateTimeOffset.UtcNow;
}
protected static double Clamp(double value, double min, double max) => Math.Min(max, Math.Max(min, value));
}
internal sealed class DashboardBrokerProvider : MutableDashboardProvider
{
public const string ProviderId = "eve.dashboard.broker";
public DashboardBrokerProvider(IReadOnlyList<RemoteDashboardProvider> remoteProviders)
: base(
new DashboardProviderManifest(
ProviderId,
"Dashboard Switchboard",
"Provider picker rendered as native Eve panels.",
"1",
"/eve/deck",
["scene2d", "provider-switching"],
UsesCultMesh: true,
Transport: "broker-local"),
CreateState(remoteProviders))
{
}
private static DashboardState CreateState(IReadOnlyList<RemoteDashboardProvider> remoteProviders)
{
var nodes = new List<DashboardNode>
{
new("provider-mimir", "Mimir Stream Layout", "dashboard-provider", -0.42, -0.34, 0.34, 0.22, "local")
{
ProviderId = MimirStreamLayoutProvider.ProviderId,
Command = "open-provider",
},
new("provider-streampixels", "StreamPixels Edge", "dashboard-provider", 0.38, -0.34, 0.34, 0.22, "ssh ready")
{
ProviderId = YggdrasilStreamPixelsProvider.ProviderId,
Command = "open-provider",
},
};
var y = 0.28;
foreach (var provider in remoteProviders)
{
nodes.Add(new DashboardNode(
$"provider-{provider.Manifest.Id}",
provider.Manifest.Title,
"dashboard-provider",
0.0,
y,
0.40,
0.18,
"external")
{
ProviderId = provider.Manifest.Id,
Command = "open-provider",
});
y = Math.Min(0.80, y + 0.22);
}
return new DashboardState
{
ProviderId = ProviderId,
Title = "Eve Dashboard Switchboard",
SelectedNodeId = nodes[0].Id,
Nodes = nodes,
};
}
}
internal sealed class MimirStreamLayoutProvider : MutableDashboardProvider
{
public const string ProviderId = "mimir.stream.layout";
public MimirStreamLayoutProvider()
: base(
new DashboardProviderManifest(
ProviderId,
"Mimir Stream Layout",
"Native scene graph editor for program layout, source transforms, visibility, and future LUT controls.",
"1",
"/eve/deck/mimir.stream.layout",
["scene2d", "visibility", "transform", "lut-presets", "audio-mix"],
UsesCultMesh: true,
Transport: "local WebSocket; state document can mirror through CultMesh."),
CreateState())
{
}
private static DashboardState CreateState() =>
new()
{
ProviderId = ProviderId,
Title = "Mimir Stream Layout",
Nodes =
[
new DashboardNode("program", "Program Output", "camera", -0.05, -0.05, 0.62, 0.34, "live"),
new DashboardNode("raven-display", "Raven Display", "screen", -0.58, -0.50, 0.34, 0.20, "waiting"),
new DashboardNode("eve-camera", "Eve Camera", "camera", 0.46, -0.48, 0.30, 0.22, "armed"),
new DashboardNode("kiyo-pro", "Kiyo Pro", "camera", -0.42, 0.42, 0.24, 0.18, "live"),
new DashboardNode("leap-field", "Leap Field", "sensor", 0.38, 0.40, 0.28, 0.24, "calibrating"),
],
};
}
internal sealed class YggdrasilStreamPixelsProvider : MutableDashboardProvider
{
public const string ProviderId = "yggdrasil.streampixels.edge";
public YggdrasilStreamPixelsProvider()
: base(
new DashboardProviderManifest(
ProviderId,
"StreamPixels Edge",
"Yggdrasil/StreamPixels live edge control surface reachable over HTTPS or SSH-forwarded localhost.",
"1",
"/eve/deck/yggdrasil.streampixels.edge",
["status", "health", "ssh-tunnel", "hls"],
UsesCultMesh: false,
Transport: "TCP/WebSocket over LAN or SSH tunnel."),
new DashboardState
{
ProviderId = ProviderId,
Title = "StreamPixels Edge",
SelectedNodeId = "hls-origin",
Nodes =
[
new DashboardNode("hls-origin", "HLS Origin", "service", -0.34, -0.18, 0.34, 0.22, "streampixels"),
new DashboardNode("rtmp-ingest", "RTMP Ingest", "service", 0.34, -0.18, 0.34, 0.22, "localhost"),
new DashboardNode("ssh-tunnel", "Yggdrasil SSH Tunnel", "transport", 0.0, 0.34, 0.44, 0.18, "tcp only"),
],
})
{
}
}
internal sealed class RemoteDashboardProvider : MutableDashboardProvider
{
private RemoteDashboardProvider(DashboardProviderManifest manifest, string upstream)
: base(
manifest,
new DashboardState
{
ProviderId = manifest.Id,
Title = manifest.Title,
SelectedNodeId = "remote-endpoint",
Nodes =
[
new DashboardNode("remote-endpoint", manifest.Title, "remote-dashboard", 0.0, -0.10, 0.46, 0.24, "registered")
{
Endpoint = upstream,
},
new DashboardNode("remote-transport", "Transport", "transport", 0.0, 0.34, 0.40, 0.18, "tcp/ws")
{
Endpoint = upstream,
},
],
})
{
}
public static RemoteDashboardProvider? FromSpec(string spec)
{
var parts = spec.Split('|', 3, StringSplitOptions.TrimEntries);
if (parts.Length < 3 || string.IsNullOrWhiteSpace(parts[0]) || string.IsNullOrWhiteSpace(parts[1]) || string.IsNullOrWhiteSpace(parts[2]))
{
Console.Error.WriteLine($"Ignoring dashboard provider spec. Expected id|title|ws-url, got: {spec}");
return null;
}
return new RemoteDashboardProvider(
new DashboardProviderManifest(
parts[0],
parts[1],
"External EveDeck dashboard provider registered at broker launch.",
"1",
parts[2],
["scene2d", "external-provider"],
UsesCultMesh: false,
Transport: "registered WebSocket endpoint"),
parts[2]);
}
}
internal sealed record HttpRequest(string Path, IReadOnlyDictionary<string, string> Headers);
internal sealed record WebSocketFrame(int Opcode, byte[] Payload);
internal sealed record DashboardSocket(TcpClient Client, NetworkStream Stream);
internal sealed record DashboardProviderCatalogDocument(IReadOnlyList<DashboardProviderManifest> Providers);
internal sealed record DashboardProviderManifest(
string Id,
string Title,
string Description,
string Version,
string Endpoint,
string[] Capabilities,
bool UsesCultMesh,
string Transport);
internal sealed class DashboardState
{
public string Type { get; set; } = "dashboard-state";
public string Schema { get; set; } = "mimir.eve_dashboard_state.v1";
public string ProviderId { get; set; } = MimirStreamLayoutProvider.ProviderId;
public string Title { get; set; } = "Mimir Dashboard";
public long Version { get; set; } = 1;
public DateTimeOffset UpdatedAt { get; set; } = DateTimeOffset.UtcNow;
@ -369,21 +738,6 @@ internal sealed class DashboardState
public string LutPreset { get; set; } = "neutral";
public List<DashboardNode> Nodes { get; set; } = [];
public static DashboardState CreateDefault()
{
return new DashboardState
{
Nodes =
[
new DashboardNode("program", "Program Output", "camera", -0.05, -0.05, 0.62, 0.34, "live"),
new DashboardNode("raven-display", "Raven Display", "screen", -0.58, -0.50, 0.34, 0.20, "waiting"),
new DashboardNode("eve-camera", "Eve Camera", "camera", 0.46, -0.48, 0.30, 0.22, "armed"),
new DashboardNode("kiyo-pro", "Kiyo Pro", "camera", -0.42, 0.42, 0.24, 0.18, "live"),
new DashboardNode("leap-field", "Leap Field", "sensor", 0.38, 0.40, 0.28, 0.24, "calibrating"),
],
};
}
}
internal sealed class DashboardNode(
@ -423,6 +777,12 @@ internal sealed class DashboardNode(
public double Height { get; set; } = height;
public string Health { get; set; } = health;
public string? ProviderId { get; set; }
public string? Command { get; set; }
public string? Endpoint { get; set; }
}
internal sealed class DashboardCommand
@ -431,6 +791,8 @@ internal sealed class DashboardCommand
public string NodeId { get; set; } = "";
public string? ProviderId { get; set; }
public double? X { get; set; }
public double? Y { get; set; }

View file

@ -104,6 +104,51 @@ public sealed record MimirFaustControlSnapshot(
[property: Key(0)] string Path,
[property: Key(1)] float Value);
[CultDocument("mimir.eve_dashboard_manifest", "mimir.eve_dashboard_manifest.v1")]
[MessagePackObject]
public sealed record MimirEveDashboardManifestDocument(
[property: Key(0)]
[property: CultName]
string ProviderId,
[property: Key(1)] string Title,
[property: Key(2)] string Description,
[property: Key(3)] string Version,
[property: Key(4)] string Endpoint,
[property: Key(5)] string[] Capabilities,
[property: Key(6)] bool UsesCultMesh,
[property: Key(7)] string Transport);
[CultDocument("mimir.eve_dashboard_state", "mimir.eve_dashboard_state.v1")]
[MessagePackObject]
public sealed record MimirEveDashboardStateDocument(
[property: Key(0)]
[property: CultName]
string ProviderId,
[property: Key(1)] string Title,
[property: Key(2)] long Version,
[property: Key(3)] string UpdatedAtUtc,
[property: Key(4)] string SelectedNodeId,
[property: Key(5)] string LutPreset,
[property: Key(6)] MimirEveDashboardNodeSnapshot[] Nodes);
[MessagePackObject]
public sealed record MimirEveDashboardNodeSnapshot(
[property: Key(0)] string Id,
[property: Key(1)] string Label,
[property: Key(2)] string Kind,
[property: Key(3)] bool Visible,
[property: Key(4)] double X,
[property: Key(5)] double Y,
[property: Key(6)] double Z,
[property: Key(7)] double Rotation,
[property: Key(8)] double Scale,
[property: Key(9)] double Width,
[property: Key(10)] double Height,
[property: Key(11)] string Health,
[property: Key(12)] string? ProviderId,
[property: Key(13)] string? Command,
[property: Key(14)] string? Endpoint);
public static class MimirCultMeshContractFactory
{
public static MimirBioacousticCodebookState CreateCodebookState(

View file

@ -6,7 +6,8 @@ public enum MimirNetworkPayloadKind
RawAudioDebugWindow,
CompressedAudioProgramFeed,
CompressedVideoProgramFeed,
RawVideoDiagnosticWindow
RawVideoDiagnosticWindow,
NativeDashboardState
}
public enum MimirNetworkTransportKind
@ -80,11 +81,24 @@ public static class MimirNetworkTransportConfigurations
Notes = "Do not trust browser media timestamps without decoded witness state."
};
public static MimirNetworkTransportConfiguration EveDashboardState { get; } = new(
"eve-dashboard-state",
"Native retained dashboard trees and provider manifests for Eve-rendered control surfaces.",
MimirNetworkTransportKind.CultMeshTypedState,
MimirNetworkPayloadKind.NativeDashboardState,
MayAffectClock: false,
CarriesRawMedia: false,
TargetLatencyMilliseconds: 50.0,
RequiredDocuments: ["mimir.eve_dashboard_manifest", "mimir.eve_dashboard_state"],
RejectionConditions: ["unknown-provider", "untrusted-provider", "schema-version-mismatch", "command-owner-conflict"],
"Dashboard providers own state and commands. Eve owns native rendering and touch. No remote UI code runs on Eve.");
public static IReadOnlyList<MimirNetworkTransportConfiguration> BuiltIn { get; } =
[
CultMeshTimingState,
RawAudioDebugWindow,
SrtProgramBridge,
WebRtcExperiment
WebRtcExperiment,
EveDashboardState
];
}