Broker native Eve dashboards
This commit is contained in:
parent
31310a9003
commit
f2ae17f342
7 changed files with 559 additions and 80 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -11,6 +11,7 @@ artifacts/obs-sdk/
|
|||
artifacts/asio/
|
||||
artifacts/bioacoustic-contestants/
|
||||
artifacts/birdsong-reference/
|
||||
artifacts/eve-dashboard/
|
||||
artifacts/python-packages/
|
||||
artifacts/spectra/**/*.wav
|
||||
tools/drivers/
|
||||
|
|
|
|||
|
|
@ -2,25 +2,34 @@
|
|||
|
||||
## Objective
|
||||
|
||||
Use EveCanvas as a native Mimir operator dashboard: scene graph, source panels,
|
||||
visibility/reset controls, and multitouch transform editing. Eve renders the
|
||||
control surface locally and sends edit intents to Mimir; it does not own final
|
||||
composition.
|
||||
Use EveCanvas as a native operator dashboard substrate: Mimir, StreamPixels, and
|
||||
other local/Yggdrasil-routed apps can publish retained dashboard trees while Eve
|
||||
renders the control surface locally and sends edit intents back. Eve does not
|
||||
own final composition or run remote UI code.
|
||||
|
||||
## Authority Map
|
||||
|
||||
- Owner: Mimir owns accepted dashboard state and transform truth.
|
||||
- Owner: each dashboard provider owns its accepted state and command handling.
|
||||
The Starfire broker owns provider discovery/routing. EveCanvas owns native
|
||||
rendering, hit-testing, and touch affordances.
|
||||
- Inputs: Eve sends `select`, `move`, `scale`, `rotate`, `toggle-visibility`,
|
||||
and `reset-transform` commands over `/eve/dashboard`.
|
||||
- Outputs: Mimir sends `dashboard-state` snapshots with nodes, health,
|
||||
visibility, dimensions, transform, selected node, and LUT preset.
|
||||
`reset-transform`, and `open-provider` commands over `/eve/deck`.
|
||||
- Outputs: providers send `dashboard-state` snapshots with provider id, title,
|
||||
nodes, health, visibility, dimensions, transform, selected node, and LUT
|
||||
preset. Provider catalogs are exposed at `/eve/deck/providers`.
|
||||
- Derived state: Eve's UIKit panels are local render projections of Mimir
|
||||
state.
|
||||
state/provider state.
|
||||
- Forbidden writers: Eve gestures do not mutate program layout directly; they
|
||||
request changes and then re-render the accepted snapshot.
|
||||
request changes and then re-render the accepted snapshot. Remote apps do not
|
||||
ship arbitrary JavaScript or native code into Eve.
|
||||
- Cut line: no camera-stream fanout to Eve for the dashboard. Source previews
|
||||
may be added later as thumbnails/proxies, but dashboard control must not
|
||||
require Starfire to render every camera feed for the iPad.
|
||||
- CultMesh line: dashboard manifests and states now have typed
|
||||
`mimir.eve_dashboard_manifest.v1` and `mimir.eve_dashboard_state.v1`
|
||||
document shapes. The live iPad lane remains WebSocket/TCP because it is the
|
||||
proven transport and works over SSH tunnels; CultMesh should mirror compact
|
||||
state where the local mesh substrate is available.
|
||||
|
||||
## Runtime
|
||||
|
||||
|
|
@ -32,16 +41,46 @@ dotnet run --project .\src\Mimir.EveDashboard\Mimir.EveDashboard.csproj -- --por
|
|||
|
||||
EveCanvas connects to:
|
||||
|
||||
```text
|
||||
ws://192.168.1.66:8795/eve/deck
|
||||
```
|
||||
|
||||
The compatibility endpoint remains:
|
||||
|
||||
```text
|
||||
ws://192.168.1.66:8795/eve/dashboard
|
||||
```
|
||||
|
||||
Broker inspection endpoints:
|
||||
|
||||
```text
|
||||
http://192.168.1.66:8795/health
|
||||
http://192.168.1.66:8795/eve/deck/manifest
|
||||
http://192.168.1.66:8795/eve/deck/providers
|
||||
ws://192.168.1.66:8795/eve/deck/{providerId}
|
||||
```
|
||||
|
||||
Extra providers can be registered at launch:
|
||||
|
||||
```powershell
|
||||
dotnet run --project .\src\Mimir.EveDashboard\Mimir.EveDashboard.csproj -- `
|
||||
--port 8795 `
|
||||
--provider "app.id|App Title|ws://127.0.0.1:14000/eve/deck"
|
||||
```
|
||||
|
||||
## Current State
|
||||
|
||||
`Mimir.EveDashboard` currently serves fixture scene nodes for the first native
|
||||
control proof. It accepts transform/visibility commands, mutates the server-side
|
||||
state, increments the state version, and broadcasts the new snapshot. The next
|
||||
cut is replacing fixture state with live `MimirPresentationControlState` and
|
||||
`Mimir.EveDashboard` now runs as a small native dashboard broker. It includes:
|
||||
|
||||
- `eve.dashboard.broker`: the switchboard rendered as provider cards.
|
||||
- `mimir.stream.layout`: the existing Mimir source-layout fixture provider.
|
||||
- `yggdrasil.streampixels.edge`: the first Yggdrasil/StreamPixels service
|
||||
dashboard placeholder for the TCP/SSH-routed live edge.
|
||||
|
||||
The Mimir provider still serves fixture scene nodes for the first native control
|
||||
proof. It accepts transform/visibility commands, mutates the provider state,
|
||||
increments the state version, and broadcasts the new snapshot. The next cut is
|
||||
replacing fixture state with live `MimirPresentationControlState` and
|
||||
`MimirSceneEditorState` snapshots.
|
||||
|
||||
## Verification
|
||||
|
|
@ -60,3 +99,12 @@ The first device screenshot proved the dashboard rendered on Eve, but also
|
|||
showed the legacy debug overlay occluding the hierarchy panel. EveCanvas hides
|
||||
that overlay while dashboard mode is active; dashboard status remains in the
|
||||
native dashboard footer.
|
||||
|
||||
The broker smoke on 2026-05-31 verified:
|
||||
|
||||
- `/health` reports active provider, client count, transport, and typed
|
||||
CultMesh dashboard document name.
|
||||
- `/eve/deck/providers` returns the three built-in provider manifests.
|
||||
- WebSocket `/eve/deck` sends the switchboard snapshot.
|
||||
- `open-provider` switches to `mimir.stream.layout` and broadcasts that
|
||||
provider state.
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -250,3 +250,4 @@
|
|||
{"ts": "2026-05-31T01:03:07+01:00", "type": "verification", "status": "ok", "note": "Eve dashboard proof: EveCanvas rendered the native dashboard on device from Mimir.EveDashboard snapshots, showing scene graph/source panels and transform controls. A command smoke moved eve-camera and received version 2 state. Screenshot exposed legacy debug overlay occlusion; EveCanvas now hides the debug overlay while dashboard mode is active."}
|
||||
{"ts": "2026-05-31T01:06:44+01:00", "type": "verification", "status": "ok", "note": "Live Leap surface-claim proof captured artifacts/runtime/mimir-leap-surface-claims-clean.png. With AQUARIUM_DISABLE_RESERVOIR_HISTORY=1, Fensalir now presents the current reservoir resolver target instead of an unwritten history target; LeapUVC frames uploaded through the Fensalir-owned D3D12 texture, packed stereo depth dispatched, point-cloud compute generated 19,200 SDF/PBR/radiosity surface splats, point-list rendering stayed diagnostic at rendered 0, and visible Leap-derived splats reached presentation."}
|
||||
{"ts": "2026-05-31T01:20:43+01:00", "type": "verification", "status": "ok", "note": "Reservoir-history startup repaired for the Leap surface-claim path. Fensalir now compiles dedicated D3D12ReservoirHistoryUpdate.hlsl instead of the monolithic post shader for D3D12ReservoirHistoryUpdateCS; history-enabled live capture artifacts/runtime/mimir-leap-history-split-shader-visible.png completed without AQUARIUM_DISABLE_RESERVOIR_HISTORY, with 19,200 Leap surface splats, point-list rendering at 0, reservoir-update timing around 13 ms, and visible resolver output. Temporal/spatial reuse is intentionally not restored in the lean current-sample history shader yet."}
|
||||
{"ts": "2026-05-31T01:22:17+01:00", "type": "verification", "status": "ok", "note": "Eve dashboard broker cut: /eve/deck now routes provider-owned retained dashboard trees, /eve/deck/providers exposes switchboard/Mimir/StreamPixels manifests, and EveCanvas connects to /eve/deck with provider-card open-provider commands. Added typed CultMesh-compatible document shapes mimir.eve_dashboard_manifest.v1 and mimir.eve_dashboard_state.v1 while keeping live iPad transport WebSocket/TCP for LAN and SSH tunnel use. Smoke verified /health, provider catalog, WebSocket switchboard state, and open-provider transition to mimir.stream.layout."}
|
||||
|
|
|
|||
|
|
@ -301,13 +301,21 @@ accepted_design:
|
|||
Faust streaming DSP path. LUT presets are typed postprocess state; exposure
|
||||
and bloom map into current Fensalir `GraphicsSettings`, while actual LUT
|
||||
texture sampling remains the named renderer hook.
|
||||
`Mimir.EveDashboard` is the first native Eve operator dashboard server:
|
||||
`/eve/dashboard` sends scene/control snapshots and accepts select, move,
|
||||
scale, rotate, visibility, and reset commands. EveCanvas renders the scene
|
||||
graph and source panels natively in UIKit with multitouch transform editing.
|
||||
This is currently fixture dashboard state for the transport/control proof;
|
||||
live binding to `MimirPresentationControlState` and `MimirSceneEditorState`
|
||||
is the next authority cut.
|
||||
`Mimir.EveDashboard` is the native Eve dashboard broker: `/eve/deck` sends
|
||||
retained dashboard trees, `/eve/dashboard` remains the compatibility
|
||||
endpoint, and `/eve/deck/providers` exposes provider manifests. Built-in
|
||||
providers are the switchboard, `mimir.stream.layout`, and
|
||||
`yggdrasil.streampixels.edge`; launch-time external providers can be
|
||||
registered as `id|title|ws-url` descriptors for LAN or SSH-forwarded TCP
|
||||
surfaces. EveCanvas renders the scene graph and source panels natively in
|
||||
UIKit with multitouch transform editing, and provider-card taps send
|
||||
`open-provider`. Dashboard manifests and states now have typed
|
||||
CultMesh-compatible document shapes `mimir.eve_dashboard_manifest.v1` and
|
||||
`mimir.eve_dashboard_state.v1`, while the live iPad lane stays
|
||||
WebSocket/TCP because it is the proven tunnelable transport. The Mimir
|
||||
stream-layout provider still uses fixture dashboard state; live binding to
|
||||
`MimirPresentationControlState` and `MimirSceneEditorState` is the next
|
||||
authority cut.
|
||||
`MimirSceneEditorState` owns the separate Mimir-window editor graph:
|
||||
editor camera, scene nodes, selected node, visibility/locks, 2D-plane
|
||||
transforms, reset affordances, and grab/rotate/resize gizmo mode. Rolling
|
||||
|
|
|
|||
Loading…
Reference in a new issue